Merge pull request #424 from ericrosenbaum/sound

Refactor sound engine
This commit is contained in:
Eric Rosenbaum 2017-02-02 17:45:45 -05:00 committed by GitHub
commit 46c0859ae9
4 changed files with 132 additions and 50 deletions

View file

@ -1,5 +1,6 @@
var MathUtil = require('../util/math-util'); var MathUtil = require('../util/math-util');
var Cast = require('../util/cast'); var Cast = require('../util/cast');
var Clone = require('../util/clone');
var Scratch3SoundBlocks = function (runtime) { var Scratch3SoundBlocks = function (runtime) {
/** /**
@ -9,6 +10,43 @@ var Scratch3SoundBlocks = function (runtime) {
this.runtime = 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. * Retrieve the block primitives implemented by this package.
* @return {object.<string, Function>} Mapping of opcode to Function. * @return {object.<string, Function>} Mapping of opcode to Function.
@ -30,6 +68,7 @@ Scratch3SoundBlocks.prototype.getPrimitives = function () {
sound_effects_menu: this.effectsMenu, sound_effects_menu: this.effectsMenu,
sound_setvolumeto: this.setVolume, sound_setvolumeto: this.setVolume,
sound_changevolumeby: this.changeVolume, sound_changevolumeby: this.changeVolume,
sound_volume: this.getVolume,
sound_settempotobpm: this.setTempo, sound_settempotobpm: this.setTempo,
sound_changetempoby: this.changeTempo, sound_changetempoby: this.changeTempo,
sound_tempo: this.getTempo sound_tempo: this.getTempo
@ -38,31 +77,50 @@ Scratch3SoundBlocks.prototype.getPrimitives = function () {
Scratch3SoundBlocks.prototype.playSound = function (args, util) { Scratch3SoundBlocks.prototype.playSound = function (args, util) {
var index = this._getSoundIndex(args.SOUND_MENU, 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) { Scratch3SoundBlocks.prototype.playSoundAndWait = function (args, util) {
var index = this._getSoundIndex(args.SOUND_MENU, 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) { Scratch3SoundBlocks.prototype._getSoundIndex = function (soundName, util) {
if (util.target.sprite.sounds.length === 0) { // if the sprite has no sounds, return -1
return 0; var len = util.target.sprite.sounds.length;
if (len === 0) {
return -1;
} }
var index; var index;
if (Number(soundName)) { // try to convert to a number and use that as an index
soundName = Number(soundName); var num = parseInt(soundName, 10);
var len = util.target.sprite.sounds.length; if (!isNaN(num)) {
index = MathUtil.wrapClamp(soundName, 1, len) - 1; index = MathUtil.wrapClamp(num, 0, len - 1);
} else {
index = util.target.getSoundIndexByName(soundName);
if (index === -1) {
index = 0;
}
}
return index; 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;
}
}
// if there is no sound by that name, return -1
return -1;
}; };
Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) { Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) {
@ -70,62 +128,100 @@ Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) {
}; };
Scratch3SoundBlocks.prototype.playNoteForBeats = 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) { 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) { Scratch3SoundBlocks.prototype.restForBeats = function (args) {
return util.target.audioPlayer.waitForBeats(args.BEATS); var beats = Cast.toNumber(args.BEATS);
return this.runtime.audioEngine.waitForBeats(beats);
}; };
Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { Scratch3SoundBlocks.prototype.setInstrument = function (args, util) {
var soundState = this._getSoundState(util.target);
var instNum = Cast.toNumber(args.INSTRUMENT); 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) { Scratch3SoundBlocks.prototype.setEffect = function (args, util) {
var effect = Cast.toString(args.EFFECT).toLowerCase();
var value = Cast.toNumber(args.VALUE); 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) { Scratch3SoundBlocks.prototype.changeEffect = function (args, util) {
var effect = Cast.toString(args.EFFECT).toLowerCase();
var value = Cast.toNumber(args.VALUE); 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) { 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(); util.target.audioPlayer.clearEffects();
}; };
Scratch3SoundBlocks.prototype.setVolume = function (args, util) { Scratch3SoundBlocks.prototype.setVolume = function (args, util) {
var value = Cast.toNumber(args.VOLUME); var volume = Cast.toNumber(args.VOLUME);
util.target.audioPlayer.setVolume(value); this._updateVolume(volume, util);
}; };
Scratch3SoundBlocks.prototype.changeVolume = function (args, util) { Scratch3SoundBlocks.prototype.changeVolume = function (args, util) {
var value = Cast.toNumber(args.VOLUME); var soundState = this._getSoundState(util.target);
util.target.audioPlayer.changeVolume(value); 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) { 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); 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); var value = Cast.toNumber(args.TEMPO);
util.target.audioPlayer.changeTempo(value); this.runtime.audioEngine.changeTempo(value);
}; };
Scratch3SoundBlocks.prototype.getTempo = function (args, util) { Scratch3SoundBlocks.prototype.getTempo = function () {
return util.target.audioPlayer.currentTempo; return this.runtime.audioEngine.currentTempo;
}; };
Scratch3SoundBlocks.prototype.soundsMenu = function (args) { Scratch3SoundBlocks.prototype.soundsMenu = function (args) {

View file

@ -61,7 +61,8 @@ var parseScratchObject = function (object, runtime, topLevel) {
rate: sound.rate, rate: sound.rate,
sampleCount: sound.sampleCount, sampleCount: sound.sampleCount,
soundID: sound.soundID, soundID: sound.soundID,
name: sound.soundName name: sound.soundName,
md5: sound.md5
}); });
} }
} }

View file

@ -408,7 +408,7 @@ var specMap = {
{ {
type: 'input', type: 'input',
inputOp: 'math_number', inputOp: 'math_number',
inputName: 'DRUMTYPE' inputName: 'DRUM'
}, },
{ {
type: 'input', type: 'input',

View file

@ -69,10 +69,9 @@ RenderedTarget.prototype.initDrawable = function () {
this.audioPlayer = null; this.audioPlayer = null;
if (this.runtime && this.runtime.audioEngine) { if (this.runtime && this.runtime.audioEngine) {
if (this.isOriginal) { if (this.isOriginal) {
this.sprite.audioPlayer = this.runtime.audioEngine.createPlayer(); this.runtime.audioEngine.loadSounds(this.sprite.sounds);
this.sprite.audioPlayer.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; 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. * Get a costume of this rendered target by id.
* @return {object} current costume * @return {object} current costume