diff --git a/package.json b/package.json index fa65340..18de459 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "object-assign": "^4.0.1", "open-iconic": "^1.1.1", "raw-loader": "^0.5.1", + "recorderjs": "git+https://github.com/mattdiamond/Recorderjs.git", "safe-json-stringify": "^1.0.3", "style-loader": "^0.13.0", "three": "^0.84.0", diff --git a/src/html/index.html b/src/html/index.html index d91e835..d533979 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -50,6 +50,7 @@ padding: 10px; z-index: 1; } + @@ -66,6 +67,7 @@
+
diff --git a/src/js/index.js b/src/js/index.js index c48a9f3..ab406a2 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -23,6 +23,7 @@ require('imports?THREE=three!three/examples/js/vr/ViveController'); import SoundEffect from './sound-effect'; import PuppetShow from './puppet-show'; +import PuppetShowRecorder from './puppet-show-recorder'; // Setup three.js WebGL renderer. Note: Antialiasing is a big performance hit. // Only enable it if you actually need to. @@ -312,6 +313,49 @@ document.getElementById('new-show').addEventListener('click', () => { puppetShow.create(); }); +/* +Set up recording +todo: nicer interface +todo: drop-down to select microphone/input if there's more than one (i.e. Vive) + +todo: maybe load recorder code in a separate chunk, only on supported devices +*/ +const puppetShowRecorder = new PuppetShowRecorder({ + puppetShow, + audioContext +}); + +const recordButton = document.getElementById('record'); +recordButton.disabled = true; +puppetShowRecorder + .on('ready', () => { + recordButton.disabled = false; + }) + .on('error', () => { + recordButton.disabled = true; + // todo: report error. try again? + }) + .on('start', () => { + recordButton.innerHTML = 'Stop'; + }) + .on('stop', () => { + recordButton.innerHTML = 'Reset'; + }) + .on('reset', () => { + recordButton.innerHTML = 'Record'; + }); + +recordButton.addEventListener('click', () => { + if (puppetShowRecorder.recording) { + puppetShowRecorder.stop(); + } else if (!puppetShowRecorder.currentTime) { + puppetShowRecorder.start(); + } else { + // todo: require confirmation? + puppetShowRecorder.reset(); + } +}); + // Request animation frame loop function let vrDisplay = null; let lastRender = 0; @@ -351,6 +395,11 @@ function animate(timestamp) { renderer.render(scene, windowCamera); } + // temp + if (puppetShowRecorder.recording) { + console.log(puppetShowRecorder.currentTime); + } + // Keep looping. if (isPresenting) { vrDisplay.requestAnimationFrame(animate); diff --git a/src/js/now.js b/src/js/now.js new file mode 100644 index 0000000..28b1c74 --- /dev/null +++ b/src/js/now.js @@ -0,0 +1,7 @@ +'use strict'; + +const now = typeof performance === 'undefined' ? Date.now : function () { + return performance.now(); +}; + +export default now; \ No newline at end of file diff --git a/src/js/puppet-show-recorder.js b/src/js/puppet-show-recorder.js new file mode 100644 index 0000000..41bba75 --- /dev/null +++ b/src/js/puppet-show-recorder.js @@ -0,0 +1,174 @@ +'use strict'; + +import Recorder from 'recorderjs'; +import eventEmitter from 'event-emitter'; +import now from './now'; + +function PuppetShowRecorder(options) { + /* + todo: + - record puppet positions + - record sound effects + - record background switches + - handle missing gUM implementation or no devices + - append recording + + - save events and assets to puppetShow + - release microphone when page is in backround and not presenting? + */ + + eventEmitter(this); + + const me = this; + + const { + puppetShow, + audioContext + } = options; + + const recordConstraints = { + audio: { + channelCount: 1 + } + }; + + const audioInputDevices = []; + + // state + let ready = false; + let recording = false; + let startTime = 0; + let endTime = 0; + + let audioInputDevice = null; + let audioStream = null; + let audioSource = null; + let audioRecorder = null; + + function getAudioStream() { + navigator.mediaDevices.getUserMedia(recordConstraints).then(stream => { + console.log('Accessed Microphone'); + // todo: update UI to show recording state + + audioSource = audioContext.createMediaStreamSource(stream); + + // need to connect to a destination. otherwise it won't process + // const zeroGain = audioContext.createGain(); + // zeroGain.gain.value = 0.0; + // audioSource.connect(zeroGain); + // zeroGain.connect(audioContext.destination); + + if (audioRecorder) { + audioRecorder.stop(); + audioRecorder.clear(); + // todo: what happens if we're recording? + } + + audioRecorder = new Recorder(audioSource, { + numChannels: 1 + }); + + const wasReady = ready; + ready = true; + if (!wasReady) { + me.emit('ready'); + } + }).catch(err => { + console.error('Cannot access microphone', err); + // todo: fire error event; set state to not ready + me.emit('error', err); + }); + } + + this.init = () => { + navigator.mediaDevices.enumerateDevices().then(devices => { + audioInputDevices.length = 0; + devices.forEach(dev => { + if (dev.kind === 'audioinput') { + audioInputDevices.push(dev); + + // todo: prioritize Vive or Rift mic + if (dev.deviceId === 'default' || !audioInputDevice) { + audioInputDevice = dev; + } + } + }); + + recordConstraints.audio.deviceId = audioInputDevice.deviceId; + // todo: get audio stream only if device changed and not recording + getAudioStream(); + }); + }; + + this.start = () => { + if (recording) { + // todo: throw error? or emit error event? + return; + } + + if (!ready) { + throw new Error('PuppetShowRecorder: Not ready to record'); + } + + // todo: allow appending + this.reset(); + + recording = true; + startTime = now(); + audioRecorder.record(); + + this.emit('start'); + }; + + this.stop = () => { + if (!recording) { + // todo: throw error? or emit error event? + return; + } + + recording = false; + endTime = now(); + audioRecorder.stop(); + + // todo: save audio asset to puppetShow if not being cleared? + + this.emit('stop'); + }; + + this.reset = () => { + this.stop(); + + if (!this.currentTime) { + return; + } + + if (audioRecorder) { + audioRecorder.clear(); + } + startTime = 0; + endTime = 0; + + // todo: clear data out of puppetShow + + this.emit('reset'); + }; + + // todo: query recording devices + // todo: select audio recording device + + this.init(); + + Object.defineProperties(this, { + ready: { + get: () => ready + }, + currentTime: { + get: () => ((recording ? now() : endTime) - startTime) / 1000 + }, + recording: { + get: () => recording + } + }); +} + +export default PuppetShowRecorder; \ No newline at end of file