@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.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- 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;
|