diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 7e1e466a0..aa227c513 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -1,5 +1,6 @@ var MathUtil = require('../util/math-util'); var Cast = require('../util/cast'); +var Clone = require('../util/clone'); var Scratch3SoundBlocks = function (runtime) { /** @@ -9,6 +10,43 @@ var Scratch3SoundBlocks = function (runtime) { this.runtime = runtime; }; +/** + * The key to load & store a target's sound-related state. + * @type {string} + */ +Scratch3SoundBlocks.STATE_KEY = 'Scratch.sound'; + +/** + * The default sound-related state, to be used when a target has no existing sound state. + * @type {SoundState} + */ +Scratch3SoundBlocks.DEFAULT_SOUND_STATE = { + volume: 100, + currentInstrument: 0, + effects: { + pitch: 0, + pan: 0, + echo: 0, + reverb: 0, + fuzz: 0, + robot: 0 + } +}; + +/** + * @param {Target} target - collect sound state for this target. + * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. + * @private + */ +Scratch3SoundBlocks.prototype._getSoundState = function (target) { + var soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY); + if (!soundState) { + soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE); + target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState); + } + return soundState; +}; + /** * Retrieve the block primitives implemented by this package. * @return {object.} Mapping of opcode to Function. @@ -30,6 +68,7 @@ Scratch3SoundBlocks.prototype.getPrimitives = function () { sound_effects_menu: this.effectsMenu, sound_setvolumeto: this.setVolume, sound_changevolumeby: this.changeVolume, + sound_volume: this.getVolume, sound_settempotobpm: this.setTempo, sound_changetempoby: this.changeTempo, sound_tempo: this.getTempo @@ -38,31 +77,50 @@ Scratch3SoundBlocks.prototype.getPrimitives = function () { Scratch3SoundBlocks.prototype.playSound = function (args, util) { var index = this._getSoundIndex(args.SOUND_MENU, util); - util.target.audioPlayer.playSound(index); + if (index >= 0) { + var md5 = util.target.sprite.sounds[index].md5; + util.target.audioPlayer.playSound(md5); + } }; Scratch3SoundBlocks.prototype.playSoundAndWait = function (args, util) { var index = this._getSoundIndex(args.SOUND_MENU, util); - return util.target.audioPlayer.playSound(index); + if (index >= 0) { + var md5 = util.target.sprite.sounds[index].md5; + return util.target.audioPlayer.playSound(md5); + } }; Scratch3SoundBlocks.prototype._getSoundIndex = function (soundName, util) { - if (util.target.sprite.sounds.length === 0) { - return 0; + // if the sprite has no sounds, return -1 + var len = util.target.sprite.sounds.length; + if (len === 0) { + return -1; } + var index; - if (Number(soundName)) { - soundName = Number(soundName); - var len = util.target.sprite.sounds.length; - index = MathUtil.wrapClamp(soundName, 1, len) - 1; - } else { - index = util.target.getSoundIndexByName(soundName); - if (index === -1) { - index = 0; + // try to convert to a number and use that as an index + var num = parseInt(soundName, 10); + if (!isNaN(num)) { + index = MathUtil.wrapClamp(num, 0, len - 1); + return index; + } + + // return the index for the sound of that name + index = this.getSoundIndexByName(soundName, util); + return index; +}; + +Scratch3SoundBlocks.prototype.getSoundIndexByName = function (soundName, util) { + var sounds = util.target.sprite.sounds; + for (var i = 0; i < sounds.length; i++) { + if (sounds[i].name === soundName) { + return i; } } - return index; + // if there is no sound by that name, return -1 + return -1; }; Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) { @@ -70,62 +128,100 @@ Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) { }; Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { - return util.target.audioPlayer.playNoteForBeats(args.NOTE, args.BEATS); + var note = Cast.toNumber(args.NOTE); + var beats = Cast.toNumber(args.BEATS); + var soundState = this._getSoundState(util.target); + var inst = soundState.currentInstrument; + return this.runtime.audioEngine.playNoteForBeatsWithInst(note, beats, inst); }; Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) { - return util.target.audioPlayer.playDrumForBeats(args.DRUM, args.BEATS); + var drum = Cast.toNumber(args.DRUM); + drum -= 1; // drums are one-indexed + drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums); + var beats = Cast.toNumber(args.BEATS); + return util.target.audioPlayer.playDrumForBeats(drum, beats); }; -Scratch3SoundBlocks.prototype.restForBeats = function (args, util) { - return util.target.audioPlayer.waitForBeats(args.BEATS); +Scratch3SoundBlocks.prototype.restForBeats = function (args) { + var beats = Cast.toNumber(args.BEATS); + return this.runtime.audioEngine.waitForBeats(beats); }; Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { + var soundState = this._getSoundState(util.target); var instNum = Cast.toNumber(args.INSTRUMENT); - return util.target.audioPlayer.setInstrument(instNum); + instNum -= 1; // instruments are one-indexed + instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments); + soundState.currentInstrument = instNum; + return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument); }; Scratch3SoundBlocks.prototype.setEffect = function (args, util) { + var effect = Cast.toString(args.EFFECT).toLowerCase(); var value = Cast.toNumber(args.VALUE); - util.target.audioPlayer.setEffect(args.EFFECT, value); + + var soundState = this._getSoundState(util.target); + if (!soundState.effects.hasOwnProperty(effect)) return; + + soundState.effects[effect] = value; + util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); }; Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { + var effect = Cast.toString(args.EFFECT).toLowerCase(); var value = Cast.toNumber(args.VALUE); - util.target.audioPlayer.changeEffect(args.EFFECT, value); + + var soundState = this._getSoundState(util.target); + if (!soundState.effects.hasOwnProperty(effect)) return; + + soundState.effects[effect] += value; + util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); }; Scratch3SoundBlocks.prototype.clearEffects = function (args, util) { + var soundState = this._getSoundState(util.target); + for (var effect in soundState.effects) { + soundState.effects[effect] = 0; + } util.target.audioPlayer.clearEffects(); }; Scratch3SoundBlocks.prototype.setVolume = function (args, util) { - var value = Cast.toNumber(args.VOLUME); - util.target.audioPlayer.setVolume(value); + var volume = Cast.toNumber(args.VOLUME); + this._updateVolume(volume, util); }; Scratch3SoundBlocks.prototype.changeVolume = function (args, util) { - var value = Cast.toNumber(args.VOLUME); - util.target.audioPlayer.changeVolume(value); + var soundState = this._getSoundState(util.target); + var volume = Cast.toNumber(args.VOLUME) + soundState.volume; + this._updateVolume(volume, util); +}; + +Scratch3SoundBlocks.prototype._updateVolume = function (volume, util) { + var soundState = this._getSoundState(util.target); + volume = MathUtil.clamp(volume, 0, 100); + soundState.volume = volume; + util.target.audioPlayer.setVolume(soundState.volume); }; Scratch3SoundBlocks.prototype.getVolume = function (args, util) { - return util.target.audioPlayer.currentVolume; + var soundState = this._getSoundState(util.target); + return soundState.volume; }; -Scratch3SoundBlocks.prototype.setTempo = function (args, util) { +Scratch3SoundBlocks.prototype.setTempo = function (args) { var value = Cast.toNumber(args.TEMPO); - util.target.audioPlayer.setTempo(value); + this.runtime.audioEngine.setTempo(value); }; -Scratch3SoundBlocks.prototype.changeTempo = function (args, util) { +Scratch3SoundBlocks.prototype.changeTempo = function (args) { var value = Cast.toNumber(args.TEMPO); - util.target.audioPlayer.changeTempo(value); + this.runtime.audioEngine.changeTempo(value); }; -Scratch3SoundBlocks.prototype.getTempo = function (args, util) { - return util.target.audioPlayer.currentTempo; +Scratch3SoundBlocks.prototype.getTempo = function () { + return this.runtime.audioEngine.currentTempo; }; Scratch3SoundBlocks.prototype.soundsMenu = function (args) { diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 40c17e309..20669466e 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -61,7 +61,8 @@ var parseScratchObject = function (object, runtime, topLevel) { rate: sound.rate, sampleCount: sound.sampleCount, soundID: sound.soundID, - name: sound.soundName + name: sound.soundName, + md5: sound.md5 }); } } diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 95aad5019..5ff494059 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -408,7 +408,7 @@ var specMap = { { type: 'input', inputOp: 'math_number', - inputName: 'DRUMTYPE' + inputName: 'DRUM' }, { type: 'input', diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index b82afa846..9bbc20b23 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -69,10 +69,9 @@ RenderedTarget.prototype.initDrawable = function () { this.audioPlayer = null; if (this.runtime && this.runtime.audioEngine) { if (this.isOriginal) { - this.sprite.audioPlayer = this.runtime.audioEngine.createPlayer(); - this.sprite.audioPlayer.loadSounds(this.sprite.sounds); + this.runtime.audioEngine.loadSounds(this.sprite.sounds); } - this.audioPlayer = this.sprite.audioPlayer; + this.audioPlayer = this.runtime.audioEngine.createPlayer(); } }; @@ -399,20 +398,6 @@ RenderedTarget.prototype.getCostumeIndexByName = function (costumeName) { return -1; }; -/** - * Get a sound index of this rendered target, by name of the sound. - * @param {?string} soundName Name of a sound. - * @return {number} Index of the named sound, or -1 if not present. - */ -RenderedTarget.prototype.getSoundIndexByName = function (soundName) { - for (var i = 0; i < this.sprite.sounds.length; i++) { - if (this.sprite.sounds[i].name === soundName) { - return i; - } - } - return -1; -}; - /** * Get a costume of this rendered target by id. * @return {object} current costume