1use std::collections::BTreeMap;
12use std::fs;
13use std::io::Cursor;
14use std::path::{Path, PathBuf};
15
16use shuyi_assets::audio::{BAKED_MUSIC_DIR, BAKED_SOUND_DIR, SAMPLE_RATE, without_extension};
17
18use crate::game::GameAudio;
19
20pub const FORMAT: u32 = 1;
23
24pub const BLOCK_BYTES: usize = 1024;
28
29pub const BLOCK_SAMPLES: usize = (BLOCK_BYTES - 4) * 2 + 1;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Job {
35 pub name: String,
37 pub bytes: Vec<u8>,
39 pub out: String,
42}
43
44pub fn jobs(audio: &GameAudio, root: &Path) -> Result<Vec<Job>, String> {
49 let mut jobs: BTreeMap<String, Job> = BTreeMap::new();
50 let mut add = |name: String, bytes: Vec<u8>, dir: &str, file: &str| {
51 let out = format!("{dir}/{}.wav", without_extension(file));
52 match jobs.get(&out) {
53 Some(job) if job.name != name => Err(format!(
54 "{} and {name} would both be {out} on the Playdate",
55 job.name
56 )),
57 Some(_) => Ok(()),
58 None => {
59 let job = Job {
60 name,
61 bytes,
62 out: out.clone(),
63 };
64 jobs.insert(out, job);
65 Ok(())
66 }
67 }
68 };
69 let files = &audio.files;
70 for (cue, sound) in &audio.manifest.sounds {
71 for file in &sound.files {
72 if (files.sound_left_out)(file) {
73 continue;
74 }
75 let bytes = (files.sound)(file).ok_or_else(|| {
76 format!(
77 "sound \"{cue}\": {}: no such file",
78 (files.sound_shown)(file)
79 )
80 })?;
81 add(
82 (files.sound_shown)(file),
83 bytes.into_owned(),
84 BAKED_SOUND_DIR,
85 file,
86 )?;
87 }
88 }
89 for (cue, track) in &audio.manifest.music {
90 let shown = format!("{}/{}", files.music_dir, track.file);
91 let bytes = match files.music {
92 Some(read) => read(&track.file).map(std::borrow::Cow::into_owned),
93 None => fs::read(root.join(&shown)).ok(),
94 };
95 let bytes = bytes.ok_or_else(|| format!("music \"{cue}\": {shown}: no such file"))?;
96 add(shown, bytes, BAKED_MUSIC_DIR, &track.file)?;
97 }
98 Ok(jobs.into_values().collect())
99}
100
101pub fn decode(name: &str, bytes: &[u8]) -> Result<Vec<i16>, String> {
104 let is_wav = Path::new(name)
105 .extension()
106 .is_some_and(|ext| ext.eq_ignore_ascii_case("wav"));
107 let (channels, rate, samples) = if is_wav {
108 decode_wav(bytes)
109 } else {
110 decode_ogg(bytes)
111 }
112 .map_err(|e| format!("{name}: {e}"))?;
113 if rate != SAMPLE_RATE {
114 return Err(format!("{name}: {rate} Hz; must be {SAMPLE_RATE} Hz"));
115 }
116 Ok(mono(&samples, channels))
117}
118
119fn decode_ogg(bytes: &[u8]) -> Result<(u16, u32, Vec<i16>), String> {
121 let mut reader =
122 lewton::inside_ogg::OggStreamReader::new(Cursor::new(bytes)).map_err(|e| e.to_string())?;
123 let channels = u16::from(reader.ident_hdr.audio_channels);
124 let rate = reader.ident_hdr.audio_sample_rate;
125 let mut samples = Vec::new();
126 while let Some(packet) = reader.read_dec_packet_itl().map_err(|e| e.to_string())? {
127 samples.extend(packet);
128 }
129 Ok((channels, rate, samples))
130}
131
132fn decode_wav(bytes: &[u8]) -> Result<(u16, u32, Vec<i16>), String> {
135 let reader = hound::WavReader::new(Cursor::new(bytes)).map_err(|e| e.to_string())?;
136 let spec = reader.spec();
137 let samples: Result<Vec<i16>, hound::Error> = match (spec.sample_format, spec.bits_per_sample) {
138 (hound::SampleFormat::Int, 16) => reader.into_samples::<i16>().collect(),
139 (hound::SampleFormat::Float, 32) => reader
140 .into_samples::<f32>()
141 .map(|s| s.map(float_to_i16))
142 .collect(),
143 (format, bits) => {
144 return Err(format!(
145 "{bits}-bit {format:?} samples; must be 16-bit PCM or 32-bit float"
146 ));
147 }
148 };
149 Ok((
150 spec.channels,
151 spec.sample_rate,
152 samples.map_err(|e| e.to_string())?,
153 ))
154}
155
156fn float_to_i16(sample: f32) -> i16 {
158 #[allow(clippy::cast_possible_truncation)]
160 let sample = (sample * 32_767.0).round().clamp(-32_768.0, 32_767.0) as i16;
161 sample
162}
163
164fn mono(samples: &[i16], channels: u16) -> Vec<i16> {
166 let channels = usize::from(channels.max(1));
167 samples
168 .chunks(channels)
169 .map(|frame| {
170 let sum: i32 = frame.iter().map(|&s| i32::from(s)).sum();
171 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
173 let mean = (sum / frame.len() as i32) as i16;
174 mean
175 })
176 .collect()
177}
178
179const STEPS: [i32; 89] = [
181 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66,
182 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449,
183 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272,
184 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493,
185 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767,
186];
187
188const INDEX_MOVES: [i32; 8] = [-1, -1, -1, -1, 2, 4, 6, 8];
190
191#[derive(Debug, Clone, Copy, Default)]
193struct Coder {
194 predictor: i32,
195 index: i32,
196}
197
198impl Coder {
199 fn step(self) -> i32 {
201 STEPS[usize::try_from(self.index).unwrap_or(0)]
202 }
203
204 fn encode(&mut self, sample: i16) -> u8 {
206 let mut diff = i32::from(sample) - self.predictor;
207 let mut code = 0;
208 if diff < 0 {
209 code = 8;
210 diff = -diff;
211 }
212 let mut step = self.step();
213 for bit in [4, 2, 1] {
214 if diff >= step {
215 code |= bit;
216 diff -= step;
217 }
218 step >>= 1;
219 }
220 self.decode(code);
221 code
222 }
223
224 fn decode(&mut self, code: u8) -> i16 {
226 let step = self.step();
227 let mut delta = step >> 3;
228 if code & 4 != 0 {
229 delta += step;
230 }
231 if code & 2 != 0 {
232 delta += step >> 1;
233 }
234 if code & 1 != 0 {
235 delta += step >> 2;
236 }
237 if code & 8 != 0 {
238 delta = -delta;
239 }
240 self.predictor = (self.predictor + delta).clamp(-32_768, 32_767);
241 self.index = (self.index + INDEX_MOVES[usize::from(code & 7)]).clamp(0, 88);
242 #[allow(clippy::cast_possible_truncation)]
244 let sample = self.predictor as i16;
245 sample
246 }
247}
248
249pub fn adpcm_wav(samples: &[i16]) -> Vec<u8> {
254 let blocks = samples.len().div_ceil(BLOCK_SAMPLES).max(1);
255 let mut data = Vec::with_capacity(blocks * BLOCK_BYTES);
256 let mut coder = Coder::default();
257 for block in 0..blocks {
258 let start = block * BLOCK_SAMPLES;
259 let sample = |i: usize| samples.get(start + i).copied().unwrap_or(0);
260 let first = sample(0);
262 coder.predictor = i32::from(first);
263 data.extend_from_slice(&first.to_le_bytes());
264 data.push(u8::try_from(coder.index).unwrap_or(0));
265 data.push(0);
266 for pair in 0..(BLOCK_BYTES - 4) {
267 let low = coder.encode(sample(1 + pair * 2));
268 let high = coder.encode(sample(2 + pair * 2));
269 data.push(low + (high << 4));
271 }
272 }
273 let u32_of = |n: usize| u32::try_from(n).unwrap_or(u32::MAX);
274 let block_bytes = u16::try_from(BLOCK_BYTES).unwrap_or(u16::MAX);
275 let block_samples = u16::try_from(BLOCK_SAMPLES).unwrap_or(u16::MAX);
276 let bytes_per_second = SAMPLE_RATE * u32::from(block_bytes) / u32::from(block_samples);
278 let mut wav = Vec::with_capacity(60 + data.len());
279 wav.extend_from_slice(b"RIFF");
280 wav.extend_from_slice(&u32_of(52 + data.len()).to_le_bytes());
281 wav.extend_from_slice(b"WAVEfmt ");
282 wav.extend_from_slice(&20u32.to_le_bytes());
283 wav.extend_from_slice(&0x11u16.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&SAMPLE_RATE.to_le_bytes());
286 wav.extend_from_slice(&bytes_per_second.to_le_bytes());
287 wav.extend_from_slice(&block_bytes.to_le_bytes());
288 wav.extend_from_slice(&4u16.to_le_bytes()); wav.extend_from_slice(&2u16.to_le_bytes()); wav.extend_from_slice(&block_samples.to_le_bytes());
291 wav.extend_from_slice(b"fact");
292 wav.extend_from_slice(&4u32.to_le_bytes());
293 wav.extend_from_slice(&u32_of(samples.len()).to_le_bytes());
294 wav.extend_from_slice(b"data");
295 wav.extend_from_slice(&u32_of(data.len()).to_le_bytes());
296 wav.extend_from_slice(&data);
297 wav
298}
299
300pub fn cache_dir(build: &Path) -> PathBuf {
302 build.join(format!("audio-v{FORMAT}"))
303}
304
305pub fn bake(jobs: &[Job], cache: &Path, into: &Path) -> Result<(), String> {
308 for job in jobs {
309 let key = format!(
310 "{:08x}-{}.wav",
311 crc32fast::hash(&job.bytes),
312 job.bytes.len()
313 );
314 let cached = cache.join(key);
315 if !cached.is_file() {
316 write(&cached, &adpcm_wav(&decode(&job.name, &job.bytes)?))?;
317 }
318 let to = into.join(&job.out);
319 if let Some(dir) = to.parent() {
320 fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
321 }
322 fs::copy(&cached, &to)
323 .map_err(|e| format!("copy {} to {}: {e}", cached.display(), to.display()))?;
324 }
325 Ok(())
326}
327
328fn write(path: &Path, bytes: &[u8]) -> Result<(), String> {
330 if let Some(dir) = path.parent() {
331 fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
332 }
333 fs::write(path, bytes).map_err(|e| format!("write {}: {e}", path.display()))
334}
335
336#[cfg(test)]
337mod tests;