@scratch/scratch-vm 11.0.0-UEPR-176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/dist/node/extension-worker.js +2 -0
- package/dist/node/extension-worker.js.map +1 -0
- package/dist/node/scratch-vm.js +3 -0
- package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
- package/dist/node/scratch-vm.js.map +1 -0
- package/dist/web/extension-worker.js +2 -0
- package/dist/web/extension-worker.js.map +1 -0
- package/dist/web/scratch-vm.js +3 -0
- package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
- package/dist/web/scratch-vm.js.map +1 -0
- package/package.json +108 -0
- package/src/.eslintrc.js +7 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
const ArgumentType = require('../../extension-support/argument-type');
|
|
2
|
+
const BlockType = require('../../extension-support/block-type');
|
|
3
|
+
const Clone = require('../../util/clone');
|
|
4
|
+
const Cast = require('../../util/cast');
|
|
5
|
+
const formatMessage = require('format-message');
|
|
6
|
+
const MathUtil = require('../../util/math-util');
|
|
7
|
+
const Timer = require('../../util/timer');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The instrument and drum sounds, loaded as static assets.
|
|
11
|
+
* @type {object}
|
|
12
|
+
*/
|
|
13
|
+
let assetData = {};
|
|
14
|
+
try {
|
|
15
|
+
assetData = require('./manifest');
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// Non-webpack environment, don't worry about assets.
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line max-len
|
|
25
|
+
const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPm11c2ljLWJsb2NrLWljb248L3RpdGxlPjxkZWZzPjxwYXRoIGQ9Ik0zMi4xOCAyNS44NzRDMzIuNjM2IDI4LjE1NyAzMC41MTIgMzAgMjcuNDMzIDMwYy0zLjA3IDAtNS45MjMtMS44NDMtNi4zNzItNC4xMjYtLjQ1OC0yLjI4NSAxLjY2NS00LjEzNiA0Ljc0My00LjEzNi42NDcgMCAxLjI4My4wODQgMS44OS4yMzQuMzM4LjA4Ni42MzcuMTguOTM4LjMwMi44Ny0uMDItLjEwNC0yLjI5NC0xLjgzNS0xMi4yMy0yLjEzNC0xMi4zMDIgMy4wNi0xLjg3IDguNzY4LTIuNzUyIDUuNzA4LS44ODUuMDc2IDQuODItMy42NSAzLjg0NC0zLjcyNC0uOTg3LTQuNjUtNy4xNTMuMjYzIDE0LjczOHptLTE2Ljk5OCA1Ljk5QzE1LjYzIDM0LjE0OCAxMy41MDcgMzYgMTAuNDQgMzZjLTMuMDcgMC01LjkyMi0xLjg1Mi02LjM4LTQuMTM2LS40NDgtMi4yODQgMS42NzQtNC4xMzUgNC43NS00LjEzNSAxLjAwMyAwIDEuOTc1LjE5NiAyLjg1NS41NDMuODIyLS4wNTUtLjE1LTIuMzc3LTEuODYyLTEyLjIyOC0yLjEzMy0xMi4zMDMgMy4wNi0xLjg3IDguNzY0LTIuNzUzIDUuNzA2LS44OTQuMDc2IDQuODItMy42NDggMy44MzQtMy43MjQtLjk4Ny00LjY1LTcuMTUyLjI2MiAxNC43Mzh6IiBpZD0iYSIvPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjx1c2UgZmlsbD0iI0ZGRiIgeGxpbms6aHJlZj0iI2EiLz48cGF0aCBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZT0iIzAwMCIgZD0iTTI4LjQ1NiAyMS42NzVjLS4wMS0uMzEyLS4wODctLjgyNS0uMjU2LTEuNzAyLS4wOTYtLjQ5NS0uNjEyLTMuMDIyLS43NTMtMy43My0uMzk1LTEuOTgtLjc2LTMuOTItMS4xNDItNi4xMTMtLjczMi00LjIyMy0uNjkzLTYuMDUuMzQ0LTYuNTI3LjUtLjIzIDEuMDYtLjA4IDEuODQuMzUuNDE0LjIyNyAyLjE4MiAxLjM2NSAyLjA3IDEuMjk2IDEuOTk0IDEuMjQyIDMuNDY0IDEuNzc0IDQuOTMgMS41NDggMS41MjYtLjIzNyAyLjUwNC0uMDYgMi44NzYuNjE4LjM0OC42MzUuMDE1IDEuNDE2LS43MyAyLjE4LTEuNDcyIDEuNTE2LTMuOTc1IDIuNTE0LTUuODQ4IDIuMDIzLS44MjItLjIyLTEuMjM4LS40NjUtMi4zOC0xLjI2N2wtLjA5NS0uMDY2Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMy4yOTQgMS4zMzYgMi4wOCA5LjE4NyAyLjYzNyAxMS42NzRsLjAwMi4wMTJjLjUyOCAyLjYzNy0xLjg3MyA0LjcyNC01LjIzNiA0LjcyNC0zLjI5IDAtNi4zNjMtMS45ODgtNi44NjItNC41MjgtLjUzLTIuNjQgMS44NzMtNC43MzQgNS4yMzMtNC43MzQuNjcyIDAgMS4zNDcuMDg1IDIuMDE0LjI1LjIyNy4wNTcuNDM2LjExOC42MzYuMTg3em0tMTYuOTk2IDUuOTljLS4wMS0uMzE4LS4wOS0uODM4LS4yNjYtMS43MzctLjA5LS40Ni0uNTk1LTIuOTM3LS43NTMtMy43MjctLjM5LTEuOTYtLjc1LTMuODktMS4xMy02LjA3LS43MzItNC4yMjMtLjY5Mi02LjA1LjM0NC02LjUyNi41MDItLjIzIDEuMDYtLjA4MiAxLjg0LjM1LjQxNS4yMjcgMi4xODIgMS4zNjQgMi4wNyAxLjI5NSAxLjk5MyAxLjI0MiAzLjQ2MiAxLjc3NCA0LjkyNiAxLjU0OCAxLjUyNS0uMjQgMi41MDQtLjA2NCAyLjg3Ni42MTQuMzQ4LjYzNS4wMTUgMS40MTUtLjcyOCAyLjE4LTEuNDc0IDEuNTE3LTMuOTc3IDIuNTEzLTUuODQ3IDIuMDE3LS44Mi0uMjItMS4yMzYtLjQ2NC0yLjM3OC0xLjI2N2wtLjA5NS0uMDY1Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMi4yOTQgMS4zMzcgMi4wNzggOS4xOSAyLjYzNiAxMS42NzVsLjAwMy4wMTNjLjUxNyAyLjYzOC0xLjg4NCA0LjczMi01LjIzNCA0LjczMi0zLjI4NyAwLTYuMzYtMS45OTMtNi44Ny00LjU0LS41Mi0yLjY0IDEuODg0LTQuNzMgNS4yNC00LjczLjkwNSAwIDEuODAzLjE1IDIuNjUuNDM2eiIvPjwvZz48L3N2Zz4=';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Icon svg to be displayed in the category menu, encoded as a data URI.
|
|
29
|
+
* @type {string}
|
|
30
|
+
*/
|
|
31
|
+
// eslint-disable-next-line max-len
|
|
32
|
+
const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE2LjA5IDEyLjkzN2MuMjI4IDEuMTQxLS44MzMgMi4wNjMtMi4zNzMgMi4wNjMtMS41MzUgMC0yLjk2Mi0uOTIyLTMuMTg2LTIuMDYzLS4yMy0xLjE0Mi44MzMtMi4wNjggMi4zNzItMi4wNjguMzIzIDAgLjY0MS4wNDIuOTQ1LjExN2EzLjUgMy41IDAgMCAxIC40NjguMTUxYy40MzUtLjAxLS4wNTItMS4xNDctLjkxNy02LjExNC0xLjA2Ny02LjE1MiAxLjUzLS45MzUgNC4zODQtMS4zNzcgMi44NTQtLjQ0Mi4wMzggMi40MS0xLjgyNSAxLjkyMi0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc3LjEzMiA3LjM3ek03LjQ2IDguNTYzYy0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc2LjEzIDcuMzdDNy44MTYgMTcuMDczIDYuNzU0IDE4IDUuMjIgMThjLTEuNTM1IDAtMi45NjEtLjkyNi0zLjE5LTIuMDY4LS4yMjQtMS4xNDIuODM3LTIuMDY3IDIuMzc1LTIuMDY3LjUwMSAwIC45ODcuMDk4IDEuNDI3LjI3Mi40MTItLjAyOC0uMDc0LTEuMTg5LS45My02LjExNEMzLjgzNCAxLjg3IDYuNDMgNy4wODcgOS4yODIgNi42NDZjMi44NTQtLjQ0Ny4wMzggMi40MS0xLjgyMyAxLjkxN3oiIGZpbGw9IiM1NzVFNzUiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Class for the music-related blocks in Scratch 3.0
|
|
36
|
+
* @param {Runtime} runtime - the runtime instantiating this block package.
|
|
37
|
+
* @constructor
|
|
38
|
+
*/
|
|
39
|
+
class Scratch3MusicBlocks {
|
|
40
|
+
constructor (runtime) {
|
|
41
|
+
/**
|
|
42
|
+
* The runtime instantiating this block package.
|
|
43
|
+
* @type {Runtime}
|
|
44
|
+
*/
|
|
45
|
+
this.runtime = runtime;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The number of drum and instrument sounds currently being played simultaneously.
|
|
49
|
+
* @type {number}
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
this._concurrencyCounter = 0;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* An array of sound players, one for each drum sound.
|
|
56
|
+
* @type {Array}
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
this._drumPlayers = [];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* An array of arrays of sound players. Each instrument has one or more audio players.
|
|
63
|
+
* @type {Array[]}
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
this._instrumentPlayerArrays = [];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* An array of arrays of sound players. Each instrument mya have an audio player for each playable note.
|
|
70
|
+
* @type {Array[]}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
this._instrumentPlayerNoteArrays = [];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An array of audio bufferSourceNodes. Each time you play an instrument or drum sound,
|
|
77
|
+
* a bufferSourceNode is created. We keep references to them to make sure their onended
|
|
78
|
+
* events can fire.
|
|
79
|
+
* @type {Array}
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
this._bufferSources = [];
|
|
83
|
+
|
|
84
|
+
this._loadAllSounds();
|
|
85
|
+
|
|
86
|
+
this._onTargetCreated = this._onTargetCreated.bind(this);
|
|
87
|
+
this.runtime.on('targetWasCreated', this._onTargetCreated);
|
|
88
|
+
|
|
89
|
+
this._playNoteForPicker = this._playNoteForPicker.bind(this);
|
|
90
|
+
this.runtime.on('PLAY_NOTE', this._playNoteForPicker);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decode the full set of drum and instrument sounds, and store the audio buffers in arrays.
|
|
95
|
+
*/
|
|
96
|
+
_loadAllSounds () {
|
|
97
|
+
const loadingPromises = [];
|
|
98
|
+
this.DRUM_INFO.forEach((drumInfo, index) => {
|
|
99
|
+
const filePath = `drums/${drumInfo.fileName}`;
|
|
100
|
+
const promise = this._storeSound(filePath, index, this._drumPlayers);
|
|
101
|
+
loadingPromises.push(promise);
|
|
102
|
+
});
|
|
103
|
+
this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => {
|
|
104
|
+
this._instrumentPlayerArrays[instrumentIndex] = [];
|
|
105
|
+
this._instrumentPlayerNoteArrays[instrumentIndex] = [];
|
|
106
|
+
instrumentInfo.samples.forEach((sample, noteIndex) => {
|
|
107
|
+
const filePath = `instruments/${instrumentInfo.dirName}/${sample}`;
|
|
108
|
+
const promise = this._storeSound(filePath, noteIndex, this._instrumentPlayerArrays[instrumentIndex]);
|
|
109
|
+
loadingPromises.push(promise);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
Promise.all(loadingPromises).then(() => {
|
|
113
|
+
// @TODO: Update the extension status indicator.
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decode a sound and store the player in an array.
|
|
119
|
+
* @param {string} filePath - the audio file name.
|
|
120
|
+
* @param {number} index - the index at which to store the audio player.
|
|
121
|
+
* @param {array} playerArray - the array of players in which to store it.
|
|
122
|
+
* @return {Promise} - a promise which will resolve once the sound has been stored.
|
|
123
|
+
*/
|
|
124
|
+
_storeSound (filePath, index, playerArray) {
|
|
125
|
+
const fullPath = `${filePath}.mp3`;
|
|
126
|
+
|
|
127
|
+
if (!assetData[fullPath]) return;
|
|
128
|
+
|
|
129
|
+
// The sound player has already been downloaded via the manifest file required above.
|
|
130
|
+
const soundBuffer = assetData[fullPath];
|
|
131
|
+
|
|
132
|
+
return this._decodeSound(soundBuffer).then(player => {
|
|
133
|
+
playerArray[index] = player;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decode a sound and return a promise with the audio buffer.
|
|
139
|
+
* @param {ArrayBuffer} soundBuffer - a buffer containing the encoded audio.
|
|
140
|
+
* @return {Promise} - a promise which will resolve once the sound has decoded.
|
|
141
|
+
*/
|
|
142
|
+
_decodeSound (soundBuffer) {
|
|
143
|
+
const engine = this.runtime.audioEngine;
|
|
144
|
+
|
|
145
|
+
if (!engine) {
|
|
146
|
+
return Promise.reject(new Error('No Audio Context Detected'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for newer promise-based API
|
|
150
|
+
return engine.decodeSoundPlayer({data: {buffer: soundBuffer}});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create data for a menu in scratch-blocks format, consisting of an array of objects with text and
|
|
155
|
+
* value properties. The text is a translated string, and the value is one-indexed.
|
|
156
|
+
* @param {object[]} info - An array of info objects each having a name property.
|
|
157
|
+
* @return {array} - An array of objects with text and value properties.
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
_buildMenu (info) {
|
|
161
|
+
return info.map((entry, index) => {
|
|
162
|
+
const obj = {};
|
|
163
|
+
obj.text = entry.name;
|
|
164
|
+
obj.value = String(index + 1);
|
|
165
|
+
return obj;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* An array of info about each drum.
|
|
171
|
+
* @type {object[]}
|
|
172
|
+
* @param {string} name - the translatable name to display in the drums menu.
|
|
173
|
+
* @param {string} fileName - the name of the audio file containing the drum sound.
|
|
174
|
+
*/
|
|
175
|
+
get DRUM_INFO () {
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
name: formatMessage({
|
|
179
|
+
id: 'music.drumSnare',
|
|
180
|
+
default: '(1) Snare Drum',
|
|
181
|
+
description: 'Sound of snare drum as used in a standard drum kit'
|
|
182
|
+
}),
|
|
183
|
+
fileName: '1-snare'
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: formatMessage({
|
|
187
|
+
id: 'music.drumBass',
|
|
188
|
+
default: '(2) Bass Drum',
|
|
189
|
+
description: 'Sound of bass drum as used in a standard drum kit'
|
|
190
|
+
}),
|
|
191
|
+
fileName: '2-bass-drum'
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: formatMessage({
|
|
195
|
+
id: 'music.drumSideStick',
|
|
196
|
+
default: '(3) Side Stick',
|
|
197
|
+
description: 'Sound of a drum stick hitting the side of a drum (usually the snare)'
|
|
198
|
+
}),
|
|
199
|
+
fileName: '3-side-stick'
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: formatMessage({
|
|
203
|
+
id: 'music.drumCrashCymbal',
|
|
204
|
+
default: '(4) Crash Cymbal',
|
|
205
|
+
description: 'Sound of a drum stick hitting a crash cymbal'
|
|
206
|
+
}),
|
|
207
|
+
fileName: '4-crash-cymbal'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: formatMessage({
|
|
211
|
+
id: 'music.drumOpenHiHat',
|
|
212
|
+
default: '(5) Open Hi-Hat',
|
|
213
|
+
description: 'Sound of a drum stick hitting a hi-hat while open'
|
|
214
|
+
}),
|
|
215
|
+
fileName: '5-open-hi-hat'
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: formatMessage({
|
|
219
|
+
id: 'music.drumClosedHiHat',
|
|
220
|
+
default: '(6) Closed Hi-Hat',
|
|
221
|
+
description: 'Sound of a drum stick hitting a hi-hat while closed'
|
|
222
|
+
}),
|
|
223
|
+
fileName: '6-closed-hi-hat'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: formatMessage({
|
|
227
|
+
id: 'music.drumTambourine',
|
|
228
|
+
default: '(7) Tambourine',
|
|
229
|
+
description: 'Sound of a tambourine being struck'
|
|
230
|
+
}),
|
|
231
|
+
fileName: '7-tambourine'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: formatMessage({
|
|
235
|
+
id: 'music.drumHandClap',
|
|
236
|
+
default: '(8) Hand Clap',
|
|
237
|
+
description: 'Sound of two hands clapping together'
|
|
238
|
+
}),
|
|
239
|
+
fileName: '8-hand-clap'
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: formatMessage({
|
|
243
|
+
id: 'music.drumClaves',
|
|
244
|
+
default: '(9) Claves',
|
|
245
|
+
description: 'Sound of claves being struck together'
|
|
246
|
+
}),
|
|
247
|
+
fileName: '9-claves'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: formatMessage({
|
|
251
|
+
id: 'music.drumWoodBlock',
|
|
252
|
+
default: '(10) Wood Block',
|
|
253
|
+
description: 'Sound of a wood block being struck'
|
|
254
|
+
}),
|
|
255
|
+
fileName: '10-wood-block'
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: formatMessage({
|
|
259
|
+
id: 'music.drumCowbell',
|
|
260
|
+
default: '(11) Cowbell',
|
|
261
|
+
description: 'Sound of a cowbell being struck'
|
|
262
|
+
}),
|
|
263
|
+
fileName: '11-cowbell'
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: formatMessage({
|
|
267
|
+
id: 'music.drumTriangle',
|
|
268
|
+
default: '(12) Triangle',
|
|
269
|
+
description: 'Sound of a triangle (instrument) being struck'
|
|
270
|
+
}),
|
|
271
|
+
fileName: '12-triangle'
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: formatMessage({
|
|
275
|
+
id: 'music.drumBongo',
|
|
276
|
+
default: '(13) Bongo',
|
|
277
|
+
description: 'Sound of a bongo being struck'
|
|
278
|
+
}),
|
|
279
|
+
fileName: '13-bongo'
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: formatMessage({
|
|
283
|
+
id: 'music.drumConga',
|
|
284
|
+
default: '(14) Conga',
|
|
285
|
+
description: 'Sound of a conga being struck'
|
|
286
|
+
}),
|
|
287
|
+
fileName: '14-conga'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: formatMessage({
|
|
291
|
+
id: 'music.drumCabasa',
|
|
292
|
+
default: '(15) Cabasa',
|
|
293
|
+
description: 'Sound of a cabasa being shaken'
|
|
294
|
+
}),
|
|
295
|
+
fileName: '15-cabasa'
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: formatMessage({
|
|
299
|
+
id: 'music.drumGuiro',
|
|
300
|
+
default: '(16) Guiro',
|
|
301
|
+
description: 'Sound of a guiro being played'
|
|
302
|
+
}),
|
|
303
|
+
fileName: '16-guiro'
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: formatMessage({
|
|
307
|
+
id: 'music.drumVibraslap',
|
|
308
|
+
default: '(17) Vibraslap',
|
|
309
|
+
description: 'Sound of a Vibraslap being played'
|
|
310
|
+
}),
|
|
311
|
+
fileName: '17-vibraslap'
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: formatMessage({
|
|
315
|
+
id: 'music.drumCuica',
|
|
316
|
+
default: '(18) Cuica',
|
|
317
|
+
description: 'Sound of a cuica being played'
|
|
318
|
+
}),
|
|
319
|
+
fileName: '18-cuica'
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* An array of info about each instrument.
|
|
326
|
+
* @type {object[]}
|
|
327
|
+
* @param {string} name - the translatable name to display in the instruments menu.
|
|
328
|
+
* @param {string} dirName - the name of the directory containing audio samples for this instrument.
|
|
329
|
+
* @param {number} [releaseTime] - an optional duration for the release portion of each note.
|
|
330
|
+
* @param {number[]} samples - an array of numbers representing the MIDI note number for each
|
|
331
|
+
* sampled sound used to play this instrument.
|
|
332
|
+
*/
|
|
333
|
+
get INSTRUMENT_INFO () {
|
|
334
|
+
return [
|
|
335
|
+
{
|
|
336
|
+
name: formatMessage({
|
|
337
|
+
id: 'music.instrumentPiano',
|
|
338
|
+
default: '(1) Piano',
|
|
339
|
+
description: 'Sound of a piano'
|
|
340
|
+
}),
|
|
341
|
+
dirName: '1-piano',
|
|
342
|
+
releaseTime: 0.5,
|
|
343
|
+
samples: [24, 36, 48, 60, 72, 84, 96, 108]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: formatMessage({
|
|
347
|
+
id: 'music.instrumentElectricPiano',
|
|
348
|
+
default: '(2) Electric Piano',
|
|
349
|
+
description: 'Sound of an electric piano'
|
|
350
|
+
}),
|
|
351
|
+
dirName: '2-electric-piano',
|
|
352
|
+
releaseTime: 0.5,
|
|
353
|
+
samples: [60]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: formatMessage({
|
|
357
|
+
id: 'music.instrumentOrgan',
|
|
358
|
+
default: '(3) Organ',
|
|
359
|
+
description: 'Sound of an organ'
|
|
360
|
+
}),
|
|
361
|
+
dirName: '3-organ',
|
|
362
|
+
releaseTime: 0.5,
|
|
363
|
+
samples: [60]
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: formatMessage({
|
|
367
|
+
id: 'music.instrumentGuitar',
|
|
368
|
+
default: '(4) Guitar',
|
|
369
|
+
description: 'Sound of an accoustic guitar'
|
|
370
|
+
}),
|
|
371
|
+
dirName: '4-guitar',
|
|
372
|
+
releaseTime: 0.5,
|
|
373
|
+
samples: [60]
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: formatMessage({
|
|
377
|
+
id: 'music.instrumentElectricGuitar',
|
|
378
|
+
default: '(5) Electric Guitar',
|
|
379
|
+
description: 'Sound of an electric guitar'
|
|
380
|
+
}),
|
|
381
|
+
dirName: '5-electric-guitar',
|
|
382
|
+
releaseTime: 0.5,
|
|
383
|
+
samples: [60]
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: formatMessage({
|
|
387
|
+
id: 'music.instrumentBass',
|
|
388
|
+
default: '(6) Bass',
|
|
389
|
+
description: 'Sound of an accoustic upright bass'
|
|
390
|
+
}),
|
|
391
|
+
dirName: '6-bass',
|
|
392
|
+
releaseTime: 0.25,
|
|
393
|
+
samples: [36, 48]
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: formatMessage({
|
|
397
|
+
id: 'music.instrumentPizzicato',
|
|
398
|
+
default: '(7) Pizzicato',
|
|
399
|
+
description: 'Sound of a string instrument (e.g. violin) being plucked'
|
|
400
|
+
}),
|
|
401
|
+
dirName: '7-pizzicato',
|
|
402
|
+
releaseTime: 0.25,
|
|
403
|
+
samples: [60]
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: formatMessage({
|
|
407
|
+
id: 'music.instrumentCello',
|
|
408
|
+
default: '(8) Cello',
|
|
409
|
+
description: 'Sound of a cello being played with a bow'
|
|
410
|
+
}),
|
|
411
|
+
dirName: '8-cello',
|
|
412
|
+
releaseTime: 0.1,
|
|
413
|
+
samples: [36, 48, 60]
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: formatMessage({
|
|
417
|
+
id: 'music.instrumentTrombone',
|
|
418
|
+
default: '(9) Trombone',
|
|
419
|
+
description: 'Sound of a trombone being played'
|
|
420
|
+
}),
|
|
421
|
+
dirName: '9-trombone',
|
|
422
|
+
samples: [36, 48, 60]
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: formatMessage({
|
|
426
|
+
id: 'music.instrumentClarinet',
|
|
427
|
+
default: '(10) Clarinet',
|
|
428
|
+
description: 'Sound of a clarinet being played'
|
|
429
|
+
}),
|
|
430
|
+
dirName: '10-clarinet',
|
|
431
|
+
samples: [48, 60]
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: formatMessage({
|
|
435
|
+
id: 'music.instrumentSaxophone',
|
|
436
|
+
default: '(11) Saxophone',
|
|
437
|
+
description: 'Sound of a saxophone being played'
|
|
438
|
+
}),
|
|
439
|
+
dirName: '11-saxophone',
|
|
440
|
+
samples: [36, 60, 84]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: formatMessage({
|
|
444
|
+
id: 'music.instrumentFlute',
|
|
445
|
+
default: '(12) Flute',
|
|
446
|
+
description: 'Sound of a flute being played'
|
|
447
|
+
}),
|
|
448
|
+
dirName: '12-flute',
|
|
449
|
+
samples: [60, 72]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: formatMessage({
|
|
453
|
+
id: 'music.instrumentWoodenFlute',
|
|
454
|
+
default: '(13) Wooden Flute',
|
|
455
|
+
description: 'Sound of a wooden flute being played'
|
|
456
|
+
}),
|
|
457
|
+
dirName: '13-wooden-flute',
|
|
458
|
+
samples: [60, 72]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: formatMessage({
|
|
462
|
+
id: 'music.instrumentBassoon',
|
|
463
|
+
default: '(14) Bassoon',
|
|
464
|
+
description: 'Sound of a bassoon being played'
|
|
465
|
+
}),
|
|
466
|
+
dirName: '14-bassoon',
|
|
467
|
+
samples: [36, 48, 60]
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: formatMessage({
|
|
471
|
+
id: 'music.instrumentChoir',
|
|
472
|
+
default: '(15) Choir',
|
|
473
|
+
description: 'Sound of a choir singing'
|
|
474
|
+
}),
|
|
475
|
+
dirName: '15-choir',
|
|
476
|
+
releaseTime: 0.25,
|
|
477
|
+
samples: [48, 60, 72]
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: formatMessage({
|
|
481
|
+
id: 'music.instrumentVibraphone',
|
|
482
|
+
default: '(16) Vibraphone',
|
|
483
|
+
description: 'Sound of a vibraphone being struck'
|
|
484
|
+
}),
|
|
485
|
+
dirName: '16-vibraphone',
|
|
486
|
+
releaseTime: 0.5,
|
|
487
|
+
samples: [60, 72]
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: formatMessage({
|
|
491
|
+
id: 'music.instrumentMusicBox',
|
|
492
|
+
default: '(17) Music Box',
|
|
493
|
+
description: 'Sound of a music box playing'
|
|
494
|
+
}),
|
|
495
|
+
dirName: '17-music-box',
|
|
496
|
+
releaseTime: 0.25,
|
|
497
|
+
samples: [60]
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: formatMessage({
|
|
501
|
+
id: 'music.instrumentSteelDrum',
|
|
502
|
+
default: '(18) Steel Drum',
|
|
503
|
+
description: 'Sound of a steel drum being struck'
|
|
504
|
+
}),
|
|
505
|
+
dirName: '18-steel-drum',
|
|
506
|
+
releaseTime: 0.5,
|
|
507
|
+
samples: [60]
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: formatMessage({
|
|
511
|
+
id: 'music.instrumentMarimba',
|
|
512
|
+
default: '(19) Marimba',
|
|
513
|
+
description: 'Sound of a marimba being struck'
|
|
514
|
+
}),
|
|
515
|
+
dirName: '19-marimba',
|
|
516
|
+
samples: [60]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: formatMessage({
|
|
520
|
+
id: 'music.instrumentSynthLead',
|
|
521
|
+
default: '(20) Synth Lead',
|
|
522
|
+
description: 'Sound of a "lead" synthesizer being played'
|
|
523
|
+
}),
|
|
524
|
+
dirName: '20-synth-lead',
|
|
525
|
+
releaseTime: 0.1,
|
|
526
|
+
samples: [60]
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: formatMessage({
|
|
530
|
+
id: 'music.instrumentSynthPad',
|
|
531
|
+
default: '(21) Synth Pad',
|
|
532
|
+
description: 'Sound of a "pad" synthesizer being played'
|
|
533
|
+
}),
|
|
534
|
+
dirName: '21-synth-pad',
|
|
535
|
+
releaseTime: 0.25,
|
|
536
|
+
samples: [60]
|
|
537
|
+
}
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* An array that is a mapping from MIDI instrument numbers to Scratch instrument numbers.
|
|
543
|
+
* @type {number[]}
|
|
544
|
+
*/
|
|
545
|
+
get MIDI_INSTRUMENTS () {
|
|
546
|
+
return [
|
|
547
|
+
// Acoustic Grand, Bright Acoustic, Electric Grand, Honky-Tonk
|
|
548
|
+
1, 1, 1, 1,
|
|
549
|
+
// Electric Piano 1, Electric Piano 2, Harpsichord, Clavinet
|
|
550
|
+
2, 2, 4, 4,
|
|
551
|
+
// Celesta, Glockenspiel, Music Box, Vibraphone
|
|
552
|
+
17, 17, 17, 16,
|
|
553
|
+
// Marimba, Xylophone, Tubular Bells, Dulcimer
|
|
554
|
+
19, 16, 17, 17,
|
|
555
|
+
// Drawbar Organ, Percussive Organ, Rock Organ, Church Organ
|
|
556
|
+
3, 3, 3, 3,
|
|
557
|
+
// Reed Organ, Accordion, Harmonica, Tango Accordion
|
|
558
|
+
3, 3, 3, 3,
|
|
559
|
+
// Nylon String Guitar, Steel String Guitar, Electric Jazz Guitar, Electric Clean Guitar
|
|
560
|
+
4, 4, 5, 5,
|
|
561
|
+
// Electric Muted Guitar, Overdriven Guitar,Distortion Guitar, Guitar Harmonics
|
|
562
|
+
5, 5, 5, 5,
|
|
563
|
+
// Acoustic Bass, Electric Bass (finger), Electric Bass (pick), Fretless Bass
|
|
564
|
+
6, 6, 6, 6,
|
|
565
|
+
// Slap Bass 1, Slap Bass 2, Synth Bass 1, Synth Bass 2
|
|
566
|
+
6, 6, 6, 6,
|
|
567
|
+
// Violin, Viola, Cello, Contrabass
|
|
568
|
+
8, 8, 8, 8,
|
|
569
|
+
// Tremolo Strings, Pizzicato Strings, Orchestral Strings, Timpani
|
|
570
|
+
8, 7, 8, 19,
|
|
571
|
+
// String Ensemble 1, String Ensemble 2, SynthStrings 1, SynthStrings 2
|
|
572
|
+
8, 8, 8, 8,
|
|
573
|
+
// Choir Aahs, Voice Oohs, Synth Voice, Orchestra Hit
|
|
574
|
+
15, 15, 15, 19,
|
|
575
|
+
// Trumpet, Trombone, Tuba, Muted Trumpet
|
|
576
|
+
9, 9, 9, 9,
|
|
577
|
+
// French Horn, Brass Section, SynthBrass 1, SynthBrass 2
|
|
578
|
+
9, 9, 9, 9,
|
|
579
|
+
// Soprano Sax, Alto Sax, Tenor Sax, Baritone Sax
|
|
580
|
+
11, 11, 11, 11,
|
|
581
|
+
// Oboe, English Horn, Bassoon, Clarinet
|
|
582
|
+
14, 14, 14, 10,
|
|
583
|
+
// Piccolo, Flute, Recorder, Pan Flute
|
|
584
|
+
12, 12, 13, 13,
|
|
585
|
+
// Blown Bottle, Shakuhachi, Whistle, Ocarina
|
|
586
|
+
13, 13, 12, 12,
|
|
587
|
+
// Lead 1 (square), Lead 2 (sawtooth), Lead 3 (calliope), Lead 4 (chiff)
|
|
588
|
+
20, 20, 20, 20,
|
|
589
|
+
// Lead 5 (charang), Lead 6 (voice), Lead 7 (fifths), Lead 8 (bass+lead)
|
|
590
|
+
20, 20, 20, 20,
|
|
591
|
+
// Pad 1 (new age), Pad 2 (warm), Pad 3 (polysynth), Pad 4 (choir)
|
|
592
|
+
21, 21, 21, 21,
|
|
593
|
+
// Pad 5 (bowed), Pad 6 (metallic), Pad 7 (halo), Pad 8 (sweep)
|
|
594
|
+
21, 21, 21, 21,
|
|
595
|
+
// FX 1 (rain), FX 2 (soundtrack), FX 3 (crystal), FX 4 (atmosphere)
|
|
596
|
+
21, 21, 21, 21,
|
|
597
|
+
// FX 5 (brightness), FX 6 (goblins), FX 7 (echoes), FX 8 (sci-fi)
|
|
598
|
+
21, 21, 21, 21,
|
|
599
|
+
// Sitar, Banjo, Shamisen, Koto
|
|
600
|
+
4, 4, 4, 4,
|
|
601
|
+
// Kalimba, Bagpipe, Fiddle, Shanai
|
|
602
|
+
17, 14, 8, 10,
|
|
603
|
+
// Tinkle Bell, Agogo, Steel Drums, Woodblock
|
|
604
|
+
17, 17, 18, 19,
|
|
605
|
+
// Taiko Drum, Melodic Tom, Synth Drum, Reverse Cymbal
|
|
606
|
+
1, 1, 1, 1,
|
|
607
|
+
// Guitar Fret Noise, Breath Noise, Seashore, Bird Tweet
|
|
608
|
+
21, 21, 21, 21,
|
|
609
|
+
// Telephone Ring, Helicopter, Applause, Gunshot
|
|
610
|
+
21, 21, 21, 21
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* An array that is a mapping from MIDI drum numbers in range (35..81) to Scratch drum numbers.
|
|
616
|
+
* It's in the format [drumNum, pitch, decay].
|
|
617
|
+
* The pitch and decay properties are not currently being used.
|
|
618
|
+
* @type {Array[]}
|
|
619
|
+
*/
|
|
620
|
+
get MIDI_DRUMS () {
|
|
621
|
+
return [
|
|
622
|
+
[1, -4], // "BassDrum" in 2.0, "Bass Drum" in 3.0 (which was "Tom" in 2.0)
|
|
623
|
+
[1, 0], // Same as just above
|
|
624
|
+
[2, 0],
|
|
625
|
+
[0, 0],
|
|
626
|
+
[7, 0],
|
|
627
|
+
[0, 2],
|
|
628
|
+
[1, -6, 4],
|
|
629
|
+
[5, 0],
|
|
630
|
+
[1, -3, 3.2],
|
|
631
|
+
[5, 0], // "HiHatPedal" in 2.0, "Closed Hi-Hat" in 3.0
|
|
632
|
+
[1, 0, 3],
|
|
633
|
+
[4, -8],
|
|
634
|
+
[1, 4, 3],
|
|
635
|
+
[1, 7, 2.7],
|
|
636
|
+
[3, -8],
|
|
637
|
+
[1, 10, 2.7],
|
|
638
|
+
[4, -2],
|
|
639
|
+
[3, -11],
|
|
640
|
+
[4, 2],
|
|
641
|
+
[6, 0],
|
|
642
|
+
[3, 0, 3.5],
|
|
643
|
+
[10, 0],
|
|
644
|
+
[3, -8, 3.5],
|
|
645
|
+
[16, -6],
|
|
646
|
+
[4, 2],
|
|
647
|
+
[12, 2],
|
|
648
|
+
[12, 0],
|
|
649
|
+
[13, 0, 0.2],
|
|
650
|
+
[13, 0, 2],
|
|
651
|
+
[13, -5, 2],
|
|
652
|
+
[12, 12],
|
|
653
|
+
[12, 5],
|
|
654
|
+
[10, 19],
|
|
655
|
+
[10, 12],
|
|
656
|
+
[14, 0],
|
|
657
|
+
[14, 0], // "Maracas" in 2.0, "Cabasa" in 3.0 (TODO: pitch up?)
|
|
658
|
+
[17, 12],
|
|
659
|
+
[17, 5],
|
|
660
|
+
[15, 0], // "GuiroShort" in 2.0, "Guiro" in 3.0 (which was "GuiroLong" in 2.0) (TODO: decay?)
|
|
661
|
+
[15, 0],
|
|
662
|
+
[8, 0],
|
|
663
|
+
[9, 0],
|
|
664
|
+
[9, -4],
|
|
665
|
+
[17, -5],
|
|
666
|
+
[17, 0],
|
|
667
|
+
[11, -6, 1],
|
|
668
|
+
[11, -6, 3]
|
|
669
|
+
];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The key to load & store a target's music-related state.
|
|
674
|
+
* @type {string}
|
|
675
|
+
*/
|
|
676
|
+
static get STATE_KEY () {
|
|
677
|
+
return 'Scratch.music';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The default music-related state, to be used when a target has no existing music state.
|
|
682
|
+
* @type {MusicState}
|
|
683
|
+
*/
|
|
684
|
+
static get DEFAULT_MUSIC_STATE () {
|
|
685
|
+
return {
|
|
686
|
+
currentInstrument: 0
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The minimum and maximum MIDI note numbers, for clamping the input to play note.
|
|
692
|
+
* @type {{min: number, max: number}}
|
|
693
|
+
*/
|
|
694
|
+
static get MIDI_NOTE_RANGE () {
|
|
695
|
+
return {min: 0, max: 130};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
|
|
700
|
+
* 100 beats at the default tempo of 60bpm is 100 seconds.
|
|
701
|
+
* @type {{min: number, max: number}}
|
|
702
|
+
*/
|
|
703
|
+
static get BEAT_RANGE () {
|
|
704
|
+
return {min: 0, max: 100};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** The minimum and maximum tempo values, in bpm.
|
|
708
|
+
* @type {{min: number, max: number}}
|
|
709
|
+
*/
|
|
710
|
+
static get TEMPO_RANGE () {
|
|
711
|
+
return {min: 20, max: 500};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The maximum number of sounds to allow to play simultaneously.
|
|
716
|
+
* @type {number}
|
|
717
|
+
*/
|
|
718
|
+
static get CONCURRENCY_LIMIT () {
|
|
719
|
+
return 30;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* @param {Target} target - collect music state for this target.
|
|
724
|
+
* @returns {MusicState} the mutable music state associated with that target. This will be created if necessary.
|
|
725
|
+
* @private
|
|
726
|
+
*/
|
|
727
|
+
_getMusicState (target) {
|
|
728
|
+
let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY);
|
|
729
|
+
if (!musicState) {
|
|
730
|
+
musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE);
|
|
731
|
+
target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState);
|
|
732
|
+
}
|
|
733
|
+
return musicState;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* When a music-playing Target is cloned, clone the music state.
|
|
738
|
+
* @param {Target} newTarget - the newly created target.
|
|
739
|
+
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
|
|
740
|
+
* @listens Runtime#event:targetWasCreated
|
|
741
|
+
* @private
|
|
742
|
+
*/
|
|
743
|
+
_onTargetCreated (newTarget, sourceTarget) {
|
|
744
|
+
if (sourceTarget) {
|
|
745
|
+
const musicState = sourceTarget.getCustomState(Scratch3MusicBlocks.STATE_KEY);
|
|
746
|
+
if (musicState) {
|
|
747
|
+
newTarget.setCustomState(Scratch3MusicBlocks.STATE_KEY, Clone.simple(musicState));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* @returns {object} metadata for this extension and its blocks.
|
|
754
|
+
*/
|
|
755
|
+
getInfo () {
|
|
756
|
+
return {
|
|
757
|
+
id: 'music',
|
|
758
|
+
name: formatMessage({
|
|
759
|
+
id: 'music.categoryName',
|
|
760
|
+
default: 'Music',
|
|
761
|
+
description: 'Label for the Music extension category'
|
|
762
|
+
}),
|
|
763
|
+
menuIconURI: menuIconURI,
|
|
764
|
+
blockIconURI: blockIconURI,
|
|
765
|
+
blocks: [
|
|
766
|
+
{
|
|
767
|
+
opcode: 'playDrumForBeats',
|
|
768
|
+
blockType: BlockType.COMMAND,
|
|
769
|
+
text: formatMessage({
|
|
770
|
+
id: 'music.playDrumForBeats',
|
|
771
|
+
default: 'play drum [DRUM] for [BEATS] beats',
|
|
772
|
+
description: 'play drum sample for a number of beats'
|
|
773
|
+
}),
|
|
774
|
+
arguments: {
|
|
775
|
+
DRUM: {
|
|
776
|
+
type: ArgumentType.NUMBER,
|
|
777
|
+
menu: 'DRUM',
|
|
778
|
+
defaultValue: 1
|
|
779
|
+
},
|
|
780
|
+
BEATS: {
|
|
781
|
+
type: ArgumentType.NUMBER,
|
|
782
|
+
defaultValue: 0.25
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
opcode: 'midiPlayDrumForBeats',
|
|
788
|
+
blockType: BlockType.COMMAND,
|
|
789
|
+
text: formatMessage({
|
|
790
|
+
id: 'music.midiPlayDrumForBeats',
|
|
791
|
+
default: 'play drum [DRUM] for [BEATS] beats',
|
|
792
|
+
description: 'play drum sample for a number of beats according to a mapping of MIDI codes'
|
|
793
|
+
}),
|
|
794
|
+
arguments: {
|
|
795
|
+
DRUM: {
|
|
796
|
+
type: ArgumentType.NUMBER,
|
|
797
|
+
menu: 'DRUM',
|
|
798
|
+
defaultValue: 1
|
|
799
|
+
},
|
|
800
|
+
BEATS: {
|
|
801
|
+
type: ArgumentType.NUMBER,
|
|
802
|
+
defaultValue: 0.25
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
hideFromPalette: true
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
opcode: 'restForBeats',
|
|
809
|
+
blockType: BlockType.COMMAND,
|
|
810
|
+
text: formatMessage({
|
|
811
|
+
id: 'music.restForBeats',
|
|
812
|
+
default: 'rest for [BEATS] beats',
|
|
813
|
+
description: 'rest (play no sound) for a number of beats'
|
|
814
|
+
}),
|
|
815
|
+
arguments: {
|
|
816
|
+
BEATS: {
|
|
817
|
+
type: ArgumentType.NUMBER,
|
|
818
|
+
defaultValue: 0.25
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
opcode: 'playNoteForBeats',
|
|
824
|
+
blockType: BlockType.COMMAND,
|
|
825
|
+
text: formatMessage({
|
|
826
|
+
id: 'music.playNoteForBeats',
|
|
827
|
+
default: 'play note [NOTE] for [BEATS] beats',
|
|
828
|
+
description: 'play a note for a number of beats'
|
|
829
|
+
}),
|
|
830
|
+
arguments: {
|
|
831
|
+
NOTE: {
|
|
832
|
+
type: ArgumentType.NOTE,
|
|
833
|
+
defaultValue: 60
|
|
834
|
+
},
|
|
835
|
+
BEATS: {
|
|
836
|
+
type: ArgumentType.NUMBER,
|
|
837
|
+
defaultValue: 0.25
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
opcode: 'setInstrument',
|
|
843
|
+
blockType: BlockType.COMMAND,
|
|
844
|
+
text: formatMessage({
|
|
845
|
+
id: 'music.setInstrument',
|
|
846
|
+
default: 'set instrument to [INSTRUMENT]',
|
|
847
|
+
description: 'set the instrument (e.g. piano, guitar, trombone) for notes played'
|
|
848
|
+
}),
|
|
849
|
+
arguments: {
|
|
850
|
+
INSTRUMENT: {
|
|
851
|
+
type: ArgumentType.NUMBER,
|
|
852
|
+
menu: 'INSTRUMENT',
|
|
853
|
+
defaultValue: 1
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
opcode: 'midiSetInstrument',
|
|
859
|
+
blockType: BlockType.COMMAND,
|
|
860
|
+
text: formatMessage({
|
|
861
|
+
id: 'music.midiSetInstrument',
|
|
862
|
+
default: 'set instrument to [INSTRUMENT]',
|
|
863
|
+
description: 'set the instrument for notes played according to a mapping of MIDI codes'
|
|
864
|
+
}),
|
|
865
|
+
arguments: {
|
|
866
|
+
INSTRUMENT: {
|
|
867
|
+
type: ArgumentType.NUMBER,
|
|
868
|
+
defaultValue: 1
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
hideFromPalette: true
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
opcode: 'setTempo',
|
|
875
|
+
blockType: BlockType.COMMAND,
|
|
876
|
+
text: formatMessage({
|
|
877
|
+
id: 'music.setTempo',
|
|
878
|
+
default: 'set tempo to [TEMPO]',
|
|
879
|
+
description: 'set tempo (speed) for notes, drums, and rests played'
|
|
880
|
+
}),
|
|
881
|
+
arguments: {
|
|
882
|
+
TEMPO: {
|
|
883
|
+
type: ArgumentType.NUMBER,
|
|
884
|
+
defaultValue: 60
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
opcode: 'changeTempo',
|
|
890
|
+
blockType: BlockType.COMMAND,
|
|
891
|
+
text: formatMessage({
|
|
892
|
+
id: 'music.changeTempo',
|
|
893
|
+
default: 'change tempo by [TEMPO]',
|
|
894
|
+
description: 'change tempo (speed) for notes, drums, and rests played'
|
|
895
|
+
}),
|
|
896
|
+
arguments: {
|
|
897
|
+
TEMPO: {
|
|
898
|
+
type: ArgumentType.NUMBER,
|
|
899
|
+
defaultValue: 20
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
opcode: 'getTempo',
|
|
905
|
+
text: formatMessage({
|
|
906
|
+
id: 'music.getTempo',
|
|
907
|
+
default: 'tempo',
|
|
908
|
+
description: 'get the current tempo (speed) for notes, drums, and rests played'
|
|
909
|
+
}),
|
|
910
|
+
blockType: BlockType.REPORTER
|
|
911
|
+
}
|
|
912
|
+
],
|
|
913
|
+
menus: {
|
|
914
|
+
DRUM: {
|
|
915
|
+
acceptReporters: true,
|
|
916
|
+
items: this._buildMenu(this.DRUM_INFO)
|
|
917
|
+
},
|
|
918
|
+
INSTRUMENT: {
|
|
919
|
+
acceptReporters: true,
|
|
920
|
+
items: this._buildMenu(this.INSTRUMENT_INFO)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Play a drum sound for some number of beats.
|
|
928
|
+
* @param {object} args - the block arguments.
|
|
929
|
+
* @param {object} util - utility object provided by the runtime.
|
|
930
|
+
* @property {int} DRUM - the number of the drum to play.
|
|
931
|
+
* @property {number} BEATS - the duration in beats of the drum sound.
|
|
932
|
+
*/
|
|
933
|
+
playDrumForBeats (args, util) {
|
|
934
|
+
this._playDrumForBeats(args.DRUM, args.BEATS, util);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Play a drum sound for some number of beats according to the range of "MIDI" drum codes supported.
|
|
939
|
+
* This block is implemented for compatibility with old Scratch projects that use the
|
|
940
|
+
* 'drum:duration:elapsed:from:' block.
|
|
941
|
+
* @param {object} args - the block arguments.
|
|
942
|
+
* @param {object} util - utility object provided by the runtime.
|
|
943
|
+
*/
|
|
944
|
+
midiPlayDrumForBeats (args, util) {
|
|
945
|
+
let drumNum = Cast.toNumber(args.DRUM);
|
|
946
|
+
drumNum = Math.round(drumNum);
|
|
947
|
+
const midiDescription = this.MIDI_DRUMS[drumNum - 35];
|
|
948
|
+
if (midiDescription) {
|
|
949
|
+
drumNum = midiDescription[0];
|
|
950
|
+
} else {
|
|
951
|
+
drumNum = 2; // Default instrument used in Scratch 2.0
|
|
952
|
+
}
|
|
953
|
+
drumNum += 1; // drumNum input to _playDrumForBeats is one-indexed
|
|
954
|
+
this._playDrumForBeats(drumNum, args.BEATS, util);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Internal code to play a drum sound for some number of beats.
|
|
959
|
+
* @param {number} drumNum - the drum number.
|
|
960
|
+
* @param {beats} beats - the duration in beats to pause after playing the sound.
|
|
961
|
+
* @param {object} util - utility object provided by the runtime.
|
|
962
|
+
*/
|
|
963
|
+
_playDrumForBeats (drumNum, beats, util) {
|
|
964
|
+
if (this._stackTimerNeedsInit(util)) {
|
|
965
|
+
drumNum = Cast.toNumber(drumNum);
|
|
966
|
+
drumNum = Math.round(drumNum);
|
|
967
|
+
drumNum -= 1; // drums are one-indexed
|
|
968
|
+
drumNum = MathUtil.wrapClamp(drumNum, 0, this.DRUM_INFO.length - 1);
|
|
969
|
+
beats = Cast.toNumber(beats);
|
|
970
|
+
beats = this._clampBeats(beats);
|
|
971
|
+
this._playDrumNum(util, drumNum);
|
|
972
|
+
this._startStackTimer(util, this._beatsToSec(beats));
|
|
973
|
+
} else {
|
|
974
|
+
this._checkStackTimer(util);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Play a drum sound using its 0-indexed number.
|
|
980
|
+
* @param {object} util - utility object provided by the runtime.
|
|
981
|
+
* @param {number} drumNum - the number of the drum to play.
|
|
982
|
+
* @private
|
|
983
|
+
*/
|
|
984
|
+
_playDrumNum (util, drumNum) {
|
|
985
|
+
if (util.runtime.audioEngine === null) return;
|
|
986
|
+
if (util.target.sprite.soundBank === null) return;
|
|
987
|
+
// If we're playing too many sounds, do not play the drum sound.
|
|
988
|
+
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const player = this._drumPlayers[drumNum];
|
|
993
|
+
|
|
994
|
+
if (typeof player === 'undefined') return;
|
|
995
|
+
|
|
996
|
+
if (player.isPlaying && !player.isStarting) {
|
|
997
|
+
// Take the internal player state and create a new player with it.
|
|
998
|
+
// `.play` does this internally but then instructs the sound to
|
|
999
|
+
// stop.
|
|
1000
|
+
player.take();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const engine = util.runtime.audioEngine;
|
|
1004
|
+
const context = engine.audioContext;
|
|
1005
|
+
const volumeGain = context.createGain();
|
|
1006
|
+
volumeGain.gain.setValueAtTime(util.target.volume / 100, engine.currentTime);
|
|
1007
|
+
volumeGain.connect(engine.getInputNode());
|
|
1008
|
+
|
|
1009
|
+
this._concurrencyCounter++;
|
|
1010
|
+
player.once('stop', () => {
|
|
1011
|
+
this._concurrencyCounter--;
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
player.play();
|
|
1015
|
+
// Connect the player to the gain node.
|
|
1016
|
+
player.connect({getInputNode () {
|
|
1017
|
+
return volumeGain;
|
|
1018
|
+
}});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Rest for some number of beats.
|
|
1023
|
+
* @param {object} args - the block arguments.
|
|
1024
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1025
|
+
* @property {number} BEATS - the duration in beats of the rest.
|
|
1026
|
+
*/
|
|
1027
|
+
restForBeats (args, util) {
|
|
1028
|
+
if (this._stackTimerNeedsInit(util)) {
|
|
1029
|
+
let beats = Cast.toNumber(args.BEATS);
|
|
1030
|
+
beats = this._clampBeats(beats);
|
|
1031
|
+
this._startStackTimer(util, this._beatsToSec(beats));
|
|
1032
|
+
} else {
|
|
1033
|
+
this._checkStackTimer(util);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Play a note using the current musical instrument for some number of beats.
|
|
1039
|
+
* This function processes the arguments, and handles the timing of the block's execution.
|
|
1040
|
+
* @param {object} args - the block arguments.
|
|
1041
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1042
|
+
* @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number.
|
|
1043
|
+
* @property {number} BEATS - the duration in beats of the note.
|
|
1044
|
+
*/
|
|
1045
|
+
playNoteForBeats (args, util) {
|
|
1046
|
+
if (this._stackTimerNeedsInit(util)) {
|
|
1047
|
+
let note = Cast.toNumber(args.NOTE);
|
|
1048
|
+
note = MathUtil.clamp(note,
|
|
1049
|
+
Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
|
|
1050
|
+
let beats = Cast.toNumber(args.BEATS);
|
|
1051
|
+
beats = this._clampBeats(beats);
|
|
1052
|
+
// If the duration is 0, do not play the note. In Scratch 2.0, "play drum for 0 beats" plays the drum,
|
|
1053
|
+
// but "play note for 0 beats" is silent.
|
|
1054
|
+
if (beats === 0) return;
|
|
1055
|
+
|
|
1056
|
+
const durationSec = this._beatsToSec(beats);
|
|
1057
|
+
|
|
1058
|
+
this._playNote(util, note, durationSec);
|
|
1059
|
+
|
|
1060
|
+
this._startStackTimer(util, durationSec);
|
|
1061
|
+
} else {
|
|
1062
|
+
this._checkStackTimer(util);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
_playNoteForPicker (noteNum, category) {
|
|
1067
|
+
if (category !== this.getInfo().name) return;
|
|
1068
|
+
const util = {
|
|
1069
|
+
runtime: this.runtime,
|
|
1070
|
+
target: this.runtime.getEditingTarget()
|
|
1071
|
+
};
|
|
1072
|
+
this._playNote(util, noteNum, 0.25);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Play a note using the current instrument for a duration in seconds.
|
|
1077
|
+
* This function actually plays the sound, and handles the timing of the sound, including the
|
|
1078
|
+
* "release" portion of the sound, which continues briefly after the block execution has finished.
|
|
1079
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1080
|
+
* @param {number} note - the pitch of the note to play, interpreted as a MIDI note number.
|
|
1081
|
+
* @param {number} durationSec - the duration in seconds to play the note.
|
|
1082
|
+
* @private
|
|
1083
|
+
*/
|
|
1084
|
+
_playNote (util, note, durationSec) {
|
|
1085
|
+
if (util.runtime.audioEngine === null) return;
|
|
1086
|
+
if (util.target.sprite.soundBank === null) return;
|
|
1087
|
+
|
|
1088
|
+
// If we're playing too many sounds, do not play the note.
|
|
1089
|
+
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Determine which of the audio samples for this instrument to play
|
|
1094
|
+
const musicState = this._getMusicState(util.target);
|
|
1095
|
+
const inst = musicState.currentInstrument;
|
|
1096
|
+
const instrumentInfo = this.INSTRUMENT_INFO[inst];
|
|
1097
|
+
const sampleArray = instrumentInfo.samples;
|
|
1098
|
+
const sampleIndex = this._selectSampleIndexForNote(note, sampleArray);
|
|
1099
|
+
|
|
1100
|
+
// If the audio sample has not loaded yet, bail out
|
|
1101
|
+
if (typeof this._instrumentPlayerArrays[inst] === 'undefined') return;
|
|
1102
|
+
if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return;
|
|
1103
|
+
|
|
1104
|
+
// Fetch the sound player to play the note.
|
|
1105
|
+
const engine = util.runtime.audioEngine;
|
|
1106
|
+
|
|
1107
|
+
if (!this._instrumentPlayerNoteArrays[inst][note]) {
|
|
1108
|
+
this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const player = this._instrumentPlayerNoteArrays[inst][note];
|
|
1112
|
+
|
|
1113
|
+
if (player.isPlaying && !player.isStarting) {
|
|
1114
|
+
// Take the internal player state and create a new player with it.
|
|
1115
|
+
// `.play` does this internally but then instructs the sound to
|
|
1116
|
+
// stop.
|
|
1117
|
+
player.take();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Set its pitch.
|
|
1121
|
+
const sampleNote = sampleArray[sampleIndex];
|
|
1122
|
+
const notePitchInterval = this._ratioForPitchInterval(note - sampleNote);
|
|
1123
|
+
|
|
1124
|
+
// Create gain nodes for this note's volume and release, and chain them
|
|
1125
|
+
// to the output.
|
|
1126
|
+
const context = engine.audioContext;
|
|
1127
|
+
const volumeGain = context.createGain();
|
|
1128
|
+
volumeGain.gain.setValueAtTime(util.target.volume / 100, engine.currentTime);
|
|
1129
|
+
const releaseGain = context.createGain();
|
|
1130
|
+
volumeGain.connect(releaseGain);
|
|
1131
|
+
releaseGain.connect(engine.getInputNode());
|
|
1132
|
+
|
|
1133
|
+
// Schedule the release of the note, ramping its gain down to zero,
|
|
1134
|
+
// and then stopping the sound.
|
|
1135
|
+
let releaseDuration = this.INSTRUMENT_INFO[inst].releaseTime;
|
|
1136
|
+
if (typeof releaseDuration === 'undefined') {
|
|
1137
|
+
releaseDuration = 0.01;
|
|
1138
|
+
}
|
|
1139
|
+
const releaseStart = context.currentTime + durationSec;
|
|
1140
|
+
const releaseEnd = releaseStart + releaseDuration;
|
|
1141
|
+
releaseGain.gain.setValueAtTime(1, releaseStart);
|
|
1142
|
+
releaseGain.gain.linearRampToValueAtTime(0.0001, releaseEnd);
|
|
1143
|
+
|
|
1144
|
+
this._concurrencyCounter++;
|
|
1145
|
+
player.once('stop', () => {
|
|
1146
|
+
this._concurrencyCounter--;
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Start playing the note
|
|
1150
|
+
player.play();
|
|
1151
|
+
// Connect the player to the gain node.
|
|
1152
|
+
player.connect({getInputNode () {
|
|
1153
|
+
return volumeGain;
|
|
1154
|
+
}});
|
|
1155
|
+
// Set playback now after play creates the outputNode.
|
|
1156
|
+
player.outputNode.playbackRate.value = notePitchInterval;
|
|
1157
|
+
// Schedule playback to stop.
|
|
1158
|
+
player.outputNode.stop(releaseEnd);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* The samples array for each instrument is the set of pitches of the available audio samples.
|
|
1163
|
+
* This function selects the best one to use to play a given input note, and returns its index
|
|
1164
|
+
* in the samples array.
|
|
1165
|
+
* @param {number} note - the input note to select a sample for.
|
|
1166
|
+
* @param {number[]} samples - an array of the pitches of the available samples.
|
|
1167
|
+
* @return {index} the index of the selected sample in the samples array.
|
|
1168
|
+
* @private
|
|
1169
|
+
*/
|
|
1170
|
+
_selectSampleIndexForNote (note, samples) {
|
|
1171
|
+
// Step backwards through the array of samples, i.e. in descending pitch, in order to find
|
|
1172
|
+
// the sample that is the closest one below (or matching) the pitch of the input note.
|
|
1173
|
+
for (let i = samples.length - 1; i >= 0; i--) {
|
|
1174
|
+
if (note >= samples[i]) {
|
|
1175
|
+
return i;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return 0;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Calcuate the frequency ratio for a given musical interval.
|
|
1183
|
+
* @param {number} interval - the pitch interval to convert.
|
|
1184
|
+
* @return {number} a ratio corresponding to the input interval.
|
|
1185
|
+
* @private
|
|
1186
|
+
*/
|
|
1187
|
+
_ratioForPitchInterval (interval) {
|
|
1188
|
+
return Math.pow(2, (interval / 12));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Clamp a duration in beats to the allowed min and max duration.
|
|
1193
|
+
* @param {number} beats - a duration in beats.
|
|
1194
|
+
* @return {number} - the clamped duration.
|
|
1195
|
+
* @private
|
|
1196
|
+
*/
|
|
1197
|
+
_clampBeats (beats) {
|
|
1198
|
+
return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Convert a number of beats to a number of seconds, using the current tempo.
|
|
1203
|
+
* @param {number} beats - number of beats to convert to secs.
|
|
1204
|
+
* @return {number} seconds - number of seconds `beats` will last.
|
|
1205
|
+
* @private
|
|
1206
|
+
*/
|
|
1207
|
+
_beatsToSec (beats) {
|
|
1208
|
+
return (60 / this.getTempo()) * beats;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Check if the stack timer needs initialization.
|
|
1213
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1214
|
+
* @return {boolean} - true if the stack timer needs to be initialized.
|
|
1215
|
+
* @private
|
|
1216
|
+
*/
|
|
1217
|
+
_stackTimerNeedsInit (util) {
|
|
1218
|
+
return !util.stackFrame.timer;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Start the stack timer and the yield the thread if necessary.
|
|
1223
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1224
|
+
* @param {number} duration - a duration in seconds to set the timer for.
|
|
1225
|
+
* @private
|
|
1226
|
+
*/
|
|
1227
|
+
_startStackTimer (util, duration) {
|
|
1228
|
+
util.stackFrame.timer = new Timer();
|
|
1229
|
+
util.stackFrame.timer.start();
|
|
1230
|
+
util.stackFrame.duration = duration;
|
|
1231
|
+
util.yield();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Check the stack timer, and if its time is not up yet, yield the thread.
|
|
1236
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1237
|
+
* @private
|
|
1238
|
+
*/
|
|
1239
|
+
_checkStackTimer (util) {
|
|
1240
|
+
const timeElapsed = util.stackFrame.timer.timeElapsed();
|
|
1241
|
+
if (timeElapsed < util.stackFrame.duration * 1000) {
|
|
1242
|
+
util.yield();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Select an instrument for playing notes.
|
|
1248
|
+
* @param {object} args - the block arguments.
|
|
1249
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1250
|
+
* @property {int} INSTRUMENT - the number of the instrument to select.
|
|
1251
|
+
*/
|
|
1252
|
+
setInstrument (args, util) {
|
|
1253
|
+
this._setInstrument(args.INSTRUMENT, util, false);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Select an instrument for playing notes according to a mapping of MIDI codes to Scratch instrument numbers.
|
|
1258
|
+
* This block is implemented for compatibility with old Scratch projects that use the 'midiInstrument:' block.
|
|
1259
|
+
* @param {object} args - the block arguments.
|
|
1260
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1261
|
+
* @property {int} INSTRUMENT - the MIDI number of the instrument to select.
|
|
1262
|
+
*/
|
|
1263
|
+
midiSetInstrument (args, util) {
|
|
1264
|
+
this._setInstrument(args.INSTRUMENT, util, true);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Internal code to select an instrument for playing notes. If mapMidi is true, set the instrument according to
|
|
1269
|
+
* the MIDI to Scratch instrument mapping.
|
|
1270
|
+
* @param {number} instNum - the instrument number.
|
|
1271
|
+
* @param {object} util - utility object provided by the runtime.
|
|
1272
|
+
* @param {boolean} mapMidi - whether or not instNum is a MIDI instrument number.
|
|
1273
|
+
*/
|
|
1274
|
+
_setInstrument (instNum, util, mapMidi) {
|
|
1275
|
+
const musicState = this._getMusicState(util.target);
|
|
1276
|
+
instNum = Cast.toNumber(instNum);
|
|
1277
|
+
instNum = Math.round(instNum);
|
|
1278
|
+
instNum -= 1; // instruments are one-indexed
|
|
1279
|
+
if (mapMidi) {
|
|
1280
|
+
instNum = (this.MIDI_INSTRUMENTS[instNum] || 0) - 1;
|
|
1281
|
+
}
|
|
1282
|
+
instNum = MathUtil.wrapClamp(instNum, 0, this.INSTRUMENT_INFO.length - 1);
|
|
1283
|
+
musicState.currentInstrument = instNum;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Set the current tempo to a new value.
|
|
1288
|
+
* @param {object} args - the block arguments.
|
|
1289
|
+
* @property {number} TEMPO - the tempo, in beats per minute.
|
|
1290
|
+
*/
|
|
1291
|
+
setTempo (args) {
|
|
1292
|
+
const tempo = Cast.toNumber(args.TEMPO);
|
|
1293
|
+
this._updateTempo(tempo);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Change the current tempo by some amount.
|
|
1298
|
+
* @param {object} args - the block arguments.
|
|
1299
|
+
* @property {number} TEMPO - the amount to change the tempo, in beats per minute.
|
|
1300
|
+
*/
|
|
1301
|
+
changeTempo (args) {
|
|
1302
|
+
const change = Cast.toNumber(args.TEMPO);
|
|
1303
|
+
const tempo = change + this.getTempo();
|
|
1304
|
+
this._updateTempo(tempo);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Update the current tempo, clamping it to the min and max allowable range.
|
|
1309
|
+
* @param {number} tempo - the tempo to set, in beats per minute.
|
|
1310
|
+
* @private
|
|
1311
|
+
*/
|
|
1312
|
+
_updateTempo (tempo) {
|
|
1313
|
+
tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max);
|
|
1314
|
+
const stage = this.runtime.getTargetForStage();
|
|
1315
|
+
if (stage) {
|
|
1316
|
+
stage.tempo = tempo;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Get the current tempo.
|
|
1322
|
+
* @return {number} - the current tempo, in beats per minute.
|
|
1323
|
+
*/
|
|
1324
|
+
getTempo () {
|
|
1325
|
+
const stage = this.runtime.getTargetForStage();
|
|
1326
|
+
if (stage) {
|
|
1327
|
+
return stage.tempo;
|
|
1328
|
+
}
|
|
1329
|
+
return 60;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
module.exports = Scratch3MusicBlocks;
|