diff --git a/README.md b/README.md index 5b698a0..4fb867b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ The [media-source-buffer](https://github.com/mdn/webaudio-examples/tree/main/med The [multi-track](https://github.com/mdn/webaudio-examples/tree/main/multi-track) directory contains an example of connecting separate independently-playable audio tracks to a single [`AudioDestinationNode`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDestinationNode) interface. [Run the example live](http://mdn.github.io/webaudio-examples/multi-track/). +### Multi track advanced + +The [multi-track-advanced](https://github.com/mdn/webaudio-examples/tree/main/multi-track-advanced) directory contains an enhanced version of the original multi-track example. This version introduces a [`GainNode`](https://developer.mozilla.org/en-US/docs/Web/API/GainNode) for each track, providing precise control over individual audio levels through volume faders. It also includes solo buttons, enabling the isolation of a specific track by muting all other tracks. [Run the example live](http://mdn.github.io/webaudio-examples/multi-track-advanced/). + ### Offline audio context The [offline-audio-context](https://github.com/mdn/webaudio-examples/tree/main/offline-audio-context) directory contains a simple example to show how a Web Audio API [`OfflineAudioContext`](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext) interface can be used to rapidly process/render audio in the background to create a buffer, which can then be used in any way you please. For more information, see [https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext). [Run example live](http://mdn.github.io/webaudio-examples/offline-audio-context/). diff --git a/multi-track-advanced/bassguitar.mp3 b/multi-track-advanced/bassguitar.mp3 new file mode 100644 index 0000000..64566fb Binary files /dev/null and b/multi-track-advanced/bassguitar.mp3 differ diff --git a/multi-track-advanced/clav.mp3 b/multi-track-advanced/clav.mp3 new file mode 100644 index 0000000..70fffd0 Binary files /dev/null and b/multi-track-advanced/clav.mp3 differ diff --git a/multi-track-advanced/drums.mp3 b/multi-track-advanced/drums.mp3 new file mode 100644 index 0000000..3e098ec Binary files /dev/null and b/multi-track-advanced/drums.mp3 differ diff --git a/multi-track-advanced/horns.mp3 b/multi-track-advanced/horns.mp3 new file mode 100644 index 0000000..ddca909 Binary files /dev/null and b/multi-track-advanced/horns.mp3 differ diff --git a/multi-track-advanced/index.html b/multi-track-advanced/index.html new file mode 100644 index 0000000..7b505de --- /dev/null +++ b/multi-track-advanced/index.html @@ -0,0 +1,271 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title>Web Audio API Mixer - Advanced</title> + <meta + name="description" + content="A way to make sure files have loaded before playing them" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + </head> + <body> + <!-- + Some browsers' autoplay policy requires that an AudioContext be initialized + during an input event in order to correctly synchronize. + + So provide a simple button to get things started. + --> + <button + id="startbutton" + class="top-left-button" + aria-label="Start loading tracks"> + Press to load tracks + </button> + + <div class="wrapper"> + <section id="tracks" role="region" aria-labelledby="tracks-title"> + <ul role="list"> + <li data-loading="true" role="listitem"> + <a href="leadguitar.mp3" class="track" tabindex="0">Lead Guitar</a> + <input + type="range" + class="fader" + min="0" + max="1" + step="0.01" + value="0.8" + aria-label="Volume control for Lead Guitar" /> + <p class="loading-text" aria-live="polite">Loading...</p> + <button + class="playbutton" + aria-describedby="guitar-play-label" + aria-pressed="false"> + <span id="guitar-play-label">Play</span> + </button> + <button + class="solobutton" + aria-describedby="guitar-solo-label" + aria-pressed="false"> + <span id="guitar-solo-label">Solo</span> + </button> + </li> + <li data-loading="true" role="listitem"> + <a href="bassguitar.mp3" class="track" tabindex="0">Bass Guitar</a> + <input + type="range" + class="fader" + min="0" + max="1" + step="0.01" + value="0.8" + aria-label="Volume control for Bass Guitar" /> + <p class="loading-text" aria-live="polite">Loading...</p> + <button + class="playbutton" + aria-describedby="bass-play-label" + aria-pressed="false"> + <span id="bass-play-label">Play</span> + </button> + <button + class="solobutton" + aria-describedby="bass-solo-label" + aria-pressed="false"> + <span id="bass-solo-label">Solo</span> + </button> + </li> + <li data-loading="true" role="listitem"> + <a href="drums.mp3" class="track" tabindex="0">Drums</a> + <input + type="range" + class="fader" + min="0" + max="1" + step="0.01" + value="0.8" + aria-label="Volume control for Drums" /> + <p class="loading-text" aria-live="polite">Loading...</p> + <button + class="playbutton" + aria-describedby="drums-play-label" + aria-pressed="false"> + <span id="drums-play-label">Play</span> + </button> + <button + class="solobutton" + aria-describedby="drums-solo-label" + aria-pressed="false"> + <span id="drums-solo-label">Solo</span> + </button> + </li> + <li data-loading="true" role="listitem"> + <a href="horns.mp3" class="track" tabindex="0">Horns</a> + <input + type="range" + class="fader" + min="0" + max="1" + step="0.01" + value="0.8" + aria-label="Volume control for Horns" /> + <p class="loading-text" aria-live="polite">Loading...</p> + <button + class="playbutton" + aria-describedby="horns-play-label" + aria-pressed="false"> + <span id="horns-play-label">Play</span> + </button> + <button + class="solobutton" + aria-describedby="horns-solo-label" + aria-pressed="false"> + <span id="horns-solo-label">Solo</span> + </button> + </li> + <li data-loading="true" role="listitem"> + <a href="clav.mp3" class="track" tabindex="0">Clavi</a> + <input + type="range" + class="fader" + min="0" + max="1" + step="0.01" + value="0.8" + aria-label="Volume control for Clavi" /> + <p class="loading-text" aria-live="polite">Loading...</p> + <button + class="playbutton" + aria-describedby="clavi-play-label" + aria-pressed="false"> + <span id="clavi-play-label">Play</span> + </button> + <button + class="solobutton" + aria-describedby="clavi-solo-label" + aria-pressed="false"> + <span id="clavi-solo-label">Solo</span> + </button> + </li> + </ul> + <p class="sourced"> + All tracks sourced from <a href="http://jplayer.org/">jplayer.org</a> + </p> + </section> + </div> + + <script> + let audioCtx = null; + let soloedButton = null; + + // Provide a start button so demo can load tracks from an event handler for cross-browser compatibility + const startButton = document.querySelector("#startbutton"); + + // Select all list elements + const trackEls = document.querySelectorAll("li"); + + // Loading function for fetching the audio file and decode the data + async function getFile(filepath) { + const response = await fetch(filepath); + const arrayBuffer = await response.arrayBuffer(); + return await audioCtx.decodeAudioData(arrayBuffer); + } + + function createGainNode() { + const gainNode = audioCtx.createGain(); + gainNode.connect(audioCtx.destination); + return gainNode; + } + + // Create a buffer, plop in data, connect and play -> modify graph here if required + function playTrack(audioBuffer, gainNode, playButton) { + const source = audioCtx.createBufferSource(); + source.buffer = audioBuffer; + source.connect(gainNode); + source.start(); + playButton.classList.add("playing"); + playButton.setAttribute("aria-pressed", "true"); + source.onended = () => { + playButton.classList.remove("playing"); + playButton.setAttribute("aria-pressed", "false"); + }; + } + + function toggleSolo(button) { + if (soloedButton === button) { + button.classList.remove("active"); + button.setAttribute("aria-pressed", "false"); + soloedButton = null; + } else { + if (soloedButton) { + soloedButton.classList.remove("active"); + soloedButton.setAttribute("aria-pressed", "false"); + } + button.classList.add("active"); + button.setAttribute("aria-pressed", "true"); + soloedButton = button; + } + updateFadersAndMute(); + } + + function updateFadersAndMute() { + // Get children + trackEls.forEach((el) => { + const fader = el.querySelector(".fader"); + // Retrieve the gain node + const gainNode = el.gainNode; + const isSoloed = el.contains(soloedButton); + + if (soloedButton) { + // Mute non-soloed tracks + gainNode.gain.value = isSoloed ? fader.value : 0; + fader.classList.toggle("disabled", !isSoloed); + } else { + // Restore all tracks if no solo is active + gainNode.gain.value = fader.value; + fader.classList.remove("disabled"); + } + }); + } + + startButton.addEventListener("click", () => { + if (audioCtx) return; + audioCtx = new AudioContext(); + startButton.hidden = true; + + trackEls.forEach((el) => { + const anchor = el.querySelector("a"); + const playButton = el.querySelector(".playbutton"); + const soloButton = el.querySelector(".solobutton"); + const loadText = el.querySelector(".loading-text"); + const fader = el.querySelector(".fader"); + + // Create a gain node + const gainNode = createGainNode(); + // Store the gain node in the element + el.gainNode = gainNode; + + fader.addEventListener("input", (e) => { + gainNode.gain.value = e.target.value; + }); + + getFile(anchor.href).then((track) => { + loadText.style.display = "none"; + playButton.style.display = "inline-block"; + soloButton.style.display = "inline-block"; + + playButton.addEventListener("click", () => { + if (audioCtx.state === "suspended") { + audioCtx.resume(); + } + playTrack(track, gainNode, playButton); + }); + + soloButton.addEventListener("click", () => toggleSolo(soloButton)); + }); + }); + }); + </script> + </body> +</html> diff --git a/multi-track-advanced/leadguitar.mp3 b/multi-track-advanced/leadguitar.mp3 new file mode 100644 index 0000000..dea0964 Binary files /dev/null and b/multi-track-advanced/leadguitar.mp3 differ diff --git a/multi-track-advanced/style.css b/multi-track-advanced/style.css new file mode 100644 index 0000000..331082b --- /dev/null +++ b/multi-track-advanced/style.css @@ -0,0 +1,128 @@ +body { + background-color: #121212; + color: white; + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} + +a { + color: #8cb4ff; +} + +.top-left-button { + position: absolute; + top: 20px; + left: 20px; + background-color: #4caf50; + border: none; + padding: 10px 15px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +.top-left-button:hover { + background-color: #45a049; +} + +.wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + padding: 20px; + box-sizing: border-box; +} + +#tracks { + display: flex; + flex-direction: row; + gap: 20px; +} + +ul { + display: flex; + gap: 20px; + padding: 0; + margin: 0; + list-style: none; +} + +li { + background-color: #1e1e1e; + padding: 20px; + padding-top: 30px; + border-radius: 8px; + width: 150px; + height: 370px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.track { + font-weight: bold; + font-size: 18px; + text-align: center; +} + +.playbutton { + background-color: #ff5050; + border: none; + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + display: none; +} + +.playbutton:hover, +.playbutton.playing { + background-color: #45a049; +} + +.solobutton { + background-color: gray; + border: none; + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + display: none; +} + +.solobutton.active { + background-color: yellow; +} + +.fader { + -webkit-appearance: slider-vertical; + width: 30px; + height: 220px; + margin-top: 10px; + cursor: pointer; +} + +.fader.disabled { + background-color: #555; + pointer-events: none; +} + +.loading-text { + font-size: 14px; + color: #bbb; +} + +.sourced { + position: absolute; + bottom: 20px; + right: 20px; + font-size: 14px; + color: #bbb; +} + +button:focus { + outline: 2px solid #2196f3; +}