diff --git a/src/js/index.js b/src/js/index.js index 1cc75ff..ec4730e 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -274,6 +274,8 @@ const puppetShow = new PuppetShow({ audioContext }); +const recordedSounds = new Map(); +const currentSounds = new Set(); const sfx = new Map(); [ 'sounds/bark.wav', @@ -449,6 +451,34 @@ function stopEditing() { updateEditingState(); } +function playSoundEvent(event) { + if (!puppetShow.playing) { + // don't play sounds while we're paused + return true; + } + + // todo: add to list of currently active audio tracks so we can resume + + const name = event.params.name; + const time = event.time; + const duration = event.duration; + const currentTime = puppetShow.currentTime; + + const timeLeft = Math.max(0, time + duration - currentTime); + if (timeLeft > 0) { + const effect = sfx.get(name) || recordedSounds.get(name); + if (!effect) { + console.warn('Unknown sound effect', name, event); + return false; + } + + effect.play(Math.max(0, currentTime - time)); + return true; + } + + return false; +} + puppetShow .on('load', () => { console.log('loaded puppet show', puppetShow.id); @@ -467,10 +497,37 @@ puppetShow console.log('error loading puppet show', id); // todo: clear stage, force redraw and report error }) - .on('play', updateButtons) - .on('pause', updateButtons) - .on('ready', updateButtons) - .on('unready', updateButtons) + .on('play', () => { + currentSounds.forEach(soundEvent => { + const isCurrent = playSoundEvent(soundEvent); + if (!isCurrent) { + currentSounds.delete(soundEvent); + } + }); + updateButtons(); + }) + .on('pause', () => { + recordedSounds.forEach(e => e.stop()); + sfx.forEach(e => e.stop()); + updateButtons(); + }) + .on('ready', () => { + puppetShow.audioAssets.forEach(asset => { + const effect = new SoundEffect({ + src: asset.buffer, + context: audioContext, + name: asset.id + }); + recordedSounds.set(asset.id, effect); + }); + updateButtons(); + }) + .on('unready', () => { + currentSounds.clear(); + recordedSounds.forEach(s => s.stop()); + recordedSounds.clear(); + updateButtons(); + }) .on('event', event => { if (puppetShowRecorder && puppetShowRecorder.recording) { return; @@ -494,20 +551,9 @@ puppetShow } if (event.type === 'sound') { - const name = event.params.name; - const time = event.time; - const duration = event.duration; - const currentTime = puppetShow.currentTime; - - const timeLeft = Math.max(0, time + duration - currentTime); - if (timeLeft > 0) { - const effect = sfx.get(name); - if (!effect) { - console.warn('Unknown sound effect', name, event); - return; - } - - effect.play(Math.max(0, currentTime - time)); + const isCurrent = playSoundEvent(event); + if (isCurrent) { + currentSounds.add(event); } return; } diff --git a/src/js/puppet-show-recorder.js b/src/js/puppet-show-recorder.js index 3921870..0b112eb 100644 --- a/src/js/puppet-show-recorder.js +++ b/src/js/puppet-show-recorder.js @@ -188,10 +188,12 @@ function PuppetShowRecorder(options) { endTime = now(); audioRecorder.stop(); + const duration = (endTime - startTime) / 1000; + // todo: save audio asset to puppetShow if not being cleared? audioRecorder.exportWAV(blob => { // todo: add time when we allow appending - puppetShow.addAudio(blob); + puppetShow.addAudio(blob, duration); }); this.emit('stop'); diff --git a/src/js/puppet-show.js b/src/js/puppet-show.js index 315bd90..558aa88 100644 --- a/src/js/puppet-show.js +++ b/src/js/puppet-show.js @@ -117,7 +117,7 @@ function PuppetShow(options) { // playback state let playStartTime = 0; let playEndTime = 0; - let playing = 0; + let playing = false; let lastUpdateTime = 0; let playEventIndex = 0; const currentEventsByType = new Map(); @@ -336,6 +336,11 @@ function PuppetShow(options) { duration = 0; assetsToLoad = 0; + if (ready) { + ready = false; + this.emit('unready'); + } + // erasing all media assets and events from server showRef.child('duration').set(0); showRef.child('events').remove(); @@ -373,15 +378,20 @@ function PuppetShow(options) { event.duration = dur; } + const isLastEvent = !events.length || events[events.length - 1].time <= event.time; events.push(event); showRef.child('events').push(event); + if (!isLastEvent) { + events.sort(sortEvents); + } + duration = Math.max(duration, time + (dur || 0)); showRef.child('duration').set(duration); showRef.child('modifyTime').set(ServerValue.TIMESTAMP); }; - this.addAudio = (encodedBlob, time) => { + this.addAudio = (encodedBlob, dur, time) => { if (!loaded) { // todo: either wait to finish loading or throw error return; @@ -424,7 +434,6 @@ function PuppetShow(options) { audioContext.decodeAudioData(fileReader.result).then(decodedBuffer => { audioObject.buffer = decodedBuffer; - // todo: set up audio source or whatever console.log('decoded audio'); duration = Math.max(duration, audioObject.time + decodedBuffer.duration); @@ -450,8 +459,11 @@ function PuppetShow(options) { console.log('saved audio file', id, snapshot); }); - // showRef.child('modifyTime').set(ServerValue.TIMESTAMP); - // todo: add to list of events + // add to list of events + this.addEvent('sound', { + name: id + }, null, dur, time); + showRef.child('modifyTime').set(ServerValue.TIMESTAMP); if (wasReady) { this.emit('unready'); @@ -503,7 +515,7 @@ function PuppetShow(options) { // find any current events of each type/index and fire an event // we assume the event handler will take care of ending the event const currentTime = this.currentTime; - let index = currentTime >= lastUpdateTime ? playEventIndex : 0; + let index = currentTime > lastUpdateTime ? playEventIndex : 0; currentEventsByType.forEach(map => map.clear()); @@ -567,6 +579,9 @@ function PuppetShow(options) { events: { value: events }, + audioAssets: { + value: audioAssets + }, playing: { get: () => playing diff --git a/src/js/sound-effect.js b/src/js/sound-effect.js index 5b3e47c..8d5c629 100644 --- a/src/js/sound-effect.js +++ b/src/js/sound-effect.js @@ -1,5 +1,7 @@ 'use strict'; +import eventEmitter from 'event-emitter'; + function SoundEffect(options) { const src = options.src; const context = options.context; @@ -8,23 +10,33 @@ function SoundEffect(options) { const sources = []; + eventEmitter(this); + // load audio file - // todo: load appropriate format based on browser support - const xhr = new XMLHttpRequest(); - xhr.responseType = 'arraybuffer'; - xhr.onload = () => { - context.decodeAudioData(xhr.response, decodedBuffer => { - buffer = decodedBuffer; - this.duration = buffer.duration; - console.log('loaded buffer', src, buffer); - }); - }; - xhr.onerror = e => { - // keep trying - console.warn('Error loading audio', src, e); - }; - xhr.open('GET', src, true); - xhr.send(); + if (options.src instanceof window.AudioBuffer) { + buffer = options.src; + this.duration = buffer.duration; + setTimeout(() => this.emit('load'), 0); + } else if (src && typeof src === 'string') { + // todo: load appropriate format based on browser support + const xhr = new XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + xhr.onload = () => { + context.decodeAudioData(xhr.response, decodedBuffer => { + buffer = decodedBuffer; + this.duration = buffer.duration; + console.log('loaded buffer', src, buffer); + }); + }; + xhr.onerror = e => { + // keep trying + console.warn('Error loading audio', src, e); + }; + xhr.open('GET', src, true); + xhr.send(); + } else { + throw new Error('SoundEffect: missing src option'); + } let stopSource; function stopEvent(evt) {