const sampleOffset = 1200; // So the beep lands accurately let frkk = null; let linenoise = null; let introcall = null; class Audio { constructor (settingsObj) { this.settingsObj = settingsObj; this.init(); this.promiseResolve = null; this.promiseReject = null; this.loaded = new Promise((resolve, reject) => { this.promiseResolve = resolve; this.promiseReject = reject; }); } async init() { // Set up web audio const AudioCtx = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioCtx; // Load file this.audioBuffer = await this.getFile(); } async getFile() { // Request file const response = await fetch(this.settingsObj.src); if (!response.ok) { console.log(`${response.url} ${response.statusText}`); this.promiseReject(); throw new Error(`${response.url} ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer); this.promiseResolve(); return audioBuffer; } } class Sound extends Audio { constructor (settingsObj) { super(settingsObj); } play () { let sampleSource = this.ctx.createBufferSource(); sampleSource.buffer = this.audioBuffer; sampleSource.connect(this.ctx.destination); sampleSource.addEventListener('ended', () => { if (this.cb) this.cb(); }) sampleSource.start(); } onEnded (cb) { this.cb = cb; } } class Loop extends Audio { constructor (settingsObj) { super(settingsObj); this.playing = []; } play () { console.log('play'); let sampleSource = this.ctx.createBufferSource(); sampleSource.buffer = this.audioBuffer; sampleSource.connect(this.ctx.destination); sampleSource.start(); this.stop(); this.playing.push(sampleSource); setTimeout(this.play.bind(this), this.settingsObj.looptime); } stop() { console.log('stop', this.playing); this.playing.forEach(sampleSource => { sampleSource.stop(this.ctx.currentTime + 0.1); // Adding .1 second to ensure overlap }); this.playing = []; } } class Sprite extends Audio { constructor(settingsObj) { super(settingsObj); this.playing = []; } play(sampleName) { const startTime = this.settingsObj.sprite[sampleName][0] / 1000; const duration = this.settingsObj.sprite[sampleName][1] / 1000; const sampleSource = this.ctx.createBufferSource(); sampleSource.buffer = this.audioBuffer; sampleSource.connect(this.ctx.destination); this.playing.push(sampleSource); sampleSource.start(this.ctx.currentTime, startTime, duration); sampleSource.addEventListener('ended', () => { this.playing.splice(this.playing.indexOf(sampleSource), 1); console.log('sample ended'); }); } } const tenSecondsInTheFuture = (dateObj) => { return new Date(dateObj.getTime() + 10000); } const nearestWholeTenSeconds = (dateObj) => { return new Date(Math.round(dateObj.getTime() / 10000) * 10000); } const nearestWholeTenSecondsInTheFuture = (dateObj) => { let nearest = nearestWholeTenSeconds(dateObj); return nearest <= dateObj ? tenSecondsInTheFuture(nearest) : nearest; } const dateToSamples = (dateObj) => { let h = dateObj.getHours(); return [ h < 10 ? `h${h}` : `m${h}`, `m${dateObj.getMinutes()}`, `s${dateObj.getSeconds()}`, 'b' ]; } const calcTimeDiff = (a, b) => { return b.getTime() - a.getTime(); } const clickHandler = (evt) => { const button = document.querySelector('.main-button button'); button.classList.add('off'); introcall.onEnded(mainLoop); introcall.play(); frkk.play('n'); linenoise.play(); // Have to play something to claim audio privileges } const scheduleSamples = (next, samples = []) => { if (samples.length === 0) return; let offset = (calcTimeDiff(new Date(), next) - samples.length * 2000); if (offset < 0) { scheduleSamples(next, samples.slice(1)); return; } else { setTimeout(() => { frkk.play(samples[0]); }, offset); scheduleSamples(next, samples.slice(1)); } } const mainLoop = () => { let now = new Date(); let next = nearestWholeTenSecondsInTheFuture(now); let samples = dateToSamples(next); console.log(samples); scheduleSamples(new Date(next.getTime() + sampleOffset), samples); let diff = next.getTime() - now.getTime(); setTimeout(() => { mainLoop(); }, diff); } const init = () => { const spriteData = {'src': './audio/frkk.mp3', 'sprite': {}}; for (let i = 0; i < 78; i++) { let name = i<10 ? `h${i}` : i<70 ? `m${i-10}` : i<76 ? `s${(i-70)*10}` : i<77 ? `b` : `n`; let timing = [i*2000, 2000]; spriteData.sprite[name] = timing; } frkk = new Sprite(spriteData); linenoise = new Loop({'src': './audio/linenoise.mp3', 'looptime': 9000}); introcall = new Sound({'src': './audio/call.mp3'}); Promise.all([frkk.loaded, linenoise.loaded, introcall.loaded]).then(() => { console.log('all loaded'); const button = document.querySelector('.main-button button'); button.addEventListener('click', clickHandler); button.classList.remove('off'); }); } window.addEventListener('DOMContentLoaded', init);