speechflow
Version:
Speech Processing Flow Graph
156 lines • 6.75 kB
JavaScript
;
/*
** 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>
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* standard dependencies */
const node_path_1 = __importDefault(require("node:path"));
const node_fs_1 = __importDefault(require("node:fs"));
const node_stream_1 = __importDefault(require("node:stream"));
/* external dependencies */
const speex_preprocess_wasm_1 = require("@sapphi-red/speex-preprocess-wasm");
/* internal dependencies */
const speechflow_node_1 = __importDefault(require("./speechflow-node"));
const util = __importStar(require("./speechflow-util"));
/* SpeechFlow node for Speex based noise suppression in audio-to-audio passing */
class SpeechFlowNodeA2ASpeex extends speechflow_node_1.default {
/* declare official node name */
static name = "a2a-speex";
/* internal state */
destroyed = false;
sampleSize = 480; /* = 10ms at 48KHz */
speexProcessor = null;
/* construct node */
constructor(id, cfg, opts, args) {
super(id, cfg, opts, args);
/* declare node configuration parameters */
this.configure({
attenuate: { type: "number", val: -18, pos: 0, match: (n) => n >= -60 && n <= 0 },
});
/* declare node input/output format */
this.input = "audio";
this.output = "audio";
}
/* open node */
async open() {
/* clear destruction flag */
this.destroyed = false;
/* validate sample rate compatibility */
if (this.config.audioSampleRate !== 48000)
throw new Error(`Speex node requires 48KHz sample rate, got ${this.config.audioSampleRate}Hz`);
/* initialize and configure Speex pre-processor */
const wasmBinary = await node_fs_1.default.promises.readFile(node_path_1.default.join(__dirname, "../node_modules/@sapphi-red/speex-preprocess-wasm/dist/speex.wasm"));
const speexModule = await (0, speex_preprocess_wasm_1.loadSpeexModule)({
wasmBinary: wasmBinary.buffer
});
this.speexProcessor = new speex_preprocess_wasm_1.SpeexPreprocessor(speexModule, this.sampleSize, this.config.audioSampleRate);
this.speexProcessor.denoise = true;
this.speexProcessor.noiseSuppress = this.params.attenuate;
this.speexProcessor.agc = false;
this.speexProcessor.vad = false;
this.speexProcessor.echoSuppress = 0;
this.speexProcessor.echoSuppressActive = 0;
/* establish a transform stream */
const self = this;
this.stream = new node_stream_1.default.Transform({
readableObjectMode: true,
writableObjectMode: true,
decodeStrings: false,
transform(chunk, 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 fixed-size segments */
util.processInt16ArrayInSegments(payload, self.sampleSize, (segment) => {
if (self.destroyed)
throw new Error("stream already destroyed");
self.speexProcessor?.processInt16(segment);
return Promise.resolve(segment);
}).then((payload) => {
if (self.destroyed)
throw new Error("stream already destroyed");
/* convert Int16Array back into Buffer */
const buf = util.convertI16ToBuf(payload);
/* update chunk */
chunk.payload = buf;
/* forward updated chunk */
this.push(chunk);
callback();
}).catch((err) => {
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;
/* destroy processor */
if (this.speexProcessor !== null) {
this.speexProcessor.destroy();
this.speexProcessor = null;
}
/* close stream */
if (this.stream !== null) {
this.stream.destroy();
this.stream = null;
}
}
}
exports.default = SpeechFlowNodeA2ASpeex;
//# sourceMappingURL=speechflow-node-a2a-speex.js.map