speechflow 1.6.4 → 1.6.6
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 +22 -0
- package/README.md +28 -3
- package/etc/speechflow.yaml +15 -13
- package/etc/stx.conf +5 -0
- package/package.json +5 -5
- package/speechflow-cli/dst/speechflow-main-api.js +3 -7
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.js +1 -1
- package/speechflow-cli/dst/speechflow-main.js +6 -0
- package/speechflow-cli/dst/speechflow-main.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +1 -21
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +12 -11
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -21
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +12 -11
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +4 -10
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +18 -16
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +8 -8
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +70 -60
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +58 -42
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +44 -10
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +213 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch2-wt.js +149 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch2-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch2.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch2.js +202 -0
- package/speechflow-cli/dst/speechflow-node-a2a-pitch2.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +13 -11
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +13 -12
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +26 -25
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +35 -7
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +16 -16
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -16
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.d.ts +1 -2
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +15 -21
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.d.ts +1 -2
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +9 -15
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.d.ts +1 -2
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +13 -18
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +4 -10
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +36 -2
- package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +5 -5
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +13 -13
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +8 -8
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +42 -8
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +6 -4
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +19 -18
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +13 -13
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +8 -8
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.js +6 -6
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-util-audio.js +22 -1
- 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 +7 -1
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +23 -3
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/etc/tsconfig.json +1 -0
- package/speechflow-cli/package.json +20 -20
- package/speechflow-cli/src/speechflow-main-api.ts +6 -13
- package/speechflow-cli/src/speechflow-main-graph.ts +1 -1
- package/speechflow-cli/src/speechflow-main.ts +4 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +1 -29
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +13 -12
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -29
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +13 -12
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +4 -10
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +19 -17
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +8 -8
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +83 -72
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +66 -46
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +11 -10
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +221 -0
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +14 -12
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +14 -13
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +26 -25
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +2 -7
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +16 -16
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +16 -16
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +15 -21
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +9 -15
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +13 -18
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +4 -10
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +3 -2
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +6 -6
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +13 -13
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +12 -16
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +2 -2
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +10 -9
- package/speechflow-cli/src/speechflow-node-xio-device.ts +7 -5
- package/speechflow-cli/src/speechflow-node-xio-file.ts +20 -19
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +14 -14
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +11 -11
- package/speechflow-cli/src/speechflow-node.ts +6 -6
- package/speechflow-cli/src/speechflow-util-audio.ts +31 -1
- package/speechflow-cli/src/speechflow-util-error.ts +9 -3
- package/speechflow-cli/src/speechflow-util-stream.ts +31 -6
- package/speechflow-ui-db/dst/index.js +25 -25
- package/speechflow-ui-db/package.json +11 -11
- package/speechflow-ui-db/src/app.vue +14 -5
- package/speechflow-ui-st/dst/index.js +460 -25
- package/speechflow-ui-st/package.json +13 -13
- package/speechflow-ui-st/src/app.vue +8 -3
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +0 -124
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +0 -1
- package/speechflow-cli/dst/speechflow-util-webaudio.d.ts +0 -13
- package/speechflow-cli/dst/speechflow-util-webaudio.js +0 -137
- package/speechflow-cli/dst/speechflow-util-webaudio.js.map +0 -1
- /package/speechflow-cli/dst/{speechflow-util-webaudio-wt.d.ts → speechflow-node-a2a-pitch2-wt.d.ts} +0 -0
|
@@ -22,9 +22,9 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
22
22
|
/* internal state */
|
|
23
23
|
private emitInterval: ReturnType<typeof setInterval> | null = null
|
|
24
24
|
private calcInterval: ReturnType<typeof setInterval> | null = null
|
|
25
|
-
private silenceTimer: ReturnType<typeof setTimeout>
|
|
25
|
+
private silenceTimer: ReturnType<typeof setTimeout> | null = null
|
|
26
26
|
private chunkBuffer = new Float32Array(0)
|
|
27
|
-
private
|
|
27
|
+
private closing = false
|
|
28
28
|
|
|
29
29
|
/* construct node */
|
|
30
30
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
@@ -32,7 +32,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
32
32
|
|
|
33
33
|
/* declare node configuration parameters */
|
|
34
34
|
this.configure({
|
|
35
|
-
interval: { type: "number", pos: 0, val:
|
|
35
|
+
interval: { type: "number", pos: 0, val: 100 },
|
|
36
36
|
mode: { type: "string", pos: 1, val: "filter", match: /^(?:filter|sink)$/ },
|
|
37
37
|
dashboard: { type: "string", val: "" }
|
|
38
38
|
})
|
|
@@ -52,74 +52,94 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
52
52
|
throw new Error("meter node currently supports PCM-S16LE audio only")
|
|
53
53
|
|
|
54
54
|
/* clear destruction flag */
|
|
55
|
-
this.
|
|
55
|
+
this.closing = false
|
|
56
56
|
|
|
57
57
|
/* internal state */
|
|
58
|
-
|
|
58
|
+
let lufsm = -60
|
|
59
|
+
let rms = -60
|
|
60
|
+
|
|
61
|
+
/* chunk processing state for LUFS-M */
|
|
62
|
+
const sampleWindowDuration = 0.4 /* LUFS-M requires 400ms */
|
|
59
63
|
const sampleWindowSize = Math.floor(this.config.audioSampleRate * sampleWindowDuration)
|
|
60
64
|
const sampleWindow = new Float32Array(sampleWindowSize)
|
|
61
65
|
sampleWindow.fill(0, 0, sampleWindowSize)
|
|
62
|
-
let lufss = -60
|
|
63
|
-
let rms = -60
|
|
64
66
|
|
|
65
|
-
/* chunk processing state */
|
|
67
|
+
/* chunk processing state for RMS */
|
|
66
68
|
const chunkDuration = 0.050 /* meter update frequency is about 50ms */
|
|
67
69
|
const samplesPerChunk = Math.floor(this.config.audioSampleRate * chunkDuration)
|
|
68
70
|
this.chunkBuffer = new Float32Array(0)
|
|
69
71
|
|
|
70
|
-
/*
|
|
71
|
-
|
|
72
|
-
/*
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
/* setup chunking interval */
|
|
73
|
+
this.calcInterval = setInterval(() => {
|
|
74
|
+
/* short-circuit during destruction */
|
|
75
|
+
if (this.closing)
|
|
76
|
+
return
|
|
75
77
|
|
|
76
|
-
/*
|
|
77
|
-
|
|
78
|
+
/* short-circuit if still not enough chunk data */
|
|
79
|
+
if (this.chunkBuffer.length < samplesPerChunk)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
/* grab the accumulated chunk data */
|
|
83
|
+
const chunkData = this.chunkBuffer
|
|
84
|
+
this.chunkBuffer = new Float32Array(0)
|
|
85
|
+
|
|
86
|
+
/* update internal audio sample sliding window for LUFS-S */
|
|
87
|
+
if (chunkData.length > sampleWindow.length)
|
|
88
|
+
sampleWindow.set(chunkData.subarray(chunkData.length - sampleWindow.length), 0)
|
|
89
|
+
else {
|
|
90
|
+
sampleWindow.set(sampleWindow.subarray(chunkData.length), 0)
|
|
91
|
+
sampleWindow.set(chunkData, sampleWindow.length - chunkData.length)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* calculate the LUFS-M metric */
|
|
95
|
+
const audioDataLUFS = {
|
|
78
96
|
sampleRate: this.config.audioSampleRate,
|
|
79
97
|
numberOfChannels: this.config.audioChannels,
|
|
80
98
|
channelData: [ sampleWindow ],
|
|
81
99
|
duration: sampleWindowDuration,
|
|
82
100
|
length: sampleWindow.length
|
|
83
101
|
} satisfies AudioData
|
|
84
|
-
const lufs = getLUFS(
|
|
102
|
+
const lufs = getLUFS(audioDataLUFS, {
|
|
85
103
|
channelMode: this.config.audioChannels === 1 ? "mono" : "stereo",
|
|
86
|
-
calculateShortTerm:
|
|
87
|
-
calculateMomentary:
|
|
104
|
+
calculateShortTerm: false,
|
|
105
|
+
calculateMomentary: true,
|
|
88
106
|
calculateLoudnessRange: false,
|
|
89
107
|
calculateTruePeak: false
|
|
90
108
|
})
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
lufsm = lufs.momentary ? Math.max(-60, lufs.momentary[0]) : -60
|
|
110
|
+
|
|
111
|
+
/* calculate the RMS metric */
|
|
112
|
+
const totalSamples = chunkData.length / this.config.audioChannels
|
|
113
|
+
const duration = totalSamples / this.config.audioSampleRate
|
|
114
|
+
const audioDataRMS = {
|
|
115
|
+
sampleRate: this.config.audioSampleRate,
|
|
116
|
+
numberOfChannels: this.config.audioChannels,
|
|
117
|
+
channelData: [ chunkData ],
|
|
118
|
+
duration,
|
|
119
|
+
length: chunkData.length
|
|
120
|
+
} satisfies AudioData
|
|
121
|
+
rms = Math.max(-60, getRMS(audioDataRMS, {
|
|
122
|
+
asDB: true
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
/* automatically clear measurement (in case no new measurements happen) */
|
|
93
126
|
if (this.silenceTimer !== null)
|
|
94
127
|
clearTimeout(this.silenceTimer)
|
|
95
128
|
this.silenceTimer = setTimeout(() => {
|
|
96
|
-
|
|
129
|
+
lufsm = -60
|
|
97
130
|
rms = -60
|
|
98
131
|
}, 500)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* setup chunking interval */
|
|
102
|
-
this.calcInterval = setInterval(() => {
|
|
103
|
-
if (this.destroyed)
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
/* process one single 50ms chunk if available */
|
|
107
|
-
if (this.chunkBuffer.length >= samplesPerChunk) {
|
|
108
|
-
const chunkData = this.chunkBuffer.slice(0, samplesPerChunk)
|
|
109
|
-
this.chunkBuffer = this.chunkBuffer.slice(samplesPerChunk)
|
|
110
|
-
processChunk(chunkData)
|
|
111
|
-
}
|
|
112
132
|
}, chunkDuration * 1000)
|
|
113
133
|
|
|
114
134
|
/* setup loudness emitting interval */
|
|
115
135
|
this.emitInterval = setInterval(() => {
|
|
116
|
-
if (this.
|
|
136
|
+
if (this.closing)
|
|
117
137
|
return
|
|
118
|
-
this.log("debug", `LUFS-
|
|
119
|
-
this.sendResponse([ "meter", "LUFS-
|
|
138
|
+
this.log("debug", `LUFS-M: ${lufsm.toFixed(1)} dB, RMS: ${rms.toFixed(1)} dB`)
|
|
139
|
+
this.sendResponse([ "meter", "LUFS-M", lufsm ])
|
|
120
140
|
this.sendResponse([ "meter", "RMS", rms ])
|
|
121
141
|
if (this.params.dashboard !== "")
|
|
122
|
-
this.sendDashboard("audio", this.params.dashboard, "final",
|
|
142
|
+
this.sendDashboard("audio", this.params.dashboard, "final", lufsm)
|
|
123
143
|
}, this.params.interval)
|
|
124
144
|
|
|
125
145
|
/* provide Duplex stream and internally attach to meter */
|
|
@@ -132,7 +152,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
132
152
|
|
|
133
153
|
/* transform audio chunk */
|
|
134
154
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
135
|
-
if (self.
|
|
155
|
+
if (self.closing) {
|
|
136
156
|
callback(new Error("stream already destroyed"))
|
|
137
157
|
return
|
|
138
158
|
}
|
|
@@ -158,12 +178,12 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
158
178
|
callback()
|
|
159
179
|
}
|
|
160
180
|
catch (error) {
|
|
161
|
-
callback(error
|
|
181
|
+
callback(util.ensureError(error, "meter processing failed"))
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
},
|
|
165
185
|
final (callback) {
|
|
166
|
-
if (self.
|
|
186
|
+
if (self.closing || self.params.mode === "sink") {
|
|
167
187
|
callback()
|
|
168
188
|
return
|
|
169
189
|
}
|
|
@@ -175,6 +195,9 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
175
195
|
|
|
176
196
|
/* close node */
|
|
177
197
|
async close () {
|
|
198
|
+
/* indicate closing immediately to stop any ongoing operations */
|
|
199
|
+
this.closing = true
|
|
200
|
+
|
|
178
201
|
/* stop intervals */
|
|
179
202
|
if (this.emitInterval !== null) {
|
|
180
203
|
clearInterval(this.emitInterval)
|
|
@@ -189,13 +212,10 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
189
212
|
this.silenceTimer = null
|
|
190
213
|
}
|
|
191
214
|
|
|
192
|
-
/*
|
|
215
|
+
/* shutdown stream */
|
|
193
216
|
if (this.stream !== null) {
|
|
194
|
-
this.stream
|
|
217
|
+
await util.destroyStream(this.stream)
|
|
195
218
|
this.stream = null
|
|
196
219
|
}
|
|
197
|
-
|
|
198
|
-
/* indicate destruction */
|
|
199
|
-
this.destroyed = true
|
|
200
220
|
}
|
|
201
221
|
}
|
|
@@ -9,6 +9,7 @@ import Stream from "node:stream"
|
|
|
9
9
|
|
|
10
10
|
/* internal dependencies */
|
|
11
11
|
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
12
|
+
import * as util from "./speechflow-util"
|
|
12
13
|
|
|
13
14
|
/* the type of muting */
|
|
14
15
|
type MuteMode =
|
|
@@ -23,7 +24,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
23
24
|
|
|
24
25
|
/* internal state */
|
|
25
26
|
private muteMode: MuteMode = "none"
|
|
26
|
-
private
|
|
27
|
+
private closing = false
|
|
27
28
|
|
|
28
29
|
/* construct node */
|
|
29
30
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
@@ -39,7 +40,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
39
40
|
|
|
40
41
|
/* receive external request */
|
|
41
42
|
async receiveRequest (params: any[]) {
|
|
42
|
-
if (this.
|
|
43
|
+
if (this.closing)
|
|
43
44
|
throw new Error("mute: node already destroyed")
|
|
44
45
|
try {
|
|
45
46
|
if (params.length === 2 && params[0] === "mode") {
|
|
@@ -61,7 +62,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
61
62
|
|
|
62
63
|
/* change mute mode */
|
|
63
64
|
setMuteMode (mode: MuteMode) {
|
|
64
|
-
if (this.
|
|
65
|
+
if (this.closing) {
|
|
65
66
|
this.log("warning", "attempted to set mute mode on destroyed node")
|
|
66
67
|
return
|
|
67
68
|
}
|
|
@@ -72,7 +73,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
72
73
|
/* open node */
|
|
73
74
|
async open () {
|
|
74
75
|
/* clear destruction flag */
|
|
75
|
-
this.
|
|
76
|
+
this.closing = false
|
|
76
77
|
|
|
77
78
|
/* establish a transform stream */
|
|
78
79
|
const self = this
|
|
@@ -81,7 +82,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
81
82
|
writableObjectMode: true,
|
|
82
83
|
decodeStrings: false,
|
|
83
84
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
84
|
-
if (self.
|
|
85
|
+
if (self.closing) {
|
|
85
86
|
callback(new Error("stream already destroyed"))
|
|
86
87
|
return
|
|
87
88
|
}
|
|
@@ -106,7 +107,7 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
106
107
|
}
|
|
107
108
|
},
|
|
108
109
|
final (callback) {
|
|
109
|
-
if (self.
|
|
110
|
+
if (self.closing) {
|
|
110
111
|
callback()
|
|
111
112
|
return
|
|
112
113
|
}
|
|
@@ -118,12 +119,12 @@ export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
|
|
|
118
119
|
|
|
119
120
|
/* close node */
|
|
120
121
|
async close () {
|
|
121
|
-
/* indicate
|
|
122
|
-
this.
|
|
122
|
+
/* indicate closing */
|
|
123
|
+
this.closing = true
|
|
123
124
|
|
|
124
|
-
/*
|
|
125
|
+
/* shutdown stream */
|
|
125
126
|
if (this.stream !== null) {
|
|
126
|
-
this.stream
|
|
127
|
+
await util.destroyStream(this.stream)
|
|
127
128
|
this.stream = null
|
|
128
129
|
}
|
|
129
130
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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 util from "./speechflow-util"
|
|
17
|
+
|
|
18
|
+
/* parameter configuration */
|
|
19
|
+
type AudioPitchShifterConfig = {
|
|
20
|
+
rate?: number
|
|
21
|
+
tempo?: number
|
|
22
|
+
pitch?: number
|
|
23
|
+
semitones?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* audio pitch shifter class using SoundTouch WebAudio worklet */
|
|
27
|
+
class AudioPitchShifter extends util.WebAudio {
|
|
28
|
+
/* internal state */
|
|
29
|
+
private pitchNode: AudioWorkletNode | null = null
|
|
30
|
+
private config: Required<AudioPitchShifterConfig>
|
|
31
|
+
|
|
32
|
+
/* construct object */
|
|
33
|
+
constructor (
|
|
34
|
+
sampleRate: number,
|
|
35
|
+
channels: number,
|
|
36
|
+
config: AudioPitchShifterConfig = {}
|
|
37
|
+
) {
|
|
38
|
+
super(sampleRate, channels)
|
|
39
|
+
this.config = {
|
|
40
|
+
rate: config.rate ?? 1.0,
|
|
41
|
+
tempo: config.tempo ?? 1.0,
|
|
42
|
+
pitch: config.pitch ?? 1.0,
|
|
43
|
+
semitones: config.semitones ?? 0.0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* setup object */
|
|
48
|
+
public async setup (): Promise<void> {
|
|
49
|
+
await super.setup()
|
|
50
|
+
|
|
51
|
+
/* add SoundTouch worklet module */
|
|
52
|
+
const packagePath = path.join(__dirname, "../node_modules/@soundtouchjs/audio-worklet")
|
|
53
|
+
const workletPath = path.join(packagePath, "dist/soundtouch-worklet.js")
|
|
54
|
+
await this.audioContext.audioWorklet.addModule(workletPath)
|
|
55
|
+
|
|
56
|
+
/* create SoundTouch worklet node */
|
|
57
|
+
this.pitchNode = new AudioWorkletNode(this.audioContext, "soundtouch-processor", {
|
|
58
|
+
numberOfInputs: 1,
|
|
59
|
+
numberOfOutputs: 1,
|
|
60
|
+
outputChannelCount: [ this.channels ]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/* set initial parameter values */
|
|
64
|
+
const params = this.pitchNode.parameters as Map<string, AudioParam>
|
|
65
|
+
params.get("rate")!.value = this.config.rate
|
|
66
|
+
params.get("tempo")!.value = this.config.tempo
|
|
67
|
+
params.get("pitch")!.value = this.config.pitch
|
|
68
|
+
params.get("pitchSemitones")!.value = this.config.semitones
|
|
69
|
+
|
|
70
|
+
/* connect nodes: source -> pitch -> capture */
|
|
71
|
+
this.sourceNode!.connect(this.pitchNode)
|
|
72
|
+
this.pitchNode.connect(this.captureNode!)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* update an audio parameter value */
|
|
76
|
+
private updateParameter (
|
|
77
|
+
paramName: string,
|
|
78
|
+
value: number,
|
|
79
|
+
configField: keyof Required<AudioPitchShifterConfig>
|
|
80
|
+
): void {
|
|
81
|
+
const params = this.pitchNode?.parameters as Map<string, AudioParam>
|
|
82
|
+
params?.get(paramName)?.setValueAtTime(value, this.audioContext.currentTime)
|
|
83
|
+
this.config[configField] = value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* update rate value */
|
|
87
|
+
public setRate (rate: number): void {
|
|
88
|
+
this.updateParameter("rate", rate, "rate")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* update tempo value */
|
|
92
|
+
public setTempo (tempo: number): void {
|
|
93
|
+
this.updateParameter("tempo", tempo, "tempo")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* update pitch shift value */
|
|
97
|
+
public setPitch (pitch: number): void {
|
|
98
|
+
this.updateParameter("pitch", pitch, "pitch")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* update pitch semitones setting */
|
|
102
|
+
public setSemitones (semitones: number): void {
|
|
103
|
+
this.updateParameter("pitchSemitones", semitones, "semitones")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* destroy the pitch shifter */
|
|
107
|
+
public async destroy (): Promise<void> {
|
|
108
|
+
/* disconnect pitch node */
|
|
109
|
+
if (this.pitchNode !== null) {
|
|
110
|
+
this.pitchNode.disconnect()
|
|
111
|
+
this.pitchNode = null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* destroy parent */
|
|
115
|
+
await super.destroy()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* SpeechFlow node for pitch adjustment using SoundTouch WebAudio */
|
|
120
|
+
export default class SpeechFlowNodeA2APitch extends SpeechFlowNode {
|
|
121
|
+
/* declare official node name */
|
|
122
|
+
public static name = "a2a-pitch"
|
|
123
|
+
|
|
124
|
+
/* internal state */
|
|
125
|
+
private closing = false
|
|
126
|
+
private pitchShifter: AudioPitchShifter | null = null
|
|
127
|
+
|
|
128
|
+
/* construct node */
|
|
129
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
130
|
+
super(id, cfg, opts, args)
|
|
131
|
+
|
|
132
|
+
/* declare node configuration parameters */
|
|
133
|
+
this.configure({
|
|
134
|
+
rate: { type: "number", val: 1.0, match: (n: number) => n >= 0.25 && n <= 4.0 },
|
|
135
|
+
tempo: { type: "number", val: 1.0, match: (n: number) => n >= 0.25 && n <= 4.0 },
|
|
136
|
+
pitch: { type: "number", val: 1.0, match: (n: number) => n >= 0.25 && n <= 4.0 },
|
|
137
|
+
semitones: { type: "number", val: 0.0, match: (n: number) => n >= -24 && n <= 24 }
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
/* declare node input/output format */
|
|
141
|
+
this.input = "audio"
|
|
142
|
+
this.output = "audio"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* open node */
|
|
146
|
+
async open () {
|
|
147
|
+
/* clear destruction flag */
|
|
148
|
+
this.closing = false
|
|
149
|
+
|
|
150
|
+
/* setup pitch shifter */
|
|
151
|
+
this.pitchShifter = new AudioPitchShifter(
|
|
152
|
+
this.config.audioSampleRate,
|
|
153
|
+
this.config.audioChannels, {
|
|
154
|
+
rate: this.params.rate,
|
|
155
|
+
tempo: this.params.tempo,
|
|
156
|
+
pitch: this.params.pitch,
|
|
157
|
+
semitones: this.params.semitones
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
await this.pitchShifter.setup()
|
|
161
|
+
|
|
162
|
+
/* establish a transform stream */
|
|
163
|
+
const self = this
|
|
164
|
+
this.stream = new Stream.Transform({
|
|
165
|
+
readableObjectMode: true,
|
|
166
|
+
writableObjectMode: true,
|
|
167
|
+
decodeStrings: false,
|
|
168
|
+
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
|
|
169
|
+
if (self.closing) {
|
|
170
|
+
callback(new Error("stream already destroyed"))
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if (!Buffer.isBuffer(chunk.payload))
|
|
174
|
+
callback(new Error("invalid chunk payload type"))
|
|
175
|
+
else {
|
|
176
|
+
/* shift pitch of audio chunk */
|
|
177
|
+
const payload = util.convertBufToI16(chunk.payload, self.config.audioLittleEndian)
|
|
178
|
+
self.pitchShifter?.process(payload).then((result) => {
|
|
179
|
+
if (self.closing)
|
|
180
|
+
throw new Error("stream already destroyed")
|
|
181
|
+
|
|
182
|
+
/* take over pitch-shifted data */
|
|
183
|
+
const payload = util.convertI16ToBuf(result, self.config.audioLittleEndian)
|
|
184
|
+
chunk.payload = payload
|
|
185
|
+
this.push(chunk)
|
|
186
|
+
callback()
|
|
187
|
+
}).catch((error: unknown) => {
|
|
188
|
+
if (!self.closing)
|
|
189
|
+
callback(util.ensureError(error, "pitch shifting failed"))
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
final (callback) {
|
|
194
|
+
if (self.closing) {
|
|
195
|
+
callback()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
this.push(null)
|
|
199
|
+
callback()
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* close node */
|
|
205
|
+
async close () {
|
|
206
|
+
/* indicate closing */
|
|
207
|
+
this.closing = true
|
|
208
|
+
|
|
209
|
+
/* destroy pitch shifter */
|
|
210
|
+
if (this.pitchShifter !== null) {
|
|
211
|
+
await this.pitchShifter.destroy()
|
|
212
|
+
this.pitchShifter = null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* shutdown stream */
|
|
216
|
+
if (this.stream !== null) {
|
|
217
|
+
await util.destroyStream(this.stream)
|
|
218
|
+
this.stream = null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -19,7 +19,7 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
19
19
|
public static name = "a2a-rnnoise"
|
|
20
20
|
|
|
21
21
|
/* internal state */
|
|
22
|
-
private
|
|
22
|
+
private closing = false
|
|
23
23
|
private sampleSize = 480 /* = 10ms at 48KHz, as required by RNNoise! */
|
|
24
24
|
private worker: Worker | null = null
|
|
25
25
|
|
|
@@ -38,12 +38,13 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
38
38
|
/* open node */
|
|
39
39
|
async open () {
|
|
40
40
|
/* clear destruction flag */
|
|
41
|
-
this.
|
|
41
|
+
this.closing = false
|
|
42
42
|
|
|
43
43
|
/* initialize worker */
|
|
44
44
|
this.worker = new Worker(resolve(__dirname, "speechflow-node-a2a-rnnoise-wt.js"))
|
|
45
45
|
this.worker.on("error", (err) => {
|
|
46
46
|
this.log("error", `RNNoise worker thread error: ${err}`)
|
|
47
|
+
this.stream?.emit("error", err)
|
|
47
48
|
})
|
|
48
49
|
this.worker.on("exit", (code) => {
|
|
49
50
|
if (code !== 0)
|
|
@@ -88,7 +89,7 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
88
89
|
/* send message to worker */
|
|
89
90
|
let seq = 0
|
|
90
91
|
const workerProcessSegment = async (segment: Int16Array<ArrayBuffer>) => {
|
|
91
|
-
if (this.
|
|
92
|
+
if (this.closing)
|
|
92
93
|
return segment
|
|
93
94
|
const id = `${seq++}`
|
|
94
95
|
return new Promise<Int16Array<ArrayBuffer>>((resolve) => {
|
|
@@ -104,7 +105,7 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
104
105
|
writableObjectMode: true,
|
|
105
106
|
decodeStrings: false,
|
|
106
107
|
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
|
|
107
|
-
if (self.
|
|
108
|
+
if (self.closing) {
|
|
108
109
|
callback(new Error("stream already destroyed"))
|
|
109
110
|
return
|
|
110
111
|
}
|
|
@@ -127,14 +128,15 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
127
128
|
/* forward updated chunk */
|
|
128
129
|
this.push(chunk)
|
|
129
130
|
callback()
|
|
130
|
-
}).catch((err:
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
}).catch((err: unknown) => {
|
|
132
|
+
const error = util.ensureError(err)
|
|
133
|
+
self.log("warning", `processing of chunk failed: ${error.message}`)
|
|
134
|
+
callback(error)
|
|
133
135
|
})
|
|
134
136
|
}
|
|
135
137
|
},
|
|
136
138
|
final (callback) {
|
|
137
|
-
if (self.
|
|
139
|
+
if (self.closing) {
|
|
138
140
|
callback()
|
|
139
141
|
return
|
|
140
142
|
}
|
|
@@ -146,8 +148,8 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
146
148
|
|
|
147
149
|
/* close node */
|
|
148
150
|
async close () {
|
|
149
|
-
/* indicate
|
|
150
|
-
this.
|
|
151
|
+
/* indicate closing */
|
|
152
|
+
this.closing = true
|
|
151
153
|
|
|
152
154
|
/* shutdown worker */
|
|
153
155
|
if (this.worker !== null) {
|
|
@@ -155,9 +157,9 @@ export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
|
|
|
155
157
|
this.worker = null
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
/*
|
|
160
|
+
/* shutdown stream */
|
|
159
161
|
if (this.stream !== null) {
|
|
160
|
-
this.stream
|
|
162
|
+
await util.destroyStream(this.stream)
|
|
161
163
|
this.stream = null
|
|
162
164
|
}
|
|
163
165
|
}
|
|
@@ -22,7 +22,7 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
22
22
|
public static name = "a2a-speex"
|
|
23
23
|
|
|
24
24
|
/* internal state */
|
|
25
|
-
private
|
|
25
|
+
private closing = false
|
|
26
26
|
private sampleSize = 480 /* = 10ms at 48KHz */
|
|
27
27
|
private speexProcessor: SpeexPreprocessor | null = null
|
|
28
28
|
|
|
@@ -43,7 +43,7 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
43
43
|
/* open node */
|
|
44
44
|
async open () {
|
|
45
45
|
/* clear destruction flag */
|
|
46
|
-
this.
|
|
46
|
+
this.closing = false
|
|
47
47
|
|
|
48
48
|
/* validate sample rate compatibility */
|
|
49
49
|
if (this.config.audioSampleRate !== 48000)
|
|
@@ -71,7 +71,7 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
71
71
|
writableObjectMode: true,
|
|
72
72
|
decodeStrings: false,
|
|
73
73
|
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
|
|
74
|
-
if (self.
|
|
74
|
+
if (self.closing) {
|
|
75
75
|
callback(new Error("stream already destroyed"))
|
|
76
76
|
return
|
|
77
77
|
}
|
|
@@ -83,12 +83,12 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
83
83
|
|
|
84
84
|
/* process Int16Array in necessary fixed-size segments */
|
|
85
85
|
util.processInt16ArrayInSegments(payload, self.sampleSize, (segment) => {
|
|
86
|
-
if (self.
|
|
86
|
+
if (self.closing)
|
|
87
87
|
throw new Error("stream already destroyed")
|
|
88
88
|
self.speexProcessor?.processInt16(segment)
|
|
89
89
|
return Promise.resolve(segment)
|
|
90
90
|
}).then((payload: Int16Array<ArrayBuffer>) => {
|
|
91
|
-
if (self.
|
|
91
|
+
if (self.closing)
|
|
92
92
|
throw new Error("stream already destroyed")
|
|
93
93
|
|
|
94
94
|
/* convert Int16Array back into Buffer */
|
|
@@ -100,14 +100,15 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
100
100
|
/* forward updated chunk */
|
|
101
101
|
this.push(chunk)
|
|
102
102
|
callback()
|
|
103
|
-
}).catch((err:
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
}).catch((err: unknown) => {
|
|
104
|
+
const error = util.ensureError(err)
|
|
105
|
+
self.log("warning", `processing of chunk failed: ${error.message}`)
|
|
106
|
+
callback(error)
|
|
106
107
|
})
|
|
107
108
|
}
|
|
108
109
|
},
|
|
109
110
|
final (callback) {
|
|
110
|
-
if (self.
|
|
111
|
+
if (self.closing) {
|
|
111
112
|
callback()
|
|
112
113
|
return
|
|
113
114
|
}
|
|
@@ -119,8 +120,8 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
119
120
|
|
|
120
121
|
/* close node */
|
|
121
122
|
async close () {
|
|
122
|
-
/* indicate
|
|
123
|
-
this.
|
|
123
|
+
/* indicate closing */
|
|
124
|
+
this.closing = true
|
|
124
125
|
|
|
125
126
|
/* destroy processor */
|
|
126
127
|
if (this.speexProcessor !== null) {
|
|
@@ -128,9 +129,9 @@ export default class SpeechFlowNodeA2ASpeex extends SpeechFlowNode {
|
|
|
128
129
|
this.speexProcessor = null
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
/*
|
|
132
|
+
/* shutdown stream */
|
|
132
133
|
if (this.stream !== null) {
|
|
133
|
-
this.stream
|
|
134
|
+
await util.destroyStream(this.stream)
|
|
134
135
|
this.stream = null
|
|
135
136
|
}
|
|
136
137
|
}
|