Skip to main content

rlx_tsac/
correct.rs

1//! Correct TSAC codec on all RLX backends.
2//!
3//! TSAC's neural codec **is** Descript-DAC-44kHz: Bellard's `tsac` ships the
4//! descript weights, BF8-quantized via the closed-source `libnc`. Verified by
5//! roundtrip comparison — rlx-dac + descript weights vs Bellard's `tsac` produce
6//! **0.9957** correlated output, both reconstructing the input at ~0.98 corr.
7//!
8//! Bellard's bundled `*_q8.bin` weights use libnc's BF8 layout, whose dequant is
9//! a documented unsolved closed-source problem (`vendor/docs/bf8-analysis.md`) —
10//! tsac-ng's own dequant gives 0.002 corr. So the *correct* path uses the real
11//! descript weights (freely downloadable) through rlx-dac's verified, all-backend
12//! (cpu/metal/mlx/wgpu) encode+decode graph. The vendored q8 path remains for
13//! bit-exact reproduction of tsac-ng files (see [`crate::rlx_decode`]).
14
15use anyhow::{Context, Result};
16use rlx_dac::DacCodec;
17use rlx_runtime::Device;
18use std::path::{Path, PathBuf};
19
20/// Fastest available RLX backend for this codec, measured on Apple silicon:
21/// **MLX ≫ Metal > wgpu > CPU** (MLX ~14–26× real-time; CPU ~0.02×). Only
22/// considers backends compiled in *and* available at runtime.
23pub fn best_device() -> Device {
24    for d in [Device::Mlx, Device::Metal, Device::Gpu] {
25        if rlx_runtime::is_available(d) {
26            return d;
27        }
28    }
29    Device::Cpu
30}
31
32/// Default directory for the descript-DAC-44kHz weights (`RLX_DAC_DIR` or
33/// `.cache/dac44`).
34pub fn default_dir() -> PathBuf {
35    std::env::var("RLX_DAC_DIR")
36        .map(PathBuf::from)
37        .unwrap_or_else(|_| PathBuf::from(".cache/dac44"))
38}
39
40/// Open the correct TSAC (= Descript-DAC-44kHz) codec on `device`, ensuring the
41/// weights are present (downloaded if the crate is built with the `oracle`
42/// feature, i.e. `rlx-dac/hf-download`). Returns a full encode+decode codec that
43/// runs on every RLX backend.
44pub fn open(device: Device) -> Result<DacCodec> {
45    open_in(&default_dir(), device)
46}
47
48/// Like [`open`] but with an explicit weights directory.
49pub fn open_in(model_dir: &Path, device: Device) -> Result<DacCodec> {
50    rlx_dac::download::ensure_weights(model_dir)?;
51    DacCodec::open_on(model_dir, device)
52}
53
54/// True when descript-44kHz weights are present in `dir` (no download needed).
55pub fn weights_available(dir: &Path) -> bool {
56    dir.join("model.safetensors").is_file() && dir.join("config.json").is_file()
57}
58
59/// File-level correct TSAC codec: WAV ⇄ `.tsac` container, encode+decode on any
60/// RLX backend. The container is a small self-describing RVQ-code blob (magic
61/// `TSRX`) — self-consistent (its own encode/decode roundtrip), distinct from
62/// Bellard's libnc container.
63pub struct CorrectCodec {
64    dac: DacCodec,
65    num_quantizers: Option<usize>,
66}
67
68/// Legacy container: header + one `u16` per RVQ code (16 bits each).
69const MAGIC_V1: &[u8; 4] = b"TSRX";
70/// Bit-packed container: header + a `bits_per_code` byte + codes packed at
71/// exactly `ceil(log2(codebook_size))` bits each (10 bits for the 1024-entry
72/// DAC codebooks), LSB-first. ~37 % smaller than V1, losslessly. V1 files still
73/// decode (see [`CorrectCodec::decode_file`]).
74const MAGIC_V2: &[u8; 4] = b"TSR2";
75
76/// Bits needed to index `codebook_size` codebook entries.
77fn bits_per_code(codebook_size: usize) -> u32 {
78    codebook_size.next_power_of_two().trailing_zeros().max(1)
79}
80
81/// Append `value`'s low `bits` bits to `buf`/`acc`/`nbits`, LSB-first.
82struct BitWriter {
83    buf: Vec<u8>,
84    acc: u64,
85    nbits: u32,
86}
87impl BitWriter {
88    fn new() -> Self {
89        Self {
90            buf: Vec::new(),
91            acc: 0,
92            nbits: 0,
93        }
94    }
95    fn put(&mut self, value: u32, bits: u32) {
96        self.acc |= (value as u64) << self.nbits;
97        self.nbits += bits;
98        while self.nbits >= 8 {
99            self.buf.push((self.acc & 0xff) as u8);
100            self.acc >>= 8;
101            self.nbits -= 8;
102        }
103    }
104    fn finish(mut self) -> Vec<u8> {
105        if self.nbits > 0 {
106            self.buf.push((self.acc & 0xff) as u8);
107        }
108        self.buf
109    }
110}
111
112struct BitReader<'a> {
113    bytes: &'a [u8],
114    pos: usize,
115    acc: u64,
116    nbits: u32,
117}
118impl<'a> BitReader<'a> {
119    fn new(bytes: &'a [u8]) -> Self {
120        Self {
121            bytes,
122            pos: 0,
123            acc: 0,
124            nbits: 0,
125        }
126    }
127    fn get(&mut self, bits: u32) -> u32 {
128        while self.nbits < bits {
129            let byte = self.bytes.get(self.pos).copied().unwrap_or(0);
130            self.pos += 1;
131            self.acc |= (byte as u64) << self.nbits;
132            self.nbits += 8;
133        }
134        let mask = if bits >= 32 {
135            u32::MAX
136        } else {
137            (1u32 << bits) - 1
138        };
139        let v = (self.acc as u32) & mask;
140        self.acc >>= bits;
141        self.nbits -= bits;
142        v
143    }
144}
145
146impl CorrectCodec {
147    /// Open on `device`, drawing weights from [`default_dir`]. `quality` maps to
148    /// the number of RVQ codebooks used (1..=9; `None` = all).
149    pub fn open(device: Device, quality: Option<u8>) -> Result<Self> {
150        Self::open_in(&default_dir(), device, quality)
151    }
152
153    pub fn open_in(model_dir: &Path, device: Device, quality: Option<u8>) -> Result<Self> {
154        let dac = open_in(model_dir, device)?;
155        let num_quantizers = quality.map(|q| (q as usize).clamp(1, 9));
156        Ok(Self {
157            dac,
158            num_quantizers,
159        })
160    }
161
162    pub fn sample_rate(&self) -> u32 {
163        self.dac.sample_rate()
164    }
165
166    /// Encode a WAV (any rate/channels — resampled to mono 44.1 kHz) to `.tsac`,
167    /// bit-packing the RVQ codes (V2 container).
168    pub fn encode_file(&self, in_audio: &Path, out_tsac: &Path) -> Result<()> {
169        let codes = self.dac.encode_wav(in_audio, self.num_quantizers)?;
170        let orig = mono_len_44k(in_audio)?;
171        let bits = bits_per_code(self.dac.config().codebook_size);
172        let mut buf = Vec::with_capacity(
173            21 + (codes.num_frames() * codes.num_quantizers * bits as usize).div_ceil(8),
174        );
175        buf.extend_from_slice(MAGIC_V2);
176        buf.extend_from_slice(&self.dac.sample_rate().to_le_bytes());
177        buf.extend_from_slice(&(orig as u32).to_le_bytes());
178        buf.extend_from_slice(&(codes.num_frames() as u32).to_le_bytes());
179        buf.extend_from_slice(&(codes.num_quantizers as u32).to_le_bytes());
180        buf.push(bits as u8);
181        let mut bw = BitWriter::new();
182        for frame in &codes.frames {
183            for &c in frame {
184                bw.put(c, bits);
185            }
186        }
187        buf.extend_from_slice(&bw.finish());
188        if let Some(parent) = out_tsac.parent() {
189            if !parent.as_os_str().is_empty() {
190                std::fs::create_dir_all(parent).ok();
191            }
192        }
193        std::fs::write(out_tsac, buf).with_context(|| format!("write {}", out_tsac.display()))?;
194        Ok(())
195    }
196
197    /// Decode a `.tsac` container (V2 packed or legacy V1 `u16`) back to a WAV.
198    pub fn decode_file(&self, in_tsac: &Path, out_wav: &Path) -> Result<()> {
199        let b = std::fs::read(in_tsac).with_context(|| format!("read {}", in_tsac.display()))?;
200        anyhow::ensure!(b.len() >= 20, "container too short");
201        let rd = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) as usize;
202        let orig = rd(8);
203        let n_frames = rd(12);
204        let n_cb = rd(16);
205        let frames = if &b[0..4] == MAGIC_V2 {
206            anyhow::ensure!(b.len() >= 21, "TSR2 truncated header");
207            let bits = b[20] as u32;
208            anyhow::ensure!((1..=16).contains(&bits), "TSR2 bad bits_per_code {bits}");
209            let need = (n_frames * n_cb * bits as usize).div_ceil(8);
210            anyhow::ensure!(b.len() >= 21 + need, "TSR2 truncated payload");
211            let mut br = BitReader::new(&b[21..]);
212            let mut frames = Vec::with_capacity(n_frames);
213            for _ in 0..n_frames {
214                let mut row = Vec::with_capacity(n_cb);
215                for _ in 0..n_cb {
216                    row.push(br.get(bits));
217                }
218                frames.push(row);
219            }
220            frames
221        } else if &b[0..4] == MAGIC_V1 {
222            anyhow::ensure!(b.len() >= 20 + n_frames * n_cb * 2, "TSRX truncated");
223            let mut frames = Vec::with_capacity(n_frames);
224            let mut off = 20;
225            for _ in 0..n_frames {
226                let mut row = Vec::with_capacity(n_cb);
227                for _ in 0..n_cb {
228                    row.push(u16::from_le_bytes([b[off], b[off + 1]]) as u32);
229                    off += 2;
230                }
231                frames.push(row);
232            }
233            frames
234        } else {
235            anyhow::bail!("not a TSRX/TSR2 container");
236        };
237        let codes = rlx_dac::DacCodes {
238            frames,
239            num_quantizers: n_cb,
240        };
241        self.dac.decode_wav(&codes, out_wav, Some(orig))
242    }
243}
244
245fn mono_len_44k(path: &Path) -> Result<usize> {
246    let r = hound::WavReader::open(path).with_context(|| format!("open {}", path.display()))?;
247    let s = r.spec();
248    let frames = (r.len() as usize) / (s.channels as usize).max(1);
249    Ok((frames as u64 * 44_100 / s.sample_rate.max(1) as u64) as usize)
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    /// The bit writer/reader round-trips arbitrary 10-bit codes losslessly,
257    /// which is what makes the packed TSR2 container bit-exact with the codes.
258    #[test]
259    fn bit_pack_roundtrip_10bit() {
260        let bits = bits_per_code(1024);
261        assert_eq!(bits, 10);
262        let codes: Vec<u32> = (0..5000u32).map(|i| (i * 37 + 11) % 1024).collect();
263        let mut bw = BitWriter::new();
264        for &c in &codes {
265            bw.put(c, bits);
266        }
267        let packed = bw.finish();
268        // 10 bits/code, byte-aligned tail.
269        assert_eq!(packed.len(), (codes.len() * bits as usize).div_ceil(8));
270        let mut br = BitReader::new(&packed);
271        for &c in &codes {
272            assert_eq!(br.get(bits), c);
273        }
274    }
275
276    #[test]
277    fn bits_per_code_sizes() {
278        assert_eq!(bits_per_code(1024), 10);
279        assert_eq!(bits_per_code(512), 9);
280        assert_eq!(bits_per_code(2048), 11);
281        assert_eq!(bits_per_code(1), 1);
282    }
283}