speechflow
Version:
Speech Processing Flow Graph
132 lines (116 loc) • 4.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"
/* internal dependencies */
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
/* the type of muting */
type MuteMode =
"none" | /* not muted */
"silenced" | /* muted by changing audio samples to silence */
"unplugged" /* muted by unplugging the audio sample flow */
/* SpeechFlow node for muting in audio-to-audio passing */
export default class SpeechFlowNodeA2AMute extends SpeechFlowNode {
/* declare official node name */
public static name = "a2a-mute"
/* internal state */
private muteMode: MuteMode = "none"
private destroyed = false
/* 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"
}
/* receive external request */
async receiveRequest (params: any[]) {
if (this.destroyed)
throw new Error("mute: node already destroyed")
try {
if (params.length === 2 && params[0] === "mode") {
if (typeof params[1] !== "string" ||
!params[1].match(/^(?:none|silenced|unplugged)$/))
throw new Error("mute: invalid mode argument in external request")
const muteMode = params[1] as MuteMode
this.setMuteMode(muteMode)
this.sendResponse([ "mute", "mode", muteMode ])
}
else
throw new Error("mute: invalid arguments in external request")
}
catch (error) {
this.log("error", `receive request error: ${error}`)
throw error
}
}
/* change mute mode */
setMuteMode (mode: MuteMode) {
if (this.destroyed) {
this.log("warning", "attempted to set mute mode on destroyed node")
return
}
this.log("info", `setting mute mode to "${mode}"`)
this.muteMode = mode
}
/* open node */
async open () {
/* clear destruction flag */
this.destroyed = false
/* establish a transform stream */
const self = this
this.stream = new Stream.Transform({
readableObjectMode: true,
writableObjectMode: true,
decodeStrings: false,
transform (chunk: SpeechFlowChunk, 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 if (self.muteMode === "unplugged")
/* pass-through nothing */
callback()
else if (self.muteMode === "silenced") {
/* pass-through a silenced chunk */
const chunkSilenced = chunk.clone()
chunkSilenced.meta.set("muted", true)
const buffer = chunkSilenced.payload as Buffer
buffer.fill(0)
this.push(chunkSilenced)
callback()
}
else {
/* pass-through original chunk */
this.push(chunk)
callback()
}
},
final (callback) {
if (self.destroyed) {
callback()
return
}
this.push(null)
callback()
}
})
}
/* close node */
async close () {
/* indicate destruction */
this.destroyed = true
/* close stream */
if (this.stream !== null) {
this.stream.destroy()
this.stream = null
}
}
}