Web Audio Madness: (era era era) Setting a negative playback rate on an AudioBufferSourceNode
Author: Andrew Gallagher
Published at: Thurs Jan 23 2025
This work is now part of the open-source library simple-reversible-audio-buffer-source-node.
In the Requirements and Use Cases of the Web Audio API spec, outlined over a decade ago in 2013, the Audio Working Group described the possibility of creating a connected DJ booth application. The following DJ workflow is detailed:
…the DJ would be able to quickly select several other track [sic], play them through headphones without affecting the main audio output of the application, and match them to the track currently playing through a mix of pausing, skipping forward or back and pitch/speed change.
This is (mostly1) possible now! Some parts of this application are relatively straightforward:
-
manipulating
detune
andplaybackRate
are sufficient for jogging a track when beatmatching -
targeting different audio destinations (e.g. —
headphones/line out) is possible via
setting the sinkId of an
AudioContext
- there are several ways to pause a piece of audio2
But one crucial feature of a DJ application that is presently nontrivial to implement is the ability to audibly rewind an audio track. Audibly rewinding a track has two critically important functions for a DJ:
- while cueing, the DJ can precisely hear where a cuepoint will start without needing to play a track forwards or consult a visual representation of the audio. Having an accurate and fast way to define cuepoints is essential for beatmatching as defined in the spec's use case.
- the DJ can perform a spinback of the track. In some genres of music, rewinding a track in this way is an essential transition technique.
The naïve approach of setting a negative
playbackRate
is
only supported in some WebKit-based browsers, so Chrome users — 65% of internet traffic — are
out of luck.
Fortunately, it is possible to implement audible rewind. Like most programming problems, this can be broken up into a set of smaller, simpler problems.
High-level overview
This work abstracts out a bit more down the line to be an
AudioNode
that generally supports a negative
playbackRate
, but for right now we'll specifically
work with our little DJ example.
What we're essentially looking to create is an
AudioNode
that can generate the
"era era era"
sound that is characteristic of DJing. We'll need to have a
reversed version of our audio, and we'll want the ability to
quickly swap between the forwards and reversed audio. When we
swap between forwards and reversed audio, we'll want to be
very precise about the start point of the audio that is
being swapped in, so that we don't have any audio jank.
This sets us up for four distinct tasks:
- Reversing our forwards audio
- Accurately tracking playback position of audio
- Swapping between forwards and reversed audio
- Putting it all together
1. Reversing our forwards audio
Our easiest task is reversing our audio — of which there are a few different approaches:
- outside of the browser with
ffmpeg
-
inside our browser or web worker by reversing all channel
data on a decoded
AudioBuffer
3
At a very high level, all digital audio just an array of floating point numbers. We just need to reverse this array.
2. Accurately tracking playback position of audio
There is not a great, built-in way to track the playback
position of an AudioBufferSourceNode
. For our DJ
application, the typical minimal approach of using
audioContext.currentTime
to bookend when audio
starts and stops would be too imprecise to prevent audio jank
and may cause misalignment between our forwards and backwards
audio tracks. Besides, detuning and adjusting
playbackRate
for beatmatching would mean we would
have to perform precise math when calculating the amount of time
played between these already imprecise bookends.
Most of the more clever workarounds to this are problem, of
course, in
a random GitHub thread. My favorite approach detailed
here
is to create an additional channel on the
AudioBuffer
, fill it with the percentage that a
channel completed with 0 at the first index and 1 at the last
index, and then read from it via an AnalyserNode
.
To elaborate briefly on this, an AudioBuffer
is
composed of channels of Float32Array
representing
the amplitudes of an audio signal. There are no rules to what
these Float32Array
s can actually encode, assuming
that the values we put in the array are finite 32-bit
floating-point numbers. When we play digital audio, your
computer traverses through this array of numbers and translates
that into sound. If we have a silent, additional channel that
represents playback percentage, we can figure out what this
current playback percentage is at a given time by reading from
this channel during playback.
3. Swapping between forwards and reversed audio
Swapping between forwards and backwards audio turns us back into
the more straightforward world of good old state management.
When our playbackRate
is positive, we want our
forward node to be playing. When we set our
playbackRate
to negative, we want our reverse node
to be playing. Finally, when we swap between directions, we want
to make sure that our nodes times are aligned. The playback
position of the reverse node is the complement of the forward
node, and vice versa — as an example: given a 10-second
long audio clip, if we are exactly 4 seconds into forwards
playback and we move into reverse,
we'll want to play the reverse audio from 6 seconds in.
To handle this swap, we'll override the
playbackRate
method of an AudioNode
.
When the amount is negative but our forwards node is currently
playing (or vice versa), we swap in the active node with the
complementary playback position of the inactive node and pause
the inactive node.5
Both nodes could be routed through a
ChannelMergerNode
, so our exposed interface may
just be a single AudioNode
.
4. Putting it all together
After wiring everything together, we're left with something like this demo.6
Putting the
playbackRate
in the negative will play the
drum track backwards.
By mapping playbackRate
to an interface
representing a jog wheel or turntable, a user could rapidly
toggle through a track and create that sweet "era era era"
sound.
- iOS still does not support having multiple simultaneous audio outputs, so advice for mobile browser-based DJ applications is to use a splitter, which, to me, is ASS.
-
Pro tip when you're pausing an
AudioBufferSourceNode
s is to set theplaybackRate
to 0. - For an example illustrating how to reverse channel data, see this implementation.
- For an example of routing a playback percentage channel through an Analyser, see this implementation.
- For an example of swapping between forwards and reverse nodes, see this implementation.
- For demo source code, refer to this repo.