Skip to main content

secs_project/
irs.rs

1use crate::{ProjectError, record::atomic_write};
2use std::fs;
3use std::io::Read;
4use std::path::{Path, PathBuf};
5
6pub const MAX_IR_BYTES: u64 = 256 * 1024 * 1024;
7
8pub fn ir_directory(project: &Path) -> PathBuf {
9    project.with_extension("irs")
10}
11pub fn ir_file(project: &Path, role: &str, pos: i64) -> PathBuf {
12    let safe = role.replace([':', '/', '\\'], "_");
13    ir_directory(project).join(format!("{safe}_P{pos}.wav"))
14}
15/// Writes the upstream mono float32 RIFF layout.
16///
17/// For values that Python's `struct.pack('<I', ...)` cannot represent, the
18/// legacy infallible API returns an empty vector instead of panicking. Call
19/// [`try_write_wav_float32`] when the error must be reported.
20pub fn write_wav_float32(samples: &[f32], sample_rate: u32) -> Vec<u8> {
21    try_write_wav_float32(samples, sample_rate).unwrap_or_default()
22}
23
24pub fn try_write_wav_float32(samples: &[f32], sample_rate: u32) -> Result<Vec<u8>, ProjectError> {
25    let bytes = samples
26        .len()
27        .checked_mul(4)
28        .ok_or_else(|| ProjectError::Wav("WAV payload size overflow".into()))?;
29    let payload_size = u32::try_from(bytes)
30        .map_err(|_| ProjectError::Wav("WAV payload exceeds RIFF limit".into()))?;
31    let riff_size = payload_size
32        .checked_add(36)
33        .ok_or_else(|| ProjectError::Wav("WAV RIFF size overflow".into()))?;
34    let byte_rate = sample_rate
35        .checked_mul(4)
36        .ok_or_else(|| ProjectError::Wav("WAV sample rate overflow".into()))?;
37    let capacity = 44usize
38        .checked_add(bytes)
39        .ok_or_else(|| ProjectError::Wav("WAV allocation size overflow".into()))?;
40    let mut out = Vec::with_capacity(capacity);
41    out.extend(b"RIFF");
42    out.extend(&riff_size.to_le_bytes());
43    out.extend(b"WAVEfmt \x10\0\0\0\x03\0\x01\0");
44    out.extend(&sample_rate.to_le_bytes());
45    out.extend(&byte_rate.to_le_bytes());
46    out.extend(&4u16.to_le_bytes());
47    out.extend(&32u16.to_le_bytes());
48    out.extend(b"data");
49    out.extend(&payload_size.to_le_bytes());
50    for x in samples {
51        out.extend(&x.to_le_bytes());
52    }
53    Ok(out)
54}
55const PCM_GUID: [u8; 16] = [1, 0, 0, 0, 0, 0, 16, 0, 128, 0, 0, 170, 0, 56, 155, 113];
56const FLOAT_GUID: [u8; 16] = [3, 0, 0, 0, 0, 0, 16, 0, 128, 0, 0, 170, 0, 56, 155, 113];
57
58#[derive(Debug, Clone, Copy)]
59struct WavFormat {
60    tag: u16,
61    channels: u16,
62    sample_rate: u32,
63    block_align: u16,
64    bits: u16,
65    valid_bits: u16,
66}
67type WavChunks = (WavFormat, Vec<u8>);
68
69fn le_u16(bytes: &[u8], offset: usize) -> Result<u16, ProjectError> {
70    let value = bytes
71        .get(offset..offset + 2)
72        .ok_or_else(|| ProjectError::Wav("truncated WAV field".into()))?;
73    Ok(u16::from_le_bytes([value[0], value[1]]))
74}
75fn le_u32(bytes: &[u8], offset: usize) -> Result<u32, ProjectError> {
76    let value = bytes
77        .get(offset..offset + 4)
78        .ok_or_else(|| ProjectError::Wav("truncated WAV field".into()))?;
79    Ok(u32::from_le_bytes(value.try_into().map_err(|_| {
80        ProjectError::Wav("truncated WAV field".into())
81    })?))
82}
83fn parse_wav_format(bytes: &[u8]) -> Result<WavFormat, ProjectError> {
84    if bytes.len() < 16 {
85        return Err(ProjectError::Wav("short fmt chunk".into()));
86    }
87    let mut tag = le_u16(bytes, 0)?;
88    let channels = le_u16(bytes, 2)?;
89    let sample_rate = le_u32(bytes, 4)?;
90    let block_align = le_u16(bytes, 12)?;
91    let bits = le_u16(bytes, 14)?;
92    let mut valid_bits = bits;
93    if tag == 0xfffe {
94        if bytes.len() < 40 || le_u16(bytes, 16)? < 22 {
95            return Err(ProjectError::Wav("short extensible fmt chunk".into()));
96        }
97        valid_bits = le_u16(bytes, 18)?;
98        let guid = bytes
99            .get(24..40)
100            .ok_or_else(|| ProjectError::Wav("missing extensible subformat".into()))?;
101        tag = if guid == PCM_GUID {
102            1
103        } else if guid == FLOAT_GUID {
104            3
105        } else {
106            return Err(ProjectError::Wav(
107                "unsupported extensible WAV subformat".into(),
108            ));
109        };
110    }
111    Ok(WavFormat {
112        tag,
113        channels,
114        sample_rate,
115        block_align,
116        bits,
117        valid_bits,
118    })
119}
120fn wav_chunks(data: &[u8], default_format: Option<WavFormat>) -> Result<WavChunks, ProjectError> {
121    if data.len() < 12 || &data[..4] != b"RIFF" || &data[8..12] != b"WAVE" {
122        return Err(ProjectError::Wav("not a RIFF/WAVE file".into()));
123    }
124    let mut pos = 12usize;
125    let mut format = None;
126    let mut payload = None;
127    while let Some(header_end) = pos.checked_add(8) {
128        if header_end > data.len() {
129            break;
130        }
131        let size_u32 = le_u32(data, pos + 4)?;
132        if &data[pos..pos + 4] == b"data" && u64::from(size_u32) > MAX_IR_BYTES {
133            return Err(ProjectError::Wav(format!(
134                "WAV data chunk exceeds {MAX_IR_BYTES} bytes"
135            )));
136        }
137        let size = size_u32 as usize;
138        let declared_end = header_end
139            .checked_add(size)
140            .ok_or_else(|| ProjectError::Wav("chunk size overflow".into()))?;
141        // F17: a cut `data` chunk is an error, not a shorter IR. Other chunks
142        // keep upstream's clamp to the file end.
143        if &data[pos..pos + 4] == b"data" && declared_end > data.len() {
144            return Err(ProjectError::Wav(format!(
145                "WAV data chunk declares {size} bytes but the file holds {}",
146                data.len() - header_end
147            )));
148        }
149        let end = declared_end.min(data.len());
150        match &data[pos..pos + 4] {
151            b"fmt " => format = Some(parse_wav_format(&data[header_end..end])?),
152            b"data" => payload = Some(data[header_end..end].to_vec()),
153            _ => {}
154        }
155        if declared_end >= data.len() {
156            break;
157        }
158        pos = declared_end
159            .checked_add(size & 1)
160            .ok_or_else(|| ProjectError::Wav("chunk padding overflow".into()))?;
161        if pos > data.len() {
162            break;
163        }
164    }
165    Ok((
166        format
167            .or(default_format)
168            .ok_or_else(|| ProjectError::Wav("missing fmt chunk".into()))?,
169        payload.ok_or_else(|| ProjectError::Wav("missing data chunk".into()))?,
170    ))
171}
172pub fn read_wav_float32(data: &[u8]) -> Result<(Vec<f32>, u32), ProjectError> {
173    let default = WavFormat {
174        tag: 3,
175        channels: 1,
176        sample_rate: 48000,
177        block_align: 4,
178        bits: 32,
179        valid_bits: 32,
180    };
181    let (format, payload) = wav_chunks(data, Some(default))?;
182    if format.tag != 3 || format.bits != 32 || payload.len() % 4 != 0 {
183        return Err(ProjectError::Wav(format!(
184            "not 32-bit float WAV (fmt={}, bits={})",
185            format.tag, format.bits
186        )));
187    }
188    Ok((
189        payload
190            .as_chunks::<4>()
191            .0
192            .iter()
193            .map(|x| f32::from_le_bytes(*x))
194            .collect(),
195        format.sample_rate,
196    ))
197}
198/// Reads mono PCM 16/24/32 and IEEE float 32/64 WAV, including extensible WAV.
199/// PCM 8-bit and valid-bit/container combinations other than left-aligned PCM in
200/// a 32-bit container are unsupported. It also does not decode compressed codecs,
201/// RF64, endian-swapped RIFX, multichannel audio, or containers other than WAV.
202pub fn read_mono_float_wav(data: &[u8]) -> Result<(Vec<f32>, u32), ProjectError> {
203    let (format, payload) = wav_chunks(data, None)?;
204    if format.channels != 1 {
205        return Err(ProjectError::Wav("WAV is not mono".into()));
206    }
207    let bytes_per_sample = usize::from(format.bits.div_ceil(8));
208    if bytes_per_sample == 0
209        || usize::from(format.block_align) != bytes_per_sample
210        || payload.len() % bytes_per_sample != 0
211    {
212        return Err(ProjectError::Wav("invalid block alignment".into()));
213    }
214    let out = match (format.tag, format.bits, format.valid_bits) {
215        (3, 32, 32) => payload
216            .as_chunks::<4>()
217            .0
218            .iter()
219            .map(|x| f32::from_le_bytes(*x))
220            .collect(),
221        (3, 64, 64) => payload
222            .as_chunks::<8>()
223            .0
224            .iter()
225            .map(|x| f64::from_le_bytes(*x) as f32)
226            .collect(),
227        (1, 16, 16) => payload
228            .as_chunks::<2>()
229            .0
230            .iter()
231            .map(|x| i16::from_le_bytes(*x) as f32 / 32768.)
232            .collect(),
233        (1, 24, 24) => payload
234            .as_chunks::<3>()
235            .0
236            .iter()
237            .map(|x| {
238                let n = ((x[0] as i32) | ((x[1] as i32) << 8) | ((x[2] as i32) << 16)) << 8 >> 8;
239                n as f32 / 8388608.
240            })
241            .collect(),
242        (1, 32, 32) => payload
243            .as_chunks::<4>()
244            .0
245            .iter()
246            .map(|x| i32::from_le_bytes(*x) as f32 / 2147483648.)
247            .collect(),
248        (1, 32, valid @ (1..=31)) => {
249            let shift = 32 - u32::from(valid);
250            let divisor = (1_u64 << (valid - 1)) as f32;
251            payload
252                .as_chunks::<4>()
253                .0
254                .iter()
255                .map(|x| (i32::from_le_bytes(*x) >> shift) as f32 / divisor)
256                .collect()
257        }
258        _ => {
259            return Err(ProjectError::Wav(format!(
260                "unsupported WAV format {}/{}/{}",
261                format.tag, format.bits, format.valid_bits
262            )));
263        }
264    };
265    Ok((out, format.sample_rate))
266}
267pub fn save_ir(
268    project: &Path,
269    role: &str,
270    pos: i64,
271    samples: &[f32],
272    rate: u32,
273) -> Result<PathBuf, ProjectError> {
274    let path = ir_file(project, role, pos);
275    let directory = ir_directory(project);
276    reject_symlink(&directory)?;
277    if !directory.exists() {
278        fs::create_dir_all(&directory)?;
279    }
280    reject_symlink(&directory)?;
281    reject_symlink(&path)?;
282    atomic_write(&path, &try_write_wav_float32(samples, rate)?)?;
283    Ok(path)
284}
285
286fn reject_symlink(path: &Path) -> Result<(), ProjectError> {
287    match fs::symlink_metadata(path) {
288        Ok(metadata) if metadata.file_type().is_symlink() => Err(ProjectError::Invalid(format!(
289            "IR write path is a symlink: {}",
290            path.display()
291        ))),
292        Ok(_) => Ok(()),
293        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
294        Err(error) => Err(error.into()),
295    }
296}
297pub fn load_ir(project: &Path, role: &str, pos: i64) -> Result<(Vec<f32>, u32), ProjectError> {
298    let path = ir_file(project, role, pos);
299    read_mono_float_wav(&read_bounded(&path, MAX_IR_BYTES, "WAV")?)
300        .map_err(|error| ProjectError::Wav(format!("{}: {error}", path.display())))
301}
302
303pub(crate) fn read_bounded(path: &Path, limit: u64, kind: &str) -> Result<Vec<u8>, ProjectError> {
304    let mut file = fs::File::open(path)?;
305    let mut bytes = Vec::new();
306    file.by_ref().take(limit + 1).read_to_end(&mut bytes)?;
307    if bytes.len() as u64 > limit {
308        return Err(ProjectError::Invalid(format!(
309            "{kind} file {} exceeds {limit} bytes",
310            path.display()
311        )));
312    }
313    Ok(bytes)
314}