speechflow
Version:
Speech Processing Flow Graph
131 lines (109 loc) • 5.18 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>
*/
import * as util from "./speechflow-util"
/* downward expander with soft knee */
class ExpanderProcessor extends AudioWorkletProcessor {
/* internal state */
private env: number[] = []
private sampleRate: number
/* eslint no-undef: off */
static get parameterDescriptors(): AudioParamDescriptor[] {
return [
{ name: "threshold", defaultValue: -45, minValue: -100, maxValue: 0, automationRate: "k-rate" }, // dBFS
{ name: "floor", defaultValue: -64, minValue: -100, maxValue: 0, automationRate: "k-rate" }, // dBFS minimum output level
{ name: "ratio", defaultValue: 4.0, minValue: 1.0, maxValue: 20, automationRate: "k-rate" }, // expansion ratio
{ name: "attack", defaultValue: 0.010, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
{ name: "release", defaultValue: 0.050, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
{ name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" }, // dB
{ name: "makeup", defaultValue: 0.0, minValue: -24, maxValue: 24, automationRate: "k-rate" } // dB
]
}
/* class constructor for custom option processing */
constructor (options: any) {
super()
const { sampleRate } = options.processorOptions
this.sampleRate = sampleRate as number
}
/* determine gain difference */
private gainDBFor (levelDB: number, thresholdDB: number, ratio: number, kneeDB: number): number {
/* short-circuit for unreasonable ratio */
if (ratio <= 1.0)
return 0
/* determine thresholds */
const halfKnee = kneeDB * 0.5
const belowKnee = levelDB < (thresholdDB - halfKnee)
const aboveThr = levelDB >= thresholdDB
/* short-circuit for no expansion (above threshold) */
if (aboveThr)
return 0
/* apply soft-knee */
if (kneeDB > 0 && !belowKnee) {
const x = (levelDB - (thresholdDB - halfKnee)) / kneeDB
const idealGainDB = (thresholdDB + (levelDB - thresholdDB) * ratio) - levelDB
return idealGainDB * x * x
}
/* determine target level */
const targetOut = thresholdDB + (levelDB - thresholdDB) / ratio
/* return gain difference */
return targetOut - levelDB
}
/* process a single sample frame */
process(
inputs: Float32Array[][],
outputs: Float32Array[][],
parameters: Record<string, Float32Array>
): boolean {
/* sanity check */
const input = inputs[0]
const output = outputs[0]
if (!input || input.length === 0 || !output)
return true
/* determine number of channels */
const nCh = input.length
/* reset envelope array if channel count changed */
if (nCh !== this.env.length)
this.env = []
/* initially just copy input to output (pass-through) */
for (let c = 0; c < output.length; c++) {
if (!output[c] || !input[c])
continue
output[c].set(input[c])
}
/* fetch parameters */
const thresholdDB = parameters["threshold"][0]
const floorDB = parameters["floor"][0]
const ratio = parameters["ratio"][0]
const kneeDB = parameters["knee"][0]
const attackS = Math.max(parameters["attack"][0], 1 / this.sampleRate)
const releaseS = Math.max(parameters["release"][0], 1 / this.sampleRate)
const makeupDB = parameters["makeup"][0]
/* update envelope per channel */
for (let ch = 0; ch < nCh; ch++)
this.env[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
/* determine linear value from decibel makeup value */
const makeUpLin = util.dB2lin(makeupDB)
/* iterate over all channels */
for (let ch = 0; ch < nCh; ch++) {
const levelDB = util.lin2dB(this.env[ch])
const gainDB = this.gainDBFor(levelDB, thresholdDB, ratio, kneeDB)
let gainLin = util.dB2lin(gainDB) * makeUpLin
/* do not attenuate below floor */
const expectedOutLevelDB = levelDB + gainDB + makeupDB
if (expectedOutLevelDB < floorDB) {
const neededLiftDB = floorDB - expectedOutLevelDB
gainLin /= util.dB2lin(neededLiftDB)
}
/* apply gain change to channel */
const inp = input[ch]
const out = output[ch]
for (let i = 0; i < inp.length; i++)
out[i] = inp[i] * gainLin
}
return true
}
}
/* register the new audio nodes */
registerProcessor("expander", ExpanderProcessor)