1use std::io::{Read, Seek, SeekFrom};
4use std::path::{Path, PathBuf};
5
6use runmat_builtins::{
7 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
8 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
9 NumericDType, StructValue, Tensor, Value,
10};
11use runmat_filesystem as fs;
12use runmat_macros::runtime_builtin;
13
14use crate::builtins::common::fs::expand_user_path;
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ShapeRequirements,
18};
19use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
20
21const AUDIOINFO_BUILTIN_NAME: &str = "audioinfo";
22const AUDIOREAD_BUILTIN_NAME: &str = "audioread";
23const MAX_AUDIOINFO_PREFIX_BYTES: u64 = 1024 * 1024;
24const MAX_AUDIOINFO_TAIL_BYTES: u64 = 64 * 1024;
25
26const AUDIOINFO_OUTPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "info",
28 ty: BuiltinParamType::Any,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "Audio metadata structure.",
32}];
33const AUDIOINFO_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
34 name: "filename",
35 ty: BuiltinParamType::StringScalar,
36 arity: BuiltinParamArity::Required,
37 default: None,
38 description: "Audio file path.",
39}];
40const AUDIOINFO_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
41 label: "info = audioinfo(filename)",
42 inputs: &AUDIOINFO_INPUTS,
43 outputs: &AUDIOINFO_OUTPUTS,
44}];
45
46const AUDIOINFO_ERROR_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
47 code: "RM.AUDIOINFO.ARGUMENT",
48 identifier: Some("RunMat:audioinfo:InvalidArgument"),
49 when: "Filename is missing or cannot be interpreted as a scalar path.",
50 message: "audioinfo: invalid filename",
51};
52const AUDIOINFO_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
53 code: "RM.AUDIOINFO.IO",
54 identifier: Some("RunMat:audioinfo:Io"),
55 when: "The audio file cannot be read.",
56 message: "audioinfo: unable to read file",
57};
58const AUDIOINFO_ERROR_FORMAT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
59 code: "RM.AUDIOINFO.FORMAT",
60 identifier: Some("RunMat:audioinfo:UnsupportedFormat"),
61 when: "The file is not a supported audio container or has malformed metadata.",
62 message: "audioinfo: unsupported or invalid audio file",
63};
64const AUDIOINFO_ERRORS: [BuiltinErrorDescriptor; 3] = [
65 AUDIOINFO_ERROR_ARGUMENT,
66 AUDIOINFO_ERROR_IO,
67 AUDIOINFO_ERROR_FORMAT,
68];
69
70pub const AUDIOINFO_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
71 signatures: &AUDIOINFO_SIGNATURES,
72 output_mode: BuiltinOutputMode::Fixed,
73 completion_policy: BuiltinCompletionPolicy::Public,
74 errors: &AUDIOINFO_ERRORS,
75};
76
77#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::audio")]
78pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
79 name: "audioinfo",
80 op_kind: GpuOpKind::Custom("io-audioinfo"),
81 supported_precisions: &[],
82 broadcast: BroadcastSemantics::None,
83 provider_hooks: &[],
84 constant_strategy: ConstantStrategy::InlineLiteral,
85 residency: ResidencyPolicy::GatherImmediately,
86 nan_mode: ReductionNaN::Include,
87 two_pass_threshold: None,
88 workgroup_size: None,
89 accepts_nan_mode: false,
90 notes: "Runs on the host; file metadata inspection is not an acceleration operation.",
91};
92
93#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::audio")]
94pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
95 name: "audioinfo",
96 shape: ShapeRequirements::Any,
97 constant_strategy: ConstantStrategy::InlineLiteral,
98 elementwise: None,
99 reduction: None,
100 emits_nan: false,
101 notes: "Not eligible for fusion; performs host-side file I/O.",
102};
103
104const AUDIOREAD_OUTPUTS: [BuiltinParamDescriptor; 2] = [
105 BuiltinParamDescriptor {
106 name: "y",
107 ty: BuiltinParamType::NumericArray,
108 arity: BuiltinParamArity::Required,
109 default: None,
110 description: "Decoded samples as an N-by-C matrix.",
111 },
112 BuiltinParamDescriptor {
113 name: "Fs",
114 ty: BuiltinParamType::NumericScalar,
115 arity: BuiltinParamArity::Optional,
116 default: None,
117 description: "Sample rate in Hz.",
118 },
119];
120const AUDIOREAD_OUTPUTS_SAMPLES: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
121 name: "y",
122 ty: BuiltinParamType::NumericArray,
123 arity: BuiltinParamArity::Required,
124 default: None,
125 description: "Decoded samples as an N-by-C matrix.",
126}];
127const AUDIOREAD_INPUTS_FILENAME: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
128 name: "filename",
129 ty: BuiltinParamType::StringScalar,
130 arity: BuiltinParamArity::Required,
131 default: None,
132 description: "Audio file path.",
133}];
134const AUDIOREAD_INPUTS_RANGE: [BuiltinParamDescriptor; 2] = [
135 BuiltinParamDescriptor {
136 name: "filename",
137 ty: BuiltinParamType::StringScalar,
138 arity: BuiltinParamArity::Required,
139 default: None,
140 description: "Audio file path.",
141 },
142 BuiltinParamDescriptor {
143 name: "samples",
144 ty: BuiltinParamType::NumericArray,
145 arity: BuiltinParamArity::Optional,
146 default: None,
147 description: "One-based inclusive frame range [first last].",
148 },
149];
150const AUDIOREAD_INPUTS_RANGE_NATIVE: [BuiltinParamDescriptor; 3] = [
151 BuiltinParamDescriptor {
152 name: "filename",
153 ty: BuiltinParamType::StringScalar,
154 arity: BuiltinParamArity::Required,
155 default: None,
156 description: "Audio file path.",
157 },
158 BuiltinParamDescriptor {
159 name: "samples",
160 ty: BuiltinParamType::NumericArray,
161 arity: BuiltinParamArity::Optional,
162 default: None,
163 description: "One-based inclusive frame range [first last].",
164 },
165 BuiltinParamDescriptor {
166 name: "datatype",
167 ty: BuiltinParamType::StringScalar,
168 arity: BuiltinParamArity::Optional,
169 default: Some("\"double\""),
170 description: "Output class; \"native\" preserves representable source classes.",
171 },
172];
173const AUDIOREAD_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
174 BuiltinSignatureDescriptor {
175 label: "y = audioread(filename)",
176 inputs: &AUDIOREAD_INPUTS_FILENAME,
177 outputs: &AUDIOREAD_OUTPUTS_SAMPLES,
178 },
179 BuiltinSignatureDescriptor {
180 label: "[y, Fs] = audioread(filename)",
181 inputs: &AUDIOREAD_INPUTS_FILENAME,
182 outputs: &AUDIOREAD_OUTPUTS,
183 },
184 BuiltinSignatureDescriptor {
185 label: "y = audioread(filename, samples)",
186 inputs: &AUDIOREAD_INPUTS_RANGE,
187 outputs: &AUDIOREAD_OUTPUTS_SAMPLES,
188 },
189 BuiltinSignatureDescriptor {
190 label: "[y, Fs] = audioread(filename, samples, datatype)",
191 inputs: &AUDIOREAD_INPUTS_RANGE_NATIVE,
192 outputs: &AUDIOREAD_OUTPUTS,
193 },
194];
195const AUDIOREAD_ERROR_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
196 code: "RM.AUDIOREAD.ARGUMENT",
197 identifier: Some("RunMat:audioread:InvalidArgument"),
198 when: "Filename, sample range, datatype, or output count is invalid.",
199 message: "audioread: invalid argument",
200};
201const AUDIOREAD_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
202 code: "RM.AUDIOREAD.IO",
203 identifier: Some("RunMat:audioread:Io"),
204 when: "The audio file cannot be read.",
205 message: "audioread: unable to read file",
206};
207const AUDIOREAD_ERROR_FORMAT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
208 code: "RM.AUDIOREAD.FORMAT",
209 identifier: Some("RunMat:audioread:UnsupportedFormat"),
210 when: "The file is not a supported audio container, uses unsupported audio coding, or is malformed.",
211 message: "audioread: unsupported or invalid audio file",
212};
213const AUDIOREAD_ERRORS: [BuiltinErrorDescriptor; 3] = [
214 AUDIOREAD_ERROR_ARGUMENT,
215 AUDIOREAD_ERROR_IO,
216 AUDIOREAD_ERROR_FORMAT,
217];
218
219pub const AUDIOREAD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
220 signatures: &AUDIOREAD_SIGNATURES,
221 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
222 completion_policy: BuiltinCompletionPolicy::Public,
223 errors: &AUDIOREAD_ERRORS,
224};
225
226#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::audio")]
227pub const AUDIOREAD_GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
228 name: "audioread",
229 op_kind: GpuOpKind::Custom("io-audioread"),
230 supported_precisions: &[],
231 broadcast: BroadcastSemantics::None,
232 provider_hooks: &[],
233 constant_strategy: ConstantStrategy::InlineLiteral,
234 residency: ResidencyPolicy::GatherImmediately,
235 nan_mode: ReductionNaN::Include,
236 two_pass_threshold: None,
237 workgroup_size: None,
238 accepts_nan_mode: false,
239 notes: "Runs on the host; file I/O and audio decoding are not acceleration operations.",
240};
241
242#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::audio")]
243pub const AUDIOREAD_FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
244 name: "audioread",
245 shape: ShapeRequirements::Any,
246 constant_strategy: ConstantStrategy::InlineLiteral,
247 elementwise: None,
248 reduction: None,
249 emits_nan: false,
250 notes: "Not eligible for fusion; performs host-side file I/O and decoding.",
251};
252
253fn audioinfo_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
254 audioinfo_error_with(error, error.message)
255}
256
257fn audioinfo_error_with(
258 error: &'static BuiltinErrorDescriptor,
259 message: impl Into<String>,
260) -> RuntimeError {
261 let mut builder = build_runtime_error(message).with_builtin(AUDIOINFO_BUILTIN_NAME);
262 if let Some(identifier) = error.identifier {
263 builder = builder.with_identifier(identifier);
264 }
265 builder.build()
266}
267
268fn audioinfo_error_with_source<E>(
269 error: &'static BuiltinErrorDescriptor,
270 message: impl Into<String>,
271 source: E,
272) -> RuntimeError
273where
274 E: std::error::Error + Send + Sync + 'static,
275{
276 let mut builder = build_runtime_error(message)
277 .with_builtin(AUDIOINFO_BUILTIN_NAME)
278 .with_source(source);
279 if let Some(identifier) = error.identifier {
280 builder = builder.with_identifier(identifier);
281 }
282 builder.build()
283}
284
285fn audioread_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
286 audioread_error_with(error, error.message)
287}
288
289fn audioread_error_with(
290 error: &'static BuiltinErrorDescriptor,
291 message: impl Into<String>,
292) -> RuntimeError {
293 let mut builder = build_runtime_error(message).with_builtin(AUDIOREAD_BUILTIN_NAME);
294 if let Some(identifier) = error.identifier {
295 builder = builder.with_identifier(identifier);
296 }
297 builder.build()
298}
299
300fn audioread_error_with_source<E>(
301 error: &'static BuiltinErrorDescriptor,
302 message: impl Into<String>,
303 source: E,
304) -> RuntimeError
305where
306 E: std::error::Error + Send + Sync + 'static,
307{
308 let mut builder = build_runtime_error(message)
309 .with_builtin(AUDIOREAD_BUILTIN_NAME)
310 .with_source(source);
311 if let Some(identifier) = error.identifier {
312 builder = builder.with_identifier(identifier);
313 }
314 builder.build()
315}
316
317fn map_audioinfo_control_flow(err: RuntimeError) -> RuntimeError {
318 let identifier = err.identifier().map(|value| value.to_string());
319 let message = err.message().to_string();
320 let mut builder = build_runtime_error(message)
321 .with_builtin(AUDIOINFO_BUILTIN_NAME)
322 .with_source(err);
323 if let Some(identifier) = identifier {
324 builder = builder.with_identifier(identifier);
325 }
326 builder.build()
327}
328
329fn map_audioread_control_flow(err: RuntimeError) -> RuntimeError {
330 let identifier = err.identifier().map(|value| value.to_string());
331 let message = err.message().to_string();
332 let mut builder = build_runtime_error(message)
333 .with_builtin(AUDIOREAD_BUILTIN_NAME)
334 .with_source(err);
335 if let Some(identifier) = identifier {
336 builder = builder.with_identifier(identifier);
337 }
338 builder.build()
339}
340
341#[runtime_builtin(
342 name = "audioinfo",
343 category = "io/audio",
344 summary = "Read metadata from an audio file.",
345 keywords = "audioinfo,audio,wav,flac,aiff,mp3,ogg,metadata,sample rate,channels",
346 accel = "cpu",
347 type_resolver(crate::builtins::io::type_resolvers::struct_type),
348 descriptor(crate::builtins::io::audio::AUDIOINFO_DESCRIPTOR),
349 builtin_path = "crate::builtins::io::audio"
350)]
351async fn audioinfo_builtin(filename: Value) -> BuiltinResult<Value> {
352 let filename = gather_if_needed_async(&filename)
353 .await
354 .map_err(map_audioinfo_control_flow)?;
355 let path = resolve_audioinfo_path(&filename)?;
356 let scan = read_audioinfo_scan(&path).await?;
357 let metadata = AudioMetadata::parse(&scan).map_err(|message| {
358 audioinfo_error_with(&AUDIOINFO_ERROR_FORMAT, format!("audioinfo: {message}"))
359 })?;
360 Ok(Value::Struct(
361 metadata.into_struct(&path, scan.file_size as f64),
362 ))
363}
364
365struct AudioInfoScan {
366 prefix: Vec<u8>,
367 tail: Option<Vec<u8>>,
368 file_size: u64,
369}
370
371async fn read_audioinfo_scan(path: &Path) -> BuiltinResult<AudioInfoScan> {
372 let mut file = fs::File::open_async(path).await.map_err(|err| {
373 audioinfo_error_with_source(
374 &AUDIOINFO_ERROR_IO,
375 format!("audioinfo: unable to read \"{}\" ({err})", path.display()),
376 err,
377 )
378 })?;
379 let metadata = match file.metadata_async().await {
380 Ok(metadata) => metadata,
381 Err(err) if err.kind() == std::io::ErrorKind::Unsupported => {
382 fs::metadata_async(path).await.map_err(|err| {
383 audioinfo_error_with_source(
384 &AUDIOINFO_ERROR_IO,
385 format!(
386 "audioinfo: unable to inspect \"{}\" after opening ({err})",
387 path.display()
388 ),
389 err,
390 )
391 })?
392 }
393 Err(err) => {
394 return Err(audioinfo_error_with_source(
395 &AUDIOINFO_ERROR_IO,
396 format!(
397 "audioinfo: unable to inspect opened file \"{}\" ({err})",
398 path.display()
399 ),
400 err,
401 ));
402 }
403 };
404 let file_size = metadata.len();
405 let prefix_len = file_size.min(MAX_AUDIOINFO_PREFIX_BYTES);
406 let mut bytes = Vec::new();
407 file.by_ref()
408 .take(prefix_len)
409 .read_to_end(&mut bytes)
410 .map_err(|err| {
411 audioinfo_error_with_source(
412 &AUDIOINFO_ERROR_IO,
413 format!("audioinfo: unable to read \"{}\" ({err})", path.display()),
414 err,
415 )
416 })?;
417 let tail = if bytes.starts_with(b"OggS") && file_size > prefix_len {
418 let tail_len = file_size.min(MAX_AUDIOINFO_TAIL_BYTES);
419 let tail_start = file_size.saturating_sub(tail_len);
420 match file.seek(SeekFrom::Start(tail_start)) {
421 Ok(_) => {
422 let mut tail = Vec::new();
423 match file.take(tail_len).read_to_end(&mut tail) {
424 Ok(_) => Some(tail),
425 Err(_) => None,
426 }
427 }
428 Err(_) => None,
429 }
430 } else {
431 None
432 };
433 Ok(AudioInfoScan {
434 prefix: bytes,
435 tail,
436 file_size,
437 })
438}
439
440#[runtime_builtin(
441 name = "audioread",
442 category = "io/audio",
443 summary = "Read audio samples from a file.",
444 keywords = "audioread,audio,wav,rf64,pcm,float,sample rate,channels",
445 accel = "cpu",
446 type_resolver(crate::builtins::io::type_resolvers::audioread_type),
447 descriptor(crate::builtins::io::audio::AUDIOREAD_DESCRIPTOR),
448 builtin_path = "crate::builtins::io::audio"
449)]
450async fn audioread_builtin(filename: Value, args: Vec<Value>) -> BuiltinResult<Value> {
451 let filename = gather_if_needed_async(&filename)
452 .await
453 .map_err(map_audioread_control_flow)?;
454 let mut gathered_args = Vec::with_capacity(args.len());
455 for arg in args {
456 gathered_args.push(
457 gather_if_needed_async(&arg)
458 .await
459 .map_err(map_audioread_control_flow)?,
460 );
461 }
462 let options = parse_audioread_options(&gathered_args)?;
463 let path = resolve_audioread_path(&filename)?;
464 let bytes = fs::read_async(&path).await.map_err(|err| {
465 audioread_error_with_source(
466 &AUDIOREAD_ERROR_IO,
467 format!("audioread: unable to read \"{}\" ({err})", path.display()),
468 err,
469 )
470 })?;
471 let decoded = decode_audio_samples(&bytes, options).map_err(|message| {
472 audioread_error_with(&AUDIOREAD_ERROR_FORMAT, format!("audioread: {message}"))
473 })?;
474
475 match crate::output_count::current_output_count() {
476 None => Ok(Value::Tensor(decoded.samples)),
477 Some(0) => Ok(Value::OutputList(Vec::new())),
478 Some(1) => Ok(Value::OutputList(vec![Value::Tensor(decoded.samples)])),
479 Some(2) => Ok(Value::OutputList(vec![
480 Value::Tensor(decoded.samples),
481 Value::Num(decoded.sample_rate),
482 ])),
483 Some(_) => Err(audioread_error_with(
484 &AUDIOREAD_ERROR_ARGUMENT,
485 "audioread: too many output arguments",
486 )),
487 }
488}
489
490#[derive(Debug, Clone, PartialEq)]
491struct AudioMetadata {
492 format: &'static str,
493 compression_method: String,
494 num_channels: u16,
495 sample_rate: f64,
496 total_samples: Option<u64>,
497 bits_per_sample: Option<u16>,
498 bit_rate: Option<f64>,
499}
500
501impl AudioMetadata {
502 fn parse(scan: &AudioInfoScan) -> Result<Self, String> {
503 let bytes = scan.prefix.as_slice();
504 if bytes.len() < 4 {
505 return Err("file is too small to contain audio metadata".to_string());
506 }
507 if bytes.starts_with(b"RIFF") || bytes.starts_with(b"RF64") {
508 return parse_wave(bytes);
509 }
510 if bytes.starts_with(b"fLaC") {
511 return parse_flac(bytes);
512 }
513 if bytes.starts_with(b"FORM") {
514 return parse_aiff(bytes);
515 }
516 if bytes.starts_with(b"OggS") {
517 return parse_ogg_vorbis(scan);
518 }
519 if let Some(mp3) = parse_mp3(bytes, scan.file_size) {
520 return Ok(mp3);
521 }
522 Err("unsupported audio format".to_string())
523 }
524
525 fn into_struct(self, path: &Path, file_size: f64) -> StructValue {
526 let total_samples = self.total_samples.map(|v| v as f64).unwrap_or(f64::NAN);
527 let duration = self
528 .total_samples
529 .map(|samples| samples as f64 / self.sample_rate)
530 .unwrap_or_else(|| {
531 self.bit_rate
532 .filter(|rate| *rate > 0.0)
533 .map(|rate| file_size * 8.0 / rate)
534 .unwrap_or(f64::NAN)
535 });
536 let mut out = StructValue::new();
537 out.insert(
538 "Filename",
539 Value::String(path.to_string_lossy().into_owned()),
540 );
541 out.insert(
542 "CompressionMethod",
543 Value::String(self.compression_method.clone()),
544 );
545 out.insert("NumChannels", Value::Num(self.num_channels as f64));
546 out.insert("SampleRate", Value::Num(self.sample_rate));
547 out.insert("TotalSamples", Value::Num(total_samples));
548 out.insert("Duration", Value::Num(duration));
549 out.insert(
550 "BitsPerSample",
551 Value::Num(self.bits_per_sample.map(|v| v as f64).unwrap_or(f64::NAN)),
552 );
553 out.insert("BitRate", Value::Num(self.bit_rate.unwrap_or(f64::NAN)));
554 out.insert("FileSize", Value::Num(file_size));
555 out.insert("Format", Value::String(self.format.to_string()));
556 out
557 }
558}
559
560fn parse_wave(bytes: &[u8]) -> Result<AudioMetadata, String> {
561 let (format, data_bytes) = parse_wave_metadata(bytes)?;
562 let total_samples = if format.block_align > 0 {
563 Some(data_bytes / format.block_align as u64)
564 } else {
565 None
566 };
567 Ok(AudioMetadata {
568 format: "WAV",
569 compression_method: wave_compression_name(format.effective_format_tag()).to_string(),
570 num_channels: format.channels,
571 sample_rate: format.sample_rate as f64,
572 total_samples,
573 bits_per_sample: Some(
574 format
575 .valid_bits_per_sample
576 .unwrap_or(format.bits_per_sample),
577 ),
578 bit_rate: Some(format.byte_rate as f64 * 8.0),
579 })
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583struct AudioreadOptions {
584 range: Option<(usize, usize)>,
585 native: bool,
586}
587
588#[derive(Debug, Clone)]
589struct DecodedAudio {
590 samples: Tensor,
591 sample_rate: f64,
592}
593
594fn parse_audioread_options(args: &[Value]) -> BuiltinResult<AudioreadOptions> {
595 let mut range = None;
596 let mut native = false;
597 match args {
598 [] => {}
599 [single] => {
600 if let Some(text) = scalar_text(single) {
601 native = parse_audioread_datatype(&text)?;
602 } else {
603 range = Some(parse_sample_range(single)?);
604 }
605 }
606 [samples, datatype] => {
607 range = Some(parse_sample_range(samples)?);
608 let text = scalar_text(datatype).ok_or_else(|| {
609 audioread_error_with(
610 &AUDIOREAD_ERROR_ARGUMENT,
611 "audioread: datatype must be \"native\" or \"double\"",
612 )
613 })?;
614 native = parse_audioread_datatype(&text)?;
615 }
616 _ => {
617 return Err(audioread_error_with(
618 &AUDIOREAD_ERROR_ARGUMENT,
619 "audioread: too many input arguments",
620 ));
621 }
622 }
623 Ok(AudioreadOptions { range, native })
624}
625
626fn parse_audioread_datatype(value: &str) -> BuiltinResult<bool> {
627 match value.trim().to_ascii_lowercase().as_str() {
628 "double" => Ok(false),
629 "native" => Ok(true),
630 _ => Err(audioread_error_with(
631 &AUDIOREAD_ERROR_ARGUMENT,
632 "audioread: datatype must be \"native\" or \"double\"",
633 )),
634 }
635}
636
637fn parse_sample_range(value: &Value) -> BuiltinResult<(usize, usize)> {
638 let data: Vec<f64> = match value {
639 Value::Tensor(t) => t.data.clone(),
640 Value::Num(n) => vec![*n],
641 Value::Int(i) => vec![i.to_f64()],
642 _ => {
643 return Err(audioread_error_with(
644 &AUDIOREAD_ERROR_ARGUMENT,
645 "audioread: sample range must be a two-element numeric vector",
646 ));
647 }
648 };
649 if data.len() != 2 {
650 return Err(audioread_error_with(
651 &AUDIOREAD_ERROR_ARGUMENT,
652 "audioread: sample range must be [first last]",
653 ));
654 }
655 let first = parse_positive_integer(data[0], "first sample")?;
656 let last = parse_positive_integer(data[1], "last sample")?;
657 if first > last {
658 return Err(audioread_error_with(
659 &AUDIOREAD_ERROR_ARGUMENT,
660 "audioread: sample range first sample must be less than or equal to last sample",
661 ));
662 }
663 Ok((first, last))
664}
665
666fn parse_positive_integer(value: f64, label: &str) -> BuiltinResult<usize> {
667 if !value.is_finite() || value < 1.0 || value.fract() != 0.0 {
668 return Err(audioread_error_with(
669 &AUDIOREAD_ERROR_ARGUMENT,
670 format!("audioread: {label} must be a positive integer"),
671 ));
672 }
673 if value > usize::MAX as f64 {
674 return Err(audioread_error_with(
675 &AUDIOREAD_ERROR_ARGUMENT,
676 format!("audioread: {label} is too large"),
677 ));
678 }
679 Ok(value as usize)
680}
681
682fn scalar_text(value: &Value) -> Option<String> {
683 match value {
684 Value::String(s) => Some(s.clone()),
685 Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
686 Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
687 _ => None,
688 }
689}
690
691fn decode_audio_samples(bytes: &[u8], options: AudioreadOptions) -> Result<DecodedAudio, String> {
692 if bytes.starts_with(b"RIFF") || bytes.starts_with(b"RF64") {
693 decode_wave_samples(bytes, options)
694 } else if bytes.starts_with(b"fLaC")
695 || bytes.starts_with(b"FORM")
696 || bytes.starts_with(b"OggS")
697 || parse_mp3(bytes, bytes.len() as u64).is_some()
698 {
699 Err("sample decoding is currently implemented for WAV/RF64 PCM and IEEE-float audio; compressed containers are metadata-only".to_string())
700 } else {
701 Err("unsupported audio format".to_string())
702 }
703}
704
705fn decode_wave_samples(bytes: &[u8], options: AudioreadOptions) -> Result<DecodedAudio, String> {
706 let parsed = parse_wave_container(bytes)?;
707 let fmt = parsed.format;
708 let channels = fmt.channels as usize;
709 let block_align = fmt.block_align as usize;
710 let bytes_per_channel = usize::from(fmt.bits_per_sample.div_ceil(8));
711 if channels == 0 || block_align == 0 || bytes_per_channel == 0 {
712 return Err("WAVE fmt chunk has invalid sample layout".to_string());
713 }
714 let minimum_block_align = channels
715 .checked_mul(bytes_per_channel)
716 .ok_or_else(|| "WAVE channel layout overflows platform limits".to_string())?;
717 if block_align < minimum_block_align {
718 return Err("WAVE block alignment is smaller than the channel sample width".to_string());
719 }
720 if parsed.data_len % block_align != 0 {
721 return Err("WAVE data chunk is not aligned to whole sample frames".to_string());
722 }
723 let total_frames = parsed.data_len / block_align;
724 let (start_frame, end_frame_exclusive) = match options.range {
725 Some((first, last)) => {
726 if last > total_frames {
727 return Err("sample range exceeds the available audio frames".to_string());
728 }
729 (first - 1, last)
730 }
731 None => (0, total_frames),
732 };
733 let frame_count = end_frame_exclusive - start_frame;
734 let sample_count = frame_count
735 .checked_mul(channels)
736 .ok_or_else(|| "decoded audio dimensions overflow platform limits".to_string())?;
737 let mut out = Vec::with_capacity(sample_count);
738 let data = &bytes[parsed.data_offset..parsed.data_offset + parsed.data_len];
739 let native_dtype = if options.native {
740 native_wave_dtype(fmt)
741 } else {
742 NumericDType::F64
743 };
744 for channel in 0..channels {
745 for frame in start_frame..end_frame_exclusive {
746 let sample_offset = frame
747 .checked_mul(block_align)
748 .and_then(|base| base.checked_add(channel * bytes_per_channel))
749 .ok_or_else(|| "sample offset overflows platform limits".to_string())?;
750 let sample_bytes = &data[sample_offset..sample_offset + bytes_per_channel];
751 let value = decode_wave_sample(sample_bytes, fmt, options.native)?;
752 out.push(value);
753 }
754 }
755 let samples = Tensor::new_with_dtype(out, vec![frame_count, channels], native_dtype)
756 .map_err(|err| format!("decoded audio tensor shape is invalid: {err}"))?;
757 Ok(DecodedAudio {
758 samples,
759 sample_rate: fmt.sample_rate as f64,
760 })
761}
762
763fn native_wave_dtype(fmt: WaveFormat) -> NumericDType {
764 match (fmt.effective_format_tag(), fmt.effective_bits_per_sample()) {
765 (0x0001, 8) => NumericDType::U8,
766 (0x0003, 32) => NumericDType::F32,
767 _ => NumericDType::F64,
768 }
769}
770
771fn decode_wave_sample(bytes: &[u8], fmt: WaveFormat, native: bool) -> Result<f64, String> {
772 match (fmt.effective_format_tag(), fmt.effective_bits_per_sample()) {
773 (0x0001, 8) => {
774 let raw = bytes[0] as f64;
775 Ok(if native { raw } else { (raw - 128.0) / 128.0 })
776 }
777 (0x0001, 16) => {
778 let raw = i16::from_le_bytes([bytes[0], bytes[1]]);
779 Ok(if native {
780 raw as f64
781 } else {
782 raw as f64 / 32768.0
783 })
784 }
785 (0x0001, 24) => {
786 let raw = sign_extend_24(bytes);
787 Ok(if native {
788 raw as f64
789 } else {
790 raw as f64 / 8_388_608.0
791 })
792 }
793 (0x0001, 32) => {
794 let raw = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
795 Ok(if native {
796 raw as f64
797 } else {
798 raw as f64 / 2_147_483_648.0
799 })
800 }
801 (0x0003, 32) => Ok(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64),
802 (0x0003, 64) => Ok(f64::from_le_bytes([
803 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
804 ])),
805 (tag, bits) => Err(format!(
806 "unsupported WAVE sample encoding tag 0x{tag:04x} with {bits} bits per sample"
807 )),
808 }
809}
810
811fn sign_extend_24(bytes: &[u8]) -> i32 {
812 let mut value = ((bytes[2] as i32) << 16) | ((bytes[1] as i32) << 8) | bytes[0] as i32;
813 if value & 0x0080_0000 != 0 {
814 value |= !0x00ff_ffff;
815 }
816 value
817}
818
819#[derive(Debug, Clone, Copy)]
820struct WaveContainer {
821 format: WaveFormat,
822 data_offset: usize,
823 data_len: usize,
824}
825
826fn parse_wave_metadata(bytes: &[u8]) -> Result<(WaveFormat, u64), String> {
827 if bytes.len() < 12 || &bytes[8..12] != b"WAVE" {
828 return Err("RIFF/RF64 file is not a WAVE container".to_string());
829 }
830 let is_rf64 = bytes.starts_with(b"RF64");
831 let mut pos = 12usize;
832 let mut fmt: Option<WaveFormat> = None;
833 let mut data_bytes: Option<u64> = None;
834 let mut rf64_data_size: Option<u64> = None;
835 while pos + 8 <= bytes.len() {
836 let id = &bytes[pos..pos + 4];
837 let declared_size = read_u32_le(bytes, pos + 4)?;
838 pos += 8;
839 if is_rf64 && id == b"data" && declared_size == u32::MAX {
840 let data_size = rf64_data_size.ok_or_else(|| {
841 "RF64 data chunk uses sentinel size without ds64 metadata".to_string()
842 })?;
843 data_bytes = Some(data_size);
844 break;
845 }
846 let size = declared_size as usize;
847 match id {
848 b"ds64" if is_rf64 => {
849 let end = pos
850 .checked_add(size)
851 .ok_or_else(|| "WAVE chunk size overflows address space".to_string())?;
852 if end > bytes.len() {
853 return Err("WAVE chunk extends past end of file".to_string());
854 }
855 if size < 24 {
856 return Err("RF64 ds64 chunk is too short".to_string());
857 }
858 rf64_data_size = Some(read_u64_le(bytes, pos + 8)?);
859 }
860 b"fmt " => {
861 let end = pos
862 .checked_add(size)
863 .ok_or_else(|| "WAVE chunk size overflows address space".to_string())?;
864 if end > bytes.len() {
865 return Err("WAVE chunk extends past end of file".to_string());
866 }
867 fmt = Some(parse_wave_fmt(&bytes[pos..end])?);
868 }
869 b"data" => {
870 data_bytes = Some(size as u64);
871 break;
872 }
873 _ => {
874 let end = pos
875 .checked_add(size)
876 .ok_or_else(|| "WAVE chunk size overflows address space".to_string())?;
877 if end > bytes.len() {
878 return Err("WAVE chunk extends past end of file".to_string());
879 }
880 }
881 }
882 let end = pos
883 .checked_add(size)
884 .ok_or_else(|| "WAVE chunk size overflows address space".to_string())?;
885 pos = end
886 .checked_add(size % 2)
887 .ok_or_else(|| "WAVE chunk padding overflows address space".to_string())?;
888 }
889
890 let fmt = fmt.ok_or_else(|| "WAVE file is missing a fmt chunk".to_string())?;
891 let data_bytes = data_bytes.ok_or_else(|| "WAVE file is missing a data chunk".to_string())?;
892 Ok((fmt, data_bytes))
893}
894
895fn parse_wave_container(bytes: &[u8]) -> Result<WaveContainer, String> {
896 if bytes.len() < 12 || &bytes[8..12] != b"WAVE" {
897 return Err("RIFF/RF64 file is not a WAVE container".to_string());
898 }
899 let is_rf64 = bytes.starts_with(b"RF64");
900 let mut pos = 12usize;
901 let mut fmt: Option<WaveFormat> = None;
902 let mut data: Option<(usize, usize)> = None;
903 let mut rf64_data_size: Option<u64> = None;
904 while pos.checked_add(8).is_some_and(|end| end <= bytes.len()) {
905 let id_end = pos
906 .checked_add(4)
907 .ok_or_else(|| "WAVE chunk offset overflows platform limits".to_string())?;
908 let size_offset = pos
909 .checked_add(4)
910 .ok_or_else(|| "WAVE chunk offset overflows platform limits".to_string())?;
911 let id = &bytes[pos..id_end];
912 let declared_size = read_u32_le(bytes, size_offset)?;
913 pos = pos
914 .checked_add(8)
915 .ok_or_else(|| "WAVE chunk offset overflows platform limits".to_string())?;
916 let size = if is_rf64 && id == b"data" && declared_size == u32::MAX {
917 let actual = rf64_data_size.ok_or_else(|| {
918 "RF64 data chunk uses sentinel size without ds64 metadata".to_string()
919 })?;
920 usize::try_from(actual)
921 .map_err(|_| "RF64 data chunk is too large for this platform".to_string())?
922 } else {
923 declared_size as usize
924 };
925 let chunk_end = pos
926 .checked_add(size)
927 .ok_or_else(|| "WAVE chunk size overflows platform limits".to_string())?;
928 if chunk_end > bytes.len() {
929 return Err("WAVE chunk extends past end of file".to_string());
930 }
931 match id {
932 b"ds64" if is_rf64 => {
933 if size < 24 {
934 return Err("RF64 ds64 chunk is too short".to_string());
935 }
936 rf64_data_size = Some(read_u64_le(bytes, pos + 8)?);
937 }
938 b"fmt " => fmt = Some(parse_wave_fmt(&bytes[pos..chunk_end])?),
939 b"data" => data = Some((pos, size)),
940 _ => {}
941 }
942 let padded_size = size
943 .checked_add(size % 2)
944 .ok_or_else(|| "WAVE padded chunk size overflows platform limits".to_string())?;
945 pos = pos
946 .checked_add(padded_size)
947 .ok_or_else(|| "WAVE chunk offset overflows platform limits".to_string())?;
948 }
949
950 let fmt = fmt.ok_or_else(|| "WAVE file is missing a fmt chunk".to_string())?;
951 let (data_offset, data_len) =
952 data.ok_or_else(|| "WAVE file is missing a data chunk".to_string())?;
953 Ok(WaveContainer {
954 format: fmt,
955 data_offset,
956 data_len,
957 })
958}
959
960#[derive(Debug, Clone, Copy)]
961struct WaveFormat {
962 format_tag: u16,
963 subformat_tag: Option<u16>,
964 channels: u16,
965 sample_rate: u32,
966 byte_rate: u32,
967 block_align: u16,
968 bits_per_sample: u16,
969 valid_bits_per_sample: Option<u16>,
970}
971
972impl WaveFormat {
973 fn effective_format_tag(self) -> u16 {
974 self.subformat_tag.unwrap_or(self.format_tag)
975 }
976
977 fn effective_bits_per_sample(self) -> u16 {
978 self.valid_bits_per_sample.unwrap_or(self.bits_per_sample)
979 }
980}
981
982fn parse_wave_fmt(chunk: &[u8]) -> Result<WaveFormat, String> {
983 if chunk.len() < 16 {
984 return Err("WAVE fmt chunk is too short".to_string());
985 }
986 let format_tag = u16::from_le_bytes([chunk[0], chunk[1]]);
987 let channels = u16::from_le_bytes([chunk[2], chunk[3]]);
988 let sample_rate = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
989 let byte_rate = u32::from_le_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]);
990 let block_align = u16::from_le_bytes([chunk[12], chunk[13]]);
991 let bits_per_sample = u16::from_le_bytes([chunk[14], chunk[15]]);
992 if channels == 0 || sample_rate == 0 || block_align == 0 {
993 return Err(
994 "WAVE fmt chunk has invalid channel count, sample rate, or block alignment".to_string(),
995 );
996 }
997
998 let mut subformat_tag = None;
999 let mut valid_bits_per_sample = None;
1000 if format_tag == 0xFFFE {
1001 if chunk.len() < 40 {
1002 return Err("WAVE extensible fmt chunk is too short".to_string());
1003 }
1004 let cb_size = u16::from_le_bytes([chunk[16], chunk[17]]);
1005 if cb_size < 22 {
1006 return Err("WAVE extensible fmt chunk has invalid extension size".to_string());
1007 }
1008 let valid = u16::from_le_bytes([chunk[18], chunk[19]]);
1009 valid_bits_per_sample = if valid == 0 { None } else { Some(valid) };
1010 subformat_tag = wave_extensible_subformat_tag(&chunk[24..40]);
1011 }
1012 Ok(WaveFormat {
1013 format_tag,
1014 subformat_tag,
1015 channels,
1016 sample_rate,
1017 byte_rate,
1018 block_align,
1019 bits_per_sample,
1020 valid_bits_per_sample,
1021 })
1022}
1023
1024fn wave_compression_name(tag: u16) -> &'static str {
1025 match tag {
1026 0x0001 => "PCM",
1027 0x0003 => "IEEE Float",
1028 0x0006 => "A-law",
1029 0x0007 => "mu-law",
1030 0xFFFE => "Extensible",
1031 _ => "Unknown",
1032 }
1033}
1034
1035fn wave_extensible_subformat_tag(guid: &[u8]) -> Option<u16> {
1036 const BASE_TAIL: [u8; 12] = [
1037 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
1038 ];
1039 if guid.len() == 16 && guid[2..4] == [0x00, 0x00] && guid[4..16] == BASE_TAIL {
1040 Some(u16::from_le_bytes([guid[0], guid[1]]))
1041 } else {
1042 None
1043 }
1044}
1045
1046fn parse_flac(bytes: &[u8]) -> Result<AudioMetadata, String> {
1047 let mut pos = 4usize;
1048 while pos + 4 <= bytes.len() {
1049 let header = bytes[pos];
1050 let block_type = header & 0x7F;
1051 let last = header & 0x80 != 0;
1052 let len = ((bytes[pos + 1] as usize) << 16)
1053 | ((bytes[pos + 2] as usize) << 8)
1054 | bytes[pos + 3] as usize;
1055 pos += 4;
1056 if pos + len > bytes.len() {
1057 return Err("FLAC metadata block extends past end of file".to_string());
1058 }
1059 if block_type == 0 {
1060 if len < 34 {
1061 return Err("FLAC STREAMINFO block is too short".to_string());
1062 }
1063 let stream = &bytes[pos..pos + len];
1064 let packed = u64::from_be_bytes([
1065 stream[10], stream[11], stream[12], stream[13], stream[14], stream[15], stream[16],
1066 stream[17],
1067 ]);
1068 let sample_rate = ((packed >> 44) & 0xFFFFF) as u32;
1069 let channels = (((packed >> 41) & 0x7) + 1) as u16;
1070 let bits_per_sample = (((packed >> 36) & 0x1F) + 1) as u16;
1071 let total_samples = packed & 0x000F_FFFF_FFFF;
1072 if sample_rate == 0 {
1073 return Err("FLAC STREAMINFO has zero sample rate".to_string());
1074 }
1075 return Ok(AudioMetadata {
1076 format: "FLAC",
1077 compression_method: "FLAC".to_string(),
1078 num_channels: channels,
1079 sample_rate: sample_rate as f64,
1080 total_samples: if total_samples == 0 {
1081 None
1082 } else {
1083 Some(total_samples)
1084 },
1085 bits_per_sample: Some(bits_per_sample),
1086 bit_rate: None,
1087 });
1088 }
1089 pos += len;
1090 if last {
1091 break;
1092 }
1093 }
1094 Err("FLAC file is missing STREAMINFO metadata".to_string())
1095}
1096
1097fn parse_aiff(bytes: &[u8]) -> Result<AudioMetadata, String> {
1098 if bytes.len() < 12 {
1099 return Err("AIFF file is too short".to_string());
1100 }
1101 let form = &bytes[8..12];
1102 if form != b"AIFF" && form != b"AIFC" {
1103 return Err("FORM container is not AIFF or AIFC".to_string());
1104 }
1105 let mut pos = 12usize;
1106 while pos + 8 <= bytes.len() {
1107 let id = &bytes[pos..pos + 4];
1108 let size = read_u32_be(bytes, pos + 4)? as usize;
1109 pos += 8;
1110 let end = pos
1111 .checked_add(size)
1112 .ok_or_else(|| "AIFF chunk size overflows address space".to_string())?;
1113 if end > bytes.len() {
1114 return Err("AIFF chunk extends past end of file".to_string());
1115 }
1116 if id == b"COMM" {
1117 if size < 18 {
1118 return Err("AIFF COMM chunk is too short".to_string());
1119 }
1120 let channels = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
1121 let total_samples = read_u32_be(bytes, pos + 2)? as u64;
1122 let bits_per_sample = u16::from_be_bytes([bytes[pos + 6], bytes[pos + 7]]);
1123 let sample_rate = read_ieee_extended_80(&bytes[pos + 8..pos + 18])?;
1124 let compression_method = if form == b"AIFC" && size >= 22 {
1125 match &bytes[pos + 18..pos + 22] {
1126 b"NONE" => "PCM",
1127 b"fl32" | b"FL32" => "IEEE Float",
1128 b"fl64" | b"FL64" => "IEEE Float",
1129 b"ulaw" | b"ULAW" => "mu-law",
1130 b"alaw" | b"ALAW" => "A-law",
1131 code => std::str::from_utf8(code).unwrap_or("Unknown"),
1132 }
1133 .to_string()
1134 } else {
1135 "PCM".to_string()
1136 };
1137 return Ok(AudioMetadata {
1138 format: if form == b"AIFC" { "AIFC" } else { "AIFF" },
1139 compression_method,
1140 num_channels: channels,
1141 sample_rate,
1142 total_samples: Some(total_samples),
1143 bits_per_sample: Some(bits_per_sample),
1144 bit_rate: Some(sample_rate * channels as f64 * bits_per_sample as f64),
1145 });
1146 }
1147 pos = end
1148 .checked_add(size % 2)
1149 .ok_or_else(|| "AIFF chunk padding overflows address space".to_string())?;
1150 }
1151 Err("AIFF file is missing COMM metadata".to_string())
1152}
1153
1154fn parse_ogg_vorbis(scan: &AudioInfoScan) -> Result<AudioMetadata, String> {
1155 let prefix = scan.prefix.as_slice();
1156 let first = parse_ogg_page(prefix, 0)?;
1157 if first.body.len() < 30 || &first.body[1..7] != b"vorbis" || first.body[0] != 1 {
1158 return Err("Ogg container is not Vorbis audio".to_string());
1159 }
1160 let channels = first.body[11] as u16;
1161 let sample_rate = u32::from_le_bytes([
1162 first.body[12],
1163 first.body[13],
1164 first.body[14],
1165 first.body[15],
1166 ]);
1167 if channels == 0 || sample_rate == 0 {
1168 return Err(
1169 "Vorbis identification header has invalid channel count or sample rate".to_string(),
1170 );
1171 }
1172 let nominal_bitrate = i32::from_le_bytes([
1173 first.body[20],
1174 first.body[21],
1175 first.body[22],
1176 first.body[23],
1177 ]);
1178 let total_samples = match scan.tail.as_deref() {
1179 Some(bytes) => find_last_ogg_granule(bytes),
1180 None if scan.file_size <= prefix.len() as u64 => find_last_ogg_granule(prefix),
1181 None => None,
1182 };
1183 Ok(AudioMetadata {
1184 format: "OGG",
1185 compression_method: "Vorbis".to_string(),
1186 num_channels: channels,
1187 sample_rate: sample_rate as f64,
1188 total_samples,
1189 bits_per_sample: None,
1190 bit_rate: if nominal_bitrate > 0 {
1191 Some(nominal_bitrate as f64)
1192 } else {
1193 None
1194 },
1195 })
1196}
1197
1198struct OggPage {
1199 granule_position: u64,
1200 body: Vec<u8>,
1201 next_pos: usize,
1202}
1203
1204fn parse_ogg_page(bytes: &[u8], pos: usize) -> Result<OggPage, String> {
1205 if pos + 27 > bytes.len() || &bytes[pos..pos + 4] != b"OggS" {
1206 return Err("invalid Ogg page header".to_string());
1207 }
1208 let segments = bytes[pos + 26] as usize;
1209 if pos + 27 + segments > bytes.len() {
1210 return Err("Ogg segment table extends past end of file".to_string());
1211 }
1212 let mut body_len = 0usize;
1213 for len in &bytes[pos + 27..pos + 27 + segments] {
1214 body_len += *len as usize;
1215 }
1216 let body_start = pos + 27 + segments;
1217 if body_start + body_len > bytes.len() {
1218 return Err("Ogg page body extends past end of file".to_string());
1219 }
1220 let granule_position = u64::from_le_bytes([
1221 bytes[pos + 6],
1222 bytes[pos + 7],
1223 bytes[pos + 8],
1224 bytes[pos + 9],
1225 bytes[pos + 10],
1226 bytes[pos + 11],
1227 bytes[pos + 12],
1228 bytes[pos + 13],
1229 ]);
1230 Ok(OggPage {
1231 granule_position,
1232 body: bytes[body_start..body_start + body_len].to_vec(),
1233 next_pos: body_start + body_len,
1234 })
1235}
1236
1237fn find_last_ogg_granule(bytes: &[u8]) -> Option<u64> {
1238 let mut pos = 0usize;
1239 let mut last = None;
1240 while pos + 27 <= bytes.len() {
1241 let Some(offset) = find_signature(&bytes[pos..], b"OggS") else {
1242 break;
1243 };
1244 pos += offset;
1245 match parse_ogg_page(bytes, pos) {
1246 Ok(page) => {
1247 if page.granule_position != u64::MAX {
1248 last = Some(page.granule_position);
1249 }
1250 pos = page.next_pos;
1251 }
1252 Err(_) => break,
1253 }
1254 }
1255 last
1256}
1257
1258fn parse_mp3(bytes: &[u8], file_size: u64) -> Option<AudioMetadata> {
1259 let mut pos = skip_id3v2(bytes);
1260 while pos + 4 <= bytes.len() {
1261 if bytes[pos] == 0xFF && (bytes[pos + 1] & 0xE0) == 0xE0 {
1262 if let Some(frame) = parse_mpeg_audio_header(&bytes[pos..pos + 4]) {
1263 let payload_bytes = file_size.saturating_sub(pos as u64) as f64;
1264 let duration = if frame.bit_rate > 0 {
1265 Some(payload_bytes * 8.0 / frame.bit_rate as f64)
1266 } else {
1267 None
1268 };
1269 let total_samples =
1270 duration.map(|seconds| (seconds * frame.sample_rate as f64).round() as u64);
1271 return Some(AudioMetadata {
1272 format: "MP3",
1273 compression_method: frame.layer.to_string(),
1274 num_channels: frame.channels,
1275 sample_rate: frame.sample_rate as f64,
1276 total_samples,
1277 bits_per_sample: None,
1278 bit_rate: Some(frame.bit_rate as f64),
1279 });
1280 }
1281 }
1282 pos += 1;
1283 }
1284 None
1285}
1286
1287fn skip_id3v2(bytes: &[u8]) -> usize {
1288 if bytes.len() >= 10 && &bytes[0..3] == b"ID3" {
1289 let size = ((bytes[6] as usize & 0x7F) << 21)
1290 | ((bytes[7] as usize & 0x7F) << 14)
1291 | ((bytes[8] as usize & 0x7F) << 7)
1292 | (bytes[9] as usize & 0x7F);
1293 10 + size
1294 } else {
1295 0
1296 }
1297}
1298
1299struct MpegFrame {
1300 layer: &'static str,
1301 sample_rate: u32,
1302 bit_rate: u32,
1303 channels: u16,
1304}
1305
1306fn parse_mpeg_audio_header(header: &[u8]) -> Option<MpegFrame> {
1307 let version_id = (header[1] >> 3) & 0x03;
1308 let layer_id = (header[1] >> 1) & 0x03;
1309 let bitrate_index = (header[2] >> 4) & 0x0F;
1310 let sample_rate_index = (header[2] >> 2) & 0x03;
1311 let channel_mode = (header[3] >> 6) & 0x03;
1312 if version_id == 1
1313 || layer_id == 0
1314 || bitrate_index == 0
1315 || bitrate_index == 0x0F
1316 || sample_rate_index == 0x03
1317 {
1318 return None;
1319 }
1320 let version = match version_id {
1321 3 => MpegVersion::V1,
1322 2 => MpegVersion::V2,
1323 0 => MpegVersion::V25,
1324 _ => return None,
1325 };
1326 let layer = match layer_id {
1327 3 => MpegLayer::LayerI,
1328 2 => MpegLayer::LayerII,
1329 1 => MpegLayer::LayerIII,
1330 _ => return None,
1331 };
1332 let sample_rate = mpeg_sample_rate(version, sample_rate_index)?;
1333 let bit_rate = mpeg_bit_rate(version, layer, bitrate_index)? * 1000;
1334 Some(MpegFrame {
1335 layer: match layer {
1336 MpegLayer::LayerI => "MPEG Layer I",
1337 MpegLayer::LayerII => "MPEG Layer II",
1338 MpegLayer::LayerIII => "MPEG Layer III",
1339 },
1340 sample_rate,
1341 bit_rate,
1342 channels: if channel_mode == 3 { 1 } else { 2 },
1343 })
1344}
1345
1346#[derive(Debug, Clone, Copy)]
1347enum MpegVersion {
1348 V1,
1349 V2,
1350 V25,
1351}
1352
1353#[derive(Debug, Clone, Copy)]
1354enum MpegLayer {
1355 LayerI,
1356 LayerII,
1357 LayerIII,
1358}
1359
1360fn mpeg_sample_rate(version: MpegVersion, index: u8) -> Option<u32> {
1361 let base = match index {
1362 0 => 44_100,
1363 1 => 48_000,
1364 2 => 32_000,
1365 _ => return None,
1366 };
1367 Some(match version {
1368 MpegVersion::V1 => base,
1369 MpegVersion::V2 => base / 2,
1370 MpegVersion::V25 => base / 4,
1371 })
1372}
1373
1374fn mpeg_bit_rate(version: MpegVersion, layer: MpegLayer, index: u8) -> Option<u32> {
1375 const V1_L1: [u32; 16] = [
1376 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0,
1377 ];
1378 const V1_L2: [u32; 16] = [
1379 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0,
1380 ];
1381 const V1_L3: [u32; 16] = [
1382 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
1383 ];
1384 const V2_L1: [u32; 16] = [
1385 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0,
1386 ];
1387 const V2_L23: [u32; 16] = [
1388 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0,
1389 ];
1390 let table = match (version, layer) {
1391 (MpegVersion::V1, MpegLayer::LayerI) => V1_L1,
1392 (MpegVersion::V1, MpegLayer::LayerII) => V1_L2,
1393 (MpegVersion::V1, MpegLayer::LayerIII) => V1_L3,
1394 (_, MpegLayer::LayerI) => V2_L1,
1395 (_, MpegLayer::LayerII | MpegLayer::LayerIII) => V2_L23,
1396 };
1397 table.get(index as usize).copied().filter(|rate| *rate > 0)
1398}
1399
1400fn read_ieee_extended_80(bytes: &[u8]) -> Result<f64, String> {
1401 if bytes.len() != 10 {
1402 return Err("AIFF sample rate field must be 80 bits".to_string());
1403 }
1404 let sign = if bytes[0] & 0x80 != 0 { -1.0 } else { 1.0 };
1405 let exponent = (((bytes[0] & 0x7F) as u16) << 8) | bytes[1] as u16;
1406 let mantissa = u64::from_be_bytes([
1407 bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9],
1408 ]);
1409 if exponent == 0 && mantissa == 0 {
1410 return Ok(0.0);
1411 }
1412 let fraction = mantissa as f64 / (1u64 << 63) as f64;
1413 Ok(sign * fraction * 2f64.powi(exponent as i32 - 16383))
1414}
1415
1416fn resolve_audioinfo_path(value: &Value) -> BuiltinResult<PathBuf> {
1417 match value {
1418 Value::String(s) => normalize_audioinfo_path(s),
1419 Value::CharArray(ca) if ca.rows == 1 => {
1420 let text: String = ca.data.iter().collect();
1421 normalize_audioinfo_path(&text)
1422 }
1423 Value::StringArray(sa) if sa.data.len() == 1 => normalize_audioinfo_path(&sa.data[0]),
1424 _ => Err(audioinfo_error(&AUDIOINFO_ERROR_ARGUMENT)),
1425 }
1426}
1427
1428fn normalize_audioinfo_path(raw: &str) -> BuiltinResult<PathBuf> {
1429 if raw.trim().is_empty() {
1430 return Err(audioinfo_error_with(
1431 &AUDIOINFO_ERROR_ARGUMENT,
1432 "audioinfo: filename must not be empty",
1433 ));
1434 }
1435 let expanded = expand_user_path(raw, AUDIOINFO_BUILTIN_NAME)
1436 .map_err(|msg| audioinfo_error_with(&AUDIOINFO_ERROR_ARGUMENT, msg))?;
1437 Ok(Path::new(&expanded).to_path_buf())
1438}
1439
1440fn resolve_audioread_path(value: &Value) -> BuiltinResult<PathBuf> {
1441 match value {
1442 Value::String(s) => normalize_audioread_path(s),
1443 Value::CharArray(ca) if ca.rows == 1 => {
1444 let text: String = ca.data.iter().collect();
1445 normalize_audioread_path(&text)
1446 }
1447 Value::StringArray(sa) if sa.data.len() == 1 => normalize_audioread_path(&sa.data[0]),
1448 _ => Err(audioread_error(&AUDIOREAD_ERROR_ARGUMENT)),
1449 }
1450}
1451
1452fn normalize_audioread_path(raw: &str) -> BuiltinResult<PathBuf> {
1453 if raw.trim().is_empty() {
1454 return Err(audioread_error_with(
1455 &AUDIOREAD_ERROR_ARGUMENT,
1456 "audioread: filename must not be empty",
1457 ));
1458 }
1459 let expanded = expand_user_path(raw, AUDIOREAD_BUILTIN_NAME)
1460 .map_err(|msg| audioread_error_with(&AUDIOREAD_ERROR_ARGUMENT, msg))?;
1461 Ok(Path::new(&expanded).to_path_buf())
1462}
1463
1464fn read_u32_le(bytes: &[u8], pos: usize) -> Result<u32, String> {
1465 if pos + 4 > bytes.len() {
1466 return Err("unexpected end of file".to_string());
1467 }
1468 Ok(u32::from_le_bytes([
1469 bytes[pos],
1470 bytes[pos + 1],
1471 bytes[pos + 2],
1472 bytes[pos + 3],
1473 ]))
1474}
1475
1476fn read_u32_be(bytes: &[u8], pos: usize) -> Result<u32, String> {
1477 if pos + 4 > bytes.len() {
1478 return Err("unexpected end of file".to_string());
1479 }
1480 Ok(u32::from_be_bytes([
1481 bytes[pos],
1482 bytes[pos + 1],
1483 bytes[pos + 2],
1484 bytes[pos + 3],
1485 ]))
1486}
1487
1488fn read_u64_le(bytes: &[u8], pos: usize) -> Result<u64, String> {
1489 if pos + 8 > bytes.len() {
1490 return Err("unexpected end of file".to_string());
1491 }
1492 Ok(u64::from_le_bytes([
1493 bytes[pos],
1494 bytes[pos + 1],
1495 bytes[pos + 2],
1496 bytes[pos + 3],
1497 bytes[pos + 4],
1498 bytes[pos + 5],
1499 bytes[pos + 6],
1500 bytes[pos + 7],
1501 ]))
1502}
1503
1504fn find_signature(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1505 haystack
1506 .windows(needle.len())
1507 .position(|window| window == needle)
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use super::*;
1513 use futures::executor::block_on;
1514 use runmat_time::unix_timestamp_ms;
1515 use std::fs;
1516 use std::sync::atomic::{AtomicU64, Ordering};
1517 use std::sync::Arc;
1518
1519 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
1520
1521 fn temp_path(ext: &str) -> PathBuf {
1522 let millis = unix_timestamp_ms();
1523 let unique = NEXT_ID.fetch_add(1, Ordering::Relaxed);
1524 let mut path = std::env::temp_dir();
1525 path.push(format!(
1526 "runmat_audioinfo_{}_{}_{}.{}",
1527 std::process::id(),
1528 millis,
1529 unique,
1530 ext
1531 ));
1532 path
1533 }
1534
1535 fn field<'a>(value: &'a Value, name: &str) -> &'a Value {
1536 let Value::Struct(st) = value else {
1537 panic!("expected struct");
1538 };
1539 st.fields
1540 .get(name)
1541 .unwrap_or_else(|| panic!("missing {name}"))
1542 }
1543
1544 fn tensor(value: Value) -> Tensor {
1545 match value {
1546 Value::Tensor(t) => t,
1547 other => panic!("expected tensor, got {other:?}"),
1548 }
1549 }
1550
1551 fn output_list(value: Value) -> Vec<Value> {
1552 match value {
1553 Value::OutputList(values) => values,
1554 other => panic!("expected output list, got {other:?}"),
1555 }
1556 }
1557
1558 fn wav_fixture(sample_rate: u32, channels: u16, bits: u16, frames: u32) -> Vec<u8> {
1559 let block_align = channels * (bits / 8);
1560 let byte_rate = sample_rate * block_align as u32;
1561 let data_size = frames * block_align as u32;
1562 let riff_size = 36 + data_size;
1563 let mut bytes = Vec::new();
1564 bytes.extend_from_slice(b"RIFF");
1565 bytes.extend_from_slice(&riff_size.to_le_bytes());
1566 bytes.extend_from_slice(b"WAVE");
1567 bytes.extend_from_slice(b"fmt ");
1568 bytes.extend_from_slice(&16u32.to_le_bytes());
1569 bytes.extend_from_slice(&1u16.to_le_bytes());
1570 bytes.extend_from_slice(&channels.to_le_bytes());
1571 bytes.extend_from_slice(&sample_rate.to_le_bytes());
1572 bytes.extend_from_slice(&byte_rate.to_le_bytes());
1573 bytes.extend_from_slice(&block_align.to_le_bytes());
1574 bytes.extend_from_slice(&bits.to_le_bytes());
1575 bytes.extend_from_slice(b"data");
1576 bytes.extend_from_slice(&data_size.to_le_bytes());
1577 bytes.resize(bytes.len() + data_size as usize, 0);
1578 bytes
1579 }
1580
1581 fn wav_with_payload(
1582 sample_rate: u32,
1583 channels: u16,
1584 bits: u16,
1585 format_tag: u16,
1586 payload: &[u8],
1587 ) -> Vec<u8> {
1588 let block_align = channels * (bits / 8);
1589 let byte_rate = sample_rate * block_align as u32;
1590 let riff_size = 36 + payload.len() as u32;
1591 let mut bytes = Vec::new();
1592 bytes.extend_from_slice(b"RIFF");
1593 bytes.extend_from_slice(&riff_size.to_le_bytes());
1594 bytes.extend_from_slice(b"WAVE");
1595 bytes.extend_from_slice(b"fmt ");
1596 bytes.extend_from_slice(&16u32.to_le_bytes());
1597 bytes.extend_from_slice(&format_tag.to_le_bytes());
1598 bytes.extend_from_slice(&channels.to_le_bytes());
1599 bytes.extend_from_slice(&sample_rate.to_le_bytes());
1600 bytes.extend_from_slice(&byte_rate.to_le_bytes());
1601 bytes.extend_from_slice(&block_align.to_le_bytes());
1602 bytes.extend_from_slice(&bits.to_le_bytes());
1603 bytes.extend_from_slice(b"data");
1604 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1605 bytes.extend_from_slice(payload);
1606 if !payload.len().is_multiple_of(2) {
1607 bytes.push(0);
1608 }
1609 bytes
1610 }
1611
1612 fn pcm16_wav(sample_rate: u32, channels: u16, samples_interleaved: &[i16]) -> Vec<u8> {
1613 let mut payload = Vec::new();
1614 for sample in samples_interleaved {
1615 payload.extend_from_slice(&sample.to_le_bytes());
1616 }
1617 wav_with_payload(sample_rate, channels, 16, 1, &payload)
1618 }
1619
1620 fn pcm8_wav(sample_rate: u32, channels: u16, samples_interleaved: &[u8]) -> Vec<u8> {
1621 wav_with_payload(sample_rate, channels, 8, 1, samples_interleaved)
1622 }
1623
1624 fn float32_wav(sample_rate: u32, channels: u16, samples_interleaved: &[f32]) -> Vec<u8> {
1625 let mut payload = Vec::new();
1626 for sample in samples_interleaved {
1627 payload.extend_from_slice(&sample.to_le_bytes());
1628 }
1629 wav_with_payload(sample_rate, channels, 32, 3, &payload)
1630 }
1631
1632 fn rf64_pcm16_wav(sample_rate: u32, channels: u16, samples_interleaved: &[i16]) -> Vec<u8> {
1633 let mut payload = Vec::new();
1634 for sample in samples_interleaved {
1635 payload.extend_from_slice(&sample.to_le_bytes());
1636 }
1637 let bits = 16u16;
1638 let block_align = channels * (bits / 8);
1639 let byte_rate = sample_rate * block_align as u32;
1640 let data_size = payload.len() as u64;
1641 let riff_size = 36u64 + data_size;
1642 let sample_count = data_size / block_align as u64;
1643 let mut bytes = Vec::new();
1644 bytes.extend_from_slice(b"RF64");
1645 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
1646 bytes.extend_from_slice(b"WAVE");
1647 bytes.extend_from_slice(b"ds64");
1648 bytes.extend_from_slice(&28u32.to_le_bytes());
1649 bytes.extend_from_slice(&riff_size.to_le_bytes());
1650 bytes.extend_from_slice(&data_size.to_le_bytes());
1651 bytes.extend_from_slice(&sample_count.to_le_bytes());
1652 bytes.extend_from_slice(&0u32.to_le_bytes());
1653 bytes.extend_from_slice(b"fmt ");
1654 bytes.extend_from_slice(&16u32.to_le_bytes());
1655 bytes.extend_from_slice(&1u16.to_le_bytes());
1656 bytes.extend_from_slice(&channels.to_le_bytes());
1657 bytes.extend_from_slice(&sample_rate.to_le_bytes());
1658 bytes.extend_from_slice(&byte_rate.to_le_bytes());
1659 bytes.extend_from_slice(&block_align.to_le_bytes());
1660 bytes.extend_from_slice(&bits.to_le_bytes());
1661 bytes.extend_from_slice(b"data");
1662 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
1663 bytes.extend_from_slice(&payload);
1664 bytes
1665 }
1666
1667 fn flac_fixture(sample_rate: u32, channels: u16, bits: u16, total_samples: u64) -> Vec<u8> {
1668 let mut bytes = Vec::new();
1669 bytes.extend_from_slice(b"fLaC");
1670 bytes.push(0x80);
1671 bytes.extend_from_slice(&[0x00, 0x00, 0x22]);
1672 let mut streaminfo = vec![0u8; 34];
1673 streaminfo[0..2].copy_from_slice(&4096u16.to_be_bytes());
1674 streaminfo[2..4].copy_from_slice(&4096u16.to_be_bytes());
1675 let packed = ((sample_rate as u64) << 44)
1676 | (((channels as u64) - 1) << 41)
1677 | (((bits as u64) - 1) << 36)
1678 | (total_samples & 0x000F_FFFF_FFFF);
1679 streaminfo[10..18].copy_from_slice(&packed.to_be_bytes());
1680 bytes.extend_from_slice(&streaminfo);
1681 bytes
1682 }
1683
1684 fn rf64_fixture(sample_rate: u32, channels: u16, bits: u16, frames: u64) -> Vec<u8> {
1685 let block_align = channels * (bits / 8);
1686 let byte_rate = sample_rate * block_align as u32;
1687 let data_size = frames * block_align as u64;
1688 let mut bytes = Vec::new();
1689 bytes.extend_from_slice(b"RF64");
1690 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
1691 bytes.extend_from_slice(b"WAVE");
1692 bytes.extend_from_slice(b"ds64");
1693 bytes.extend_from_slice(&24u32.to_le_bytes());
1694 bytes.extend_from_slice(&(36u64 + data_size).to_le_bytes());
1695 bytes.extend_from_slice(&data_size.to_le_bytes());
1696 bytes.extend_from_slice(&frames.to_le_bytes());
1697 bytes.extend_from_slice(b"fmt ");
1698 bytes.extend_from_slice(&16u32.to_le_bytes());
1699 bytes.extend_from_slice(&1u16.to_le_bytes());
1700 bytes.extend_from_slice(&channels.to_le_bytes());
1701 bytes.extend_from_slice(&sample_rate.to_le_bytes());
1702 bytes.extend_from_slice(&byte_rate.to_le_bytes());
1703 bytes.extend_from_slice(&block_align.to_le_bytes());
1704 bytes.extend_from_slice(&bits.to_le_bytes());
1705 bytes.extend_from_slice(b"data");
1706 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
1707 bytes
1708 }
1709
1710 fn ogg_vorbis_identification_fixture(granule: u64) -> Vec<u8> {
1711 let mut body = vec![0u8; 30];
1712 body[0] = 1;
1713 body[1..7].copy_from_slice(b"vorbis");
1714 body[11] = 2;
1715 body[12..16].copy_from_slice(&44_100u32.to_le_bytes());
1716 body[20..24].copy_from_slice(&128_000i32.to_le_bytes());
1717
1718 let mut bytes = Vec::new();
1719 bytes.extend_from_slice(b"OggS");
1720 bytes.push(0);
1721 bytes.push(0);
1722 bytes.extend_from_slice(&granule.to_le_bytes());
1723 bytes.extend_from_slice(&1u32.to_le_bytes());
1724 bytes.extend_from_slice(&0u32.to_le_bytes());
1725 bytes.extend_from_slice(&0u32.to_le_bytes());
1726 bytes.push(1);
1727 bytes.push(body.len() as u8);
1728 bytes.extend_from_slice(&body);
1729 bytes
1730 }
1731
1732 fn audio_scan(prefix: Vec<u8>) -> AudioInfoScan {
1733 AudioInfoScan {
1734 file_size: prefix.len() as u64,
1735 prefix,
1736 tail: None,
1737 }
1738 }
1739
1740 fn audio_scan_with_file_size(prefix: Vec<u8>, file_size: u64) -> AudioInfoScan {
1741 AudioInfoScan {
1742 prefix,
1743 tail: None,
1744 file_size,
1745 }
1746 }
1747
1748 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1749 #[test]
1750 fn audioinfo_descriptor_covers_core_form() {
1751 assert_eq!(
1752 AUDIOINFO_DESCRIPTOR.signatures[0].label,
1753 "info = audioinfo(filename)"
1754 );
1755 }
1756
1757 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1758 #[test]
1759 fn audioread_descriptor_covers_core_forms() {
1760 let labels: Vec<&str> = AUDIOREAD_DESCRIPTOR
1761 .signatures
1762 .iter()
1763 .map(|sig| sig.label)
1764 .collect();
1765 assert_eq!(
1766 labels,
1767 vec![
1768 "y = audioread(filename)",
1769 "[y, Fs] = audioread(filename)",
1770 "y = audioread(filename, samples)",
1771 "[y, Fs] = audioread(filename, samples, datatype)",
1772 ]
1773 );
1774 }
1775
1776 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1777 #[test]
1778 fn audioinfo_reads_wav_metadata() {
1779 let _lock = runmat_filesystem::provider_override_lock();
1780 let path = temp_path("wav");
1781 fs::write(&path, wav_fixture(44_100, 2, 16, 4)).expect("write fixture");
1782
1783 let info = block_on(audioinfo_builtin(Value::from(
1784 path.to_string_lossy().into_owned(),
1785 )))
1786 .expect("audioinfo");
1787
1788 assert_eq!(field(&info, "Format"), &Value::String("WAV".to_string()));
1789 assert_eq!(
1790 field(&info, "CompressionMethod"),
1791 &Value::String("PCM".to_string())
1792 );
1793 assert_eq!(field(&info, "NumChannels"), &Value::Num(2.0));
1794 assert_eq!(field(&info, "SampleRate"), &Value::Num(44_100.0));
1795 assert_eq!(field(&info, "TotalSamples"), &Value::Num(4.0));
1796 assert_eq!(field(&info, "BitsPerSample"), &Value::Num(16.0));
1797 let _ = fs::remove_file(path);
1798 }
1799
1800 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1801 #[test]
1802 fn audioinfo_reads_rf64_data_size_from_ds64() {
1803 let metadata =
1804 AudioMetadata::parse(&audio_scan(rf64_fixture(48_000, 2, 16, 5))).expect("rf64");
1805 assert_eq!(metadata.format, "WAV");
1806 assert_eq!(metadata.total_samples, Some(5));
1807 assert_eq!(metadata.num_channels, 2);
1808 }
1809
1810 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1811 #[test]
1812 fn audioinfo_keeps_rf64_sample_count_in_u64_space() {
1813 let block_align = 2u64 * (16u64 / 8);
1814 let frames = (u32::MAX as u64 / block_align) + 10;
1815 let metadata =
1816 AudioMetadata::parse(&audio_scan(rf64_fixture(48_000, 2, 16, frames))).expect("rf64");
1817 assert_eq!(metadata.total_samples, Some(frames));
1818 }
1819
1820 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1821 #[test]
1822 fn audioread_reads_mono_pcm16_as_normalized_double() {
1823 let _lock = runmat_filesystem::provider_override_lock();
1824 let path = temp_path("wav");
1825 fs::write(&path, pcm16_wav(8_000, 1, &[-32768, 0, 32767])).expect("write fixture");
1826
1827 let y = tensor(
1828 block_on(audioread_builtin(
1829 Value::from(path.to_string_lossy().into_owned()),
1830 Vec::new(),
1831 ))
1832 .expect("audioread"),
1833 );
1834
1835 assert_eq!(y.shape, vec![3, 1]);
1836 assert_eq!(y.dtype, NumericDType::F64);
1837 assert_eq!(y.data[0], -1.0);
1838 assert_eq!(y.data[1], 0.0);
1839 assert!((y.data[2] - (32767.0 / 32768.0)).abs() < 1e-12);
1840 let _ = fs::remove_file(path);
1841 }
1842
1843 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1844 #[test]
1845 fn audioread_stereo_output_is_column_major_by_channel() {
1846 let _lock = runmat_filesystem::provider_override_lock();
1847 let path = temp_path("wav");
1848 fs::write(
1849 &path,
1850 pcm16_wav(
1851 44_100,
1852 2,
1853 &[
1854 32767, -32768, 0, 16384, ],
1857 ),
1858 )
1859 .expect("write fixture");
1860
1861 let y = tensor(
1862 block_on(audioread_builtin(
1863 Value::from(path.to_string_lossy().into_owned()),
1864 Vec::new(),
1865 ))
1866 .expect("audioread"),
1867 );
1868
1869 assert_eq!(y.shape, vec![2, 2]);
1870 assert!((y.data[0] - (32767.0 / 32768.0)).abs() < 1e-12);
1871 assert_eq!(y.data[1], 0.0);
1872 assert_eq!(y.data[2], -1.0);
1873 assert_eq!(y.data[3], 0.5);
1874 let _ = fs::remove_file(path);
1875 }
1876
1877 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1878 #[test]
1879 fn audioread_sample_range_is_one_based_inclusive_and_returns_fs() {
1880 let _lock = runmat_filesystem::provider_override_lock();
1881 let path = temp_path("wav");
1882 fs::write(&path, pcm16_wav(22_050, 1, &[-32768, -16384, 0, 16384])).expect("write fixture");
1883 let _guard = crate::output_count::push_output_count(Some(2));
1884
1885 let outputs = output_list(
1886 block_on(audioread_builtin(
1887 Value::from(path.to_string_lossy().into_owned()),
1888 vec![Value::Tensor(
1889 Tensor::new(vec![2.0, 3.0], vec![1, 2]).expect("range"),
1890 )],
1891 ))
1892 .expect("audioread"),
1893 );
1894
1895 let y = tensor(outputs[0].clone());
1896 assert_eq!(outputs[1], Value::Num(22_050.0));
1897 assert_eq!(y.shape, vec![2, 1]);
1898 assert_eq!(y.data, vec![-0.5, 0.0]);
1899 let _ = fs::remove_file(path);
1900 }
1901
1902 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1903 #[test]
1904 fn audioread_native_uint8_preserves_dtype_and_values() {
1905 let _lock = runmat_filesystem::provider_override_lock();
1906 let path = temp_path("wav");
1907 fs::write(&path, pcm8_wav(11_025, 1, &[0, 128, 255])).expect("write fixture");
1908
1909 let y = tensor(
1910 block_on(audioread_builtin(
1911 Value::from(path.to_string_lossy().into_owned()),
1912 vec![Value::from("native")],
1913 ))
1914 .expect("audioread"),
1915 );
1916
1917 assert_eq!(y.dtype, NumericDType::U8);
1918 assert_eq!(y.data, vec![0.0, 128.0, 255.0]);
1919 let _ = fs::remove_file(path);
1920 }
1921
1922 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1923 #[test]
1924 fn audioread_reads_float32_wave() {
1925 let _lock = runmat_filesystem::provider_override_lock();
1926 let path = temp_path("wav");
1927 fs::write(&path, float32_wav(48_000, 1, &[-0.25, 0.0, 0.5])).expect("write fixture");
1928
1929 let y = tensor(
1930 block_on(audioread_builtin(
1931 Value::from(path.to_string_lossy().into_owned()),
1932 Vec::new(),
1933 ))
1934 .expect("audioread"),
1935 );
1936
1937 assert_eq!(y.shape, vec![3, 1]);
1938 assert_eq!(y.data, vec![-0.25, 0.0, 0.5]);
1939 let _ = fs::remove_file(path);
1940 }
1941
1942 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1943 #[test]
1944 fn audioread_reads_rf64_data_size_from_ds64() {
1945 let _lock = runmat_filesystem::provider_override_lock();
1946 let path = temp_path("wav");
1947 fs::write(&path, rf64_pcm16_wav(32_000, 1, &[0, 16384])).expect("write fixture");
1948
1949 let outputs = {
1950 let _guard = crate::output_count::push_output_count(Some(2));
1951 output_list(
1952 block_on(audioread_builtin(
1953 Value::from(path.to_string_lossy().into_owned()),
1954 Vec::new(),
1955 ))
1956 .expect("audioread"),
1957 )
1958 };
1959
1960 let y = tensor(outputs[0].clone());
1961 assert_eq!(outputs[1], Value::Num(32_000.0));
1962 assert_eq!(y.shape, vec![2, 1]);
1963 assert_eq!(y.data, vec![0.0, 0.5]);
1964 let _ = fs::remove_file(path);
1965 }
1966
1967 #[cfg(not(target_arch = "wasm32"))]
1968 #[test]
1969 fn audioread_reads_via_active_filesystem_provider() {
1970 let _lock = runmat_filesystem::provider_override_lock();
1971 let dir = tempfile::tempdir().expect("tempdir");
1972 let provider =
1973 runmat_filesystem::SandboxFsProvider::new(dir.path().to_path_buf()).expect("sandbox");
1974 let _guard = runmat_filesystem::replace_provider(Arc::new(provider));
1975 block_on(runmat_filesystem::write_async(
1976 "/audio.wav",
1977 pcm16_wav(16_000, 1, &[0, 16384]),
1978 ))
1979 .expect("provider write");
1980
1981 let outputs = {
1982 let _out_guard = crate::output_count::push_output_count(Some(2));
1983 output_list(
1984 block_on(audioread_builtin(Value::from("/audio.wav"), Vec::new()))
1985 .expect("audioread"),
1986 )
1987 };
1988
1989 let y = tensor(outputs[0].clone());
1990 assert_eq!(outputs[1], Value::Num(16_000.0));
1991 assert_eq!(y.shape, vec![2, 1]);
1992 assert_eq!(y.data, vec![0.0, 0.5]);
1993 }
1994
1995 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1996 #[test]
1997 fn audioinfo_reads_flac_streaminfo() {
1998 let _lock = runmat_filesystem::provider_override_lock();
1999 let path = temp_path("flac");
2000 fs::write(&path, flac_fixture(48_000, 2, 24, 96_000)).expect("write fixture");
2001
2002 let info = block_on(audioinfo_builtin(Value::from(
2003 path.to_string_lossy().into_owned(),
2004 )))
2005 .expect("audioinfo");
2006
2007 assert_eq!(field(&info, "Format"), &Value::String("FLAC".to_string()));
2008 assert_eq!(field(&info, "NumChannels"), &Value::Num(2.0));
2009 assert_eq!(field(&info, "SampleRate"), &Value::Num(48_000.0));
2010 assert_eq!(field(&info, "TotalSamples"), &Value::Num(96_000.0));
2011 assert_eq!(field(&info, "Duration"), &Value::Num(2.0));
2012 assert_eq!(field(&info, "BitsPerSample"), &Value::Num(24.0));
2013 let _ = fs::remove_file(path);
2014 }
2015
2016 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2017 #[test]
2018 fn audioinfo_rejects_unknown_data() {
2019 let _lock = runmat_filesystem::provider_override_lock();
2020 let path = temp_path("bin");
2021 fs::write(&path, b"not audio").expect("write fixture");
2022
2023 let err = block_on(audioinfo_builtin(Value::from(
2024 path.to_string_lossy().into_owned(),
2025 )))
2026 .expect_err("format error");
2027 assert!(err.message().contains("unsupported audio format"));
2028 let _ = fs::remove_file(path);
2029 }
2030
2031 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2032 #[test]
2033 fn audioinfo_parses_mp3_frame_header() {
2034 let mut bytes = vec![0xFF, 0xFB, 0x90, 0x64];
2035 bytes.resize(417, 0);
2036 let metadata = AudioMetadata::parse(&audio_scan(bytes)).expect("mp3 metadata");
2037 assert_eq!(metadata.format, "MP3");
2038 assert_eq!(metadata.compression_method, "MPEG Layer III");
2039 assert_eq!(metadata.num_channels, 2);
2040 assert_eq!(metadata.sample_rate, 44_100.0);
2041 assert_eq!(metadata.bit_rate, Some(128_000.0));
2042 }
2043
2044 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2045 #[test]
2046 fn audioinfo_mp3_duration_uses_actual_file_size() {
2047 let bytes = vec![0xFF, 0xFB, 0x90, 0x64];
2048 let metadata =
2049 AudioMetadata::parse(&audio_scan_with_file_size(bytes, 16_000)).expect("mp3 metadata");
2050 assert_eq!(metadata.total_samples, Some(44_100));
2051 }
2052
2053 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2054 #[test]
2055 fn audioinfo_large_non_ogg_scan_skips_tail_window() {
2056 let path = temp_path("wav");
2057 let bytes = wav_fixture(44_100, 1, 16, 600_000);
2058 fs::write(&path, &bytes).expect("write fixture");
2059
2060 let scan = block_on(read_audioinfo_scan(&path)).expect("audio scan");
2061 assert_eq!(scan.file_size, bytes.len() as u64);
2062 assert!(scan.tail.is_none());
2063
2064 let _ = fs::remove_file(path);
2065 }
2066
2067 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2068 #[test]
2069 fn audioinfo_ogg_large_file_without_tail_granule_reports_unknown_samples() {
2070 let scan = audio_scan_with_file_size(ogg_vorbis_identification_fixture(0), 1_000_000);
2071 let metadata = AudioMetadata::parse(&scan).expect("ogg metadata");
2072 assert_eq!(metadata.format, "OGG");
2073 assert_eq!(metadata.total_samples, None);
2074 }
2075}