skip to content

JavaScript: Creating Sounds with AudioContext

 Tweet0 Tweets

Did you know that it's now possible to generate sounds from scratch using only JavaScript. Previously this required Flash/Shockwave or loading an audio file.

Playing audio files became much easier in HTML5, but can now be done away with entirely if you only want to play simple harmonics.

Creating an oscillating soundwave

The AudioContext interface is extremely powerful letting you chain together various effects. For our purposes we are using only the OscillatorNode (represents a periodic waveform) and gainNode (a change in volume) to create our sounds.

Here we've set up two control forms, each controlling a separate OscialltorNode (frequency, wave type) and GainNode (volume) to create a constant tone which can then be manipulated. The code for each form is otherwise identical.

Sound Player 1


Sound Player 2


If you're feeling masochistic, try playing some sounds with adjacent frequencies.

AudioContext Mechanics

For the effects demonstrated on this page we're using the following interfaces:

SoundPlayer Node configuration diagram

Rather than operating on them directly, we've created a SoundPlayer class which does most of the work, including ramping the volume up and down to avoid that annoying clicking sound as the wave is truncated at the start and finish.

Complications arise when trying to play sounds in series, because commands are processed relative to the current time (baseAudioContext.currentTime) which represents a hardware timestamp starting from zero once the system is activated.

SoundPlayer.js class

Our SoundPlayer class enables all the example on this page, plus the sound effects in our new JavaScript Graphing Game.

The constructor accepts an AudioContext object, after which a single sound/note can be started and have it's properties controlled. A single AudioContext is sufficient for all sounds on the page.

// Original JavaScript code by Chirp Internet: www.chirp.com.au // Please acknowledge use of this code by including this header. function SoundPlayer(audioContext, filterNode) { this.audioCtx = audioContext; this.gainNode = this.audioCtx.createGain(); if(filterNode) { // run output through extra filter (already connected to audioContext) this.gainNode.connect(filterNode); } else { this.gainNode.connect(this.audioCtx.destination); } this.oscillator = null; } SoundPlayer.prototype.setFrequency = function(val, when) { if(when) { this.oscillator.frequency.setValueAtTime(val, this.audioCtx.currentTime + when); } else { this.oscillator.frequency.setValueAtTime(val, this.audioCtx.currentTime); } return this; }; SoundPlayer.prototype.setVolume = function(val, when) { if(when) { this.gainNode.gain.exponentialRampToValueAtTime(val, this.audioCtx.currentTime + when); } else { this.gainNode.gain.setValueAtTime(val, this.audioCtx.currentTime); } return this; }; SoundPlayer.prototype.setWaveType = function(waveType) { this.oscillator.type = waveType; return this; }; SoundPlayer.prototype.play = function(freq, vol, wave, when) { this.oscillator = this.audioCtx.createOscillator(); this.oscillator.connect(this.gainNode); this.setFrequency(freq); if(wave) { this.setWaveType(wave); } this.setVolume(1/1000); if(when) { this.setVolume(1/1000, when - 0.02); this.oscillator.start(when - 0.02); this.setVolume(vol, when); } else { this.oscillator.start(); this.setVolume(vol, 0.02); } return this; }; SoundPlayer.prototype.stop = function(when) { if(when) { this.gainNode.gain.setTargetAtTime(1/1000, this.audioCtx.currentTime + when - 0.05, 0.02); this.oscillator.stop(this.audioCtx.currentTime + when); } else { this.gainNode.gain.setTargetAtTime(1/1000, this.audioCtx.currentTime, 0.02); this.oscillator.stop(this.audioCtx.currentTime + 0.05); } return this; };

expand code box

Public methods

The SoundPlayer class has the following public methods:

__construct(AudioContext)
creates a GainNode and connects it to the destination.
play(frequency, gain, waveType[, when])
creates an OscillatorNode to play the specified sound and starts playing, now or in the future.
stop([when])
stops playing, now or in the future.
setFrequency(frequency[, when])
changes the frequency in Hz, now or in the future.
setVolume(gain[, when])
changes the volume multiplier, now or in the future.
setWaveType(type)
changes the wave type (sine, square, sawtooth or triangle).

Notably, these functions can all be chained. For example, to play a particular sound for a half second:

<script src="/soundplayer.js"></script> <script> const AudioContext = window.AudioContext || window.webkitAudioContext; const audio = new AudioContext(); (new SoundPlayer(audio)).play(440.0, 0.8, "sine").stop(0.5); </script>

And to play a sound with a frequency transition:

(new SoundPlayer(audio)).play(440, 0.5, "square").setFrequency(880, 0.1).stop(0.2);

This approach is used in the piano keyboard demonstration below and, in most browsers, the commands can even be pasted directly into the Browser Console.

Piano Keyboard

Using our new SoundPlayer class we can easily attach it using event listeners to elements on the page to play different notes. For example, here we've set up a piano keyboard where each key plays the respective note:

(initial click needed)

Using HTML to mark up the keys as follows:

<div id="piano"> <div data-frequency="261.626" data-note="C"> <div data-frequency="277.18" data-note="C#"></div> </div> <div data-frequency="293.665" data-note="D"> <div data-frequency="311.127" data-note="D#"></div> </div> <div data-frequency="329.628" data-note="E"></div> <div data-frequency="349.228" data-note="F"> <div data-frequency="369.994" data-note="F#"></div> </div> ... </div>

The data attributes are used to determine which note (frequency in Hz) to play when the key is clicked, and to display the key labels.

The source code is simplicity itself:

<script src="/soundplayer.js"></script> <script> const AudioContext = window.AudioContext || window.webkitAudioContext; const audio = new AudioContext(); const playNote = (e) => { // play note according to data-frequency attribute (new SoundPlayer(audio)).play(e.target.dataset.frequency, 0.8, "sine").stop(0.5); e.cancelBubble = true; }; for(let el of document.getElementById("piano").getElementsByTagName("DIV")) { el.addEventListener("click", playNote, false); } </script>

For those not familar with the new arrow functions syntax, (e) => in the above code snippets is equivalent to function(e).

Depending on your system capabilities there may be some distortion when playing overlapping notes. To reduce that we can add a DynamicsCompressorNode to the mix:

<script src="/soundplayer.js"></script> <script> const AudioContext = window.AudioContext || window.webkitAudioContext; const audio = new AudioContext(); const compressor = audio.createDynamicsCompressor(); compressor.connect(audio.destination); const playNote = (e) => { // play note according to data-frequency attribute (new SoundPlayer(audio, compressor)).play(e.target.dataset.frequency, 0.8, "sine").setVolume(1/1000, 0.55).stop(0.6); e.cancelBubble = true; }; for(let el of document.getElementById("piano").getElementsByTagName("DIV")) { el.addEventListener("click", playNote, false); } </script>

Now all the notes should be clean even if you jam your hands down on the keyboard. We've also tweaked the note decay profile.

What's actually happening behing the scenes is illustrated below:

SoundPlayer Node configuration diagram

So our Soundplayer class is handling the frequency, wave type and volume, while the output (AudioContext.destination) and any intervening filter nodes are supplied from outside code.

Playing a series of notes

To play notes in series (without using a buffer or requestAnimationFrame for timing) we just need to specifiy a start and end time in seconds.

You might recognise this tune, for example:

(new SoundPlayer(audio)).play(587.3, 0.5, "sine").stop(0.25); (new SoundPlayer(audio)).play(587.3, 0.5, "sine", 0.3).stop(0.35); (new SoundPlayer(audio)).play(659.3, 0.5, "sine", 0.4).stop(0.55); (new SoundPlayer(audio)).play(587.3, 0.5, "sine", 0.6).stop(0.75); (new SoundPlayer(audio)).play(784.0, 0.5, "sine", 0.8).stop(0.95); (new SoundPlayer(audio)).play(740.0, 0.5, "sine", 1.0).stop(1.40);

Note that Safari (12.0.3) will not play sounds automatically on page load. Instead they are queued until a user action invokes the AudioContext interface.

References

< JavaScript

Send a message to The Art of Web:


used only for us to reply, and to display your gravatar.

<- copy the digits from the image into this box

press <Esc> or click outside this box to close

Post your comment or question
top