Add VoxLock target-speaker voice isolation to the speech denoisers (#4085)

VoxLock keeps the voice of one speaker (the caller) and removes the other
voices. It runs in the offline and online speech denoisers next to GTCRN
and DPDFNet, with two ONNX models: an enroll model that runs once on a
voice sample of the caller and a chunk model that runs on each chunk of
the audio and carries the LSTM state.

Without a voice sample, the offline denoiser uses the loudest 3 seconds
in the first 15 seconds of the input, and the online denoiser passes
audio through until it has heard 3 seconds of speech. A voice sample can
be given with --speech-denoiser-voxlock-voice-sample.

The voice sample is optional, so instead of exiting, log a warning and
take the voice sample from the input audio as if none was given.
This commit is contained in:
Arbaz Siddiqui authored and GitHub committed 2026-10-25 08:43:19 +08:00
1 parent c65f321699
commit fa0d247a14
21 files changed
+1901 -3

No files matched your search

@@ -31,3 +31,38 @@ $EXE \
--output-wav=./enhanced_speech_16k.wav
rm ./gtcrn_simple.onnx
log "------------------------------------------------------------"
log "Run voxlock"
log "------------------------------------------------------------"
voxlock_url=https://github.com/arbazsiddiqui/voxlock/releases/download/sherpa-onnx-models
download $voxlock_url/voxlock_enroll.onnx
download $voxlock_url/voxlock_chunk_int8.onnx
download $voxlock_url/voxlock_test_call.wav
download $voxlock_url/voxlock_test_voice_sample.wav
$EXE \
--debug=1 \
--speech-denoiser-voxlock-enroll-model=./voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=./voxlock_chunk_int8.onnx \
--input-wav=./voxlock_test_call.wav \
--output-wav=./voxlock_enhanced_speech_16k.wav
$EXE \
--speech-denoiser-voxlock-enroll-model=./voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=./voxlock_chunk_int8.onnx \
--speech-denoiser-voxlock-voice-sample=./voxlock_test_voice_sample.wav \
--input-wav=./voxlock_test_call.wav \
--output-wav=./voxlock_enhanced_speech_with_voice_sample_16k.wav
ONLINE_EXE=${EXE/offline-denoiser/online-denoiser}
which $ONLINE_EXE
$ONLINE_EXE \
--speech-denoiser-voxlock-enroll-model=./voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=./voxlock_chunk_int8.onnx \
--chunk-duration-ms=80 \
--input-wav=./voxlock_test_call.wav \
--output-wav=./voxlock_streaming_enhanced_speech_16k.wav
rm ./voxlock_enroll.onnx ./voxlock_chunk_int8.onnx
+1 -1
View File
@@ -106,7 +106,7 @@ This repository supports running the following functions **locally**
- Spoken language identification
- Audio tagging
- VAD (e.g., [silero-vad][silero-vad])
- Speech enhancement (e.g., [gtcrn][gtcrn], [DPDFNet](https://github.com/ceva-ip/DPDFNet))
- Speech enhancement (e.g., [gtcrn][gtcrn], [DPDFNet](https://github.com/ceva-ip/DPDFNet), [VoxLock](https://github.com/arbazsiddiqui/voxlock))
- Keyword spotting
- Source separation (e.g., [spleeter][spleeter], [UVR][UVR])
+30
View File
@@ -0,0 +1,30 @@
# Introduction
This folder contains scripts for adding meta data to the
[VoxLock](https://github.com/arbazsiddiqui/voxlock) ONNX models from
<https://huggingface.co/arbazsiddiqui/voxlock/tree/main/onnx>.
VoxLock is a target-speaker voice isolation model. It keeps the voice of one
speaker, e.g., the caller in a phone call, and removes the other voices.
It has two models:
- `voxlock_enroll.onnx` runs once on a voice sample of the caller
(exactly 3 seconds at 16 kHz)
- `voxlock_chunk*.onnx` runs on each chunk of the audio and carries the
LSTM state from chunk to chunk
| Precision | Enroll model | Chunk model |
|---|---|---|
| int8 (LSTMs in 8-bit) | `voxlock_enroll.onnx` | `voxlock_chunk_int8.onnx` |
| int8-full (LSTMs and matrix multiplies in 8-bit) | `voxlock_enroll.onnx` | `voxlock_chunk_int8_full.onnx` |
| fp16 (float16 weights) | `voxlock_enroll_fp16.onnx` | `voxlock_chunk_fp16.onnx` |
| fp32 | `voxlock_enroll.onnx` | `voxlock_chunk.onnx` |
The models with meta data can be downloaded from
<https://github.com/arbazsiddiqui/voxlock/releases/tag/sherpa-onnx-models>
- [./run.sh](./run.sh) downloads the models and runs
[./add_meta_data.py](./add_meta_data.py), which also checks that the outputs
of each model do not change
- [./test.py](./test.py) compares the output of `sherpa-onnx-offline-denoiser`
with the [voxlock](https://pypi.org/project/voxlock/) Python package
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# Copyright 2026 Arbaz Siddiqui
"""
Add meta data to the VoxLock ONNX models from
https://huggingface.co/arbazsiddiqui/voxlock/tree/main/onnx
enroll.onnx runs once on a voice sample of the caller:
NodeArg(name='enroll', type='tensor(float)', shape=[1, 'Te'])
-----
NodeArg(name='enroll_mag', type='tensor(float)', shape=[1, 257, 'Fe'])
NodeArg(name='context', type='tensor(float)', shape=[1, 512, 'Fc'])
chunk.onnx runs on each chunk of the call and carries the LSTM state:
NodeArg(name='wav', type='tensor(float)', shape=[1, 'S'])
NodeArg(name='enroll_mag', type='tensor(float)', shape=[1, 257, 'Fe'])
NodeArg(name='context', type='tensor(float)', shape=[1, 512, 'Fc'])
NodeArg(name='h', type='tensor(float)', shape=[6, 32, 256])
NodeArg(name='c', type='tensor(float)', shape=[6, 32, 256])
-----
NodeArg(name='frames', type='tensor(float)', shape=[1, 'K', 512])
NodeArg(name='h_out', type='tensor(float)', shape=[6, 32, 256])
NodeArg(name='c_out', type='tensor(float)', shape=[6, 32, 256])
S = (K - 1) * hop_length + n_fft. Each output frame is the inverse DFT of
one enhanced STFT frame times the synthesis window. The caller overlap-adds
the frames and divides by the summed squared window.
"""
import argparse
from pathlib import Path
import numpy as np
import onnx
import onnxruntime as ort
# Weights of VoxLock 1.1.1
HF_REVISION = "a50f4e1d109301d4b6f61dec57ebb1a61e7d8f03"
# (input file, output file, role, precision)
MODELS = [
("enroll.onnx", "voxlock_enroll.onnx", "enroll", "fp32"),
("chunk.onnx", "voxlock_chunk.onnx", "chunk", "fp32"),
("chunk_int8.onnx", "voxlock_chunk_int8.onnx", "chunk", "int8"),
("chunk_int8_full.onnx", "voxlock_chunk_int8_full.onnx", "chunk", "int8-full"),
("enroll_fp16.onnx", "voxlock_enroll_fp16.onnx", "enroll", "fp16"),
("chunk_fp16.onnx", "voxlock_chunk_fp16.onnx", "chunk", "fp16"),
]
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--in-dir",
type=str,
default="./onnx",
help="Directory with the ONNX files downloaded from Hugging Face",
)
parser.add_argument(
"--out-dir",
type=str,
default="./models",
help="Directory to save the ONNX files with meta data",
)
return parser.parse_args()
def get_meta_data(role: str, precision: str):
meta_data = {
"model_type": "voxlock",
"model_role": role,
"version": 1,
"model_version": "1.1.1",
"precision": precision,
"sample_rate": 16000,
"n_fft": 512,
"hop_length": 128,
"window_length": 512,
"window_type": "hann",
"num_freq_bins": 257,
"context_dim": 512,
# The chunk model accepts only the outputs of the enroll model for a
# voice sample of exactly this length
"voice_sample_seconds": 3,
"model_url": "https://huggingface.co/arbazsiddiqui/voxlock",
"hf_revision": HF_REVISION,
"code_url": "https://github.com/arbazsiddiqui/voxlock",
"license": "Apache-2.0",
"comment": "VoxLock target-speaker voice isolation. "
"The enroll model runs once on 3 seconds of the caller's voice; "
"the chunk model runs on each chunk of the call.",
}
if role == "chunk":
meta_data["state_shape"] = "6,32,256"
meta_data["frames_per_chunk"] = 100
# The network returns the caller at about -2x its level in the input
meta_data["output_gain"] = -0.5
return meta_data
def add_meta_data(in_filename: str, out_filename: str, meta_data):
model = onnx.load(in_filename)
# Keep the keys onnxruntime.quantization added, e.g., onnx.infer
existing = {p.key for p in model.metadata_props}
for key in meta_data:
assert key not in existing, (in_filename, key)
for key, value in meta_data.items():
meta = model.metadata_props.add()
meta.key = key
meta.value = str(value)
onnx.save(model, out_filename)
def make_inputs(sess, rng):
"""Random inputs for a session; enroll_mag and context come from enroll."""
names = [i.name for i in sess.get_inputs()]
if names == ["enroll"]:
return {"enroll": (rng.standard_normal((1, 48000)) * 0.1).astype(np.float32)}
num_frames = 100
return {
"wav": (rng.standard_normal((1, (num_frames - 1) * 128 + 512)) * 0.1).astype(
np.float32
),
"enroll_mag": np.abs(rng.standard_normal((1, 257, 376))).astype(np.float32),
"context": rng.standard_normal((1, 512, 298)).astype(np.float32),
"h": (rng.standard_normal((6, 32, 256)) * 0.1).astype(np.float32),
"c": (rng.standard_normal((6, 32, 256)) * 0.1).astype(np.float32),
}
def check(in_filename: str, out_filename: str, meta_data):
opts = ort.SessionOptions()
opts.log_severity_level = 3
opts.intra_op_num_threads = 1
a = ort.InferenceSession(in_filename, opts, providers=["CPUExecutionProvider"])
b = ort.InferenceSession(out_filename, opts, providers=["CPUExecutionProvider"])
got = b.get_modelmeta().custom_metadata_map
for key, value in meta_data.items():
assert got[key] == str(value), (key, got[key], value)
inputs = make_inputs(a, np.random.default_rng(0))
for x, y in zip(a.run(None, inputs), b.run(None, inputs)):
assert np.array_equal(x, y), out_filename
def main():
args = get_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
for in_name, out_name, role, precision in MODELS:
in_filename = str(Path(args.in_dir) / in_name)
out_filename = str(out_dir / out_name)
meta_data = get_meta_data(role, precision)
add_meta_data(in_filename, out_filename, meta_data)
check(in_filename, out_filename, meta_data)
print(f"{in_filename} -> {out_filename}: meta data added, outputs unchanged")
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Copyright 2026 Arbaz Siddiqui
set -ex
# Weights of VoxLock 1.1.1
revision=a50f4e1d109301d4b6f61dec57ebb1a61e7d8f03
models=(
enroll.onnx
chunk.onnx
chunk_int8.onnx
chunk_int8_full.onnx
enroll_fp16.onnx
chunk_fp16.onnx
)
mkdir -p onnx
for m in ${models[@]}; do
if [ ! -f onnx/$m ]; then
curl -fSL --output onnx/$m.tmp https://huggingface.co/arbazsiddiqui/voxlock/resolve/$revision/onnx/$m
mv onnx/$m.tmp onnx/$m
fi
done
python3 ./add_meta_data.py --in-dir ./onnx --out-dir ./models
ls -lh models
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# Copyright 2026 Arbaz Siddiqui
"""
Compare the output of sherpa-onnx-offline-denoiser with the reference
implementation in the voxlock Python package (pip install voxlock==1.1.1).
./build/bin/sherpa-onnx-offline-denoiser \
--speech-denoiser-voxlock-enroll-model=./models/voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=./models/voxlock_chunk_int8.onnx \
--input-wav=./voxlock_test_call.wav \
--output-wav=./sherpa-onnx.wav
python3 ./test.py \
--precision int8 \
--input-wav ./voxlock_test_call.wav \
--sherpa-onnx-wav ./sherpa-onnx.wav
The input wave must be 16 kHz; sherpa-onnx and the voxlock package use
different resamplers.
"""
import argparse
import tempfile
from pathlib import Path
import numpy as np
import soundfile as sf
from voxlock import VoxLock
from voxlock.core import PRECISIONS
# precision: (enroll model, chunk model) in ./models
MODELS = {
"fp32": ("voxlock_enroll.onnx", "voxlock_chunk.onnx"),
"int8": ("voxlock_enroll.onnx", "voxlock_chunk_int8.onnx"),
"int8-full": ("voxlock_enroll.onnx", "voxlock_chunk_int8_full.onnx"),
"fp16": ("voxlock_enroll_fp16.onnx", "voxlock_chunk_fp16.onnx"),
}
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("--model-dir", type=str, default="./models")
parser.add_argument("--precision", type=str, default="int8", choices=MODELS)
parser.add_argument("--input-wav", type=str, required=True)
parser.add_argument("--sherpa-onnx-wav", type=str, required=True)
parser.add_argument(
"--voice-sample",
type=str,
default="",
help="The --speech-denoiser-voxlock-voice-sample given to sherpa-onnx. "
"It must be exactly 3 seconds long for the voxlock package.",
)
return parser.parse_args()
def main():
args = get_args()
x, sample_rate = sf.read(args.input_wav, dtype="float32", always_2d=True)
assert sample_rate == 16000, sample_rate
x = np.ascontiguousarray(x[:, 0])
sample = None
if args.voice_sample:
sample, sample_rate = sf.read(args.voice_sample, dtype="float32")
assert sample_rate == 16000, sample_rate
with tempfile.TemporaryDirectory() as d:
# VoxLock(model_dir) expects the file names in voxlock.core.PRECISIONS
for src, dst in zip(MODELS[args.precision], PRECISIONS[args.precision]):
(Path(d) / f"{dst}.onnx").symlink_to((Path(args.model_dir) / src).resolve())
expected = VoxLock(d, precision=args.precision, threads=1).isolate(
x, sample=sample
)
# sherpa-onnx saves 16-bit samples, scaled by 32767
expected = np.trunc(np.clip(expected * 32767, -32768, 32767)) / 32768
got, sample_rate = sf.read(args.sherpa_onnx_wav, dtype="float32")
assert sample_rate == 16000, sample_rate
assert len(got) == len(expected), (len(got), len(expected))
diff = np.abs(got.astype(np.float64) - expected)
err = (diff**2).sum()
if err == 0:
print("max abs diff: 0, the outputs are identical")
return
snr = 10 * np.log10((expected.astype(np.float64) ** 2).sum() / err)
print(f"max abs diff: {diff.max():.6f}, SNR: {snr:.1f} dB")
if __name__ == "__main__":
main()
+2
View File
@@ -326,6 +326,8 @@ list(APPEND sources
offline-speech-denoiser-gtcrn-model.cc
offline-speech-denoiser-impl.cc
offline-speech-denoiser-model-config.cc
offline-speech-denoiser-voxlock-model-config.cc
offline-speech-denoiser-voxlock-model.cc
offline-speech-denoiser.cc
online-speech-denoiser-impl.cc
online-speech-denoiser.cc
@@ -17,6 +17,7 @@
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-dpdfnet-impl.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-gtcrn-impl.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-impl.h"
namespace sherpa_onnx {
@@ -24,11 +25,15 @@ std::unique_ptr<OfflineSpeechDenoiserImpl> OfflineSpeechDenoiserImpl::Create(
const OfflineSpeechDenoiserConfig &config) {
const bool has_gtcrn = !config.model.gtcrn.model.empty();
const bool has_dpdfnet = !config.model.dpdfnet.model.empty();
const bool has_voxlock = !config.model.voxlock.enroll_model.empty() ||
!config.model.voxlock.chunk_model.empty();
if (has_gtcrn) {
return std::make_unique<OfflineSpeechDenoiserGtcrnImpl>(config);
} else if (has_dpdfnet) {
return std::make_unique<OfflineSpeechDenoiserDpdfNetImpl>(config);
} else if (has_voxlock) {
return std::make_unique<OfflineSpeechDenoiserVoxLockImpl>(config);
}
SHERPA_ONNX_LOGE("Please provide one speech denoising model.");
@@ -40,11 +45,15 @@ std::unique_ptr<OfflineSpeechDenoiserImpl> OfflineSpeechDenoiserImpl::Create(
Manager *mgr, const OfflineSpeechDenoiserConfig &config) {
const bool has_gtcrn = !config.model.gtcrn.model.empty();
const bool has_dpdfnet = !config.model.dpdfnet.model.empty();
const bool has_voxlock = !config.model.voxlock.enroll_model.empty() ||
!config.model.voxlock.chunk_model.empty();
if (has_gtcrn) {
return std::make_unique<OfflineSpeechDenoiserGtcrnImpl>(mgr, config);
} else if (has_dpdfnet) {
return std::make_unique<OfflineSpeechDenoiserDpdfNetImpl>(mgr, config);
} else if (has_voxlock) {
return std::make_unique<OfflineSpeechDenoiserVoxLockImpl>(mgr, config);
}
SHERPA_ONNX_LOGE("Please provide one speech denoising model.");
@@ -14,6 +14,7 @@ namespace sherpa_onnx {
void OfflineSpeechDenoiserModelConfig::Register(ParseOptions *po) {
gtcrn.Register(po);
dpdfnet.Register(po);
voxlock.Register(po);
po->Register("num-threads", &num_threads,
"Number of threads to run the neural network");
@@ -26,7 +27,10 @@ void OfflineSpeechDenoiserModelConfig::Register(ParseOptions *po) {
}
bool OfflineSpeechDenoiserModelConfig::Validate() const {
if (gtcrn.model.empty() && dpdfnet.model.empty()) {
const bool has_voxlock =
!voxlock.enroll_model.empty() || !voxlock.chunk_model.empty();
if (gtcrn.model.empty() && dpdfnet.model.empty() && !has_voxlock) {
SHERPA_ONNX_LOGE("Please provide a speech denoising model.");
return false;
}
@@ -35,7 +39,11 @@ bool OfflineSpeechDenoiserModelConfig::Validate() const {
return gtcrn.Validate();
}
return dpdfnet.Validate();
if (!dpdfnet.model.empty()) {
return dpdfnet.Validate();
}
return voxlock.Validate();
}
std::string OfflineSpeechDenoiserModelConfig::ToString() const {
@@ -44,6 +52,7 @@ std::string OfflineSpeechDenoiserModelConfig::ToString() const {
os << "OfflineSpeechDenoiserModelConfig(";
os << "gtcrn=" << gtcrn.ToString() << ", ";
os << "dpdfnet=" << dpdfnet.ToString() << ", ";
os << "voxlock=" << voxlock.ToString() << ", ";
os << "num_threads=" << num_threads << ", ";
os << "debug=" << (debug ? "True" : "False") << ", ";
os << "provider=\"" << provider << "\")";
@@ -8,6 +8,7 @@
#include "sherpa-onnx/csrc/offline-speech-denoiser-dpdfnet-model-config.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-gtcrn-model-config.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-config.h"
#include "sherpa-onnx/csrc/parse-options.h"
namespace sherpa_onnx {
@@ -15,6 +16,7 @@ namespace sherpa_onnx {
struct OfflineSpeechDenoiserModelConfig {
OfflineSpeechDenoiserGtcrnModelConfig gtcrn;
OfflineSpeechDenoiserDpdfNetModelConfig dpdfnet;
OfflineSpeechDenoiserVoxLockModelConfig voxlock;
int32_t num_threads = 1;
bool debug = false;
@@ -0,0 +1,122 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-impl.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
#define SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
#include <string>
#include <utility>
#include <vector>
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-impl.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser.h"
#include "sherpa-onnx/csrc/speech-denoiser-voxlock-stream.h"
namespace sherpa_onnx {
class OfflineSpeechDenoiserVoxLockImpl : public OfflineSpeechDenoiserImpl {
public:
explicit OfflineSpeechDenoiserVoxLockImpl(
const OfflineSpeechDenoiserConfig &config)
: model_(config.model) {
Init(config);
}
template <typename Manager>
OfflineSpeechDenoiserVoxLockImpl(Manager *mgr,
const OfflineSpeechDenoiserConfig &config)
: model_(mgr, config.model) {
Init(mgr, config);
}
DenoisedAudio Run(const float *samples, int32_t n,
int32_t sample_rate) const override {
const auto &meta = model_.GetMetaData();
std::vector<float> tmp;
auto p = samples;
if (sample_rate != meta.sample_rate) {
SHERPA_ONNX_LOGE(
"Creating a resampler:\n"
" in_sample_rate: %d\n"
" output_sample_rate: %d\n",
sample_rate, meta.sample_rate);
CreateVoxLockResampler(sample_rate, meta.sample_rate)
->Resample(samples, n, true, &tmp);
p = tmp.data();
n = tmp.size();
}
DenoisedAudio denoised_audio;
denoised_audio.sample_rate = meta.sample_rate;
const std::vector<float> *voice_sample = &voice_sample_;
std::vector<float> selected;
if (voice_sample->empty()) {
selected = SelectVoxLockVoiceSample(
p, n, meta.sample_rate, meta.voice_sample_seconds, kSearchSeconds);
voice_sample = &selected;
}
if (voice_sample->empty()) {
SHERPA_ONNX_LOGE(
"The input is shorter than %d seconds, too short to take a voice "
"sample from: %d samples. Return it unchanged.",
meta.voice_sample_seconds, n);
denoised_audio.samples.assign(p, p + n);
return denoised_audio;
}
SpeechDenoiserVoxLockStream stream(&model_, voice_sample->data(),
voice_sample->size());
denoised_audio.samples = stream.Push(p, n);
auto tail = stream.Flush();
denoised_audio.samples.insert(denoised_audio.samples.end(), tail.begin(),
tail.end());
denoised_audio.samples.resize(n);
return denoised_audio;
}
int32_t GetSampleRate() const override {
return model_.GetMetaData().sample_rate;
}
private:
void Init(const OfflineSpeechDenoiserConfig &config) {
const auto &meta = model_.GetMetaData();
const auto &filename = config.model.voxlock.voice_sample;
if (!filename.empty()) {
voice_sample_ = ReadVoxLockVoiceSample(filename, meta.sample_rate,
meta.voice_sample_seconds);
}
}
template <typename Manager>
void Init(Manager *mgr, const OfflineSpeechDenoiserConfig &config) {
const auto &meta = model_.GetMetaData();
const auto &filename = config.model.voxlock.voice_sample;
if (!filename.empty()) {
voice_sample_ = ReadVoxLockVoiceSample(mgr, filename, meta.sample_rate,
meta.voice_sample_seconds);
}
}
private:
// Without a voice sample in the config, the voice sample is the loudest
// voice_sample_seconds seconds in the first kSearchSeconds seconds
static constexpr int32_t kSearchSeconds = 15;
OfflineSpeechDenoiserVoxLockModel model_;
std::vector<float> voice_sample_;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
@@ -0,0 +1,76 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-config.cc
//
// Copyright (c) 2026 Arbaz Siddiqui
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-config.h"
#include <sstream>
#include <string>
#include "sherpa-onnx/csrc/file-utils.h"
#include "sherpa-onnx/csrc/macros.h"
namespace sherpa_onnx {
void OfflineSpeechDenoiserVoxLockModelConfig::Register(ParseOptions *po) {
po->Register("speech-denoiser-voxlock-enroll-model", &enroll_model,
"Path to the VoxLock enroll model, e.g., voxlock_enroll.onnx. "
"It runs once on a voice sample of the caller. Download the "
"models from "
"https://github.com/arbazsiddiqui/voxlock/releases/tag/"
"sherpa-onnx-models");
po->Register("speech-denoiser-voxlock-chunk-model", &chunk_model,
"Path to the VoxLock chunk model, e.g., "
"voxlock_chunk_int8.onnx or voxlock_chunk.onnx. It runs on "
"each chunk of the audio.");
po->Register(
"speech-denoiser-voxlock-voice-sample", &voice_sample,
"Optional. Path to a wave file with at least 3 seconds of the caller's "
"voice. VoxLock keeps this voice and removes the others. From a longer "
"file, it uses the loudest 3 seconds within the first 30 seconds. A "
"file that is shorter or cannot be read is ignored. If empty or "
"ignored, the offline denoiser uses the loudest 3 seconds in the first "
"15 seconds of the input, and the online denoiser passes audio through "
"unchanged until it has heard 3 seconds of speech and then uses that "
"speech.");
}
bool OfflineSpeechDenoiserVoxLockModelConfig::Validate() const {
if (enroll_model.empty()) {
SHERPA_ONNX_LOGE("Please provide --speech-denoiser-voxlock-enroll-model");
return false;
}
if (!FileExists(enroll_model)) {
SHERPA_ONNX_LOGE("voxlock enroll model file '%s' does not exist",
enroll_model.c_str());
return false;
}
if (chunk_model.empty()) {
SHERPA_ONNX_LOGE("Please provide --speech-denoiser-voxlock-chunk-model");
return false;
}
if (!FileExists(chunk_model)) {
SHERPA_ONNX_LOGE("voxlock chunk model file '%s' does not exist",
chunk_model.c_str());
return false;
}
return true;
}
std::string OfflineSpeechDenoiserVoxLockModelConfig::ToString() const {
std::ostringstream os;
os << "OfflineSpeechDenoiserVoxLockModelConfig(";
os << "enroll_model=\"" << enroll_model << "\", ";
os << "chunk_model=\"" << chunk_model << "\", ";
os << "voice_sample=\"" << voice_sample << "\")";
return os.str();
}
} // namespace sherpa_onnx
@@ -0,0 +1,49 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-config.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_CONFIG_H_
#define SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_CONFIG_H_
#include <string>
#include "sherpa-onnx/csrc/parse-options.h"
namespace sherpa_onnx {
// VoxLock keeps the voice of one speaker (the caller) and removes other
// voices. See https://github.com/arbazsiddiqui/voxlock
struct OfflineSpeechDenoiserVoxLockModelConfig {
// Runs once on a voice sample of the caller
std::string enroll_model;
// Runs on each chunk of the audio
std::string chunk_model;
// Optional. A wave file with at least 3 seconds of the caller's voice.
// From a longer file, the loudest 3 seconds within its first 30 seconds
// are used. A file that is shorter or cannot be read is ignored, as if
// it were empty.
// If empty, the voice sample is taken from the input audio: the loudest
// 3 seconds in the first 15 seconds for the offline denoiser, and the
// first 3 seconds of speech for the online denoiser, which passes audio
// through unchanged until then.
std::string voice_sample;
OfflineSpeechDenoiserVoxLockModelConfig() = default;
OfflineSpeechDenoiserVoxLockModelConfig(const std::string &enroll_model,
const std::string &chunk_model,
const std::string &voice_sample)
: enroll_model(enroll_model),
chunk_model(chunk_model),
voice_sample(voice_sample) {}
void Register(ParseOptions *po);
bool Validate() const;
std::string ToString() const;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_CONFIG_H_
@@ -0,0 +1,36 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-meta-data.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_META_DATA_H_
#define SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_META_DATA_H_
#include <cstdint>
#include <string>
#include <vector>
namespace sherpa_onnx {
// please refer to
// https://github.com/k2-fsa/sherpa-onnx/blob/master/scripts/voxlock/add_meta_data.py
struct OfflineSpeechDenoiserVoxLockModelMetaData {
int32_t version = 1;
int32_t sample_rate = 0;
int32_t n_fft = 0;
int32_t hop_length = 0;
int32_t window_length = 0;
std::string window_type;
// The voice sample must be exactly this long. The chunk model accepts
// only the outputs of the enroll model for a voice sample of this length.
int32_t voice_sample_seconds = 0;
// From the chunk model
int32_t frames_per_chunk = 0;
float output_gain = 0;
std::vector<int64_t> state_shape;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_META_DATA_H_
@@ -0,0 +1,423 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.cc
//
// Copyright (c) 2026 Arbaz Siddiqui
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.h"
#include <array>
#include <cmath>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#if __ANDROID_API__ >= 9
#include "android/asset_manager.h"
#include "android/asset_manager_jni.h"
#endif
#if __OHOS__
#include "rawfile/raw_file_manager.h"
#endif
#include "sherpa-onnx/csrc/file-utils.h"
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/onnx-utils.h"
#include "sherpa-onnx/csrc/session.h"
#include "sherpa-onnx/csrc/text-utils.h"
namespace sherpa_onnx {
namespace {
// Properties that both the enroll and the chunk model must agree on
struct VoxLockSharedMetaData {
int32_t version = 0;
int32_t sample_rate = 0;
int32_t n_fft = 0;
int32_t hop_length = 0;
int32_t window_length = 0;
std::string window_type;
};
VoxLockSharedMetaData ReadSharedMetaData(const Ort::ModelMetadata &meta_data,
const std::string &expected_role,
const std::string &filename) {
Ort::AllocatorWithDefaultOptions allocator; // used in the macros below
std::string model_type;
SHERPA_ONNX_READ_META_DATA_STR(model_type, "model_type");
if (model_type != "voxlock") {
SHERPA_ONNX_LOGE("Expect model type 'voxlock' in '%s'. Given: '%s'",
filename.c_str(), model_type.c_str());
SHERPA_ONNX_EXIT(-1);
}
std::string role;
SHERPA_ONNX_READ_META_DATA_STR(role, "model_role");
if (role != expected_role) {
SHERPA_ONNX_LOGE(
"Expect a voxlock %s model in '%s'. Given a voxlock %s model. Did you "
"swap the enroll and chunk models?",
expected_role.c_str(), filename.c_str(), role.c_str());
SHERPA_ONNX_EXIT(-1);
}
VoxLockSharedMetaData ans;
SHERPA_ONNX_READ_META_DATA(ans.version, "version");
SHERPA_ONNX_READ_META_DATA(ans.sample_rate, "sample_rate");
SHERPA_ONNX_READ_META_DATA(ans.n_fft, "n_fft");
SHERPA_ONNX_READ_META_DATA(ans.hop_length, "hop_length");
SHERPA_ONNX_READ_META_DATA(ans.window_length, "window_length");
SHERPA_ONNX_READ_META_DATA_STR(ans.window_type, "window_type");
return ans;
}
} // namespace
class OfflineSpeechDenoiserVoxLockModel::Impl {
public:
explicit Impl(const OfflineSpeechDenoiserModelConfig &config)
: config_(config),
env_(ORT_LOGGING_LEVEL_ERROR),
sess_opts_(GetSessionOptions(config)),
allocator_{} {
{
auto buf = ReadFile(config.voxlock.enroll_model);
InitEnroll(buf.data(), buf.size());
}
{
auto buf = ReadFile(config.voxlock.chunk_model);
InitChunk(buf.data(), buf.size());
}
}
template <typename Manager>
Impl(Manager *mgr, const OfflineSpeechDenoiserModelConfig &config)
: config_(config),
env_(ORT_LOGGING_LEVEL_ERROR),
sess_opts_(GetSessionOptions(config)),
allocator_{} {
{
auto buf = ReadFile(mgr, config.voxlock.enroll_model);
InitEnroll(buf.data(), buf.size());
}
{
auto buf = ReadFile(mgr, config.voxlock.chunk_model);
InitChunk(buf.data(), buf.size());
}
}
std::pair<Ort::Value, Ort::Value> Enroll(const float *samples,
int32_t n) const {
if (n != meta_.voice_sample_seconds * meta_.sample_rate) {
SHERPA_ONNX_LOGE(
"The voxlock voice sample must have exactly %d samples (%d seconds "
"at %d Hz). Given: %d",
meta_.voice_sample_seconds * meta_.sample_rate,
meta_.voice_sample_seconds, meta_.sample_rate, n);
SHERPA_ONNX_EXIT(-1);
}
std::vector<float> x(samples, samples + n);
auto memory_info =
Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault);
std::array<int64_t, 2> x_shape{1, n};
Ort::Value x_tensor = Ort::Value::CreateTensor(
memory_info, x.data(), x.size(), x_shape.data(), x_shape.size());
auto out = enroll_sess_->Run({}, enroll_input_names_ptr_.data(), &x_tensor,
1, enroll_output_names_ptr_.data(),
enroll_output_names_ptr_.size());
return {std::move(out[0]), std::move(out[1])};
}
States GetInitStates() {
Ort::Value h = Ort::Value::CreateTensor<float>(
allocator_, meta_.state_shape.data(), meta_.state_shape.size());
Ort::Value c = Ort::Value::CreateTensor<float>(
allocator_, meta_.state_shape.data(), meta_.state_shape.size());
Fill<float>(&h, 0);
Fill<float>(&c, 0);
States states;
states.reserve(2);
states.push_back(std::move(h));
states.push_back(std::move(c));
return states;
}
std::pair<Ort::Value, States> Run(Ort::Value wav, Ort::Value enroll_mag,
Ort::Value context, States states) const {
std::vector<Ort::Value> inputs;
inputs.reserve(3 + states.size());
inputs.push_back(std::move(wav));
inputs.push_back(std::move(enroll_mag));
inputs.push_back(std::move(context));
for (auto &s : states) {
inputs.push_back(std::move(s));
}
auto out = chunk_sess_->Run(
{}, chunk_input_names_ptr_.data(), inputs.data(), inputs.size(),
chunk_output_names_ptr_.data(), chunk_output_names_ptr_.size());
States next_states;
next_states.reserve(out.size() - 1);
for (int32_t k = 1; k < static_cast<int32_t>(out.size()); ++k) {
next_states.push_back(std::move(out[k]));
}
return {std::move(out[0]), std::move(next_states)};
}
const OfflineSpeechDenoiserVoxLockModelMetaData &GetMetaData() const {
return meta_;
}
private:
void InitEnroll(void *model_data, size_t model_data_length) {
enroll_sess_ = std::make_unique<Ort::Session>(
env_, model_data, model_data_length, sess_opts_);
GetInputNames(enroll_sess_.get(), &enroll_input_names_,
&enroll_input_names_ptr_);
GetOutputNames(enroll_sess_.get(), &enroll_output_names_,
&enroll_output_names_ptr_);
Ort::ModelMetadata meta_data = enroll_sess_->GetModelMetadata();
if (config_.debug) {
PrintInfo("voxlock enroll model", meta_data, enroll_input_names_,
enroll_output_names_);
}
auto shared =
ReadSharedMetaData(meta_data, "enroll", config_.voxlock.enroll_model);
if (enroll_input_names_.size() != 1 || enroll_output_names_.size() != 2) {
SHERPA_ONNX_LOGE(
"Expect the voxlock enroll model to have 1 input and 2 outputs. "
"Given %d inputs and %d outputs",
static_cast<int32_t>(enroll_input_names_.size()),
static_cast<int32_t>(enroll_output_names_.size()));
SHERPA_ONNX_EXIT(-1);
}
meta_.version = shared.version;
meta_.sample_rate = shared.sample_rate;
meta_.n_fft = shared.n_fft;
meta_.hop_length = shared.hop_length;
meta_.window_length = shared.window_length;
meta_.window_type = shared.window_type;
Ort::AllocatorWithDefaultOptions allocator; // used in the macros below
SHERPA_ONNX_READ_META_DATA(meta_.voice_sample_seconds,
"voice_sample_seconds");
if (meta_.sample_rate <= 0 || meta_.hop_length <= 0 ||
meta_.n_fft != meta_.window_length || meta_.n_fft % 2 != 0 ||
meta_.n_fft <= 2 * meta_.hop_length || meta_.window_type != "hann" ||
meta_.voice_sample_seconds <= 0) {
SHERPA_ONNX_LOGE(
"Unsupported voxlock meta data. sample_rate=%d, n_fft=%d, "
"hop_length=%d, window_length=%d, window_type=%s, "
"voice_sample_seconds=%d",
meta_.sample_rate, meta_.n_fft, meta_.hop_length, meta_.window_length,
meta_.window_type.c_str(), meta_.voice_sample_seconds);
SHERPA_ONNX_EXIT(-1);
}
}
void InitChunk(void *model_data, size_t model_data_length) {
chunk_sess_ = std::make_unique<Ort::Session>(env_, model_data,
model_data_length, sess_opts_);
GetInputNames(chunk_sess_.get(), &chunk_input_names_,
&chunk_input_names_ptr_);
GetOutputNames(chunk_sess_.get(), &chunk_output_names_,
&chunk_output_names_ptr_);
Ort::ModelMetadata meta_data = chunk_sess_->GetModelMetadata();
if (config_.debug) {
PrintInfo("voxlock chunk model", meta_data, chunk_input_names_,
chunk_output_names_);
}
auto shared =
ReadSharedMetaData(meta_data, "chunk", config_.voxlock.chunk_model);
// inputs: wav, enroll_mag, context, h, c
// outputs: frames, h_out, c_out
if (chunk_input_names_.size() != 5 || chunk_output_names_.size() != 3) {
SHERPA_ONNX_LOGE(
"Expect the voxlock chunk model to have 5 inputs and 3 outputs. "
"Given %d inputs and %d outputs",
static_cast<int32_t>(chunk_input_names_.size()),
static_cast<int32_t>(chunk_output_names_.size()));
SHERPA_ONNX_EXIT(-1);
}
if (shared.version != meta_.version ||
shared.sample_rate != meta_.sample_rate ||
shared.n_fft != meta_.n_fft || shared.hop_length != meta_.hop_length ||
shared.window_length != meta_.window_length ||
shared.window_type != meta_.window_type) {
SHERPA_ONNX_LOGE(
"The voxlock enroll and chunk models do not match.\n"
" enroll: version=%d, sample_rate=%d, n_fft=%d, hop_length=%d, "
"window_length=%d, window_type=%s\n"
" chunk: version=%d, sample_rate=%d, n_fft=%d, hop_length=%d, "
"window_length=%d, window_type=%s",
meta_.version, meta_.sample_rate, meta_.n_fft, meta_.hop_length,
meta_.window_length, meta_.window_type.c_str(), shared.version,
shared.sample_rate, shared.n_fft, shared.hop_length,
shared.window_length, shared.window_type.c_str());
SHERPA_ONNX_EXIT(-1);
}
Ort::AllocatorWithDefaultOptions allocator; // used in the macros below
int32_t voice_sample_seconds = 0;
SHERPA_ONNX_READ_META_DATA(voice_sample_seconds, "voice_sample_seconds");
if (voice_sample_seconds != meta_.voice_sample_seconds) {
SHERPA_ONNX_LOGE(
"The voxlock enroll and chunk models do not match. "
"voice_sample_seconds: %d vs %d",
meta_.voice_sample_seconds, voice_sample_seconds);
SHERPA_ONNX_EXIT(-1);
}
SHERPA_ONNX_READ_META_DATA(meta_.frames_per_chunk, "frames_per_chunk");
SHERPA_ONNX_READ_META_DATA_VEC(meta_.state_shape, "state_shape");
std::string output_gain;
SHERPA_ONNX_READ_META_DATA_STR(output_gain, "output_gain");
meta_.output_gain = ToFloatOrDefault(output_gain, NAN);
if (meta_.frames_per_chunk <= 0 || !std::isfinite(meta_.output_gain)) {
SHERPA_ONNX_LOGE(
"Invalid voxlock meta data. frames_per_chunk=%d, output_gain=%s",
meta_.frames_per_chunk, output_gain.c_str());
SHERPA_ONNX_EXIT(-1);
}
auto h_shape =
chunk_sess_->GetInputTypeInfo(3).GetTensorTypeAndShapeInfo().GetShape();
auto c_shape =
chunk_sess_->GetInputTypeInfo(4).GetTensorTypeAndShapeInfo().GetShape();
if (h_shape != meta_.state_shape || c_shape != meta_.state_shape) {
SHERPA_ONNX_LOGE(
"The state shape in the voxlock chunk model does not match its meta "
"data");
SHERPA_ONNX_EXIT(-1);
}
}
void PrintInfo(const std::string &name, const Ort::ModelMetadata &meta_data,
const std::vector<std::string> &input_names,
const std::vector<std::string> &output_names) const {
std::ostringstream os;
os << "---" << name << "---\n";
PrintModelMetadata(os, meta_data);
os << "----------input names----------\n";
int32_t i = 0;
for (const auto &s : input_names) {
os << i << " " << s << "\n";
++i;
}
os << "----------output names----------\n";
i = 0;
for (const auto &s : output_names) {
os << i << " " << s << "\n";
++i;
}
#if __OHOS__
SHERPA_ONNX_LOGE("%{public}s\n", os.str().c_str());
#else
SHERPA_ONNX_LOGE("%s\n", os.str().c_str());
#endif
}
private:
OfflineSpeechDenoiserModelConfig config_;
OfflineSpeechDenoiserVoxLockModelMetaData meta_;
Ort::Env env_;
Ort::SessionOptions sess_opts_;
Ort::AllocatorWithDefaultOptions allocator_;
std::unique_ptr<Ort::Session> enroll_sess_;
std::vector<std::string> enroll_input_names_;
std::vector<const char *> enroll_input_names_ptr_;
std::vector<std::string> enroll_output_names_;
std::vector<const char *> enroll_output_names_ptr_;
std::unique_ptr<Ort::Session> chunk_sess_;
std::vector<std::string> chunk_input_names_;
std::vector<const char *> chunk_input_names_ptr_;
std::vector<std::string> chunk_output_names_;
std::vector<const char *> chunk_output_names_ptr_;
};
OfflineSpeechDenoiserVoxLockModel::~OfflineSpeechDenoiserVoxLockModel() =
default; // NOLINT
OfflineSpeechDenoiserVoxLockModel::OfflineSpeechDenoiserVoxLockModel(
const OfflineSpeechDenoiserModelConfig &config)
: impl_(std::make_unique<Impl>(config)) {}
template <typename Manager>
OfflineSpeechDenoiserVoxLockModel::OfflineSpeechDenoiserVoxLockModel(
Manager *mgr, const OfflineSpeechDenoiserModelConfig &config)
: impl_(std::make_unique<Impl>(mgr, config)) {}
std::pair<Ort::Value, Ort::Value> OfflineSpeechDenoiserVoxLockModel::Enroll(
const float *samples, int32_t n) const {
return impl_->Enroll(samples, n);
}
OfflineSpeechDenoiserVoxLockModel::States
OfflineSpeechDenoiserVoxLockModel::GetInitStates() const {
return impl_->GetInitStates();
}
std::pair<Ort::Value, OfflineSpeechDenoiserVoxLockModel::States>
OfflineSpeechDenoiserVoxLockModel::Run(Ort::Value wav, Ort::Value enroll_mag,
Ort::Value context,
States states) const {
return impl_->Run(std::move(wav), std::move(enroll_mag), std::move(context),
std::move(states));
}
const OfflineSpeechDenoiserVoxLockModelMetaData &
OfflineSpeechDenoiserVoxLockModel::GetMetaData() const {
return impl_->GetMetaData();
}
#if __ANDROID_API__ >= 9
template OfflineSpeechDenoiserVoxLockModel::OfflineSpeechDenoiserVoxLockModel(
AAssetManager *mgr, const OfflineSpeechDenoiserModelConfig &config);
#endif
#if __OHOS__
template OfflineSpeechDenoiserVoxLockModel::OfflineSpeechDenoiserVoxLockModel(
NativeResourceManager *mgr, const OfflineSpeechDenoiserModelConfig &config);
#endif
} // namespace sherpa_onnx
@@ -0,0 +1,69 @@
// sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_H_
#define SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_H_
#include <memory>
#include <utility>
#include <vector>
#include "onnxruntime_cxx_api.h" // NOLINT
#include "sherpa-onnx/csrc/offline-speech-denoiser-model-config.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model-meta-data.h"
namespace sherpa_onnx {
class OfflineSpeechDenoiserVoxLockModel {
public:
~OfflineSpeechDenoiserVoxLockModel();
explicit OfflineSpeechDenoiserVoxLockModel(
const OfflineSpeechDenoiserModelConfig &config);
template <typename Manager>
OfflineSpeechDenoiserVoxLockModel(
Manager *mgr, const OfflineSpeechDenoiserModelConfig &config);
using States = std::vector<Ort::Value>;
/* Run the enroll model on a voice sample of the caller.
*
* @param samples Voice sample at GetMetaData().sample_rate. It must have
* exactly GetMetaData().voice_sample_seconds seconds.
* @param n Number of samples.
*
* @return Return a pair containing:
* - enroll_mag, a tensor of shape (1, num_freq_bins, num_frames)
* - context, a tensor of shape (1, context_dim, num_context_frames)
*/
std::pair<Ort::Value, Ort::Value> Enroll(const float *samples,
int32_t n) const;
// Zero LSTM states h and c
States GetInitStates() const;
/* Run the chunk model.
*
* @param wav A tensor of shape (1, (K - 1) * hop_length + n_fft)
* @param enroll_mag Returned by Enroll()
* @param context Returned by Enroll()
* @param states LSTM states from GetInitStates() or a previous call
*
* @return Return a pair containing:
* - frames, a tensor of shape (1, K, n_fft). Each frame is windowed by
* the synthesis window and is ready for overlap-add.
* - next states
*/
std::pair<Ort::Value, States> Run(Ort::Value wav, Ort::Value enroll_mag,
Ort::Value context, States states) const;
const OfflineSpeechDenoiserVoxLockModelMetaData &GetMetaData() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_OFFLINE_SPEECH_DENOISER_VOXLOCK_MODEL_H_
@@ -18,6 +18,7 @@
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/online-speech-denoiser-dpdfnet-impl.h"
#include "sherpa-onnx/csrc/online-speech-denoiser-gtcrn-impl.h"
#include "sherpa-onnx/csrc/online-speech-denoiser-voxlock-impl.h"
namespace sherpa_onnx {
@@ -25,11 +26,15 @@ std::unique_ptr<OnlineSpeechDenoiserImpl> OnlineSpeechDenoiserImpl::Create(
const OnlineSpeechDenoiserConfig &config) {
const bool has_gtcrn = !config.model.gtcrn.model.empty();
const bool has_dpdfnet = !config.model.dpdfnet.model.empty();
const bool has_voxlock = !config.model.voxlock.enroll_model.empty() ||
!config.model.voxlock.chunk_model.empty();
if (has_gtcrn) {
return std::make_unique<OnlineSpeechDenoiserGtcrnImpl>(config);
} else if (has_dpdfnet) {
return std::make_unique<OnlineSpeechDenoiserDpdfNetImpl>(config);
} else if (has_voxlock) {
return std::make_unique<OnlineSpeechDenoiserVoxLockImpl>(config);
}
SHERPA_ONNX_LOGE("Please provide one speech denoising model.");
@@ -41,11 +46,15 @@ std::unique_ptr<OnlineSpeechDenoiserImpl> OnlineSpeechDenoiserImpl::Create(
Manager *mgr, const OnlineSpeechDenoiserConfig &config) {
const bool has_gtcrn = !config.model.gtcrn.model.empty();
const bool has_dpdfnet = !config.model.dpdfnet.model.empty();
const bool has_voxlock = !config.model.voxlock.enroll_model.empty() ||
!config.model.voxlock.chunk_model.empty();
if (has_gtcrn) {
return std::make_unique<OnlineSpeechDenoiserGtcrnImpl>(mgr, config);
} else if (has_dpdfnet) {
return std::make_unique<OnlineSpeechDenoiserDpdfNetImpl>(mgr, config);
} else if (has_voxlock) {
return std::make_unique<OnlineSpeechDenoiserVoxLockImpl>(mgr, config);
}
SHERPA_ONNX_LOGE("Please provide one speech denoising model.");
@@ -0,0 +1,322 @@
// sherpa-onnx/csrc/online-speech-denoiser-voxlock-impl.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_ONLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
#define SHERPA_ONNX_CSRC_ONLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <deque>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.h"
#include "sherpa-onnx/csrc/online-speech-denoiser-impl.h"
#include "sherpa-onnx/csrc/resample.h"
#include "sherpa-onnx/csrc/speech-denoiser-voxlock-stream.h"
namespace sherpa_onnx {
/* With a voice sample in the config, it isolates from the first sample.
*
* Without one, it passes audio through unchanged until it has heard
* voice_sample_seconds seconds of speech, takes that speech as the voice
* sample, and isolates from then on.
*
* Speech is 10 ms frames at least 10 dB above the noise floor, the quietest
* frame of the last 2 seconds. Digital silence (below -90 dBFS) is left out
* of the floor. At the switch, the stream first runs over the last 250 ms of
* audio, whose output is dropped, so it does not start from an empty LSTM
* state, and the isolated audio fades in over 10 ms.
*/
class OnlineSpeechDenoiserVoxLockImpl : public OnlineSpeechDenoiserImpl {
public:
explicit OnlineSpeechDenoiserVoxLockImpl(
const OnlineSpeechDenoiserConfig &config)
: model_(config.model) {
Init(config);
}
template <typename Manager>
OnlineSpeechDenoiserVoxLockImpl(Manager *mgr,
const OnlineSpeechDenoiserConfig &config)
: model_(mgr, config.model) {
Init(mgr, config);
}
DenoisedAudio Run(const float *samples, int32_t n,
int32_t sample_rate) override {
const int32_t model_sample_rate = model_.GetMetaData().sample_rate;
if (sample_rate <= 0) {
SHERPA_ONNX_LOGE("Expected sample_rate > 0. Given: %d", sample_rate);
return {{}, model_sample_rate};
}
if (n < 0) {
SHERPA_ONNX_LOGE("Expected n >= 0. Given: %d", n);
return {{}, model_sample_rate};
}
if (n == 0) {
return {{}, model_sample_rate};
}
if (input_sample_rate_ == -1) {
input_sample_rate_ = sample_rate;
if (input_sample_rate_ != model_sample_rate) {
SHERPA_ONNX_LOGE(
"Creating a streaming resampler:\n"
" in_sample_rate: %d\n"
" output_sample_rate: %d\n",
input_sample_rate_, model_sample_rate);
resampler_ =
CreateVoxLockResampler(input_sample_rate_, model_sample_rate);
}
} else if (sample_rate != input_sample_rate_) {
SHERPA_ONNX_LOGE(
"Streaming denoiser expects a fixed input sample rate. Previous: %d. "
"Current: %d. Call Flush() or Reset() before changing it.",
input_sample_rate_, sample_rate);
return {{}, model_sample_rate};
}
std::vector<float> resampled;
if (resampler_) {
resampler_->Resample(samples, n, false, &resampled);
} else {
resampled.assign(samples, samples + n);
}
DenoisedAudio ans;
ans.sample_rate = model_sample_rate;
ans.samples = Process(resampled.data(), resampled.size());
total_output_samples_ += ans.samples.size();
return ans;
}
DenoisedAudio Flush() override {
DenoisedAudio ans;
ans.sample_rate = model_.GetMetaData().sample_rate;
if (resampler_) {
std::vector<float> tail;
float dummy = 0;
resampler_->Resample(&dummy, 0, true, &tail);
ans.samples = Process(tail.data(), tail.size());
}
if (stream_) {
auto tail = PostProcess(stream_->Flush());
ans.samples.insert(ans.samples.end(), tail.begin(), tail.end());
}
int64_t remaining = total_input_samples_ - total_output_samples_;
if (remaining < 0) {
remaining = 0;
}
if (ans.samples.size() > static_cast<size_t>(remaining)) {
ans.samples.resize(static_cast<size_t>(remaining));
}
Reset();
return ans;
}
void Reset() override {
resampler_.reset();
input_sample_rate_ = -1;
total_input_samples_ = 0;
total_output_samples_ = 0;
voiced_.clear();
energy_.clear();
rest_.clear();
recent_.clear();
after_.clear();
num_to_skip_ = 0;
if (voice_sample_.empty()) {
stream_.reset();
num_faded_ = 0;
} else {
stream_ = std::make_unique<SpeechDenoiserVoxLockStream>(
&model_, voice_sample_.data(), voice_sample_.size());
num_faded_ = fade_length_;
}
}
int32_t GetSampleRate() const override {
return model_.GetMetaData().sample_rate;
}
int32_t GetFrameShiftInSamples() const override {
return model_.GetMetaData().hop_length;
}
private:
void Init(const OnlineSpeechDenoiserConfig &config) {
const auto &meta = model_.GetMetaData();
const auto &filename = config.model.voxlock.voice_sample;
if (!filename.empty()) {
voice_sample_ = ReadVoxLockVoiceSample(filename, meta.sample_rate,
meta.voice_sample_seconds);
}
InitStream();
}
template <typename Manager>
void Init(Manager *mgr, const OnlineSpeechDenoiserConfig &config) {
const auto &meta = model_.GetMetaData();
const auto &filename = config.model.voxlock.voice_sample;
if (!filename.empty()) {
voice_sample_ = ReadVoxLockVoiceSample(mgr, filename, meta.sample_rate,
meta.voice_sample_seconds);
}
InitStream();
}
void InitStream() {
const auto &meta = model_.GetMetaData();
frame_length_ = meta.sample_rate / 100;
warm_up_length_ = meta.sample_rate / 4;
fade_length_ = frame_length_;
num_frames_needed_ = meta.voice_sample_seconds * 100;
Reset();
}
// samples are at the model's sample rate
std::vector<float> Process(const float *samples, int32_t n) {
total_input_samples_ += n;
if (!stream_) {
FindVoiceSample(samples, n);
return {samples, samples + n};
}
if (num_faded_ < fade_length_) {
after_.insert(after_.end(), samples, samples + n);
}
return PostProcess(stream_->Push(samples, n));
}
void FindVoiceSample(const float *samples, int32_t n) {
recent_.insert(recent_.end(), samples, samples + n);
if (static_cast<int32_t>(recent_.size()) > warm_up_length_) {
recent_.erase(recent_.begin(), recent_.end() - warm_up_length_);
}
rest_.insert(rest_.end(), samples, samples + n);
const int32_t num_frames =
static_cast<int32_t>(rest_.size()) / frame_length_;
for (int32_t i = 0; i != num_frames; ++i) {
const float *frame = rest_.data() + i * frame_length_;
double sum = 0;
for (int32_t k = 0; k != frame_length_; ++k) {
sum += static_cast<double>(frame[k]) * frame[k];
}
float mean = static_cast<float>(sum / frame_length_);
float db = 10 * std::log10(mean + 1e-10f);
if (db > kSilenceDb) {
energy_.push_back(db);
if (static_cast<int32_t>(energy_.size()) > kNoiseFloorFrames) {
energy_.pop_front();
}
}
if (static_cast<int32_t>(energy_.size()) >= kMinNoiseFloorFrames &&
db >= *std::min_element(energy_.begin(), energy_.end()) +
kSpeechMarginDb) {
voiced_.insert(voiced_.end(), frame, frame + frame_length_);
}
}
rest_.erase(rest_.begin(), rest_.begin() + num_frames * frame_length_);
const int32_t needed = num_frames_needed_ * frame_length_;
if (static_cast<int32_t>(voiced_.size()) < needed) {
return;
}
stream_ = std::make_unique<SpeechDenoiserVoxLockStream>(
&model_, voiced_.data(), needed);
voiced_.clear();
energy_.clear();
num_to_skip_ = recent_.size();
auto warm_up = stream_->Push(recent_.data(), recent_.size());
num_to_skip_ -= std::min<int32_t>(num_to_skip_, warm_up.size());
}
// Drop the output of the warm-up audio and fade in from the input
std::vector<float> PostProcess(std::vector<float> samples) {
int32_t cut = std::min<int32_t>(num_to_skip_, samples.size());
num_to_skip_ -= cut;
samples.erase(samples.begin(), samples.begin() + cut);
if (num_faded_ < fade_length_ && !samples.empty()) {
int32_t n = std::min<int32_t>(samples.size(), fade_length_ - num_faded_);
n = std::min<int32_t>(n,
static_cast<int32_t>(after_.size()) - num_faded_);
for (int32_t i = 0; i < n; ++i) {
float r = (static_cast<float>(num_faded_ + i) + 1) / fade_length_;
samples[i] = r * samples[i] + (1 - r) * after_[num_faded_ + i];
}
num_faded_ += n;
if (num_faded_ >= fade_length_) {
after_.clear();
}
}
return samples;
}
private:
static constexpr float kSilenceDb = -90;
static constexpr float kSpeechMarginDb = 10;
static constexpr int32_t kNoiseFloorFrames = 200;
static constexpr int32_t kMinNoiseFloorFrames = 20;
OfflineSpeechDenoiserVoxLockModel model_;
std::vector<float> voice_sample_;
std::unique_ptr<SpeechDenoiserVoxLockStream> stream_;
std::unique_ptr<LinearResample> resampler_;
int32_t input_sample_rate_ = -1;
int64_t total_input_samples_ = 0;
int64_t total_output_samples_ = 0;
int32_t frame_length_ = 0;
int32_t warm_up_length_ = 0;
int32_t fade_length_ = 0;
int32_t num_frames_needed_ = 0;
std::vector<float> voiced_;
std::deque<float> energy_;
std::vector<float> rest_;
std::vector<float> recent_;
std::vector<float> after_;
int32_t num_to_skip_ = 0;
int32_t num_faded_ = 0;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_ONLINE_SPEECH_DENOISER_VOXLOCK_IMPL_H_
@@ -56,6 +56,27 @@ wget https://github.com/k2-fsa/sherpa-onnx/releases/download/speech-enhancement-
--output-wav=output_48k.wav
# For the highest-quality 48 kHz model, use dpdfnet8_48khz_hr.onnx.
(3) Use VoxLock models
VoxLock keeps the voice of one speaker, e.g., the caller in a phone call,
and removes the other voices. Download the models from
https://github.com/arbazsiddiqui/voxlock/releases/tag/sherpa-onnx-models
wget https://github.com/arbazsiddiqui/voxlock/releases/download/sherpa-onnx-models/voxlock_enroll.onnx
wget https://github.com/arbazsiddiqui/voxlock/releases/download/sherpa-onnx-models/voxlock_chunk_int8.onnx
wget https://github.com/arbazsiddiqui/voxlock/releases/download/sherpa-onnx-models/voxlock_test_call.wav
./bin/sherpa-onnx-offline-denoiser \
--speech-denoiser-voxlock-enroll-model=voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=voxlock_chunk_int8.onnx \
--input-wav=voxlock_test_call.wav \
--output-wav=output_16k.wav
# It keeps the voice in the loudest 3 seconds of the first 15 seconds.
# To choose the voice yourself, pass at least 3 seconds of it with
# --speech-denoiser-voxlock-voice-sample=voxlock_test_voice_sample.wav
# Use voxlock_chunk.onnx for the float32 model.
)usage";
sherpa_onnx::ParseOptions po(kUsageMessage);
@@ -38,6 +38,10 @@ Currently this binary supports:
dpdfnet8_8khz.onnx
dpdfnet2_48khz_hr.onnx
dpdfnet8_48khz_hr.onnx
voxlock_enroll.onnx with voxlock_chunk_int8.onnx or voxlock_chunk.onnx
VoxLock models are available from
https://github.com/arbazsiddiqui/voxlock/releases/tag/sherpa-onnx-models
Usage:
@@ -61,6 +65,19 @@ Usage:
# Use dpdfnet2_8khz.onnx or dpdfnet8_8khz.onnx for 8 kHz audio, and
# dpdfnet8_48khz_hr.onnx for the highest-quality 48 kHz audio.
./bin/sherpa-onnx-online-denoiser \
--speech-denoiser-voxlock-enroll-model=voxlock_enroll.onnx \
--speech-denoiser-voxlock-chunk-model=voxlock_chunk_int8.onnx \
--num-threads=2 \
--chunk-duration-ms=80 \
--input-wav=voxlock_test_call.wav \
--output-wav=output_16k.wav
# VoxLock passes the audio through unchanged until it has heard 3 seconds
# of speech, then keeps that voice and removes the others. Pass
# --speech-denoiser-voxlock-voice-sample=voxlock_test_voice_sample.wav
# to keep the voice in a sample of at least 3 seconds from the first chunk.
)usage";
sherpa_onnx::ParseOptions po(kUsageMessage);
@@ -0,0 +1,373 @@
// sherpa-onnx/csrc/speech-denoiser-voxlock-stream.h
//
// Copyright (c) 2026 Arbaz Siddiqui
#ifndef SHERPA_ONNX_CSRC_SPEECH_DENOISER_VOXLOCK_STREAM_H_
#define SHERPA_ONNX_CSRC_SPEECH_DENOISER_VOXLOCK_STREAM_H_
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <memory>
#include <sstream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "Eigen/Dense"
#include "onnxruntime_cxx_api.h" // NOLINT
#include "sherpa-onnx/csrc/file-utils.h"
#include "sherpa-onnx/csrc/macros.h"
#include "sherpa-onnx/csrc/offline-speech-denoiser-voxlock-model.h"
#include "sherpa-onnx/csrc/onnx-utils.h"
#include "sherpa-onnx/csrc/resample.h"
#include "sherpa-onnx/csrc/wave-reader.h"
namespace sherpa_onnx {
inline std::unique_ptr<LinearResample> CreateVoxLockResampler(
int32_t in_sample_rate, int32_t out_sample_rate) {
float min_freq = std::min<int32_t>(in_sample_rate, out_sample_rate);
float lowpass_cutoff = 0.99f * 0.5f * min_freq;
int32_t lowpass_filter_width = 6;
return std::make_unique<LinearResample>(in_sample_rate, out_sample_rate,
lowpass_cutoff, lowpass_filter_width);
}
/* Return the loudest `seconds` seconds in the first `search_seconds` seconds
* of the audio, measured over 10 ms frames. It returns an empty vector if
* the audio is shorter than `seconds` seconds.
*
* This is the voice sample VoxLock is benchmarked with.
*/
inline std::vector<float> SelectVoxLockVoiceSample(const float *samples,
int32_t n,
int32_t sample_rate,
int32_t seconds,
int32_t search_seconds) {
const int32_t num_samples = std::min(n, search_seconds * sample_rate);
const int32_t frame_length = sample_rate / 100;
const int32_t num_frames = num_samples / frame_length;
const int32_t window = seconds * 100;
const int32_t length = seconds * sample_rate;
if (num_samples < length) {
return {};
}
if (num_frames <= window) {
return {samples, samples + length};
}
using RowMajorMatrixXf =
Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
// Mean energy of each 10 ms frame, (num_frames,)
Eigen::VectorXf energy =
Eigen::Map<const RowMajorMatrixXf>(samples, num_frames, frame_length)
.cast<double>()
.array()
.square()
.rowwise()
.mean()
.cast<float>();
// Row i is the window that starts at frame i. The windows overlap, so
// consecutive rows are one element apart. Only windows whose `length`
// samples fit in the audio are candidates; frame_length * 100 is less than
// sample_rate if sample_rate is not a multiple of 100.
const int32_t num_windows = std::min(
num_frames - window + 1, (num_samples - length) / frame_length + 1);
Eigen::Map<const RowMajorMatrixXf, 0, Eigen::Stride<1, 1>> windows(
energy.data(), num_windows, window);
// The first window with the largest energy
Eigen::Index best = 0;
windows.cast<double>().rowwise().sum().maxCoeff(&best);
const float *start = samples + best * frame_length;
return {start, start + length};
}
// A voice sample longer than the model's voice_sample_seconds is searched
// for its loudest voice_sample_seconds seconds within its first
// kVoxLockMaxVoiceSampleSeconds seconds
constexpr int32_t kVoxLockMaxVoiceSampleSeconds = 30;
// Resample a voice sample to sample_rate and return exactly `seconds` seconds
// of it. Return an empty vector if it is shorter than `seconds` seconds.
inline std::vector<float> PrepareVoxLockVoiceSample(
const std::vector<float> &samples, int32_t file_sample_rate,
int32_t sample_rate, int32_t seconds, const std::string &filename) {
std::vector<float> resampled;
if (file_sample_rate == sample_rate) {
resampled = samples;
} else {
CreateVoxLockResampler(file_sample_rate, sample_rate)
->Resample(samples.data(), samples.size(), true, &resampled);
}
const int32_t n = resampled.size();
if (n < seconds * sample_rate) {
SHERPA_ONNX_LOGE(
"Ignore the voxlock voice sample '%s': it is %.3f seconds long, "
"shorter than %d seconds. Take the voice sample from the input audio "
"instead.",
filename.c_str(), static_cast<float>(n) / sample_rate, seconds);
return {};
}
if (n > seconds * sample_rate) {
SHERPA_ONNX_LOGE(
"The voxlock voice sample '%s' is %.3f seconds long. Use its loudest "
"%d seconds within its first %d seconds.",
filename.c_str(), static_cast<float>(n) / sample_rate, seconds,
kVoxLockMaxVoiceSampleSeconds);
}
return SelectVoxLockVoiceSample(resampled.data(), n, sample_rate, seconds,
kVoxLockMaxVoiceSampleSeconds);
}
// Read a voice sample from a wave file and return exactly `seconds` seconds
// of it. Return an empty vector if it cannot be read or is too short.
inline std::vector<float> ReadVoxLockVoiceSample(const std::string &filename,
int32_t sample_rate,
int32_t seconds) {
int32_t file_sample_rate = -1;
bool is_ok = false;
std::vector<float> samples = ReadWave(filename, &file_sample_rate, &is_ok);
if (!is_ok) {
SHERPA_ONNX_LOGE(
"Ignore the voxlock voice sample '%s': failed to read it. Take the "
"voice sample from the input audio instead.",
filename.c_str());
return {};
}
return PrepareVoxLockVoiceSample(samples, file_sample_rate, sample_rate,
seconds, filename);
}
// Read a voice sample from a wave file with a manager, e.g., the asset
// manager on Android, and return exactly `seconds` seconds of it. Return an
// empty vector if it cannot be read or is too short.
template <typename Manager>
std::vector<float> ReadVoxLockVoiceSample(Manager *mgr,
const std::string &filename,
int32_t sample_rate,
int32_t seconds) {
auto buf = ReadFile(mgr, filename);
std::istringstream is(std::string(buf.data(), buf.size()));
int32_t file_sample_rate = -1;
bool is_ok = false;
std::vector<float> samples = ReadWave(is, &file_sample_rate, &is_ok);
if (!is_ok) {
SHERPA_ONNX_LOGE(
"Ignore the voxlock voice sample '%s': failed to read it. Take the "
"voice sample from the input audio instead.",
filename.c_str());
return {};
}
return PrepareVoxLockVoiceSample(samples, file_sample_rate, sample_rate,
seconds, filename);
}
/* Isolates the caller's voice in a stream of audio at the model's sample
* rate, locked to the voice in a voice sample.
*
* The call is reflect-padded by n_fft / 2 samples at both ends. Every
* frames_per_chunk frames of the padded call go through the chunk model,
* which carries the LSTM state across chunks. The frames it returns are
* overlap-added at hop_length and divided by the summed squared window.
* The output lags the input by n_fft - hop_length samples until Flush().
*/
class SpeechDenoiserVoxLockStream {
public:
SpeechDenoiserVoxLockStream(const OfflineSpeechDenoiserVoxLockModel *model,
const float *voice_sample, int32_t n)
: model_(model),
n_fft_(model->GetMetaData().n_fft),
hop_length_(model->GetMetaData().hop_length),
pad_(model->GetMetaData().n_fft / 2),
frames_per_chunk_(model->GetMetaData().frames_per_chunk),
output_gain_(model->GetMetaData().output_gain),
memory_info_(
Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault)),
window_square_(n_fft_),
overlap_add_(n_fft_),
window_sum_(n_fft_) {
std::tie(enroll_mag_, context_) = model_->Enroll(voice_sample, n);
states_ = model_->GetInitStates();
constexpr double kPi = 3.14159265358979323846;
for (int32_t i = 0; i != n_fft_; ++i) {
// periodic Hann window
float w = static_cast<float>(0.5 - 0.5 * std::cos(2 * kPi * i / n_fft_));
window_square_[i] = w * w;
}
}
/* @param samples Audio samples at the model's sample rate
* @param n Number of samples
*
* @return Return the isolated samples that are ready.
*/
std::vector<float> Push(const float *samples, int32_t n) {
if (!started_) {
head_.insert(head_.end(), samples, samples + n);
if (static_cast<int32_t>(head_.size()) <= pad_) {
return {};
}
// reflect padding at the start, as torch.stft(center=True)
buffer_.reserve(pad_ + head_.size());
for (int32_t i = pad_; i >= 1; --i) {
buffer_.push_back(head_[i]);
}
buffer_.insert(buffer_.end(), head_.begin(), head_.end());
head_.clear();
head_.shrink_to_fit();
num_to_drop_ = pad_;
started_ = true;
} else {
buffer_.insert(buffer_.end(), samples, samples + n);
}
return RunChunks();
}
// Return the samples still held back. Call it once, at the end of the
// stream.
std::vector<float> Flush() {
if (!started_) {
// Too short to build the padding at the start; return it unchanged
return std::exchange(head_, {});
}
// reflect padding at the end
const int32_t end = static_cast<int32_t>(buffer_.size());
for (int32_t i = 0; i != pad_; ++i) {
buffer_.push_back(buffer_[end - 2 - i]);
}
std::vector<float> ans = RunChunks();
for (int32_t i = 0; i != n_fft_; ++i) {
ans.push_back(Normalize(i) * output_gain_);
}
started_ = false;
buffer_.clear();
offset_ = 0;
return ans;
}
private:
float Normalize(int32_t i) const {
return overlap_add_[i] / (window_sum_[i] > 1e-11f ? window_sum_[i] : 1.0f);
}
std::vector<float> RunChunks() {
std::vector<float> ans;
while (static_cast<int32_t>(buffer_.size()) - offset_ >= n_fft_) {
const int32_t available = static_cast<int32_t>(buffer_.size()) - offset_;
const int32_t num_frames =
std::min(frames_per_chunk_, 1 + (available - n_fft_) / hop_length_);
const int32_t num_samples = (num_frames - 1) * hop_length_ + n_fft_;
std::array<int64_t, 2> wav_shape{1, num_samples};
Ort::Value wav = Ort::Value::CreateTensor(
memory_info_, buffer_.data() + offset_, num_samples, wav_shape.data(),
wav_shape.size());
Ort::Value frames{nullptr};
std::tie(frames, states_) =
model_->Run(std::move(wav), View(&enroll_mag_), View(&context_),
std::move(states_));
if (frames.GetTensorTypeAndShapeInfo().GetElementCount() !=
static_cast<size_t>(num_frames) * n_fft_) {
SHERPA_ONNX_LOGE(
"The voxlock chunk model returned %d values. Expect %d",
static_cast<int32_t>(
frames.GetTensorTypeAndShapeInfo().GetElementCount()),
num_frames * n_fft_);
SHERPA_ONNX_EXIT(-1);
}
const int32_t done = num_frames * hop_length_;
overlap_add_.resize(n_fft_ + done, 0);
window_sum_.resize(n_fft_ + done, 0);
const float *p = frames.GetTensorData<float>();
for (int32_t j = 0; j != num_frames; ++j) {
float *dst = overlap_add_.data() + j * hop_length_;
float *dst_sum = window_sum_.data() + j * hop_length_;
for (int32_t i = 0; i != n_fft_; ++i) {
dst[i] += p[j * n_fft_ + i];
dst_sum[i] += window_square_[i];
}
}
const int32_t cut = std::min(num_to_drop_, done);
num_to_drop_ -= cut;
for (int32_t i = cut; i != done; ++i) {
ans.push_back(Normalize(i) * output_gain_);
}
overlap_add_.erase(overlap_add_.begin(), overlap_add_.begin() + done);
window_sum_.erase(window_sum_.begin(), window_sum_.begin() + done);
offset_ += done;
}
if (offset_ > static_cast<int32_t>(buffer_.size()) / 2) {
buffer_.erase(buffer_.begin(), buffer_.begin() + offset_);
offset_ = 0;
}
return ans;
}
private:
// Borrowed, not owned by this class. It must outlive this object.
const OfflineSpeechDenoiserVoxLockModel *model_;
int32_t n_fft_;
int32_t hop_length_;
int32_t pad_;
int32_t frames_per_chunk_;
float output_gain_;
Ort::MemoryInfo memory_info_;
Ort::Value enroll_mag_{nullptr};
Ort::Value context_{nullptr};
OfflineSpeechDenoiserVoxLockModel::States states_;
std::vector<float> window_square_;
// Input samples until there are enough to build the padding at the start
std::vector<float> head_;
// Padded input; samples before offset_ have been consumed
std::vector<float> buffer_;
int32_t offset_ = 0;
std::vector<float> overlap_add_;
std::vector<float> window_sum_;
// Number of output samples that belong to the padding at the start
int32_t num_to_drop_ = 0;
bool started_ = false;
};
} // namespace sherpa_onnx
#endif // SHERPA_ONNX_CSRC_SPEECH_DENOISER_VOXLOCK_STREAM_H_