Skip to content

Commit

Permalink
Add multi-track advanced example
Browse files Browse the repository at this point in the history
  • Loading branch information
mattfsourcecode committed Oct 13, 2024
1 parent 32e9c8c commit d2752b7
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 adds volume faders for each track, providing precise control over individual audio levels. It also introduces solo buttons, which allow the isolation of a specific track by bypassing 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/).
Expand Down
Binary file added multi-track-advanced/bassguitar.mp3
Binary file not shown.
Binary file added multi-track-advanced/clav.mp3
Binary file not shown.
Binary file added multi-track-advanced/drums.mp3
Binary file not shown.
Binary file added multi-track-advanced/horns.mp3
Binary file not shown.
222 changes: 222 additions & 0 deletions multi-track-advanced/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<!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">
Press to load tracks
</button>

<div class="wrapper">
<section id="tracks">
<ul>
<li data-loading="true">
<a href="leadguitar.mp3" class="track">Lead Guitar</a>
<input
type="range"
class="fader"
min="0"
max="1"
step="0.01"
value="0.8" />
<p class="loading-text">Loading...</p>
<button class="playbutton" aria-describedby="guitar-play-label">
<span id="guitar-play-label">Play</span>
</button>
<button class="solobutton" aria-describedby="guitar-solo-label">
<span>Solo</span>
</button>
</li>
<li data-loading="true">
<a href="bassguitar.mp3" class="track">Bass Guitar</a>
<input
type="range"
class="fader"
min="0"
max="1"
step="0.01"
value="0.8" />
<p class="loading-text">Loading...</p>
<button class="playbutton" aria-describedby="bass-play-label">
<span id="bass-play-label">Play</span>
</button>
<button class="solobutton" aria-describedby="bass-solo-label">
<span>Solo</span>
</button>
</li>
<li data-loading="true">
<a href="drums.mp3" class="track">Drums</a>
<input
type="range"
class="fader"
min="0"
max="1"
step="0.01"
value="0.8" />
<p class="loading-text">Loading...</p>
<button class="playbutton" aria-describedby="drums-play-label">
<span id="drums-play-label">Play</span>
</button>
<button class="solobutton" aria-describedby="drums-solo-label">
<span>Solo</span>
</button>
</li>
<li data-loading="true">
<a href="horns.mp3" class="track">Horns</a>
<input
type="range"
class="fader"
min="0"
max="1"
step="0.01"
value="0.8" />
<p class="loading-text">Loading...</p>
<button class="playbutton" aria-describedby="horns-play-label">
<span id="horns-play-label">Play</span>
</button>
<button class="solobutton" aria-describedby="horns-solo-label">
<span>Solo</span>
</button>
</li>
<li data-loading="true">
<a href="clav.mp3" class="track">Clavi</a>
<input
type="range"
class="fader"
min="0"
max="1"
step="0.01"
value="0.8" />
<p class="loading-text">Loading...</p>
<button class="playbutton" aria-describedby="clavi-play-label">
<span id="clavi-play-label">Play</span>
</button>
<button class="solobutton" aria-describedby="clavi-solo-label">
<span>Solo</span>
</button>
</li>
</ul>
<p class="sourced">
All tracks sourced from <a href="http://jplayer.org/">jplayer.org</a>
</p>
</section>
</div>

<script type="text/javascript">
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");
source.onended = () => playButton.classList.remove("playing");
}

function toggleSolo(button) {
if (soloedButton === button) {
button.classList.remove("active");
soloedButton = null;
} else {
if (soloedButton) soloedButton.classList.remove("active");
button.classList.add("active");
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>
Binary file added multi-track-advanced/leadguitar.mp3
Binary file not shown.
128 changes: 128 additions & 0 deletions multi-track-advanced/style.css
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit d2752b7

Please sign in to comment.