audiowaveform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/Cargo.lock +1108 -0
  4. data/Cargo.toml +28 -0
  5. data/README.md +255 -0
  6. data/bindings/ruby/CHANGELOG.md +29 -0
  7. data/bindings/ruby/Cargo.lock +621 -0
  8. data/bindings/ruby/Cargo.toml +8 -0
  9. data/bindings/ruby/README.md +193 -0
  10. data/bindings/ruby/ext/audiowaveform/Cargo.toml +20 -0
  11. data/bindings/ruby/ext/audiowaveform/build.rs +4 -0
  12. data/bindings/ruby/ext/audiowaveform/extconf.rb +6 -0
  13. data/bindings/ruby/ext/audiowaveform/src/lib.rs +239 -0
  14. data/bindings/ruby/lib/audiowaveform/version.rb +5 -0
  15. data/bindings/ruby/lib/audiowaveform.rb +131 -0
  16. data/crates/audiowaveform/Cargo.toml +73 -0
  17. data/crates/audiowaveform/examples/generate_from_pcm.rs +25 -0
  18. data/crates/audiowaveform/examples/generate_waveform.rs +18 -0
  19. data/crates/audiowaveform/examples/render_waveform.rs +18 -0
  20. data/crates/audiowaveform/examples/resample_waveform.rs +19 -0
  21. data/crates/audiowaveform/src/audio.rs +938 -0
  22. data/crates/audiowaveform/src/color.rs +201 -0
  23. data/crates/audiowaveform/src/error.rs +89 -0
  24. data/crates/audiowaveform/src/format.rs +215 -0
  25. data/crates/audiowaveform/src/lib.rs +79 -0
  26. data/crates/audiowaveform/src/render.rs +802 -0
  27. data/crates/audiowaveform/src/wav.rs +95 -0
  28. data/crates/audiowaveform/src/waveform.rs +790 -0
  29. data/crates/audiowaveform/tests/formats.rs +223 -0
  30. data/crates/audiowaveform/tests/generate.rs +233 -0
  31. data/crates/audiowaveform/tests/render.rs +263 -0
  32. data/crates/audiowaveform/tests/support/mod.rs +125 -0
  33. data/crates/audiowaveform/tests/wav.rs +54 -0
  34. data/crates/audiowaveform/tests/waveform_io.rs +255 -0
  35. data/crates/audiowaveform-cli/Cargo.toml +43 -0
  36. data/crates/audiowaveform-cli/src/main.rs +803 -0
  37. data/crates/audiowaveform-cli/tests/cli.rs +483 -0
  38. data/crates/audiowaveform-cli/tests/support/mod.rs +111 -0
  39. data/sig/audiowaveform.rbs +33 -0
  40. metadata +100 -0
@@ -0,0 +1,938 @@
1
+ #[cfg(feature = "decode")]
2
+ use std::fs::File;
3
+ use std::io::Read;
4
+ #[cfg(feature = "decode")]
5
+ use std::io::Seek;
6
+ #[cfg(feature = "decode")]
7
+ use std::path::Path;
8
+
9
+ #[cfg(feature = "decode")]
10
+ use symphonia::core::audio::{AudioBufferRef, SampleBuffer, Signal};
11
+ #[cfg(feature = "decode")]
12
+ use symphonia::core::codecs::{CODEC_TYPE_ALAC, DecoderOptions};
13
+ #[cfg(feature = "decode")]
14
+ use symphonia::core::errors::Error as SymphoniaError;
15
+ #[cfg(feature = "decode")]
16
+ use symphonia::core::formats::FormatOptions;
17
+ #[cfg(feature = "decode")]
18
+ use symphonia::core::io::{MediaSource, MediaSourceStream};
19
+ #[cfg(feature = "decode")]
20
+ use symphonia::core::meta::MetadataOptions;
21
+ #[cfg(feature = "decode")]
22
+ use symphonia::core::probe::Hint;
23
+ #[cfg(feature = "decode")]
24
+ use symphonia::default::{get_codecs, get_probe};
25
+
26
+ #[cfg(feature = "decode")]
27
+ use crate::AudioFormat;
28
+ use crate::{AmplitudeScale, Error, Waveform, WaveformPoint};
29
+
30
+ #[cfg(feature = "decode")]
31
+ struct ReadSeekMediaSource<R> {
32
+ inner: R,
33
+ byte_len: Option<u64>,
34
+ }
35
+
36
+ #[cfg(feature = "decode")]
37
+ impl<R> ReadSeekMediaSource<R> {
38
+ fn new(inner: R, byte_len: Option<u64>) -> Self {
39
+ Self { inner, byte_len }
40
+ }
41
+ }
42
+
43
+ #[cfg(feature = "decode")]
44
+ impl<R: Read> Read for ReadSeekMediaSource<R> {
45
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
46
+ self.inner.read(buf)
47
+ }
48
+ }
49
+
50
+ #[cfg(feature = "decode")]
51
+ impl<R: Seek> Seek for ReadSeekMediaSource<R> {
52
+ fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
53
+ self.inner.seek(pos)
54
+ }
55
+ }
56
+
57
+ #[cfg(feature = "decode")]
58
+ impl<R: Read + Seek + Send + Sync> MediaSource for ReadSeekMediaSource<R> {
59
+ fn is_seekable(&self) -> bool {
60
+ true
61
+ }
62
+
63
+ fn byte_len(&self) -> Option<u64> {
64
+ self.byte_len
65
+ }
66
+ }
67
+
68
+ /// A decoded interleaved PCM audio buffer.
69
+ #[derive(Clone, Debug, PartialEq)]
70
+ pub struct PcmAudio {
71
+ sample_rate: u32,
72
+ channels: u16,
73
+ samples: Vec<i16>,
74
+ }
75
+
76
+ impl PcmAudio {
77
+ /// Creates a PCM buffer from interleaved 16-bit samples.
78
+ pub fn new(sample_rate: u32, channels: u16, samples: Vec<i16>) -> Result<Self, Error> {
79
+ if sample_rate == 0 {
80
+ return Err(Error::invalid_argument(
81
+ "sample rate",
82
+ "Invalid input sample rate: must be greater than zero",
83
+ ));
84
+ }
85
+ if channels == 0 {
86
+ return Err(Error::invalid_argument(
87
+ "channels",
88
+ "Invalid number of input channels: must be greater than zero",
89
+ ));
90
+ }
91
+ if !samples.len().is_multiple_of(usize::from(channels)) {
92
+ return Err(Error::invalid_argument(
93
+ "samples",
94
+ "Interleaved PCM sample count must be divisible by the channel count",
95
+ ));
96
+ }
97
+ Ok(Self {
98
+ sample_rate,
99
+ channels,
100
+ samples,
101
+ })
102
+ }
103
+
104
+ /// Returns the source sample rate in Hz.
105
+ pub const fn sample_rate(&self) -> u32 {
106
+ self.sample_rate
107
+ }
108
+
109
+ /// Returns the channel count.
110
+ pub const fn channels(&self) -> u16 {
111
+ self.channels
112
+ }
113
+
114
+ /// Returns the interleaved PCM samples.
115
+ pub fn samples(&self) -> &[i16] {
116
+ &self.samples
117
+ }
118
+
119
+ /// Returns the number of audio frames.
120
+ pub fn frame_count(&self) -> usize {
121
+ self.samples.len() / usize::from(self.channels)
122
+ }
123
+
124
+ /// Returns the duration in seconds.
125
+ pub fn duration_seconds(&self) -> f64 {
126
+ self.frame_count() as f64 / self.sample_rate as f64
127
+ }
128
+ }
129
+
130
+ /// Waveform scale selection.
131
+ #[derive(Clone, Copy, Debug, PartialEq)]
132
+ pub enum ScaleSpec {
133
+ /// Use a fixed number of source samples per waveform point.
134
+ SamplesPerPixel(u32),
135
+ /// Derive samples per waveform point from a target number of rendered pixels per second.
136
+ PixelsPerSecond(u32),
137
+ /// Fit a duration or full clip into the provided width.
138
+ FitWidth {
139
+ /// Output width in pixels.
140
+ width_pixels: u32,
141
+ /// Optional `(start_time, end_time)` range in seconds.
142
+ time_range: Option<(f64, f64)>,
143
+ },
144
+ }
145
+
146
+ impl ScaleSpec {
147
+ /// Resolves the scale to a concrete number of samples per waveform point.
148
+ pub fn resolve(self, sample_rate: u32, frame_count: usize) -> Result<u32, Error> {
149
+ let resolved = match self {
150
+ Self::SamplesPerPixel(value) => value,
151
+ Self::PixelsPerSecond(value) => {
152
+ if value == 0 {
153
+ return Err(Error::invalid_argument(
154
+ "pixels per second",
155
+ "Invalid pixels per second: must be greater than zero",
156
+ ));
157
+ }
158
+ sample_rate / value
159
+ }
160
+ Self::FitWidth {
161
+ width_pixels,
162
+ time_range,
163
+ } => {
164
+ if width_pixels == 0 {
165
+ return Err(Error::invalid_argument(
166
+ "image width",
167
+ "Invalid image width: minimum 1",
168
+ ));
169
+ }
170
+ let frames = if let Some((start, end)) = time_range {
171
+ if !start.is_finite() || start < 0.0 {
172
+ return Err(Error::invalid_argument(
173
+ "start time",
174
+ "Invalid start time: minimum 0",
175
+ ));
176
+ }
177
+ if !end.is_finite() || end < start {
178
+ return Err(Error::invalid_argument(
179
+ "end time",
180
+ format!("Invalid end time, must be greater than {start}"),
181
+ ));
182
+ }
183
+ ((end - start) * sample_rate as f64) as u64
184
+ } else {
185
+ frame_count as u64
186
+ };
187
+ (frames / u64::from(width_pixels)) as u32
188
+ }
189
+ };
190
+
191
+ if resolved < 2 {
192
+ return Err(Error::invalid_argument("zoom", "Invalid zoom: minimum 2"));
193
+ }
194
+
195
+ Ok(resolved)
196
+ }
197
+ }
198
+
199
+ /// Waveform generation settings.
200
+ #[derive(Clone, Debug, PartialEq)]
201
+ pub struct GenerateOptions {
202
+ /// Scale selection.
203
+ pub scale: ScaleSpec,
204
+ /// Whether to keep each source channel separate in the waveform output.
205
+ pub split_channels: bool,
206
+ /// Optional post-generation amplitude scaling.
207
+ pub amplitude_scale: Option<AmplitudeScale>,
208
+ }
209
+
210
+ impl Default for GenerateOptions {
211
+ fn default() -> Self {
212
+ Self {
213
+ scale: ScaleSpec::SamplesPerPixel(256),
214
+ split_channels: false,
215
+ amplitude_scale: None,
216
+ }
217
+ }
218
+ }
219
+
220
+ /// Supported raw audio sample encodings.
221
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
222
+ pub enum RawSampleFormat {
223
+ /// Signed 8-bit integer.
224
+ S8,
225
+ /// Unsigned 8-bit integer.
226
+ U8,
227
+ /// Signed little-endian 16-bit integer.
228
+ S16Le,
229
+ /// Signed big-endian 16-bit integer.
230
+ S16Be,
231
+ /// Signed little-endian 24-bit integer.
232
+ S24Le,
233
+ /// Signed big-endian 24-bit integer.
234
+ S24Be,
235
+ /// Signed little-endian 32-bit integer.
236
+ S32Le,
237
+ /// Signed big-endian 32-bit integer.
238
+ S32Be,
239
+ /// Little-endian 32-bit float.
240
+ F32Le,
241
+ /// Big-endian 32-bit float.
242
+ F32Be,
243
+ /// Little-endian 64-bit float.
244
+ F64Le,
245
+ /// Big-endian 64-bit float.
246
+ F64Be,
247
+ }
248
+
249
+ impl RawSampleFormat {
250
+ /// Returns the canonical CLI-friendly name.
251
+ pub const fn as_str(self) -> &'static str {
252
+ match self {
253
+ Self::S8 => "s8",
254
+ Self::U8 => "u8",
255
+ Self::S16Le => "s16le",
256
+ Self::S16Be => "s16be",
257
+ Self::S24Le => "s24le",
258
+ Self::S24Be => "s24be",
259
+ Self::S32Le => "s32le",
260
+ Self::S32Be => "s32be",
261
+ Self::F32Le => "f32le",
262
+ Self::F32Be => "f32be",
263
+ Self::F64Le => "f64le",
264
+ Self::F64Be => "f64be",
265
+ }
266
+ }
267
+ }
268
+
269
+ impl std::str::FromStr for RawSampleFormat {
270
+ type Err = Error;
271
+
272
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
273
+ match s.to_ascii_lowercase().as_str() {
274
+ "s8" => Ok(Self::S8),
275
+ "u8" => Ok(Self::U8),
276
+ "s16le" => Ok(Self::S16Le),
277
+ "s16be" => Ok(Self::S16Be),
278
+ "s24le" => Ok(Self::S24Le),
279
+ "s24be" => Ok(Self::S24Be),
280
+ "s32le" => Ok(Self::S32Le),
281
+ "s32be" => Ok(Self::S32Be),
282
+ "f32le" => Ok(Self::F32Le),
283
+ "f32be" => Ok(Self::F32Be),
284
+ "f64le" => Ok(Self::F64Le),
285
+ "f64be" => Ok(Self::F64Be),
286
+ _ => Err(Error::UnsupportedFormat {
287
+ format: s.to_string(),
288
+ }),
289
+ }
290
+ }
291
+ }
292
+
293
+ /// Configuration for decoding raw audio.
294
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
295
+ pub struct RawAudioConfig {
296
+ /// Source sample rate in Hz.
297
+ pub sample_rate: u32,
298
+ /// Channel count.
299
+ pub channels: u16,
300
+ /// Raw sample encoding.
301
+ pub sample_format: RawSampleFormat,
302
+ }
303
+
304
+ impl RawAudioConfig {
305
+ /// Creates a raw audio configuration.
306
+ pub fn new(
307
+ sample_rate: u32,
308
+ channels: u16,
309
+ sample_format: RawSampleFormat,
310
+ ) -> Result<Self, Error> {
311
+ if sample_rate == 0 {
312
+ return Err(Error::invalid_argument(
313
+ "raw sample rate",
314
+ "Invalid input sample rate: must be greater than zero",
315
+ ));
316
+ }
317
+ if channels == 0 {
318
+ return Err(Error::invalid_argument(
319
+ "raw channels",
320
+ "Invalid number of input channels: must be greater than zero",
321
+ ));
322
+ }
323
+ Ok(Self {
324
+ sample_rate,
325
+ channels,
326
+ sample_format,
327
+ })
328
+ }
329
+ }
330
+
331
+ /// Generates a waveform from in-memory PCM samples.
332
+ pub fn generate_waveform_from_pcm(
333
+ pcm: &PcmAudio,
334
+ options: &GenerateOptions,
335
+ ) -> Result<Waveform, Error> {
336
+ let samples_per_pixel = options.scale.resolve(pcm.sample_rate, pcm.frame_count())?;
337
+ let output_channels = if options.split_channels {
338
+ pcm.channels
339
+ } else {
340
+ 1
341
+ };
342
+ let mut waveform = Waveform::new(pcm.sample_rate, samples_per_pixel, output_channels)?;
343
+
344
+ let channels = usize::from(pcm.channels);
345
+ let output_channels_usize = usize::from(output_channels);
346
+ let mut mins = vec![i16::MAX; output_channels_usize];
347
+ let mut maxs = vec![i16::MIN; output_channels_usize];
348
+ let mut count = 0_u32;
349
+
350
+ for frame in pcm.samples.chunks_exact(channels) {
351
+ if output_channels == 1 {
352
+ let sample = frame.iter().map(|value| i32::from(*value)).sum::<i32>() / channels as i32;
353
+ mins[0] = mins[0].min(sample as i16);
354
+ maxs[0] = maxs[0].max(sample as i16);
355
+ } else {
356
+ for (channel, sample) in frame.iter().enumerate() {
357
+ mins[channel] = mins[channel].min(*sample);
358
+ maxs[channel] = maxs[channel].max(*sample);
359
+ }
360
+ }
361
+
362
+ count += 1;
363
+ if count == samples_per_pixel {
364
+ flush_frame(&mut waveform, &mins, &maxs)?;
365
+ mins.fill(i16::MAX);
366
+ maxs.fill(i16::MIN);
367
+ count = 0;
368
+ }
369
+ }
370
+
371
+ if count > 0 {
372
+ flush_frame(&mut waveform, &mins, &maxs)?;
373
+ }
374
+
375
+ match options.amplitude_scale {
376
+ Some(scale) => waveform.scale_amplitude(scale),
377
+ None => Ok(waveform),
378
+ }
379
+ }
380
+
381
+ /// Generates a waveform from raw audio bytes.
382
+ pub fn generate_waveform_from_raw_reader<R: Read>(
383
+ mut reader: R,
384
+ config: &RawAudioConfig,
385
+ options: &GenerateOptions,
386
+ ) -> Result<Waveform, Error> {
387
+ let pcm = decode_raw_audio_reader(&mut reader, config)?;
388
+ generate_waveform_from_pcm(&pcm, options)
389
+ }
390
+
391
+ /// Decodes raw audio bytes into interleaved 16-bit PCM.
392
+ pub fn decode_raw_audio_reader<R: Read>(
393
+ mut reader: R,
394
+ config: &RawAudioConfig,
395
+ ) -> Result<PcmAudio, Error> {
396
+ let mut bytes = Vec::new();
397
+ reader.read_to_end(&mut bytes)?;
398
+ parse_raw_audio(&bytes, config)
399
+ }
400
+
401
+ /// Generates a waveform from an audio file path using Symphonia.
402
+ #[cfg(feature = "decode")]
403
+ pub fn generate_waveform_from_path(
404
+ path: impl AsRef<Path>,
405
+ options: &GenerateOptions,
406
+ ) -> Result<Waveform, Error> {
407
+ let path = path.as_ref();
408
+ let format = AudioFormat::from_path(path).ok_or_else(|| Error::UnsupportedFormat {
409
+ format: path
410
+ .extension()
411
+ .and_then(|value| value.to_str())
412
+ .unwrap_or_default()
413
+ .to_string(),
414
+ })?;
415
+ let file = File::open(path)?;
416
+ generate_waveform_from_reader(file, Some(format), options)
417
+ }
418
+
419
+ /// Decodes an audio file path into interleaved 16-bit PCM using Symphonia.
420
+ #[cfg(feature = "decode")]
421
+ pub fn decode_audio_from_path(path: impl AsRef<Path>) -> Result<PcmAudio, Error> {
422
+ let path = path.as_ref();
423
+ let format = AudioFormat::from_path(path).ok_or_else(|| Error::UnsupportedFormat {
424
+ format: path
425
+ .extension()
426
+ .and_then(|value| value.to_str())
427
+ .unwrap_or_default()
428
+ .to_string(),
429
+ })?;
430
+ let file = File::open(path)?;
431
+ decode_audio_from_reader(file, Some(format))
432
+ }
433
+
434
+ /// Generates a waveform from an arbitrary seekable audio reader using Symphonia.
435
+ #[cfg(feature = "decode")]
436
+ pub fn generate_waveform_from_reader<R: Read + Seek + Send + Sync + 'static>(
437
+ reader: R,
438
+ format_hint: Option<AudioFormat>,
439
+ options: &GenerateOptions,
440
+ ) -> Result<Waveform, Error> {
441
+ let pcm = decode_audio_from_reader(reader, format_hint)?;
442
+ generate_waveform_from_pcm(&pcm, options)
443
+ }
444
+
445
+ /// Decodes an arbitrary seekable audio reader into interleaved 16-bit PCM using Symphonia.
446
+ #[cfg(feature = "decode")]
447
+ pub fn decode_audio_from_reader<R: Read + Seek + Send + Sync + 'static>(
448
+ reader: R,
449
+ format_hint: Option<AudioFormat>,
450
+ ) -> Result<PcmAudio, Error> {
451
+ decode_audio_reader(reader, format_hint)
452
+ }
453
+
454
+ fn flush_frame(waveform: &mut Waveform, mins: &[i16], maxs: &[i16]) -> Result<(), Error> {
455
+ let points = mins
456
+ .iter()
457
+ .zip(maxs.iter())
458
+ .map(|(min, max)| WaveformPoint {
459
+ min: *min,
460
+ max: *max,
461
+ })
462
+ .collect::<Vec<_>>();
463
+ waveform.push_frame(&points)
464
+ }
465
+
466
+ fn parse_raw_audio(bytes: &[u8], config: &RawAudioConfig) -> Result<PcmAudio, Error> {
467
+ let width = raw_sample_width(config.sample_format);
468
+ if !bytes.len().is_multiple_of(width) {
469
+ return Err(Error::invalid_data(
470
+ "Raw audio byte length is not aligned to the sample format",
471
+ ));
472
+ }
473
+
474
+ let mut samples = Vec::with_capacity(bytes.len() / width);
475
+ for chunk in bytes.chunks_exact(width) {
476
+ samples.push(parse_raw_sample(chunk, config.sample_format));
477
+ }
478
+ PcmAudio::new(config.sample_rate, config.channels, samples)
479
+ }
480
+
481
+ fn raw_sample_width(format: RawSampleFormat) -> usize {
482
+ match format {
483
+ RawSampleFormat::S8 | RawSampleFormat::U8 => 1,
484
+ RawSampleFormat::S16Le | RawSampleFormat::S16Be => 2,
485
+ RawSampleFormat::S24Le | RawSampleFormat::S24Be => 3,
486
+ RawSampleFormat::S32Le
487
+ | RawSampleFormat::S32Be
488
+ | RawSampleFormat::F32Le
489
+ | RawSampleFormat::F32Be => 4,
490
+ RawSampleFormat::F64Le | RawSampleFormat::F64Be => 8,
491
+ }
492
+ }
493
+
494
+ fn parse_raw_sample(bytes: &[u8], format: RawSampleFormat) -> i16 {
495
+ match format {
496
+ RawSampleFormat::S8 => i16::from(i8::from_ne_bytes([bytes[0]])) << 8,
497
+ RawSampleFormat::U8 => (i16::from(bytes[0]) - 128) << 8,
498
+ RawSampleFormat::S16Le => i16::from_le_bytes([bytes[0], bytes[1]]),
499
+ RawSampleFormat::S16Be => i16::from_be_bytes([bytes[0], bytes[1]]),
500
+ RawSampleFormat::S24Le => {
501
+ clamp_float_to_i16(sign_extend_24([bytes[0], bytes[1], bytes[2]]) as f64 / 256.0)
502
+ }
503
+ RawSampleFormat::S24Be => {
504
+ clamp_float_to_i16(sign_extend_24([bytes[2], bytes[1], bytes[0]]) as f64 / 256.0)
505
+ }
506
+ RawSampleFormat::S32Le => clamp_float_to_i16(
507
+ i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 / 65_536.0,
508
+ ),
509
+ RawSampleFormat::S32Be => clamp_float_to_i16(
510
+ i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 / 65_536.0,
511
+ ),
512
+ RawSampleFormat::F32Le => clamp_float_to_i16(
513
+ f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
514
+ * f64::from(i16::MAX),
515
+ ),
516
+ RawSampleFormat::F32Be => clamp_float_to_i16(
517
+ f32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
518
+ * f64::from(i16::MAX),
519
+ ),
520
+ RawSampleFormat::F64Le => clamp_float_to_i16(
521
+ f64::from_le_bytes(bytes.try_into().expect("checked width")) * f64::from(i16::MAX),
522
+ ),
523
+ RawSampleFormat::F64Be => clamp_float_to_i16(
524
+ f64::from_be_bytes(bytes.try_into().expect("checked width")) * f64::from(i16::MAX),
525
+ ),
526
+ }
527
+ }
528
+
529
+ fn sign_extend_24(bytes: [u8; 3]) -> i32 {
530
+ let sign = if bytes[2] & 0x80 != 0 { 0xFF } else { 0x00 };
531
+ i32::from_le_bytes([bytes[0], bytes[1], bytes[2], sign])
532
+ }
533
+
534
+ fn clamp_float_to_i16(value: f64) -> i16 {
535
+ value.clamp(f64::from(i16::MIN), f64::from(i16::MAX)) as i16
536
+ }
537
+
538
+ #[cfg(feature = "decode")]
539
+ pub(crate) fn decode_audio_reader<R: Read + Seek + Send + Sync + 'static>(
540
+ mut reader: R,
541
+ format_hint: Option<AudioFormat>,
542
+ ) -> Result<PcmAudio, Error> {
543
+ let mut hint = Hint::new();
544
+ if let Some(format) = format_hint {
545
+ format.ensure_enabled()?;
546
+ hint.with_extension(format.as_str());
547
+ }
548
+
549
+ let byte_len = reader.stream_position().ok().and_then(|position| {
550
+ let len = reader.seek(std::io::SeekFrom::End(0)).ok();
551
+ let _ = reader.seek(std::io::SeekFrom::Start(position));
552
+ len
553
+ });
554
+ let source = MediaSourceStream::new(
555
+ Box::new(ReadSeekMediaSource::new(reader, byte_len)),
556
+ Default::default(),
557
+ );
558
+ let probed = get_probe().format(
559
+ &hint,
560
+ source,
561
+ &FormatOptions::default(),
562
+ &MetadataOptions::default(),
563
+ )?;
564
+ let mut format = probed.format;
565
+ let track = format
566
+ .default_track()
567
+ .into_iter()
568
+ .chain(format.tracks())
569
+ .find(|track| get_codecs().get_codec(track.codec_params.codec).is_some())
570
+ .ok_or_else(|| Error::UnsupportedFormat {
571
+ format: "no supported audio track in this build".into(),
572
+ })?;
573
+ let track_id = track.id;
574
+ let mut codec_params = track.codec_params.clone();
575
+ if codec_params.codec == CODEC_TYPE_ALAC {
576
+ // CAF can wrap ALAC configuration in legacy frma/alac atoms; the decoder wants the payload.
577
+ const WRAPPER: &[u8] = b"\x00\x00\x00\x0cfrmaalac\x00\x00\x00\x24alac\x00\x00\x00\x00";
578
+ if let Some(cookie) = &codec_params.extra_data
579
+ && cookie.len() == WRAPPER.len() + 24
580
+ && cookie.starts_with(WRAPPER)
581
+ {
582
+ codec_params.extra_data = Some(cookie[WRAPPER.len()..].into());
583
+ }
584
+ }
585
+ let mut sample_rate = codec_params.sample_rate;
586
+ let mut channels = codec_params
587
+ .channels
588
+ .map(|channels| channels.count() as u16);
589
+ let encoder_delay = codec_params.delay.unwrap_or(0) as usize;
590
+ let mut decoder = get_codecs().make(&codec_params, &DecoderOptions::default())?;
591
+
592
+ let mut samples = Vec::new();
593
+ loop {
594
+ let packet = match format.next_packet() {
595
+ Ok(packet) => packet,
596
+ Err(SymphoniaError::IoError(error))
597
+ if error.kind() == std::io::ErrorKind::UnexpectedEof =>
598
+ {
599
+ break;
600
+ }
601
+ Err(error) => return Err(error.into()),
602
+ };
603
+
604
+ if packet.track_id() != track_id {
605
+ continue;
606
+ }
607
+
608
+ let decoded = match decoder.decode(&packet) {
609
+ Ok(decoded) => decoded,
610
+ Err(SymphoniaError::DecodeError(_)) => continue,
611
+ Err(SymphoniaError::IoError(error))
612
+ if error.kind() == std::io::ErrorKind::UnexpectedEof =>
613
+ {
614
+ break;
615
+ }
616
+ Err(error) => return Err(error.into()),
617
+ };
618
+
619
+ let spec = decoded.spec();
620
+ let decoded_channels = spec.channels.count() as u16;
621
+ if !samples.is_empty()
622
+ && (sample_rate != Some(spec.rate) || channels != Some(decoded_channels))
623
+ {
624
+ return Err(Error::invalid_data(
625
+ "Audio stream changes sample rate or channel count",
626
+ ));
627
+ }
628
+ // MP4 can leave channel metadata in codec configuration rather than the container track.
629
+ sample_rate = Some(spec.rate);
630
+ channels = Some(decoded_channels);
631
+
632
+ match decoded {
633
+ AudioBufferRef::F32(buffer) => {
634
+ extend_interleaved_f32_samples(buffer.as_ref(), &mut samples);
635
+ }
636
+ AudioBufferRef::F64(buffer) => {
637
+ extend_interleaved_f64_samples(buffer.as_ref(), &mut samples);
638
+ }
639
+ _ => {
640
+ let spec = *decoded.spec();
641
+ let mut sample_buffer = SampleBuffer::<i16>::new(decoded.capacity() as u64, spec);
642
+ sample_buffer.copy_interleaved_ref(decoded);
643
+ samples.extend_from_slice(sample_buffer.samples());
644
+ }
645
+ }
646
+ }
647
+
648
+ let sample_rate = sample_rate.ok_or(Error::MissingMetadata {
649
+ name: "sample_rate",
650
+ })?;
651
+ let channels = channels.ok_or(Error::MissingMetadata { name: "channels" })?;
652
+ if encoder_delay > 0 {
653
+ let samples_to_skip = encoder_delay.saturating_mul(usize::from(channels));
654
+ if samples_to_skip < samples.len() {
655
+ samples.drain(..samples_to_skip);
656
+ } else {
657
+ samples.clear();
658
+ }
659
+ }
660
+
661
+ PcmAudio::new(sample_rate, channels, samples)
662
+ }
663
+
664
+ #[cfg(feature = "decode")]
665
+ fn extend_interleaved_f32_samples(
666
+ buffer: &symphonia::core::audio::AudioBuffer<f32>,
667
+ samples: &mut Vec<i16>,
668
+ ) {
669
+ let channels = buffer.spec().channels.count();
670
+ for frame in 0..buffer.frames() {
671
+ for channel in 0..channels {
672
+ let sample = buffer.chan(channel)[frame];
673
+ samples.push(clamp_float_to_i16(f64::from(sample) * f64::from(i16::MAX)));
674
+ }
675
+ }
676
+ }
677
+
678
+ #[cfg(feature = "decode")]
679
+ fn extend_interleaved_f64_samples(
680
+ buffer: &symphonia::core::audio::AudioBuffer<f64>,
681
+ samples: &mut Vec<i16>,
682
+ ) {
683
+ let channels = buffer.spec().channels.count();
684
+ for frame in 0..buffer.frames() {
685
+ for channel in 0..channels {
686
+ let sample = buffer.chan(channel)[frame];
687
+ samples.push(clamp_float_to_i16(sample * f64::from(i16::MAX)));
688
+ }
689
+ }
690
+ }
691
+
692
+ #[cfg(test)]
693
+ mod tests {
694
+ use std::str::FromStr;
695
+
696
+ use super::{
697
+ GenerateOptions, PcmAudio, RawAudioConfig, RawSampleFormat, ScaleSpec,
698
+ generate_waveform_from_pcm, parse_raw_audio, parse_raw_sample,
699
+ };
700
+ use crate::{AmplitudeScale, WaveformPoint};
701
+
702
+ #[test]
703
+ fn validates_pcm_audio_construction() {
704
+ let pcm = PcmAudio::new(48_000, 2, vec![1, 2, 3, 4]).expect("pcm");
705
+ assert_eq!(pcm.frame_count(), 2);
706
+ assert_eq!(pcm.duration_seconds(), 2.0 / 48_000.0);
707
+
708
+ let error = PcmAudio::new(0, 1, vec![1]).expect_err("invalid sample rate");
709
+ assert_eq!(
710
+ error.to_string(),
711
+ "Invalid input sample rate: must be greater than zero"
712
+ );
713
+
714
+ let error = PcmAudio::new(48_000, 0, vec![1]).expect_err("invalid channels");
715
+ assert_eq!(
716
+ error.to_string(),
717
+ "Invalid number of input channels: must be greater than zero"
718
+ );
719
+
720
+ let error = PcmAudio::new(48_000, 2, vec![1, 2, 3]).expect_err("unaligned samples");
721
+ assert_eq!(
722
+ error.to_string(),
723
+ "Interleaved PCM sample count must be divisible by the channel count"
724
+ );
725
+ }
726
+
727
+ #[test]
728
+ fn resolves_scale_specifications_and_rejects_invalid_values() {
729
+ assert_eq!(
730
+ ScaleSpec::SamplesPerPixel(64)
731
+ .resolve(48_000, 96_000)
732
+ .expect("samples per pixel"),
733
+ 64
734
+ );
735
+ assert_eq!(
736
+ ScaleSpec::PixelsPerSecond(100)
737
+ .resolve(48_000, 96_000)
738
+ .expect("pixels per second"),
739
+ 480
740
+ );
741
+ assert_eq!(
742
+ ScaleSpec::FitWidth {
743
+ width_pixels: 400,
744
+ time_range: Some((0.0, 4.0)),
745
+ }
746
+ .resolve(48_000, 0)
747
+ .expect("fit width"),
748
+ 480
749
+ );
750
+
751
+ let error = ScaleSpec::PixelsPerSecond(0)
752
+ .resolve(48_000, 0)
753
+ .expect_err("invalid pixels per second");
754
+ assert_eq!(
755
+ error.to_string(),
756
+ "Invalid pixels per second: must be greater than zero"
757
+ );
758
+
759
+ let error = ScaleSpec::FitWidth {
760
+ width_pixels: 0,
761
+ time_range: None,
762
+ }
763
+ .resolve(48_000, 96_000)
764
+ .expect_err("invalid width");
765
+ assert_eq!(error.to_string(), "Invalid image width: minimum 1");
766
+
767
+ let error = ScaleSpec::FitWidth {
768
+ width_pixels: 400,
769
+ time_range: Some((5.0, 4.0)),
770
+ }
771
+ .resolve(48_000, 96_000)
772
+ .expect_err("invalid range");
773
+ assert_eq!(
774
+ error.to_string(),
775
+ "Invalid end time, must be greater than 5"
776
+ );
777
+
778
+ let error = ScaleSpec::FitWidth {
779
+ width_pixels: 400,
780
+ time_range: Some((f64::INFINITY, 10.0)),
781
+ }
782
+ .resolve(48_000, 96_000)
783
+ .expect_err("non-finite start time");
784
+ assert_eq!(error.to_string(), "Invalid start time: minimum 0");
785
+
786
+ let error = ScaleSpec::FitWidth {
787
+ width_pixels: 400,
788
+ time_range: Some((0.0, f64::INFINITY)),
789
+ }
790
+ .resolve(48_000, 96_000)
791
+ .expect_err("non-finite end time");
792
+ assert_eq!(
793
+ error.to_string(),
794
+ "Invalid end time, must be greater than 0"
795
+ );
796
+
797
+ let error = ScaleSpec::FitWidth {
798
+ width_pixels: 100_000,
799
+ time_range: None,
800
+ }
801
+ .resolve(48_000, 96_000)
802
+ .expect_err("zoom too small");
803
+ assert_eq!(error.to_string(), "Invalid zoom: minimum 2");
804
+ }
805
+
806
+ #[test]
807
+ fn parses_raw_sample_formats_and_validates_raw_audio_config() {
808
+ assert_eq!(
809
+ RawSampleFormat::from_str("s16le").expect("raw sample format"),
810
+ RawSampleFormat::S16Le
811
+ );
812
+ assert_eq!(
813
+ RawSampleFormat::from_str("F64BE").expect("raw sample format"),
814
+ RawSampleFormat::F64Be
815
+ );
816
+ let error = RawSampleFormat::from_str("pcm").expect_err("unsupported format");
817
+ assert_eq!(error.to_string(), "Unsupported format: pcm");
818
+
819
+ let config = RawAudioConfig::new(44_100, 2, RawSampleFormat::S16Le).expect("config");
820
+ assert_eq!(config.sample_rate, 44_100);
821
+ assert_eq!(config.channels, 2);
822
+
823
+ let error =
824
+ RawAudioConfig::new(0, 1, RawSampleFormat::S16Le).expect_err("invalid sample rate");
825
+ assert_eq!(
826
+ error.to_string(),
827
+ "Invalid input sample rate: must be greater than zero"
828
+ );
829
+
830
+ let error =
831
+ RawAudioConfig::new(44_100, 0, RawSampleFormat::S16Le).expect_err("invalid channels");
832
+ assert_eq!(
833
+ error.to_string(),
834
+ "Invalid number of input channels: must be greater than zero"
835
+ );
836
+ }
837
+
838
+ #[test]
839
+ fn decodes_representative_raw_sample_formats() {
840
+ let cases = [
841
+ (RawSampleFormat::S8, vec![0x80], i16::MIN),
842
+ (RawSampleFormat::U8, vec![0xff], 32_512),
843
+ (RawSampleFormat::S16Le, vec![0x34, 0x12], 0x1234),
844
+ (RawSampleFormat::S16Be, vec![0x12, 0x34], 0x1234),
845
+ (RawSampleFormat::S24Le, vec![0x00, 0x00, 0x01], 256),
846
+ (RawSampleFormat::S24Be, vec![0x01, 0x00, 0x00], 256),
847
+ (RawSampleFormat::S32Le, vec![0x00, 0x00, 0x01, 0x00], 1),
848
+ (RawSampleFormat::S32Be, vec![0x00, 0x01, 0x00, 0x00], 1),
849
+ (
850
+ RawSampleFormat::F32Le,
851
+ 1.0_f32.to_le_bytes().to_vec(),
852
+ i16::MAX,
853
+ ),
854
+ (
855
+ RawSampleFormat::F32Be,
856
+ 1.0_f32.to_be_bytes().to_vec(),
857
+ i16::MAX,
858
+ ),
859
+ (
860
+ RawSampleFormat::F64Le,
861
+ 1.0_f64.to_le_bytes().to_vec(),
862
+ i16::MAX,
863
+ ),
864
+ (
865
+ RawSampleFormat::F64Be,
866
+ 1.0_f64.to_be_bytes().to_vec(),
867
+ i16::MAX,
868
+ ),
869
+ ];
870
+
871
+ for (format, bytes, expected) in cases {
872
+ assert_eq!(parse_raw_sample(&bytes, format), expected, "{format:?}");
873
+ }
874
+ }
875
+
876
+ #[test]
877
+ fn decodes_raw_audio_and_rejects_unaligned_buffers() {
878
+ let config = RawAudioConfig::new(16_000, 1, RawSampleFormat::S16Le).expect("config");
879
+ let pcm = parse_raw_audio(&[0x01, 0x00, 0xff, 0xff], &config).expect("parse raw");
880
+ assert_eq!(pcm.samples(), &[1, -1]);
881
+
882
+ let error = parse_raw_audio(&[0x01], &config).expect_err("unaligned bytes");
883
+ assert_eq!(
884
+ error.to_string(),
885
+ "Raw audio byte length is not aligned to the sample format"
886
+ );
887
+ }
888
+
889
+ #[test]
890
+ fn generates_waveforms_from_pcm_for_mixed_and_split_channels() {
891
+ let pcm = PcmAudio::new(48_000, 2, vec![100, 300, 200, 400, -100, -300, -200, -400])
892
+ .expect("pcm");
893
+
894
+ let mixed = generate_waveform_from_pcm(
895
+ &pcm,
896
+ &GenerateOptions {
897
+ scale: ScaleSpec::SamplesPerPixel(2),
898
+ split_channels: false,
899
+ amplitude_scale: None,
900
+ },
901
+ )
902
+ .expect("mixed waveform");
903
+ assert_eq!(mixed.channels(), 1);
904
+ assert_eq!(
905
+ mixed.point(0, 0).expect("first point"),
906
+ WaveformPoint { min: 200, max: 300 }
907
+ );
908
+ assert_eq!(
909
+ mixed.point(0, 1).expect("second point"),
910
+ WaveformPoint {
911
+ min: -300,
912
+ max: -200,
913
+ }
914
+ );
915
+
916
+ let split = generate_waveform_from_pcm(
917
+ &pcm,
918
+ &GenerateOptions {
919
+ scale: ScaleSpec::SamplesPerPixel(2),
920
+ split_channels: true,
921
+ amplitude_scale: Some(AmplitudeScale::Fixed(2.0)),
922
+ },
923
+ )
924
+ .expect("split waveform");
925
+ assert_eq!(split.channels(), 2);
926
+ assert_eq!(
927
+ split.point(0, 0).expect("left point"),
928
+ WaveformPoint { min: 200, max: 400 }
929
+ );
930
+ assert_eq!(
931
+ split.point(1, 1).expect("right point"),
932
+ WaveformPoint {
933
+ min: -800,
934
+ max: -600,
935
+ }
936
+ );
937
+ }
938
+ }