use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
println!("cargo:rerun-if-changed=build.rs");
gen_annex_h(&out_dir);
gen_annex_e(&out_dir);
gen_annex_f(&out_dir);
gen_annex_g(&out_dir);
gen_annex_b(&out_dir);
gen_annex_c(&out_dir);
gen_annex_d(&out_dir);
gen_annex_i(&out_dir);
gen_annex_j(&out_dir);
gen_annex_s(&out_dir);
gen_annex_l(&out_dir);
gen_annex_m(&out_dir);
gen_annex_n(&out_dir);
gen_annex_o(&out_dir);
gen_annex_p(&out_dir);
gen_annex_q(&out_dir);
gen_annex_r(&out_dir);
gen_annex_t(&out_dir);
gen_imbe_bit_prioritization(&out_dir);
gen_ambe_bit_prioritization(&out_dir);
}
fn gen_annex_h(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_h_interleave.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut entries: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(72);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("symbol") {
continue; }
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
5,
"Annex H line {}: expected 5 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let symbol: usize = cols[0].parse().expect("symbol index");
let bit1_vec: u8 = cols[1].parse().expect("bit1_vector");
let bit1_idx: u8 = cols[2].parse().expect("bit1_index");
let bit0_vec: u8 = cols[3].parse().expect("bit0_vector");
let bit0_idx: u8 = cols[4].parse().expect("bit0_index");
assert_eq!(
symbol,
entries.len(),
"Annex H symbols must be sequential 0..72"
);
assert!(bit1_vec < 8 && bit0_vec < 8, "vector index out of range");
entries.push((bit1_vec, bit1_idx, bit0_vec, bit0_idx));
}
assert_eq!(
entries.len(),
72,
"Annex H must have 72 symbols, got {}",
entries.len()
);
const VECTOR_LENGTHS: [u8; 8] = [23, 23, 23, 23, 15, 15, 15, 7];
let mut seen = [[false; 23]; 8];
for (bit1_vec, bit1_idx, bit0_vec, bit0_idx) in &entries {
for &(v, i) in &[(*bit1_vec, *bit1_idx), (*bit0_vec, *bit0_idx)] {
assert!(
i < VECTOR_LENGTHS[v as usize],
"Annex H: vec {v} index {i} exceeds vector length"
);
assert!(
!seen[v as usize][i as usize],
"Annex H: (vec {v}, idx {i}) appears more than once"
);
seen[v as usize][i as usize] = true;
}
}
for (v, row) in seen.iter().enumerate() {
for (i, &b) in row.iter().enumerate().take(VECTOR_LENGTHS[v] as usize) {
assert!(b, "Annex H: (vec {v}, idx {i}) never appears");
}
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_h_interleave.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const ANNEX_H: [AnnexHEntry; 72] = [\n");
for (bit1_vec, bit1_idx, bit0_vec, bit0_idx) in &entries {
out.push_str(&format!(
" AnnexHEntry {{ bit1_vec: {bit1_vec}, bit1_idx: {bit1_idx}, \
bit0_vec: {bit0_vec}, bit0_idx: {bit0_idx} }},\n"
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_h.rs"), out).expect("write annex_h.rs");
}
fn gen_annex_e(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_e_gain_quantizer.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut levels: Vec<f32> = Vec::with_capacity(64);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("b2_index") {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
2,
"annex E line {}: expected 2 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let idx: usize = cols[0].parse().expect("b2_index");
let level: f32 = cols[1].parse().expect("level");
assert_eq!(idx, levels.len(), "Annex E rows must be sequential 0..64");
levels.push(level);
}
assert_eq!(levels.len(), 64, "Annex E must have exactly 64 entries");
for w in levels.windows(2) {
assert!(w[0] < w[1], "Annex E levels must be strictly increasing");
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_e_gain_quantizer.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("/// IMBE full-rate `b̂₂` gain levels (Annex E).\n");
out.push_str("pub const IMBE_GAIN_LEVELS: [f32; 64] = [\n");
for (i, level) in levels.iter().enumerate() {
out.push_str(&format!(" {level:.6}, // b̂₂ = {i}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_e_gain.rs"), out).expect("write annex_e_gain.rs");
}
fn gen_annex_f(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_f_gain_allocation.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut table: Vec<Vec<(u8, f32)>> = (0..48).map(|_| Vec::with_capacity(5)).collect();
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('L') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
4,
"annex F line {}: expected 4 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let l: u8 = cols[0].parse().expect("L");
let m: u8 = cols[1].parse().expect("m");
let b_m: u8 = cols[2].parse().expect("B_m");
let delta_m: f32 = cols[3].parse().expect("Delta_m");
assert!((9..=56).contains(&l), "annex F: L={l} out of range");
assert!((3..=7).contains(&m), "annex F: m={m} out of range");
assert!(b_m >= 1 && b_m <= 10, "annex F: B_m={b_m} out of range");
assert!(delta_m > 0.0, "annex F: delta_m must be positive");
let l_idx = (l - 9) as usize;
let m_idx = (m - 3) as usize;
assert_eq!(
table[l_idx].len(),
m_idx,
"annex F rows must be sorted by (L, m)"
);
table[l_idx].push((b_m, delta_m));
}
for (i, row) in table.iter().enumerate() {
assert_eq!(row.len(), 5, "annex F: L={} expected 5 entries", i + 9);
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_f_gain_allocation.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const IMBE_GAIN_ALLOC: [[GainAlloc; 5]; 48] = [\n");
for (l_idx, row) in table.iter().enumerate() {
out.push_str(&format!(" // L = {}\n [\n", l_idx + 9));
for (b_m, delta_m) in row {
out.push_str(&format!(
" GainAlloc {{ b_m: {b_m}, delta_m: {delta_m:.6} }},\n"
));
}
out.push_str(" ],\n");
}
out.push_str("];\n");
fs::write(out_dir.join("annex_f_gain_alloc.rs"), out).expect("write annex_f_gain_alloc.rs");
}
fn gen_annex_g(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_g_hoc_allocation.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut per_l: Vec<Vec<(u8, u8, u8, u8)>> = (0..48).map(|_| Vec::new()).collect();
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('L') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
5,
"annex G line {}: expected 5 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let l: u8 = cols[0].parse().expect("L");
let c_i: u8 = cols[1].parse().expect("C_i");
let c_k: u8 = cols[2].parse().expect("C_k");
let b_m: u8 = cols[3].parse().expect("b_m");
let b_m_bits: u8 = cols[4].parse().expect("B_m");
assert!((9..=56).contains(&l), "annex G: L={l} out of range");
assert!((1..=6).contains(&c_i), "annex G: C_i={c_i} out of range");
assert!(c_k >= 2, "annex G: C_k={c_k} below 2");
assert!(b_m_bits <= 10, "annex G: B_m={b_m_bits} exceeds 10");
per_l[(l - 9) as usize].push((c_i, c_k, b_m, b_m_bits));
}
let mut total = 0usize;
for (i, rows) in per_l.iter().enumerate() {
let l = (i + 9) as u8;
let expected = (l - 6) as usize;
assert_eq!(
rows.len(),
expected,
"annex G: L={l} expected {expected} rows, got {}",
rows.len()
);
for (j, row) in rows.iter().enumerate() {
let expected_bm = 8 + j as u8;
assert_eq!(row.2, expected_bm, "annex G: L={l} row {j} b_m mismatch");
}
total += rows.len();
}
assert_eq!(total, 1272, "annex G: total row count mismatch");
let mut flat: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(total);
let mut offsets: [(u32, u32); 48] = [(0, 0); 48];
for (i, rows) in per_l.iter().enumerate() {
let off = flat.len() as u32;
let len = rows.len() as u32;
offsets[i] = (off, len);
flat.extend_from_slice(rows);
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_g_hoc_allocation.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str(&format!(
"pub(crate) const IMBE_HOC_ENTRIES: [HocAlloc; {}] = [\n",
flat.len()
));
for (c_i, c_k, b_m, b_m_bits) in &flat {
out.push_str(&format!(
" HocAlloc {{ c_i: {c_i}, c_k: {c_k}, b_m: {b_m}, b_m_bits: {b_m_bits} }},\n"
));
}
out.push_str("];\n\n");
out.push_str("/// `(offset, len)` pairs into IMBE_HOC_ENTRIES, indexed by `L − 9`.\n");
out.push_str("pub(crate) const IMBE_HOC_OFFSETS: [(u32, u32); 48] = [\n");
for (off, len) in &offsets {
out.push_str(&format!(" ({off}, {len}),\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_g_hoc_alloc.rs"), out).expect("write annex_g_hoc_alloc.rs");
}
fn gen_annex_b(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_b_analysis_window.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut samples: Vec<(i32, f32)> = Vec::with_capacity(301);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('n') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
2,
"annex B line {}: expected 2 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let n: i32 = cols[0].parse().expect("n");
let w: f32 = cols[1].parse().expect("wI_n");
assert!((-150..=150).contains(&n), "annex B: n={n} out of range");
let expected_idx = (n + 150) as usize;
assert_eq!(
samples.len(),
expected_idx,
"annex B rows must be sequential n = −150..=150"
);
assert!(w >= 0.0, "annex B: wI({n}) = {w} negative");
samples.push((n, w));
}
assert_eq!(samples.len(), 301, "Annex B must have 301 entries");
for k in 1..=150 {
let a = samples[(150 - k) as usize].1;
let b = samples[(150 + k) as usize].1;
assert!(
(a - b).abs() < 1e-5,
"annex B: wI({}) = {a} != wI({}) = {b}",
-k,
k
);
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_b_analysis_window.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("/// Length of the initial pitch-estimation window (n = −150..=150).\n");
out.push_str("/// Annex B analysis-window length (samples).\n");
out.push_str("pub const ANALYSIS_WINDOW_LEN: usize = 301;\n\n");
out.push_str("/// Analysis window wI(n) per BABA-A Annex B.\n");
out.push_str("/// Indexed `[n + 150]` so `IMBE_ANALYSIS_WINDOW[0] = wI(−150)`.\n");
out.push_str("/// IMBE Annex B analysis window `wA(n)` for n ∈ [-150, 150].\n");
out.push_str("pub const IMBE_ANALYSIS_WINDOW: [f32; ANALYSIS_WINDOW_LEN] = [\n");
for (n, w) in &samples {
out.push_str(&format!(" {w:.8}, // n = {n}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_b_analysis_window.rs"), out)
.expect("write annex_b_analysis_window.rs");
}
fn gen_annex_c(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_c_pitch_refinement_window.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut samples: Vec<(i32, f32)> = Vec::with_capacity(221);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('n') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
2,
"annex C line {}: expected 2 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let n: i32 = cols[0].parse().expect("n");
let w: f32 = cols[1].parse().expect("wR_n");
assert!((-110..=110).contains(&n), "annex C: n={n} out of range");
let expected_idx = (n + 110) as usize;
assert_eq!(
samples.len(),
expected_idx,
"annex C rows must be sequential n = −110..=110"
);
assert!(w >= 0.0, "annex C: wR({n}) = {w} negative");
samples.push((n, w));
}
assert_eq!(samples.len(), 221, "Annex C must have 221 entries");
for k in 1..=110 {
let a = samples[(110 - k) as usize].1;
let b = samples[(110 + k) as usize].1;
assert!(
(a - b).abs() < 1e-5,
"annex C: wR({}) = {a} != wR({}) = {b}",
-k,
k
);
}
assert!(
(samples[110].1 - 1.0).abs() < 1e-6,
"annex C: expected wR(0) = 1.0, got {}",
samples[110].1
);
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_c_pitch_refinement_window.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("/// Length of the pitch refinement window (n = −110..=110).\n");
out.push_str("/// Annex C pitch-refinement window length (samples).\n");
out.push_str("pub const REFINEMENT_WINDOW_LEN: usize = 221;\n\n");
out.push_str("/// Pitch refinement window wR(n) per BABA-A Annex C.\n");
out.push_str("/// Indexed `[n + 110]` so `IMBE_REFINEMENT_WINDOW[0] = wR(−110)`.\n");
out.push_str("/// IMBE Annex C pitch-refinement window `wR(n)` for n ∈ [-110, 110].\n");
out.push_str("pub const IMBE_REFINEMENT_WINDOW: [f32; REFINEMENT_WINDOW_LEN] = [\n");
for (n, w) in &samples {
out.push_str(&format!(" {w:.8}, // n = {n}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_c_refinement_window.rs"), out)
.expect("write annex_c_refinement_window.rs");
}
fn gen_annex_d(out_dir: &PathBuf) {
let taps: [(i32, f64); 21] = [
(-10, -0.002898),
(-9, -0.002831),
(-8, 0.005666),
(-7, 0.016601),
(-6, 0.008800),
(-5, -0.026955),
(-4, -0.055990),
(-3, -0.015116),
(-2, 0.118754),
(-1, 0.278990),
(0, 0.351338),
(1, 0.278990),
(2, 0.118754),
(3, -0.015116),
(4, -0.055990),
(5, -0.026955),
(6, 0.008800),
(7, 0.016601),
(8, 0.005666),
(9, -0.002831),
(10, -0.002898),
];
for k in 1..=10 {
let a = taps[(10 - k) as usize].1;
let b = taps[(10 + k) as usize].1;
assert!(
(a - b).abs() < 1e-9,
"annex D: h_LPF({}) = {a} != h_LPF({}) = {b}",
-k,
k
);
}
let mut out = String::new();
out.push_str("// Auto-generated from BABA-A Annex D via the implementation spec.\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("/// Length of the Annex D FIR low-pass filter (n = −10..=10).\n");
out.push_str("/// Annex D pitch-tracker LPF length (taps).\n");
out.push_str("pub const ANNEX_D_LPF_LEN: usize = 21;\n\n");
out.push_str("/// Annex D FIR low-pass filter h_LPF(n) used in initial pitch\n");
out.push_str("/// autocorrelation. Indexed `[n + 10]`.\n");
out.push_str("/// IMBE Annex D pitch-tracker LPF taps `h_LPF(j)` for j ∈ [-10, 10].\n");
out.push_str("pub const IMBE_ANNEX_D_LPF: [f32; ANNEX_D_LPF_LEN] = [\n");
for (n, h) in &taps {
out.push_str(&format!(" {h:.6}, // n = {n}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_d_lpf.rs"), out)
.expect("write annex_d_lpf.rs");
}
fn gen_annex_i(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_i_synthesis_window.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut samples: Vec<(i32, f32)> = Vec::with_capacity(211);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('n') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
2,
"annex I line {}: expected 2 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let n: i32 = cols[0].parse().expect("n");
let w: f32 = cols[1].parse().expect("wS_n");
assert!((-105..=105).contains(&n), "annex I: n={n} out of range");
let expected_idx = (n + 105) as usize;
assert_eq!(
samples.len(),
expected_idx,
"annex I rows must be sequential n = −105..=105"
);
assert!(w >= 0.0, "annex I: wS({n}) = {w} negative");
samples.push((n, w));
}
assert_eq!(samples.len(), 211, "Annex I must have 211 entries");
for k in 1..=105 {
let a = samples[(105 - k) as usize].1;
let b = samples[(105 + k) as usize].1;
assert!(
(a - b).abs() < 1e-6,
"annex I: wS({}) = {a} != wS({}) = {b}",
-k,
k
);
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_i_synthesis_window.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("/// Length of the speech synthesis window (n = −105..=105).\n");
out.push_str("/// Annex I synthesis-window length (samples).\n");
out.push_str("pub const SYNTH_WINDOW_LEN: usize = 211;\n\n");
out.push_str("/// Speech synthesis window wS(n) per BABA-A Annex I.\n");
out.push_str("/// Indexed `[n + 105]` so `IMBE_SYNTH_WINDOW[0] = wS(−105)`.\n");
out.push_str("/// IMBE Annex I synthesis window `wS(n)` for n ∈ [-105, 105].\n");
out.push_str("pub const IMBE_SYNTH_WINDOW: [f32; SYNTH_WINDOW_LEN] = [\n");
for (n, w) in &samples {
out.push_str(&format!(" {w:.6}, // n = {n}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_i_synth_window.rs"), out)
.expect("write annex_i_synth_window.rs");
}
fn gen_annex_j(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_j_block_lengths.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut blocks: Vec<[u8; 6]> = Vec::with_capacity(48);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('L') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
7,
"annex J line {}: expected 7 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let l: u8 = cols[0].parse().expect("L");
assert!((9..=56).contains(&l));
assert_eq!((l - 9) as usize, blocks.len(), "annex J rows must be sequential");
let mut row = [0u8; 6];
for i in 0..6 {
row[i] = cols[i + 1].parse().expect("J_i");
}
let sum: u32 = row.iter().map(|&x| x as u32).sum();
assert_eq!(sum, l as u32, "annex J: L={l}, sum(J)={sum}");
let lo = l / 6;
let hi = (l + 5) / 6;
for &j in &row {
assert!(j >= lo && j <= hi, "annex J: L={l}, J̃ out of range");
}
for i in 0..5 {
assert!(row[i] <= row[i + 1], "annex J: L={l} blocks not non-decreasing");
}
blocks.push(row);
}
assert_eq!(blocks.len(), 48, "annex J: expected 48 rows");
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_j_block_lengths.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const IMBE_BLOCK_LENGTHS: [[u8; 6]; 48] = [\n");
for (l_idx, row) in blocks.iter().enumerate() {
out.push_str(&format!(
" [{}, {}, {}, {}, {}, {}], // L = {}\n",
row[0], row[1], row[2], row[3], row[4], row[5], l_idx + 9
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_j_blocks.rs"), out).expect("write annex_j_blocks.rs");
}
fn gen_annex_s(out_dir: &PathBuf) {
let csv_path = "spec_tables/annex_s_interleave.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut entries: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(36);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("symbol") {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
5,
"Annex S line {}: expected 5 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let symbol: usize = cols[0].parse().expect("symbol");
let bit1_vec: u8 = cols[1].parse().expect("bit1_vector");
let bit1_idx: u8 = cols[2].parse().expect("bit1_index");
let bit0_vec: u8 = cols[3].parse().expect("bit0_vector");
let bit0_idx: u8 = cols[4].parse().expect("bit0_index");
assert_eq!(symbol, entries.len(), "Annex S symbols must be sequential 0..36");
assert!(bit1_vec < 4 && bit0_vec < 4, "vector index out of range");
entries.push((bit1_vec, bit1_idx, bit0_vec, bit0_idx));
}
assert_eq!(entries.len(), 36, "Annex S must have 36 symbols");
const AMBE_CODE_WIDTHS: [u8; 4] = [24, 23, 11, 14];
let mut seen = [[false; 24]; 4];
for (bit1_vec, bit1_idx, bit0_vec, bit0_idx) in &entries {
for &(v, i) in &[(*bit1_vec, *bit1_idx), (*bit0_vec, *bit0_idx)] {
assert!(
i < AMBE_CODE_WIDTHS[v as usize],
"Annex S: vec {v} idx {i} exceeds width"
);
assert!(!seen[v as usize][i as usize], "Annex S: ({v}, {i}) twice");
seen[v as usize][i as usize] = true;
}
}
for (v, row) in seen.iter().enumerate() {
for (i, &b) in row.iter().enumerate().take(AMBE_CODE_WIDTHS[v] as usize) {
assert!(b, "Annex S: ({v}, {i}) never appears");
}
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_s_interleave.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const ANNEX_S: [AnnexSEntry; 36] = [\n");
for (bit1_vec, bit1_idx, bit0_vec, bit0_idx) in &entries {
out.push_str(&format!(
" AnnexSEntry {{ bit1_vec: {bit1_vec}, bit1_idx: {bit1_idx}, \
bit0_vec: {bit0_vec}, bit0_idx: {bit0_idx} }},\n"
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_s.rs"), out).expect("write annex_s.rs");
}
fn parse_csv_rows(
csv_path: &str,
expected_cols: usize,
mut row_fn: impl FnMut(usize, &[&str]),
) -> usize {
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut count = 0;
let mut saw_header = false;
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if !saw_header && line.chars().next().map_or(false, |c| c.is_ascii_alphabetic()) {
saw_header = true;
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
expected_cols,
"{csv_path} line {}: expected {expected_cols} cols, got {}",
lineno + 1,
cols.len()
);
row_fn(count, &cols);
count += 1;
}
count
}
fn gen_annex_l(out_dir: &PathBuf) {
let two_pi = 2.0 * std::f32::consts::PI;
let mut entries: Vec<(u8, u8, f32)> = Vec::with_capacity(120);
parse_csv_rows(
"spec_tables/annex_l_pitch_table.csv",
3,
|row_idx, cols| {
let b0: u8 = cols[0].parse().expect("b0");
let l: u8 = cols[1].parse().expect("L");
let w_cycles: f32 = cols[2].parse().expect("omega_0");
assert_eq!(b0 as usize, row_idx, "Annex L must be sequential");
assert!((9..=56).contains(&l), "Annex L L={l} out of range");
assert!(w_cycles > 0.0, "Annex L omega_0 must be positive");
entries.push((b0, l, w_cycles * two_pi));
},
);
assert_eq!(entries.len(), 120, "Annex L must have 120 entries");
for w in entries.windows(2) {
assert!(w[0].2 > w[1].2, "Annex L ω₀ not monotone decreasing");
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_l_pitch_table.csv\n");
out.push_str("// CSV stores cycles/sample; values converted to rad/sample at load.\n");
out.push_str("/// AMBE+2 half-rate Annex L pitch quantization table (120 entries indexed by `b̂₀`).\n");
out.push_str("pub const AMBE_PITCH_TABLE: [PitchEntry; 120] = [\n");
for (_, l, w) in &entries {
out.push_str(&format!(" PitchEntry {{ l: {l}, omega_0: {w:.6} }},\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_l_pitch.rs"), out).expect("write annex_l_pitch.rs");
}
fn gen_annex_m(out_dir: &PathBuf) {
let mut rows: Vec<[u8; 8]> = Vec::with_capacity(32);
parse_csv_rows(
"spec_tables/annex_m_vuv_codebook.csv",
9,
|row_idx, cols| {
let b1: u8 = cols[0].parse().expect("b1");
assert_eq!(b1 as usize, row_idx);
let mut v = [0u8; 8];
for i in 0..8 {
let bit: u8 = cols[i + 1].parse().expect("v_k");
assert!(bit <= 1, "V/UV bit must be 0 or 1");
v[i] = bit;
}
rows.push(v);
},
);
assert_eq!(rows.len(), 32);
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_m_vuv_codebook.csv\n");
out.push_str("/// AMBE+2 half-rate Annex M V/UV codebook (32 codewords × 8 bands).\n");
out.push_str("pub const AMBE_VUV_CODEBOOK: [[bool; 8]; 32] = [\n");
for row in &rows {
out.push_str(&format!(
" [{}, {}, {}, {}, {}, {}, {}, {}],\n",
row[0] == 1, row[1] == 1, row[2] == 1, row[3] == 1,
row[4] == 1, row[5] == 1, row[6] == 1, row[7] == 1,
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_m_vuv.rs"), out).expect("write annex_m_vuv.rs");
}
fn gen_annex_n(out_dir: &PathBuf) {
let mut rows: Vec<[u8; 4]> = Vec::with_capacity(48);
parse_csv_rows(
"spec_tables/annex_n_block_lengths.csv",
5,
|row_idx, cols| {
let l: u8 = cols[0].parse().expect("L");
assert_eq!((l - 9) as usize, row_idx);
let mut r = [0u8; 4];
for i in 0..4 {
r[i] = cols[i + 1].parse().expect("J_i");
}
let sum: u32 = r.iter().map(|&x| x as u32).sum();
assert_eq!(sum, l as u32, "Annex N L={l}: Σ J̃_i = {sum}");
rows.push(r);
},
);
assert_eq!(rows.len(), 48);
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_n_block_lengths.csv\n");
out.push_str("/// AMBE+2 half-rate Annex N block-length table (48 L-values × 4 blocks).\n");
out.push_str("pub const AMBE_BLOCK_LENGTHS: [[u8; 4]; 48] = [\n");
for (l_idx, r) in rows.iter().enumerate() {
out.push_str(&format!(
" [{}, {}, {}, {}], // L = {}\n",
r[0], r[1], r[2], r[3], l_idx + 9
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_n_blocks.rs"), out).expect("write annex_n_blocks.rs");
}
fn gen_annex_o(out_dir: &PathBuf) {
let mut levels: Vec<f32> = Vec::with_capacity(32);
parse_csv_rows(
"spec_tables/annex_o_gain_quantizer.csv",
2,
|row_idx, cols| {
let b2: u8 = cols[0].parse().expect("b2");
assert_eq!(b2 as usize, row_idx);
let lvl: f32 = cols[1].parse().expect("gain_level");
levels.push(lvl);
},
);
assert_eq!(levels.len(), 32);
for w in levels.windows(2) {
assert!(w[0] < w[1], "Annex O not monotone increasing");
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_o_gain_quantizer.csv\n");
out.push_str("/// AMBE+2 half-rate Annex O differential-gain levels (32 entries indexed by `b̂₂`).\n");
out.push_str("pub const AMBE_GAIN_LEVELS: [f32; 32] = [\n");
for (i, lvl) in levels.iter().enumerate() {
out.push_str(&format!(" {lvl:.6}, // b̂₂ = {i}\n"));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_o_gain.rs"), out).expect("write annex_o_gain.rs");
}
fn gen_annex_p(out_dir: &PathBuf) {
let mut rows: Vec<[f32; 3]> = Vec::with_capacity(512);
parse_csv_rows(
"spec_tables/annex_p_prba24_codebook.csv",
4,
|row_idx, cols| {
let b3: u16 = cols[0].parse().expect("b3");
assert_eq!(b3 as usize, row_idx);
rows.push([
cols[1].parse().expect("G2"),
cols[2].parse().expect("G3"),
cols[3].parse().expect("G4"),
]);
},
);
assert_eq!(rows.len(), 512);
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_p_prba24_codebook.csv\n");
out.push_str("/// AMBE+2 half-rate Annex P PRBA[2..4] codebook (512 entries × 3 components).\n");
out.push_str("pub const AMBE_PRBA24: [[f32; 3]; 512] = [\n");
for r in &rows {
out.push_str(&format!(" [{:.6}, {:.6}, {:.6}],\n", r[0], r[1], r[2]));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_p_prba24.rs"), out).expect("write annex_p_prba24.rs");
}
fn gen_annex_q(out_dir: &PathBuf) {
let mut rows: Vec<[f32; 4]> = Vec::with_capacity(128);
parse_csv_rows(
"spec_tables/annex_q_prba58_codebook.csv",
5,
|row_idx, cols| {
let b4: u8 = cols[0].parse().expect("b4");
assert_eq!(b4 as usize, row_idx);
rows.push([
cols[1].parse().expect("G5"),
cols[2].parse().expect("G6"),
cols[3].parse().expect("G7"),
cols[4].parse().expect("G8"),
]);
},
);
assert_eq!(rows.len(), 128);
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_q_prba58_codebook.csv\n");
out.push_str("/// AMBE+2 half-rate Annex Q PRBA[5..8] codebook (128 entries × 4 components).\n");
out.push_str("pub const AMBE_PRBA58: [[f32; 4]; 128] = [\n");
for r in &rows {
out.push_str(&format!(
" [{:.6}, {:.6}, {:.6}, {:.6}],\n",
r[0], r[1], r[2], r[3]
));
}
out.push_str("];\n");
fs::write(out_dir.join("annex_q_prba58.rs"), out).expect("write annex_q_prba58.rs");
}
fn gen_annex_r(out_dir: &PathBuf) {
let specs = [
("spec_tables/annex_r_hoc_b5.csv", 32usize, "AMBE_HOC_B5", "b5"),
("spec_tables/annex_r_hoc_b6.csv", 16, "AMBE_HOC_B6", "b6"),
("spec_tables/annex_r_hoc_b7.csv", 16, "AMBE_HOC_B7", "b7"),
("spec_tables/annex_r_hoc_b8.csv", 8, "AMBE_HOC_B8", "b8"),
];
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_r_hoc_b{5..8}.csv\n");
for (path, expected_count, const_name, col_label) in specs {
let mut rows: Vec<[f32; 4]> = Vec::with_capacity(expected_count);
parse_csv_rows(path, 5, |row_idx, cols| {
let b: u16 = cols[0].parse().expect(col_label);
assert_eq!(b as usize, row_idx);
rows.push([
cols[1].parse().expect("H_i,1"),
cols[2].parse().expect("H_i,2"),
cols[3].parse().expect("H_i,3"),
cols[4].parse().expect("H_i,4"),
]);
});
assert_eq!(rows.len(), expected_count, "{path}: row count");
out.push_str(&format!(
"/// AMBE+2 half-rate Annex R higher-order-coefficient codebook \
{const_name} (`b̂` = {col_label}).\n"
));
out.push_str(&format!(
"pub const {const_name}: [[f32; 4]; {expected_count}] = [\n"
));
for r in &rows {
out.push_str(&format!(
" [{:.6}, {:.6}, {:.6}, {:.6}],\n",
r[0], r[1], r[2], r[3]
));
}
out.push_str("];\n\n");
}
fs::write(out_dir.join("annex_r_hoc.rs"), out).expect("write annex_r_hoc.rs");
}
fn gen_annex_t(out_dir: &PathBuf) {
let mut rows: Vec<(u8, f32, u8, u8)> = Vec::new();
parse_csv_rows(
"spec_tables/annex_t_tone_params.csv",
4,
|_row_idx, cols| {
let id: u8 = cols[0].parse().expect("tone_id");
let f0: f32 = cols[1].parse().expect("f0");
let l1: u8 = cols[2].parse().expect("l1");
let l2: u8 = cols[3].parse().expect("l2");
rows.push((id, f0, l1, l2));
},
);
assert!(!rows.is_empty(), "Annex T must have at least one row");
for w in rows.windows(2) {
assert!(w[0].0 < w[1].0, "Annex T IDs not strictly increasing");
}
let mut table: [Option<(f32, u8, u8)>; 256] = [None; 256];
for (id, f0, l1, l2) in &rows {
table[*id as usize] = Some((*f0, *l1, *l2));
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/annex_t_tone_params.csv\n");
out.push_str("/// AMBE+2 half-rate Annex T tone-frame parameter table indexed by `I_D` (256 entries; `None` for reserved IDs).\n");
out.push_str("pub const ANNEX_T: [Option<ToneParams>; 256] = [\n");
for entry in &table {
match entry {
None => out.push_str(" None,\n"),
Some((f0, l1, l2)) => out.push_str(&format!(
" Some(ToneParams {{ f0: {f0:.4}, l1: {l1}, l2: {l2} }}),\n"
)),
}
}
out.push_str("];\n");
fs::write(out_dir.join("annex_t_tones.rs"), out).expect("write annex_t_tones.rs");
}
fn gen_imbe_bit_prioritization(out_dir: &PathBuf) {
let csv_path = "spec_tables/imbe_bit_prioritization.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut per_l: Vec<Vec<(u8, u8, u8, u8)>> = (0..48).map(|_| Vec::with_capacity(88)).collect();
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('L') {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
5,
"imbe prioritization line {}: expected 5 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let l: u8 = cols[0].parse().expect("L");
let src_param: u8 = cols[1].parse().expect("src_param");
let src_bit: u8 = cols[2].parse().expect("src_bit");
let dst_vec: u8 = cols[3].parse().expect("dst_vec");
let dst_bit: u8 = cols[4].parse().expect("dst_bit");
assert!((9..=56).contains(&l), "L={l} out of range");
assert!(dst_vec < 8, "dst_vec out of range");
let dst_width = [12u8, 12, 12, 12, 11, 11, 11, 7][dst_vec as usize];
assert!(dst_bit < dst_width, "dst_bit {dst_bit} exceeds vec {dst_vec} width");
per_l[(l - 9) as usize].push((src_param, src_bit, dst_vec, dst_bit));
}
for (i, rows) in per_l.iter().enumerate() {
assert_eq!(rows.len(), 88, "L={}: expected 88 rows, got {}", i + 9, rows.len());
let mut seen = [[false; 12]; 8];
for (_, _, v, b) in rows {
assert!(!seen[*v as usize][*b as usize],
"L={}: (dst_vec={v}, dst_bit={b}) appears twice", i + 9);
seen[*v as usize][*b as usize] = true;
}
let widths = [12u8, 12, 12, 12, 11, 11, 11, 7];
for (v, w) in widths.iter().enumerate() {
for b in 0..*w {
assert!(seen[v][b as usize],
"L={}: (dst_vec={v}, dst_bit={b}) never appears", i + 9);
}
}
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/imbe_bit_prioritization.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const IMBE_BIT_MAP: [[BitMap; 88]; 48] = [\n");
for (l_idx, rows) in per_l.iter().enumerate() {
out.push_str(&format!(" // L = {}\n [\n", l_idx + 9));
for (sp, sb, dv, db) in rows {
out.push_str(&format!(
" BitMap {{ src_param: {sp}, src_bit: {sb}, dst_vec: {dv}, dst_bit: {db} }},\n"
));
}
out.push_str(" ],\n");
}
out.push_str("];\n");
fs::write(out_dir.join("imbe_bit_priority.rs"), out).expect("write imbe_bit_priority.rs");
}
fn gen_ambe_bit_prioritization(out_dir: &PathBuf) {
let csv_path = "spec_tables/ambe_bit_prioritization.csv";
println!("cargo:rerun-if-changed={csv_path}");
let content = fs::read_to_string(csv_path)
.unwrap_or_else(|e| panic!("failed to read {csv_path}: {e}"));
let mut rows: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(49);
for (lineno, raw) in content.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("src_param") {
continue;
}
let cols: Vec<&str> = line.split(',').map(str::trim).collect();
assert_eq!(
cols.len(),
4,
"ambe prioritization line {}: expected 4 columns, got {}: {raw:?}",
lineno + 1,
cols.len()
);
let src_param: u8 = cols[0].parse().expect("src_param");
let src_bit: u8 = cols[1].parse().expect("src_bit");
let dst_vec: u8 = cols[2].parse().expect("dst_vec");
let dst_bit: u8 = cols[3].parse().expect("dst_bit");
assert!(dst_vec < 4, "half-rate dst_vec out of range");
let dst_width = [12u8, 12, 11, 14][dst_vec as usize];
assert!(dst_bit < dst_width, "dst_bit {dst_bit} exceeds vec {dst_vec} width");
rows.push((src_param, src_bit, dst_vec, dst_bit));
}
assert_eq!(rows.len(), 49, "ambe prioritization must have 49 entries");
let widths = [12u8, 12, 11, 14];
let mut seen = [[false; 14]; 4];
for (_, _, v, b) in &rows {
assert!(!seen[*v as usize][*b as usize],
"ambe: (dst_vec={v}, dst_bit={b}) appears twice");
seen[*v as usize][*b as usize] = true;
}
for (v, w) in widths.iter().enumerate() {
for b in 0..*w {
assert!(seen[v][b as usize],
"ambe: (dst_vec={v}, dst_bit={b}) never appears");
}
}
let mut out = String::new();
out.push_str("// Auto-generated from spec_tables/ambe_bit_prioritization.csv\n");
out.push_str("// Do not edit — regenerated each build.\n");
out.push_str("pub(crate) const AMBE_BIT_MAP: [BitMap; 49] = [\n");
for (sp, sb, dv, db) in &rows {
out.push_str(&format!(
" BitMap {{ src_param: {sp}, src_bit: {sb}, dst_vec: {dv}, dst_bit: {db} }},\n"
));
}
out.push_str("];\n");
fs::write(out_dir.join("ambe_bit_priority.rs"), out).expect("write ambe_bit_priority.rs");
}