diff --git a/.travis.yml b/.travis.yml
index 76950271a..50ae66370 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,8 +28,8 @@ jobs:
       env: NPM_SCRIPT=build
       before_deploy:
       - npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
-      - git config --global user.email $(git log --pretty=format:"%ae" -n1)
-      - git config --global user.name $(git log --pretty=format:"%an" -n1)
+      - git config --global user.email "$(git log --pretty=format:"%ae" -n1)"
+      - git config --global user.name "$(git log --pretty=format:"%an" -n1)"
       deploy:
       - provider: npm
         on:
diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js
index a599b314c..16b041bf8 100644
--- a/src/blocks/scratch3_sound.js
+++ b/src/blocks/scratch3_sound.js
@@ -88,6 +88,7 @@ class Scratch3SoundBlocks {
         if (!soundState) {
             soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
             target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
+            target.soundEffects = soundState.effects;
         }
         return soundState;
     }
@@ -139,20 +140,19 @@ class Scratch3SoundBlocks {
     }
 
     playSound (args, util) {
-        const index = this._getSoundIndex(args.SOUND_MENU, util);
-        if (index >= 0) {
-            const soundId = util.target.sprite.sounds[index].soundId;
-            if (util.target.audioPlayer === null) return;
-            util.target.audioPlayer.playSound(soundId);
-        }
+        // Don't return the promise, it's the only difference for AndWait
+        this.playSoundAndWait(args, util);
     }
 
     playSoundAndWait (args, util) {
         const index = this._getSoundIndex(args.SOUND_MENU, util);
         if (index >= 0) {
-            const soundId = util.target.sprite.sounds[index].soundId;
-            if (util.target.audioPlayer === null) return;
-            return util.target.audioPlayer.playSound(soundId);
+            const {target} = util;
+            const {sprite} = target;
+            const {soundId} = sprite.sounds[index];
+            if (sprite.soundBank) {
+                return sprite.soundBank.playSound(target, soundId);
+            }
         }
     }
 
@@ -199,8 +199,9 @@ class Scratch3SoundBlocks {
     }
 
     _stopAllSoundsForTarget (target) {
-        if (target.audioPlayer === null) return;
-        target.audioPlayer.stopAllSounds();
+        if (target.sprite.soundBank) {
+            target.sprite.soundBank.stopAllSounds(target);
+        }
     }
 
     setEffect (args, util) {
@@ -224,23 +225,19 @@ class Scratch3SoundBlocks {
             soundState.effects[effect] = value;
         }
 
-        const effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect];
-        soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max);
-
-        if (util.target.audioPlayer === null) return;
-        util.target.audioPlayer.setEffect(effect, soundState.effects[effect]);
+        const {min, max} = Scratch3SoundBlocks.EFFECT_RANGE[effect];
+        soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], min, max);
 
+        this._syncEffectsForTarget(util.target);
         // Yield until the next tick.
         return Promise.resolve();
     }
 
     _syncEffectsForTarget (target) {
-        if (!target || !target.audioPlayer) return;
-        const soundState = this._getSoundState(target);
-        for (const effect in soundState.effects) {
-            if (!soundState.effects.hasOwnProperty(effect)) continue;
-            target.audioPlayer.setEffect(effect, soundState.effects[effect]);
-        }
+        if (!target || !target.sprite.soundBank) return;
+        target.soundEffects = this._getSoundState(target).effects;
+
+        target.sprite.soundBank.setEffects(target);
     }
 
     clearEffects (args, util) {
@@ -253,8 +250,7 @@ class Scratch3SoundBlocks {
             if (!soundState.effects.hasOwnProperty(effect)) continue;
             soundState.effects[effect] = 0;
         }
-        if (target.audioPlayer === null) return;
-        target.audioPlayer.clearEffects();
+        this._syncEffectsForTarget(target);
     }
 
     _clearEffectsForAllTargets () {
@@ -278,8 +274,7 @@ class Scratch3SoundBlocks {
     _updateVolume (volume, util) {
         volume = MathUtil.clamp(volume, 0, 100);
         util.target.volume = volume;
-        if (util.target.audioPlayer === null) return;
-        util.target.audioPlayer.setVolume(util.target.volume);
+        this._syncEffectsForTarget(util.target);
 
         // Yield until the next tick.
         return Promise.resolve();
diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index 13122c1f1..0a6750224 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -6,6 +6,7 @@ const Clone = require('../util/clone');
 const {Map} = require('immutable');
 const BlocksExecuteCache = require('./blocks-execute-cache');
 const log = require('../util/log');
+const Variable = require('./variable');
 
 /**
  * @fileoverview
@@ -670,6 +671,44 @@ class Blocks {
         this.resetCache();
     }
 
+    /**
+     * Returns a map of all references to variables or lists from blocks
+     * in this block container.
+     * @return {object} A map of variable ID to a list of all variable references
+     * for that ID. A variable reference contains the field referencing that variable
+     * and also the type of the variable being referenced.
+     */
+    getAllVariableAndListReferences () {
+        const blocks = this._blocks;
+        const allReferences = Object.create(null);
+        for (const blockId in blocks) {
+            let varOrListField = null;
+            let varType = null;
+            if (blocks[blockId].fields.VARIABLE) {
+                varOrListField = blocks[blockId].fields.VARIABLE;
+                varType = Variable.SCALAR_TYPE;
+            } else if (blocks[blockId].fields.LIST) {
+                varOrListField = blocks[blockId].fields.LIST;
+                varType = Variable.LIST_TYPE;
+            }
+            if (varOrListField) {
+                const currVarId = varOrListField.id;
+                if (allReferences[currVarId]) {
+                    allReferences[currVarId].push({
+                        referencingField: varOrListField,
+                        type: varType
+                    });
+                } else {
+                    allReferences[currVarId] = [{
+                        referencingField: varOrListField,
+                        type: varType
+                    }];
+                }
+            }
+        }
+        return allReferences;
+    }
+
     /**
      * Keep blocks up to date after a variable gets renamed.
      * @param {string} varId The id of the variable that was renamed
diff --git a/src/engine/target.js b/src/engine/target.js
index a8c6828ee..69620c91b 100644
--- a/src/engine/target.js
+++ b/src/engine/target.js
@@ -6,6 +6,7 @@ const Comment = require('../engine/comment');
 const uid = require('../util/uid');
 const {Map} = require('immutable');
 const log = require('../util/log');
+const StringUtil = require('../util/string-util');
 
 /**
  * @fileoverview
@@ -80,14 +81,40 @@ class Target extends EventEmitter {
     }
 
     /**
-     * Look up a variable object, and create it if one doesn't exist.
+     * Get the names of all the variables of the given type that are in scope for this target.
+     * For targets that are not the stage, this includes any target-specific
+     * variables as well as any stage variables.
+     * For the stage, this is all stage variables.
+     * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE
+     * @return {Array<string>} A list of variable names
+     */
+    getAllVariableNamesInScopeByType (type) {
+        if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
+        const targetVariables = Object.values(this.variables)
+            .filter(v => v.type === type)
+            .map(variable => variable.name);
+        if (this.isStage || !this.runtime) {
+            return targetVariables;
+        }
+        const stage = this.runtime.getTargetForStage();
+        const stageVariables = stage.getAllVariableNamesInScopeByType(type);
+        return targetVariables.concat(stageVariables);
+    }
+
+    /**
+     * Look up a variable object, first by id, and then by name if the id is not found.
+     * Create a new variable if both lookups fail.
      * @param {string} id Id of the variable.
      * @param {string} name Name of the variable.
      * @return {!Variable} Variable object.
      */
     lookupOrCreateVariable (id, name) {
-        const variable = this.lookupVariableById(id);
+        let variable = this.lookupVariableById(id);
         if (variable) return variable;
+
+        variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE);
+        if (variable) return variable;
+
         // No variable with this name exists - create it locally.
         const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
         this.variables[id] = newVariable;
@@ -161,6 +188,40 @@ class Target extends EventEmitter {
         }
     }
 
+    /**
+     * Look up a variable object by its name and variable type.
+     * Search begins with local variables; then global variables if a local one
+     * was not found.
+     * @param {string} name Name of the variable.
+     * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE.
+     * @return {?Variable} Variable object if found, or null if not.
+     */
+    lookupVariableByNameAndType (name, type) {
+        if (typeof name !== 'string') return;
+        if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
+
+        for (const varId in this.variables) {
+            const currVar = this.variables[varId];
+            if (currVar.name === name && currVar.type === type) {
+                return currVar;
+            }
+        }
+
+        if (this.runtime && !this.isStage) {
+            const stage = this.runtime.getTargetForStage();
+            if (stage) {
+                for (const varId in stage.variables) {
+                    const currVar = stage.variables[varId];
+                    if (currVar.name === name && currVar.type === type) {
+                        return currVar;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
     /**
     * Look up a list object for this target, and create it if one doesn't exist.
     * Search begins for local lists; then look for globals.
@@ -169,8 +230,12 @@ class Target extends EventEmitter {
     * @return {!Varible} Variable object representing the found/created list.
      */
     lookupOrCreateList (id, name) {
-        const list = this.lookupVariableById(id);
+        let list = this.lookupVariableById(id);
         if (list) return list;
+
+        list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE);
+        if (list) return list;
+
         // No variable with this name exists - create it locally.
         const newList = new Variable(id, name, Variable.LIST_TYPE, false);
         this.variables[id] = newList;
@@ -240,10 +305,13 @@ class Target extends EventEmitter {
                         name: 'VARIABLE',
                         value: id
                     }, this.runtime);
-                    this.runtime.requestUpdateMonitor(Map({
-                        id: id,
-                        params: blocks._getBlockParams(blocks.getBlock(variable.id))
-                    }));
+                    const monitorBlock = blocks.getBlock(variable.id);
+                    if (monitorBlock) {
+                        this.runtime.requestUpdateMonitor(Map({
+                            id: id,
+                            params: blocks._getBlockParams(monitorBlock)
+                        }));
+                    }
                 }
 
             }
@@ -264,6 +332,101 @@ class Target extends EventEmitter {
         }
     }
 
+    /**
+     * Fixes up variable references in this target avoiding conflicts with
+     * pre-existing variables in the same scope.
+     * This is used when uploading this target as a new sprite into an existing
+     * project, where the new sprite may contain references
+     * to variable names that already exist as global variables in the project
+     * (and thus are in scope for variable references in the given sprite).
+     *
+     * If the given target has a block that references an existing global variable and that
+     * variable *does not* exist in the target itself (e.g. it was a global variable in the
+     * project the sprite was originally exported from), fix the variable references in this sprite
+     * to reference the id of the pre-existing global variable.
+     * If the given target has a block that references an existing global variable and that
+     * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
+     * then the variable is renamed to distinguish itself from the pre-existing variable.
+     * All blocks that reference the local variable will be updated to use the new name.
+     */
+    fixUpVariableReferences () {
+        if (!this.runtime) return; // There's no runtime context to conflict with
+        if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
+        const stage = this.runtime.getTargetForStage();
+        if (!stage || !stage.variables) return;
+
+        const renameConflictingLocalVar = (id, name, type) => {
+            const conflict = stage.lookupVariableByNameAndType(name, type);
+            if (conflict) {
+                const newName = StringUtil.unusedName(
+                    `${this.getName()}: ${name}`,
+                    this.getAllVariableNamesInScopeByType(type));
+                this.renameVariable(id, newName);
+                return newName;
+            }
+            return null;
+        };
+
+        const allReferences = this.blocks.getAllVariableAndListReferences();
+        const unreferencedLocalVarIds = [];
+        if (Object.keys(this.variables).length > 0) {
+            for (const localVarId in this.variables) {
+                if (!this.variables.hasOwnProperty(localVarId)) continue;
+                if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
+            }
+        }
+        const conflictIdsToReplace = Object.create(null);
+        for (const varId in allReferences) {
+            // We don't care about which var ref we get, they should all have the same var info
+            const varRef = allReferences[varId][0];
+            const varName = varRef.referencingField.value;
+            const varType = varRef.type;
+            if (this.lookupVariableById(varId)) {
+                // Found a variable with the id in either the target or the stage,
+                // figure out which one.
+                if (this.variables.hasOwnProperty(varId)) {
+                    // If the target has the variable, then check whether the stage
+                    // has one with the same name and type. If it does, then rename
+                    // this target specific variable so that there is a distinction.
+                    const newVarName = renameConflictingLocalVar(varId, varName, varType);
+
+                    if (newVarName) {
+                        // We are not calling this.blocks.updateBlocksAfterVarRename
+                        // here because it will search through all the blocks. We already
+                        // have access to all the references for this var id.
+                        allReferences[varId].map(ref => {
+                            ref.referencingField.value = newVarName;
+                            return ref;
+                        });
+                    }
+                }
+            } else {
+                const existingVar = this.lookupVariableByNameAndType(varName, varType);
+                if (existingVar && !conflictIdsToReplace[varId]) {
+                    conflictIdsToReplace[varId] = existingVar.id;
+                }
+            }
+        }
+        // Rename any local variables that were missed above because they aren't
+        // referenced by any blocks
+        for (const id in unreferencedLocalVarIds) {
+            const varId = unreferencedLocalVarIds[id];
+            const name = this.variables[varId].name;
+            const type = this.variables[varId].type;
+            renameConflictingLocalVar(varId, name, type);
+        }
+        // Finally, handle global var conflicts (e.g. a sprite is uploaded, and has
+        // blocks referencing some variable that the sprite does not own, and this
+        // variable conflicts with a global var)
+        for (const conflictId in conflictIdsToReplace) {
+            const existingId = conflictIdsToReplace[conflictId];
+            allReferences[conflictId].map(varRef => {
+                varRef.referencingField.id = existingId;
+                return varRef;
+            });
+        }
+    }
+
     /**
      * Post/edit sprite info.
      * @param {object} data An object with sprite info data to set.
diff --git a/src/extensions/scratch3_music/index.js b/src/extensions/scratch3_music/index.js
index 4450a69e8..1ef4d6942 100644
--- a/src/extensions/scratch3_music/index.js
+++ b/src/extensions/scratch3_music/index.js
@@ -52,18 +52,25 @@ class Scratch3MusicBlocks {
         this._concurrencyCounter = 0;
 
         /**
-         * An array of audio buffers, one for each drum sound.
+         * An array of sound players, one for each drum sound.
          * @type {Array}
          * @private
          */
-        this._drumBuffers = [];
+        this._drumPlayers = [];
 
         /**
-         * An array of arrays of audio buffers. Each instrument has one or more audio buffers.
+         * An array of arrays of sound players. Each instrument has one or more audio players.
          * @type {Array[]}
          * @private
          */
-        this._instrumentBufferArrays = [];
+        this._instrumentPlayerArrays = [];
+
+        /**
+         * An array of arrays of sound players. Each instrument mya have an audio player for each playable note.
+         * @type {Array[]}
+         * @private
+         */
+        this._instrumentPlayerNoteArrays = [];
 
         /**
          * An array of audio bufferSourceNodes. Each time you play an instrument or drum sound,
@@ -87,14 +94,15 @@ class Scratch3MusicBlocks {
         const loadingPromises = [];
         this.DRUM_INFO.forEach((drumInfo, index) => {
             const filePath = `drums/${drumInfo.fileName}`;
-            const promise = this._storeSound(filePath, index, this._drumBuffers);
+            const promise = this._storeSound(filePath, index, this._drumPlayers);
             loadingPromises.push(promise);
         });
         this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => {
-            this._instrumentBufferArrays[instrumentIndex] = [];
+            this._instrumentPlayerArrays[instrumentIndex] = [];
+            this._instrumentPlayerNoteArrays[instrumentIndex] = [];
             instrumentInfo.samples.forEach((sample, noteIndex) => {
                 const filePath = `instruments/${instrumentInfo.dirName}/${sample}`;
-                const promise = this._storeSound(filePath, noteIndex, this._instrumentBufferArrays[instrumentIndex]);
+                const promise = this._storeSound(filePath, noteIndex, this._instrumentPlayerArrays[instrumentIndex]);
                 loadingPromises.push(promise);
             });
         });
@@ -104,22 +112,22 @@ class Scratch3MusicBlocks {
     }
 
     /**
-     * Decode a sound and store the buffer in an array.
+     * Decode a sound and store the player in an array.
      * @param {string} filePath - the audio file name.
-     * @param {number} index - the index at which to store the audio buffer.
-     * @param {array} bufferArray - the array of buffers in which to store it.
+     * @param {number} index - the index at which to store the audio player.
+     * @param {array} playerArray - the array of players in which to store it.
      * @return {Promise} - a promise which will resolve once the sound has been stored.
      */
-    _storeSound (filePath, index, bufferArray) {
+    _storeSound (filePath, index, playerArray) {
         const fullPath = `${filePath}.mp3`;
 
         if (!assetData[fullPath]) return;
 
-        // The sound buffer has already been downloaded via the manifest file required above.
+        // The sound player has already been downloaded via the manifest file required above.
         const soundBuffer = assetData[fullPath];
 
-        return this._decodeSound(soundBuffer).then(buffer => {
-            bufferArray[index] = buffer;
+        return this._decodeSound(soundBuffer).then(player => {
+            playerArray[index] = player;
         });
     }
 
@@ -129,24 +137,14 @@ class Scratch3MusicBlocks {
      * @return {Promise} - a promise which will resolve once the sound has decoded.
      */
     _decodeSound (soundBuffer) {
-        const context = this.runtime.audioEngine && this.runtime.audioEngine.audioContext;
+        const engine = this.runtime.audioEngine;
 
-        if (!context) {
+        if (!engine) {
             return Promise.reject(new Error('No Audio Context Detected'));
         }
 
         // Check for newer promise-based API
-        if (context.decodeAudioData.length === 1) {
-            return context.decodeAudioData(soundBuffer);
-        } else { // eslint-disable-line no-else-return
-            // Fall back to callback API
-            return new Promise((resolve, reject) =>
-                context.decodeAudioData(soundBuffer,
-                    buffer => resolve(buffer),
-                    error => reject(error)
-                )
-            );
-        }
+        return engine.decodeSoundPlayer({data: {buffer: soundBuffer}});
     }
 
     /**
@@ -623,7 +621,11 @@ class Scratch3MusicBlocks {
     getInfo () {
         return {
             id: 'music',
-            name: 'Music',
+            name: formatMessage({
+                id: 'music.categoryName',
+                default: 'Music',
+                description: 'Label for the Music extension category'
+            }),
             menuIconURI: menuIconURI,
             blockIconURI: blockIconURI,
             blocks: [
@@ -774,26 +776,34 @@ class Scratch3MusicBlocks {
      */
     _playDrumNum (util, drumNum) {
         if (util.runtime.audioEngine === null) return;
-        if (util.target.audioPlayer === null) return;
+        if (util.target.sprite.soundBank === null) return;
         // If we're playing too many sounds, do not play the drum sound.
         if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
             return;
         }
-        const outputNode = util.target.audioPlayer.getInputNode();
-        const context = util.runtime.audioEngine.audioContext;
-        const bufferSource = context.createBufferSource();
-        bufferSource.buffer = this._drumBuffers[drumNum];
-        bufferSource.connect(outputNode);
-        bufferSource.start();
 
-        const bufferSourceIndex = this._bufferSources.length;
-        this._bufferSources.push(bufferSource);
+        const player = this._drumPlayers[drumNum];
+
+        if (typeof player === 'undefined') return;
+
+        if (player.isPlaying && !player.isStarting) {
+            // Take the internal player state and create a new player with it.
+            // `.play` does this internally but then instructs the sound to
+            // stop.
+            player.take();
+        }
+
+        const engine = util.runtime.audioEngine;
+        const chain = engine.createEffectChain();
+        chain.setEffectsFromTarget(util.target);
+        player.connect(chain);
 
         this._concurrencyCounter++;
-        bufferSource.onended = () => {
+        player.once('stop', () => {
             this._concurrencyCounter--;
-            delete this._bufferSources[bufferSourceIndex];
-        };
+        });
+
+        player.play();
     }
 
     /**
@@ -852,7 +862,7 @@ class Scratch3MusicBlocks {
      */
     _playNote (util, note, durationSec) {
         if (util.runtime.audioEngine === null) return;
-        if (util.target.audioPlayer === null) return;
+        if (util.target.sprite.soundBank === null) return;
 
         // If we're playing too many sounds, do not play the note.
         if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
@@ -867,28 +877,37 @@ class Scratch3MusicBlocks {
         const sampleIndex = this._selectSampleIndexForNote(note, sampleArray);
 
         // If the audio sample has not loaded yet, bail out
-        if (typeof this._instrumentBufferArrays[inst] === 'undefined') return;
-        if (typeof this._instrumentBufferArrays[inst][sampleIndex] === 'undefined') return;
+        if (typeof this._instrumentPlayerArrays[inst] === 'undefined') return;
+        if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return;
 
-        // Create the audio buffer to play the note, and set its pitch
-        const context = util.runtime.audioEngine.audioContext;
-        const bufferSource = context.createBufferSource();
+        // Fetch the sound player to play the note.
+        const engine = util.runtime.audioEngine;
 
-        const bufferSourceIndex = this._bufferSources.length;
-        this._bufferSources.push(bufferSource);
+        if (!this._instrumentPlayerNoteArrays[inst][note]) {
+            this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take();
+        }
 
-        bufferSource.buffer = this._instrumentBufferArrays[inst][sampleIndex];
+        const player = this._instrumentPlayerNoteArrays[inst][note];
+
+        if (player.isPlaying && !player.isStarting) {
+            // Take the internal player state and create a new player with it.
+            // `.play` does this internally but then instructs the sound to
+            // stop.
+            player.take();
+        }
+
+        const chain = engine.createEffectChain();
+        chain.setEffectsFromTarget(util.target);
+
+        // Set its pitch.
         const sampleNote = sampleArray[sampleIndex];
-        bufferSource.playbackRate.value = this._ratioForPitchInterval(note - sampleNote);
+        const notePitchInterval = this._ratioForPitchInterval(note - sampleNote);
 
-        // Create a gain node for this note, and connect it to the sprite's audioPlayer.
-        const gainNode = context.createGain();
-        bufferSource.connect(gainNode);
-        const outputNode = util.target.audioPlayer.getInputNode();
-        gainNode.connect(outputNode);
-
-        // Start playing the note
-        bufferSource.start();
+        // Create a gain node for this note, and connect it to the sprite's
+        // simulated effectChain.
+        const context = engine.audioContext;
+        const releaseGain = context.createGain();
+        releaseGain.connect(chain.getInputNode());
 
         // Schedule the release of the note, ramping its gain down to zero,
         // and then stopping the sound.
@@ -898,16 +917,24 @@ class Scratch3MusicBlocks {
         }
         const releaseStart = context.currentTime + durationSec;
         const releaseEnd = releaseStart + releaseDuration;
-        gainNode.gain.setValueAtTime(1, releaseStart);
-        gainNode.gain.linearRampToValueAtTime(0.0001, releaseEnd);
-        bufferSource.stop(releaseEnd);
+        releaseGain.gain.setValueAtTime(1, releaseStart);
+        releaseGain.gain.linearRampToValueAtTime(0.0001, releaseEnd);
 
-        // Update the concurrency counter
         this._concurrencyCounter++;
-        bufferSource.onended = () => {
+        player.once('stop', () => {
             this._concurrencyCounter--;
-            delete this._bufferSources[bufferSourceIndex];
-        };
+        });
+
+        // Start playing the note
+        player.play();
+        // Connect the player to the gain node.
+        player.connect({getInputNode () {
+            return releaseGain;
+        }});
+        // Set playback now after play creates the outputNode.
+        player.outputNode.playbackRate.value = notePitchInterval;
+        // Schedule playback to stop.
+        player.outputNode.stop(releaseEnd);
     }
 
     /**
diff --git a/src/extensions/scratch3_pen/index.js b/src/extensions/scratch3_pen/index.js
index e4211b328..4c46ce370 100644
--- a/src/extensions/scratch3_pen/index.js
+++ b/src/extensions/scratch3_pen/index.js
@@ -282,7 +282,11 @@ class Scratch3PenBlocks {
     getInfo () {
         return {
             id: 'pen',
-            name: 'Pen',
+            name: formatMessage({
+                id: 'pen.categoryName',
+                default: 'Pen',
+                description: 'Label for the pen extension category'
+            }),
             blockIconURI: blockIconURI,
             blocks: [
                 {
diff --git a/src/extensions/scratch3_translate/index.js b/src/extensions/scratch3_translate/index.js
index 1d0c2ec05..787b8545f 100644
--- a/src/extensions/scratch3_translate/index.js
+++ b/src/extensions/scratch3_translate/index.js
@@ -86,7 +86,11 @@ class Scratch3TranslateBlocks {
     getInfo () {
         return {
             id: 'translate',
-            name: 'Translate',
+            name: formatMessage({
+                id: 'translate.categoryName',
+                default: 'Translate',
+                description: 'Label for the translate extension category'
+            }),
             menuIconURI: '', // TODO: Add the final icons.
             blockIconURI: '',
             blocks: [
diff --git a/src/extensions/scratch3_video_sensing/index.js b/src/extensions/scratch3_video_sensing/index.js
index c6e29f85b..afe8f44a1 100644
--- a/src/extensions/scratch3_video_sensing/index.js
+++ b/src/extensions/scratch3_video_sensing/index.js
@@ -372,7 +372,11 @@ class Scratch3VideoSensingBlocks {
         // Return extension definition
         return {
             id: 'videoSensing',
-            name: 'Video Motion',
+            name: formatMessage({
+                id: 'videoSensing.categoryName',
+                default: 'Video Motion',
+                description: 'Label for the video motion extension category'
+            }),
             blocks: [
                 {
                     // @todo this hat needs to be set itself to restart existing
diff --git a/src/import/load-sound.js b/src/import/load-sound.js
index e13db14d2..8f4f7a1a6 100644
--- a/src/import/load-sound.js
+++ b/src/import/load-sound.js
@@ -8,27 +8,32 @@ const log = require('../util/log');
  * @property {Buffer} data - sound data will be written here once loaded.
  * @param {!Asset} soundAsset - the asset loaded from storage.
  * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
+ * @param {Sprite} sprite - Scratch sprite to add sounds to.
  * @returns {!Promise} - a promise which will resolve to the sound when ready.
  */
-const loadSoundFromAsset = function (sound, soundAsset, runtime) {
+const loadSoundFromAsset = function (sound, soundAsset, runtime, sprite) {
     sound.assetId = soundAsset.assetId;
     if (!runtime.audioEngine) {
         log.error('No audio engine present; cannot load sound asset: ', sound.md5);
         return Promise.resolve(sound);
     }
-    return runtime.audioEngine.decodeSound(Object.assign(
+    return runtime.audioEngine.decodeSoundPlayer(Object.assign(
         {},
         sound,
         {data: soundAsset.data}
-    )).then(soundId => {
-        sound.soundId = soundId;
+    )).then(soundPlayer => {
+        sound.soundId = soundPlayer.id;
         // Set the sound sample rate and sample count based on the
         // the audio buffer from the audio engine since the sound
         // gets resampled by the audio engine
-        const soundBuffer = runtime.audioEngine.getSoundBuffer(soundId);
+        const soundBuffer = soundPlayer.buffer;
         sound.rate = soundBuffer.sampleRate;
         sound.sampleCount = soundBuffer.length;
 
+        if (sprite.soundBank !== null) {
+            sprite.soundBank.addSoundPlayer(soundPlayer);
+        }
+
         return sound;
     });
 };
@@ -39,9 +44,10 @@ const loadSoundFromAsset = function (sound, soundAsset, runtime) {
  * @property {string} md5 - the MD5 and extension of the sound to be loaded.
  * @property {Buffer} data - sound data will be written here once loaded.
  * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
+ * @param {Sprite} sprite - Scratch sprite to add sounds to.
  * @returns {!Promise} - a promise which will resolve to the sound when ready.
  */
-const loadSound = function (sound, runtime) {
+const loadSound = function (sound, runtime, sprite) {
     if (!runtime.storage) {
         log.error('No storage module present; cannot load sound asset: ', sound.md5);
         return Promise.resolve(sound);
@@ -52,7 +58,7 @@ const loadSound = function (sound, runtime) {
     return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
         .then(soundAsset => {
             sound.dataFormat = ext;
-            return loadSoundFromAsset(sound, soundAsset, runtime);
+            return loadSoundFromAsset(sound, soundAsset, runtime, sprite);
         });
 };
 
diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js
index 9ca378d90..ffde8220a 100644
--- a/src/serialization/sb2.js
+++ b/src/serialization/sb2.js
@@ -420,7 +420,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
             // followed by the file ext
             const assetFileName = `${soundSource.soundID}.${ext}`;
             soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName)
-                .then(() => loadSound(sound, runtime)));
+                .then(() => loadSound(sound, runtime, sprite)));
         }
     }
 
diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js
index 99c6ec226..16db421fe 100644
--- a/src/serialization/sb3.js
+++ b/src/serialization/sb3.js
@@ -460,17 +460,27 @@ const serializeTarget = function (target, extensions) {
 
 /**
  * Serializes the specified VM runtime.
- * @param  {!Runtime} runtime VM runtime instance to be serialized.
+ * @param {!Runtime} runtime VM runtime instance to be serialized.
+ * @param {string=} targetId Optional target id if serializing only a single target
  * @return {object} Serialized runtime instance.
  */
-const serialize = function (runtime) {
+const serialize = function (runtime, targetId) {
     // Fetch targets
     const obj = Object.create(null);
     // Create extension set to hold extension ids found while serializing targets
     const extensions = new Set();
-    const flattenedOriginalTargets = JSON.parse(JSON.stringify(
+    const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ?
+        [runtime.getTargetById(targetId)] :
         runtime.targets.filter(target => target.isOriginal)));
-    obj.targets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
+
+    const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
+
+    if (targetId) {
+        return serializedTargets[0];
+    }
+
+    obj.targets = serializedTargets;
+
 
     // TODO Serialize monitors
 
@@ -819,7 +829,7 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
         // any translation that needs to happen will happen in the process
         // of building up the costume object into an sb3 format
         return deserializeSound(sound, runtime, zip)
-            .then(() => loadSound(sound, runtime));
+            .then(() => loadSound(sound, runtime, sprite));
         // Only attempt to load the sound after the deserialization
         // process has been completed.
     });
@@ -945,10 +955,11 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
     return Promise.all(
         ((isSingleSprite ? [json] : json.targets) || []).map(target =>
             parseScratchObject(target, runtime, extensions, zip))
-    ).then(targets => ({
-        targets,
-        extensions
-    }));
+    )
+        .then(targets => ({
+            targets,
+            extensions
+        }));
 };
 
 module.exports = {
diff --git a/src/serialization/serialize-assets.js b/src/serialization/serialize-assets.js
index 9dc7bf0d9..ac444f11a 100644
--- a/src/serialization/serialize-assets.js
+++ b/src/serialization/serialize-assets.js
@@ -5,10 +5,11 @@
  * to be written and the contents of the file, the serialized asset.
  * @param {Runtime} runtime The runtime with the assets to be serialized
  * @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes'
+ * @param {string=} optTargetId Optional target id to serialize assets for
  * @returns {Array<object>} An array of file descriptors for each asset
  */
-const serializeAssets = function (runtime, assetType) {
-    const targets = runtime.targets;
+const serializeAssets = function (runtime, assetType, optTargetId) {
+    const targets = optTargetId ? [runtime.getTargetById(optTargetId)] : runtime.targets;
     const assetDescs = [];
     for (let i = 0; i < targets.length; i++) {
         const currTarget = targets[i];
@@ -27,14 +28,16 @@ const serializeAssets = function (runtime, assetType) {
 };
 
 /**
- * Serialize all the sounds in the provided runtime into an array of file
- * descriptors. A file descriptor is an object containing the name of the file
+ * Serialize all the sounds in the provided runtime or, if a target id is provided,
+ * in the specified target into an array of file descriptors.
+ * A file descriptor is an object containing the name of the file
  * to be written and the contents of the file, the serialized sound.
  * @param {Runtime} runtime The runtime with the sounds to be serialized
+ * @param {string=} optTargetId Optional targetid for serializing sounds of a single target
  * @returns {Array<object>} An array of file descriptors for each sound
  */
-const serializeSounds = function (runtime) {
-    return serializeAssets(runtime, 'sounds');
+const serializeSounds = function (runtime, optTargetId) {
+    return serializeAssets(runtime, 'sounds', optTargetId);
 };
 
 /**
@@ -42,10 +45,11 @@ const serializeSounds = function (runtime) {
  * descriptors. A file descriptor is an object containing the name of the file
  * to be written and the contents of the file, the serialized costume.
  * @param {Runtime} runtime The runtime with the costumes to be serialized
+ * @param {string} optTargetId Optional targetid for serializing costumes of a single target
  * @returns {Array<object>} An array of file descriptors for each costume
  */
-const serializeCostumes = function (runtime) {
-    return serializeAssets(runtime, 'costumes');
+const serializeCostumes = function (runtime, optTargetId) {
+    return serializeAssets(runtime, 'costumes', optTargetId);
 };
 
 module.exports = {
diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js
index 632374384..eb996786b 100644
--- a/src/sprites/rendered-target.js
+++ b/src/sprites/rendered-target.js
@@ -170,21 +170,30 @@ class RenderedTarget extends Target {
         }
     }
 
+    get audioPlayer () {
+        /* eslint-disable no-console */
+        console.warn('get audioPlayer deprecated, please update to use .sprite.soundBank methods');
+        console.warn(new Error('stack for debug').stack);
+        /* eslint-enable no-console */
+        const bank = this.sprite.soundBank;
+        const audioPlayerProxy = {
+            playSound: soundId => bank.play(this, soundId)
+        };
+
+        Object.defineProperty(this, 'audioPlayer', {
+            configurable: false,
+            enumerable: true,
+            writable: false,
+            value: audioPlayerProxy
+        });
+
+        return audioPlayerProxy;
+    }
+
     /**
      * Initialize the audio player for this sprite or clone.
      */
     initAudio () {
-        this.audioPlayer = null;
-        if (this.runtime && this.runtime.audioEngine) {
-            this.audioPlayer = this.runtime.audioEngine.createPlayer();
-            // If this is a clone, it gets a reference to its parent's activeSoundPlayers object.
-            if (!this.isOriginal) {
-                const parent = this.sprite.clones[0];
-                if (parent && parent.audioPlayer) {
-                    this.audioPlayer.activeSoundPlayers = parent.audioPlayer.activeSoundPlayers;
-                }
-            }
-        }
     }
 
     /**
@@ -1004,8 +1013,8 @@ class RenderedTarget extends Target {
             const newTarget = newSprite.createClone();
             // Copy all properties.
             // @todo refactor with clone methods
-            newTarget.x = Math.random() * 400 / 2;
-            newTarget.y = Math.random() * 300 / 2;
+            newTarget.x = (Math.random() - 0.5) * 400 / 2;
+            newTarget.y = (Math.random() - 0.5) * 300 / 2;
             newTarget.direction = this.direction;
             newTarget.draggable = this.draggable;
             newTarget.visible = this.visible;
@@ -1034,9 +1043,8 @@ class RenderedTarget extends Target {
      */
     onStopAll () {
         this.clearEffects();
-        if (this.audioPlayer) {
-            this.audioPlayer.stopAllSounds();
-            this.audioPlayer.clearEffects();
+        if (this.sprite.soundBank) {
+            this.sprite.soundBank.stopAllSounds();
         }
     }
 
@@ -1132,10 +1140,6 @@ class RenderedTarget extends Target {
                 this.runtime.requestRedraw();
             }
         }
-        if (this.audioPlayer) {
-            this.audioPlayer.stopAllSounds();
-            this.audioPlayer.dispose();
-        }
     }
 }
 
diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js
index de735ecc3..bc61960a0 100644
--- a/src/sprites/sprite.js
+++ b/src/sprites/sprite.js
@@ -8,7 +8,8 @@ const StageLayering = require('../engine/stage-layering');
 class Sprite {
     /**
      * Sprite to be used on the Scratch stage.
-     * All clones of a sprite have shared blocks, shared costumes, shared variables.
+     * All clones of a sprite have shared blocks, shared costumes, shared variables,
+     * shared sounds, etc.
      * @param {?Blocks} blocks Shared blocks object for all clones of sprite.
      * @param {Runtime} runtime Reference to the runtime.
      * @constructor
@@ -47,6 +48,11 @@ class Sprite {
          * @type {Array.<!RenderedTarget>}
          */
         this.clones = [];
+
+        this.soundBank = null;
+        if (this.runtime && this.runtime.audioEngine) {
+            this.soundBank = this.runtime.audioEngine.createBank();
+        }
     }
 
     /**
@@ -149,12 +155,18 @@ class Sprite {
         newSprite.sounds = this.sounds.map(sound => {
             const newSound = Object.assign({}, sound);
             const soundAsset = this.runtime.storage.get(sound.assetId);
-            assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime));
+            assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, this));
             return newSound;
         });
 
         return Promise.all(assetPromises).then(() => newSprite);
     }
+
+    dispose () {
+        if (this.soundBank) {
+            this.soundBank.dispose();
+        }
+    }
 }
 
 module.exports = Sprite;
diff --git a/src/virtual-machine.js b/src/virtual-machine.js
index 767641eec..940991e49 100644
--- a/src/virtual-machine.js
+++ b/src/virtual-machine.js
@@ -263,14 +263,7 @@ class VirtualMachine extends EventEmitter {
 
         // Put everything in a zip file
         zip.file('project.json', projectJson);
-        for (let i = 0; i < soundDescs.length; i++) {
-            const currSound = soundDescs[i];
-            zip.file(currSound.fileName, currSound.fileContent);
-        }
-        for (let i = 0; i < costumeDescs.length; i++) {
-            const currCostume = costumeDescs[i];
-            zip.file(currCostume.fileName, currCostume.fileContent);
-        }
+        this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
 
         return zip.generateAsync({
             type: 'blob',
@@ -281,6 +274,43 @@ class VirtualMachine extends EventEmitter {
         });
     }
 
+    _addFileDescsToZip (fileDescs, zip) {
+        for (let i = 0; i < fileDescs.length; i++) {
+            const currFileDesc = fileDescs[i];
+            zip.file(currFileDesc.fileName, currFileDesc.fileContent);
+        }
+    }
+
+    /**
+     * Exports a sprite in the sprite3 format.
+     * @param {string} targetId ID of the target to export
+     * @param {string=} optZipType Optional type that the resulting
+     * zip should be outputted in. Options are: base64, binarystring,
+     * array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to
+     * blob if argument not provided.
+     * See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option
+     * for more information about these options.
+     * @return {object} A generated zip of the sprite and its assets in the format
+     * specified by optZipType or blob by default.
+     */
+    exportSprite (targetId, optZipType) {
+        const soundDescs = serializeSounds(this.runtime, targetId);
+        const costumeDescs = serializeCostumes(this.runtime, targetId);
+        const spriteJson = JSON.stringify(sb3.serialize(this.runtime, targetId));
+
+        const zip = new JSZip();
+        zip.file('sprite.json', spriteJson);
+        this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
+
+        return zip.generateAsync({
+            type: typeof optZipType === 'string' ? optZipType : 'blob',
+            compression: 'DEFLATE',
+            compressionOptions: {
+                level: 6
+            }
+        });
+    }
+
     /**
      * Export project as a Scratch 3.0 JSON representation.
      * @return {string} Serialized state of the runtime.
@@ -368,6 +398,10 @@ class VirtualMachine extends EventEmitter {
                 this.editingTarget = targets[0];
             }
 
+            if (!wholeProject) {
+                this.editingTarget.fixUpVariableReferences();
+            }
+
             // Update the VM user's knowledge of targets and blocks on the workspace.
             this.emitTargetsUpdate();
             this.emitWorkspaceUpdate();
@@ -495,7 +529,7 @@ class VirtualMachine extends EventEmitter {
     duplicateSound (soundIndex) {
         const originalSound = this.editingTarget.getSounds()[soundIndex];
         const clone = Object.assign({}, originalSound);
-        return loadSound(clone, this.runtime).then(() => {
+        return loadSound(clone, this.runtime, this.editingTarget.sprite).then(() => {
             this.editingTarget.addSound(clone, soundIndex + 1);
             this.emitTargetsUpdate();
         });
@@ -525,7 +559,7 @@ class VirtualMachine extends EventEmitter {
      * @returns {?Promise} - a promise that resolves when the sound has been decoded and added
      */
     addSound (soundObject) {
-        return loadSound(soundObject, this.runtime).then(() => {
+        return loadSound(soundObject, this.runtime, this.editingTarget.sprite).then(() => {
             this.editingTarget.addSound(soundObject);
             this.emitTargetsUpdate();
         });
@@ -549,7 +583,7 @@ class VirtualMachine extends EventEmitter {
     getSoundBuffer (soundIndex) {
         const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
         if (id && this.runtime && this.runtime.audioEngine) {
-            return this.runtime.audioEngine.getSoundBuffer(id);
+            return this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer;
         }
         return null;
     }
@@ -564,7 +598,7 @@ class VirtualMachine extends EventEmitter {
         const sound = this.editingTarget.sprite.sounds[soundIndex];
         const id = sound ? sound.soundId : null;
         if (id && this.runtime && this.runtime.audioEngine) {
-            this.runtime.audioEngine.updateSoundBuffer(id, newBuffer);
+            this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer = newBuffer;
         }
         // Update sound in runtime
         if (soundEncoding) {
@@ -966,8 +1000,8 @@ class VirtualMachine extends EventEmitter {
     shareSoundToTarget (soundIndex, targetId) {
         const originalSound = this.editingTarget.getSounds()[soundIndex];
         const clone = Object.assign({}, originalSound);
-        return loadSound(clone, this.runtime).then(() => {
-            const target = this.runtime.getTargetById(targetId);
+        const target = this.runtime.getTargetById(targetId);
+        return loadSound(clone, this.runtime, target.sprite).then(() => {
             if (target) {
                 target.addSound(clone);
                 this.emitTargetsUpdate();
diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index a5a126f07..dbddd2777 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -90,5 +90,17 @@
         "type": "comment_create",
         "commentId": "a comment",
         "xy": {"x": 10, "y": 20}
+    },
+    "mockVariableBlock": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type='data_variable' id='a block' x='0' y='0'><field name='VARIABLE' id='mock var id' variabletype=''>a mock variable</field></block>"
+        }
+    },
+    "mockListBlock": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type='data_listcontents' id='another block' x='0' y='0'><field name='LIST' id='mock list id' variabletype=''>a mock list</field></block>"
+        }
     }
 }
diff --git a/test/unit/blocks_sounds.js b/test/unit/blocks_sounds.js
index 9a44e7f9f..53acc8aff 100644
--- a/test/unit/blocks_sounds.js
+++ b/test/unit/blocks_sounds.js
@@ -11,10 +11,10 @@ const util = {
                 {name: 'second name', soundId: 'second soundId'},
                 {name: 'third name', soundId: 'third soundId'},
                 {name: '6', soundId: 'fourth soundId'}
-            ]
-        },
-        audioPlayer: {
-            playSound: soundId => (playedSound = soundId)
+            ],
+            soundBank: {
+                playSound: (target, soundId) => (playedSound = soundId)
+            }
         }
     }
 };
diff --git a/test/unit/engine_blocks.js b/test/unit/engine_blocks.js
index 08b0b414e..5659b6878 100644
--- a/test/unit/engine_blocks.js
+++ b/test/unit/engine_blocks.js
@@ -1,5 +1,8 @@
 const test = require('tap').test;
 const Blocks = require('../../src/engine/blocks');
+const Variable = require('../../src/engine/variable');
+const adapter = require('../../src/engine/adapter');
+const events = require('../fixtures/events.json');
 
 test('spec', t => {
     const b = new Blocks();
@@ -776,3 +779,32 @@ test('updateTargetSpecificBlocks changes sprite clicked hat to stage clicked for
 
     t.end();
 });
+
+test('getAllVariableAndListReferences returns an empty map references when variable blocks do not exist', t => {
+    const b = new Blocks();
+    t.equal(Object.keys(b.getAllVariableAndListReferences()).length, 0);
+    t.end();
+});
+
+test('getAllVariableAndListReferences returns references when variable blocks exist', t => {
+    const b = new Blocks();
+
+    let varListRefs = b.getAllVariableAndListReferences();
+    t.equal(Object.keys(varListRefs).length, 0);
+
+    b.createBlock(adapter(events.mockVariableBlock)[0]);
+    b.createBlock(adapter(events.mockListBlock)[0]);
+
+    varListRefs = b.getAllVariableAndListReferences();
+    t.equal(Object.keys(varListRefs).length, 2);
+    t.equal(Array.isArray(varListRefs['mock var id']), true);
+    t.equal(varListRefs['mock var id'].length, 1);
+    t.equal(varListRefs['mock var id'][0].type, Variable.SCALAR_TYPE);
+    t.equal(varListRefs['mock var id'][0].referencingField.value, 'a mock variable');
+    t.equal(Array.isArray(varListRefs['mock list id']), true);
+    t.equal(varListRefs['mock list id'].length, 1);
+    t.equal(varListRefs['mock list id'][0].type, Variable.LIST_TYPE);
+    t.equal(varListRefs['mock list id'][0].referencingField.value, 'a mock list');
+
+    t.end();
+});
diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js
index a92dd5e61..098908e1f 100644
--- a/test/unit/engine_target.js
+++ b/test/unit/engine_target.js
@@ -1,6 +1,9 @@
 const test = require('tap').test;
 const Target = require('../../src/engine/target');
 const Variable = require('../../src/engine/variable');
+const adapter = require('../../src/engine/adapter');
+const Runtime = require('../../src/engine/runtime');
+const events = require('../fixtures/events.json');
 
 test('spec', t => {
     const target = new Target();
@@ -145,7 +148,7 @@ test('deleteVariable2', t => {
     t.end();
 });
 
-test('lookupOrCreateList creates a list if var with given id does not exist', t => {
+test('lookupOrCreateList creates a list if var with given id or var with given name does not exist', t => {
     const target = new Target();
     const variables = target.variables;
 
@@ -174,6 +177,22 @@ test('lookupOrCreateList returns list if one with given id exists', t => {
     t.end();
 });
 
+test('lookupOrCreateList succeeds in finding list if id is incorrect but name matches', t => {
+    const target = new Target();
+    const variables = target.variables;
+
+    t.equal(Object.keys(variables).length, 0);
+    target.createVariable('foo', 'bar', Variable.LIST_TYPE);
+    t.equal(Object.keys(variables).length, 1);
+
+    const listVar = target.lookupOrCreateList('not foo', 'bar');
+    t.equal(Object.keys(variables).length, 1);
+    t.equal(listVar.id, 'foo');
+    t.equal(listVar.name, 'bar');
+
+    t.end();
+});
+
 test('lookupBroadcastMsg returns the var with given id if exists', t => {
     const target = new Target();
     const variables = target.variables;
@@ -263,3 +282,147 @@ test('creating a comment with a blockId also updates the comment property on the
 
     t.end();
 });
+
+test('fixUpVariableReferences fixes sprite global var conflicting with project global var', t => {
+    const runtime = new Runtime();
+
+    const stage = new Target(runtime);
+    stage.isStage = true;
+
+    const target = new Target(runtime);
+    target.isStage = false;
+
+    runtime.targets = [stage, target];
+
+    // Create a global variable
+    stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
+
+    target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
+
+    t.equal(Object.keys(target.variables).length, 0);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
+
+    target.fixUpVariableReferences();
+
+    t.equal(Object.keys(target.variables).length, 0);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'pre-existing global var id');
+
+    t.end();
+});
+
+test('fixUpVariableReferences fixes sprite local var conflicting with project global var', t => {
+    const runtime = new Runtime();
+
+    const stage = new Target(runtime);
+    stage.isStage = true;
+
+    const target = new Target(runtime);
+    target.isStage = false;
+    target.getName = () => 'Target';
+
+    runtime.targets = [stage, target];
+
+    // Create a global variable
+    stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
+    target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
+
+    target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
+    t.equal(target.variables['mock var id'].name, 'a mock variable');
+
+    target.fixUpVariableReferences();
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
+    t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
+
+    t.end();
+});
+
+test('fixUpVariableReferences fixes conflicting sprite local var without blocks referencing var', t => {
+    const runtime = new Runtime();
+
+    const stage = new Target(runtime);
+    stage.isStage = true;
+
+    const target = new Target(runtime);
+    target.isStage = false;
+    target.getName = () => 'Target';
+
+    runtime.targets = [stage, target];
+
+    // Create a global variable
+    stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
+    target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
+
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.equal(target.variables['mock var id'].name, 'a mock variable');
+
+    target.fixUpVariableReferences();
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 1);
+    t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
+
+    t.end();
+});
+
+test('fixUpVariableReferences does not change variable name if there is no variable conflict', t => {
+    const runtime = new Runtime();
+
+    const stage = new Target(runtime);
+    stage.isStage = true;
+
+    const target = new Target(runtime);
+    target.isStage = false;
+    target.getName = () => 'Target';
+
+    runtime.targets = [stage, target];
+
+    // Create a global variable
+    stage.createVariable('pre-existing global var id', 'a variable', Variable.SCALAR_TYPE);
+    stage.createVariable('pre-existing global list id', 'a mock variable', Variable.LIST_TYPE);
+    target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
+
+    target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 2);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
+    t.equal(target.variables['mock var id'].name, 'a mock variable');
+
+    target.fixUpVariableReferences();
+
+    t.equal(Object.keys(target.variables).length, 1);
+    t.equal(Object.keys(stage.variables).length, 2);
+    t.type(target.blocks.getBlock('a block'), 'object');
+    t.type(target.blocks.getBlock('a block').fields, 'object');
+    t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
+    t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
+    t.equal(target.variables['mock var id'].name, 'a mock variable');
+
+    t.end();
+});