speechflow 1.4.5 → 1.5.0
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/CHANGELOG.md +28 -0
- package/README.md +220 -7
- package/etc/claude.md +70 -0
- package/etc/speechflow.yaml +5 -3
- package/etc/stx.conf +7 -0
- package/package.json +7 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +13 -3
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +317 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +15 -13
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +171 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +11 -6
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +141 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +13 -15
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
- package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +44 -31
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +44 -45
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +10 -12
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +22 -27
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
- package/speechflow-cli/dst/speechflow-node.js +13 -2
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-utils.js +123 -35
- package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
- package/speechflow-cli/dst/speechflow.js +69 -14
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +112 -11
- package/speechflow-cli/etc/stx.conf +2 -2
- package/speechflow-cli/etc/tsconfig.json +1 -1
- package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
- package/speechflow-cli/package.json +102 -94
- package/speechflow-cli/src/lib.d.ts +24 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +13 -3
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
- package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +308 -0
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +15 -13
- package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
- package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +12 -7
- package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +152 -0
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +13 -15
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +55 -42
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +58 -58
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +15 -16
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +27 -32
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
- package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
- package/speechflow-cli/src/speechflow-node.ts +21 -8
- package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
- package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
- package/speechflow-cli/src/speechflow-utils.ts +125 -32
- package/speechflow-cli/src/speechflow.ts +74 -17
- package/speechflow-ui-db/dst/index.js +31 -31
- package/speechflow-ui-db/etc/eslint.mjs +0 -1
- package/speechflow-ui-db/etc/tsc-client.json +3 -3
- package/speechflow-ui-db/package.json +11 -10
- package/speechflow-ui-db/src/app.vue +20 -6
- package/speechflow-ui-st/dst/index.js +26 -26
- package/speechflow-ui-st/etc/eslint.mjs +0 -1
- package/speechflow-ui-st/etc/tsc-client.json +3 -3
- package/speechflow-ui-st/package.json +11 -10
- package/speechflow-ui-st/src/app.vue +5 -12
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import Stream from "node:stream"
|
|
10
|
+
import { EventEmitter } from "node:events"
|
|
11
|
+
|
|
12
|
+
/* external dependencies */
|
|
13
|
+
import { GainNode, AudioWorkletNode } from "node-web-audio-api"
|
|
14
|
+
|
|
15
|
+
/* internal dependencies */
|
|
16
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
17
|
+
import * as utils from "./speechflow-utils"
|
|
18
|
+
import { WebAudio } from "./speechflow-utils-audio"
|
|
19
|
+
|
|
20
|
+
/* internal types */
|
|
21
|
+
interface AudioCompressorConfig {
|
|
22
|
+
thresholdDb?: number
|
|
23
|
+
ratio?: number
|
|
24
|
+
attackMs?: number
|
|
25
|
+
releaseMs?: number
|
|
26
|
+
kneeDb?: number
|
|
27
|
+
makeupDb?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* audio compressor class */
|
|
31
|
+
class AudioCompressor extends WebAudio {
|
|
32
|
+
/* internal state */
|
|
33
|
+
private type: "standalone" | "sidechain"
|
|
34
|
+
private mode: "compress" | "measure" | "adjust"
|
|
35
|
+
private config: Required<AudioCompressorConfig>
|
|
36
|
+
private compressorNode: AudioWorkletNode | null = null
|
|
37
|
+
private gainNode: GainNode | null = null
|
|
38
|
+
|
|
39
|
+
/* construct object */
|
|
40
|
+
constructor(
|
|
41
|
+
sampleRate: number,
|
|
42
|
+
channels: number,
|
|
43
|
+
type: "standalone" | "sidechain" = "standalone",
|
|
44
|
+
mode: "compress" | "measure" | "adjust" = "compress",
|
|
45
|
+
config: AudioCompressorConfig = {}
|
|
46
|
+
) {
|
|
47
|
+
super(sampleRate, channels)
|
|
48
|
+
|
|
49
|
+
/* store type and mode */
|
|
50
|
+
this.type = type
|
|
51
|
+
this.mode = mode
|
|
52
|
+
|
|
53
|
+
/* store configuration */
|
|
54
|
+
this.config = {
|
|
55
|
+
thresholdDb: config.thresholdDb ?? -23,
|
|
56
|
+
ratio: config.ratio ?? 4.0,
|
|
57
|
+
attackMs: config.attackMs ?? 10,
|
|
58
|
+
releaseMs: config.releaseMs ?? 50,
|
|
59
|
+
kneeDb: config.kneeDb ?? 6.0,
|
|
60
|
+
makeupDb: config.makeupDb ?? 0
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* setup object */
|
|
65
|
+
public async setup (): Promise<void> {
|
|
66
|
+
await super.setup()
|
|
67
|
+
|
|
68
|
+
/* add audio worklet module */
|
|
69
|
+
const url = path.resolve(__dirname, "speechflow-node-a2a-compressor-wt.js")
|
|
70
|
+
await this.audioContext.audioWorklet.addModule(url)
|
|
71
|
+
|
|
72
|
+
/* determine operation modes */
|
|
73
|
+
const needsCompressor = (this.type === "standalone" && this.mode === "compress") ||
|
|
74
|
+
(this.type === "sidechain" && this.mode === "measure")
|
|
75
|
+
const needsGain = (this.type === "standalone" && this.mode === "compress") ||
|
|
76
|
+
(this.type === "sidechain" && this.mode === "adjust")
|
|
77
|
+
|
|
78
|
+
/* create compressor worklet node */
|
|
79
|
+
if (needsCompressor) {
|
|
80
|
+
this.compressorNode = new AudioWorkletNode(this.audioContext, "compressor", {
|
|
81
|
+
numberOfInputs: 1,
|
|
82
|
+
numberOfOutputs: 1,
|
|
83
|
+
processorOptions: {
|
|
84
|
+
sampleRate: this.audioContext.sampleRate
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* create gain node */
|
|
90
|
+
if (needsGain)
|
|
91
|
+
this.gainNode = this.audioContext.createGain()
|
|
92
|
+
|
|
93
|
+
/* connect nodes (according to type and mode) */
|
|
94
|
+
if (this.type === "standalone" && this.mode === "compress") {
|
|
95
|
+
this.sourceNode!.connect(this.compressorNode!)
|
|
96
|
+
this.compressorNode!.connect(this.gainNode!)
|
|
97
|
+
this.gainNode!.connect(this.captureNode!)
|
|
98
|
+
}
|
|
99
|
+
else if (this.type === "sidechain" && this.mode === "measure") {
|
|
100
|
+
this.sourceNode!.connect(this.compressorNode!)
|
|
101
|
+
}
|
|
102
|
+
else if (this.type === "sidechain" && this.mode === "adjust") {
|
|
103
|
+
this.sourceNode!.connect(this.gainNode!)
|
|
104
|
+
this.gainNode!.connect(this.captureNode!)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* configure compressor worklet node */
|
|
108
|
+
const currentTime = this.audioContext.currentTime
|
|
109
|
+
if (needsCompressor) {
|
|
110
|
+
const node = this.compressorNode!
|
|
111
|
+
const params = node.parameters as Map<string, AudioParam>
|
|
112
|
+
params.get("threshold")!.setValueAtTime(this.config.thresholdDb, currentTime)
|
|
113
|
+
params.get("ratio")!.setValueAtTime(this.config.ratio, currentTime)
|
|
114
|
+
params.get("attack")!.setValueAtTime(this.config.attackMs / 1000, currentTime)
|
|
115
|
+
params.get("release")!.setValueAtTime(this.config.releaseMs / 1000, currentTime)
|
|
116
|
+
params.get("knee")!.setValueAtTime(this.config.kneeDb, currentTime)
|
|
117
|
+
params.get("makeup")!.setValueAtTime(this.config.makeupDb, currentTime)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* configure gain node */
|
|
121
|
+
if (needsGain) {
|
|
122
|
+
const gain = Math.pow(10, this.config.makeupDb / 20)
|
|
123
|
+
this.gainNode!.gain.setValueAtTime(gain, currentTime)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* get the current gain reduction */
|
|
128
|
+
public getGainReduction (): number {
|
|
129
|
+
const processor = (this.compressorNode as any)?.port?.processor
|
|
130
|
+
return processor?.reduction ?? 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* set the current gain */
|
|
134
|
+
public setGain (decibel: number): void {
|
|
135
|
+
const gain = Math.pow(10, decibel / 20)
|
|
136
|
+
this.gainNode?.gain.setTargetAtTime(gain, this.audioContext.currentTime, 0.002)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* destroy the compressor */
|
|
140
|
+
public async destroy (): Promise<void> {
|
|
141
|
+
await super.destroy()
|
|
142
|
+
|
|
143
|
+
/* destroy nodes */
|
|
144
|
+
if (this.compressorNode !== null) {
|
|
145
|
+
this.compressorNode.disconnect()
|
|
146
|
+
this.compressorNode = null
|
|
147
|
+
}
|
|
148
|
+
if (this.gainNode !== null) {
|
|
149
|
+
this.gainNode.disconnect()
|
|
150
|
+
this.gainNode = null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* SpeechFlow node for compression in audio-to-audio passing */
|
|
156
|
+
export default class SpeechFlowNodeCompressor extends SpeechFlowNode {
|
|
157
|
+
/* declare official node name */
|
|
158
|
+
public static name = "compressor"
|
|
159
|
+
|
|
160
|
+
/* internal state */
|
|
161
|
+
private destroyed = false
|
|
162
|
+
private compressor: AudioCompressor | null = null
|
|
163
|
+
private bus: EventEmitter | null = null
|
|
164
|
+
private intervalId: ReturnType<typeof setInterval> | null = null
|
|
165
|
+
|
|
166
|
+
/* construct node */
|
|
167
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
168
|
+
super(id, cfg, opts, args)
|
|
169
|
+
|
|
170
|
+
/* declare node configuration parameters */
|
|
171
|
+
this.configure({
|
|
172
|
+
type: { type: "string", val: "standalone", match: /^(?:standalone|sidechain)$/ },
|
|
173
|
+
mode: { type: "string", val: "compress", match: /^(?:compress|measure|adjust)$/ },
|
|
174
|
+
bus: { type: "string", val: "compressor", match: /^.+$/ },
|
|
175
|
+
thresholdDb: { type: "number", val: -23, match: (n: number) => n <= 0 && n >= -100 },
|
|
176
|
+
ratio: { type: "number", val: 4.0, match: (n: number) => n >= 1 && n <= 20 },
|
|
177
|
+
attackMs: { type: "number", val: 10, match: (n: number) => n >= 0 && n <= 1000 },
|
|
178
|
+
releaseMs: { type: "number", val: 50, match: (n: number) => n >= 0 && n <= 1000 },
|
|
179
|
+
kneeDb: { type: "number", val: 6.0, match: (n: number) => n >= 0 && n <= 40 },
|
|
180
|
+
makeupDb: { type: "number", val: 0, match: (n: number) => n >= -24 && n <= 24 }
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
/* sanity check mode and role */
|
|
184
|
+
if (this.params.type === "standalone" && this.params.mode !== "compress")
|
|
185
|
+
throw new Error("type \"standalone\" implies mode \"compress\"")
|
|
186
|
+
if (this.params.type === "sidechain" && this.params.mode === "compress")
|
|
187
|
+
throw new Error("type \"sidechain\" implies mode \"measure\" or \"adjust\"")
|
|
188
|
+
|
|
189
|
+
/* declare node input/output format */
|
|
190
|
+
this.input = "audio"
|
|
191
|
+
this.output = "audio"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* open node */
|
|
195
|
+
async open () {
|
|
196
|
+
/* clear destruction flag */
|
|
197
|
+
this.destroyed = false
|
|
198
|
+
|
|
199
|
+
/* setup compressor */
|
|
200
|
+
this.compressor = new AudioCompressor(
|
|
201
|
+
this.config.audioSampleRate,
|
|
202
|
+
this.config.audioChannels,
|
|
203
|
+
this.params.type,
|
|
204
|
+
this.params.mode, {
|
|
205
|
+
thresholdDb: this.params.thresholdDb,
|
|
206
|
+
ratio: this.params.ratio,
|
|
207
|
+
attackMs: this.params.attackMs,
|
|
208
|
+
releaseMs: this.params.releaseMs,
|
|
209
|
+
kneeDb: this.params.kneeDb,
|
|
210
|
+
makeupDb: this.params.makeupDb
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
await this.compressor.setup()
|
|
214
|
+
|
|
215
|
+
/* optionally establish sidechain processing */
|
|
216
|
+
if (this.params.type === "sidechain") {
|
|
217
|
+
this.bus = this.accessBus(this.params.bus)
|
|
218
|
+
if (this.params.mode === "measure") {
|
|
219
|
+
this.intervalId = setInterval(() => {
|
|
220
|
+
const decibel = this.compressor?.getGainReduction()
|
|
221
|
+
this.bus?.emit("sidechain-decibel", decibel)
|
|
222
|
+
}, 10)
|
|
223
|
+
}
|
|
224
|
+
else if (this.params.mode === "adjust") {
|
|
225
|
+
this.bus.on("sidechain-decibel", (decibel: number) => {
|
|
226
|
+
this.compressor?.setGain(decibel)
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* establish a transform stream */
|
|
232
|
+
const self = this
|
|
233
|
+
this.stream = new Stream.Transform({
|
|
234
|
+
readableObjectMode: true,
|
|
235
|
+
writableObjectMode: true,
|
|
236
|
+
decodeStrings: false,
|
|
237
|
+
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
|
|
238
|
+
if (self.destroyed) {
|
|
239
|
+
callback(new Error("stream already destroyed"))
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (!Buffer.isBuffer(chunk.payload))
|
|
243
|
+
callback(new Error("invalid chunk payload type"))
|
|
244
|
+
else {
|
|
245
|
+
/* compress chunk */
|
|
246
|
+
const payload = utils.convertBufToI16(chunk.payload)
|
|
247
|
+
self.compressor?.process(payload).then((result) => {
|
|
248
|
+
if (self.destroyed)
|
|
249
|
+
throw new Error("stream already destroyed")
|
|
250
|
+
if ((self.params.type === "standalone" && self.params.mode === "compress") ||
|
|
251
|
+
(self.params.type === "sidechain" && self.params.mode === "adjust") ) {
|
|
252
|
+
/* take over compressed data */
|
|
253
|
+
const payload = utils.convertI16ToBuf(result)
|
|
254
|
+
chunk.payload = payload
|
|
255
|
+
}
|
|
256
|
+
this.push(chunk)
|
|
257
|
+
callback()
|
|
258
|
+
}).catch((error) => {
|
|
259
|
+
callback(new Error(`compression failed: ${error}`))
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
final (callback) {
|
|
264
|
+
if (self.destroyed) {
|
|
265
|
+
callback()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
this.push(null)
|
|
269
|
+
callback()
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* close node */
|
|
275
|
+
async close () {
|
|
276
|
+
/* indicate destruction */
|
|
277
|
+
this.destroyed = true
|
|
278
|
+
|
|
279
|
+
/* clear interval */
|
|
280
|
+
if (this.intervalId !== null) {
|
|
281
|
+
clearInterval(this.intervalId)
|
|
282
|
+
this.intervalId = null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* destroy bus */
|
|
286
|
+
if (this.bus !== null) {
|
|
287
|
+
this.bus.removeAllListeners()
|
|
288
|
+
this.bus = null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* destroy compressor */
|
|
292
|
+
if (this.compressor !== null) {
|
|
293
|
+
await this.compressor.destroy()
|
|
294
|
+
this.compressor = null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* close stream */
|
|
298
|
+
if (this.stream !== null) {
|
|
299
|
+
this.stream.destroy()
|
|
300
|
+
this.stream = null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as utils from "./speechflow-utils"
|
|
8
|
+
|
|
9
|
+
/* downward expander with soft knee */
|
|
10
|
+
class ExpanderProcessor extends AudioWorkletProcessor {
|
|
11
|
+
/* internal state */
|
|
12
|
+
private env: number[] = []
|
|
13
|
+
private sampleRate: number
|
|
14
|
+
|
|
15
|
+
/* eslint no-undef: off */
|
|
16
|
+
static get parameterDescriptors(): AudioParamDescriptor[] {
|
|
17
|
+
return [
|
|
18
|
+
{ name: "threshold", defaultValue: -45, minValue: -100, maxValue: 0, automationRate: "k-rate" }, // dBFS
|
|
19
|
+
{ name: "floor", defaultValue: -64, minValue: -100, maxValue: 0, automationRate: "k-rate" }, // dBFS minimum output level
|
|
20
|
+
{ name: "ratio", defaultValue: 4.0, minValue: 1.0, maxValue: 20, automationRate: "k-rate" }, // expansion ratio
|
|
21
|
+
{ name: "attack", defaultValue: 0.010, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
|
|
22
|
+
{ name: "release", defaultValue: 0.050, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
|
|
23
|
+
{ name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" }, // dB
|
|
24
|
+
{ name: "makeup", defaultValue: 0.0, minValue: -24, maxValue: 24, automationRate: "k-rate" } // dB
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* class constructor for custom option processing */
|
|
29
|
+
constructor (options: any) {
|
|
30
|
+
super()
|
|
31
|
+
const { sampleRate } = options.processorOptions
|
|
32
|
+
this.sampleRate = sampleRate as number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* determine gain difference */
|
|
36
|
+
private gainDBFor (levelDB: number, thresholdDB: number, ratio: number, kneeDB: number): number {
|
|
37
|
+
/* short-circuit for unreasonable ratio */
|
|
38
|
+
if (ratio <= 1.0)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
/* determine thresholds */
|
|
42
|
+
const halfKnee = kneeDB * 0.5
|
|
43
|
+
const belowKnee = levelDB < (thresholdDB - halfKnee)
|
|
44
|
+
const aboveThr = levelDB >= thresholdDB
|
|
45
|
+
|
|
46
|
+
/* short-circuit for no expansion (above threshold) */
|
|
47
|
+
if (aboveThr)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
/* apply soft-knee */
|
|
51
|
+
if (kneeDB > 0 && !belowKnee) {
|
|
52
|
+
const x = (levelDB - (thresholdDB - halfKnee)) / kneeDB
|
|
53
|
+
const idealGainDB = (thresholdDB + (levelDB - thresholdDB) * ratio) - levelDB
|
|
54
|
+
return idealGainDB * x * x
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* determine target level */
|
|
58
|
+
const targetOut = thresholdDB + (levelDB - thresholdDB) / ratio
|
|
59
|
+
|
|
60
|
+
/* return gain difference */
|
|
61
|
+
return targetOut - levelDB
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* update envelope (smoothed amplitude contour) for single channel */
|
|
65
|
+
private updateEnvelopeForChannel (
|
|
66
|
+
chan: number,
|
|
67
|
+
samples: Float32Array,
|
|
68
|
+
attack: number,
|
|
69
|
+
release: number
|
|
70
|
+
): void {
|
|
71
|
+
/* fetch old envelope value */
|
|
72
|
+
if (this.env[chan] === undefined)
|
|
73
|
+
this.env[chan] = 1e-12
|
|
74
|
+
let env = this.env[chan]
|
|
75
|
+
|
|
76
|
+
/* calculate attack/release alpha values */
|
|
77
|
+
const alphaA = Math.exp(-1 / (attack * this.sampleRate))
|
|
78
|
+
const alphaR = Math.exp(-1 / (release * this.sampleRate))
|
|
79
|
+
|
|
80
|
+
/* iterate over all samples and calculate RMS */
|
|
81
|
+
for (const s of samples) {
|
|
82
|
+
const x = Math.abs(s)
|
|
83
|
+
const det = x * x
|
|
84
|
+
if (det > env)
|
|
85
|
+
env = alphaA * env + (1 - alphaA) * det
|
|
86
|
+
else
|
|
87
|
+
env = alphaR * env + (1 - alphaR) * det
|
|
88
|
+
}
|
|
89
|
+
this.env[chan] = Math.sqrt(Math.max(env, 1e-12))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* process a single sample frame */
|
|
93
|
+
process(
|
|
94
|
+
inputs: Float32Array[][],
|
|
95
|
+
outputs: Float32Array[][],
|
|
96
|
+
parameters: Record<string, Float32Array>
|
|
97
|
+
): boolean {
|
|
98
|
+
/* sanity check */
|
|
99
|
+
const input = inputs[0]
|
|
100
|
+
const output = outputs[0]
|
|
101
|
+
if (!input || input.length === 0 || !output)
|
|
102
|
+
return true
|
|
103
|
+
|
|
104
|
+
/* determine number of channels */
|
|
105
|
+
const nCh = input.length
|
|
106
|
+
|
|
107
|
+
/* reset envelope array if channel count changed */
|
|
108
|
+
if (nCh !== this.env.length)
|
|
109
|
+
this.env = []
|
|
110
|
+
|
|
111
|
+
/* initially just copy input to output (pass-through) */
|
|
112
|
+
for (let c = 0; c < output.length; c++) {
|
|
113
|
+
if (!output[c] || !input[c])
|
|
114
|
+
continue
|
|
115
|
+
output[c].set(input[c])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* fetch parameters */
|
|
119
|
+
const thresholdDB = parameters["threshold"][0]
|
|
120
|
+
const floorDB = parameters["floor"][0]
|
|
121
|
+
const ratio = parameters["ratio"][0]
|
|
122
|
+
const kneeDB = parameters["knee"][0]
|
|
123
|
+
const attackS = Math.max(parameters["attack"][0], 1 / this.sampleRate)
|
|
124
|
+
const releaseS = Math.max(parameters["release"][0], 1 / this.sampleRate)
|
|
125
|
+
const makeupDB = parameters["makeup"][0]
|
|
126
|
+
|
|
127
|
+
/* update envelope per channel */
|
|
128
|
+
for (let ch = 0; ch < nCh; ch++)
|
|
129
|
+
this.updateEnvelopeForChannel(ch, input[ch], attackS, releaseS)
|
|
130
|
+
|
|
131
|
+
/* determine linear value from decibel makeup value */
|
|
132
|
+
const makeUpLin = utils.dB2lin(makeupDB)
|
|
133
|
+
|
|
134
|
+
/* iterate over all channels */
|
|
135
|
+
for (let ch = 0; ch < nCh; ch++) {
|
|
136
|
+
const levelDB = utils.lin2dB(this.env[ch])
|
|
137
|
+
const gainDB = this.gainDBFor(levelDB, thresholdDB, ratio, kneeDB)
|
|
138
|
+
let gainLin = utils.dB2lin(gainDB) * makeUpLin
|
|
139
|
+
|
|
140
|
+
/* do not attenuate below floor */
|
|
141
|
+
const expectedOutLevelDB = levelDB + gainDB + makeupDB
|
|
142
|
+
if (expectedOutLevelDB < floorDB) {
|
|
143
|
+
const neededLiftDB = floorDB - expectedOutLevelDB
|
|
144
|
+
gainLin /= utils.dB2lin(neededLiftDB)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* apply gain change to channel */
|
|
148
|
+
const inp = input[ch]
|
|
149
|
+
const out = output[ch]
|
|
150
|
+
for (let i = 0; i < inp.length; i++)
|
|
151
|
+
out[i] = inp[i] * gainLin
|
|
152
|
+
}
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* register the new audio nodes */
|
|
158
|
+
registerProcessor("expander", ExpanderProcessor)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import Stream from "node:stream"
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
12
|
+
import { AudioWorkletNode } from "node-web-audio-api"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
16
|
+
import * as utils from "./speechflow-utils"
|
|
17
|
+
import { WebAudio } from "./speechflow-utils-audio"
|
|
18
|
+
|
|
19
|
+
/* internal types */
|
|
20
|
+
interface AudioExpanderConfig {
|
|
21
|
+
thresholdDb?: number
|
|
22
|
+
floorDb?: number
|
|
23
|
+
ratio?: number
|
|
24
|
+
attackMs?: number
|
|
25
|
+
releaseMs?: number
|
|
26
|
+
kneeDb?: number
|
|
27
|
+
makeupDb?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* audio noise expander class */
|
|
31
|
+
class AudioExpander extends WebAudio {
|
|
32
|
+
/* internal state */
|
|
33
|
+
private config: Required<AudioExpanderConfig>
|
|
34
|
+
private expanderNode: AudioWorkletNode | null = null
|
|
35
|
+
|
|
36
|
+
/* construct object */
|
|
37
|
+
constructor(
|
|
38
|
+
sampleRate: number,
|
|
39
|
+
channels: number,
|
|
40
|
+
config: AudioExpanderConfig = {}
|
|
41
|
+
) {
|
|
42
|
+
super(sampleRate, channels)
|
|
43
|
+
|
|
44
|
+
/* store configuration */
|
|
45
|
+
this.config = {
|
|
46
|
+
thresholdDb: config.thresholdDb ?? -45,
|
|
47
|
+
floorDb: config.floorDb ?? -64,
|
|
48
|
+
ratio: config.ratio ?? 4.0,
|
|
49
|
+
attackMs: config.attackMs ?? 10,
|
|
50
|
+
releaseMs: config.releaseMs ?? 50,
|
|
51
|
+
kneeDb: config.kneeDb ?? 6.0,
|
|
52
|
+
makeupDb: config.makeupDb ?? 0
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* initialize object */
|
|
57
|
+
public async setup (): Promise<void> {
|
|
58
|
+
await super.setup()
|
|
59
|
+
|
|
60
|
+
/* add audio worklet module */
|
|
61
|
+
const url = path.resolve(__dirname, "speechflow-node-a2a-expander-wt.js")
|
|
62
|
+
await this.audioContext.audioWorklet.addModule(url)
|
|
63
|
+
|
|
64
|
+
/* create expander node */
|
|
65
|
+
this.expanderNode = new AudioWorkletNode(this.audioContext, "expander", {
|
|
66
|
+
numberOfInputs: 1,
|
|
67
|
+
numberOfOutputs: 1,
|
|
68
|
+
processorOptions: {
|
|
69
|
+
sampleRate: this.audioContext.sampleRate
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/* configure expander node */
|
|
74
|
+
const currentTime = this.audioContext.currentTime
|
|
75
|
+
const node = this.expanderNode!
|
|
76
|
+
const params = node.parameters as Map<string, AudioParam>
|
|
77
|
+
params.get("threshold")!.setValueAtTime(this.config.thresholdDb, currentTime)
|
|
78
|
+
params.get("floor")!.setValueAtTime(this.config.floorDb, currentTime)
|
|
79
|
+
params.get("ratio")!.setValueAtTime(this.config.ratio, currentTime)
|
|
80
|
+
params.get("attack")!.setValueAtTime(this.config.attackMs / 1000, currentTime)
|
|
81
|
+
params.get("release")!.setValueAtTime(this.config.releaseMs / 1000, currentTime)
|
|
82
|
+
params.get("knee")!.setValueAtTime(this.config.kneeDb, currentTime)
|
|
83
|
+
params.get("makeup")!.setValueAtTime(this.config.makeupDb, currentTime)
|
|
84
|
+
|
|
85
|
+
/* connect nodes */
|
|
86
|
+
this.sourceNode!.connect(this.expanderNode)
|
|
87
|
+
this.expanderNode.connect(this.captureNode!)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async destroy (): Promise<void> {
|
|
91
|
+
await super.destroy()
|
|
92
|
+
|
|
93
|
+
/* destroy expander node */
|
|
94
|
+
if (this.expanderNode !== null) {
|
|
95
|
+
this.expanderNode.disconnect()
|
|
96
|
+
this.expanderNode = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* SpeechFlow node for noise expander in audio-to-audio passing */
|
|
102
|
+
export default class SpeechFlowNodeExpander extends SpeechFlowNode {
|
|
103
|
+
/* declare official node name */
|
|
104
|
+
public static name = "expander"
|
|
105
|
+
|
|
106
|
+
/* internal state */
|
|
107
|
+
private destroyed = false
|
|
108
|
+
private expander: AudioExpander | null = null
|
|
109
|
+
|
|
110
|
+
/* construct node */
|
|
111
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
112
|
+
super(id, cfg, opts, args)
|
|
113
|
+
|
|
114
|
+
/* declare node configuration parameters */
|
|
115
|
+
this.configure({
|
|
116
|
+
thresholdDb: { type: "number", val: -45, match: (n: number) => n <= 0 && n >= -100 },
|
|
117
|
+
floorDb: { type: "number", val: -64, match: (n: number) => n <= 0 && n >= -100 },
|
|
118
|
+
ratio: { type: "number", val: 4.0, match: (n: number) => n >= 1 && n <= 20 },
|
|
119
|
+
attackMs: { type: "number", val: 10, match: (n: number) => n >= 0 && n <= 1000 },
|
|
120
|
+
releaseMs: { type: "number", val: 50, match: (n: number) => n >= 0 && n <= 1000 },
|
|
121
|
+
kneeDb: { type: "number", val: 6.0, match: (n: number) => n >= 0 && n <= 40 },
|
|
122
|
+
makeupDb: { type: "number", val: 0, match: (n: number) => n >= -24 && n <= 24 }
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/* sanity check floor vs threshold */
|
|
126
|
+
if (this.params.floorDb >= this.params.thresholdDb)
|
|
127
|
+
throw new Error("floor dB must be less than threshold dB for proper expansion")
|
|
128
|
+
|
|
129
|
+
/* declare node input/output format */
|
|
130
|
+
this.input = "audio"
|
|
131
|
+
this.output = "audio"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* open node */
|
|
135
|
+
async open () {
|
|
136
|
+
/* clear destruction flag */
|
|
137
|
+
this.destroyed = false
|
|
138
|
+
|
|
139
|
+
/* setup expander */
|
|
140
|
+
this.expander = new AudioExpander(
|
|
141
|
+
this.config.audioSampleRate,
|
|
142
|
+
this.config.audioChannels, {
|
|
143
|
+
thresholdDb: this.params.thresholdDb,
|
|
144
|
+
floorDb: this.params.floorDb,
|
|
145
|
+
ratio: this.params.ratio,
|
|
146
|
+
attackMs: this.params.attackMs,
|
|
147
|
+
releaseMs: this.params.releaseMs,
|
|
148
|
+
kneeDb: this.params.kneeDb,
|
|
149
|
+
makeupDb: this.params.makeupDb
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
await this.expander.setup()
|
|
153
|
+
|
|
154
|
+
/* establish a transform stream */
|
|
155
|
+
const self = this
|
|
156
|
+
this.stream = new Stream.Transform({
|
|
157
|
+
readableObjectMode: true,
|
|
158
|
+
writableObjectMode: true,
|
|
159
|
+
decodeStrings: false,
|
|
160
|
+
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
|
|
161
|
+
if (self.destroyed) {
|
|
162
|
+
callback(new Error("stream already destroyed"))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (!Buffer.isBuffer(chunk.payload))
|
|
166
|
+
callback(new Error("invalid chunk payload type"))
|
|
167
|
+
else {
|
|
168
|
+
/* expand chunk */
|
|
169
|
+
const payload = utils.convertBufToI16(chunk.payload)
|
|
170
|
+
self.expander?.process(payload).then((result) => {
|
|
171
|
+
if (self.destroyed)
|
|
172
|
+
throw new Error("stream already destroyed")
|
|
173
|
+
|
|
174
|
+
/* take over expanded data */
|
|
175
|
+
const payload = utils.convertI16ToBuf(result)
|
|
176
|
+
chunk.payload = payload
|
|
177
|
+
this.push(chunk)
|
|
178
|
+
callback()
|
|
179
|
+
}).catch((error) => {
|
|
180
|
+
callback(new Error(`expansion failed: ${error}`))
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
final (callback) {
|
|
185
|
+
if (self.destroyed) {
|
|
186
|
+
callback()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
this.push(null)
|
|
190
|
+
callback()
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* close node */
|
|
196
|
+
async close () {
|
|
197
|
+
/* indicate destruction */
|
|
198
|
+
this.destroyed = true
|
|
199
|
+
|
|
200
|
+
/* destroy expander */
|
|
201
|
+
if (this.expander !== null) {
|
|
202
|
+
await this.expander.destroy()
|
|
203
|
+
this.expander = null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* close stream */
|
|
207
|
+
if (this.stream !== null) {
|
|
208
|
+
this.stream.destroy()
|
|
209
|
+
this.stream = null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|