speechflow
Version:
Speech Processing Flow Graph
166 lines (147 loc) • 6.34 kB
text/typescript
/*
** SpeechFlow - Speech Processing Flow Graph
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
*/
/* standard dependencies */
import Stream from "node:stream"
import { Worker } from "node:worker_threads"
import { resolve } from "node:path"
/* internal dependencies */
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
import * as util from "./speechflow-util"
/* SpeechFlow node for RNNoise based noise suppression in audio-to-audio passing */
export default class SpeechFlowNodeA2ARNNoise extends SpeechFlowNode {
/* declare official node name */
public static name = "a2a-rnnoise"
/* internal state */
private destroyed = false
private sampleSize = 480 /* = 10ms at 48KHz, as required by RNNoise! */
private worker: Worker | null = null
/* construct node */
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
super(id, cfg, opts, args)
/* declare node configuration parameters */
this.configure({})
/* declare node input/output format */
this.input = "audio"
this.output = "audio"
}
/* open node */
async open () {
/* clear destruction flag */
this.destroyed = false
/* initialize worker */
this.worker = new Worker(resolve(__dirname, "speechflow-node-a2a-rnnoise-wt.js"))
this.worker.on("error", (err) => {
this.log("error", `RNNoise worker thread error: ${err}`)
this.stream?.emit("error", err)
})
this.worker.on("exit", (code) => {
if (code !== 0)
this.log("error", `RNNoise worker thread exited with error code ${code}`)
else
this.log("info", `RNNoise worker thread exited with regular code ${code}`)
})
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("RNNoise worker thread initialization timeout"))
}, 5000)
this.worker!.once("message", (msg: any) => {
clearTimeout(timeout)
if (typeof msg === "object" && msg !== null && msg.type === "ready")
resolve()
else if (typeof msg === "object" && msg !== null && msg.type === "failed")
reject(new Error(msg.message ?? "RNNoise worker thread initialization failed"))
else
reject(new Error(`RNNoise worker thread sent unexpected message on startup`))
})
this.worker!.once("error", (err) => {
clearTimeout(timeout)
reject(err)
})
})
/* receive message from worker */
const pending = new Map<string, (arr: Int16Array<ArrayBuffer>) => void>()
this.worker.on("message", (msg: any) => {
if (typeof msg === "object" && msg !== null && msg.type === "process-done") {
const cb = pending.get(msg.id)
pending.delete(msg.id)
if (cb)
cb(msg.data)
else
this.log("warning", `RNNoise worker thread sent back unexpected id: ${msg.id}`)
}
else
this.log("warning", `RNNoise worker thread sent unexpected message: ${JSON.stringify(msg)}`)
})
/* send message to worker */
let seq = 0
const workerProcessSegment = async (segment: Int16Array<ArrayBuffer>) => {
if (this.destroyed)
return segment
const id = `${seq++}`
return new Promise<Int16Array<ArrayBuffer>>((resolve) => {
pending.set(id, (segment: Int16Array<ArrayBuffer>) => { resolve(segment) })
this.worker!.postMessage({ type: "process", id, data: segment }, [ segment.buffer ])
})
}
/* establish a transform stream */
const self = this
this.stream = new Stream.Transform({
readableObjectMode: true,
writableObjectMode: true,
decodeStrings: false,
transform (chunk: SpeechFlowChunk & { payload: Buffer }, encoding, callback) {
if (self.destroyed) {
callback(new Error("stream already destroyed"))
return
}
if (!Buffer.isBuffer(chunk.payload))
callback(new Error("invalid chunk payload type"))
else {
/* convert Buffer into Int16Array */
const payload = util.convertBufToI16(chunk.payload)
/* process Int16Array in necessary segments */
util.processInt16ArrayInSegments(payload, self.sampleSize, (segment) =>
workerProcessSegment(segment)
).then((payload: Int16Array<ArrayBuffer>) => {
/* convert Int16Array into Buffer */
const buf = util.convertI16ToBuf(payload)
/* update chunk */
chunk.payload = buf
/* forward updated chunk */
this.push(chunk)
callback()
}).catch((err: Error) => {
self.log("warning", `processing of chunk failed: ${err}`)
callback(err)
})
}
},
final (callback) {
if (self.destroyed) {
callback()
return
}
this.push(null)
callback()
}
})
}
/* close node */
async close () {
/* indicate destruction */
this.destroyed = true
/* shutdown worker */
if (this.worker !== null) {
this.worker.terminate()
this.worker = null
}
/* close stream */
if (this.stream !== null) {
this.stream.destroy()
this.stream = null
}
}
}