2017-04-17 15:10:04 -04:00
|
|
|
const MathUtil = require('../util/math-util');
|
|
|
|
const Cast = require('../util/cast');
|
|
|
|
const Clone = require('../util/clone');
|
2016-09-15 16:51:24 -04:00
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
const Scratch3SoundBlocks = function (runtime) {
|
2016-08-09 15:40:50 -04:00
|
|
|
/**
|
|
|
|
* The runtime instantiating this block package.
|
|
|
|
* @type {Runtime}
|
|
|
|
*/
|
|
|
|
this.runtime = runtime;
|
2017-01-03 23:41:49 -05:00
|
|
|
};
|
2016-08-09 15:40:50 -04:00
|
|
|
|
2017-01-31 18:33:32 -05:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-03-17 17:23:53 -04:00
|
|
|
/**
|
|
|
|
* The minimum and maximum MIDI note numbers, for clamping the input to play note.
|
|
|
|
* @type {{min: number, max: number}}
|
|
|
|
*/
|
|
|
|
Scratch3SoundBlocks.MIDI_NOTE_RANGE = {min: 36, max: 96}; // C2 to C7
|
|
|
|
|
2017-03-17 17:29:23 -04:00
|
|
|
/**
|
|
|
|
* The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
|
|
|
|
* 100 beats at the default tempo of 60bpm is 100 seconds.
|
|
|
|
* @type {{min: number, max: number}}
|
|
|
|
*/
|
|
|
|
Scratch3SoundBlocks.BEAT_RANGE = {min: 0, max: 100};
|
|
|
|
|
2017-03-17 18:13:33 -04:00
|
|
|
/** The minimum and maximum tempo values, in bpm.
|
|
|
|
* @type {{min: number, max: number}}
|
|
|
|
*/
|
|
|
|
Scratch3SoundBlocks.TEMPO_RANGE = {min: 20, max: 500};
|
|
|
|
|
2017-03-20 14:53:04 -04:00
|
|
|
/** The minimum and maximum values for each sound effect.
|
|
|
|
* @type {{effect:{min: number, max: number}}}
|
|
|
|
*/
|
|
|
|
Scratch3SoundBlocks.EFFECT_RANGE = {
|
|
|
|
pitch: {min: -600, max: 600}, // -5 to 5 octaves
|
|
|
|
pan: {min: -100, max: 100}, // 100% left to 100% right
|
|
|
|
echo: {min: 0, max: 100}, // 0 to max (75%) feedback
|
|
|
|
reverb: {min: 0, max: 100}, // wet/dry: 0 to 100% wet
|
|
|
|
fuzz: {min: 0, max: 100}, // wed/dry: 0 to 100% wet
|
|
|
|
robot: {min: 0, max: 600} // 0 to 5 octaves
|
|
|
|
};
|
|
|
|
|
2017-01-31 18:33:32 -05:00
|
|
|
/**
|
|
|
|
* @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) {
|
2017-04-17 15:10:04 -04:00
|
|
|
let soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY);
|
2017-01-31 18:33:32 -05:00
|
|
|
if (!soundState) {
|
|
|
|
soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
|
|
|
|
target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
|
|
|
|
}
|
|
|
|
return soundState;
|
|
|
|
};
|
|
|
|
|
2016-08-09 15:40:50 -04:00
|
|
|
/**
|
|
|
|
* Retrieve the block primitives implemented by this package.
|
2017-02-01 15:59:50 -05:00
|
|
|
* @return {object.<string, Function>} Mapping of opcode to Function.
|
2016-08-09 15:40:50 -04:00
|
|
|
*/
|
2017-01-03 23:41:49 -05:00
|
|
|
Scratch3SoundBlocks.prototype.getPrimitives = function () {
|
2016-08-09 15:40:50 -04:00
|
|
|
return {
|
2017-01-03 23:41:49 -05:00
|
|
|
sound_play: this.playSound,
|
|
|
|
sound_playuntildone: this.playSoundAndWait,
|
|
|
|
sound_stopallsounds: this.stopAllSounds,
|
|
|
|
sound_playnoteforbeats: this.playNoteForBeats,
|
|
|
|
sound_playdrumforbeats: this.playDrumForBeats,
|
2017-01-06 10:31:01 -05:00
|
|
|
sound_restforbeats: this.restForBeats,
|
2017-01-03 23:41:49 -05:00
|
|
|
sound_setinstrumentto: this.setInstrument,
|
|
|
|
sound_seteffectto: this.setEffect,
|
|
|
|
sound_changeeffectby: this.changeEffect,
|
|
|
|
sound_cleareffects: this.clearEffects,
|
|
|
|
sound_sounds_menu: this.soundsMenu,
|
|
|
|
sound_beats_menu: this.beatsMenu,
|
|
|
|
sound_effects_menu: this.effectsMenu,
|
|
|
|
sound_setvolumeto: this.setVolume,
|
|
|
|
sound_changevolumeby: this.changeVolume,
|
2017-01-09 15:48:02 -05:00
|
|
|
sound_volume: this.getVolume,
|
2017-01-06 10:31:01 -05:00
|
|
|
sound_settempotobpm: this.setTempo,
|
|
|
|
sound_changetempoby: this.changeTempo,
|
|
|
|
sound_tempo: this.getTempo
|
2016-08-09 15:40:50 -04:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype.playSound = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
2017-01-31 18:33:32 -05:00
|
|
|
if (index >= 0) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const md5 = util.target.sprite.sounds[index].md5;
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-31 18:33:32 -05:00
|
|
|
util.target.audioPlayer.playSound(md5);
|
|
|
|
}
|
2016-09-27 17:09:53 -04:00
|
|
|
};
|
|
|
|
|
2016-10-17 17:16:55 -04:00
|
|
|
Scratch3SoundBlocks.prototype.playSoundAndWait = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
2017-01-31 18:33:32 -05:00
|
|
|
if (index >= 0) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const md5 = util.target.sprite.sounds[index].md5;
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-31 18:33:32 -05:00
|
|
|
return util.target.audioPlayer.playSound(md5);
|
|
|
|
}
|
2016-10-17 17:16:55 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype._getSoundIndex = function (soundName, util) {
|
2017-01-31 18:33:32 -05:00
|
|
|
// if the sprite has no sounds, return -1
|
2017-04-17 15:10:04 -04:00
|
|
|
const len = util.target.sprite.sounds.length;
|
2017-01-31 18:33:32 -05:00
|
|
|
if (len === 0) {
|
|
|
|
return -1;
|
2016-09-28 16:42:25 -04:00
|
|
|
}
|
2017-01-31 18:33:32 -05:00
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
let index;
|
2017-01-03 23:41:49 -05:00
|
|
|
|
2017-01-31 18:33:32 -05:00
|
|
|
// try to convert to a number and use that as an index
|
2017-04-17 15:10:04 -04:00
|
|
|
const num = parseInt(soundName, 10);
|
2017-01-31 18:33:32 -05:00
|
|
|
if (!isNaN(num)) {
|
|
|
|
index = MathUtil.wrapClamp(num, 0, len - 1);
|
|
|
|
return index;
|
2016-09-28 16:42:25 -04:00
|
|
|
}
|
2017-01-31 18:33:32 -05:00
|
|
|
|
|
|
|
// return the index for the sound of that name
|
|
|
|
index = this.getSoundIndexByName(soundName, util);
|
2016-10-17 17:16:55 -04:00
|
|
|
return index;
|
2016-08-09 15:40:50 -04:00
|
|
|
};
|
|
|
|
|
2017-01-31 18:33:32 -05:00
|
|
|
Scratch3SoundBlocks.prototype.getSoundIndexByName = function (soundName, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const sounds = util.target.sprite.sounds;
|
|
|
|
for (let i = 0; i < sounds.length; i++) {
|
2017-01-31 18:33:32 -05:00
|
|
|
if (sounds[i].name === soundName) {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if there is no sound by that name, return -1
|
|
|
|
return -1;
|
|
|
|
};
|
|
|
|
|
2016-08-11 16:47:01 -04:00
|
|
|
Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) {
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-04 18:37:55 -05:00
|
|
|
util.target.audioPlayer.stopAllSounds();
|
2016-08-11 16:47:01 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
let note = Cast.toNumber(args.NOTE);
|
2017-03-17 17:23:53 -04:00
|
|
|
note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max);
|
2017-04-17 15:10:04 -04:00
|
|
|
let beats = Cast.toNumber(args.BEATS);
|
2017-03-17 17:29:23 -04:00
|
|
|
beats = this._clampBeats(beats);
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
|
|
|
const inst = soundState.currentInstrument;
|
|
|
|
const vol = soundState.volume;
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-02-09 14:52:15 -05:00
|
|
|
return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, vol);
|
2017-01-06 11:49:25 -05:00
|
|
|
};
|
2016-10-19 15:44:30 -04:00
|
|
|
|
2016-08-11 16:47:01 -04:00
|
|
|
Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
let drum = Cast.toNumber(args.DRUM);
|
2017-01-11 11:23:39 -05:00
|
|
|
drum -= 1; // drums are one-indexed
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-01-11 11:41:21 -05:00
|
|
|
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums);
|
2017-04-17 15:10:04 -04:00
|
|
|
let beats = Cast.toNumber(args.BEATS);
|
2017-03-17 17:29:23 -04:00
|
|
|
beats = this._clampBeats(beats);
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-10 18:00:33 -05:00
|
|
|
return util.target.audioPlayer.playDrumForBeats(drum, beats);
|
2017-01-06 10:31:01 -05:00
|
|
|
};
|
|
|
|
|
2017-01-30 10:56:31 -05:00
|
|
|
Scratch3SoundBlocks.prototype.restForBeats = function (args) {
|
2017-04-17 15:10:04 -04:00
|
|
|
let beats = Cast.toNumber(args.BEATS);
|
2017-03-17 17:29:23 -04:00
|
|
|
beats = this._clampBeats(beats);
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-01-30 10:56:31 -05:00
|
|
|
return this.runtime.audioEngine.waitForBeats(beats);
|
2016-08-11 16:47:01 -04:00
|
|
|
};
|
|
|
|
|
2017-03-17 17:29:23 -04:00
|
|
|
Scratch3SoundBlocks.prototype._clampBeats = function (beats) {
|
|
|
|
return MathUtil.clamp(beats, Scratch3SoundBlocks.BEAT_RANGE.min, Scratch3SoundBlocks.BEAT_RANGE.max);
|
|
|
|
};
|
|
|
|
|
2016-10-19 15:44:30 -04:00
|
|
|
Scratch3SoundBlocks.prototype.setInstrument = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
|
|
|
let instNum = Cast.toNumber(args.INSTRUMENT);
|
2017-01-11 11:23:39 -05:00
|
|
|
instNum -= 1; // instruments are one-indexed
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-01-11 11:41:21 -05:00
|
|
|
instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments);
|
2017-01-31 18:33:32 -05:00
|
|
|
soundState.currentInstrument = instNum;
|
|
|
|
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument);
|
2016-10-19 15:44:30 -04:00
|
|
|
};
|
|
|
|
|
2016-08-11 16:47:01 -04:00
|
|
|
Scratch3SoundBlocks.prototype.setEffect = function (args, util) {
|
2017-03-20 14:53:04 -04:00
|
|
|
this._updateEffect(args, util, false);
|
2016-08-11 16:47:01 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype.changeEffect = function (args, util) {
|
2017-03-20 14:53:04 -04:00
|
|
|
this._updateEffect(args, util, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype._updateEffect = function (args, util, change) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
|
|
|
const value = Cast.toNumber(args.VALUE);
|
2017-01-31 18:33:32 -05:00
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
2017-01-31 18:33:32 -05:00
|
|
|
if (!soundState.effects.hasOwnProperty(effect)) return;
|
|
|
|
|
2017-03-20 14:53:04 -04:00
|
|
|
if (change) {
|
|
|
|
soundState.effects[effect] += value;
|
|
|
|
} else {
|
|
|
|
soundState.effects[effect] = value;
|
|
|
|
}
|
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
const effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect];
|
2017-03-20 14:53:04 -04:00
|
|
|
soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max);
|
|
|
|
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-31 18:33:32 -05:00
|
|
|
util.target.audioPlayer.setEffect(effect, soundState.effects[effect]);
|
2016-08-11 16:47:01 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype.clearEffects = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
|
|
|
for (const effect in soundState.effects) {
|
2017-02-09 21:12:47 -05:00
|
|
|
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
2017-01-31 18:33:32 -05:00
|
|
|
soundState.effects[effect] = 0;
|
|
|
|
}
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-04 18:37:55 -05:00
|
|
|
util.target.audioPlayer.clearEffects();
|
2016-08-11 16:47:01 -04:00
|
|
|
};
|
|
|
|
|
2016-10-27 11:31:22 -04:00
|
|
|
Scratch3SoundBlocks.prototype.setVolume = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const volume = Cast.toNumber(args.VOLUME);
|
2017-01-31 18:33:32 -05:00
|
|
|
this._updateVolume(volume, util);
|
2016-10-27 11:31:22 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype.changeVolume = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
|
|
|
const volume = Cast.toNumber(args.VOLUME) + soundState.volume;
|
2017-01-31 18:33:32 -05:00
|
|
|
this._updateVolume(volume, util);
|
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype._updateVolume = function (volume, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
2017-01-31 18:33:32 -05:00
|
|
|
volume = MathUtil.clamp(volume, 0, 100);
|
|
|
|
soundState.volume = volume;
|
2017-02-03 17:39:36 -05:00
|
|
|
if (util.target.audioPlayer === null) return;
|
2017-01-31 18:33:32 -05:00
|
|
|
util.target.audioPlayer.setVolume(soundState.volume);
|
2016-10-27 11:31:22 -04:00
|
|
|
};
|
|
|
|
|
2017-01-06 10:31:11 -05:00
|
|
|
Scratch3SoundBlocks.prototype.getVolume = function (args, util) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const soundState = this._getSoundState(util.target);
|
2017-01-31 18:33:32 -05:00
|
|
|
return soundState.volume;
|
2017-01-06 10:31:11 -05:00
|
|
|
};
|
|
|
|
|
2017-01-09 15:47:29 -05:00
|
|
|
Scratch3SoundBlocks.prototype.setTempo = function (args) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const tempo = Cast.toNumber(args.TEMPO);
|
2017-03-17 18:13:33 -04:00
|
|
|
this._updateTempo(tempo);
|
2016-10-27 11:31:22 -04:00
|
|
|
};
|
|
|
|
|
2017-01-09 15:47:29 -05:00
|
|
|
Scratch3SoundBlocks.prototype.changeTempo = function (args) {
|
2017-04-17 15:10:04 -04:00
|
|
|
const change = Cast.toNumber(args.TEMPO);
|
2017-03-17 18:13:33 -04:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-04-17 15:10:04 -04:00
|
|
|
const tempo = change + this.runtime.audioEngine.currentTempo;
|
2017-03-17 18:13:33 -04:00
|
|
|
this._updateTempo(tempo);
|
|
|
|
};
|
|
|
|
|
|
|
|
Scratch3SoundBlocks.prototype._updateTempo = function (tempo) {
|
|
|
|
tempo = MathUtil.clamp(tempo, Scratch3SoundBlocks.TEMPO_RANGE.min, Scratch3SoundBlocks.TEMPO_RANGE.max);
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-03-17 18:13:33 -04:00
|
|
|
this.runtime.audioEngine.setTempo(tempo);
|
2016-10-27 11:31:22 -04:00
|
|
|
};
|
|
|
|
|
2017-01-09 15:47:29 -05:00
|
|
|
Scratch3SoundBlocks.prototype.getTempo = function () {
|
2017-02-03 17:39:36 -05:00
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return;
|
2017-01-09 15:47:29 -05:00
|
|
|
return this.runtime.audioEngine.currentTempo;
|
2017-01-06 10:31:01 -05:00
|
|
|
};
|
|
|
|
|
2016-10-13 11:54:00 -04:00
|
|
|
Scratch3SoundBlocks.prototype.soundsMenu = function (args) {
|
2016-08-11 16:47:01 -04:00
|
|
|
return args.SOUND_MENU;
|
|
|
|
};
|
|
|
|
|
2016-10-13 11:54:00 -04:00
|
|
|
Scratch3SoundBlocks.prototype.beatsMenu = function (args) {
|
|
|
|
return args.BEATS;
|
2016-08-09 15:40:50 -04:00
|
|
|
};
|
|
|
|
|
2016-10-13 11:54:00 -04:00
|
|
|
Scratch3SoundBlocks.prototype.effectsMenu = function (args) {
|
2016-08-11 16:47:01 -04:00
|
|
|
return args.EFFECT;
|
|
|
|
};
|
|
|
|
|
2016-08-09 15:40:50 -04:00
|
|
|
module.exports = Scratch3SoundBlocks;
|