如何解决AudioContext、getUserMedia 和 websockets 音频流
我正在尝试制作一个尽可能简单的 Javascript 前端,它允许我在使用 getUserMedia
在网络浏览器中单击鼠标时接收来自用户麦克风的音频,将其修改为自定义示例rate 和 monochannel,然后通过 websocket 将其流式传输到我的服务器,在那里它将被中继到 Watson Speech API。
我已经使用 autobahn 构建了 websocket 服务器。我一直在尝试在 whisper 和 ws-audio-api 上绘制更新的客户端库,但这两个库似乎都过时了,并且包含了很多我不需要的功能,我正试图过滤掉这些功能。我正在使用 XAudioJS 重新采样音频。
我目前的进展是在这个 Codepen 中。我被困住了,无法找到更清晰的例子。
- whisper 和 ws-audio-api 在页面加载时初始化 AudioContext,至少在 Chrome 和 iOS 中导致 error 作为音频上下文现在必须初始化为对用户交互的响应。我试图将 AudioContext 移动到
onClick
事件中,但这导致我必须单击两次才能开始流式传输。我目前在audio_context.resume()
事件中使用onClick
但这似乎是一个迂回的解决方案,导致页面显示它始终在记录,即使它没有记录,这可能会让我的用户感到不安。 如何正确启动点击记录并在点击时终止记录? - 我已将弃用的
Navigator.getUserMedia()
更新为MediaDevices.getUserMedia()
,但不确定是否需要更改第 83-86 行上的旧供应商前缀以匹配新功能? - 最重要的是,一旦我从
getUserMedia
获得一个流,我该如何正确地对其重新采样并将其转发到打开的 websocket?我对从节点到节点弹跳音频的结构感到有些困惑,我需要第 93-108 行的帮助。
解决方法
我找到了帮助 here,并且能够基于 vin-ni 的 Google-Cloud-Speech-Node-Socket-Playground 中的代码构建一个更现代的 JavaScript 前端,我对其进行了一些调整。 2021 年的许多现有音频流演示要么已经过时,要么具有大量“额外”功能,这增加了开始使用 websockets 和音频流的障碍。我创建了这个“裸机”脚本,它将音频流减少到只有四个关键功能:
- 打开网络套接字
- 开始直播
- 重新采样音频
- 停止直播
希望这个 KISS(Keep It Simple,Stupid)演示可以帮助其他人比我花的时间更快地开始流式传输音频。
这是我的 JavaScript 前端
//================= CONFIG =================
// Global Variables
let websocket_uri = 'ws://127.0.0.1:9001';
let bufferSize = 4096,AudioContext,context,processor,input,globalStream,websocket;
// Initialize WebSocket
initWebSocket();
//================= RECORDING =================
function startRecording() {
streamStreaming = true;
AudioContext = window.AudioContext || window.webkitAudioContext;
context = new AudioContext({
// if Non-interactive,use 'playback' or 'balanced' // https://developer.mozilla.org/en-US/docs/Web/API/AudioContextLatencyCategory
latencyHint: 'interactive',});
processor = context.createScriptProcessor(bufferSize,1,1);
processor.connect(context.destination);
context.resume();
var handleSuccess = function (stream) {
globalStream = stream;
input = context.createMediaStreamSource(stream);
input.connect(processor);
processor.onaudioprocess = function (e) {
var left = e.inputBuffer.getChannelData(0);
var left16 = downsampleBuffer(left,44100,16000);
websocket.send(left16);
};
};
navigator.mediaDevices.getUserMedia({audio: true,video: false}).then(handleSuccess);
} // closes function startRecording()
function stopRecording() {
streamStreaming = false;
let track = globalStream.getTracks()[0];
track.stop();
input.disconnect(processor);
processor.disconnect(context.destination);
context.close().then(function () {
input = null;
processor = null;
context = null;
AudioContext = null;
});
} // closes function stopRecording()
function initWebSocket() {
// Create WebSocket
websocket = new WebSocket(websocket_uri);
//console.log("Websocket created...");
// WebSocket Definitions: executed when triggered webSocketStatus
websocket.onopen = function() {
console.log("connected to server");
//websocket.send("CONNECTED TO YOU");
document.getElementById("webSocketStatus").innerHTML = 'Connected';
}
websocket.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
document.getElementById("webSocketStatus").innerHTML = 'Not Connected';
}
websocket.onmessage = function(e) {
//console.log("message received: " + e.data);
console.log(e.data);
try {
result = JSON.parse(e.data);
} catch (e) {
$('.message').html('Error retrieving data: ' + e);
}
if (typeof(result) !== 'undefined' && typeof(result.error) !== 'undefined') {
$('.message').html('Error: ' + result.error);
}
else {
$('.message').html('Welcome!');
}
}
} // closes function initWebSocket()
function downsampleBuffer (buffer,sampleRate,outSampleRate) {
if (outSampleRate == sampleRate) {
return buffer;
}
if (outSampleRate > sampleRate) {
throw 'downsampling rate show be smaller than original sample rate';
}
var sampleRateRatio = sampleRate / outSampleRate;
var newLength = Math.round(buffer.length / sampleRateRatio);
var result = new Int16Array(newLength);
var offsetResult = 0;
var offsetBuffer = 0;
while (offsetResult < result.length) {
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
var accum = 0,count = 0;
for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = Math.min(1,accum / count) * 0x7fff;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result.buffer;
} // closes function downsampleBuffer()
还有我的 index.html
文件
<!DOCTYPE html>
<html>
<head>
<script src='jquery-1.8.3.js'></script>
<script src='client.js'></script>
</head>
<body>
<div class='message'>Welcome!</div>
<button onclick='startRecording()'>Start recording</button>
<button onclick='stopRecording()'>Stop recording</button>
<br/>
<div>WebSocket: <span id="webSocketStatus">Not Connected</span></div>
</body>
</html>
您可以使用大多数可以在 Crossbario's GitHub 上找到的 Autobahn python 回显服务器进行测试。 startRecording()
和 stopRecording()
函数也可以从 Storyline 或 H5P 中的变量调用,如果有人想将其用于 ed tech 中的语音识别(像我一样)。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。