speechflow 1.6.2 → 1.6.4
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/.claude/settings.local.json +3 -0
- package/CHANGELOG.md +16 -0
- package/README.md +82 -45
- package/etc/speechflow.yaml +19 -14
- package/package.json +5 -5
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +2 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +2 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +3 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +11 -8
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +22 -2
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.d.ts +3 -0
- package/speechflow-cli/dst/speechflow-node-xio-device.js +99 -84
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-util-error.js +7 -0
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/etc/stx.conf +6 -10
- package/speechflow-cli/package.json +13 -13
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +2 -1
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +2 -1
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +11 -8
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +4 -1
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +25 -2
- package/speechflow-cli/src/speechflow-node-xio-device.ts +108 -89
- package/speechflow-cli/src/speechflow-util-error.ts +7 -0
- package/speechflow-ui-db/dst/index.js +24 -33
- package/speechflow-ui-db/package.json +12 -10
- package/speechflow-ui-db/src/app.vue +16 -2
- package/speechflow-ui-st/.claude/settings.local.json +3 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.eot +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.ttf +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.woff +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.eot +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.ttf +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.woff +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.eot +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.ttf +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.woff +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.eot +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.ttf +0 -0
- package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.woff +0 -0
- package/speechflow-ui-st/dst/index.css +2 -2
- package/speechflow-ui-st/dst/index.js +26 -28
- package/speechflow-ui-st/package.json +11 -10
- package/speechflow-ui-st/src/app.vue +142 -48
- package/speechflow-ui-st/src/index.ts +4 -0
|
@@ -33,12 +33,16 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
33
33
|
/* declare node configuration parameters */
|
|
34
34
|
this.configure({
|
|
35
35
|
interval: { type: "number", pos: 0, val: 250 },
|
|
36
|
+
mode: { type: "string", pos: 1, val: "filter", match: /^(?:filter|sink)$/ },
|
|
36
37
|
dashboard: { type: "string", val: "" }
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
/* declare node input/output format */
|
|
40
41
|
this.input = "audio"
|
|
41
|
-
this.
|
|
42
|
+
if (this.params.mode === "filter")
|
|
43
|
+
this.output = "audio"
|
|
44
|
+
else if (this.params.mode === "sink")
|
|
45
|
+
this.output = "none"
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/* open node */
|
|
@@ -53,7 +57,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
53
57
|
/* internal state */
|
|
54
58
|
const sampleWindowDuration = 3 /* LUFS-S requires 3s */
|
|
55
59
|
const sampleWindowSize = Math.floor(this.config.audioSampleRate * sampleWindowDuration)
|
|
56
|
-
|
|
60
|
+
const sampleWindow = new Float32Array(sampleWindowSize)
|
|
57
61
|
sampleWindow.fill(0, 0, sampleWindowSize)
|
|
58
62
|
let lufss = -60
|
|
59
63
|
let rms = -60
|
|
@@ -66,10 +70,8 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
66
70
|
/* define chunk processing function */
|
|
67
71
|
const processChunk = (chunkData: Float32Array) => {
|
|
68
72
|
/* update internal audio sample sliding window */
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
newWindow.set(chunkData, sampleWindowSize - chunkData.length)
|
|
72
|
-
sampleWindow = newWindow
|
|
73
|
+
sampleWindow.set(sampleWindow.subarray(chunkData.length), 0)
|
|
74
|
+
sampleWindow.set(chunkData, sampleWindowSize - chunkData.length)
|
|
73
75
|
|
|
74
76
|
/* calculate the LUFS-S and RMS metric */
|
|
75
77
|
const audioData = {
|
|
@@ -151,7 +153,8 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
151
153
|
self.chunkBuffer = newBuffer
|
|
152
154
|
|
|
153
155
|
/* pass-through original audio chunk */
|
|
154
|
-
|
|
156
|
+
if (self.params.mode === "filter")
|
|
157
|
+
this.push(chunk)
|
|
155
158
|
callback()
|
|
156
159
|
}
|
|
157
160
|
catch (error) {
|
|
@@ -160,7 +163,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
|
|
|
160
163
|
}
|
|
161
164
|
},
|
|
162
165
|
final (callback) {
|
|
163
|
-
if (self.destroyed) {
|
|
166
|
+
if (self.destroyed || self.params.mode === "sink") {
|
|
164
167
|
callback()
|
|
165
168
|
return
|
|
166
169
|
}
|
|
@@ -123,7 +123,10 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
|
|
|
123
123
|
if (text === "")
|
|
124
124
|
this.log("info", `empty/dummy text received (start: ${data.start}s, duration: ${data.duration.toFixed(2)}s)`)
|
|
125
125
|
else {
|
|
126
|
-
this.log("info", `text received (start: ${data.start}s,
|
|
126
|
+
this.log("info", `text received (start: ${data.start}s, ` +
|
|
127
|
+
`duration: ${data.duration.toFixed(2)}s, ` +
|
|
128
|
+
`kind: ${isFinal ? "final" : "intermediate"}): ` +
|
|
129
|
+
`${text}"`)
|
|
127
130
|
const start = Duration.fromMillis(data.start * 1000).plus(this.timeZeroOffset)
|
|
128
131
|
const end = start.plus({ seconds: data.duration })
|
|
129
132
|
const metas = metastore.fetch(start, end)
|
|
@@ -18,6 +18,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
18
18
|
/* declare official node name */
|
|
19
19
|
public static name = "x2x-trace"
|
|
20
20
|
|
|
21
|
+
/* internal state */
|
|
22
|
+
private destroyed = false
|
|
23
|
+
|
|
21
24
|
/* construct node */
|
|
22
25
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
23
26
|
super(id, cfg, opts, args)
|
|
@@ -26,6 +29,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
26
29
|
this.configure({
|
|
27
30
|
type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
|
|
28
31
|
name: { type: "string", pos: 1, val: "trace" },
|
|
32
|
+
mode: { type: "string", pos: 2, val: "filter", match: /^(?:filter|sink)$/ },
|
|
29
33
|
dashboard: { type: "string", val: "" }
|
|
30
34
|
})
|
|
31
35
|
|
|
@@ -35,7 +39,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
35
39
|
|
|
36
40
|
/* declare node input/output format */
|
|
37
41
|
this.input = this.params.type
|
|
38
|
-
|
|
42
|
+
if (this.params.mode === "filter")
|
|
43
|
+
this.output = this.params.type
|
|
44
|
+
else if (this.params.mode === "sink")
|
|
45
|
+
this.output = "none"
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/* open node */
|
|
@@ -48,6 +55,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
48
55
|
this.log(level, msg)
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
/* clear destruction flag */
|
|
59
|
+
this.destroyed = false
|
|
60
|
+
|
|
51
61
|
/* helper functions for formatting */
|
|
52
62
|
const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
|
|
53
63
|
const fmtMeta = (meta: Map<string, any>) => {
|
|
@@ -74,6 +84,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
74
84
|
highWaterMark: 1,
|
|
75
85
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
76
86
|
let error: Error | undefined
|
|
87
|
+
if (self.destroyed) {
|
|
88
|
+
callback(new Error("stream already destroyed"))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
77
91
|
if (Buffer.isBuffer(chunk.payload)) {
|
|
78
92
|
if (self.params.type === "audio")
|
|
79
93
|
log("debug", fmtChunkBase(chunk) +
|
|
@@ -94,7 +108,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
94
108
|
else
|
|
95
109
|
error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
|
|
96
110
|
}
|
|
97
|
-
if (
|
|
111
|
+
if (self.params.mode === "sink")
|
|
112
|
+
callback()
|
|
113
|
+
else if (error !== undefined)
|
|
98
114
|
callback(error)
|
|
99
115
|
else {
|
|
100
116
|
this.push(chunk, encoding)
|
|
@@ -102,6 +118,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
102
118
|
}
|
|
103
119
|
},
|
|
104
120
|
final (callback) {
|
|
121
|
+
if (self.destroyed || self.params.mode === "sink") {
|
|
122
|
+
callback()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
105
125
|
this.push(null)
|
|
106
126
|
callback()
|
|
107
127
|
}
|
|
@@ -115,5 +135,8 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
|
|
|
115
135
|
this.stream.destroy()
|
|
116
136
|
this.stream = null
|
|
117
137
|
}
|
|
138
|
+
|
|
139
|
+
/* indicate destruction */
|
|
140
|
+
this.destroyed = true
|
|
118
141
|
}
|
|
119
142
|
}
|
|
@@ -83,6 +83,84 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
|
|
|
83
83
|
return device
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/* NOTICE: "naudion" actually implements Stream.{Readable,Writable,Duplex}, but
|
|
87
|
+
declares just its sub-interface NodeJS.{Readable,Writable,Duplex}Stream,
|
|
88
|
+
so it is correct to cast it back to Stream.{Readable,Writable,Duplex}
|
|
89
|
+
in the following device stream setup functions! */
|
|
90
|
+
|
|
91
|
+
/* INTERNAL: setup duplex stream */
|
|
92
|
+
private setupDuplexStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
|
|
93
|
+
if (device.maxInputChannels === 0)
|
|
94
|
+
throw new Error(`device "${device.id}" does not have any input channels (required by read/write mode)`)
|
|
95
|
+
if (device.maxOutputChannels === 0)
|
|
96
|
+
throw new Error(`device "${device.id}" does not have any output channels (required by read/write mode)`)
|
|
97
|
+
this.log("info", `resolved "${this.params.device}" to duplex device "${device.id}"`)
|
|
98
|
+
this.io = PortAudio.AudioIO({
|
|
99
|
+
inOptions: {
|
|
100
|
+
deviceId: device.id,
|
|
101
|
+
channelCount: this.config.audioChannels,
|
|
102
|
+
sampleRate: this.config.audioSampleRate,
|
|
103
|
+
sampleFormat: this.config.audioBitDepth,
|
|
104
|
+
highwaterMark
|
|
105
|
+
},
|
|
106
|
+
outOptions: {
|
|
107
|
+
deviceId: device.id,
|
|
108
|
+
channelCount: this.config.audioChannels,
|
|
109
|
+
sampleRate: this.config.audioSampleRate,
|
|
110
|
+
sampleFormat: this.config.audioBitDepth,
|
|
111
|
+
highwaterMark
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
this.stream = this.io as unknown as Stream.Duplex
|
|
115
|
+
|
|
116
|
+
/* convert regular stream into object-mode stream */
|
|
117
|
+
const wrapper1 = util.createTransformStreamForWritableSide()
|
|
118
|
+
const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
|
|
119
|
+
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* INTERNAL: setup input stream */
|
|
123
|
+
private setupInputStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
|
|
124
|
+
if (device.maxInputChannels === 0)
|
|
125
|
+
throw new Error(`device "${device.id}" does not have any input channels (required by read mode)`)
|
|
126
|
+
this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
|
|
127
|
+
this.io = PortAudio.AudioIO({
|
|
128
|
+
inOptions: {
|
|
129
|
+
deviceId: device.id,
|
|
130
|
+
channelCount: this.config.audioChannels,
|
|
131
|
+
sampleRate: this.config.audioSampleRate,
|
|
132
|
+
sampleFormat: this.config.audioBitDepth,
|
|
133
|
+
highwaterMark
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
this.stream = this.io as unknown as Stream.Readable
|
|
137
|
+
|
|
138
|
+
/* convert regular stream into object-mode stream */
|
|
139
|
+
const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
|
|
140
|
+
this.stream = Stream.compose(this.stream, wrapper)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* INTERNAL: setup output stream */
|
|
144
|
+
private setupOutputStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
|
|
145
|
+
if (device.maxOutputChannels === 0)
|
|
146
|
+
throw new Error(`device "${device.id}" does not have any output channels (required by write mode)`)
|
|
147
|
+
this.log("info", `resolved "${this.params.device}" to output device "${device.id}"`)
|
|
148
|
+
this.io = PortAudio.AudioIO({
|
|
149
|
+
outOptions: {
|
|
150
|
+
deviceId: device.id,
|
|
151
|
+
channelCount: this.config.audioChannels,
|
|
152
|
+
sampleRate: this.config.audioSampleRate,
|
|
153
|
+
sampleFormat: this.config.audioBitDepth,
|
|
154
|
+
highwaterMark
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
this.stream = this.io as unknown as Stream.Writable
|
|
158
|
+
|
|
159
|
+
/* convert regular stream into object-mode stream */
|
|
160
|
+
const wrapper = util.createTransformStreamForWritableSide()
|
|
161
|
+
this.stream = Stream.compose(wrapper, this.stream)
|
|
162
|
+
}
|
|
163
|
+
|
|
86
164
|
/* open node */
|
|
87
165
|
async open () {
|
|
88
166
|
if (this.params.device === "")
|
|
@@ -104,107 +182,48 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
|
|
|
104
182
|
(this.config.audioBitDepth / 8)
|
|
105
183
|
) / (1000 / this.params.chunk)
|
|
106
184
|
|
|
107
|
-
/* establish device
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (this.params.mode === "
|
|
113
|
-
|
|
114
|
-
if (device.maxInputChannels === 0)
|
|
115
|
-
throw new Error(`device "${device.id}" does not have any input channels (required by read/write mode)`)
|
|
116
|
-
if (device.maxOutputChannels === 0)
|
|
117
|
-
throw new Error(`device "${device.id}" does not have any output channels (required by read/write mode)`)
|
|
118
|
-
this.log("info", `resolved "${this.params.device}" to duplex device "${device.id}"`)
|
|
119
|
-
this.io = PortAudio.AudioIO({
|
|
120
|
-
inOptions: {
|
|
121
|
-
deviceId: device.id,
|
|
122
|
-
channelCount: this.config.audioChannels,
|
|
123
|
-
sampleRate: this.config.audioSampleRate,
|
|
124
|
-
sampleFormat: this.config.audioBitDepth,
|
|
125
|
-
highwaterMark
|
|
126
|
-
},
|
|
127
|
-
outOptions: {
|
|
128
|
-
deviceId: device.id,
|
|
129
|
-
channelCount: this.config.audioChannels,
|
|
130
|
-
sampleRate: this.config.audioSampleRate,
|
|
131
|
-
sampleFormat: this.config.audioBitDepth,
|
|
132
|
-
highwaterMark
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
this.stream = this.io as unknown as Stream.Duplex
|
|
136
|
-
|
|
137
|
-
/* convert regular stream into object-mode stream */
|
|
138
|
-
const wrapper1 = util.createTransformStreamForWritableSide()
|
|
139
|
-
const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
|
|
140
|
-
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
141
|
-
}
|
|
142
|
-
else if (this.params.mode === "r") {
|
|
143
|
-
/* input device */
|
|
144
|
-
if (device.maxInputChannels === 0)
|
|
145
|
-
throw new Error(`device "${device.id}" does not have any input channels (required by read mode)`)
|
|
146
|
-
this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
|
|
147
|
-
this.io = PortAudio.AudioIO({
|
|
148
|
-
inOptions: {
|
|
149
|
-
deviceId: device.id,
|
|
150
|
-
channelCount: this.config.audioChannels,
|
|
151
|
-
sampleRate: this.config.audioSampleRate,
|
|
152
|
-
sampleFormat: this.config.audioBitDepth,
|
|
153
|
-
highwaterMark
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
this.stream = this.io as unknown as Stream.Readable
|
|
157
|
-
|
|
158
|
-
/* convert regular stream into object-mode stream */
|
|
159
|
-
const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
|
|
160
|
-
this.stream = Stream.compose(this.stream, wrapper)
|
|
161
|
-
}
|
|
162
|
-
else if (this.params.mode === "w") {
|
|
163
|
-
/* output device */
|
|
164
|
-
if (device.maxOutputChannels === 0)
|
|
165
|
-
throw new Error(`device "${device.id}" does not have any output channels (required by write mode)`)
|
|
166
|
-
this.log("info", `resolved "${this.params.device}" to output device "${device.id}"`)
|
|
167
|
-
this.io = PortAudio.AudioIO({
|
|
168
|
-
outOptions: {
|
|
169
|
-
deviceId: device.id,
|
|
170
|
-
channelCount: this.config.audioChannels,
|
|
171
|
-
sampleRate: this.config.audioSampleRate,
|
|
172
|
-
sampleFormat: this.config.audioBitDepth,
|
|
173
|
-
highwaterMark
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
this.stream = this.io as unknown as Stream.Writable
|
|
177
|
-
|
|
178
|
-
/* convert regular stream into object-mode stream */
|
|
179
|
-
const wrapper = util.createTransformStreamForWritableSide()
|
|
180
|
-
this.stream = Stream.compose(wrapper, this.stream)
|
|
181
|
-
}
|
|
182
|
-
else
|
|
183
|
-
throw new Error(`device "${device.id}" does not have any input or output channels`)
|
|
185
|
+
/* establish device stream */
|
|
186
|
+
if (this.params.mode === "rw")
|
|
187
|
+
this.setupDuplexStream(device, highwaterMark)
|
|
188
|
+
else if (this.params.mode === "r")
|
|
189
|
+
this.setupInputStream(device, highwaterMark)
|
|
190
|
+
else if (this.params.mode === "w")
|
|
191
|
+
this.setupOutputStream(device, highwaterMark)
|
|
184
192
|
|
|
185
193
|
/* pass-through PortAudio errors */
|
|
186
|
-
this.io
|
|
194
|
+
this.io!.on("error", (err) => {
|
|
187
195
|
this.emit("error", err)
|
|
188
196
|
})
|
|
189
197
|
|
|
190
198
|
/* start PortAudio */
|
|
191
|
-
this.io
|
|
199
|
+
this.io!.start()
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
/* close node */
|
|
195
203
|
async close () {
|
|
196
204
|
/* shutdown PortAudio */
|
|
197
205
|
if (this.io !== null) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
await
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
const catchHandler = (err: unknown) => {
|
|
207
|
+
const error = util.ensureError(err)
|
|
208
|
+
if (!error.message.match(/AudioIO Quit expects 1 argument/))
|
|
209
|
+
throw error
|
|
210
|
+
}
|
|
211
|
+
await Promise.race([
|
|
212
|
+
util.timeoutPromise(2 * 1000, "PortAudio abort timeout"),
|
|
213
|
+
new Promise<void>((resolve) => {
|
|
214
|
+
this.io!.abort(() => {
|
|
215
|
+
resolve()
|
|
216
|
+
})
|
|
217
|
+
}).catch(catchHandler)
|
|
218
|
+
])
|
|
219
|
+
await Promise.race([
|
|
220
|
+
util.timeoutPromise(2 * 1000, "PortAudio quit timeout"),
|
|
221
|
+
new Promise<void>((resolve) => {
|
|
222
|
+
this.io!.quit(() => {
|
|
223
|
+
resolve()
|
|
224
|
+
})
|
|
225
|
+
}).catch(catchHandler)
|
|
226
|
+
])
|
|
208
227
|
this.io = null
|
|
209
228
|
}
|
|
210
229
|
}
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/* helper function for promise-based timeout */
|
|
8
|
+
export function timeoutPromise (duration: number = 10 * 1000, info = "timeout") {
|
|
9
|
+
return new Promise<void>((resolve, reject) => {
|
|
10
|
+
setTimeout(() => { reject(new Error(info)) }, duration)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/* helper function for retrieving an Error object */
|
|
8
15
|
export function ensureError (error: unknown, prefix?: string, debug = false): Error {
|
|
9
16
|
if (error instanceof Error && prefix === undefined && debug === false)
|