同时将音频和麦克风流录制为不同的 MediaRecorder 轨道

Record audio and microphone stream as different MediaRecorder tracks at the same time

我正在尝试记录用户对某些音频内容的响应(通过麦克风),这样我就可以以精确的方式分析用户对此音频的响应的客户端时间。理想情况下,我会沿着同一时间线录制两条轨道:(1) 来自用户的麦克风流; (2) 用户听到的音频流。

我对网络音频没有经验 API,但是使用以前的一些 SO 答案我得出了以下解决方案:我连接音频源 (source) 和麦克风源 (stream) 在单个流 (combinedStream) 中,它被馈送到 MediaRecorder.

我的问题:

  1. 这会录制 单个 轨道(即音频和麦克风信号必须使用 post 处理分开)。是否可以将它们录制到两个轨道中?例如粗略地作为立体声信号的两个通道?

  2. 我不清楚这是否是对延迟最敏感的方法,可能存在与连接流相关的开销,或者与实际音频播放相关的未捕获延迟客户端?任何建议将不胜感激 - 目前在音频源和播放之间有大约 10-20 毫秒的延迟(通过查看音频流和通过扬声器播放之间的延迟粗略测量,如麦克风流上拾取的那样)。

  3. 我不太了解HTML5音频,但也许有更好的解决方案?

谢谢!

      ...

      // Audio for playback
      var source = context.createBufferSource();
      source.buffer = ...
      source.connect(context.destination);

      // Merge audio source with microphone stream
      const mediaStreamDestination = audioContext.createMediaStreamDestination();
      const sourceMic = jsPsych.pluginAPI.audioContext().createMediaStreamSource(stream);
      sourceMic.connect(mediaStreamDestination);
      source.connect(mediaStreamDestination);
      let combinedStream = new MediaStream([...mediaStreamDestination.stream.getAudioTracks()]);
      
      // Media recorder
      mediaRecorder = new MediaRecorder(combinedStream);
      mediaRecorder.ondataavailable = function(event) {
        chunks.push(event.data);
      };

      ...

看起来 MediaRecorder 不会这样做,至少 not in Chromium

MediaSourceExtensions does allow control over the playing of multiple tracks from inside Matroska files. webm is a subset of Matroska. The current version of the webm standard does not allow multiple tracks. But MSE可以玩

可以想象您可以使用两个 MediaRecorder,然后使用 ebml 代码编写代码来破解两个 Matroska 容器并将它们组合成一个具有多个轨道的输出 Matroska 数据流。这是一项非常重要的编程任务。

要在两个不同的声道​​上录制两个不同的音频源(例如立体声文件中的左右声道),您可以使用 ChannelMergerNode

基本上它与您的设置相同,不同之处在于当连接两个源时您将通过 connect( destination, input_channel, output_channel ) 方法设置输出通道:

使用两个振荡器:

onclick = ()=>{
  onclick = null;
  const ctx = new AudioContext();
  const osc1 = ctx.createOscillator();
  const osc2 = ctx.createOscillator();
  osc1.frequency.value = 300;
  osc1.start(0);
  osc2.start(0);

  const merger = ctx.createChannelMerger();
  const dest = ctx.createMediaStreamDestination();
  merger.connect( dest );

  osc1.connect( merger, 0, 0 );
  osc2.connect( merger, 0, 1 );

  // for nodes to output sound in Chrome
  // they need to be connected to the destination
  // ...
  const mute = ctx.createGain();
  mute.gain.value = 0;
  mute.connect( ctx.destination );
  osc1.connect( mute );
  osc2.connect( mute );

  const chunks = [];
  const rec = new MediaRecorder( dest.stream );
  rec.ondataavailable = e => chunks.push(e.data)
  rec.onstop = e => {
    output.src = URL.createObjectURL( new Blob( chunks ) );
  };
  rec.start();
  setTimeout( () => rec.stop(), 5000 );
  log.remove();
};
<p id="log">click to start recording of 5s sample</p>
<audio id="output" controls></audio>

And as a fiddle using gUM