From fbeb1de1c1f0dd76b061054667c50dfa89b8c35f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 6 Nov 2017 16:57:16 -0500 Subject: [PATCH 1/8] Add music extension file --- src/blocks/scratch3_music.js | 301 +++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 src/blocks/scratch3_music.js diff --git a/src/blocks/scratch3_music.js b/src/blocks/scratch3_music.js new file mode 100644 index 000000000..b7cde9611 --- /dev/null +++ b/src/blocks/scratch3_music.js @@ -0,0 +1,301 @@ +const ArgumentType = require('../extension-support/argument-type'); +const BlockType = require('../extension-support/block-type'); +const Clone = require('../util/clone'); +const Cast = require('../util/cast'); +const MathUtil = require('../util/math-util'); + +const drumNames = [ + 'Snare Drum', + 'Bass Drum', + 'Side Stick', + 'Crash Cymbal', + 'Open Hi-Hat', + 'Closed Hi-Hat', + 'Tambourine', + 'Hand Clap', + 'Claves', + 'Wood Block', + 'Cowbell', + 'Triangle', + 'Bongo', + 'Conga', + 'Cabasa', + 'Guiro', + 'Vibraslap', + 'Open Cuica' +]; + +const instrumentNames = [ + 'Piano', + 'Electric Piano', + 'Organ', + 'Guitar', + 'Electric Guitar', + 'Bass', + 'Pizzicato', + 'Cello', + 'Trombone', + 'Clarinet', + 'Saxophone', + 'Flute', + 'Wooden Flute', + 'Bassoon', + 'Choir', + 'Vibraphone', + 'Music Box', + 'Steel Drum', + 'Marimba', + 'Synth Lead', + 'Synth Pad' +]; + +/** + * Class for the music-related blocks in Scratch 3.0 + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3MusicBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.drumMenu = this.buildMenu(drumNames); + this.instrumentMenu = this.buildMenu(instrumentNames); + } + + buildMenu (names) { + const menu = []; + for (let i = 0; i < names.length; i++) { + const entry = {}; + const num = i + 1; // Menu numbers are one-indexed + entry.text = `(${num}) ${names[i]}`; + entry.value = String(num); + menu.push(entry); + } + return menu; + } + + /** + * The key to load & store a target's music-related state. + * @type {string} + */ + static get STATE_KEY () { + return 'Scratch.music'; + } + + /** + * The default music-related state, to be used when a target has no existing music state. + * @type {MusicState} + */ + static get DEFAULT_MUSIC_STATE () { + return { + currentInstrument: 0 + }; + } + + /** + * The minimum and maximum MIDI note numbers, for clamping the input to play note. + * @type {{min: number, max: number}} + */ + static get MIDI_NOTE_RANGE () { + return {min: 36, max: 96}; // C2 to C7 + } + + /** + * 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}} + */ + static get BEAT_RANGE () { + return {min: 0, max: 100}; + } + + /** The minimum and maximum tempo values, in bpm. + * @type {{min: number, max: number}} + */ + static get TEMPO_RANGE () { + return {min: 20, max: 500}; + } + + /** + * @param {Target} target - collect music state for this target. + * @returns {MusicState} the mutable music state associated with that target. This will be created if necessary. + * @private + */ + _getMusicState (target) { + let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY); + if (!musicState) { + musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE); + target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState); + } + return musicState; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'music', + name: 'Music', + blocks: [ + { + opcode: 'playDrumForBeats', + blockType: BlockType.COMMAND, + text: 'play drum [DRUM] for [BEATS] beats', + arguments: { + DRUM: { + type: ArgumentType.NUMBER, + menu: 'drums', + defaultValue: 1 + }, + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'restForBeats', + blockType: BlockType.COMMAND, + text: 'rest for [BEATS] beats', + arguments: { + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'playNoteForBeats', + blockType: BlockType.COMMAND, + text: 'play note [NOTE] for [BEATS] beats', + arguments: { + NOTE: { + type: ArgumentType.NUMBER, + defaultValue: 60 + }, + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'setInstrument', + blockType: BlockType.COMMAND, + text: 'set instrument to [INSTRUMENT]', + arguments: { + INSTRUMENT: { + type: ArgumentType.NUMBER, + menu: 'instruments', + defaultValue: 1 + } + } + }, + { + opcode: 'setTempo', + blockType: BlockType.COMMAND, + text: 'set tempo to [TEMPO]', + arguments: { + TEMPO: { + type: ArgumentType.NUMBER, + defaultValue: 60 + } + } + }, + { + opcode: 'changeTempo', + blockType: BlockType.COMMAND, + text: 'change tempo by [TEMPO]', + arguments: { + TEMPO: { + type: ArgumentType.NUMBER, + defaultValue: 20 + } + } + }, + { + opcode: 'getTempo', + text: 'tempo', + blockType: BlockType.REPORTER + } + ], + menus: { + drums: this.drumMenu, + instruments: this.instrumentMenu + } + }; + } + + playDrumForBeats (args, util) { + let drum = Cast.toNumber(args.DRUM); + drum -= 1; // drums are one-indexed + if (typeof this.runtime.audioEngine === 'undefined') return; + drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1); + let beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); + if (util.target.audioPlayer === null) return; + return util.target.audioPlayer.playDrumForBeats(drum, beats); + } + + restForBeats (args) { + let beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); + if (typeof this.runtime.audioEngine === 'undefined') return; + return this.runtime.audioEngine.waitForBeats(beats); + } + + playNoteForBeats (args, util) { + let note = Cast.toNumber(args.NOTE); + note = MathUtil.clamp(note, Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max); + let beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); + const musicState = this._getMusicState(util.target); + const inst = musicState.currentInstrument; + if (typeof this.runtime.audioEngine === 'undefined') return; + return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100); + } + + setInstrument (args, util) { + const musicState = this._getMusicState(util.target); + let instNum = Cast.toNumber(args.INSTRUMENT); + instNum -= 1; // instruments are one-indexed + if (typeof this.runtime.audioEngine === 'undefined') return; + instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1); + musicState.currentInstrument = instNum; + return this.runtime.audioEngine.instrumentPlayer.loadInstrument(musicState.currentInstrument); + } + + _clampBeats (beats) { + return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max); + } + + setTempo (args) { + const tempo = Cast.toNumber(args.TEMPO); + this._updateTempo(tempo); + } + + changeTempo (args) { + const change = Cast.toNumber(args.TEMPO); + if (typeof this.runtime.audioEngine === 'undefined') return; + const tempo = change + this.runtime.audioEngine.currentTempo; + this._updateTempo(tempo); + } + + _updateTempo (tempo) { + tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max); + if (typeof this.runtime.audioEngine === 'undefined') return; + this.runtime.audioEngine.setTempo(tempo); + } + + getTempo () { + if (typeof this.runtime.audioEngine === 'undefined') return; + return this.runtime.audioEngine.currentTempo; + } +} + +module.exports = Scratch3MusicBlocks; From b2941f8c5a420321d7a0b926a65220089f8fc61c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 11:27:33 -0500 Subject: [PATCH 2/8] Load music as internal extension --- src/extension-support/extension-manager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 5cfa2b4a3..938e791dc 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -8,9 +8,11 @@ const BlockType = require('./block-type'); // TODO: change extension spec so that library info, including extension ID, can be collected through static methods const Scratch3PenBlocks = require('../blocks/scratch3_pen'); const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2'); +const Scratch3MusicBlocks = require('../blocks/scratch3_music'); const builtinExtensions = { pen: Scratch3PenBlocks, - wedo2: Scratch3WeDo2Blocks + wedo2: Scratch3WeDo2Blocks, + music: Scratch3MusicBlocks }; /** From 533ed60d98915770e20faee5191c87364e85f2a8 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 11:27:50 -0500 Subject: [PATCH 3/8] Update sb2 specmap with music extension blocks --- src/serialization/sb2_specmap.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/serialization/sb2_specmap.js b/src/serialization/sb2_specmap.js index cf5be1c89..b511d59c8 100644 --- a/src/serialization/sb2_specmap.js +++ b/src/serialization/sb2_specmap.js @@ -418,7 +418,7 @@ const specMap = { ] }, 'playDrum': { - opcode: 'sound_playdrumforbeats', + opcode: 'music.playDrumForBeats', argMap: [ { type: 'input', @@ -433,7 +433,7 @@ const specMap = { ] }, 'rest:elapsed:from:': { - opcode: 'sound_restforbeats', + opcode: 'music.restForBeats', argMap: [ { type: 'input', @@ -443,7 +443,7 @@ const specMap = { ] }, 'noteOn:duration:elapsed:from:': { - opcode: 'sound_playnoteforbeats', + opcode: 'music.playNoteForBeats', argMap: [ { type: 'input', @@ -458,7 +458,7 @@ const specMap = { ] }, 'instrument:': { - opcode: 'sound_setinstrumentto', + opcode: 'music.setInstrument', argMap: [ { type: 'input', @@ -493,7 +493,7 @@ const specMap = { ] }, 'changeTempoBy:': { - opcode: 'sound_changetempoby', + opcode: 'music.changeTempo', argMap: [ { type: 'input', @@ -503,7 +503,7 @@ const specMap = { ] }, 'setTempoTo:': { - opcode: 'sound_settempotobpm', + opcode: 'music.setTempo', argMap: [ { type: 'input', @@ -513,7 +513,7 @@ const specMap = { ] }, 'tempo': { - opcode: 'sound_tempo', + opcode: 'music.getTempo', argMap: [ ] }, From 87cf546b038bc32f6c82bd36df7bf41f1271e382 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 11:28:04 -0500 Subject: [PATCH 4/8] Remove music opcodes from sound bocks --- src/blocks/scratch3_sound.js | 76 +----------------------------------- 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 1f7745742..6f5fed972 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -91,10 +91,6 @@ class Scratch3SoundBlocks { sound_play: this.playSound, sound_playuntildone: this.playSoundAndWait, sound_stopallsounds: this.stopAllSounds, - sound_playnoteforbeats: this.playNoteForBeats, - sound_playdrumforbeats: this.playDrumForBeats, - sound_restforbeats: this.restForBeats, - sound_setinstrumentto: this.setInstrument, sound_seteffectto: this.setEffect, sound_changeeffectby: this.changeEffect, sound_cleareffects: this.clearEffects, @@ -103,10 +99,7 @@ class Scratch3SoundBlocks { 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 + sound_volume: this.getVolume }; } @@ -167,50 +160,6 @@ class Scratch3SoundBlocks { util.target.audioPlayer.stopAllSounds(); } - playNoteForBeats (args, util) { - let note = Cast.toNumber(args.NOTE); - note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max); - let beats = Cast.toNumber(args.BEATS); - beats = this._clampBeats(beats); - const soundState = this._getSoundState(util.target); - const inst = soundState.currentInstrument; - const vol = soundState.volume; - if (typeof this.runtime.audioEngine === 'undefined') return; - return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, vol); - } - - playDrumForBeats (args, util) { - let drum = Cast.toNumber(args.DRUM); - drum -= 1; // drums are one-indexed - if (typeof this.runtime.audioEngine === 'undefined') return; - drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1); - let beats = Cast.toNumber(args.BEATS); - beats = this._clampBeats(beats); - if (util.target.audioPlayer === null) return; - return util.target.audioPlayer.playDrumForBeats(drum, beats); - } - - restForBeats (args) { - let beats = Cast.toNumber(args.BEATS); - beats = this._clampBeats(beats); - if (typeof this.runtime.audioEngine === 'undefined') return; - return this.runtime.audioEngine.waitForBeats(beats); - } - - _clampBeats (beats) { - return MathUtil.clamp(beats, Scratch3SoundBlocks.BEAT_RANGE.min, Scratch3SoundBlocks.BEAT_RANGE.max); - } - - setInstrument (args, util) { - const soundState = this._getSoundState(util.target); - let instNum = Cast.toNumber(args.INSTRUMENT); - instNum -= 1; // instruments are one-indexed - if (typeof this.runtime.audioEngine === 'undefined') return; - instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1); - soundState.currentInstrument = instNum; - return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument); - } - setEffect (args, util) { this._updateEffect(args, util, false); } @@ -273,29 +222,6 @@ class Scratch3SoundBlocks { return soundState.volume; } - setTempo (args) { - const tempo = Cast.toNumber(args.TEMPO); - this._updateTempo(tempo); - } - - changeTempo (args) { - const change = Cast.toNumber(args.TEMPO); - if (typeof this.runtime.audioEngine === 'undefined') return; - const tempo = change + this.runtime.audioEngine.currentTempo; - this._updateTempo(tempo); - } - - _updateTempo (tempo) { - tempo = MathUtil.clamp(tempo, Scratch3SoundBlocks.TEMPO_RANGE.min, Scratch3SoundBlocks.TEMPO_RANGE.max); - if (typeof this.runtime.audioEngine === 'undefined') return; - this.runtime.audioEngine.setTempo(tempo); - } - - getTempo () { - if (typeof this.runtime.audioEngine === 'undefined') return; - return this.runtime.audioEngine.currentTempo; - } - soundsMenu (args) { return args.SOUND_MENU; } From 9bd48e8761384e62223de7850034062cb58e7cb7 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 15:21:20 -0500 Subject: [PATCH 5/8] Mark the buildMenu function private --- src/blocks/scratch3_music.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_music.js b/src/blocks/scratch3_music.js index b7cde9611..cdd9fc33c 100644 --- a/src/blocks/scratch3_music.js +++ b/src/blocks/scratch3_music.js @@ -62,11 +62,12 @@ class Scratch3MusicBlocks { */ this.runtime = runtime; - this.drumMenu = this.buildMenu(drumNames); - this.instrumentMenu = this.buildMenu(instrumentNames); + this.drumMenu = this._buildMenu(drumNames); + this.instrumentMenu = this._buildMenu(instrumentNames); } buildMenu (names) { + _buildMenu (names) { const menu = []; for (let i = 0; i < names.length; i++) { const entry = {}; From 112bb55b9b02494fbf78cddea60d706ef3cff086 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 15:21:47 -0500 Subject: [PATCH 6/8] JSDoc --- src/blocks/scratch3_music.js | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_music.js b/src/blocks/scratch3_music.js index cdd9fc33c..f70fd48ae 100644 --- a/src/blocks/scratch3_music.js +++ b/src/blocks/scratch3_music.js @@ -4,6 +4,10 @@ const Clone = require('../util/clone'); const Cast = require('../util/cast'); const MathUtil = require('../util/math-util'); +/** + * An array of drum names, used in the play drum block. + * @type {string[]} + */ const drumNames = [ 'Snare Drum', 'Bass Drum', @@ -25,6 +29,10 @@ const drumNames = [ 'Open Cuica' ]; +/** + * An array of instrument names, used in the set instrument block. + * @type {string[]} + */ const instrumentNames = [ 'Piano', 'Electric Piano', @@ -66,7 +74,13 @@ class Scratch3MusicBlocks { this.instrumentMenu = this._buildMenu(instrumentNames); } - buildMenu (names) { + /** + * Build a menu using an array of strings. + * Used for creating the drum and instrument menus. + * @param {string[]} names - An array of names. + * @return {array} - An array of objects with text and value properties, for constructing a block menu. + * @private + */ _buildMenu (names) { const menu = []; for (let i = 0; i < names.length; i++) { @@ -232,6 +246,14 @@ class Scratch3MusicBlocks { }; } + /** + * Play a drum sound for some number of beats. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {int} DRUM - the number of the drum to play. + * @property {number} BEATS - the duration in beats of the drum sound. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ playDrumForBeats (args, util) { let drum = Cast.toNumber(args.DRUM); drum -= 1; // drums are one-indexed @@ -243,6 +265,13 @@ class Scratch3MusicBlocks { return util.target.audioPlayer.playDrumForBeats(drum, beats); } + /** + * Rest for some number of beats. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {number} BEATS - the duration in beats of the rest. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ restForBeats (args) { let beats = Cast.toNumber(args.BEATS); beats = this._clampBeats(beats); @@ -250,6 +279,14 @@ class Scratch3MusicBlocks { return this.runtime.audioEngine.waitForBeats(beats); } + /** + * Play a note using the current musical instrument for some number of beats. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number. + * @property {number} BEATS - the duration in beats of the note. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ playNoteForBeats (args, util) { let note = Cast.toNumber(args.NOTE); note = MathUtil.clamp(note, Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max); @@ -261,6 +298,13 @@ class Scratch3MusicBlocks { return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100); } + /** + * Select an instrument for playing notes. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {int} INSTRUMENT - the number of the instrument to select. + * @return {Promise} - a promise which will resolve once the instrument has loaded. + */ setInstrument (args, util) { const musicState = this._getMusicState(util.target); let instNum = Cast.toNumber(args.INSTRUMENT); @@ -271,15 +315,31 @@ class Scratch3MusicBlocks { return this.runtime.audioEngine.instrumentPlayer.loadInstrument(musicState.currentInstrument); } + /** + * Clamp a duration in beats to the allowed min and max duration. + * @param {number} beats - a duration in beats. + * @return {number} - the clamped duration. + * @private + */ _clampBeats (beats) { return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max); } + /** + * Set the current tempo to a new value. + * @param {object} args - the block arguments. + * @property {number} TEMPO - the tempo, in beats per minute. + */ setTempo (args) { const tempo = Cast.toNumber(args.TEMPO); this._updateTempo(tempo); } + /** + * Change the current tempo by some amount. + * @param {object} args - the block arguments. + * @property {number} TEMPO - the amount to change the tempo, in beats per minute. + */ changeTempo (args) { const change = Cast.toNumber(args.TEMPO); if (typeof this.runtime.audioEngine === 'undefined') return; @@ -287,12 +347,21 @@ class Scratch3MusicBlocks { this._updateTempo(tempo); } + /** + * Update the current tempo, clamping it to the min and max allowable range. + * @param {number} tempo - the tempo to set, in beats per minute. + * @private + */ _updateTempo (tempo) { tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max); if (typeof this.runtime.audioEngine === 'undefined') return; this.runtime.audioEngine.setTempo(tempo); } + /** + * Get the current tempo. + * @return {number} - the current tempo, in beats per minute. + */ getTempo () { if (typeof this.runtime.audioEngine === 'undefined') return; return this.runtime.audioEngine.currentTempo; From dc4767646fce8cda2f22c33aed4464f45c3589f8 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 16:26:20 -0500 Subject: [PATCH 7/8] Move music-related unit tests into a separate file --- test/unit/blocks_music.js | 48 ++++++++++++++++++++++++++++++++++++++ test/unit/blocks_sounds.js | 44 +++------------------------------- 2 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 test/unit/blocks_music.js diff --git a/test/unit/blocks_music.js b/test/unit/blocks_music.js new file mode 100644 index 000000000..3b8f8d949 --- /dev/null +++ b/test/unit/blocks_music.js @@ -0,0 +1,48 @@ +const test = require('tap').test; +const Music = require('../../src/blocks/scratch3_music'); +let playedDrum; +let playedInstrument; +const runtime = { + audioEngine: { + numDrums: 3, + numInstruments: 3, + instrumentPlayer: { + loadInstrument: instrument => (playedInstrument = instrument) + } + } +}; +const blocks = new Music(runtime); +const util = { + target: { + audioPlayer: { + playDrumForBeats: drum => (playedDrum = drum) + } + } +}; + +test('playDrum uses 1-indexing and wrap clamps', t => { + let args = {DRUM: 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + args = {DRUM: runtime.audioEngine.numDrums + 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + t.end(); +}); + +test('setInstrument uses 1-indexing and wrap clamps', t => { + // Stub getMusicState + blocks._getMusicState = () => ({}); + + let args = {INSTRUMENT: 1}; + blocks.setInstrument(args, util); + t.strictEqual(playedInstrument, 0); + + args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1}; + blocks.setInstrument(args, util); + t.strictEqual(playedInstrument, 0); + + t.end(); +}); diff --git a/test/unit/blocks_sounds.js b/test/unit/blocks_sounds.js index 753f46f0c..9a44e7f9f 100644 --- a/test/unit/blocks_sounds.js +++ b/test/unit/blocks_sounds.js @@ -1,18 +1,8 @@ const test = require('tap').test; const Sound = require('../../src/blocks/scratch3_sound'); let playedSound; -let playedDrum; -let playedInstrument; -const runtime = { - audioEngine: { - numDrums: 3, - numInstruments: 3, - instrumentPlayer: { - loadInstrument: instrument => (playedInstrument = instrument) - } - } -}; -const blocks = new Sound(runtime); + +const blocks = new Sound(); const util = { target: { sprite: { @@ -24,8 +14,7 @@ const util = { ] }, audioPlayer: { - playSound: soundId => (playedSound = soundId), - playDrumForBeats: drum => (playedDrum = drum) + playSound: soundId => (playedSound = soundId) } } }; @@ -82,30 +71,3 @@ test('playSound prioritizes sound name if given a string', t => { t.strictEqual(playedSound, 'fourth soundId'); t.end(); }); - -test('playDrum uses 1-indexing and wrap clamps', t => { - let args = {DRUM: 1}; - blocks.playDrumForBeats(args, util); - t.strictEqual(playedDrum, 0); - - args = {DRUM: runtime.audioEngine.numDrums + 1}; - blocks.playDrumForBeats(args, util); - t.strictEqual(playedDrum, 0); - - t.end(); -}); - -test('setInstrument uses 1-indexing and wrap clamps', t => { - // Stub getSoundState - blocks._getSoundState = () => ({}); - - let args = {INSTRUMENT: 1}; - blocks.setInstrument(args, util); - t.strictEqual(playedInstrument, 0); - - args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1}; - blocks.setInstrument(args, util); - t.strictEqual(playedInstrument, 0); - - t.end(); -}); From c0fdbc2f4d2f3ddc78c1b39f0476624ac1cf32b2 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 7 Nov 2017 16:26:35 -0500 Subject: [PATCH 8/8] Fix sound integration test --- test/integration/sound.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/integration/sound.js b/test/integration/sound.js index 50100d70f..1c5f20dcd 100644 --- a/test/integration/sound.js +++ b/test/integration/sound.js @@ -1,12 +1,17 @@ +const Worker = require('tiny-worker'); const path = require('path'); const test = require('tap').test; const makeTestStorage = require('../fixtures/make-test-storage'); const extract = require('../fixtures/extract'); const VirtualMachine = require('../../src/index'); +const dispatch = require('../../src/dispatch/central-dispatch'); const uri = path.resolve(__dirname, '../fixtures/sound.sb2'); const project = extract(uri); +// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. +dispatch.workerClass = Worker; + test('sound', t => { const vm = new VirtualMachine(); vm.attachStorage(makeTestStorage());