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,790 @@
1
+ use std::fs::File;
2
+ use std::io::{BufReader, BufWriter, ErrorKind, Read, Write};
3
+ use std::path::Path;
4
+
5
+ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
6
+ use serde::Deserialize;
7
+
8
+ use crate::audio::ScaleSpec;
9
+ use crate::{Error, WaveformFormat};
10
+
11
+ const FLAG_8_BIT: u32 = 0x0000_0001;
12
+ const MAX_CHANNELS: usize = 24;
13
+
14
+ /// A single waveform point containing the minimum and maximum sample value for a bucket.
15
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
16
+ pub struct WaveformPoint {
17
+ /// Minimum sample value for the bucket.
18
+ pub min: i16,
19
+ /// Maximum sample value for the bucket.
20
+ pub max: i16,
21
+ }
22
+
23
+ /// Waveform amplitude post-scaling.
24
+ #[derive(Clone, Copy, Debug, PartialEq)]
25
+ pub enum AmplitudeScale {
26
+ /// Scale using a fixed multiplier.
27
+ Fixed(f64),
28
+ /// Scale automatically so the current range fills the full 16-bit output range.
29
+ Auto,
30
+ }
31
+
32
+ /// Immutable-ish waveform value containing metadata and interleaved min/max data.
33
+ #[derive(Clone, Debug, PartialEq)]
34
+ pub struct Waveform {
35
+ sample_rate: u32,
36
+ samples_per_pixel: u32,
37
+ channels: u16,
38
+ storage_bits: u8,
39
+ data: Vec<i16>,
40
+ }
41
+
42
+ #[derive(Debug, Deserialize)]
43
+ struct JsonWaveform {
44
+ version: i32,
45
+ channels: Option<u16>,
46
+ sample_rate: u32,
47
+ samples_per_pixel: u32,
48
+ bits: u8,
49
+ length: usize,
50
+ data: Vec<i32>,
51
+ }
52
+
53
+ impl Waveform {
54
+ /// Creates an empty waveform with the given metadata.
55
+ ///
56
+ /// ```rust
57
+ /// use audiowaveform::Waveform;
58
+ ///
59
+ /// let waveform = Waveform::new(48_000, 256, 1)?;
60
+ /// assert!(waveform.is_empty());
61
+ /// # Ok::<(), audiowaveform::Error>(())
62
+ /// ```
63
+ pub fn new(sample_rate: u32, samples_per_pixel: u32, channels: u16) -> Result<Self, Error> {
64
+ Self::validate_metadata(sample_rate, samples_per_pixel, channels)?;
65
+ Ok(Self {
66
+ sample_rate,
67
+ samples_per_pixel,
68
+ channels,
69
+ storage_bits: 16,
70
+ data: Vec::new(),
71
+ })
72
+ }
73
+
74
+ /// Creates a waveform from already interleaved min/max samples.
75
+ pub fn from_interleaved_samples(
76
+ sample_rate: u32,
77
+ samples_per_pixel: u32,
78
+ channels: u16,
79
+ data: Vec<i16>,
80
+ storage_bits: u8,
81
+ ) -> Result<Self, Error> {
82
+ Self::validate_metadata(sample_rate, samples_per_pixel, channels)?;
83
+ Self::validate_storage_bits(storage_bits)?;
84
+ let frame_width = usize::from(channels) * 2;
85
+ if !data.len().is_multiple_of(frame_width) {
86
+ return Err(Error::invalid_data(
87
+ "Waveform data length must be divisible by two samples per channel",
88
+ ));
89
+ }
90
+ Ok(Self {
91
+ sample_rate,
92
+ samples_per_pixel,
93
+ channels,
94
+ storage_bits,
95
+ data,
96
+ })
97
+ }
98
+
99
+ /// Loads a waveform from a path, inferring the format from the extension when omitted.
100
+ pub fn load_from_path(
101
+ path: impl AsRef<Path>,
102
+ format: Option<WaveformFormat>,
103
+ ) -> Result<Self, Error> {
104
+ let path = path.as_ref();
105
+ let resolved = format
106
+ .or_else(|| WaveformFormat::from_path(path))
107
+ .ok_or_else(|| Error::UnsupportedFormat {
108
+ format: path
109
+ .extension()
110
+ .and_then(|value| value.to_str())
111
+ .unwrap_or_default()
112
+ .to_string(),
113
+ })?;
114
+ let file = File::open(path)?;
115
+ Self::load_from_reader(BufReader::new(file), resolved)
116
+ }
117
+
118
+ /// Loads a waveform from an arbitrary reader.
119
+ pub fn load_from_reader<R: Read>(reader: R, format: WaveformFormat) -> Result<Self, Error> {
120
+ match format {
121
+ WaveformFormat::Dat => Self::read_dat(reader),
122
+ WaveformFormat::Json => Self::read_json(reader),
123
+ WaveformFormat::Txt => Err(Error::UnsupportedFormat {
124
+ format: "txt".to_string(),
125
+ }),
126
+ }
127
+ }
128
+
129
+ /// Saves the waveform to a path, inferring the format from the extension when omitted.
130
+ pub fn save_to_path(
131
+ &self,
132
+ path: impl AsRef<Path>,
133
+ format: Option<WaveformFormat>,
134
+ ) -> Result<(), Error> {
135
+ let path = path.as_ref();
136
+ let resolved = format
137
+ .or_else(|| WaveformFormat::from_path(path))
138
+ .ok_or_else(|| Error::UnsupportedFormat {
139
+ format: path
140
+ .extension()
141
+ .and_then(|value| value.to_str())
142
+ .unwrap_or_default()
143
+ .to_string(),
144
+ })?;
145
+ let file = File::create(path)?;
146
+ self.write_to_writer(BufWriter::new(file), resolved, None)
147
+ }
148
+
149
+ /// Writes the waveform to an arbitrary writer.
150
+ pub fn write_to_writer<W: Write>(
151
+ &self,
152
+ writer: W,
153
+ format: WaveformFormat,
154
+ bits: Option<u8>,
155
+ ) -> Result<(), Error> {
156
+ let bits = bits.unwrap_or(self.storage_bits);
157
+ Self::validate_storage_bits(bits)?;
158
+ match format {
159
+ WaveformFormat::Dat => self.write_dat(writer, bits),
160
+ WaveformFormat::Json => self.write_json(writer, bits),
161
+ WaveformFormat::Txt => self.write_txt(writer, bits),
162
+ }
163
+ }
164
+
165
+ /// Returns the sample rate of the source audio.
166
+ pub const fn sample_rate(&self) -> u32 {
167
+ self.sample_rate
168
+ }
169
+
170
+ /// Returns the number of source samples represented by each waveform point.
171
+ pub const fn samples_per_pixel(&self) -> u32 {
172
+ self.samples_per_pixel
173
+ }
174
+
175
+ /// Returns the number of waveform channels.
176
+ pub const fn channels(&self) -> u16 {
177
+ self.channels
178
+ }
179
+
180
+ /// Returns the preferred serialized bit depth.
181
+ pub const fn storage_bits(&self) -> u8 {
182
+ self.storage_bits
183
+ }
184
+
185
+ /// Returns the number of waveform points per channel.
186
+ pub fn len(&self) -> usize {
187
+ self.data.len() / (usize::from(self.channels) * 2)
188
+ }
189
+
190
+ /// Returns `true` when the waveform contains no points.
191
+ pub fn is_empty(&self) -> bool {
192
+ self.data.is_empty()
193
+ }
194
+
195
+ /// Returns the duration represented by the waveform in seconds.
196
+ pub fn duration_seconds(&self) -> f64 {
197
+ self.len() as f64 * self.samples_per_pixel as f64 / self.sample_rate as f64
198
+ }
199
+
200
+ /// Appends a frame containing one point per channel.
201
+ pub fn push_frame(&mut self, points: &[WaveformPoint]) -> Result<(), Error> {
202
+ if points.len() != usize::from(self.channels) {
203
+ return Err(Error::invalid_argument(
204
+ "points",
205
+ format!(
206
+ "Expected {} points, received {}",
207
+ self.channels,
208
+ points.len()
209
+ ),
210
+ ));
211
+ }
212
+ for point in points {
213
+ self.data.push(point.min);
214
+ self.data.push(point.max);
215
+ }
216
+ Ok(())
217
+ }
218
+
219
+ /// Returns the waveform point at `index` for the given `channel`.
220
+ pub fn point(&self, channel: u16, index: usize) -> Option<WaveformPoint> {
221
+ if channel >= self.channels || index >= self.len() {
222
+ return None;
223
+ }
224
+ let offset = self.offset(channel, index);
225
+ Some(WaveformPoint {
226
+ min: self.data[offset],
227
+ max: self.data[offset + 1],
228
+ })
229
+ }
230
+
231
+ /// Returns the interleaved internal min/max data.
232
+ pub fn interleaved_samples(&self) -> &[i16] {
233
+ &self.data
234
+ }
235
+
236
+ /// Returns the heap allocation size of the sample buffer, including unused capacity.
237
+ pub fn allocated_bytes(&self) -> usize {
238
+ self.data.capacity() * std::mem::size_of::<i16>()
239
+ }
240
+
241
+ /// Returns a copy of the waveform with its serialized bit-depth preference changed.
242
+ pub fn with_storage_bits(mut self, bits: u8) -> Result<Self, Error> {
243
+ Self::validate_storage_bits(bits)?;
244
+ self.storage_bits = bits;
245
+ Ok(self)
246
+ }
247
+
248
+ /// Returns the amplitude scale needed to fill the 16-bit range over the provided region.
249
+ pub fn auto_amplitude_scale(&self, start_index: usize, end_index: usize) -> Result<f64, Error> {
250
+ if start_index >= end_index || end_index > self.len() {
251
+ return Err(Error::invalid_argument(
252
+ "amplitude range",
253
+ "Invalid amplitude scaling range",
254
+ ));
255
+ }
256
+
257
+ let mut low = i32::MAX;
258
+ let mut high = i32::MIN;
259
+ for index in start_index..end_index {
260
+ for channel in 0..self.channels {
261
+ let point = self.point(channel, index).expect("point bounds checked");
262
+ low = low.min(i32::from(point.min));
263
+ high = high.max(i32::from(point.max));
264
+ }
265
+ }
266
+
267
+ let high_scale = if high == 0 {
268
+ 1.0
269
+ } else {
270
+ 32767.0 / f64::from(high)
271
+ };
272
+ let low_scale = if low == 0 {
273
+ 1.0
274
+ } else {
275
+ 32767.0 / f64::from(low)
276
+ };
277
+
278
+ Ok(high_scale.min(low_scale).abs())
279
+ }
280
+
281
+ /// Scales all waveform points by the provided amplitude strategy.
282
+ pub fn scale_amplitude(&self, scale: AmplitudeScale) -> Result<Self, Error> {
283
+ let multiplier = match scale {
284
+ AmplitudeScale::Fixed(multiplier) => {
285
+ if !multiplier.is_finite() || multiplier < 0.0 {
286
+ return Err(Error::invalid_argument(
287
+ "amplitude scale",
288
+ "Invalid amplitude scale: must be a positive number",
289
+ ));
290
+ }
291
+ multiplier
292
+ }
293
+ AmplitudeScale::Auto if self.is_empty() => 1.0,
294
+ AmplitudeScale::Auto => self.auto_amplitude_scale(0, self.len())?,
295
+ };
296
+
297
+ let mut scaled = self.clone();
298
+ for sample in &mut scaled.data {
299
+ *sample = clamp_scaled(i32::from(*sample), multiplier);
300
+ }
301
+ Ok(scaled)
302
+ }
303
+
304
+ /// Resamples the waveform to a coarser scale.
305
+ ///
306
+ /// The resulting waveform always has the same sample rate and channel count
307
+ /// as the original.
308
+ pub fn resample(&self, scale: ScaleSpec) -> Result<Self, Error> {
309
+ let total_frames = self.len() * self.samples_per_pixel as usize;
310
+ let output_samples_per_pixel = scale.resolve(self.sample_rate, total_frames)?;
311
+ if output_samples_per_pixel == self.samples_per_pixel {
312
+ return Ok(self.clone());
313
+ }
314
+ if output_samples_per_pixel < self.samples_per_pixel {
315
+ return Err(Error::invalid_argument(
316
+ "zoom",
317
+ format!("Invalid zoom, minimum: {}", self.samples_per_pixel),
318
+ ));
319
+ }
320
+
321
+ let input_samples_per_pixel = self.samples_per_pixel;
322
+ let mut output = Self::new(self.sample_rate, output_samples_per_pixel, self.channels)?;
323
+ output.storage_bits = self.storage_bits;
324
+
325
+ let channels = usize::from(self.channels);
326
+ let mut min = vec![0_i16; channels];
327
+ let mut max = vec![0_i16; channels];
328
+ if !self.is_empty() {
329
+ for channel in 0..self.channels {
330
+ let point = self.point(channel, 0).expect("point within bounds");
331
+ min[channel as usize] = point.min;
332
+ max[channel as usize] = point.max;
333
+ }
334
+ }
335
+
336
+ let mut input_index = 0_usize;
337
+ let mut output_index = 0_usize;
338
+ let mut last_input_index = 0_usize;
339
+
340
+ while input_index < self.len() {
341
+ while sample_at_pixel(output_index, output_samples_per_pixel)
342
+ / input_samples_per_pixel as usize
343
+ == input_index
344
+ {
345
+ if output_index > 0 {
346
+ flush_resampled_frame(&mut output, &min, &max)?;
347
+ }
348
+ last_input_index = input_index;
349
+ output_index += 1;
350
+
351
+ let current = sample_at_pixel(output_index, output_samples_per_pixel);
352
+ let previous = sample_at_pixel(output_index - 1, output_samples_per_pixel);
353
+ if current != previous {
354
+ min.fill(i16::MAX);
355
+ max.fill(i16::MIN);
356
+ }
357
+ }
358
+
359
+ let mut stop = sample_at_pixel(output_index, output_samples_per_pixel)
360
+ / input_samples_per_pixel as usize;
361
+ stop = stop.min(self.len());
362
+ while input_index < stop {
363
+ for channel in 0..self.channels {
364
+ let point = self
365
+ .point(channel, input_index)
366
+ .expect("point within bounds");
367
+ let channel_index = channel as usize;
368
+ if point.min < min[channel_index] {
369
+ min[channel_index] = point.min;
370
+ }
371
+ if point.max > max[channel_index] {
372
+ max[channel_index] = point.max;
373
+ }
374
+ }
375
+ input_index += 1;
376
+ }
377
+ }
378
+
379
+ if input_index != last_input_index {
380
+ flush_resampled_frame(&mut output, &min, &max)?;
381
+ }
382
+
383
+ Ok(output)
384
+ }
385
+
386
+ fn validate_metadata(
387
+ sample_rate: u32,
388
+ samples_per_pixel: u32,
389
+ channels: u16,
390
+ ) -> Result<(), Error> {
391
+ if sample_rate == 0 {
392
+ return Err(Error::invalid_argument(
393
+ "sample rate",
394
+ "Invalid sample rate: minimum 1 Hz",
395
+ ));
396
+ }
397
+ if samples_per_pixel < 2 {
398
+ return Err(Error::invalid_argument(
399
+ "samples per pixel",
400
+ "Invalid samples per pixel: minimum 2",
401
+ ));
402
+ }
403
+ if channels == 0 || usize::from(channels) > MAX_CHANNELS {
404
+ return Err(Error::invalid_argument(
405
+ "channels",
406
+ format!("Invalid channels: must be between 1 and {MAX_CHANNELS}"),
407
+ ));
408
+ }
409
+ Ok(())
410
+ }
411
+
412
+ fn validate_storage_bits(bits: u8) -> Result<(), Error> {
413
+ if bits != 8 && bits != 16 {
414
+ return Err(Error::invalid_argument(
415
+ "bits",
416
+ "Invalid bits: must be either 8 or 16",
417
+ ));
418
+ }
419
+ Ok(())
420
+ }
421
+
422
+ fn offset(&self, channel: u16, index: usize) -> usize {
423
+ (index * usize::from(self.channels) + usize::from(channel)) * 2
424
+ }
425
+
426
+ fn read_dat<R: Read>(mut reader: R) -> Result<Self, Error> {
427
+ let version = reader.read_i32::<LittleEndian>()?;
428
+ if version != 1 && version != 2 {
429
+ return Err(Error::invalid_data(format!(
430
+ "Cannot load data file version: {version}"
431
+ )));
432
+ }
433
+ let flags = reader.read_u32::<LittleEndian>()?;
434
+ let sample_rate = reader.read_u32::<LittleEndian>()?;
435
+ let samples_per_pixel = reader.read_u32::<LittleEndian>()?;
436
+ let length = reader.read_u32::<LittleEndian>()? as usize;
437
+ let channels = if version == 2 {
438
+ let channels = reader.read_i32::<LittleEndian>()?;
439
+ u16::try_from(channels).map_err(|_| {
440
+ Error::invalid_argument(
441
+ "channels",
442
+ format!("Invalid channels: must be between 1 and {MAX_CHANNELS}"),
443
+ )
444
+ })?
445
+ } else {
446
+ 1
447
+ };
448
+ Self::validate_metadata(sample_rate, samples_per_pixel, channels)?;
449
+
450
+ let bits = if flags & FLAG_8_BIT != 0 { 8 } else { 16 };
451
+ let mut data = Vec::with_capacity(length * usize::from(channels) * 2);
452
+ if bits == 8 {
453
+ for _ in 0..length * usize::from(channels) {
454
+ let Some(min_value) = read_optional_i8(&mut reader)? else {
455
+ break;
456
+ };
457
+ let Some(max_value) = read_optional_i8(&mut reader)? else {
458
+ break;
459
+ };
460
+ data.push(i16::from(min_value) * 256);
461
+ data.push(i16::from(max_value) * 256);
462
+ }
463
+ } else {
464
+ for _ in 0..length * usize::from(channels) * 2 {
465
+ let Some(value) = read_optional_i16(&mut reader)? else {
466
+ break;
467
+ };
468
+ data.push(value);
469
+ }
470
+ }
471
+ Self::from_interleaved_samples(sample_rate, samples_per_pixel, channels, data, bits)
472
+ }
473
+
474
+ fn read_json<R: Read>(reader: R) -> Result<Self, Error> {
475
+ let json: JsonWaveform = serde_json::from_reader(reader)?;
476
+ if json.version != 1 && json.version != 2 {
477
+ return Err(Error::invalid_data("Invalid version: expecting 1 or 2"));
478
+ }
479
+ let channels = json.channels.unwrap_or(1);
480
+ Self::validate_metadata(json.sample_rate, json.samples_per_pixel, channels)?;
481
+ Self::validate_storage_bits(json.bits)?;
482
+
483
+ let expected = json
484
+ .length
485
+ .checked_mul(usize::from(channels))
486
+ .and_then(|value| value.checked_mul(2))
487
+ .ok_or_else(|| Error::invalid_data("Waveform length is too large"))?;
488
+ if json.data.len() != expected {
489
+ return Err(Error::invalid_data(format!(
490
+ "Length mismatch: expected {expected} values, found {}",
491
+ json.data.len()
492
+ )));
493
+ }
494
+
495
+ let mut data = Vec::with_capacity(expected);
496
+ if json.bits == 8 {
497
+ for value in json.data {
498
+ if !(-128..=127).contains(&value) {
499
+ return Err(Error::invalid_data(format!(
500
+ "Data value out of range: {value}"
501
+ )));
502
+ }
503
+ data.push((value as i16) * 256);
504
+ }
505
+ } else {
506
+ for value in json.data {
507
+ if !(-32768..=32767).contains(&value) {
508
+ return Err(Error::invalid_data(format!(
509
+ "Data value out of range: {value}"
510
+ )));
511
+ }
512
+ data.push(value as i16);
513
+ }
514
+ }
515
+
516
+ Self::from_interleaved_samples(
517
+ json.sample_rate,
518
+ json.samples_per_pixel,
519
+ channels,
520
+ data,
521
+ json.bits,
522
+ )
523
+ }
524
+
525
+ fn write_dat<W: Write>(&self, mut writer: W, bits: u8) -> Result<(), Error> {
526
+ let version = if self.channels == 1 { 1_i32 } else { 2_i32 };
527
+ writer.write_i32::<LittleEndian>(version)?;
528
+ let flags = if bits == 8 { FLAG_8_BIT } else { 0 };
529
+ writer.write_u32::<LittleEndian>(flags)?;
530
+ writer.write_u32::<LittleEndian>(self.sample_rate)?;
531
+ writer.write_u32::<LittleEndian>(self.samples_per_pixel)?;
532
+ writer.write_u32::<LittleEndian>(self.len() as u32)?;
533
+ if version == 2 {
534
+ writer.write_u32::<LittleEndian>(u32::from(self.channels))?;
535
+ }
536
+ if bits == 8 {
537
+ for value in &self.data {
538
+ writer.write_i8((value / 256) as i8)?;
539
+ }
540
+ } else {
541
+ for value in &self.data {
542
+ writer.write_i16::<LittleEndian>(*value)?;
543
+ }
544
+ }
545
+ Ok(())
546
+ }
547
+
548
+ fn write_txt<W: Write>(&self, mut writer: W, bits: u8) -> Result<(), Error> {
549
+ for index in 0..self.len() {
550
+ for channel in 0..self.channels {
551
+ if channel > 0 {
552
+ writer.write_all(b",")?;
553
+ }
554
+ let point = self.point(channel, index).expect("point within range");
555
+ if bits == 8 {
556
+ write!(writer, "{},{}", point.min / 256, point.max / 256)?;
557
+ } else {
558
+ write!(writer, "{},{}", point.min, point.max)?;
559
+ }
560
+ }
561
+ writer.write_all(b"\n")?;
562
+ }
563
+ Ok(())
564
+ }
565
+
566
+ fn write_json<W: Write>(&self, mut writer: W, bits: u8) -> Result<(), Error> {
567
+ write!(
568
+ writer,
569
+ "{{\"version\":2,\"channels\":{},\"sample_rate\":{},\"samples_per_pixel\":{},\"bits\":{},\"length\":{},\"data\":[",
570
+ self.channels,
571
+ self.sample_rate,
572
+ self.samples_per_pixel,
573
+ bits,
574
+ self.len()
575
+ )?;
576
+ for (index, value) in self.data.iter().enumerate() {
577
+ if index > 0 {
578
+ writer.write_all(b",")?;
579
+ }
580
+ let serialized = if bits == 8 { value / 256 } else { *value };
581
+ write!(writer, "{serialized}")?;
582
+ }
583
+ writer.write_all(b"]}\n")?;
584
+ Ok(())
585
+ }
586
+ }
587
+
588
+ fn clamp_scaled(value: i32, multiplier: f64) -> i16 {
589
+ let scaled = (f64::from(value) * multiplier).clamp(f64::from(i16::MIN), f64::from(i16::MAX));
590
+ scaled as i16
591
+ }
592
+
593
+ fn read_optional_i8<R: Read>(reader: &mut R) -> Result<Option<i8>, Error> {
594
+ match reader.read_i8() {
595
+ Ok(value) => Ok(Some(value)),
596
+ Err(error) if error.kind() == ErrorKind::UnexpectedEof => Ok(None),
597
+ Err(error) => Err(error.into()),
598
+ }
599
+ }
600
+
601
+ fn read_optional_i16<R: Read>(reader: &mut R) -> Result<Option<i16>, Error> {
602
+ match reader.read_i16::<LittleEndian>() {
603
+ Ok(value) => Ok(Some(value)),
604
+ Err(error) if error.kind() == ErrorKind::UnexpectedEof => Ok(None),
605
+ Err(error) => Err(error.into()),
606
+ }
607
+ }
608
+
609
+ fn sample_at_pixel(index: usize, samples_per_pixel: u32) -> usize {
610
+ index * samples_per_pixel as usize
611
+ }
612
+
613
+ fn flush_resampled_frame(waveform: &mut Waveform, min: &[i16], max: &[i16]) -> Result<(), Error> {
614
+ let points = min
615
+ .iter()
616
+ .zip(max.iter())
617
+ .map(|(min, max)| WaveformPoint {
618
+ min: *min,
619
+ max: *max,
620
+ })
621
+ .collect::<Vec<_>>();
622
+ waveform.push_frame(&points)
623
+ }
624
+
625
+ #[cfg(test)]
626
+ mod tests {
627
+ use super::{AmplitudeScale, Waveform, WaveformFormat, WaveformPoint};
628
+ use crate::audio::ScaleSpec;
629
+
630
+ fn sample_waveform() -> Waveform {
631
+ let mut waveform = Waveform::new(48_000, 64, 1).expect("waveform");
632
+ waveform
633
+ .push_frame(&[WaveformPoint { min: -10, max: 20 }])
634
+ .expect("first frame");
635
+ waveform
636
+ .push_frame(&[WaveformPoint { min: -30, max: 40 }])
637
+ .expect("second frame");
638
+ waveform
639
+ }
640
+
641
+ #[test]
642
+ fn validates_waveform_metadata_and_frame_shapes() {
643
+ let error = Waveform::new(0, 64, 1).expect_err("invalid sample rate");
644
+ assert_eq!(error.to_string(), "Invalid sample rate: minimum 1 Hz");
645
+
646
+ let error = Waveform::new(48_000, 1, 1).expect_err("invalid scale");
647
+ assert_eq!(error.to_string(), "Invalid samples per pixel: minimum 2");
648
+
649
+ let error = Waveform::new(48_000, 64, 25).expect_err("invalid channels");
650
+ assert_eq!(
651
+ error.to_string(),
652
+ "Invalid channels: must be between 1 and 24"
653
+ );
654
+
655
+ let error = Waveform::from_interleaved_samples(48_000, 64, 2, vec![1, 2, 3], 16)
656
+ .expect_err("unaligned waveform data");
657
+ assert_eq!(
658
+ error.to_string(),
659
+ "Waveform data length must be divisible by two samples per channel"
660
+ );
661
+
662
+ let mut waveform = Waveform::new(48_000, 64, 2).expect("waveform");
663
+ let error = waveform
664
+ .push_frame(&[WaveformPoint { min: 0, max: 1 }])
665
+ .expect_err("wrong frame width");
666
+ assert_eq!(error.to_string(), "Expected 2 points, received 1");
667
+ }
668
+
669
+ #[test]
670
+ fn scales_waveform_amplitude_using_fixed_and_auto_modes() {
671
+ let waveform = sample_waveform();
672
+
673
+ let fixed = waveform
674
+ .scale_amplitude(AmplitudeScale::Fixed(2.0))
675
+ .expect("scale fixed");
676
+ assert_eq!(
677
+ fixed.point(0, 0).expect("scaled point"),
678
+ WaveformPoint { min: -20, max: 40 }
679
+ );
680
+
681
+ let auto = waveform
682
+ .scale_amplitude(AmplitudeScale::Auto)
683
+ .expect("scale auto");
684
+ assert_eq!(
685
+ auto.point(0, 1).expect("auto point"),
686
+ WaveformPoint {
687
+ min: -32_767,
688
+ max: 32_767,
689
+ }
690
+ );
691
+
692
+ let error = waveform
693
+ .scale_amplitude(AmplitudeScale::Fixed(-1.0))
694
+ .expect_err("negative scale");
695
+ assert_eq!(
696
+ error.to_string(),
697
+ "Invalid amplitude scale: must be a positive number"
698
+ );
699
+
700
+ for value in [f64::NAN, f64::INFINITY] {
701
+ let error = waveform
702
+ .scale_amplitude(AmplitudeScale::Fixed(value))
703
+ .expect_err("non-finite amplitude scale");
704
+ assert_eq!(
705
+ error.to_string(),
706
+ "Invalid amplitude scale: must be a positive number"
707
+ );
708
+ }
709
+ }
710
+
711
+ #[test]
712
+ fn resamples_waveforms_to_a_coarser_scale() {
713
+ let mut waveform = Waveform::new(48_000, 512, 1).expect("waveform");
714
+ for point in [
715
+ WaveformPoint { min: 0, max: 0 },
716
+ WaveformPoint { min: -10, max: 10 },
717
+ WaveformPoint { min: 0, max: 0 },
718
+ WaveformPoint { min: -5, max: 7 },
719
+ WaveformPoint { min: -5, max: 7 },
720
+ WaveformPoint { min: 0, max: 0 },
721
+ WaveformPoint { min: 0, max: 0 },
722
+ WaveformPoint { min: 0, max: 0 },
723
+ WaveformPoint { min: 0, max: 0 },
724
+ WaveformPoint { min: -2, max: 2 },
725
+ ] {
726
+ waveform.push_frame(&[point]).expect("push point");
727
+ }
728
+
729
+ let resampled = waveform
730
+ .resample(ScaleSpec::SamplesPerPixel(1024))
731
+ .expect("resample waveform");
732
+ assert_eq!(resampled.len(), 5);
733
+ assert_eq!(resampled.samples_per_pixel(), 1024);
734
+ assert_eq!(
735
+ resampled.point(0, 0).expect("point 0"),
736
+ WaveformPoint { min: -10, max: 10 }
737
+ );
738
+ assert_eq!(
739
+ resampled.point(0, 1).expect("point 1"),
740
+ WaveformPoint { min: -5, max: 7 }
741
+ );
742
+ assert_eq!(
743
+ resampled.point(0, 2).expect("point 2"),
744
+ WaveformPoint { min: -5, max: 7 }
745
+ );
746
+ assert_eq!(
747
+ resampled.point(0, 3).expect("point 3"),
748
+ WaveformPoint { min: 0, max: 0 }
749
+ );
750
+ assert_eq!(
751
+ resampled.point(0, 4).expect("point 4"),
752
+ WaveformPoint { min: -2, max: 2 }
753
+ );
754
+
755
+ let error = waveform
756
+ .resample(ScaleSpec::SamplesPerPixel(256))
757
+ .expect_err("finer zoom should fail");
758
+ assert_eq!(error.to_string(), "Invalid zoom, minimum: 512");
759
+ }
760
+
761
+ #[test]
762
+ fn serializes_text_and_json_for_two_channel_waveforms() {
763
+ let waveform = Waveform::from_interleaved_samples(
764
+ 44_100,
765
+ 256,
766
+ 2,
767
+ vec![-1024, 1024, -2048, 2048, -3072, 3072, -4096, 4096],
768
+ 16,
769
+ )
770
+ .expect("waveform");
771
+
772
+ let mut txt = Vec::new();
773
+ waveform
774
+ .write_to_writer(&mut txt, WaveformFormat::Txt, Some(8))
775
+ .expect("write txt");
776
+ assert_eq!(
777
+ String::from_utf8(txt).expect("utf8"),
778
+ "-4,4,-8,8\n-12,12,-16,16\n"
779
+ );
780
+
781
+ let mut json = Vec::new();
782
+ waveform
783
+ .write_to_writer(&mut json, WaveformFormat::Json, Some(16))
784
+ .expect("write json");
785
+ assert_eq!(
786
+ String::from_utf8(json).expect("utf8"),
787
+ "{\"version\":2,\"channels\":2,\"sample_rate\":44100,\"samples_per_pixel\":256,\"bits\":16,\"length\":2,\"data\":[-1024,1024,-2048,2048,-3072,3072,-4096,4096]}\n"
788
+ );
789
+ }
790
+ }