Created
August 31, 2023 20:31
-
-
Save chrisn/376c73de59b1bcacb6f44bba45ea2811 to your computer and use it in GitHub Desktop.
Peaks.js with multiple audio tracks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Peaks.js Demo Page</title> | |
<link rel="stylesheet" type="text/css" href="style.css"> | |
</head> | |
<body> | |
<div id="titles"> | |
<h1>Peaks.js</h1> | |
<p> | |
Peaks.js is a JavaScript library that allows you to display and | |
interact with audio waveforms in the browser. | |
</p> | |
<h2>Demo pages</h2> | |
<p> | |
The following pages demonstrate various configuration options: | |
</p> | |
<p> | |
<a href="/index.html">Precomputed Waveform Data</a> | | |
<a href="/webaudio.html">Web Audio API</a> | | |
<a href="/zoomable-waveform.html">Single Zoomable Waveform</a> | | |
<a href="/overview-waveform.html">Single Fixed Waveform</a> | | |
<a href="/cue-events.html">Cue Events</a> | | |
<a href="/set-source.html">Changing the Media URL</a> | | |
<a href="/multi-channel.html">Multi-Channel Waveform</a> | | |
<a href="/custom-markers">Custom Point and Segment Markers</a> | | |
<a href="/overlay-segments.html">Overlay Segments</a> | | |
External Audio Player | | |
<a href="/scrollbar.html">Scrollbar</a> | |
</p> | |
<h2>Demo: External Player</h2> | |
<p> | |
This demo shows how to use an external player for audio playback with Peaks.js. | |
A Tone.js player is set up in the example. | |
</p> | |
</div> | |
<div class="waveform-container"> | |
<div id="zoomview-container"></div> | |
</div> | |
<div class="waveform-container"> | |
<div id="overview-container"></div> | |
</div> | |
<div id="demo-controls"> | |
<div id="controls"> | |
<div> | |
<button data-action="play">Play</button> | |
<button data-action="pause">Pause</button> | |
<input type="text" id="seek-time" value="0.0"> | |
<button data-action="seek">Seek</button> | |
<button data-action="zoom-in">Zoom in</button> | |
<button data-action="zoom-out">Zoom out</button> | |
<input type="text" id="segment-start" value="2.0"> | |
<input type="text" id="segment-end" value="5.0"> | |
<button data-action="play-segment">Play segment</button> | |
</div> | |
<div> | |
<label for="select-audio">Select audio:</label> | |
<select id="select-audio"></select> | |
</div> | |
</div> | |
</div> | |
<script src="/peaks.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script> | |
<script> | |
function getSources() { | |
return [ | |
{ | |
title: 'Tree of Life', | |
mediaUrl: '/TOL_6min_720p_download.mp3', | |
}, | |
{ | |
title: 'Desert Island Discs', | |
mediaUrl: '/sample.mp3', | |
}, | |
{ | |
title: 'BBC Sound Effect (Cars)', | |
mediaUrl: '/07023003.mp3', | |
} | |
]; | |
} | |
function fetchAndDecode(audioContext, url) { | |
// Load audio file into an AudioBuffer | |
return fetch(url) | |
.then(function(response) { | |
return response.arrayBuffer(); | |
}) | |
.then(function(buffer) { | |
return audioContext.decodeAudioData(buffer); | |
}); | |
} | |
function fetchAndUpdateWaveform(peaksInstance, audioContext, source) { | |
return fetchAndDecode(audioContext, source.mediaUrl) | |
.then(function(audioBuffer) { | |
// Update the Peaks.js waveform view with the new AudioBuffer. | |
// The AudioBuffer is passed through to the external player's setSource() | |
// method, to update the player. | |
peaksInstance.setSource({ | |
webAudio: { | |
audioBuffer: audioBuffer, | |
scale: 128, | |
multiChannel: false | |
} | |
}, function(error) { | |
if (error) { | |
console.error('setSource error', error); | |
} | |
}); | |
}); | |
} | |
(async function(Peaks) { | |
async function initPeaks(options) { | |
return new Promise(function(resolve, reject) { | |
Peaks.init(options, function(err, peaks) { | |
if (err) { | |
reject(err); | |
} | |
else { | |
resolve(peaks); | |
} | |
}); | |
}); | |
} | |
const audioContext = Tone.context; | |
const audioBuffer1 = await fetchAndDecode(audioContext, "/TOL_6min_720p_download.mp3"); | |
const audioBuffer2 = await fetchAndDecode(audioContext, "/sample.mp3"); | |
const player = { | |
externalPlayer1: new Tone.Player(audioBuffer1).toDestination(), | |
externalPlayer2: new Tone.Player(audioBuffer2).toDestination(), | |
eventEmitter: null, | |
init: function(eventEmitter) { | |
this.eventEmitter = eventEmitter; | |
this.externalPlayer1.sync(); | |
this.externalPlayer2.sync(); | |
this.externalPlayer1.start(); | |
this.externalPlayer2.start(); | |
Tone.Transport.scheduleRepeat(() => { | |
const time = this.getCurrentTime(); | |
eventEmitter.emit('player.timeupdate', time); | |
if (time >= this.getDuration()) { | |
Tone.Transport.stop(); | |
} | |
}, 0.25); | |
return Promise.resolve(); | |
}, | |
destroy: function() { | |
Tone.context.dispose(); | |
this.externalPlayer1 = null; | |
this.externalPlayer2 = null; | |
this.eventEmitter = null; | |
}, | |
setSource: function(opts) { | |
if (this.isPlaying()) { | |
this.pause(); | |
} | |
// TODO: Update the Tone.js Player object with the new AudioBuffer | |
return Promise.reject(); | |
}, | |
play: function() { | |
return Tone.start().then(() => { | |
Tone.Transport.start(); | |
this.eventEmitter.emit('player.playing', this.getCurrentTime()); | |
}); | |
}, | |
pause: function() { | |
Tone.Transport.pause(); | |
this.eventEmitter.emit('player.pause', this.getCurrentTime()); | |
}, | |
isPlaying: function() { | |
return Tone.Transport.state === "started"; | |
}, | |
seek: function(time) { | |
Tone.Transport.seconds = time; | |
this.eventEmitter.emit('player.seeked', this.getCurrentTime()); | |
this.eventEmitter.emit('player.timeupdate', this.getCurrentTime()); | |
}, | |
isSeeking: function() { | |
return false; | |
}, | |
getCurrentTime: function() { | |
return Tone.Transport.seconds; | |
}, | |
getDuration: function() { | |
const duration1 = this.externalPlayer1.buffer.duration; | |
const duration2 = this.externalPlayer2.buffer.duration; | |
return duration1 > duration2 ? duration1 : duration2; | |
} | |
}; | |
const peaksInstance = await initPeaks({ | |
zoomview: { | |
container: document.getElementById('zoomview-container') | |
}, | |
player: player, | |
webAudio: { | |
audioBuffer: audioBuffer1, | |
scale: 128, | |
multiChannel: false | |
}, | |
keyboard: true, | |
showPlayheadTime: true, | |
zoomLevels: [128, 256, 512, 1024, 2048, 4096] | |
}); | |
const peaksInstance2 = await initPeaks({ | |
zoomview: { | |
container: document.getElementById('overview-container') | |
}, | |
player: player, | |
webAudio: { | |
audioBuffer: audioBuffer2, | |
scale: 128, | |
multiChannel: false | |
}, | |
keyboard: true, | |
showPlayheadTime: true, | |
zoomLevels: [128, 256, 512, 1024, 2048, 4096] | |
}); | |
console.log('Peaks instance ready'); | |
document.querySelector('[data-action="play"]').addEventListener('click', function() { | |
peaksInstance.player.play(); | |
}); | |
document.querySelector('[data-action="pause"]').addEventListener('click', function() { | |
peaksInstance.player.pause(); | |
}); | |
document.querySelector('button[data-action="seek"]').addEventListener('click', function(event) { | |
const time = document.getElementById('seek-time').value; | |
const seconds = parseFloat(time); | |
if (!Number.isNaN(seconds)) { | |
peaksInstance.player.seek(seconds); | |
} | |
}); | |
document.querySelector('[data-action="zoom-in"]').addEventListener('click', function() { | |
peaksInstance.zoom.zoomIn(); | |
}); | |
document.querySelector('[data-action="zoom-out"]').addEventListener('click', function() { | |
peaksInstance.zoom.zoomOut(); | |
}); | |
document.querySelector('[data-action="play-segment"]').addEventListener('click', function () { | |
const start = document.getElementById('segment-start').value; | |
const startInSeconds = parseFloat(start); | |
const end = document.getElementById('segment-end').value; | |
const endInSeconds = parseFloat(end); | |
peaksInstance.player.playSegment({ startTime: startInSeconds, endTime: endInSeconds, editable: true }); | |
}); | |
const select = document.getElementById('select-audio'); | |
const sources = getSources(); | |
for (let i = 0; i < sources.length; i++) { | |
select.options[i] = new Option(sources[i].title, i); | |
} | |
select.addEventListener('change', function(event) { | |
const source = sources[event.target.value]; | |
fetchAndUpdateWaveform(peaksInstance, audioContext, source); | |
}); | |
})(peaks); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment