speechflow 2.2.1 → 2.3.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/{etc/claude.md → AGENTS.md} +8 -3
- package/CHANGELOG.md +98 -1
- package/README.md +28 -4
- package/etc/speechflow.yaml +3 -1
- package/etc/stx.conf +1 -1
- package/package.json +6 -6
- package/speechflow-cli/dst/speechflow-main-api.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-main-api.js +57 -16
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
- package/speechflow-cli/dst/speechflow-main-config.js +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.js +55 -21
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-nodes.js +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +6 -3
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +17 -19
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +25 -8
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +16 -13
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +7 -7
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +7 -4
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +21 -16
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +33 -11
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +42 -23
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +8 -2
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +24 -10
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +22 -7
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +10 -2
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +3 -0
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +160 -57
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +27 -15
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +84 -63
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +75 -20
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.js +5 -7
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +28 -15
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +2 -2
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.js +13 -3
- package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
- package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +9 -17
- package/speechflow-cli/dst/speechflow-util-queue.js +98 -78
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.js +1 -1
- package/speechflow-cli/dst/speechflow.d.ts +1 -1
- package/speechflow-cli/dst/speechflow.js +1 -1
- package/speechflow-cli/etc/eslint.mjs +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/etc/stx.conf +8 -2
- package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
- package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
- package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
- package/speechflow-cli/package.json +40 -30
- package/speechflow-cli/src/lib.d.ts +19 -1
- package/speechflow-cli/src/speechflow-main-api.ts +64 -19
- package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
- package/speechflow-cli/src/speechflow-main-config.ts +1 -1
- package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
- package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
- package/speechflow-cli/src/speechflow-main-status.ts +6 -3
- package/speechflow-cli/src/speechflow-main.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +19 -20
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +31 -13
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +17 -13
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +9 -8
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +8 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +22 -18
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +47 -25
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +12 -4
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +24 -10
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +22 -7
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +10 -2
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +215 -62
- package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
- package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +30 -16
- package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +92 -70
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +79 -22
- package/speechflow-cli/src/speechflow-node.ts +7 -8
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
- package/speechflow-cli/src/speechflow-util-audio.ts +31 -17
- package/speechflow-cli/src/speechflow-util-error.ts +3 -3
- package/speechflow-cli/src/speechflow-util-llm.ts +14 -3
- package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
- package/speechflow-cli/src/speechflow-util-queue.ts +103 -81
- package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
- package/speechflow-cli/src/speechflow-util.ts +1 -1
- package/speechflow-cli/src/speechflow.ts +1 -1
- package/speechflow-ui-db/dst/index.html +1 -1
- package/speechflow-ui-db/dst/index.js +15 -15
- package/speechflow-ui-db/etc/eslint.mjs +1 -1
- package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-db/etc/stx.conf +1 -1
- package/speechflow-ui-db/etc/stylelint.js +1 -1
- package/speechflow-ui-db/etc/stylelint.yaml +1 -1
- package/speechflow-ui-db/etc/vite-client.mts +1 -1
- package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-db/package.json +22 -16
- package/speechflow-ui-db/src/app.styl +1 -1
- package/speechflow-ui-db/src/app.vue +1 -1
- package/speechflow-ui-db/src/index.html +1 -1
- package/speechflow-ui-db/src/index.ts +1 -1
- package/speechflow-ui-st/dst/index.html +1 -1
- package/speechflow-ui-st/dst/index.js +31 -31
- package/speechflow-ui-st/etc/eslint.mjs +1 -1
- package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-st/etc/stx.conf +1 -1
- package/speechflow-ui-st/etc/stylelint.js +1 -1
- package/speechflow-ui-st/etc/stylelint.yaml +1 -1
- package/speechflow-ui-st/etc/vite-client.mts +1 -1
- package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-st/package.json +23 -17
- package/speechflow-ui-st/src/app.styl +1 -1
- package/speechflow-ui-st/src/app.vue +1 -1
- package/speechflow-ui-st/src/index.html +1 -1
- package/speechflow-ui-st/src/index.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -37,6 +37,7 @@ export class APIServer {
|
|
|
37
37
|
private wsPeers = new Map<string, WSPeerInfo>()
|
|
38
38
|
private hapi: HAPI.Server | null = null
|
|
39
39
|
private sendOSC: ((url: string, ...args: any[]) => void) | null = null
|
|
40
|
+
private graph: NodeGraph | null = null
|
|
40
41
|
|
|
41
42
|
/* creation */
|
|
42
43
|
constructor (
|
|
@@ -45,6 +46,9 @@ export class APIServer {
|
|
|
45
46
|
|
|
46
47
|
/* start API server service */
|
|
47
48
|
async start (args: CLIOptions, graph: NodeGraph): Promise<void> {
|
|
49
|
+
/* remember graph for later cleanup */
|
|
50
|
+
this.graph = graph
|
|
51
|
+
|
|
48
52
|
/* define external request/response structure */
|
|
49
53
|
const requestValidator = arktype.type({
|
|
50
54
|
request: "string",
|
|
@@ -67,10 +71,13 @@ export class APIServer {
|
|
|
67
71
|
throw new Error(`external request failed: no such node <${name}>`)
|
|
68
72
|
}
|
|
69
73
|
else {
|
|
74
|
+
const ac = new AbortController()
|
|
70
75
|
await Promise.race([
|
|
71
76
|
foundNode.receiveRequest(argList),
|
|
72
|
-
util.timeout(10 * 1000)
|
|
73
|
-
]).
|
|
77
|
+
util.timeout(10 * 1000, "timeout", ac.signal)
|
|
78
|
+
]).finally(() => {
|
|
79
|
+
ac.abort()
|
|
80
|
+
}).catch((err: Error) => {
|
|
74
81
|
this.cli.log("warning", `external request to node <${name}> failed: ${err.message}`)
|
|
75
82
|
throw err
|
|
76
83
|
})
|
|
@@ -133,9 +140,14 @@ export class APIServer {
|
|
|
133
140
|
method: "GET",
|
|
134
141
|
path: "/api/dashboard",
|
|
135
142
|
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
136
|
-
const config = []
|
|
143
|
+
const config: { type: string, id: string, name: string }[] = []
|
|
144
|
+
if (args.d === "")
|
|
145
|
+
return h.response(config).code(200)
|
|
137
146
|
for (const block of args.d.split(",")) {
|
|
138
|
-
const
|
|
147
|
+
const parts = block.split(":")
|
|
148
|
+
if (parts.length !== 3)
|
|
149
|
+
continue
|
|
150
|
+
const [ type, id, name ] = parts
|
|
139
151
|
config.push({ type, id, name })
|
|
140
152
|
}
|
|
141
153
|
return h.response(config).code(200)
|
|
@@ -215,7 +227,10 @@ export class APIServer {
|
|
|
215
227
|
for (const [ peer, info ] of this.wsPeers.entries()) {
|
|
216
228
|
this.cli.log("debug", `HAPI: remote peer ${peer}: sending ${data}`)
|
|
217
229
|
if (info.ws.readyState === WebSocket.OPEN)
|
|
218
|
-
info.ws.send(data)
|
|
230
|
+
info.ws.send(data, (err) => {
|
|
231
|
+
if (err)
|
|
232
|
+
this.cli.log("warning", `HAPI: peer ${peer}: send failed: ${err.message}`)
|
|
233
|
+
})
|
|
219
234
|
}
|
|
220
235
|
})
|
|
221
236
|
}
|
|
@@ -230,13 +245,18 @@ export class APIServer {
|
|
|
230
245
|
const port = Number.parseInt(m[2], 10)
|
|
231
246
|
this.sendOSC = (url: string, ...argList: any[]) => {
|
|
232
247
|
const msg = new OSC.Message(url, ...argList)
|
|
233
|
-
|
|
248
|
+
try {
|
|
249
|
+
osc.send(msg, { host, port })
|
|
250
|
+
}
|
|
251
|
+
catch (err: any) {
|
|
252
|
+
this.cli.log("warning", `OSC/UDP send to ${host}:${port} failed: ${err.message}`)
|
|
253
|
+
}
|
|
234
254
|
}
|
|
235
255
|
}
|
|
236
256
|
|
|
237
257
|
/* hook for send-dashboard method of nodes */
|
|
238
258
|
for (const node of graph.getGraphNodes()) {
|
|
239
|
-
node.on("send-dashboard", (info: {
|
|
259
|
+
node.on("send-dashboard", async (info: {
|
|
240
260
|
type: "audio" | "text",
|
|
241
261
|
id: string,
|
|
242
262
|
kind: "final" | "intermediate",
|
|
@@ -250,16 +270,26 @@ export class APIServer {
|
|
|
250
270
|
for (const [ peer, peerInfo ] of this.wsPeers.entries()) {
|
|
251
271
|
this.cli.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
|
|
252
272
|
if (peerInfo.ws.readyState === WebSocket.OPEN)
|
|
253
|
-
peerInfo.ws.send(data)
|
|
273
|
+
peerInfo.ws.send(data, (err) => {
|
|
274
|
+
if (err)
|
|
275
|
+
this.cli.log("warning", `HAPI: dashboard peer ${peer}: send failed: ${err.message}`)
|
|
276
|
+
})
|
|
254
277
|
}
|
|
278
|
+
const promises: Promise<void>[] = []
|
|
255
279
|
for (const n of graph.getGraphNodes()) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
const ac = new AbortController()
|
|
281
|
+
promises.push(
|
|
282
|
+
Promise.race([
|
|
283
|
+
n.receiveDashboard(info.type, info.id, info.kind, info.value),
|
|
284
|
+
util.timeout(10 * 1000, "timeout", ac.signal)
|
|
285
|
+
]).finally(() => {
|
|
286
|
+
ac.abort()
|
|
287
|
+
}).catch((err: Error) => {
|
|
288
|
+
this.cli.log("warning", `sending dashboard info to node <${n.id}> failed: ${err.message}`)
|
|
289
|
+
})
|
|
290
|
+
)
|
|
262
291
|
}
|
|
292
|
+
await Promise.allSettled(promises)
|
|
263
293
|
if (args.o !== "" && this.sendOSC)
|
|
264
294
|
this.sendOSC("/speechflow/dashboard", info.type, info.id, info.kind, info.value)
|
|
265
295
|
})
|
|
@@ -268,6 +298,15 @@ export class APIServer {
|
|
|
268
298
|
|
|
269
299
|
/* stop API server service */
|
|
270
300
|
async stop (args: CLIOptions): Promise<void> {
|
|
301
|
+
/* remove event listeners from graph nodes */
|
|
302
|
+
if (this.graph) {
|
|
303
|
+
for (const node of this.graph.getGraphNodes()) {
|
|
304
|
+
node.removeAllListeners("send-response")
|
|
305
|
+
node.removeAllListeners("send-dashboard")
|
|
306
|
+
}
|
|
307
|
+
this.graph = null
|
|
308
|
+
}
|
|
309
|
+
|
|
271
310
|
/* shutdown HAPI service */
|
|
272
311
|
if (this.hapi) {
|
|
273
312
|
this.cli.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
|
|
@@ -294,10 +333,13 @@ export class APIServer {
|
|
|
294
333
|
}
|
|
295
334
|
}))
|
|
296
335
|
}
|
|
336
|
+
const ac = new AbortController()
|
|
297
337
|
await Promise.race([
|
|
298
338
|
Promise.all(closePromises),
|
|
299
|
-
util.timeout(5 * 1000)
|
|
300
|
-
]).
|
|
339
|
+
util.timeout(5 * 1000, "timeout", ac.signal)
|
|
340
|
+
]).finally(() => {
|
|
341
|
+
ac.abort()
|
|
342
|
+
}).catch((error: unknown) => {
|
|
301
343
|
this.cli.log("warning", `HAPI: WebSockets failed to close: ${util.ensureError(error).message}`)
|
|
302
344
|
})
|
|
303
345
|
this.wsPeers.clear()
|
|
@@ -305,7 +347,7 @@ export class APIServer {
|
|
|
305
347
|
}
|
|
306
348
|
|
|
307
349
|
/* send information to dashboard on error */
|
|
308
|
-
|
|
350
|
+
sendErrorToDashboard (time: number, node: string, level: string, message: string) {
|
|
309
351
|
const data = JSON.stringify({
|
|
310
352
|
response: "HINT",
|
|
311
353
|
node,
|
|
@@ -314,7 +356,10 @@ export class APIServer {
|
|
|
314
356
|
for (const [ peer, peerInfo ] of this.wsPeers.entries()) {
|
|
315
357
|
this.cli.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
|
|
316
358
|
if (peerInfo.ws.readyState === WebSocket.OPEN)
|
|
317
|
-
peerInfo.ws.send(data)
|
|
359
|
+
peerInfo.ws.send(data, (err) => {
|
|
360
|
+
if (err)
|
|
361
|
+
this.cli.log("warning", `HAPI: dashboard peer ${peer}: send failed: ${err.message}`)
|
|
362
|
+
})
|
|
318
363
|
}
|
|
319
364
|
}
|
|
320
365
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -185,7 +185,7 @@ export class CLIContext {
|
|
|
185
185
|
if (this.args.V) {
|
|
186
186
|
process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
|
|
187
187
|
process.stderr.write(`${pkg.description}\n`)
|
|
188
|
-
process.stderr.write(`Copyright (c) 2024-
|
|
188
|
+
process.stderr.write(`Copyright (c) 2024-2026 ${pkg.author.name} <${pkg.author.url}>\n`)
|
|
189
189
|
process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
|
|
190
190
|
process.exit(0)
|
|
191
191
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -171,10 +171,13 @@ export class NodeGraph {
|
|
|
171
171
|
/* open node */
|
|
172
172
|
this.cli.log("info", `open node <${node.id}>`)
|
|
173
173
|
node.setTimeZero(this.timeZero)
|
|
174
|
+
const ac = new AbortController()
|
|
174
175
|
await Promise.race([
|
|
175
176
|
node.open(),
|
|
176
|
-
util.timeout(30 * 1000)
|
|
177
|
-
]).
|
|
177
|
+
util.timeout(30 * 1000, "timeout", ac.signal)
|
|
178
|
+
]).finally(() => {
|
|
179
|
+
ac.abort()
|
|
180
|
+
}).catch((err: Error) => {
|
|
178
181
|
this.cli.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
|
|
179
182
|
throw new Error(`failed to open node <${node.id}>: ${err.message}`)
|
|
180
183
|
})
|
|
@@ -211,8 +214,9 @@ export class NodeGraph {
|
|
|
211
214
|
this.cli.log("info", `observe stream of node <${node.id}> for finish event`)
|
|
212
215
|
this.activeNodes.add(node)
|
|
213
216
|
const deactivateNode = (node: SpeechFlowNode, msg: string) => {
|
|
214
|
-
if (this.activeNodes.has(node))
|
|
215
|
-
|
|
217
|
+
if (!this.activeNodes.has(node))
|
|
218
|
+
return
|
|
219
|
+
this.activeNodes.delete(node)
|
|
216
220
|
this.cli.log("info", `${msg} (${this.activeNodes.size} active nodes remaining)`)
|
|
217
221
|
if (this.activeNodes.size === 0) {
|
|
218
222
|
const timeFinished = DateTime.now()
|
|
@@ -220,7 +224,9 @@ export class NodeGraph {
|
|
|
220
224
|
this.cli.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " +
|
|
221
225
|
`(total duration: ${duration?.toFormat("hh:mm:ss.SSS") ?? "unknown"}) ****`)
|
|
222
226
|
this.finishEvents.emit("finished")
|
|
223
|
-
this.shutdown("finished", args, api)
|
|
227
|
+
this.shutdown("finished", args, api).catch((err: Error) => {
|
|
228
|
+
this.cli.log("error", `failed to shutdown: ${err.message}`)
|
|
229
|
+
})
|
|
224
230
|
}
|
|
225
231
|
}
|
|
226
232
|
node.stream.on("error", (err: unknown) => {
|
|
@@ -228,12 +234,26 @@ export class NodeGraph {
|
|
|
228
234
|
this.cli.log("warning", `stream of node <${node.id}> raised "error" event: ${error.message}`)
|
|
229
235
|
api.sendErrorToDashboard(Date.now(), node.id, "warning", error.message)
|
|
230
236
|
})
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
|
|
238
|
+
/* listen for the semantically correct completion event per stream type:
|
|
239
|
+
- for Duplex/Transform listen only for "end"
|
|
240
|
+
(readable-side, fires last, guarantees all output is drained),
|
|
241
|
+
- for pure Readable listen only for "end"
|
|
242
|
+
- for pure Writable listen only for "finish" */
|
|
243
|
+
if (node.stream instanceof Stream.Duplex)
|
|
244
|
+
node.stream.on("end", () => {
|
|
245
|
+
deactivateNode(node, `readable stream side (output) of node <${node.id}> raised "end" event`)
|
|
246
|
+
})
|
|
247
|
+
else if (node.stream instanceof Stream.Readable)
|
|
248
|
+
node.stream.on("end", () => {
|
|
249
|
+
deactivateNode(node, `readable stream side (output) of node <${node.id}> raised "end" event`)
|
|
250
|
+
})
|
|
251
|
+
else if (node.stream instanceof Stream.Writable)
|
|
252
|
+
node.stream.on("finish", () => {
|
|
253
|
+
deactivateNode(node, `writable stream side (input) of node <${node.id}> raised "finish" event`)
|
|
254
|
+
})
|
|
255
|
+
else
|
|
256
|
+
throw new Error(`stream of node <${node.id}> is neither of Duplex, Writable, nor Readable type`)
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
/* start of internal stream processing */
|
|
@@ -250,13 +270,16 @@ export class NodeGraph {
|
|
|
250
270
|
const stream = node.stream
|
|
251
271
|
if ((stream instanceof Stream.Writable || stream instanceof Stream.Duplex)
|
|
252
272
|
&& (!stream.writableEnded && !stream.destroyed)) {
|
|
273
|
+
const ac = new AbortController()
|
|
253
274
|
drainPromises.push(
|
|
254
275
|
Promise.race([
|
|
255
276
|
new Promise<void>((resolve) => {
|
|
256
277
|
stream.end(() => { resolve() })
|
|
257
278
|
}),
|
|
258
|
-
util.timeout(5000)
|
|
259
|
-
]).
|
|
279
|
+
util.timeout(5000, "timeout", ac.signal)
|
|
280
|
+
]).finally(() => {
|
|
281
|
+
ac.abort()
|
|
282
|
+
}).catch(() => {
|
|
260
283
|
/* ignore timeout -- stream will be destroyed later */
|
|
261
284
|
})
|
|
262
285
|
)
|
|
@@ -297,10 +320,13 @@ export class NodeGraph {
|
|
|
297
320
|
async closeNodes (): Promise<void> {
|
|
298
321
|
for (const node of this.graphNodes) {
|
|
299
322
|
this.cli.log("info", `close node <${node.id}>`)
|
|
323
|
+
const ac = new AbortController()
|
|
300
324
|
await Promise.race([
|
|
301
325
|
node.close(),
|
|
302
|
-
util.timeout(10 * 1000)
|
|
303
|
-
]).
|
|
326
|
+
util.timeout(10 * 1000, "timeout", ac.signal)
|
|
327
|
+
]).finally(() => {
|
|
328
|
+
ac.abort()
|
|
329
|
+
}).catch((err: Error) => {
|
|
304
330
|
this.cli.log("warning", `node <${node.id}> failed to close: ${err.message}`)
|
|
305
331
|
})
|
|
306
332
|
}
|
|
@@ -319,25 +345,33 @@ export class NodeGraph {
|
|
|
319
345
|
|
|
320
346
|
/* graph destruction: PASS 5: destroy nodes */
|
|
321
347
|
destroyNodes (): void {
|
|
322
|
-
for (const node of this.graphNodes)
|
|
348
|
+
for (const node of this.graphNodes)
|
|
323
349
|
this.cli.log("info", `destroy node <${node.id}>`)
|
|
324
|
-
|
|
325
|
-
}
|
|
350
|
+
this.graphNodes.clear()
|
|
326
351
|
}
|
|
327
352
|
|
|
328
353
|
/* setup signal handling for shutdown */
|
|
329
354
|
setupSignalHandlers (args: CLIOptions, api: APIServer): void {
|
|
330
355
|
/* internal helper functions */
|
|
331
|
-
const shutdownHandler = (signal: string) =>
|
|
332
|
-
this.shutdown(signal, args, api)
|
|
333
356
|
const logError = (error: Error) => {
|
|
334
357
|
if (this.debug)
|
|
335
358
|
this.cli.log("error", `${error.message}\n${error.stack}`)
|
|
336
359
|
else
|
|
337
360
|
this.cli.log("error", error.message)
|
|
338
361
|
}
|
|
362
|
+
const shutdownHandler = (signal: string) => {
|
|
363
|
+
this.shutdown(signal, args, api).catch((err: unknown) => {
|
|
364
|
+
const error = util.ensureError(err, "shutdown error")
|
|
365
|
+
logError(error)
|
|
366
|
+
process.exit(1)
|
|
367
|
+
})
|
|
368
|
+
}
|
|
339
369
|
|
|
340
|
-
/* hook into process signals */
|
|
370
|
+
/* re-hook into process signals */
|
|
371
|
+
process.removeAllListeners("SIGINT")
|
|
372
|
+
process.removeAllListeners("SIGUSR1")
|
|
373
|
+
process.removeAllListeners("SIGUSR2")
|
|
374
|
+
process.removeAllListeners("SIGTERM")
|
|
341
375
|
process.on("SIGINT", () => { shutdownHandler("SIGINT") })
|
|
342
376
|
process.on("SIGUSR1", () => { shutdownHandler("SIGUSR1") })
|
|
343
377
|
process.on("SIGUSR2", () => { shutdownHandler("SIGUSR2") })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -47,10 +47,13 @@ export class NodeStatusManager {
|
|
|
47
47
|
this.cli.log("info", `gathering status of node <${name}>`)
|
|
48
48
|
const node = new nodes[name](name, cfg, {}, [])
|
|
49
49
|
node._accessBus = accessBus
|
|
50
|
+
const ac = new AbortController()
|
|
50
51
|
const status = await Promise.race<{ [ key: string ]: string | number }>([
|
|
51
52
|
node.status(),
|
|
52
|
-
util.timeout(10 * 1000)
|
|
53
|
-
]).
|
|
53
|
+
util.timeout(10 * 1000, "timeout", ac.signal)
|
|
54
|
+
]).finally(() => {
|
|
55
|
+
ac.abort()
|
|
56
|
+
}).catch((err: Error) => {
|
|
54
57
|
this.cli.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
|
|
55
58
|
return {} as { [ key: string ]: string | number }
|
|
56
59
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -21,8 +21,7 @@ class CompressorProcessor extends AudioWorkletProcessor {
|
|
|
21
21
|
{ name: "ratio", defaultValue: 4.0, minValue: 1.0, maxValue: 20, automationRate: "k-rate" }, // compression ratio
|
|
22
22
|
{ name: "attack", defaultValue: 0.010, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
|
|
23
23
|
{ name: "release", defaultValue: 0.050, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
|
|
24
|
-
{ name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" }
|
|
25
|
-
{ name: "makeup", defaultValue: 0.0, minValue: -24, maxValue: 24, automationRate: "k-rate" } // dB
|
|
24
|
+
{ name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" } // dB
|
|
26
25
|
]
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -39,20 +38,19 @@ class CompressorProcessor extends AudioWorkletProcessor {
|
|
|
39
38
|
if (ratio <= 1.0)
|
|
40
39
|
return 0
|
|
41
40
|
|
|
42
|
-
/* determine
|
|
41
|
+
/* determine knee boundaries (symmetric around threshold) */
|
|
43
42
|
const halfKnee = kneeDB * 0.5
|
|
44
|
-
const
|
|
45
|
-
const aboveKnee = levelDB
|
|
43
|
+
const belowKnee = levelDB < (thresholdDB - halfKnee)
|
|
44
|
+
const aboveKnee = levelDB > (thresholdDB + halfKnee)
|
|
46
45
|
|
|
47
|
-
/* short-circuit for no compression (below
|
|
48
|
-
if (
|
|
46
|
+
/* short-circuit for no compression (below knee) */
|
|
47
|
+
if (belowKnee)
|
|
49
48
|
return 0
|
|
50
49
|
|
|
51
|
-
/* apply soft-knee */
|
|
50
|
+
/* apply soft-knee (standard textbook quadratic) */
|
|
52
51
|
if (kneeDB > 0 && !aboveKnee) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return idealGainDB * x * x
|
|
52
|
+
const d = levelDB - thresholdDB + halfKnee
|
|
53
|
+
return (1.0 / ratio - 1.0) * d * d / (2.0 * kneeDB)
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/* determine target level */
|
|
@@ -94,21 +92,18 @@ class CompressorProcessor extends AudioWorkletProcessor {
|
|
|
94
92
|
const kneeDB = parameters["knee"][0]
|
|
95
93
|
const attackS = Math.max(parameters["attack"][0], 1 / this.sampleRate)
|
|
96
94
|
const releaseS = Math.max(parameters["release"][0], 1 / this.sampleRate)
|
|
97
|
-
const makeupDB = parameters["makeup"][0]
|
|
98
95
|
|
|
99
|
-
/* update envelope per channel */
|
|
96
|
+
/* update envelope per channel and collect RMS values */
|
|
97
|
+
const rms = Array.from<number>({ length: nCh })
|
|
100
98
|
for (let ch = 0; ch < nCh; ch++)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/* determine linear value from decibel makeup value */
|
|
104
|
-
const makeUpLin = util.dB2lin(makeupDB)
|
|
99
|
+
rms[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
|
|
105
100
|
|
|
106
101
|
/* iterate over all channels */
|
|
107
102
|
this.reduction = 0
|
|
108
103
|
for (let ch = 0; ch < nCh; ch++) {
|
|
109
|
-
const levelDB = util.lin2dB(
|
|
104
|
+
const levelDB = util.lin2dB(rms[ch])
|
|
110
105
|
const gainDB = this.gainDBFor(levelDB, thresholdDB, ratio, kneeDB)
|
|
111
|
-
const gainLin = util.dB2lin(gainDB)
|
|
106
|
+
const gainLin = util.dB2lin(gainDB)
|
|
112
107
|
|
|
113
108
|
/* on first channel, calculate reduction */
|
|
114
109
|
if (ch === 0)
|
|
@@ -120,6 +115,10 @@ class CompressorProcessor extends AudioWorkletProcessor {
|
|
|
120
115
|
for (let i = 0; i < inp.length; i++)
|
|
121
116
|
out[i] = inp[i] * gainLin
|
|
122
117
|
}
|
|
118
|
+
|
|
119
|
+
/* report reduction to main thread */
|
|
120
|
+
this.port.postMessage({ type: "reduction", reduction: this.reduction })
|
|
121
|
+
|
|
123
122
|
return true
|
|
124
123
|
}
|
|
125
124
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -29,11 +29,13 @@ interface AudioCompressorConfig {
|
|
|
29
29
|
/* audio compressor class */
|
|
30
30
|
class AudioCompressor extends util.WebAudio {
|
|
31
31
|
/* internal state */
|
|
32
|
-
private type:
|
|
33
|
-
private mode:
|
|
34
|
-
private config:
|
|
35
|
-
private compressorNode:
|
|
36
|
-
private gainNode:
|
|
32
|
+
private type: "standalone" | "sidechain"
|
|
33
|
+
private mode: "compress" | "measure" | "adjust"
|
|
34
|
+
private config: Required<AudioCompressorConfig>
|
|
35
|
+
private compressorNode: AudioWorkletNode | null = null
|
|
36
|
+
private gainNode: GainNode | null = null
|
|
37
|
+
private _reduction = 0
|
|
38
|
+
private _reductionListener: ((event: MessageEvent) => void) | null = null
|
|
37
39
|
|
|
38
40
|
/* construct object */
|
|
39
41
|
constructor (
|
|
@@ -85,6 +87,16 @@ class AudioCompressor extends util.WebAudio {
|
|
|
85
87
|
})
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
/* listen for reduction updates from compressor worklet */
|
|
91
|
+
if (needsCompressor) {
|
|
92
|
+
this._reductionListener = (event: MessageEvent) => {
|
|
93
|
+
if (event.data?.type === "reduction")
|
|
94
|
+
this._reduction = event.data.reduction
|
|
95
|
+
}
|
|
96
|
+
this.compressorNode!.port.addEventListener("message", this._reductionListener)
|
|
97
|
+
this.compressorNode!.port.start()
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
/* create gain node */
|
|
89
101
|
if (needsGain)
|
|
90
102
|
this.gainNode = this.audioContext.createGain()
|
|
@@ -97,6 +109,7 @@ class AudioCompressor extends util.WebAudio {
|
|
|
97
109
|
}
|
|
98
110
|
else if (this.type === "sidechain" && this.mode === "measure") {
|
|
99
111
|
this.sourceNode!.connect(this.compressorNode!)
|
|
112
|
+
this.compressorNode!.connect(this.captureNode!)
|
|
100
113
|
}
|
|
101
114
|
else if (this.type === "sidechain" && this.mode === "adjust") {
|
|
102
115
|
this.sourceNode!.connect(this.gainNode!)
|
|
@@ -112,7 +125,6 @@ class AudioCompressor extends util.WebAudio {
|
|
|
112
125
|
params.get("attack")!.setValueAtTime(this.config.attackMs / 1000, currentTime)
|
|
113
126
|
params.get("release")!.setValueAtTime(this.config.releaseMs / 1000, currentTime)
|
|
114
127
|
params.get("knee")!.setValueAtTime(this.config.kneeDb, currentTime)
|
|
115
|
-
params.get("makeup")!.setValueAtTime(this.config.makeupDb, currentTime)
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
/* configure gain node */
|
|
@@ -124,8 +136,7 @@ class AudioCompressor extends util.WebAudio {
|
|
|
124
136
|
|
|
125
137
|
/* get the current gain reduction */
|
|
126
138
|
public getGainReduction (): number {
|
|
127
|
-
|
|
128
|
-
return processor?.reduction ?? 0
|
|
139
|
+
return this._reduction
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/* set the current gain */
|
|
@@ -138,6 +149,10 @@ class AudioCompressor extends util.WebAudio {
|
|
|
138
149
|
public async destroy (): Promise<void> {
|
|
139
150
|
/* destroy nodes */
|
|
140
151
|
if (this.compressorNode !== null) {
|
|
152
|
+
if (this._reductionListener !== null) {
|
|
153
|
+
this.compressorNode.port.removeEventListener("message", this._reductionListener)
|
|
154
|
+
this._reductionListener = null
|
|
155
|
+
}
|
|
141
156
|
this.compressorNode.disconnect()
|
|
142
157
|
this.compressorNode = null
|
|
143
158
|
}
|
|
@@ -244,7 +259,7 @@ export default class SpeechFlowNodeA2ACompressor extends SpeechFlowNode {
|
|
|
244
259
|
callback(new Error("compressor not initialized"))
|
|
245
260
|
else {
|
|
246
261
|
/* compress chunk */
|
|
247
|
-
const payload = util.convertBufToI16(chunk.payload)
|
|
262
|
+
const payload = util.convertBufToI16(chunk.payload, self.config.audioLittleEndian)
|
|
248
263
|
self.compressor.process(payload).then((result) => {
|
|
249
264
|
if (self.closing) {
|
|
250
265
|
callback(new Error("stream already destroyed"))
|
|
@@ -253,10 +268,13 @@ export default class SpeechFlowNodeA2ACompressor extends SpeechFlowNode {
|
|
|
253
268
|
if ((self.params.type === "standalone" && self.params.mode === "compress")
|
|
254
269
|
|| (self.params.type === "sidechain" && self.params.mode === "adjust")) {
|
|
255
270
|
/* take over compressed data */
|
|
256
|
-
const payload = util.convertI16ToBuf(result)
|
|
257
|
-
|
|
271
|
+
const payload = util.convertI16ToBuf(result, self.config.audioLittleEndian)
|
|
272
|
+
const chunkNew = chunk.clone()
|
|
273
|
+
chunkNew.payload = payload
|
|
274
|
+
this.push(chunkNew)
|
|
258
275
|
}
|
|
259
|
-
|
|
276
|
+
else
|
|
277
|
+
this.push(chunk)
|
|
260
278
|
callback()
|
|
261
279
|
}).catch((error: unknown) => {
|
|
262
280
|
if (self.closing)
|