Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 108 additions & 21 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,100 @@ use std::env;
use std::fs;
use std::path::PathBuf;

/// All models documented in `ceva-ip/DPDFNet@main:package/src/dpdfnet/models.py`.
/// Entries: `(registry_name, sample_rate_hz, description)`.
/// All models documented in `ceva-ip/DPDFNet@main:package/src/dpdfnet/models.py`,
/// plus the split-band variants of the 16 kHz networks.
///
/// There is deliberately no per-model block size here any more. Inference
/// runs on a worker thread, so the audio callback does the FFT, the
/// overlap-add and two buffer copies and nothing else — its cost no longer
/// depends on the model. `tests/callback_deadline.rs` holds every model to
/// every block from 10 ms up.
/// Entries: `(registry_name, model_dir, model_rate_hz, stft_rate_hz, description)`.
///
/// What still varies per model is whether the worker keeps up: 3.3 ms of
/// inference per 10 ms hop for DPDFNet-8, a third of a core. That is a
/// CPU-load question for the quality tier, and the plugin reports it on the
/// `Hops Total` and `Hops Enhanced` control ports rather than promising it
/// here.
const REGISTRY: &[(&str, usize, &str)] = &[
/// `model_dir` is where the IR files live under `model/`; it differs from
/// `registry_name` only for the split-band variants, which reuse the base
/// 16 kHz model's IR unchanged. `model_rate` is the rate the network was
/// trained at — it fixes the tensor geometry and the magnitude the model
/// expects. `stft_rate` is the rate the wrapper runs its STFT at, which is also
/// the host rate the plugin accepts.
///
/// A split-band variant pairs a 48 kHz `stft_rate` with a 16 kHz `model_rate`:
/// the STFT runs at 48 kHz (nfft 960), the network is fed the first 161 bins
/// (0–8 kHz — the exact frequencies it was trained on, once scaled to 16 kHz
/// magnitudes), and the wrapper reconstructs the band above 8 kHz. The result
/// is full-band output with no resampler, instead of the telephone-band output
/// the pure 16 kHz plugin gives inside a 48 kHz graph.
///
/// There is deliberately no per-model block size here. Inference runs on a
/// worker thread, so the audio callback does the FFT, the overlap-add and two
/// buffer copies and nothing else — its cost no longer depends on the model.
/// `tests/callback_deadline.rs` holds every model to every block from 10 ms up.
const REGISTRY: &[(&str, &str, usize, usize, &str)] = &[
(
"baseline",
"baseline",
16_000,
16_000,
"DPDFNet 16 kHz baseline (fastest, lowest compute)",
),
(
"dpdfnet2",
"dpdfnet2",
16_000,
16_000,
"DPDFNet-2 16 kHz (balanced quality/speed)",
),
("dpdfnet4", 16_000, "DPDFNet-4 16 kHz (higher quality)"),
(
"dpdfnet4",
"dpdfnet4",
16_000,
16_000,
"DPDFNet-4 16 kHz (higher quality)",
),
(
"dpdfnet8",
"dpdfnet8",
16_000,
16_000,
"DPDFNet-8 16 kHz (highest quality 16 kHz, offline only)",
),
(
"dpdfnet2_48khz_hr",
"dpdfnet2_48khz_hr",
48_000,
48_000,
"DPDFNet-2 48 kHz hi-res (full-band, balanced)",
),
(
"dpdfnet8_48khz_hr",
"dpdfnet8_48khz_hr",
48_000,
48_000,
"DPDFNet-8 48 kHz hi-res (full-band, highest quality, offline only)",
),
(
"baseline_sb",
"baseline",
16_000,
48_000,
"DPDFNet baseline split-band (full-band, lowest compute)",
),
(
"dpdfnet2_sb",
"dpdfnet2",
16_000,
48_000,
"DPDFNet-2 split-band (full-band, balanced, no resampler)",
),
(
"dpdfnet4_sb",
"dpdfnet4",
16_000,
48_000,
"DPDFNet-4 split-band (full-band, higher quality)",
),
(
"dpdfnet8_sb",
"dpdfnet8",
16_000,
48_000,
"DPDFNet-8 split-band (full-band, highest quality, offline only)",
),
];

fn fnv1a64(s: &str) -> u64 {
Expand All @@ -84,18 +137,21 @@ fn main() {

let entry = REGISTRY
.iter()
.find(|(name, _, _)| *name == model_name)
.find(|(name, ..)| *name == model_name)
.unwrap_or_else(|| {
let known: Vec<&str> = REGISTRY.iter().map(|(n, _, _)| *n).collect();
let known: Vec<&str> = REGISTRY.iter().map(|(n, ..)| *n).collect();
panic!(
"unknown DPDFNet model `{model_name}`; valid choices: {}",
known.join(", ")
);
});
let (name, sample_rate, description) = (entry.0, entry.1, entry.2);
let (name, model_subdir, model_rate, stft_rate, description) =
(entry.0, entry.1, entry.2, entry.3, entry.4);

let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR unset");
let model_dir = PathBuf::from(&manifest_dir).join("model").join(name);
let model_dir = PathBuf::from(&manifest_dir)
.join("model")
.join(model_subdir);
let xml_path = model_dir.join("model.xml");
let bin_path = model_dir.join("model.bin");
let state_path = model_dir.join("init_state.bin");
Expand All @@ -109,10 +165,32 @@ fn main() {
println!("cargo:rerun-if-changed={}", path.display());
}

// STFT geometry follows directly from sample_rate at frame_ms=20.0.
let win_len = sample_rate / 50;
// Two geometries. The MODEL band is fixed by the rate the network was
// trained at (frame_ms = 20.0): its win/hop/bins set the ONNX tensor shape
// and never change for a given IR. The STFT the wrapper actually runs is at
// `stft_rate` — equal to the model rate for a normal model, 48 kHz for a
// split-band variant. When they differ, the audio callback runs the bigger
// FFT, feeds the model the low `freq_bins`, and reconstructs the rest.
let win_len = model_rate / 50;
let hop_size = win_len / 2;
let freq_bins = win_len / 2 + 1;
let split_band = stft_rate != model_rate;
let stft_win_len = stft_rate / 50;
let stft_hop = stft_win_len / 2;
let stft_freq_bins = stft_win_len / 2 + 1;
// `SPECTRUM_SCALE` is `win_len / stft_win_len`: the magnitude the model
// expects vs. what a `stft_win_len` FFT produces for the same tone, since
// the window sum scales with its length. The low bins are divided by it
// going in and multiplied coming out. Emitted as a literal `1.0` for a
// normal build (a `320.0 / 320.0` would trip `clippy::eq_op`) and as the
// exact division for a split-band build (1/3 has no finite decimal form).
// Verified against the ONNX: the scaled 48 kHz low bins match native 16 kHz
// to −39 dB.
let spectrum_scale = if split_band {
format!("{win_len}.0 / {stft_win_len}.0")
} else {
"1.0".to_string()
};

// Recurrent state size = init_state.bin length / 4 (f32 little-endian).
let state_bytes = fs::metadata(&state_path)
Expand All @@ -136,11 +214,20 @@ fn main() {
r#"// AUTO-GENERATED by build.rs. Do not edit.

pub const MODEL_NAME: &str = "{name}";
pub const SAMPLE_RATE: usize = {sample_rate};
pub const SAMPLE_RATE: usize = {model_rate};
pub const WIN_LEN: usize = {win_len};
pub const HOP_SIZE: usize = {hop_size};
pub const FREQ_BINS: usize = {freq_bins};
pub const STATE_SIZE: usize = {state_size};

// Host / STFT geometry. Equal to the model geometry for a normal build; for a
// split-band variant the host runs at 48 kHz while FREQ_BINS stays the model's.
pub const HOST_SAMPLE_RATE: usize = {stft_rate};
pub const STFT_WIN_LEN: usize = {stft_win_len};
pub const STFT_HOP: usize = {stft_hop};
pub const STFT_FREQ_BINS: usize = {stft_freq_bins};
pub const SPLIT_BAND: bool = {split_band};
pub const SPECTRUM_SCALE: f32 = {spectrum_scale};
pub const LADSPA_LABEL: &str = "{label}";
pub const LADSPA_NAME: &str = "{display}";
pub const LADSPA_UNIQUE_ID: u64 = {unique_id};
Expand Down
43 changes: 42 additions & 1 deletion pkgbuild/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pkgname=(
'dpdfnet-ladspa-dpdfnet8'
'dpdfnet-ladspa-dpdfnet2-48khz-hr'
'dpdfnet-ladspa-dpdfnet8-48khz-hr'
'dpdfnet-ladspa-baseline-sb'
'dpdfnet-ladspa-dpdfnet2-sb'
'dpdfnet-ladspa-dpdfnet4-sb'
'dpdfnet-ladspa-dpdfnet8-sb'
)
pkgver=$(date +%y.%m.%d)
pkgrel=$(date +%H%M)
Expand Down Expand Up @@ -71,6 +75,17 @@ _models=(
'dpdfnet8_48khz_hr'
)

# Split-band variants. They reuse the 16 kHz models' IR, so they need no ONNX
# of their own and no prepare() step — only a build(), after the base models
# above have populated `model/<name>/`. Kept out of `_models` because that
# array is index-aligned with source=/sha256sums=.
_split_band=(
'baseline_sb'
'dpdfnet2_sb'
'dpdfnet4_sb'
'dpdfnet8_sb'
)

source=(
# Repo itself. Build runners (BigLinux build-package, makechrootpkg)
# only copy PKGBUILD into the chroot, so the crate sources must
Expand Down Expand Up @@ -136,7 +151,7 @@ build() {
# `cargo:rerun-if-env-changed=DPDFNET_MODEL`, but separate target
# trees make the per-model artifact location unambiguous for the
# package_*() install steps below).
for model in "${_models[@]}"; do
for model in "${_models[@]}" "${_split_band[@]}"; do
msg "Building libdpdfnet_${model}_ladspa.so"
DPDFNET_MODEL="${model}" \
CARGO_TARGET_DIR="target-${model}" \
Expand Down Expand Up @@ -213,3 +228,29 @@ package_dpdfnet-ladspa-dpdfnet8-48khz-hr() {
depends=("${_split_depends[@]}")
_install_model dpdfnet8_48khz_hr "${pkgname}"
}

# Split-band variants: the 16 kHz networks run at 48 kHz with the band above
# 8 kHz reconstructed, so they are full-band and need no resampler in the graph.
package_dpdfnet-ladspa-baseline-sb() {
pkgdesc='DPDFNet baseline split-band LADSPA plugin (full-band, lowest compute)'
depends=("${_split_depends[@]}")
_install_model baseline_sb "${pkgname}"
}

package_dpdfnet-ladspa-dpdfnet2-sb() {
pkgdesc='DPDFNet-2 split-band LADSPA plugin (full-band, balanced, no resampler)'
depends=("${_split_depends[@]}")
_install_model dpdfnet2_sb "${pkgname}"
}

package_dpdfnet-ladspa-dpdfnet4-sb() {
pkgdesc='DPDFNet-4 split-band LADSPA plugin (full-band, higher quality)'
depends=("${_split_depends[@]}")
_install_model dpdfnet4_sb "${pkgname}"
}

package_dpdfnet-ladspa-dpdfnet8-sb() {
pkgdesc='DPDFNet-8 split-band LADSPA plugin (full-band, highest quality)'
depends=("${_split_depends[@]}")
_install_model dpdfnet8_sb "${pkgname}"
}
Loading