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",
"escape-html": "1.0.3",
"eslint": "^4.5.0",
"eslint-config-scratch": "^4.0.0",
"eslint-config-scratch": "^5.0.0",
"expose-loader": "0.7.4",
"gh-pages": "^0.12.0",
"gh-pages": "^1.1.0",
"got": "5.7.1",
"highlightjs": "^9.8.0",
"htmlparser2": "3.9.2",
@ -44,18 +44,18 @@
"json": "^9.0.4",
"lodash.defaultsdeep": "4.6.0",
"minilog": "3.1.0",
"promise": "7.1.1",
"promise": "8.0.1",
"scratch-audio": "latest",
"scratch-blocks": "latest",
"scratch-render": "latest",
"scratch-storage": "^0.3.0",
"script-loader": "0.7.0",
"socket.io-client": "1.7.3",
"script-loader": "0.7.2",
"socket.io-client": "2.0.4",
"stats.js": "^0.17.0",
"tap": "^10.2.0",
"tiny-worker": "^2.1.1",
"webpack": "^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>}
*/
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.
* @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) {
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) {
// Ignore blocks prefixed with branch prefix.
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
@ -117,6 +146,8 @@ class Blocks {
inputs[input] = block.inputs[input];
}
}
this._cache.inputs[block.id] = inputs;
return inputs;
}
@ -149,16 +180,24 @@ class Blocks {
* @return {?string} ID of procedure definition.
*/
getProcedureDefinition (name) {
const blockID = this._cache.procedureDefinitions[name];
if (typeof blockID !== 'undefined') {
return blockID;
}
for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue;
const block = this._blocks[id];
if (block.opcode === 'procedures_definition') {
const internal = this._getCustomBlockInternal(block);
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;
}
@ -168,14 +207,23 @@ class Blocks {
* @return {?Array.<string>} List of param names for a procedure.
*/
getProcedureParamNames (name) {
const cachedNames = this._cache.procedureParamNames[name];
if (typeof cachedNames !== 'undefined') {
return cachedNames;
}
for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue;
const block = this._blocks[id];
if (block.opcode === 'procedures_prototype' &&
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;
}
@ -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
* @param {!object} block Blockly create event to be processed
@ -289,6 +346,8 @@ class Blocks {
if (block.topLevel) {
this._addScript(block.id);
}
this.resetCache();
}
/**
@ -301,7 +360,6 @@ class Blocks {
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
const block = this._blocks[args.id];
if (typeof block === 'undefined') return;
const wasMonitored = block.isMonitored;
switch (args.element) {
case 'field':
@ -337,6 +395,8 @@ class Blocks {
}
break;
}
this.resetCache();
}
/**
@ -393,6 +453,7 @@ class Blocks {
}
this._blocks[e.id].parent = e.newParent;
}
this.resetCache();
}
@ -447,6 +508,8 @@ class Blocks {
// Delete block itself.
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
const Scratch3PenBlocks = require('../blocks/scratch3_pen');
const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2');
const Scratch3MusicBlocks = require('../blocks/scratch3_music');
const Scratch3MusicBlocks = require('../extensions/scratch3_music');
const builtinExtensions = {
pen: Scratch3PenBlocks,
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 BlockType = require('../extension-support/block-type');
const Clone = require('../util/clone');
const Cast = require('../util/cast');
const MathUtil = require('../util/math-util');
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'
];
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 Timer = require('../../util/timer');
/**
* Class for the music-related blocks in Scratch 3.0
@ -78,27 +25,246 @@ class Scratch3MusicBlocks {
*/
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.
* 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.
* Download and decode the full set of drum sounds, and store the audio buffers
* in the drum buffers array.
* @TODO: Also load the instrument sounds here (rename this fn)
*/
_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
*/
_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);
_buildMenu (info) {
return info.map((entry, index) => {
const obj = {};
obj.text = entry.name;
obj.value = index + 1;
return obj;
});
}
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};
}
/**
* 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.
* @returns {MusicState} the mutable music state associated with that target. This will be created if necessary.
@ -248,8 +422,8 @@ class Scratch3MusicBlocks {
}
],
menus: {
drums: this.drumMenu,
instruments: this.instrumentMenu
drums: this._buildMenu(this.DRUM_INFO),
instruments: this._buildMenu(this.INSTRUMENT_INFO)
}
};
}
@ -264,20 +438,41 @@ class Scratch3MusicBlocks {
playDrumForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) {
let drum = Cast.toNumber(args.DRUM);
drum = Math.round(drum);
drum -= 1; // drums are one-indexed
if (typeof this.runtime.audioEngine === 'undefined') return;
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
drum = MathUtil.wrapClamp(drum, 0, this.DRUM_INFO.length - 1);
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
if (util.target.audioPlayer !== null) {
util.target.audioPlayer.playDrumForBeats(drum, beats);
}
this._playDrumNum(util, drum);
this._startStackTimer(util, this._beatsToSec(beats));
} else {
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.
* @param {object} args - the block arguments.
@ -359,10 +554,8 @@ class Scratch3MusicBlocks {
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.stackFrame.duration = duration;
if (util.stackFrame.duration > 0) {
util.yield();
}
}
/**
* 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 Music = require('../../src/blocks/scratch3_music');
const Music = require('../../src/extensions/scratch3_music/index.js');
let playedDrum;
let playedInstrument;
const runtime = {
audioEngine: {
numDrums: 3,
numInstruments: 3,
instrumentPlayer: {
loadInstrument: instrument => (playedInstrument = instrument)
@ -12,13 +11,14 @@ const runtime = {
}
};
const blocks = new Music(runtime);
blocks._playDrumNum = (util, drum) => (playedDrum = drum);
const util = {
stackFrame: Object.create(null),
target: {
audioPlayer: {
playDrumForBeats: drum => (playedDrum = drum)
}
audioPlayer: null
},
stackFrame: Object.create(null)
yield: () => null
};
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);
t.strictEqual(playedDrum, 0);
args = {DRUM: runtime.audioEngine.numDrums + 1};
args = {DRUM: blocks.DRUM_INFO.length + 1};
blocks.playDrumForBeats(args, util);
t.strictEqual(playedDrum, 0);

View file

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