微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

AudioContext、getUserMedia 和 websockets 音频流

如何解决AudioContext、getUserMedia 和 websockets 音频流

我正在尝试制作一个尽可能简单的 Javascript 前端,它允许我在使用 getUserMedia 在网络浏览器中单击鼠标时接收来自用户麦克风的音频,将其修改自定义示例rate 和 monochannel,然后通过 websocket 将其流式传输到我的服务器,在那里它将被中继到 Watson Speech API。

我已经使用 autobahn 构建了 websocket 服务器。我一直在尝试在 whisperws-audio-api 上绘制更新的客户端库,但这两个库似乎都过时了,并且包含了很多我不需要的功能,我正试图过滤掉这些功能。我正在使用 XAudioJS 重新采样音频。

我目前的进展是在这Codepen 中。我被困住了,无法找到更清晰的例子。

  1. whisper 和 ws-audio-api 在页面加载时初始化 AudioContext,至少在 Chrome 和 iOS 中导致 error 作为音频上下文现在必须初始化为对用户交互的响应。我试图将 AudioContext 移动到 onClick 事件中,但这导致我必须单击两次才能开始流式传输。我目前在 audio_context.resume() 事件中使用 onClick 但这似乎是一个迂回的解决方案,导致页面显示它始终在记录,即使它没有记录,这可能会让我的用户感到不安。 如何正确启动点击记录并在点击时终止记录?
  2. 我已将弃用的 Navigator.getUserMedia() 更新为 MediaDevices.getUserMedia(),但不确定是否需要更改第 83-86 行上的旧供应商前缀以匹配新功能
  3. 最重要的是,一旦我从 getUserMedia 获得一个流,我该如何正确地对其重新采样并将其转发到打开的 websocket?我对从节点到节点弹跳音频的结构感到有些困惑,我需要第 93-108 行的帮助。

解决方法

我找到了帮助 here,并且能够基于 vin-ni 的 Google-Cloud-Speech-Node-Socket-Playground 中的代码构建一个更现代的 JavaScript 前端,我对其进行了一些调整。 2021 年的许多现有音频流演示要么已经过时,要么具有大量“额外”功能,这增加了开始使用 websockets 和音频流的障碍。我创建了这个“裸机”脚本,它将音频流减少到只有四个关键功能:

  1. 打开网络套接字
  2. 开始直播
  3. 重新采样音频
  4. 停止直播

希望这个 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 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?