@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,766 @@
1
+ const formatMessage = require('format-message');
2
+ const languageNames = require('scratch-translate-extension-languages');
3
+
4
+ const ArgumentType = require('../../extension-support/argument-type');
5
+ const BlockType = require('../../extension-support/block-type');
6
+ const Cast = require('../../util/cast');
7
+ const MathUtil = require('../../util/math-util');
8
+ const Clone = require('../../util/clone');
9
+ const log = require('../../util/log');
10
+ const {fetchWithTimeout} = require('../../util/fetch-with-timeout');
11
+
12
+ /**
13
+ * Icon svg to be displayed in the blocks category menu, encoded as a data URI.
14
+ * @type {string}
15
+ */
16
+ // eslint-disable-next-line max-len
17
+ const menuIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1RleHQtdG8tU3BlZWNoLU1lbnU8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZyBpZD0iRXh0ZW5zaW9ucy9Tb2Z0d2FyZS9UZXh0LXRvLVNwZWVjaC1NZW51IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0idGV4dDJzcGVlY2giIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuMDAwMDAwLCAyLjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik01Ljc1LDguODM0NjcxNzMgQzUuNzUsOC4zMjY5NjM0NCA1LjAwMzAwNzI3LDguMDQyMjEzNzEgNC41NTYyODAxMiw4LjQ0NDE0OTk5IEwzLjIwNjI4MDEyLDkuNTI1MzU3MDIgQzIuNjk2NzMzNzgsOS45MzM0NDk2OCAyLjAzNzQ4Njc1LDEwLjE2NTg3ODggMS4zNSwxMC4xNjU4Nzg4IEwxLjE1LDEwLjE2NTg3ODggQzAuNjMyNTk2MTY1LDEwLjE2NTg3ODggMC4yNSwxMC41MTA2MDAyIDAuMjUsMTAuOTUyMDM1NSBMMC4yNSwxMy4wNjkzOTkzIEMwLjI1LDEzLjUxMDgzNDYgMC42MzI1OTYxNjUsMTMuODU1NTU2IDEuMTUsMTMuODU1NTU2IEwxLjM1LDEzLjg1NTU1NiBDMi4wNzg3Nzg0MSwxMy44NTU1NTYgMi43MjY4NjE2MSwxNC4wNjY3NjM2IDMuMjU5ODYwNDksMTQuNDk5IEw0LjU1OTIwMTQ3LDE1LjU3OTY2MDggQzUuMDEzMDkyNzYsMTUuOTU0NTM5NiA1Ljc1LDE1LjY3MzYzNDQgNS43NSwxNS4xNDE3MTI4IEw1Ljc1LDguODM0NjcxNzMgWiIgaWQ9InNwZWFrZXIiIHN0cm9rZS1vcGFjaXR5PSIwLjE1IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMC41IiBmaWxsPSIjNEQ0RDREIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMC43MDQ4MzEzLDggQzkuNzkwNjc0NjgsOS4xMzExNDg0NyA4LjMwNjYxODQsOS43MTQyODU3MSA3LjgzMzMzMzMzLDkuNzE0Mjg1NzEgQzcuODMzMzMzMzMsOS43MTQyODU3MSA3LjUsOS43MTQyODU3MSA3LjUsOS4zODA5NTIzOCBDNy41LDkuMDg1MjI2ODQgOC4wNjIyMDE2OCw4LjkwMTk0MTY0IDguMTg5MDYwNjcsNy41Njc1NDA1OCBDNi44ODk5Njk5MSw2LjkwNjc5MDA1IDYsNS41NTczMjY4MyA2LDQgQzYsMS43OTA4NjEgNy43OTA4NjEsNC4wNTgxMjI1MWUtMTYgMTAsMCBMMTIsMCBDMTQuMjA5MTM5LC00LjA1ODEyMjUxZS0xNiAxNiwxLjc5MDg2MSAxNiw0IEMxNiw2LjIwOTEzOSAxNC4yMDkxMzksOCAxMiw4IEwxMC43MDQ4MzEzLDggWiIgaWQ9InNwZWVjaCIgZmlsbD0iIzBFQkQ4QyI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+';
18
+
19
+ /**
20
+ * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
21
+ * @type {string}
22
+ */
23
+ // eslint-disable-next-line max-len
24
+ const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1RleHQtdG8tU3BlZWNoLUJsb2NrPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9IkV4dGVuc2lvbnMvU29mdHdhcmUvVGV4dC10by1TcGVlY2gtQmxvY2siIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1vcGFjaXR5PSIwLjE1Ij4KICAgICAgICA8ZyBpZD0idGV4dDJzcGVlY2giIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuMDAwMDAwLCA0LjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIj4KICAgICAgICAgICAgPHBhdGggZD0iTTExLjUsMTcuNjY5MzQzNSBDMTEuNSwxNi42NTM5MjY5IDEwLjAwNjAxNDUsMTYuMDg0NDI3NCA5LjExMjU2MDI0LDE2Ljg4ODMgTDYuNDEyNTYwMjQsMTkuMDUwNzE0IEM1LjM5MzQ2NzU1LDE5Ljg2Njg5OTQgNC4wNzQ5NzM1MSwyMC4zMzE3NTc1IDIuNywyMC4zMzE3NTc1IEwyLjMsMjAuMzMxNzU3NSBDMS4yNjUxOTIzMywyMC4zMzE3NTc1IDAuNSwyMS4wMjEyMDAzIDAuNSwyMS45MDQwNzEgTDAuNSwyNi4xMzg3OTg2IEMwLjUsMjcuMDIxNjY5MyAxLjI2NTE5MjMzLDI3LjcxMTExMiAyLjMsMjcuNzExMTEyIEwyLjcsMjcuNzExMTEyIEM0LjE1NzU1NjgyLDI3LjcxMTExMiA1LjQ1MzcyMzIyLDI4LjEzMzUyNzEgNi41MTk3MjA5OCwyOC45OTggTDkuMTE4NDAyOTMsMzEuMTU5MzIxNiBDMTAuMDI2MTg1NSwzMS45MDkwNzkzIDExLjUsMzEuMzQ3MjY4OSAxMS41LDMwLjI4MzQyNTUgTDExLjUsMTcuNjY5MzQzNSBaIiBpZD0ic3BlYWtlciIgZmlsbD0iIzRENEQ0RCI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMjEuNjQzNjA2NiwxNi41IEMxOS45NzcwMDk5LDE4LjQzNzAyMzQgMTcuMTA1MDI3NSwxOS45Mjg1NzE0IDE1LjY2NjY2NjcsMTkuOTI4NTcxNCBDMTUuNTEyNjM5NywxOS45Mjg1NzE0IDE1LjMxNjYyOTIsMTkuODk1OTAzIDE1LjEwOTcyNjUsMTkuNzkyNDUxNyBDMTQuNzM3NjAzOSwxOS42MDYzOTA0IDE0LjUsMTkuMjQ5OTg0NiAxNC41LDE4Ljc2MTkwNDggQzE0LjUsMTguNjU2ODA0MSAxNC41MTcwNTU1LDE4LjU1NDUwNzYgMTQuNTQ5NDQ2NywxOC40NTQwODQ0IEMxNC42MjU3NTQ1LDE4LjIxNzUwNjMgMTUuMTczNTcyMSwxNy40Njc1MzEgMTUuMjc3MjA3MSwxNy4yODA5ODgxIEMxNS41NDYzNTI2LDE2Ljc5NjUyNjEgMTUuNzM5MDI1LDE2LjIwNjM1NjEgMTUuODQzMjg5MSwxNS40MTYwMDM0IEMxMy4xODk3MDA1LDEzLjkyNjgzNjkgMTEuNSwxMS4xMTM5NjY4IDExLjUsOCBDMTEuNSwzLjMwNTU3OTYzIDE1LjMwNTU3OTYsLTAuNSAyMCwtMC41IEwyNCwtMC41IEMyOC42OTQ0MjA0LC0wLjUgMzIuNSwzLjMwNTU3OTYzIDMyLjUsOCBDMzIuNSwxMi42OTQ0MjA0IDI4LjY5NDQyMDQsMTYuNSAyNCwxNi41IEwyMS42NDM2MDY2LDE2LjUgWiIgaWQ9InNwZWVjaCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+';
25
+
26
+ /**
27
+ * The url of the synthesis server.
28
+ * @type {string}
29
+ */
30
+ const SERVER_HOST = 'https://synthesis-service.scratch.mit.edu';
31
+
32
+ /**
33
+ * How long to wait in ms before timing out requests to synthesis server.
34
+ * @type {int}
35
+ */
36
+ const SERVER_TIMEOUT = 10000; // 10 seconds
37
+
38
+ /**
39
+ * Volume for playback of speech sounds, as a percentage.
40
+ * @type {number}
41
+ */
42
+ const SPEECH_VOLUME = 250;
43
+
44
+ /**
45
+ * An id for one of the voices.
46
+ */
47
+ const ALTO_ID = 'ALTO';
48
+
49
+ /**
50
+ * An id for one of the voices.
51
+ */
52
+ const TENOR_ID = 'TENOR';
53
+
54
+ /**
55
+ * An id for one of the voices.
56
+ */
57
+ const SQUEAK_ID = 'SQUEAK';
58
+
59
+ /**
60
+ * An id for one of the voices.
61
+ */
62
+ const GIANT_ID = 'GIANT';
63
+
64
+ /**
65
+ * An id for one of the voices.
66
+ */
67
+ const KITTEN_ID = 'KITTEN';
68
+
69
+ /**
70
+ * Playback rate for the tenor voice, for cases where we have only a female gender voice.
71
+ */
72
+ const FEMALE_TENOR_RATE = 0.89; // -2 semitones
73
+
74
+ /**
75
+ * Playback rate for the giant voice, for cases where we have only a female gender voice.
76
+ */
77
+ const FEMALE_GIANT_RATE = 0.79; // -4 semitones
78
+
79
+ /**
80
+ * Language ids. The value for each language id is a valid Scratch locale.
81
+ */
82
+ const ARABIC_ID = 'ar';
83
+ const CHINESE_ID = 'zh-cn';
84
+ const DANISH_ID = 'da';
85
+ const DUTCH_ID = 'nl';
86
+ const ENGLISH_ID = 'en';
87
+ const FRENCH_ID = 'fr';
88
+ const GERMAN_ID = 'de';
89
+ const HINDI_ID = 'hi';
90
+ const ICELANDIC_ID = 'is';
91
+ const ITALIAN_ID = 'it';
92
+ const JAPANESE_ID = 'ja';
93
+ const KOREAN_ID = 'ko';
94
+ const NORWEGIAN_ID = 'nb';
95
+ const POLISH_ID = 'pl';
96
+ const PORTUGUESE_BR_ID = 'pt-br';
97
+ const PORTUGUESE_ID = 'pt';
98
+ const ROMANIAN_ID = 'ro';
99
+ const RUSSIAN_ID = 'ru';
100
+ const SPANISH_ID = 'es';
101
+ const SPANISH_419_ID = 'es-419';
102
+ const SWEDISH_ID = 'sv';
103
+ const TURKISH_ID = 'tr';
104
+ const WELSH_ID = 'cy';
105
+
106
+ /**
107
+ * Class for the text2speech blocks.
108
+ * @constructor
109
+ */
110
+ class Scratch3Text2SpeechBlocks {
111
+ constructor (runtime) {
112
+ /**
113
+ * The runtime instantiating this block package.
114
+ * @type {Runtime}
115
+ */
116
+ this.runtime = runtime;
117
+
118
+ /**
119
+ * Map of soundPlayers by sound id.
120
+ * @type {Map<string, SoundPlayer>}
121
+ */
122
+ this._soundPlayers = new Map();
123
+
124
+ this._stopAllSpeech = this._stopAllSpeech.bind(this);
125
+ if (this.runtime) {
126
+ this.runtime.on('PROJECT_STOP_ALL', this._stopAllSpeech);
127
+ }
128
+
129
+ this._onTargetCreated = this._onTargetCreated.bind(this);
130
+ if (this.runtime) {
131
+ runtime.on('targetWasCreated', this._onTargetCreated);
132
+ }
133
+
134
+ /**
135
+ * A list of all Scratch locales that are supported by the extension.
136
+ * @type {Array}
137
+ */
138
+ this._supportedLocales = this._getSupportedLocales();
139
+ }
140
+
141
+ /**
142
+ * An object with info for each voice.
143
+ */
144
+ get VOICE_INFO () {
145
+ return {
146
+ [ALTO_ID]: {
147
+ name: formatMessage({
148
+ id: 'text2speech.alto',
149
+ default: 'alto',
150
+ description: 'Name for a voice with ambiguous gender.'
151
+ }),
152
+ gender: 'female',
153
+ playbackRate: 1
154
+ },
155
+ [TENOR_ID]: {
156
+ name: formatMessage({
157
+ id: 'text2speech.tenor',
158
+ default: 'tenor',
159
+ description: 'Name for a voice with ambiguous gender.'
160
+ }),
161
+ gender: 'male',
162
+ playbackRate: 1
163
+ },
164
+ [SQUEAK_ID]: {
165
+ name: formatMessage({
166
+ id: 'text2speech.squeak',
167
+ default: 'squeak',
168
+ description: 'Name for a funny voice with a high pitch.'
169
+ }),
170
+ gender: 'female',
171
+ playbackRate: 1.19 // +3 semitones
172
+ },
173
+ [GIANT_ID]: {
174
+ name: formatMessage({
175
+ id: 'text2speech.giant',
176
+ default: 'giant',
177
+ description: 'Name for a funny voice with a low pitch.'
178
+ }),
179
+ gender: 'male',
180
+ playbackRate: 0.84 // -3 semitones
181
+ },
182
+ [KITTEN_ID]: {
183
+ name: formatMessage({
184
+ id: 'text2speech.kitten',
185
+ default: 'kitten',
186
+ description: 'A baby cat.'
187
+ }),
188
+ gender: 'female',
189
+ playbackRate: 1.41 // +6 semitones
190
+ }
191
+ };
192
+ }
193
+
194
+ /**
195
+ * An object with information for each language.
196
+ *
197
+ * A note on the different sets of locales referred to in this extension:
198
+ *
199
+ * SCRATCH LOCALE
200
+ * Set by the editor, and used to store the language state in the project.
201
+ * Listed in l10n: https://github.com/scratchfoundation/scratch-l10n/blob/master/src/supported-locales.js
202
+ * SUPPORTED LOCALE
203
+ * A Scratch locale that has a corresponding extension locale.
204
+ * EXTENSION LOCALE
205
+ * A locale corresponding to one of the available spoken languages
206
+ * in the extension. There can be multiple supported locales for a single
207
+ * extension locale. For example, for both written versions of chinese,
208
+ * zh-cn and zh-tw, we use a single spoken language (Mandarin). So there
209
+ * are two supported locales, with a single extension locale.
210
+ * SPEECH SYNTH LOCALE
211
+ * A different locale code system, used by our speech synthesis service.
212
+ * Each extension locale has a speech synth locale.
213
+ */
214
+ get LANGUAGE_INFO () {
215
+ return {
216
+ [ARABIC_ID]: {
217
+ name: 'Arabic',
218
+ locales: ['ar'],
219
+ speechSynthLocale: 'arb',
220
+ singleGender: true
221
+ },
222
+ [CHINESE_ID]: {
223
+ name: 'Chinese (Mandarin)',
224
+ locales: ['zh-cn', 'zh-tw'],
225
+ speechSynthLocale: 'cmn-CN',
226
+ singleGender: true
227
+ },
228
+ [DANISH_ID]: {
229
+ name: 'Danish',
230
+ locales: ['da'],
231
+ speechSynthLocale: 'da-DK'
232
+ },
233
+ [DUTCH_ID]: {
234
+ name: 'Dutch',
235
+ locales: ['nl'],
236
+ speechSynthLocale: 'nl-NL'
237
+ },
238
+ [ENGLISH_ID]: {
239
+ name: 'English',
240
+ locales: ['en'],
241
+ speechSynthLocale: 'en-US'
242
+ },
243
+ [FRENCH_ID]: {
244
+ name: 'French',
245
+ locales: ['fr'],
246
+ speechSynthLocale: 'fr-FR'
247
+ },
248
+ [GERMAN_ID]: {
249
+ name: 'German',
250
+ locales: ['de'],
251
+ speechSynthLocale: 'de-DE'
252
+ },
253
+ [HINDI_ID]: {
254
+ name: 'Hindi',
255
+ locales: ['hi'],
256
+ speechSynthLocale: 'hi-IN',
257
+ singleGender: true
258
+ },
259
+ [ICELANDIC_ID]: {
260
+ name: 'Icelandic',
261
+ locales: ['is'],
262
+ speechSynthLocale: 'is-IS'
263
+ },
264
+ [ITALIAN_ID]: {
265
+ name: 'Italian',
266
+ locales: ['it'],
267
+ speechSynthLocale: 'it-IT'
268
+ },
269
+ [JAPANESE_ID]: {
270
+ name: 'Japanese',
271
+ locales: ['ja', 'ja-hira'],
272
+ speechSynthLocale: 'ja-JP'
273
+ },
274
+ [KOREAN_ID]: {
275
+ name: 'Korean',
276
+ locales: ['ko'],
277
+ speechSynthLocale: 'ko-KR',
278
+ singleGender: true
279
+ },
280
+ [NORWEGIAN_ID]: {
281
+ name: 'Norwegian',
282
+ locales: ['nb', 'nn'],
283
+ speechSynthLocale: 'nb-NO',
284
+ singleGender: true
285
+ },
286
+ [POLISH_ID]: {
287
+ name: 'Polish',
288
+ locales: ['pl'],
289
+ speechSynthLocale: 'pl-PL'
290
+ },
291
+ [PORTUGUESE_BR_ID]: {
292
+ name: 'Portuguese (Brazilian)',
293
+ locales: ['pt-br'],
294
+ speechSynthLocale: 'pt-BR'
295
+ },
296
+ [PORTUGUESE_ID]: {
297
+ name: 'Portuguese (European)',
298
+ locales: ['pt'],
299
+ speechSynthLocale: 'pt-PT'
300
+ },
301
+ [ROMANIAN_ID]: {
302
+ name: 'Romanian',
303
+ locales: ['ro'],
304
+ speechSynthLocale: 'ro-RO',
305
+ singleGender: true
306
+ },
307
+ [RUSSIAN_ID]: {
308
+ name: 'Russian',
309
+ locales: ['ru'],
310
+ speechSynthLocale: 'ru-RU'
311
+ },
312
+ [SPANISH_ID]: {
313
+ name: 'Spanish (European)',
314
+ locales: ['es'],
315
+ speechSynthLocale: 'es-ES'
316
+ },
317
+ [SPANISH_419_ID]: {
318
+ name: 'Spanish (Latin American)',
319
+ locales: ['es-419'],
320
+ speechSynthLocale: 'es-US'
321
+ },
322
+ [SWEDISH_ID]: {
323
+ name: 'Swedish',
324
+ locales: ['sv'],
325
+ speechSynthLocale: 'sv-SE',
326
+ singleGender: true
327
+ },
328
+ [TURKISH_ID]: {
329
+ name: 'Turkish',
330
+ locales: ['tr'],
331
+ speechSynthLocale: 'tr-TR',
332
+ singleGender: true
333
+ },
334
+ [WELSH_ID]: {
335
+ name: 'Welsh',
336
+ locales: ['cy'],
337
+ speechSynthLocale: 'cy-GB',
338
+ singleGender: true
339
+ }
340
+ };
341
+ }
342
+
343
+ /**
344
+ * The key to load & store a target's text2speech state.
345
+ * @return {string} The key.
346
+ */
347
+ static get STATE_KEY () {
348
+ return 'Scratch.text2speech';
349
+ }
350
+
351
+ /**
352
+ * The default state, to be used when a target has no existing state.
353
+ * @type {Text2SpeechState}
354
+ */
355
+ static get DEFAULT_TEXT2SPEECH_STATE () {
356
+ return {
357
+ voiceId: ALTO_ID
358
+ };
359
+ }
360
+
361
+ /**
362
+ * A default language to use for speech synthesis.
363
+ * @type {string}
364
+ */
365
+ get DEFAULT_LANGUAGE () {
366
+ return ENGLISH_ID;
367
+ }
368
+
369
+ /**
370
+ * @param {Target} target - collect state for this target.
371
+ * @returns {Text2SpeechState} the mutable state associated with that target. This will be created if necessary.
372
+ * @private
373
+ */
374
+ _getState (target) {
375
+ let state = target.getCustomState(Scratch3Text2SpeechBlocks.STATE_KEY);
376
+ if (!state) {
377
+ state = Clone.simple(Scratch3Text2SpeechBlocks.DEFAULT_TEXT2SPEECH_STATE);
378
+ target.setCustomState(Scratch3Text2SpeechBlocks.STATE_KEY, state);
379
+ }
380
+ return state;
381
+ }
382
+
383
+ /**
384
+ * When a Target is cloned, clone the state.
385
+ * @param {Target} newTarget - the newly created target.
386
+ * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
387
+ * @listens Runtime#event:targetWasCreated
388
+ * @private
389
+ */
390
+ _onTargetCreated (newTarget, sourceTarget) {
391
+ if (sourceTarget) {
392
+ const state = sourceTarget.getCustomState(Scratch3Text2SpeechBlocks.STATE_KEY);
393
+ if (state) {
394
+ newTarget.setCustomState(Scratch3Text2SpeechBlocks.STATE_KEY, Clone.simple(state));
395
+ }
396
+ }
397
+ }
398
+
399
+ /**
400
+ * @returns {object} metadata for this extension and its blocks.
401
+ */
402
+ getInfo () {
403
+ // Only localize the default input to the "speak" block if we are in a
404
+ // supported language.
405
+ let defaultTextToSpeak = 'hello';
406
+ if (this.isSupportedLanguage(this.getEditorLanguage())) {
407
+ defaultTextToSpeak = formatMessage({
408
+ id: 'text2speech.defaultTextToSpeak',
409
+ default: 'hello',
410
+ description: 'hello: the default text to speak'
411
+ });
412
+ }
413
+
414
+ return {
415
+ id: 'text2speech',
416
+ name: formatMessage({
417
+ id: 'text2speech.categoryName',
418
+ default: 'Text to Speech',
419
+ description: 'Name of the Text to Speech extension.'
420
+ }),
421
+ blockIconURI: blockIconURI,
422
+ menuIconURI: menuIconURI,
423
+ blocks: [
424
+ {
425
+ opcode: 'speakAndWait',
426
+ text: formatMessage({
427
+ id: 'text2speech.speakAndWaitBlock',
428
+ default: 'speak [WORDS]',
429
+ description: 'Speak some words.'
430
+ }),
431
+ blockType: BlockType.COMMAND,
432
+ arguments: {
433
+ WORDS: {
434
+ type: ArgumentType.STRING,
435
+ defaultValue: defaultTextToSpeak
436
+ }
437
+ }
438
+ },
439
+ {
440
+ opcode: 'setVoice',
441
+ text: formatMessage({
442
+ id: 'text2speech.setVoiceBlock',
443
+ default: 'set voice to [VOICE]',
444
+ description: 'Set the voice for speech synthesis.'
445
+ }),
446
+ blockType: BlockType.COMMAND,
447
+ arguments: {
448
+ VOICE: {
449
+ type: ArgumentType.STRING,
450
+ menu: 'voices',
451
+ defaultValue: ALTO_ID
452
+ }
453
+ }
454
+ },
455
+ {
456
+ opcode: 'setLanguage',
457
+ text: formatMessage({
458
+ id: 'text2speech.setLanguageBlock',
459
+ default: 'set language to [LANGUAGE]',
460
+ description: 'Set the language for speech synthesis.'
461
+ }),
462
+ blockType: BlockType.COMMAND,
463
+ arguments: {
464
+ LANGUAGE: {
465
+ type: ArgumentType.STRING,
466
+ menu: 'languages',
467
+ defaultValue: this.getCurrentLanguage()
468
+ }
469
+ }
470
+ }
471
+ ],
472
+ menus: {
473
+ voices: {
474
+ acceptReporters: true,
475
+ items: this.getVoiceMenu()
476
+ },
477
+ languages: {
478
+ acceptReporters: true,
479
+ items: this.getLanguageMenu()
480
+ }
481
+ }
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Get the language code currently set in the editor, or fall back to the
487
+ * browser locale.
488
+ * @return {string} a Scratch locale code.
489
+ */
490
+ getEditorLanguage () {
491
+ const locale = formatMessage.setup().locale ||
492
+ navigator.language || navigator.userLanguage || this.DEFAULT_LANGUAGE;
493
+ return locale.toLowerCase();
494
+ }
495
+
496
+ /**
497
+ * Get the language code currently set for the extension.
498
+ * @returns {string} a Scratch locale code.
499
+ */
500
+ getCurrentLanguage () {
501
+ const stage = this.runtime.getTargetForStage();
502
+ if (!stage) return this.DEFAULT_LANGUAGE;
503
+ // If no language has been set, set it to the editor locale (or default).
504
+ if (!stage.textToSpeechLanguage) {
505
+ this.setCurrentLanguage(this.getEditorLanguage());
506
+ }
507
+ return stage.textToSpeechLanguage;
508
+ }
509
+
510
+ /**
511
+ * Set the language code for the extension.
512
+ * It is stored in the stage so it can be saved and loaded with the project.
513
+ * @param {string} locale a locale code.
514
+ */
515
+ setCurrentLanguage (locale) {
516
+ const stage = this.runtime.getTargetForStage();
517
+ if (!stage) return;
518
+
519
+ if (this.isSupportedLanguage(locale)) {
520
+ stage.textToSpeechLanguage = this._getExtensionLocaleForSupportedLocale(locale);
521
+ }
522
+
523
+ // Support language names dropped onto the menu via reporter block
524
+ // such as a variable containing a language name (in any language),
525
+ // or the translate extension's language reporter.
526
+ const localeForDroppedName = languageNames.nameMap[locale.toLowerCase()];
527
+ if (localeForDroppedName && this.isSupportedLanguage(localeForDroppedName)) {
528
+ stage.textToSpeechLanguage =
529
+ this._getExtensionLocaleForSupportedLocale(localeForDroppedName);
530
+ }
531
+
532
+ // If the language is null, set it to the default language.
533
+ // This can occur e.g. if the extension was loaded with the editor
534
+ // set to a language that is not in the list.
535
+ if (!stage.textToSpeechLanguage) {
536
+ stage.textToSpeechLanguage = this.DEFAULT_LANGUAGE;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Get the extension locale for a supported locale, or null.
542
+ * @param {string} locale a locale code.
543
+ * @returns {?string} a locale supported by the extension.
544
+ */
545
+ _getExtensionLocaleForSupportedLocale (locale) {
546
+ for (const lang in this.LANGUAGE_INFO) {
547
+ if (this.LANGUAGE_INFO[lang].locales.includes(locale)) {
548
+ return lang;
549
+ }
550
+ }
551
+ log.error(`cannot find extension locale for locale ${locale}`);
552
+ }
553
+
554
+ /**
555
+ * Get the locale code used by the speech synthesis server corresponding to
556
+ * the current language code set for the extension.
557
+ * @returns {string} a speech synthesis locale.
558
+ */
559
+ _getSpeechSynthLocale () {
560
+ let speechSynthLocale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale;
561
+ if (this.LANGUAGE_INFO[this.getCurrentLanguage()]) {
562
+ speechSynthLocale = this.LANGUAGE_INFO[this.getCurrentLanguage()].speechSynthLocale;
563
+ }
564
+ return speechSynthLocale;
565
+ }
566
+
567
+ /**
568
+ * Get an array of the locales supported by this extension.
569
+ * @returns {Array} An array of locale strings.
570
+ */
571
+ _getSupportedLocales () {
572
+ return Object.keys(this.LANGUAGE_INFO).reduce((acc, lang) =>
573
+ acc.concat(this.LANGUAGE_INFO[lang].locales), []);
574
+ }
575
+
576
+ /**
577
+ * Check if a Scratch language code is in the list of supported languages for the
578
+ * speech synthesis service.
579
+ * @param {string} languageCode the language code to check.
580
+ * @returns {boolean} true if the language code is supported.
581
+ */
582
+ isSupportedLanguage (languageCode) {
583
+ return this._supportedLocales.includes(languageCode);
584
+ }
585
+
586
+ /**
587
+ * Get the menu of voices for the "set voice" block.
588
+ * @return {array} the text and value for each menu item.
589
+ */
590
+ getVoiceMenu () {
591
+ return Object.keys(this.VOICE_INFO).map(voiceId => ({
592
+ text: this.VOICE_INFO[voiceId].name,
593
+ value: voiceId
594
+ }));
595
+ }
596
+
597
+ /**
598
+ * Get the localized menu of languages for the "set language" block.
599
+ * For each language:
600
+ * if there is a custom translated spoken language name, use that;
601
+ * otherwise use the translation in the languageNames menuMap;
602
+ * otherwise fall back to the untranslated name in LANGUAGE_INFO.
603
+ * @return {array} the text and value for each menu item.
604
+ */
605
+ getLanguageMenu () {
606
+ const editorLanguage = this.getEditorLanguage();
607
+ // Get the array of localized language names
608
+ const localizedNameMap = {};
609
+ let nameArray = languageNames.menuMap[editorLanguage];
610
+ if (nameArray) {
611
+ // Also get any localized names of spoken languages
612
+ let spokenNameArray = [];
613
+ if (languageNames.spokenLanguages) {
614
+ spokenNameArray = languageNames.spokenLanguages[editorLanguage];
615
+ nameArray = nameArray.concat(spokenNameArray);
616
+ }
617
+ // Create a map of language code to localized name
618
+ // The localized spoken language names have been concatenated onto
619
+ // the end of the name array, so the result of the forEach below is
620
+ // when there is both a written language name (e.g. 'Chinese
621
+ // (simplified)') and a spoken language name (e.g. 'Chinese
622
+ // (Mandarin)', we always use the spoken version.
623
+ nameArray.forEach(lang => {
624
+ localizedNameMap[lang.code] = lang.name;
625
+ });
626
+ }
627
+
628
+ return Object.keys(this.LANGUAGE_INFO).map(key => {
629
+ let name = this.LANGUAGE_INFO[key].name;
630
+ const localizedName = localizedNameMap[key];
631
+ if (localizedName) {
632
+ name = localizedName;
633
+ }
634
+ // Uppercase the first character of the name
635
+ name = name.charAt(0).toUpperCase() + name.slice(1);
636
+ return {
637
+ text: name,
638
+ value: key
639
+ };
640
+ });
641
+ }
642
+
643
+ /**
644
+ * Set the voice for speech synthesis for this sprite.
645
+ * @param {object} args Block arguments
646
+ * @param {object} util Utility object provided by the runtime.
647
+ */
648
+ setVoice (args, util) {
649
+ const state = this._getState(util.target);
650
+
651
+ let voice = args.VOICE;
652
+
653
+ // If the arg is a dropped number, treat it as a voice index
654
+ let voiceNum = parseInt(voice, 10);
655
+ if (!isNaN(voiceNum)) {
656
+ voiceNum -= 1; // Treat dropped args as one-indexed
657
+ voiceNum = MathUtil.wrapClamp(voiceNum, 0, Object.keys(this.VOICE_INFO).length - 1);
658
+ voice = Object.keys(this.VOICE_INFO)[voiceNum];
659
+ }
660
+
661
+ // Only set the voice if the arg is a valid voice id.
662
+ if (Object.keys(this.VOICE_INFO).includes(voice)) {
663
+ state.voiceId = voice;
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Set the language for speech synthesis.
669
+ * @param {object} args Block arguments
670
+ */
671
+ setLanguage (args) {
672
+ this.setCurrentLanguage(args.LANGUAGE);
673
+ }
674
+
675
+ /**
676
+ * Stop all currently playing speech sounds.
677
+ */
678
+ _stopAllSpeech () {
679
+ this._soundPlayers.forEach(player => {
680
+ player.stop();
681
+ });
682
+ }
683
+
684
+ /**
685
+ * Convert the provided text into a sound file and then play the file.
686
+ * @param {object} args Block arguments
687
+ * @param {object} util Utility object provided by the runtime.
688
+ * @return {Promise} A promise that resolves after playing the sound
689
+ */
690
+ speakAndWait (args, util) {
691
+ // Cast input to string
692
+ let words = Cast.toString(args.WORDS);
693
+ let locale = this._getSpeechSynthLocale();
694
+
695
+ const state = this._getState(util.target);
696
+
697
+ let gender = this.VOICE_INFO[state.voiceId].gender;
698
+ let playbackRate = this.VOICE_INFO[state.voiceId].playbackRate;
699
+
700
+ // Special case for voices where the synthesis service only provides a
701
+ // single gender voice. In that case, always request the female voice,
702
+ // and set special playback rates for the tenor and giant voices.
703
+ if (this.LANGUAGE_INFO[this.getCurrentLanguage()].singleGender) {
704
+ gender = 'female';
705
+ if (state.voiceId === TENOR_ID) {
706
+ playbackRate = FEMALE_TENOR_RATE;
707
+ }
708
+ if (state.voiceId === GIANT_ID) {
709
+ playbackRate = FEMALE_GIANT_RATE;
710
+ }
711
+ }
712
+
713
+ if (state.voiceId === KITTEN_ID) {
714
+ words = words.replace(/\S+/g, 'meow');
715
+ locale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale;
716
+ }
717
+
718
+ // Build up URL
719
+ let path = `${SERVER_HOST}/synth`;
720
+ path += `?locale=${locale}`;
721
+ path += `&gender=${gender}`;
722
+ path += `&text=${encodeURIComponent(words.substring(0, 128))}`;
723
+
724
+ // Perform HTTP request to get audio file
725
+ return fetchWithTimeout(path, {}, SERVER_TIMEOUT)
726
+ .then(res => {
727
+ if (res.status !== 200) {
728
+ throw new Error(`HTTP ${res.status} error reaching translation service`);
729
+ }
730
+
731
+ return res.arrayBuffer();
732
+ })
733
+ .then(buffer => {
734
+ // Play the sound
735
+ const sound = {
736
+ data: {
737
+ buffer
738
+ }
739
+ };
740
+ return this.runtime.audioEngine.decodeSoundPlayer(sound);
741
+ })
742
+ .then(soundPlayer => {
743
+ this._soundPlayers.set(soundPlayer.id, soundPlayer);
744
+
745
+ soundPlayer.setPlaybackRate(playbackRate);
746
+
747
+ // Increase the volume
748
+ const engine = this.runtime.audioEngine;
749
+ const chain = engine.createEffectChain();
750
+ chain.set('volume', SPEECH_VOLUME);
751
+ soundPlayer.connect(chain);
752
+
753
+ soundPlayer.play();
754
+ return new Promise(resolve => {
755
+ soundPlayer.on('stop', () => {
756
+ this._soundPlayers.delete(soundPlayer.id);
757
+ resolve();
758
+ });
759
+ });
760
+ })
761
+ .catch(err => {
762
+ log.warn(err);
763
+ });
764
+ }
765
+ }
766
+ module.exports = Scratch3Text2SpeechBlocks;