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}
15pub 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 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}
198pub 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}