@scratch/scratch-vm 11.0.0-beta.1

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.
Files changed (175) hide show
  1. package/.jsdoc.json +20 -0
  2. package/.nvmrc +1 -0
  3. package/CHANGELOG.md +7004 -0
  4. package/LICENSE +12 -0
  5. package/README.md +120 -0
  6. package/TRADEMARK +1 -0
  7. package/commitlint.config.js +4 -0
  8. package/docs/extensions.md +527 -0
  9. package/package.json +103 -0
  10. package/release.config.js +13 -0
  11. package/src/blocks/scratch3_control.js +206 -0
  12. package/src/blocks/scratch3_core_example.js +69 -0
  13. package/src/blocks/scratch3_data.js +257 -0
  14. package/src/blocks/scratch3_event.js +137 -0
  15. package/src/blocks/scratch3_looks.js +612 -0
  16. package/src/blocks/scratch3_motion.js +288 -0
  17. package/src/blocks/scratch3_operators.js +154 -0
  18. package/src/blocks/scratch3_procedures.js +79 -0
  19. package/src/blocks/scratch3_sensing.js +336 -0
  20. package/src/blocks/scratch3_sound.js +350 -0
  21. package/src/dispatch/central-dispatch.js +143 -0
  22. package/src/dispatch/shared-dispatch.js +235 -0
  23. package/src/dispatch/worker-dispatch.js +110 -0
  24. package/src/engine/adapter.js +176 -0
  25. package/src/engine/block-utility.js +242 -0
  26. package/src/engine/blocks-execute-cache.js +19 -0
  27. package/src/engine/blocks-runtime-cache.js +78 -0
  28. package/src/engine/blocks.js +1299 -0
  29. package/src/engine/comment.js +56 -0
  30. package/src/engine/execute.js +580 -0
  31. package/src/engine/monitor-record.js +23 -0
  32. package/src/engine/mutation-adapter.js +48 -0
  33. package/src/engine/profiler.js +390 -0
  34. package/src/engine/runtime.js +2686 -0
  35. package/src/engine/scratch-blocks-constants.js +27 -0
  36. package/src/engine/sequencer.js +361 -0
  37. package/src/engine/stage-layering.js +29 -0
  38. package/src/engine/target.js +799 -0
  39. package/src/engine/thread.js +404 -0
  40. package/src/engine/variable.js +70 -0
  41. package/src/extension-support/argument-type.js +47 -0
  42. package/src/extension-support/block-type.js +50 -0
  43. package/src/extension-support/define-messages.js +18 -0
  44. package/src/extension-support/extension-manager.js +440 -0
  45. package/src/extension-support/extension-metadata.js +64 -0
  46. package/src/extension-support/extension-worker.js +59 -0
  47. package/src/extension-support/reporter-scope.js +18 -0
  48. package/src/extension-support/target-type.js +17 -0
  49. package/src/extensions/scratch3_boost/index.js +2113 -0
  50. package/src/extensions/scratch3_ev3/index.js +1355 -0
  51. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  52. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  53. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  54. package/src/extensions/scratch3_microbit/index.js +984 -0
  55. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  56. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  57. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  58. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  59. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  116. package/src/extensions/scratch3_music/index.js +1333 -0
  117. package/src/extensions/scratch3_music/manifest.js +63 -0
  118. package/src/extensions/scratch3_pen/index.js +770 -0
  119. package/src/extensions/scratch3_speech2text/index.js +700 -0
  120. package/src/extensions/scratch3_text2speech/index.js +766 -0
  121. package/src/extensions/scratch3_translate/index.js +286 -0
  122. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  123. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  124. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  125. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  126. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  127. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  128. package/src/import/load-costume.js +416 -0
  129. package/src/import/load-sound.js +116 -0
  130. package/src/index.js +10 -0
  131. package/src/io/ble.js +256 -0
  132. package/src/io/bt.js +202 -0
  133. package/src/io/clock.js +39 -0
  134. package/src/io/cloud.js +169 -0
  135. package/src/io/keyboard.js +146 -0
  136. package/src/io/mouse.js +145 -0
  137. package/src/io/mouseWheel.js +28 -0
  138. package/src/io/userData.js +24 -0
  139. package/src/io/video.js +210 -0
  140. package/src/playground/benchmark.css +83 -0
  141. package/src/playground/benchmark.js +753 -0
  142. package/src/playground/index.html +111 -0
  143. package/src/playground/suite.css +78 -0
  144. package/src/playground/suite.html +25 -0
  145. package/src/playground/suite.js +544 -0
  146. package/src/playground/video-sensing.html +18 -0
  147. package/src/playground/video-sensing.js +133 -0
  148. package/src/serialization/deserialize-assets.js +178 -0
  149. package/src/serialization/sb2.js +1295 -0
  150. package/src/serialization/sb2_specmap.js +1818 -0
  151. package/src/serialization/sb3.js +1320 -0
  152. package/src/serialization/serialize-assets.js +60 -0
  153. package/src/sprites/rendered-target.js +1115 -0
  154. package/src/sprites/sprite.js +177 -0
  155. package/src/util/base64-util.js +48 -0
  156. package/src/util/cast.js +218 -0
  157. package/src/util/clone.js +17 -0
  158. package/src/util/color.js +204 -0
  159. package/src/util/fetch-with-timeout.js +53 -0
  160. package/src/util/get-monitor-id.js +33 -0
  161. package/src/util/jsonrpc.js +114 -0
  162. package/src/util/log.js +4 -0
  163. package/src/util/math-util.js +122 -0
  164. package/src/util/maybe-format-message.js +18 -0
  165. package/src/util/new-block-ids.js +33 -0
  166. package/src/util/rateLimiter.js +73 -0
  167. package/src/util/scratch-link-websocket.js +133 -0
  168. package/src/util/string-util.js +94 -0
  169. package/src/util/task-queue.js +203 -0
  170. package/src/util/timer.js +103 -0
  171. package/src/util/uid.js +29 -0
  172. package/src/util/variable-util.js +48 -0
  173. package/src/util/xml-escape.js +33 -0
  174. package/src/virtual-machine.js +1586 -0
  175. package/webpack.config.js +134 -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;