Web Audio API

The Web Audio API provides a versatile system for controlling audio on the Web, allowing developers to choose audio sources, add effects to audio, create audio visualizations, apply spatial effects such as panning, etc. With Web Audio API, game developers can play multiple pieces of audio concurrently on browsers.

The Web Audio API involves handling audio operations inside an audio context and has been designed to allow modular routing. Basic audio operations are performed with audio nodes, which are linked together to form an audio routing graph.

Audio nodes typically start with one or more sources. Sources provide arrays of sound intensities (samples) at very small time slices, often tens of thousands of them per second. These could be either computed mathematically (such as OscillatorNode), or they can be recordings from sound/video files (like AudioBufferSourceNode and MediaElementAudioSourceNode) and audio streams (MediaStreamAudioSourceNode). In fact, sound files are just recordings of sound intensities themselves, which come in from microphones or electric instruments and get mixed down into a single, complicated wave.

Outputs of these nodes could be linked to inputs of others, which mix or modify these streams of sound samples into different streams. A common modification is multiplying the samples by a value to make them louder or quieter (as is the case with GainNode). Once the sound has been sufficiently processed for the intended effect, it can be linked to the input of a destination (AudioContext.destination), which sends the sound to the speakers or headphones.

A simple, typical workflow for web audio would look like this:

  1. Create audio context.
  2. Inside the context, create sources — such as <audio>, oscillator, stream...
  3. Create effects nodes, such as reverb, biquad filter, panner, compressor...
  4. Choose the destination of the audio, for example your system speakers.
  5. Connect the sources up to the effects, and the effects to the destination.

Timing is controlled with high precision and low latency, allowing developers to write code that responds accurately to events and is able to target specific samples, even at a high sample rate. So applications such as drum machines and sequencers are well within reach.

The Web Audio API also allows us to control how audio is spatialized. Using a system based on a source-listener model, it allows control of the panning model and deals with distance-induced attenuation induced by a moving source (or moving listener).

The Web Audio API is complex to those who aren't familiar with audio or music terms. A comprehensive explanation of the subject is beyond the scope of this book.

Interfaces:
  1. General audio graph:
    1. AudioContext
    2. AudioContextOptions
    3. AudioNode
    4. AudioParam
    5. AudioParamMap
    6. BaseAudioContext
  2. Sources:
    1. AudioScheduledSourceNode
    2. OscillatorNode
    3. AudioBuffer
    4. AudioBufferSourceNode
    5. MediaElementAudioSourceNode
    6. MediaStreamAudioSourceNode
    7. MediaStreamTrackAudioSourceNode
  3. Effect filters:
    1. BiquadFilterNode
    2. ConvolverNode
    3. DelayNode
    4. DynamicsCompressorNode
    5. GainNode
    6. WaveShaperNode
    7. PeriodicWave
    8. IIRFilterNode
  4. Destinations:
    1. AudioDestinationNode
    2. MediaStreamAudioDestinationNode
  5. Analysis & Visualization:
    1. AnalyserNode
  6. Channels:
    1. ChannelSplitterNode
    2. ChannelMergerNode
  7. Spatialization:
    1. AudioListener
    2. PannerNode
    3. StereoPannerNode
  8. Worklet:
    1. AudioWorklet
    2. AudioWorkletNode
    3. AudioWorkletProcessor
    4. AudioWorkletGlobalScope
  9. Background rendering:
    1. OfflineAudioContext
    2. OfflineAudioCompletionEvent
This presents a playable musical keyboard on the browser.
RESETRUNFULL
<!DOCTYPE html><html><head>
   <style>
      .container {
            overflow-x: auto;
            overflow-y: hidden;
            width: 1400px;
            height: 110px;
            white-space: nowrap;
            margin: 10px;
        }
        .keyboard {
            width: auto;
            padding: 0;
            margin: 0;
        }
        .key {
            cursor: pointer;
            font: 16px "Open Sans", "Lucida Grande", "Arial", sans-serif;
            border: 1px solid black;
            border-radius: 5px;
            width: 20px;
            height: 80px;
            text-align: center;
            box-shadow: 2px 2px darkgray;
            display: inline-block;
            position: relative;
            margin-right: 3px;
            user-select: none;
            -moz-user-select: none;
            -webkit-user-select: none;
            -ms-user-select: none;
        }
        .key div {
            position: absolute;
            bottom: 0;
            text-align: center;
            width: 100%;
            pointer-events: none;
        }
        .key div sub {
            font-size: 10px;
            pointer-events: none;
        }
        .key:hover {
            background-color: #eef;
        }
        .key:active {
            background-color: #000;
            color: #fff;
        }
        .octave {
            display: inline-block;
            padding: 0 6px 0 0;
        }
        .settingsBar {
            padding-top: 8px;
            font: 14px "Open Sans", "Lucida Grande", "Arial", sans-serif;
            position: relative;
            vertical-align: middle;
            width: 100%;
            height: 30px;
        }
        .left {
            width: 50%;
            position: absolute;
            left: 0;
            display: table-cell;
            vertical-align: middle;
        }
        .left span, .left input {
            vertical-align: middle;
        }
        .right {
            width: 50%;
            position: absolute;
            right: 0;
            display: table-cell;
            vertical-align: middle;
        }
        .right span {
            vertical-align: middle;
        }
        .right input {
            vertical-align: baseline;
        }
   </style>
</head><body style="padding:20px;">
    <div class="container">
        <div class="keyboard"></div>
    </div>
    <div class="settingsBar">
        <div class="left">
          <span>Volume: </span>
          <input type="range" min="0.0" max="1.0" step="0.01" value="0.5" list="volumes" name="volume">
          <datalist id="volumes">
              <option value="0.0" label="Mute">
              <option value="1.0" label="100%">
          </datalist>
        </div>
        <div class="right">
            <span>Current waveform: </span>
            <select name="waveform">
                <option value="sine">Sine</option>
                <option value="square" selected>Square</option>
                <option value="sawtooth">Sawtooth</option>
                <option value="triangle">Triangle</option>
                <option value="custom">Custom</option>
            </select>
       </div>
    </div>
<script>
let audioContext = new AudioContext();
let oscList = [];
let masterGainNode = null;
let keyboard = document.querySelector(".keyboard");
let wavePicker = document.querySelector("select[name='waveform']");
let volumeControl = document.querySelector("input[name='volume']");
let noteFreq = null;
let customWaveform = null;
let sineTerms = null;
let cosineTerms = null;

function createNoteTable() {
  let noteFreq = [];
  for (let i=0; i< 9; i++) noteFreq[i] = [];
  noteFreq[0]["A"] = 27.500000000000000;
  noteFreq[0]["A#"] = 29.135235094880619;
  noteFreq[0]["B"] = 30.867706328507756;
  noteFreq[1]["C"] = 32.703195662574829;
  noteFreq[1]["C#"] = 34.647828872109012;
  noteFreq[1]["D"] = 36.708095989675945;
  noteFreq[1]["D#"] = 38.890872965260113;
  noteFreq[1]["E"] = 41.203444614108741;
  noteFreq[1]["F"] = 43.653528929125485;
  noteFreq[1]["F#"] = 46.249302838954299;
  noteFreq[1]["G"] = 48.999429497718661;
  noteFreq[1]["G#"] = 51.913087197493142;
  noteFreq[1]["A"] = 55.000000000000000;
  noteFreq[1]["A#"] = 58.270470189761239;
  noteFreq[1]["B"] = 61.735412657015513;
  noteFreq[2]["C"] = 65.406391325149658;
  noteFreq[2]["C#"] = 69.295657744218024;
  noteFreq[2]["D"] = 73.416191979351890;
  noteFreq[2]["D#"] = 77.781745930520227;
  noteFreq[2]["E"] = 82.406889228217482;
  noteFreq[2]["F"] = 87.307057858250971;
  noteFreq[2]["F#"] = 92.498605677908599;
  noteFreq[2]["G"] = 97.998858995437323;
  noteFreq[2]["G#"] = 103.826174394986284;
  noteFreq[2]["A"] = 110.000000000000000;
  noteFreq[2]["A#"] = 116.540940379522479;
  noteFreq[2]["B"] = 123.470825314031027;
  noteFreq[3]["C"] = 130.812782650299317;
  noteFreq[3]["C#"] = 138.591315488436048;
  noteFreq[3]["D"] = 146.832383958703780;
  noteFreq[3]["D#"] = 155.563491861040455;
  noteFreq[3]["E"] = 164.813778456434964;
  noteFreq[3]["F"] = 174.614115716501942;
  noteFreq[3]["F#"] = 184.997211355817199;
  noteFreq[3]["G"] = 195.997717990874647;
  noteFreq[3]["G#"] = 207.652348789972569;
  noteFreq[3]["A"] = 220.000000000000000;
  noteFreq[3]["A#"] = 233.081880759044958;
  noteFreq[3]["B"] = 246.941650628062055;
  noteFreq[4]["C"] = 261.625565300598634;
  noteFreq[4]["C#"] = 277.182630976872096;
  noteFreq[4]["D"] = 293.664767917407560;
  noteFreq[4]["D#"] = 311.126983722080910;
  noteFreq[4]["E"] = 329.627556912869929;
  noteFreq[4]["F"] = 349.228231433003884;
  noteFreq[4]["F#"] = 369.994422711634398;
  noteFreq[4]["G"] = 391.995435981749294;
  noteFreq[4]["G#"] = 415.304697579945138;
  noteFreq[4]["A"] = 440.000000000000000;
  noteFreq[4]["A#"] = 466.163761518089916;
  noteFreq[4]["B"] = 493.883301256124111;
  noteFreq[5]["C"] = 523.251130601197269;
  noteFreq[5]["C#"] = 554.365261953744192;
  noteFreq[5]["D"] = 587.329535834815120;
  noteFreq[5]["D#"] = 622.253967444161821;
  noteFreq[5]["E"] = 659.255113825739859;
  noteFreq[5]["F"] = 698.456462866007768;
  noteFreq[5]["F#"] = 739.988845423268797;
  noteFreq[5]["G"] = 783.990871963498588;
  noteFreq[5]["G#"] = 830.609395159890277;
  noteFreq[5]["A"] = 880.000000000000000;
  noteFreq[5]["A#"] = 932.327523036179832;
  noteFreq[5]["B"] = 987.766602512248223;
  noteFreq[6]["C"] = 1046.502261202394538;
  noteFreq[6]["C#"] = 1108.730523907488384;
  noteFreq[6]["D"] = 1174.659071669630241;
  noteFreq[6]["D#"] = 1244.507934888323642;
  noteFreq[6]["E"] = 1318.510227651479718;
  noteFreq[6]["F"] = 1396.912925732015537;
  noteFreq[6]["F#"] = 1479.977690846537595;
  noteFreq[6]["G"] = 1567.981743926997176;
  noteFreq[6]["G#"] = 1661.218790319780554;
  noteFreq[6]["A"] = 1760.000000000000000;
  noteFreq[6]["A#"] = 1864.655046072359665;
  noteFreq[6]["B"] = 1975.533205024496447;
  noteFreq[7]["C"] = 2093.004522404789077;
  noteFreq[7]["C#"] = 2217.461047814976769;
  noteFreq[7]["D"] = 2349.318143339260482;
  noteFreq[7]["D#"] = 2489.015869776647285;
  noteFreq[7]["E"] = 2637.020455302959437;
  noteFreq[7]["F"] = 2793.825851464031075;
  noteFreq[7]["F#"] = 2959.955381693075191;
  noteFreq[7]["G"] = 3135.963487853994352;
  noteFreq[7]["G#"] = 3322.437580639561108;
  noteFreq[7]["A"] = 3520.000000000000000;
  noteFreq[7]["A#"] = 3729.310092144719331;
  noteFreq[7]["B"] = 3951.066410048992894;
  noteFreq[8]["C"] = 4186.009044809578154;
  return noteFreq;
}
function setup() {
   noteFreq = createNoteTable();
   volumeControl.addEventListener("change", changeVolume, false);
   masterGainNode = audioContext.createGain();
   masterGainNode.connect(audioContext.destination);
   masterGainNode.gain.value = volumeControl.value;
   // Create the keys; skip any that are sharp or flat; for our purposes we don't need them. Each octave is inserted into a <div> of class "octave".
   noteFreq.forEach(function(keys, idx) {
      let keyList = Object.entries(keys);
      let octaveElem = document.createElement("div");
      octaveElem.className = "octave";
      keyList.forEach(function(key) {
         if (key[0].length == 1) {
            octaveElem.appendChild(createKey(key[0], idx, key[1]));
         }
      });
      keyboard.appendChild(octaveElem);
  });
  document.querySelector("div[data-note='B'][data-octave='5']").scrollIntoView(false);
  sineTerms = new Float32Array([0, 0, 1, 0, 1]);
  cosineTerms = new Float32Array(sineTerms.length);
  customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms);
  for (i=0; i<9; i++) oscList[i] = [];
}
setup();

function createKey(note, octave, freq) {
  let keyElement = document.createElement("div");
  let labelElement = document.createElement("div");
  keyElement.className = "key";
  keyElement.dataset["octave"] = octave;
  keyElement.dataset["note"] = note;
  keyElement.dataset["frequency"] = freq;
  labelElement.innerHTML = note + "<sub>" + octave + "</sub>";
  keyElement.appendChild(labelElement);
  keyElement.addEventListener("mousedown", notePressed, false);
  keyElement.addEventListener("mouseup", noteReleased, false);
  keyElement.addEventListener("mouseover", notePressed, false);
  keyElement.addEventListener("mouseleave", noteReleased, false);
  return keyElement;
}
function playTone(freq) {
  let osc = audioContext.createOscillator();
  osc.connect(masterGainNode);
  let type = wavePicker.options[wavePicker.selectedIndex].value;
  if (type == "custom") osc.setPeriodicWave(customWaveform);
  else osc.type = type;
  osc.frequency.value = freq;
  osc.start();
  return osc;
}
function notePressed(event) {
  if (event.buttons & 1) {
    let dataset = event.target.dataset;
    if (!dataset["pressed"]) {
      oscList[dataset["octave"][dataset["note"]]] =  playTone(dataset["frequency"]);
      dataset["pressed"] = "yes";
    }
  }
}
function noteReleased(event) {
   let dataset = event.target.dataset;
   if (dataset && dataset["pressed"]) {
      oscList[dataset["octave"][dataset["note"]]].stop();
      oscList[dataset["octave"][dataset["note"]]] = null;
      delete dataset["pressed"];
   }
}
function changeVolume(event) {
  masterGainNode.gain.value = volumeControl.value;
}
</script></body></html>