Skip to content

Commit f61cf2f

Browse files
NishiOwODangoCat
andauthored
Add DECtalk (#2481)
Original code is at https://github.com/dectalk/tw-dectalk --------- Co-authored-by: DangoCat[bot] <dangocat@users.noreply.github.com>
1 parent d4acd5d commit f61cf2f

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

extension-dependencies.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
"https://raw.githubusercontent.com/PsychoGoldfishNG/NewgroundsIO-JS/b0383337ef72d42dac12b7a4d7a24dfbc4105eb3/dist/NewgroundsIO.min.js": {
4040
"sha256": "cc18cf00a76e0bc8a2117b9ac4c7c123131a7377e186576182d2a1552510a86a",
4141
"contentType": "text/plain; charset=utf-8"
42+
},
43+
"https://raw.githubusercontent.com/dectalk/tw-dectalk/79a9f2538e7cf712e6fd25d4345fab531c31800b/dtc.js": {
44+
"sha256": "f432689868a2e3d6e3f02b9bb60f2bf81349b9721e333cc80f94d533afda24b9",
45+
"contentType": "application/octet-stream"
4246
}
4347
}
4448
}

extensions/NishiOwO/dectalk.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Name: DECtalk
2+
// ID: nishiowoDectalk
3+
// Description: Use DECtalk.
4+
// By: NishiOwO
5+
// License: BSD-3-Clause
6+
7+
// Repository is at https://github.com/dectalk/tw-dectalk
8+
9+
(async function (Scratch) {
10+
"use strict";
11+
12+
if (!Scratch.extensions.unsandboxed) {
13+
throw new Error("DECtalk must be run unsandboxed");
14+
}
15+
16+
let Module, speak, speak_init;
17+
let g_buffer = {};
18+
let g_sources = [];
19+
let embedded = false;
20+
var DECtalkMini;
21+
22+
/* DO NOT REMOVE THE COMMENT BELOW!!! */
23+
/* EMBED DTC.JS HERE */
24+
25+
let dtc;
26+
if (embedded) {
27+
dtc = DECtalkMini;
28+
} else {
29+
dtc = await Scratch.external.evalAndReturn(
30+
"https://raw.githubusercontent.com/dectalk/tw-dectalk/79a9f2538e7cf712e6fd25d4345fab531c31800b/dtc.js",
31+
"DECtalkMini"
32+
);
33+
}
34+
35+
// @ts-ignore
36+
window.onDECtalkAudioCallback = function (tts, buffer, length, phoneme) {
37+
let arr_r = new Int16Array(Module.HEAP16.buffer, buffer, length);
38+
let arr = new Int16Array(length);
39+
40+
for (let i = 0; i < arr.length; i++) arr[i] = arr_r[i];
41+
42+
if (!g_buffer[tts]) g_buffer[tts] = [];
43+
g_buffer[tts].push(arr);
44+
};
45+
46+
class DECtalk {
47+
getInfo() {
48+
const blockIconURI =
49+
"data:image/png;base64,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";
50+
51+
return {
52+
id: "nishiowoDectalk",
53+
name: Scratch.translate("DECtalk"),
54+
blockIconURI: blockIconURI,
55+
color1: "#b3353f",
56+
blocks: [
57+
{
58+
opcode: "speakAndWait",
59+
blockType: Scratch.BlockType.COMMAND,
60+
text: Scratch.translate("speak [WORDS]"),
61+
arguments: {
62+
WORDS: {
63+
type: Scratch.ArgumentType.STRING,
64+
defaultValue: "Hello",
65+
},
66+
},
67+
},
68+
{
69+
opcode: "stopAll",
70+
blockType: Scratch.BlockType.COMMAND,
71+
text: Scratch.translate("stop all speaking"),
72+
},
73+
],
74+
};
75+
}
76+
77+
speakAndWait(args) {
78+
return new Promise(function (res, rej) {
79+
const audioContext = Scratch.vm.runtime.audioEngine.audioContext;
80+
const str = Module.stringToNewUTF8(Scratch.Cast.toString(args.WORDS));
81+
const tts = speak(str);
82+
Module._free(str);
83+
84+
let b = 0;
85+
if (g_buffer[tts]) {
86+
for (let i = 0; i < g_buffer[tts].length; i++) {
87+
b += g_buffer[tts][i].length;
88+
}
89+
} else {
90+
res();
91+
}
92+
93+
const audioBuffer = audioContext.createBuffer(1, b, 11025);
94+
const channelData = audioBuffer.getChannelData(0);
95+
96+
b = 0;
97+
98+
for (let i = 0; i < g_buffer[tts].length; i++) {
99+
for (let j = 0; j < g_buffer[tts][i].length; j++) {
100+
channelData[b + j] = g_buffer[tts][i][j] / 32767;
101+
}
102+
b += g_buffer[tts][i].length;
103+
}
104+
105+
const currentSource = audioContext.createBufferSource();
106+
currentSource.buffer = audioBuffer;
107+
currentSource.connect(audioContext.destination);
108+
109+
g_sources.push(currentSource);
110+
111+
currentSource.onended = function () {
112+
g_sources = g_sources.filter((x) => x != currentSource);
113+
res();
114+
};
115+
116+
currentSource.start();
117+
118+
if (g_buffer[tts]) delete g_buffer[tts];
119+
});
120+
}
121+
122+
stopAll() {
123+
for (let i of g_sources) {
124+
i.stop();
125+
}
126+
g_sources = [];
127+
}
128+
}
129+
130+
Module = await dtc();
131+
speak_init = Module.cwrap("speak_init", null, []);
132+
speak = Module.cwrap("speak", "number", ["number"]);
133+
134+
speak_init();
135+
136+
Scratch.extensions.register(new DECtalk());
137+
})(Scratch);

extensions/extensions.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,6 @@
9999
"itchio",
100100
"gamejolt",
101101
"obviousAlexC/newgroundsIO",
102+
"NishiOwO/dectalk",
102103
"Lily/McUtils" // McUtils should always be the last item.
103104
]

0 commit comments

Comments
 (0)