diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js index aa7182fa7329145078a1ff27270700164444efff..cdae3ef89a7b42c4745e2227a08836bc5388380b 100644 --- a/src/components/audio-feedback.js +++ b/src/components/audio-feedback.js @@ -6,11 +6,13 @@ AFRAME.registerComponent("networked-audio-analyser", { async init() { this.volume = 0; + this.prevVolume = 0; + this.smoothing = 0.3; this.el.addEventListener("sound-source-set", event => { const ctx = THREE.AudioContext.getContext(); this.analyser = ctx.createAnalyser(); this.analyser.fftSize = 32; - this.levels = new Uint8Array(this.analyser.frequencyBinCount); + this.levels = new Float32Array(this.analyser.frequencyBinCount); event.detail.soundSource.connect(this.analyser); }); }, @@ -18,27 +20,15 @@ AFRAME.registerComponent("networked-audio-analyser", { tick: function() { if (!this.analyser) return; - this.analyser.getByteFrequencyData(this.levels); + this.analyser.getFloatTimeDomainData(this.levels); let sum = 0; for (let i = 0; i < this.levels.length; i++) { - sum += this.levels[i]; + const amplitude = this.levels[i]; + sum += amplitude * amplitude; } - this.volume = sum / this.levels.length; - } -}); - -/** - * Sets an entity's color base on audioFrequencyChange events. - * @component matcolor-audio-feedback - */ -AFRAME.registerComponent("matcolor-audio-feedback", { - tick() { - const audioAnalyser = this.el.components["networked-audio-analyser"]; - - if (!audioAnalyser || !this.mat) return; - - this.object3D.mesh.color.setScalar(1 + audioAnalyser.volume / 255 * 2); + this.volume = this.smoothing * Math.sqrt(sum / this.levels.length) + (1 - this.smoothing) * this.prevVolume; + this.prevVolume = this.volume; } }); @@ -64,6 +54,7 @@ AFRAME.registerComponent("scale-audio-feedback", { if (!audioAnalyser) return; - this.el.object3D.scale.setScalar(minScale + (maxScale - minScale) * audioAnalyser.volume / 255); + const scale = Math.min(maxScale, minScale + (maxScale - minScale) * audioAnalyser.volume * 8); + this.el.object3D.scale.setScalar(scale); } }); diff --git a/src/components/avatar-replay.js b/src/components/avatar-replay.js index 260744549a7f3eb60c210bb90f7ec063c454e888..b1dd13bf7f20ded46150246d445b2ac611277698 100644 --- a/src/components/avatar-replay.js +++ b/src/components/avatar-replay.js @@ -1,5 +1,3 @@ -import botRecording from "../assets/avatars/bot-recording.json"; - // These controls are removed from the controller entities so that motion-capture-replayer is in full control of them. const controlsBlacklist = [ "tracked-controls", @@ -20,24 +18,30 @@ AFRAME.registerComponent("avatar-replay", { schema: { camera: { type: "selector" }, leftController: { type: "selector" }, - rightController: { type: "selector" } + rightController: { type: "selector" }, + recordingUrl: { type: "string" } }, init: function() { - const { camera, leftController, rightController } = this.data; + this.modelLoaded = new Promise(resolve => this.el.addEventListener("model-loaded", resolve)); + }, + update: function() { + const { camera, leftController, rightController, recordingUrl } = this.data; + const fetchRecording = fetch(recordingUrl).then(resp => resp.json()); camera.setAttribute("motion-capture-replayer", { loop: true }); this._setupController(leftController); this._setupController(rightController); - this.el.addEventListener("model-loaded", () => { + this.dataLoaded = Promise.all([fetchRecording, this.modelLoaded]).then(([recording]) => { const cameraReplayer = camera.components["motion-capture-replayer"]; - cameraReplayer.startReplaying(botRecording.camera); + cameraReplayer.startReplaying(recording.camera); const leftControllerReplayer = leftController.components["motion-capture-replayer"]; - leftControllerReplayer.startReplaying(botRecording.left); + leftControllerReplayer.startReplaying(recording.left); const rightControllerReplayer = rightController.components["motion-capture-replayer"]; - rightControllerReplayer.startReplaying(botRecording.right); + rightControllerReplayer.startReplaying(recording.right); }); }, + _setupController: function(controller) { controlsBlacklist.forEach(controlsComponent => controller.removeAttribute(controlsComponent)); controller.setAttribute("visible", true); diff --git a/src/hub.html b/src/hub.html index a75e5fa915dd613e33c9a3f50be78f248326732b..9302575b9801fe8205d00758d38e30803bae6b4b 100644 --- a/src/hub.html +++ b/src/hub.html @@ -23,8 +23,6 @@ </head> <body data-html-prefix="<%= HTML_PREFIX %>"> - <audio id="bot-recording" loop muted crossorigin="anonymous" src="./assets/avatars/bot-recording.mp3"></audio> - <audio id="test-tone"> <source src="./assets/sfx/tone.webm" type="audio/webm"/> <source src="./assets/sfx/tone.mp3" type="audio/mpeg"/> diff --git a/src/hub.js b/src/hub.js index 9bddbbabbef7587f7e1c4813450bfcf765359102..fa21149111a673f2390b7acc6b33eaeb3dbfa840 100644 --- a/src/hub.js +++ b/src/hub.js @@ -370,15 +370,27 @@ const onReady = async () => { playerRig.setAttribute("avatar-replay", { camera: "#player-camera", leftController: "#player-left-controller", - rightController: "#player-right-controller" + rightController: "#player-right-controller", + recordingUrl: "/assets/avatars/bot-recording.json" }); - const audio = document.getElementById("bot-recording"); - mediaStream.addTrack(audio.captureStream().getAudioTracks()[0]); + + const audioEl = document.createElement("audio"); + audioEl.loop = true; + audioEl.muted = true; + audioEl.crossorigin = "anonymous"; + audioEl.src = "/assets/avatars/bot-recording.mp3"; + document.body.appendChild(audioEl); + // Wait for runner script to interact with the page so that we can play audio. - await new Promise(resolve => { + const interacted = new Promise(resolve => { window.interacted = resolve; }); - audio.play(); + const canPlay = new Promise(resolve => { + audioEl.addEventListener("canplay", resolve); + }); + await Promise.all([canPlay, interacted]); + mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]); + audioEl.play(); } if (mediaStream) { diff --git a/src/network-schemas.js b/src/network-schemas.js index 11c6e411dac5254b6382231f9f5013a2e78966fd..840ecb97bb55e0e3cd3c3b2f98a6cbc073c2a526 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -1,10 +1,16 @@ function registerNetworkSchemas() { - const positionRequiresUpdate = (oldData, newData) => { - return !NAF.utils.almostEqualVec3(oldData, newData, 0.001); - }; - - const rotationRequiresUpdate = (oldData, newData) => { - return !NAF.utils.almostEqualVec3(oldData, newData, 0.5); + const vectorRequiresUpdate = epsilon => { + let prev = null; + return curr => { + if (prev === null) { + prev = new THREE.Vector3(curr.x, curr.y, curr.z); + return true; + } else if (!NAF.utils.almostEqualVec3(prev, curr, epsilon)) { + prev.copy(curr); + return true; + } + return false; + }; }; NAF.schemas.add({ @@ -12,11 +18,11 @@ function registerNetworkSchemas() { components: [ { component: "position", - requiresNetworkUpdate: positionRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { component: "rotation", - requiresNetworkUpdate: rotationRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, "scale", "player-info", @@ -24,22 +30,22 @@ function registerNetworkSchemas() { { selector: ".camera", component: "position", - requiresNetworkUpdate: positionRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { selector: ".camera", component: "rotation", - requiresNetworkUpdate: rotationRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, { selector: ".left-controller", component: "position", - requiresNetworkUpdate: positionRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { selector: ".left-controller", component: "rotation", - requiresNetworkUpdate: rotationRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, { selector: ".left-controller", @@ -48,12 +54,12 @@ function registerNetworkSchemas() { { selector: ".right-controller", component: "position", - requiresNetworkUpdate: positionRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { selector: ".right-controller", component: "rotation", - requiresNetworkUpdate: rotationRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, { selector: ".right-controller", @@ -80,11 +86,11 @@ function registerNetworkSchemas() { components: [ { component: "position", - requiresNetworkUpdate: positionRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { component: "rotation", - requiresNetworkUpdate: rotationRequiresUpdate + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, "scale" ] diff --git a/webpack.config.js b/webpack.config.js index cd9c6fc47c04b5847dfc9073b1177e6b2cafa90e..e0b082e08f58abdfee4977a7029cec5f613463b2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -227,6 +227,18 @@ const config = { to: "hub-preview.png" } ]), + new CopyWebpackPlugin([ + { + from: "src/assets/avatars/bot-recording.json", + to: "assets/avatars/bot-recording.json" + } + ]), + new CopyWebpackPlugin([ + { + from: "src/assets/avatars/bot-recording.mp3", + to: "assets/avatars/bot-recording.mp3" + } + ]), // Extract required css and add a content hash. new ExtractTextPlugin({ filename: "assets/stylesheets/[name]-[contenthash].css", diff --git a/yarn.lock b/yarn.lock index 6cf9431ac4a939f35c72c2ee56ee83933c7227da..2c6a760c24ff95f22f87aad97650ff2ca9c63d27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1643,9 +1643,9 @@ buffer@^5.0.2: base64-js "^1.0.2" ieee754 "^1.1.4" -buffered-interpolation@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/buffered-interpolation/-/buffered-interpolation-0.2.3.tgz#6e723d44c4f4aa76704fc470654174e279591c31" +buffered-interpolation@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/buffered-interpolation/-/buffered-interpolation-0.2.4.tgz#74210ccb57855e611d1dbb97b4689a3585caa4af" builtin-modules@^1.0.0: version "1.1.1" @@ -3277,6 +3277,10 @@ fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + fast-diff@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" @@ -5515,11 +5519,12 @@ neo-async@^2.5.0: "networked-aframe@https://github.com/mozillareality/networked-aframe#mr-social-client/master": version "0.6.1" - resolved "https://github.com/mozillareality/networked-aframe#7b88e49e855b60e376886abe23ea311b27acdffe" + resolved "https://github.com/mozillareality/networked-aframe#06236f794f83cfebdc4ea9f3a9e8a5804f5bdcf9" dependencies: - buffered-interpolation "^0.2.3" + buffered-interpolation "^0.2.4" easyrtc "1.1.0" express "^4.10.7" + fast-deep-equal "^2.0.1" serve-static "^1.8.0" socket.io "^1.4.5" socket.io-client "^1.4.5"