【发布时间】:2019-11-10 02:37:19
【问题描述】:
我正在构建一个网页,该网页记录来自用户设备的音频并将其发送到 Microsoft 的认知语音服务以进行语音到文本的转换。到目前为止,我已经能够创建和播放用 JavaScript 制作的 .ogg 文件,但我需要获取 .wav 格式的文件。
无法依赖 Blob 类型 audio/wav,因为并非所有浏览器都支持它(至少我的浏览器不支持)。 Blob 被发送到 Django 服务器并由其存储。当我尝试使用 PySoundFile 打开这些文件时,我收到一条错误消息 File contains data in an unknown format。正在使用 new Blob(chunks, { type: 'audio/ogg; codecs=opus' }) 创建 blob,并使用 django.db.FileField 保存。 Blob 块来自MediaRecorder.ondataavailable。
更新: 我放弃了使用MediaRecorder,而是选择了ScriptProcessorNode。同样,Firefox 可以工作,但 Chrome 不能。似乎 Chrome 在音频的末尾得到了一小部分,并在音频的长度上重复了这一点。这是我使用的代码,它基于 Matt Diamond 在github.com/mattdiamond/Recorderjs 的工作。可以在webaudiodemos.appspot.com/AudioRecorder/index.html 看到使用他的作品的演示,它适用于我的 Firefox 和 Chrome。另外,我的原始代码在一个类中,但我不想包含整个类。如果我在翻译中出现任何语法错误,我深表歉意。
let recBuffers = [[], []];
let recLength = 0;
let numChannels = 2;
let listening = false;
let timeout = null;
let constraints = {
audio: true
};
let failedToGetUserMedia = false;
if (navigator.getUserMedia) {
navigator.getUserMedia(constraints, (stream) => {
init(stream);
}, (err) => {
alert('Unable to access audio.\n\n' + err);
console.log('The following error occurred: ' + err);
failedToGetUserMedia = true;
});
}
else if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
init(stream);
}).catch((err) => {
alert('Unable to access audio.\n\n' + err);
console.log('The following error occurred: ' + err);
failedToGetUserMedia = true;
});
}
else failedToGetUserMedia = true;
function beginRecording() {
recBuffers = [[], []];
recLength = 0;
listening = true;
timeout = setTimeout(() => {
endRecording();
}, maxTime);
}
function endRecording() {
clearTimeout(timeout);
timeout = null;
exportWAV();
}
function init(stream) {
let audioContext = new AudioContext();
let source = audioContext.createMediaStreamSource(stream);
let context = source.context;
let node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, 4096, numChannels, numChannels);
node.onaudioprocess = (e) => {
if (!listening) return;
for (var i = 0; i < numChannels; i++) {
recBuffers[i].push(e.inputBuffer.getChannelData(i));
}
recLength += recBuffers[0][0].length;
}
source.connect(node);
node.connect(context.destination);
}
function mergeBuffers(buffers, len) {
let result = new Float32Array(len);
let offset = 0;
for (var i = 0; i < buffers.length; i++) {
result.set(buffers[i], offset);
offset += buffers[i].length;
}
return result;
}
function interleave(inputL, inputR) {
let len = inputL.length + inputR.length;
let result = new Float32Array(len);
let index = 0;
let inputIndex = 0;
while (index < len) {
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
inputIndex++;
}
return result;
}
function exportWAV() {
let buffers = [];
for (var i = 0; i < numChannels; i++) {
buffers.push(mergeBuffers(recBuffers[i], recLength));
}
let interleaved = numChannels == 2 ? interleave(buffers[0], buffers[1]) : buffers[0];
let dataView = encodeWAV(interleaved);
let blob = new Blob([ dataView ], { type: 'audio/wav' });
blob.name = Math.floor((new Date()).getTime() / 1000) + '.wav';
listening = false;
return blob;
}
function floatTo16BitPCM(output, offset, input){
for (var i = 0; i < input.length; i++, offset+=2){
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
function writeString(view, offset, string){
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function encodeWAV(samples){
var buffer = new ArrayBuffer(44 + samples.length * 2);
var view = new DataView(buffer);
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 36 + samples.length * 2, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, numChannels, true);
/* sample rate */
view.setUint32(24, context.sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, context.sampleRate * 4, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, numChannels * 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, samples.length * 2, true);
floatTo16BitPCM(view, 44, samples);
return view;
}
if (!failedToGetUserMedia) beginRecording();
更新: 我已经确认,当 Chrome 缓冲区的值作为 Firefox 上交错的输入提供时,输出与 Chrome 的输出相同。这表明 Chrome 没有用正确的值填充 recBuffers。事实上,当我查看 Chrome 上的 recBuffers 时,每个通道都充满了交替列表。例如:
recBuffers = [[
[2, 3],
[7, 1],
[2, 3],
[7, 1],
[2, 3],
[7, 1],
[2, 3],
[7, 1],
[2, 3],
[7, 1]
], [
[5, 4],
[6, 8],
[5, 4],
[6, 8],
[5, 4],
[6, 8],
[5, 4],
[6, 8],
[5, 4],
[6, 8]
]]
当然,实际值不同。这只是一个例子来说明这一点。
【问题讨论】:
-
先用ScriptProcessorNode记录原始PCM,而不是稍后转换。
-
我想首先以 .wav 格式录制它,但我的浏览器不支持音频/wav blob 类型,并且根据developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode,不推荐使用 ScriptProcessorNode。谢谢你的建议。如果您还有其他人,请告诉我。
-
您的浏览器并不关心 blob 是什么类型...您实际上指的是您的浏览器不支持带有 MediaRecorder 的
audio/wav。而且,虽然 ScriptProcessorNode 已被弃用……但几乎没有。在更好地支持 AudioWorkletNode 之前,他们不会将其删除,届时您可以使用它来代替。 (或者,更有可能的是,audio/wav将首先在您的浏览器中得到支持。)要真正破坏 ScriptProcessorNode 还需要很多年。 -
您能否提供有关如何使用 ScriptProcessorNode 以 wav 格式录制音频的任何见解?将 Blob 类型设置为 'audio/wav; codecs=ms_pcm' 不起作用,我无法让 Mozilla 的 ScriptProcessorNode 上的示例正常工作,我只能让 Chrome 播放音频文件。 PySoundFile 无法打开它们,Windows Media Player 也无法打开。
-
处理
audioprocess事件。e.inputBuffer.getChannelData(buffer)将为您提供原始的 32 位 PCM 数据。从那里,您可以将其按原样写入您自己的 WAV 文件(不要忘记标题:soundfile.sapp.org/doc/WaveFormat),或将其从 32 位浮点数转换为 16 位小端格式。另外,不要忘记检查音频上下文的采样率并将其包含在您的 WAV 文件中。
标签: javascript google-chrome audio wav