Reputation: 11
I'm writing a Flask application and am using Javascript MediaRecorder to record the user's audio. I managed to get the Blob populated and sent back to my Flask endpoint, but I can't seem to convert it into a working audio file.
Javascript
const blob = new Blob(chunks, { type: "audio/wav" });
chunks = [];
const audioURL = window.URL.createObjectURL(blob);
audio.src = audioURL;
console.log("recorder stopped");
const formData = new FormData();
formData.append("audioData", blob);
console.log(blob);
fetch("/upload-audio", {
method: "POST",
body: formData,
})
.then((response) => {
// Handle the response from the server
})
.catch((error) => {
// Handle any errors that occurred
});
Python - Flask
@app.route('/upload-audio', methods=['POST'])
def upload_audio():
audio_data = request.files['audioData'].read()
print("audio data", audio_data)
audio_data_save_attempt = request.files['audioData']
audio_data_save_attempt.save("filenametest.wav")
return "Trying to get audio data saved to disk"
Python returns the following:
print("audio data", audio_data) returns: audio data b'\x1aE\xdf\xa3\x9fB\x86\x81\x01B\xf7\x81\x01B\xf2\x81\x04B\xf3\x81\x08B\x82\x84webmB\x87\x81\x04B\x85\x81\x02\x18S\x80g\x01\xff\xff\xff\xff\xff\xff\xff\x15I\xa9f\x99*\xd7\xb1\x83\x0fB@M\x80\x86ChromeWA\x86Chrome\x1` etc..
audio_data_save_attempt creates a file but the file is not openable.
My gut feeling is that audio data does have the frames information but I can't get it to convert properly. I tried using io.BytesIO and the python wave module and neither seem to have worked for me, although I am very new to working with audio and could have made a mistake.
# Create a wave file from the audio data
with io.BytesIO(audio_data) as audio_stream:
with wave.open(audio_stream, mode="rb") as audio_file:
# Create a new wave file in write mode
with wave.open("audio.wav", mode="wb") as output_file:
output_file.setparams(audio_file.getparams())
output_file.writeframes(audio_file.readframes(audio_file.getnframes()))
Upvotes: 1
Views: 1238
Reputation: 8592
It could have several causes why the audio file cannot be played.
On the one hand, the pointer, when you read a file completely, points to the end of the file. Before you can read the data a second time to save it, you have to put the pointer back to the beginning of the file using file.seek(0)
.
On the other hand, it seems to me that your data is not in the wave format. Depending on the platform and browser, the audio data are exported in different formats. The Chrome browser tends to the 'WebA' format.
Below is an example of recording audio with the browser. The data is sent to the server and then saved. The user receives an overview of the files recorded and can play them.
In this example, several, but possibly not all file formats are supported.
from flask import (
Flask,
Response,
abort,
current_app,
render_template,
redirect,
request,
stream_with_context,
url_for
)
from collections import namedtuple
from glob import glob
from mimetypes import add_type, guess_extension, guess_type
from werkzeug.utils import secure_filename
import os
add_type('audio/aac', '.m4a', strict=True)
Record = namedtuple('Record', ('filename', 'created_at'))
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
try:
os.makedirs(os.path.join(
app.instance_path,
app.config.get('UPLOAD_FOLDER', 'uploads')
), exist_ok=True)
except:
pass
@app.route('/')
def audio_index():
patterns = [
'*.m4a', # MacOS & Safari
'*.wav', # Windows & ?
'*.weba' # MacOS & Chrome
]
path = os.path.join(
current_app.instance_path,
current_app.config.get('UPLOAD_FOLDER', 'uploads')
)
records = [
Record(fn[len(path)+1:], os.path.getctime(fn)) \
for ptrn in patterns for fn in glob(os.path.join(path, ptrn))
]
return render_template('index.html', records=records)
@app.route('/audio-upload', methods=['POST'])
def audio_upload():
if 'audio_file' in request.files:
file = request.files['audio_file']
extname = guess_extension(file.mimetype)
if not extname:
abort(400)
# Check for allowed file extensions.
i = 1
while True:
dst = os.path.join(
current_app.instance_path,
current_app.config.get('UPLOAD_FOLDER', 'uploads'),
secure_filename(f'audio_record_{i}{extname}'))
if not os.path.exists(dst): break
i += 1
file.save(dst)
return redirect(url_for('audio_index'))
@app.route('/audio/stream/<path:fn>', methods=['GET'])
def audio_download(fn):
@stream_with_context
def generator(src):
CHUNK_SIZE = 1024
with open(src, 'rb') as fp:
while True:
data = fp.read(CHUNK_SIZE)
if not data: break
yield data
src = os.path.join(
current_app.instance_path,
current_app.config.get('UPLOAD_FOLDER', 'uploads'),
fn
)
if not os.path.exists(src):
return abort(404)
mime,_ = guess_type(src)
return Response(generator(src), mimetype=mime)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Audio Index</title>
</head>
<body>
<div class="rec-container">
<div class="rec-column rec-column-1">
<button class="rec-btn" id="toggle-rec-btn">Record</button>
</div>
<div class="rec-column rec-column-2">
{% for record in records|sort(attribute='created_at', reverse=True) -%}
<div class="rec-item">
<div class="content">
<span class="rec-title">{{ record.filename }}</span>
<audio
controls
src="{{ url_for('audio_download', fn=record.filename) }}"
></audio>
</div>
</div>
{% endfor -%}
</div>
</div>
<script type="text/javascript">
(function() {
const uploadURL = "{{ url_for('audio_upload') }}";
const startButton = document.getElementById("toggle-rec-btn");
startButton.addEventListener("click", function() {
if (!navigator.mediaDevices) {
console.error("getUserMedia not supported.")
return;
}
const constraints = { audio: true };
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
let chunks = []
let recorder = new MediaRecorder(stream);
recorder.ondataavailable = event => {
chunks.push(event.data);
};
recorder.onstop = event => {
console.log("Recording stopped.")
let blob = new Blob(chunks, { type: recorder.mimeType });
chunks = [];
startButton.disabled = false;
let formData = new FormData();
formData.append('audio_file', blob);
fetch(uploadURL, {
method: "POST",
cache: "no-cache",
body: formData
}).then(resp => {
if (resp.status === 200) {
window.location.reload(true);
} else {
console.error("Error:", resp)
}
}).catch(err => {
console.error(err);
});
};
recorder.onstart = event => {
console.log("Recording started.");
startButton.disabled = true;
setTimeout(function() { recorder.stop(); }, 10000);
};
recorder.start();
})
.catch(function(err) {
console.error(err);
});
});
})();
</script>
</body>
</html>
Upvotes: 0