This document will attempt to describe some of the technical details of
Earwurm
.
If you are finding the API
difficult to understand - or that Earwurm
is behaving contrary to your expectations - reading this document should help clarify that confusion.
In order to better understand the terminology used when discussing “browser audio”, it is recommended that you familiarize yourself with the Web Audio API
.
The quickest way to describe the “structure” of an Earwurm
is:
- An
Earwurm
is a “manager” of sounds, responsible for:- Loading the audio asset.
- Preparing that asset to be played.
- Coordinating how that asset gets played.
- The
Manager
contains a library of “stacks”. - A
Stack
contains a queue of “sounds”. - A
Sound
is created and queued within theStack
.- The queue builds up upon
stack.prepare()
. - The queue decreases upon
sound.stop()
(orended
event). - The queue has a maximum size.
- A
Sound
is automatically destroyed as new sounds are added that would exceed that maximum size.
- A
- The queue builds up upon
- As sounds begin playing, their output travels through a “chain of gain nodes”, the final “destination” being your device’s speakers.
soundGain > stackGain > managerGain > device speakers
.
Earwurm
is made up of the following 3
components:
Whenever you instantiate a new Earwurm()
, you are creating a “Manager” that will act as your “audio hub” - a centralized tool to manage all of your audio.
The “Manager” will be responsible for knowing exactly what sounds you feed to it, and provide you a mechanism to interact with those sounds.
There are a few “master controls” available on the “Manager” which allow you to affect all sounds at once (volume
and mute
). For the most part however, you will want to control each sound individually.
In order to play a sound, you must first add it to the “Manager’s” library
as an LibraryEntry
. Each “entry” is then transformed into a Stack
, which surfaces an API that allows you to interact with the sound within.
const manager = new Earwurm();
const singleEntry: LibraryEntry = {
id: 'MySoundId',
path: 'path/to/my/sound.webm',
};
manager.add(singleEntry, ...additionalEntries);
Once an “entry” has been successfully added to the library
, you can retrieve and interact with it like so:
const soundStack: Stack | undefined = manager.get('MySoundId');
const sound = await soundStack?.prepare();
sound?.play();
While the “Manager” is our tool to manage all sounds in the library, the “Stack” is a bit like a manager for an individual sound.
The Web Audio API
considers sounds to be “single-use”. This means that - once played - the sound is “destroyed” in order to free up resources. When you want to play the same sound again, you have to “re-create it”.
Since “user interface audio” could require rapid execution of an identical sound (with the intention to “overlap” consecutive executions), Earwurm
creates a new instance of a Sound
upon every execution of .prepare()
.
Upon calling .prepare()
, the Stack
will instantiate a new Sound(args)
and add it to the queue
.
A rough pseudo-code visualization:
const soundStack = manager.get('MySound');
const sound = await soundStack?.prepare();
// We can imagine some internal code that looks like:
const updatedStack = [...stack.queue, new Sound(id: totalSoundsCreated + 1)];
// We then make 3 consecutive calls to `play` on the same `Sound`:
sound?.play();
sound?.play();
sound?.play();
// If we were to inspect that `Entry` at this exact moment,
// it might look something like this:
const mySoundsEntry = {
id: 'MySound',
path: 'path/to/my/sound.webm',
state: 'playing',
queue: [
{
id: 'MySound-1',
state: 'playing',
},
{
id: 'MySound-2',
state: 'playing',
},
{
id: 'MySound-3',
state: 'playing',
},
],
// …other properties on the `Stack` instance…
};
To better understand a few details of the Stack
, let’s say:
- The
sound.webm
asset has aduration
of1s
. - Each call to
.play()
was made200ms
apart. - While all sounds are
playing
, thestack.queue
has alength
of3
. MySound-1
would “expire” and leave thestack.queue
after it has finished playing.- Once
MySound-1
expires, thestack.queue
would have alength
of2
. MySound-2
now has200ms
remaining before it expires, whileMySound-3
has400ms
remaining.- If
MySound-2
had it’sstate
set topaused
, then it would remain in thestack.queue
even afterMySound-3
expires.
Calling some operations directly on the Stack
will affect every Sound
within:
- All sounds in the
Stack
will have theirstate
set topaused
if.pause()
is called. - Likewise, all sounds in the
Stack
will be removed if.stop()
is called.
How to gain access to an individual Sound
:
Whenever you call .prepare()
on a Stack
, it will re-create the necessary audio data, and return the generated Sound
.
const echo1 = soundStack.prepare();
echo1.volume = 0.8;
const echo2 = soundStack.prepare();
echo2.volume = 0.6;
const echo3 = soundStack.prepare();
echo3.volume = 0.4;
echo1.play();
setTimeout(() => echo2.play(), 100);
setTimeout(() => echo3.play(), 200);
Each call to .prepare()
will create the Sound
and add it to the queue
, setting each sound.state
to created
until .play()
is called on that individual instance.
There is a maximum of 8
sounds that can be added to the queue
at any given time. If a call to .prepare()
attempts to create a Sound
outside of that range, the oldest Sound
within the queue
will be “stopped and destroyed”. This could result in an abrupt stop to a currently playing
sound. The reason for this maximum limit is to mitigate performance degradation.
As the name implies, this is the actual audio that lives inside the Manager > Stack
.
mySound.play();
mySound.loop = true;
mySound.volume = 0.6;
mySound.mute = true;
mySound.pause();