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"