Merge branch 'develop' into greenkeeper/expose-loader-0.7.4

This commit is contained in:
Andrew Sliwinski 2017-11-20 20:14:03 -05:00 committed by GitHub
commit 51bd0349b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 367 additions and 105 deletions

View file

@ -33,9 +33,9 @@
"copy-webpack-plugin": "4.0.1", "copy-webpack-plugin": "4.0.1",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"eslint": "^4.5.0", "eslint": "^4.5.0",
"eslint-config-scratch": "^4.0.0", "eslint-config-scratch": "^5.0.0",
"expose-loader": "0.7.4", "expose-loader": "0.7.4",
"gh-pages": "^0.12.0", "gh-pages": "^1.1.0",
"got": "5.7.1", "got": "5.7.1",
"highlightjs": "^9.8.0", "highlightjs": "^9.8.0",
"htmlparser2": "3.9.2", "htmlparser2": "3.9.2",
@ -44,18 +44,18 @@
"json": "^9.0.4", "json": "^9.0.4",
"lodash.defaultsdeep": "4.6.0", "lodash.defaultsdeep": "4.6.0",
"minilog": "3.1.0", "minilog": "3.1.0",
"promise": "7.1.1", "promise": "8.0.1",
"scratch-audio": "latest", "scratch-audio": "latest",
"scratch-blocks": "latest", "scratch-blocks": "latest",
"scratch-render": "latest", "scratch-render": "latest",
"scratch-storage": "^0.3.0", "scratch-storage": "^0.3.0",
"script-loader": "0.7.0", "script-loader": "0.7.2",
"socket.io-client": "1.7.3", "socket.io-client": "2.0.4",
"stats.js": "^0.17.0", "stats.js": "^0.17.0",
"tap": "^10.2.0", "tap": "^10.2.0",
"tiny-worker": "^2.1.1", "tiny-worker": "^2.1.1",
"webpack": "^2.4.1", "webpack": "^2.4.1",
"webpack-dev-server": "^2.4.1", "webpack-dev-server": "^2.4.1",
"worker-loader": "0.8.1" "worker-loader": "1.1.0"
} }
} }

View file

@ -25,6 +25,30 @@ class Blocks {
* @type {Array.<String>} * @type {Array.<String>}
*/ */
this._scripts = []; this._scripts = [];
/**
* Runtime Cache
* @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}}
* @private
*/
this._cache = {
/**
* Cache block inputs by block id
* @type {object.<string, !Array.<object>>}
*/
inputs: {},
/**
* Cache procedure Param Names by block id
* @type {object.<string, ?Array.<string>>}
*/
procedureParamNames: {},
/**
* Cache procedure definitions by block id
* @type {object.<string, ?string>}
*/
procedureDefinitions: {}
};
} }
/** /**
@ -105,11 +129,16 @@ class Blocks {
/** /**
* Get all non-branch inputs for a block. * Get all non-branch inputs for a block.
* @param {?object} block the block to query. * @param {?object} block the block to query.
* @return {!object} All non-branch inputs and their associated blocks. * @return {?Array.<object>} All non-branch inputs and their associated blocks.
*/ */
getInputs (block) { getInputs (block) {
if (typeof block === 'undefined') return null; if (typeof block === 'undefined') return null;
const inputs = {}; let inputs = this._cache.inputs[block.id];
if (typeof inputs !== 'undefined') {
return inputs;
}
inputs = {};
for (const input in block.inputs) { for (const input in block.inputs) {
// Ignore blocks prefixed with branch prefix. // Ignore blocks prefixed with branch prefix.
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !== if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
@ -117,6 +146,8 @@ class Blocks {
inputs[input] = block.inputs[input]; inputs[input] = block.inputs[input];
} }
} }
this._cache.inputs[block.id] = inputs;
return inputs; return inputs;
} }
@ -149,16 +180,24 @@ class Blocks {
* @return {?string} ID of procedure definition. * @return {?string} ID of procedure definition.
*/ */
getProcedureDefinition (name) { getProcedureDefinition (name) {
const blockID = this._cache.procedureDefinitions[name];
if (typeof blockID !== 'undefined') {
return blockID;
}
for (const id in this._blocks) { for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue; if (!this._blocks.hasOwnProperty(id)) continue;
const block = this._blocks[id]; const block = this._blocks[id];
if (block.opcode === 'procedures_definition') { if (block.opcode === 'procedures_definition') {
const internal = this._getCustomBlockInternal(block); const internal = this._getCustomBlockInternal(block);
if (internal && internal.mutation.proccode === name) { if (internal && internal.mutation.proccode === name) {
return id; // The outer define block id this._cache.procedureDefinitions[name] = id; // The outer define block id
return id;
} }
} }
} }
this._cache.procedureDefinitions[name] = null;
return null; return null;
} }
@ -168,14 +207,23 @@ class Blocks {
* @return {?Array.<string>} List of param names for a procedure. * @return {?Array.<string>} List of param names for a procedure.
*/ */
getProcedureParamNames (name) { getProcedureParamNames (name) {
const cachedNames = this._cache.procedureParamNames[name];
if (typeof cachedNames !== 'undefined') {
return cachedNames;
}
for (const id in this._blocks) { for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue; if (!this._blocks.hasOwnProperty(id)) continue;
const block = this._blocks[id]; const block = this._blocks[id];
if (block.opcode === 'procedures_prototype' && if (block.opcode === 'procedures_prototype' &&
block.mutation.proccode === name) { block.mutation.proccode === name) {
return JSON.parse(block.mutation.argumentnames); const paramNames = JSON.parse(block.mutation.argumentnames);
this._cache.procedureParamNames[name] = paramNames;
return paramNames;
} }
} }
this._cache.procedureParamNames[name] = null;
return null; return null;
} }
@ -271,6 +319,15 @@ class Blocks {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/**
* Reset all runtime caches.
*/
resetCache () {
this._cache.inputs = {};
this._cache.procedureParamNames = {};
this._cache.procedureDefinitions = {};
}
/** /**
* Block management: create blocks and scripts from a `create` event * Block management: create blocks and scripts from a `create` event
* @param {!object} block Blockly create event to be processed * @param {!object} block Blockly create event to be processed
@ -289,6 +346,8 @@ class Blocks {
if (block.topLevel) { if (block.topLevel) {
this._addScript(block.id); this._addScript(block.id);
} }
this.resetCache();
} }
/** /**
@ -301,7 +360,6 @@ class Blocks {
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return; if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
const block = this._blocks[args.id]; const block = this._blocks[args.id];
if (typeof block === 'undefined') return; if (typeof block === 'undefined') return;
const wasMonitored = block.isMonitored; const wasMonitored = block.isMonitored;
switch (args.element) { switch (args.element) {
case 'field': case 'field':
@ -337,6 +395,8 @@ class Blocks {
} }
break; break;
} }
this.resetCache();
} }
/** /**
@ -393,6 +453,7 @@ class Blocks {
} }
this._blocks[e.id].parent = e.newParent; this._blocks[e.id].parent = e.newParent;
} }
this.resetCache();
} }
@ -447,6 +508,8 @@ class Blocks {
// Delete block itself. // Delete block itself.
delete this._blocks[blockId]; delete this._blocks[blockId];
this.resetCache();
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View file

@ -8,7 +8,7 @@ const BlockType = require('./block-type');
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods // TODO: change extension spec so that library info, including extension ID, can be collected through static methods
const Scratch3PenBlocks = require('../blocks/scratch3_pen'); const Scratch3PenBlocks = require('../blocks/scratch3_pen');
const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2'); const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2');
const Scratch3MusicBlocks = require('../blocks/scratch3_music'); const Scratch3MusicBlocks = require('../extensions/scratch3_music');
const builtinExtensions = { const builtinExtensions = {
pen: Scratch3PenBlocks, pen: Scratch3PenBlocks,
wedo2: Scratch3WeDo2Blocks, wedo2: Scratch3WeDo2Blocks,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,62 +1,9 @@
const ArgumentType = require('../extension-support/argument-type'); const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../extension-support/block-type'); const BlockType = require('../../extension-support/block-type');
const Clone = require('../util/clone'); const Clone = require('../../util/clone');
const Cast = require('../util/cast'); const Cast = require('../../util/cast');
const MathUtil = require('../util/math-util'); const MathUtil = require('../../util/math-util');
const Timer = require('../util/timer'); const Timer = require('../../util/timer');
/**
* An array of drum names, used in the play drum block.
* @type {string[]}
*/
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'
];
/**
* An array of instrument names, used in the set instrument block.
* @type {string[]}
*/
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 * Class for the music-related blocks in Scratch 3.0
@ -78,27 +25,246 @@ class Scratch3MusicBlocks {
*/ */
this.tempo = 60; this.tempo = 60;
this.drumMenu = this._buildMenu(drumNames); /**
this.instrumentMenu = this._buildMenu(instrumentNames); * The number of drum sounds currently being played simultaneously.
* @type {number}
* @private
*/
this._drumConcurrencyCounter = 0;
/**
* An array of audio buffers, one for each drum sound.
* @type {Array}
* @private
*/
this._drumBuffers = [];
this._loadAllDrumSounds();
} }
/** /**
* Build a menu using an array of strings. * Download and decode the full set of drum sounds, and store the audio buffers
* Used for creating the drum and instrument menus. * in the drum buffers array.
* @param {string[]} names - An array of names. * @TODO: Also load the instrument sounds here (rename this fn)
* @return {array} - An array of objects with text and value properties, for constructing a block menu. */
_loadAllDrumSounds () {
const loadingPromises = [];
this.DRUM_INFO.forEach((drumInfo, index) => {
const promise = this._loadSound(drumInfo.fileName, index, this._drumBuffers);
loadingPromises.push(promise);
});
Promise.all(loadingPromises).then(() => {
// @TODO: Update the extension status indicator.
});
}
/**
* Download and decode a sound, and store the buffer in an array.
* @param {string} fileName - 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.
* @return {Promise} - a promise which will resolve once the sound has loaded.
*/
_loadSound (fileName, index, bufferArray) {
if (!this.runtime.storage) return;
if (!this.runtime.audioEngine) return;
return this.runtime.storage.load(this.runtime.storage.AssetType.Sound, fileName, 'mp3')
.then(soundAsset =>
this.runtime.audioEngine.audioContext.decodeAudioData(soundAsset.data.buffer)
)
.then(buffer => {
bufferArray[index] = buffer;
});
}
/**
* Create data for a menu in scratch-blocks format, consisting of an array of objects with text and
* value properties. The text is a translated string, and the value is one-indexed.
* @param {object[]} info - An array of info objects each having a name property.
* @return {array} - An array of objects with text and value properties.
* @private * @private
*/ */
_buildMenu (names) { _buildMenu (info) {
const menu = []; return info.map((entry, index) => {
for (let i = 0; i < names.length; i++) { const obj = {};
const entry = {}; obj.text = entry.name;
const num = i + 1; // Menu numbers are one-indexed obj.value = index + 1;
entry.text = `(${num}) ${names[i]}`; return obj;
entry.value = String(num); });
menu.push(entry);
} }
return menu;
/**
* An array of translatable drum names and corresponding audio file names.
* @type {array}
*/
get DRUM_INFO () {
return [
{
name: '(1) Snare Drum',
fileName: '1-snare'
},
{
name: '(2) Bass Drum',
fileName: '2-bass-drum'
},
{
name: '(3) Side Stick',
fileName: '3-side-stick'
},
{
name: '(4) Crash Cymbal',
fileName: '4-crash-cymbal'
},
{
name: '(5) Open Hi-Hat',
fileName: '5-open-hi-hat'
},
{
name: '(6) Closed Hi-Hat',
fileName: '6-closed-hi-hat'
},
{
name: '(7) Tambourine',
fileName: '7-tambourine'
},
{
name: '(8) Hand Clap',
fileName: '8-hand-clap'
},
{
name: '(9) Claves',
fileName: '9-claves'
},
{
name: '(10) Wood Block',
fileName: '10-wood-block'
},
{
name: '(11) Cowbell',
fileName: '11-cowbell'
},
{
name: '(12) Triangle',
fileName: '12-triangle'
},
{
name: '(13) Bongo',
fileName: '13-bongo'
},
{
name: '(14) Conga',
fileName: '14-conga'
},
{
name: '(15) Cabasa',
fileName: '15-cabasa'
},
{
name: '(16) Guiro',
fileName: '16-guiro'
},
{
name: '(17) Vibraslap',
fileName: '17-vibraslap'
},
{
name: '(18) Cuica',
fileName: '18-cuica'
}
];
}
/**
* An array of translatable instrument names and corresponding audio file names.
* @type {array}
*/
get INSTRUMENT_INFO () {
return [
{
name: '(1) Piano',
fileName: '1-piano'
},
{
name: '(2) Electric Piano',
fileName: '2-electric-piano'
},
{
name: '(3) Organ',
fileName: '3-organ'
},
{
name: '(4) Guitar',
fileName: '4-guitar'
},
{
name: '(5) Electric Guitar',
fileName: '5-electric-guitar'
},
{
name: '(6) Bass',
fileName: '6-bass'
},
{
name: '(7) Pizzicato',
fileName: '7-pizzicato'
},
{
name: '(8) Cello',
fileName: '8-cello'
},
{
name: '(9) Trombone',
fileName: '9-trombone'
},
{
name: '(10) Clarinet',
fileName: '10-clarinet'
},
{
name: '(11) Saxophone',
fileName: '11-saxophone'
},
{
name: '(12) Flute',
fileName: '12-flute'
},
{
name: '(13) Wooden Flute',
fileName: '13-wooden-flute'
},
{
name: '(14) Bassoon',
fileName: '14-bassoon'
},
{
name: '(15) Choir',
fileName: '15-choir'
},
{
name: '(16) Vibraphone',
fileName: '16-vibraphone'
},
{
name: '(17) Music Box',
fileName: '17-music-box'
},
{
name: '(18) Steel Drum',
fileName: '18-steel-drum'
},
{
name: '(19) Marimba',
fileName: '19-marimba'
},
{
name: '(20) Synth Lead',
fileName: '20-synth-lead'
},
{
name: '(21) Synth Pad',
fileName: '21-synth-pad'
}
];
} }
/** /**
@ -143,6 +309,14 @@ class Scratch3MusicBlocks {
return {min: 20, max: 500}; return {min: 20, max: 500};
} }
/**
* The maximum number of sounds to allow to play simultaneously.
* @type {number}
*/
static get CONCURRENCY_LIMIT () {
return 30;
}
/** /**
* @param {Target} target - collect music state for this target. * @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. * @returns {MusicState} the mutable music state associated with that target. This will be created if necessary.
@ -248,8 +422,8 @@ class Scratch3MusicBlocks {
} }
], ],
menus: { menus: {
drums: this.drumMenu, drums: this._buildMenu(this.DRUM_INFO),
instruments: this.instrumentMenu instruments: this._buildMenu(this.INSTRUMENT_INFO)
} }
}; };
} }
@ -264,20 +438,41 @@ class Scratch3MusicBlocks {
playDrumForBeats (args, util) { playDrumForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) { if (this._stackTimerNeedsInit(util)) {
let drum = Cast.toNumber(args.DRUM); let drum = Cast.toNumber(args.DRUM);
drum = Math.round(drum);
drum -= 1; // drums are one-indexed drum -= 1; // drums are one-indexed
if (typeof this.runtime.audioEngine === 'undefined') return; drum = MathUtil.wrapClamp(drum, 0, this.DRUM_INFO.length - 1);
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
let beats = Cast.toNumber(args.BEATS); let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats); beats = this._clampBeats(beats);
if (util.target.audioPlayer !== null) { this._playDrumNum(util, drum);
util.target.audioPlayer.playDrumForBeats(drum, beats);
}
this._startStackTimer(util, this._beatsToSec(beats)); this._startStackTimer(util, this._beatsToSec(beats));
} else { } else {
this._checkStackTimer(util); this._checkStackTimer(util);
} }
} }
/**
* Play a drum sound using its 0-indexed number.
* @param {object} util - utility object provided by the runtime.
* @param {number} drumNum - the number of the drum to play.
* @private
*/
_playDrumNum (util, drumNum) {
if (util.target.audioPlayer === null) return;
// If we're playing too many sounds, do not play the drum sound.
if (this._drumConcurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
return;
}
const outputNode = util.target.audioPlayer.getInputNode();
const bufferSource = this.runtime.audioEngine.audioContext.createBufferSource();
bufferSource.buffer = this._drumBuffers[drumNum];
bufferSource.connect(outputNode);
bufferSource.start();
this._drumConcurrencyCounter++;
bufferSource.onended = () => {
this._drumConcurrencyCounter--;
};
}
/** /**
* Rest for some number of beats. * Rest for some number of beats.
* @param {object} args - the block arguments. * @param {object} args - the block arguments.
@ -359,10 +554,8 @@ class Scratch3MusicBlocks {
util.stackFrame.timer = new Timer(); util.stackFrame.timer = new Timer();
util.stackFrame.timer.start(); util.stackFrame.timer.start();
util.stackFrame.duration = duration; util.stackFrame.duration = duration;
if (util.stackFrame.duration > 0) {
util.yield(); util.yield();
} }
}
/** /**
* Check the stack timer, and if its time is not up yet, yield the thread. * Check the stack timer, and if its time is not up yet, yield the thread.

View file

@ -1,10 +1,9 @@
const test = require('tap').test; const test = require('tap').test;
const Music = require('../../src/blocks/scratch3_music'); const Music = require('../../src/extensions/scratch3_music/index.js');
let playedDrum; let playedDrum;
let playedInstrument; let playedInstrument;
const runtime = { const runtime = {
audioEngine: { audioEngine: {
numDrums: 3,
numInstruments: 3, numInstruments: 3,
instrumentPlayer: { instrumentPlayer: {
loadInstrument: instrument => (playedInstrument = instrument) loadInstrument: instrument => (playedInstrument = instrument)
@ -12,13 +11,14 @@ const runtime = {
} }
}; };
const blocks = new Music(runtime); const blocks = new Music(runtime);
blocks._playDrumNum = (util, drum) => (playedDrum = drum);
const util = { const util = {
stackFrame: Object.create(null),
target: { target: {
audioPlayer: { audioPlayer: null
playDrumForBeats: drum => (playedDrum = drum)
}
}, },
stackFrame: Object.create(null) yield: () => null
}; };
test('playDrum uses 1-indexing and wrap clamps', t => { test('playDrum uses 1-indexing and wrap clamps', t => {
@ -26,7 +26,7 @@ test('playDrum uses 1-indexing and wrap clamps', t => {
blocks.playDrumForBeats(args, util); blocks.playDrumForBeats(args, util);
t.strictEqual(playedDrum, 0); t.strictEqual(playedDrum, 0);
args = {DRUM: runtime.audioEngine.numDrums + 1}; args = {DRUM: blocks.DRUM_INFO.length + 1};
blocks.playDrumForBeats(args, util); blocks.playDrumForBeats(args, util);
t.strictEqual(playedDrum, 0); t.strictEqual(playedDrum, 0);

View file

@ -60,7 +60,13 @@ module.exports = [
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
path: path.resolve(__dirname, 'dist/node'), path: path.resolve(__dirname, 'dist/node'),
filename: '[name].js' filename: '[name].js'
} },
plugins: base.plugins.concat([
new CopyWebpackPlugin([{
from: './src/extensions/scratch3_music/assets',
to: 'assets/scratch3_music'
}])
])
}), }),
// Playground // Playground
defaultsDeep({}, base, { defaultsDeep({}, base, {