@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,700 @@
1
+ const ArgumentType = require('../../extension-support/argument-type');
2
+ const Cast = require('../../util/cast');
3
+ const BlockType = require('../../extension-support/block-type');
4
+ const formatMessage = require('format-message');
5
+ const log = require('../../util/log');
6
+ const DiffMatchPatch = require('diff-match-patch');
7
+
8
+
9
+ /**
10
+ * Url of icon to be displayed at the left edge of each extension block.
11
+ * @type {string}
12
+ */
13
+ // eslint-disable-next-line max-len
14
+ const iconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4K';
15
+
16
+
17
+ /**
18
+ * Url of icon to be displayed in the toolbox menu for the extension category.
19
+ * @type {string}
20
+ */
21
+ // eslint-disable-next-line max-len
22
+ const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzc1NzU3NSI+CiAgICA8cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPgogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgo8L3N2Zz4K';
23
+
24
+
25
+ /**
26
+ * The url of the speech server.
27
+ * @type {string}
28
+ */
29
+ const serverURL = 'wss://speech.scratch.mit.edu';
30
+
31
+ /**
32
+ * The amount of time to wait between when we stop sending speech data to the server and when
33
+ * we expect the transcription result marked with isFinal: true to come back from the server.
34
+ * @type {int}
35
+ */
36
+ const finalResponseTimeoutDurationMs = 3000;
37
+
38
+ /**
39
+ * The max amount of time the Listen And Wait block will listen for. It may listen for less time
40
+ * if we get back results that are good and think the user is done talking.
41
+ * Currently set to 10sec. This should not exceed the speech api limit (60sec) without redoing how
42
+ * we stream the microphone data data.
43
+ * @type {int}
44
+ */
45
+ const listenAndWaitBlockTimeoutMs = 10000;
46
+
47
+
48
+ class Scratch3Speech2TextBlocks {
49
+ constructor (runtime) {
50
+ /**
51
+ * The runtime instantiating this block package.
52
+ * @type {Runtime}
53
+ */
54
+ this.runtime = runtime;
55
+
56
+ /**
57
+ * An array of phrases from the [when I hear] hat blocks.
58
+ * The list of phrases in the when I hear hat blocks. This list is sent
59
+ * to the speech api to seed the recognition engine and for deciding
60
+ * whether the transcription results match.
61
+ * @type {Array}
62
+ * @private
63
+ */
64
+ this._phraseList = [];
65
+
66
+ /**
67
+ * The most recent transcription result received from the speech API that we decided to keep.
68
+ * This is the value returned by the reporter block.
69
+ * @type {String}
70
+ * @private
71
+ */
72
+ this._currentUtterance = '';
73
+
74
+ /**
75
+ * Similar to _currentUtterance, but set back to '' at the beginning of listening block
76
+ * and on green flag.
77
+ * Used to get the hat blocks to edge trigger. In order to detect someone saying
78
+ * the same thing twice in two subsequent listen and wait blocks
79
+ * and still trigger the hat, we need this to go from
80
+ * '' at the beginning of the listen block to '<transcription value>' at the end.
81
+ * @type {string}
82
+ * @private
83
+ */
84
+ this._utteranceForEdgeTrigger = null;
85
+
86
+ /**
87
+ * The list of queued `resolve` callbacks for 'Listen and Wait' blocks.
88
+ * We only listen to for one utterance at a time. We may encounter multiple
89
+ * 'Listen and wait' blocks that tell us to start listening. If one starts
90
+ * and hasn't receieved results back yet, when we encounter more, any further ones
91
+ * will all resolve when we get the next acceptable transcription result back.
92
+ * @type {!Array}
93
+ * @private
94
+ */
95
+ this._speechPromises = [];
96
+
97
+ /**
98
+ * The id of the timeout that will run if we start listening and don't get any
99
+ * transcription results back. e.g. because we didn't hear anything.
100
+ * @type {number}
101
+ * @private
102
+ */
103
+ this._speechTimeoutId = null;
104
+
105
+ /**
106
+ * The id of the timeout that will run to wait for after we're done listening but
107
+ * are still waiting for a potential isFinal:true transcription result to come back.
108
+ * @type {number}
109
+ * @private
110
+ */
111
+ this._speechFinalResponseTimeout = null;
112
+
113
+ /**
114
+ * The ScriptProcessorNode hooked up to the audio context.
115
+ * @type {ScriptProcessorNode}
116
+ * @private
117
+ */
118
+ this._scriptNode = null;
119
+
120
+ /**
121
+ * The socket used to communicate with the speech server to send microphone data
122
+ * and recieve transcription results.
123
+ * @type {WebSocket}
124
+ * @private
125
+ */
126
+ this._socket = null;
127
+
128
+ /**
129
+ * The AudioContext used to manage the microphone.
130
+ * @type {AudioContext}
131
+ * @private
132
+ */
133
+ this._context = null;
134
+
135
+ /**
136
+ * MediaStreamAudioSourceNode to handle microphone data.
137
+ * @type {MediaStreamAudioSourceNode}
138
+ * @private
139
+ */
140
+ this._sourceNode = null;
141
+
142
+ /**
143
+ * A Promise whose fulfillment handler receives a MediaStream object when the microphone has been obtained.
144
+ * @type {Promise}
145
+ * @private
146
+ */
147
+ this._audioPromise = null;
148
+
149
+
150
+ /**
151
+ * Diff Match Patch is used to do some fuzzy matching of the transcription results
152
+ * with what is in the hat blocks.
153
+ */
154
+ this._dmp = new DiffMatchPatch();
155
+ // Threshold for diff match patch to use: (0.0 = perfection, 1.0 = very loose).
156
+ this._dmp.Match_Threshold = 0.3;
157
+
158
+ this._newSocketCallback = this._newSocketCallback.bind(this);
159
+ this._setupSocketCallback = this._setupSocketCallback.bind(this);
160
+ this._socketMessageCallback = this._socketMessageCallback.bind(this);
161
+ this._processAudioCallback = this._processAudioCallback.bind(this);
162
+ this._onTranscriptionFromServer = this._onTranscriptionFromServer.bind(this);
163
+ this._resetListening = this._resetListening.bind(this);
164
+ this._stopTranscription = this._stopTranscription.bind(this);
165
+
166
+
167
+ this.runtime.on('PROJECT_STOP_ALL', this._resetListening.bind(this));
168
+ this.runtime.on('PROJECT_START', this._resetEdgeTriggerUtterance.bind(this));
169
+
170
+ }
171
+
172
+ /**
173
+ * Scans all the 'When I hear' hat blocks for each sprite and pulls out the text. The list
174
+ * is sent off to the speech recognition server as hints. This *only* reads the value out of
175
+ * the hat block shadow. If a block is dropped on top of the shadow, it is skipped.
176
+ * @returns {Array} list of strings from the hat blocks in the project.
177
+ * @private
178
+ */
179
+ _scanBlocksForPhraseList () {
180
+ const words = [];
181
+ // For each each target, walk through the top level blocks and check whether
182
+ // they are speech hat/when I hear blocks.
183
+ this.runtime.targets.forEach(target => {
184
+ target.blocks._scripts.forEach(id => {
185
+ const b = target.blocks.getBlock(id);
186
+ if (b.opcode === 'speech_whenIHearHat') {
187
+ // Grab the text from the hat block's shadow.
188
+ const inputId = b.inputs.PHRASE.block;
189
+ const inputBlock = target.blocks.getBlock(inputId);
190
+ // Only grab the value from text blocks. This means we'll
191
+ // miss some. e.g. values in variables or other reporters.
192
+ if (inputBlock.opcode === 'text') {
193
+ const word = target.blocks.getBlock(inputId).fields.TEXT.value;
194
+ words.push(word);
195
+ }
196
+ }
197
+ });
198
+ });
199
+ return words;
200
+ }
201
+
202
+ /**
203
+ * Get the viewer's language code.
204
+ * @return {string} the language code.
205
+ */
206
+ _getViewerLanguageCode () {
207
+ return formatMessage.setup().locale || navigator.language || navigator.userLanguage || 'en-US';
208
+ }
209
+
210
+ /**
211
+ * Resets all things related to listening. Called on Red Stop sign button.
212
+ * - suspends audio processing
213
+ * - closes socket with speech socket server
214
+ * - clears out any remaining speech blocks that are waiting.
215
+ * @private.
216
+ */
217
+ _resetListening () {
218
+ this.runtime.emitMicListening(false);
219
+ this._stopListening();
220
+ this._closeWebsocket();
221
+ this._resolveSpeechPromises();
222
+ }
223
+
224
+ /**
225
+ * Reset the utterance we look for in the when I hear hat block back to
226
+ * the empty string.
227
+ * @private
228
+ */
229
+ _resetEdgeTriggerUtterance () {
230
+ this._utteranceForEdgeTrigger = '';
231
+ }
232
+
233
+ /**
234
+ * Close the connection to the socket server if it is open.
235
+ * @private
236
+ */
237
+ _closeWebsocket () {
238
+ if (this._socket && this._socket.readyState === this._socket.OPEN) {
239
+ this._socket.close();
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Call to suspend getting data from the microphone.
245
+ * @private
246
+ */
247
+ _stopListening () {
248
+ // Note that this can be called before any Listen And Wait block did setup,
249
+ // so check that things exist before disconnecting them.
250
+ if (this._context) {
251
+ this._context.suspend.bind(this._context);
252
+ }
253
+ // This is called on green flag to reset things that may never have existed
254
+ // in the first place. Do a bunch of checks.
255
+ if (this._scriptNode) {
256
+ this._scriptNode.removeEventListener('audioprocess', this._processAudioCallback);
257
+ this._scriptNode.disconnect();
258
+ }
259
+ if (this._sourceNode) {
260
+ this._sourceNode.disconnect();
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Resolves all the speech promises we've accumulated so far and empties out the list.
266
+ * @private
267
+ */
268
+ _resolveSpeechPromises () {
269
+ for (let i = 0; i < this._speechPromises.length; i++) {
270
+ const resFn = this._speechPromises[i];
271
+ resFn();
272
+ }
273
+ this._speechPromises = [];
274
+ }
275
+
276
+ /**
277
+ * Called when we want to stop listening (e.g. when a listen block times out)
278
+ * but we still want to wait a little to see if we get any transcription results
279
+ * back before yielding the block execution.
280
+ * @private
281
+ */
282
+ _stopTranscription () {
283
+ this._stopListening();
284
+ if (this._socket && this._socket.readyState === this._socket.OPEN) {
285
+ this._socket.send('stopTranscription');
286
+ }
287
+ // Give it a couple seconds to response before giving up and assuming nothing else will come back.
288
+ this._speechFinalResponseTimeout = setTimeout(this._resetListening, finalResponseTimeoutDurationMs);
289
+ }
290
+
291
+ /**
292
+ * Decides whether to keep a given transcirption result.
293
+ * @param {number} fuzzyMatchIndex Index of the fuzzy match or -1 if there is no match.
294
+ * @param {object} result The json object representing the transcription result.
295
+ * @param {string} normalizedTranscript The transcription text used for matching (i.e. lowercased, no punctuation).
296
+ * @returns {boolean} true If a result is good enough to be kept.
297
+ * @private
298
+ */
299
+ _shouldKeepResult (fuzzyMatchIndex, result, normalizedTranscript) {
300
+ // The threshold above which we decide transcription results are unlikely to change again.
301
+ // See https://cloud.google.com/speech-to-text/docs/basics#streaming_responses.
302
+ const stabilityThreshold = .85;
303
+
304
+ // For responsiveness of the When I Hear hat blocks, sometimes we want to keep results that are not
305
+ // yet marked 'isFinal' by the speech api. Here are some signals we use.
306
+
307
+ // If the result from the speech api isn't very stable and we only had a fuzzy match, we don't want to use it.
308
+ const shouldKeepFuzzyMatch = fuzzyMatchIndex !== -1 && result.stability > stabilityThreshold;
309
+
310
+ // TODO: This is for debugging. Remove when this function is finalized.
311
+ if (shouldKeepFuzzyMatch) {
312
+ log.info(`Fuzzy match with high stability.`);
313
+ log.info(`match index is ${fuzzyMatchIndex}`);
314
+ const phrases = this._phraseList.join(' ');
315
+ const matchPhrase = phrases.substring(fuzzyMatchIndex, fuzzyMatchIndex + normalizedTranscript.length);
316
+ log.info(`fuzzy match: ${matchPhrase} in ${normalizedTranscript}`);
317
+ }
318
+
319
+ // If the result is in the phraseList (i.e. it matches one of the 'When I Hear' blocks), we keep it.
320
+ // This might be aggressive... but so far seems to be a good thing.
321
+ const shouldKeepPhraseListMatch = this._phraseList.includes(normalizedTranscript);
322
+ // TODO: This is just for debugging. Remove when this function is finalized.
323
+ if (shouldKeepPhraseListMatch) {
324
+ log.info(`phrase list ${this._phraseList} includes ${normalizedTranscript}`);
325
+ }
326
+ // TODO: This is for debugging. Remove when this function is finalized.
327
+ if (result.isFinal) {
328
+ log.info(`result is final`);
329
+ }
330
+
331
+ if (!result.isFinal && !shouldKeepPhraseListMatch && !shouldKeepFuzzyMatch) {
332
+ return false;
333
+ }
334
+ return true;
335
+ }
336
+
337
+ /**
338
+ * Normalizes text a bit to facilitate matching. Lowercases, removes some punctuation and whitespace.
339
+ * @param {string} text The text to normalzie
340
+ * @returns {string} The normalized text.
341
+ * @private
342
+ */
343
+ _normalizeText (text) {
344
+ text = Cast.toString(text).toLowerCase();
345
+ text = text.replace(/[.?!]/g, '');
346
+ text = text.trim();
347
+ return text;
348
+ }
349
+
350
+ /**
351
+ * Call into diff match patch library to compute whether there is a fuzzy match.
352
+ * @param {string} text The text to search in.
353
+ * @param {string} pattern The pattern to look for in text.
354
+ * @returns {number} The index of the match or -1 if there isn't one.
355
+ */
356
+ _computeFuzzyMatch (text, pattern) {
357
+ // Don't bother matching if any are null.
358
+ if (!pattern || !text) {
359
+ return -1;
360
+ }
361
+ let match = -1;
362
+ try {
363
+ // Look for the text in the pattern starting at position 0.
364
+ match = this._dmp.match_main(text, pattern, 0);
365
+ } catch (e) {
366
+ // This can happen inf the text or pattern gets too long. If so just substring match.
367
+ return pattern.indexOf(text);
368
+ }
369
+ return match;
370
+ }
371
+
372
+ /**
373
+ * Processes the results we get back from the speech server. Decides whether the results
374
+ * are good enough to keep. If they are, resolves the 'Listen and Wait' blocks promise and cleans up.
375
+ * @param {object} result The transcription result.
376
+ * @private
377
+ */
378
+ _processTranscriptionResult (result) {
379
+ log.info(`Got result: ${JSON.stringify(result)}`);
380
+ const transcriptionResult = this._normalizeText(result.alternatives[0].transcript);
381
+
382
+ // Waiting for an exact match is not satisfying. It makes it hard to catch
383
+ // things like homonyms or things that sound similar "let us" vs "lettuce". Using the fuzzy matching helps
384
+ // more aggressively match the phrases that are in the "When I hear" hat blocks.
385
+ const phrases = this._phraseList.join(' ');
386
+ const fuzzyMatchIndex = this._computeFuzzyMatch(phrases, transcriptionResult);
387
+
388
+ // If the result isn't good enough yet, return without saving and resolving the promises.
389
+ if (!this._shouldKeepResult(fuzzyMatchIndex, result, transcriptionResult)) {
390
+ return;
391
+ }
392
+
393
+ this._currentUtterance = transcriptionResult;
394
+ log.info(`Keeing result: ${this._currentUtterance}`);
395
+ this._utteranceForEdgeTrigger = transcriptionResult;
396
+
397
+ // We're done listening so resolove all the promises and reset everying so we're ready for next time.
398
+ this._resetListening();
399
+
400
+ // We got results so clear out the timeouts.
401
+ if (this._speechTimeoutId) {
402
+ clearTimeout(this._speechTimeoutId);
403
+ this._speechTimeoutId = null;
404
+ }
405
+ if (this._speechFinalResponseTimeout) {
406
+ clearTimeout(this._speechFinalResponseTimeout);
407
+ this._speechFinalResponseTimeout = null;
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Handle a message from the socket. It contains transcription results.
413
+ * @param {MessageEvent} e The message event containing data from speech server.
414
+ * @private
415
+ */
416
+ _onTranscriptionFromServer (e) {
417
+ let result = null;
418
+ try {
419
+ result = JSON.parse(e.data);
420
+ } catch (ex) {
421
+ log.error(`Problem parsing json. continuing: ${ex}`);
422
+ // TODO: Question - Should we kill listening and continue?
423
+ return;
424
+ }
425
+ this._processTranscriptionResult(result);
426
+ }
427
+
428
+
429
+ /**
430
+ * Decide whether the pattern given matches the text. Uses fuzzy matching
431
+ * @param {string} pattern The pattern to look for. Usually this is the transcription result
432
+ * @param {string} text The text to look in. Usually this is the set of phrases from the when I hear blocks
433
+ * @returns {boolean} true if there is a fuzzy match.
434
+ * @private
435
+ */
436
+ _speechMatches (pattern, text) {
437
+ pattern = this._normalizeText(pattern);
438
+ text = this._normalizeText(text);
439
+ const match = this._computeFuzzyMatch(text, pattern);
440
+ return match !== -1;
441
+ }
442
+
443
+ /**
444
+ * Kick off the listening process.
445
+ * @private
446
+ */
447
+ _startListening () {
448
+ this.runtime.emitMicListening(true);
449
+ this._initListening();
450
+ // Force the block to timeout if we don't get any results back/the user didn't say anything.
451
+ this._speechTimeoutId = setTimeout(this._stopTranscription, listenAndWaitBlockTimeoutMs);
452
+ }
453
+
454
+ /**
455
+ * Resume listening for audio and re-open the socket to send data.
456
+ * @private
457
+ */
458
+ _resumeListening () {
459
+ this._context.resume.bind(this._context);
460
+ this._newWebsocket();
461
+ }
462
+
463
+ /**
464
+ * Does all setup to get microphone data and initializes the web socket.
465
+ * that data to the speech server.
466
+ * @private
467
+ */
468
+ _initListening () {
469
+ this._initializeMicrophone();
470
+ this._initScriptNode();
471
+ this._newWebsocket();
472
+ }
473
+
474
+ /**
475
+ * Initialize the audio context and connect the microphone.
476
+ * @private
477
+ */
478
+ _initializeMicrophone () {
479
+ // Don't make a new context if we already made one.
480
+ if (!this._context) {
481
+ // Safari still needs a webkit prefix for audio context
482
+ this._context = new (window.AudioContext || window.webkitAudioContext)();
483
+ }
484
+ // In safari we have to call getUserMedia every time we want to listen. Other browsers allow
485
+ // you to reuse the mediaStream. See #1202 for more context.
486
+ this._audioPromise = navigator.mediaDevices.getUserMedia({
487
+ audio: true
488
+ });
489
+
490
+ this._audioPromise.then().catch(e => {
491
+ log.error(`Problem connecting to microphone: ${e}`);
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Sets up the script processor and the web socket.
497
+ * @private
498
+ *
499
+ */
500
+ _initScriptNode () {
501
+ // Create a node that sends raw bytes across the websocket
502
+ this._scriptNode = this._context.createScriptProcessor(4096, 1, 1);
503
+ }
504
+
505
+ /**
506
+ * Callback called when it is time to setup the new web socket.
507
+ * @param {Function} resolve - function to call when the web socket opens succesfully.
508
+ * @param {Function} reject - function to call if opening the web socket fails.
509
+ */
510
+ _newSocketCallback (resolve, reject) {
511
+ this._socket = new WebSocket(serverURL);
512
+ this._socket.addEventListener('open', resolve);
513
+ this._socket.addEventListener('error', reject);
514
+ }
515
+
516
+ /**
517
+ * Callback called once we've initially established the web socket is open and working.
518
+ * Sets up the callback for subsequent messages (i.e. transcription results) and
519
+ * connects to the script node to get data.
520
+ * @private
521
+ */
522
+ _socketMessageCallback () {
523
+ this._socket.addEventListener('message', this._onTranscriptionFromServer);
524
+ this._startByteStream();
525
+ }
526
+
527
+ /**
528
+ * Sets up callback for when socket and audio are initialized.
529
+ * @private
530
+ */
531
+ _newWebsocket () {
532
+ const websocketPromise = new Promise(this._newSocketCallback);
533
+ Promise.all([this._audioPromise, websocketPromise]).then(
534
+ this._setupSocketCallback)
535
+ .catch(e => {
536
+ log.error(`Problem with setup: ${e}`);
537
+ });
538
+ }
539
+
540
+ /**
541
+ * Callback to handle initial setting up of a socket.
542
+ * Currently we send a setup message (only contains sample rate) but might
543
+ * be useful to send more data so we can do quota stuff.
544
+ * @param {Array} values The
545
+ */
546
+ _setupSocketCallback (values) {
547
+ this._micStream = values[0];
548
+ this._socket = values[1].target;
549
+
550
+ this._socket.addEventListener('error', e => {
551
+ log.error(`Error from web socket: ${e}`);
552
+ });
553
+
554
+ // Send the initial configuration message. When the server acknowledges
555
+ // it, start streaming the audio bytes to the server and listening for
556
+ // transcriptions.
557
+ this._socket.addEventListener('message', this._socketMessageCallback, {once: true});
558
+ const langCode = this._getViewerLanguageCode();
559
+ this._socket.send(JSON.stringify(
560
+ {
561
+ sampleRate: this._context.sampleRate,
562
+ phrases: this._phraseList,
563
+ locale: langCode
564
+ }
565
+ ));
566
+ }
567
+
568
+ /**
569
+ * Do setup so we can start streaming mic data.
570
+ * @private
571
+ */
572
+ _startByteStream () {
573
+ // Hook up the scriptNode to the mic
574
+ this._sourceNode = this._context.createMediaStreamSource(this._micStream);
575
+ this._sourceNode.connect(this._scriptNode);
576
+ this._scriptNode.addEventListener('audioprocess', this._processAudioCallback);
577
+ this._scriptNode.connect(this._context.destination);
578
+ }
579
+
580
+ /**
581
+ * Called when we have data from the microphone. Takes that data and ships
582
+ * it off to the speech server for transcription.
583
+ * @param {audioProcessingEvent} e The event with audio data in it.
584
+ * @private
585
+ */
586
+ _processAudioCallback (e) {
587
+ if (this._socket.readyState === WebSocket.CLOSED ||
588
+ this._socket.readyState === WebSocket.CLOSING) {
589
+ log.error(`Not sending data because not in ready state. State: ${this._socket.readyState}`);
590
+ // TODO: should we stop trying and reset state so it might work next time?
591
+ return;
592
+ }
593
+ const MAX_INT = Math.pow(2, 16 - 1) - 1;
594
+ const floatSamples = e.inputBuffer.getChannelData(0);
595
+ // The samples are floats in range [-1, 1]. Convert to 16-bit signed
596
+ // integer.
597
+ this._socket.send(Int16Array.from(floatSamples.map(n => n * MAX_INT)));
598
+ }
599
+
600
+ /**
601
+ * The key to load & store a target's speech-related state.
602
+ * @type {string}
603
+ */
604
+ static get STATE_KEY () {
605
+ return 'Scratch.speech';
606
+ }
607
+
608
+ /**
609
+ * @returns {object} Metadata for this extension and its blocks.
610
+ */
611
+ getInfo () {
612
+ return {
613
+ id: 'speech2text',
614
+ name: formatMessage({
615
+ id: 'speech.extensionName',
616
+ default: 'Speech to Text',
617
+ description: 'Name of extension that adds speech recognition blocks.'
618
+ }),
619
+ menuIconURI: menuIconURI,
620
+ blockIconURI: iconURI,
621
+ blocks: [
622
+ {
623
+ opcode: 'listenAndWait',
624
+ text: formatMessage({
625
+ id: 'speech.listenAndWait',
626
+ default: 'listen and wait',
627
+ // eslint-disable-next-line max-len
628
+ description: 'Start listening to the microphone and wait for a result from the speech recognition system.'
629
+ }),
630
+ blockType: BlockType.COMMAND
631
+ },
632
+ {
633
+ opcode: 'whenIHearHat',
634
+ text: formatMessage({
635
+ id: 'speech.whenIHear',
636
+ default: 'when I hear [PHRASE]',
637
+ // eslint-disable-next-line max-len
638
+ description: 'Event that triggers when the text entered on the block is recognized by the speech recognition system.'
639
+ }),
640
+ blockType: BlockType.HAT,
641
+ arguments: {
642
+ PHRASE: {
643
+ type: ArgumentType.STRING,
644
+ defaultValue: formatMessage({
645
+ id: 'speech.defaultWhenIHearValue',
646
+ default: 'let\'s go',
647
+ description: 'The default phrase/word that, when heard, triggers the event.'
648
+ })
649
+ }
650
+ }
651
+ },
652
+ {
653
+ opcode: 'getSpeech',
654
+ text: formatMessage({
655
+ id: 'speech.speechReporter',
656
+ default: 'speech',
657
+ description: 'Get the text of spoken words transcribed by the speech recognition system.'
658
+ }),
659
+ blockType: BlockType.REPORTER
660
+ }
661
+ ]
662
+ };
663
+ }
664
+
665
+ /**
666
+ * Start the listening process if it isn't already in progress.
667
+ * @return {Promise} A promise that will resolve when listening is complete.
668
+ */
669
+ listenAndWait () {
670
+ this._phraseList = this._scanBlocksForPhraseList();
671
+ this._resetEdgeTriggerUtterance();
672
+
673
+ const speechPromise = new Promise(resolve => {
674
+ const listeningInProgress = this._speechPromises.length > 0;
675
+ this._speechPromises.push(resolve);
676
+ if (!listeningInProgress) {
677
+ this._startListening();
678
+ }
679
+ });
680
+ return speechPromise;
681
+ }
682
+
683
+ /**
684
+ * An edge triggered hat block to listen for a specific phrase.
685
+ * @param {object} args - the block arguments.
686
+ * @return {boolean} true if the phrase matches what was transcribed.
687
+ */
688
+ whenIHearHat (args) {
689
+ return this._speechMatches(args.PHRASE, this._utteranceForEdgeTrigger);
690
+ }
691
+
692
+ /**
693
+ * Reporter for the last heard phrase/utterance.
694
+ * @return {string} The lastest thing we heard from a listen and wait block.
695
+ */
696
+ getSpeech () {
697
+ return this._currentUtterance;
698
+ }
699
+ }
700
+ module.exports = Scratch3Speech2TextBlocks;