Crackling and popping playing back audio recorded through web audio API

Viewed 443

The overall project is complex, so here is a simple description of the pipeline:

  • One or more sound sources are inputs to an AudioContext graph. The inputs come from MediaStream objects, so I use context.createMediaStreamSource() to create the source nodes.
  • The output is also a MediaStream, so I use context.createMediaStreamDestination() for the destination node.
  • The output audio tracks are copied over to a MediaStream that contains video tracks. This stream is used to record.
  • The recording is done as webm and the content is saved in chunks.
  • The chunks are appended to a SourceBuffer object connected to a MediaSource objects that played into a video tag.

The problem I'm having is that, in Chrome only, playback via the media source has crackling and popping. Furthermore, the audio track can end a few seconds early for longer recordings (>1-2 minutes). This does not happen if I do not pipe audio through the Web Audio API graph. That is, if I feed the MediaRecorder a regular stream with native video and audio tracks, the playback is perfect.

It should also be noted that even with the Web Audio API input, there is no crackling or popping in playback if the webm content is put into a blob and then into a URL and played from that. Nor is there any crackling or popping if the webm content is extracted from the browser and played in a video player.

Some code.

Audio context is created thus:

        let audioContext: AudioContext | null = null;
        if (window.AudioContext) {
            audioContext = new AudioContext({ sampleRate: 48000, latencyHint: 'interactive' });
        } else if ((window as any).webkitAudioContext) {
            audioContext = new (window as any).webkitAudioContext() as AudioContext;
        }

The code that builds the graph is somewhat complex due to some special logic we have to do. But it essentially boils down to this:

// output stream
const stream = new MediaStream();

// videoInput contains a video source via getUserMedia()
for (const track of videoInput.videoTracks) {
    stream.addTrack(track);
}

// send all audio inputs to this node
const collectionNode = audioContext.createMediaStreamDestination();

// here we set up just one source, but the real code may create more than one of these
const source = audioContext.createMediaStreamSource(input.stream);
source.connect(collectionNode);

// extract tracks and put them in the output stream
for (const track of collectionNode.stream.getAudioTracks()) {
    stream.addTrack(track);
}

At this point, stream is the MediaStream object that is fed to the recorder.

The MSE code is likewise complex to deal with error cases and buffering. As part of initialization, I do set the duration property, and the codec is exactly the same codec as is used in recording. There is an early check in the system to make sure that the recorder uses a codec that is also compatible with the media source, using the isTypeSupported() method on both.

I have found a few Chromium bugs about this, but all from several years ago. The fact that playback is fine when Web Audio API is not used for the recording implies to me that the issue is not necessarily with MSE.

I'm a bit stuck on this. I'm happy to provide additional information. The full code is far too big to put in here, but I can provide other snippets as need be.

1 Answers

I have one quick suggestion worth trying out. I have noticed if I set the sample rate in either the audiocontext. i.e when you do:

audioContext = new AudioContext({ sampleRate: 48000, latencyHint: 'interactive' });

Or if you set audioBitsPerSecond in the media recorder it can lead to bad sounding recordings. Try leaving these unset and let me know if you get the same issue!

Related