devalang_wasm/engine/audio/
duration.rs1use crate::language::syntax::ast::DurationValue;
2use anyhow::{Result, anyhow};
3
4pub fn duration_to_seconds(duration: &DurationValue, bpm: f32) -> Result<f32> {
10 if !bpm.is_finite() || bpm <= 0.0 {
11 return Err(anyhow!("Invalid BPM for duration conversion: {}", bpm));
12 }
13
14 match duration {
15 DurationValue::Milliseconds(ms) => Ok(ms / 1000.0),
16 DurationValue::Beats(beats) => Ok(beats * seconds_per_beat(bpm)?),
17 DurationValue::Beat(value) => parse_beat_fraction(value)
18 .map(|beats| beats * seconds_per_beat(bpm).unwrap_or(0.0))
19 .ok_or_else(|| anyhow!("Invalid beat fraction: {}", value)),
20 DurationValue::Number(ms) => Ok(ms / 1000.0),
21 DurationValue::Identifier(identifier) => parse_temporal_identifier(identifier)
22 .map(|beats| beats * seconds_per_beat(bpm).unwrap_or(0.0))
23 .ok_or_else(|| anyhow!("Cannot resolve identifier duration: {}", identifier)),
24 DurationValue::Auto => Ok(seconds_per_beat(bpm)?),
25 }
26}
27
28pub fn seconds_per_beat(bpm: f32) -> Result<f32> {
29 if !bpm.is_finite() || bpm <= 0.0 {
30 Err(anyhow!("Invalid BPM for beat duration: {}", bpm))
31 } else {
32 Ok(60.0 / bpm)
33 }
34}
35
36fn parse_beat_fraction(value: &str) -> Option<f32> {
37 if let Ok(beats) = value.trim().parse::<f32>() {
38 return Some(beats);
39 }
40
41 let mut parts = value.split('/');
42 let numerator = parts.next()?.trim().parse::<f32>().ok()?;
43 let denominator = parts.next()?.trim().parse::<f32>().ok()?;
44 if parts.next().is_some() || denominator.abs() <= f32::EPSILON {
45 return None;
46 }
47
48 Some(numerator / denominator)
49}
50
51fn parse_temporal_identifier(identifier: &str) -> Option<f32> {
52 let rest = identifier.strip_prefix("__temporal__")?;
53 let last_underscore = rest.rfind('_')?;
54 rest[..last_underscore].parse::<f32>().ok()
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn test_duration_to_seconds_converts_supported_units() {
63 assert_eq!(
64 duration_to_seconds(&DurationValue::Milliseconds(500.0), 120.0).unwrap(),
65 0.5
66 );
67 assert_eq!(
68 duration_to_seconds(&DurationValue::Beats(2.0), 120.0).unwrap(),
69 1.0
70 );
71 assert_eq!(
72 duration_to_seconds(&DurationValue::Beat("1/4".to_string()), 120.0).unwrap(),
73 0.125
74 );
75 assert_eq!(
76 duration_to_seconds(
77 &DurationValue::Identifier("__temporal__4_beat".to_string()),
78 120.0,
79 )
80 .unwrap(),
81 2.0
82 );
83 }
84
85 #[test]
86 fn test_duration_to_seconds_rejects_invalid_values() {
87 assert!(duration_to_seconds(&DurationValue::Auto, 0.0).is_err());
88 assert!(duration_to_seconds(&DurationValue::Beat("1/0".to_string()), 120.0).is_err());
89 assert!(
90 duration_to_seconds(&DurationValue::Identifier("missing".to_string()), 120.0).is_err()
91 );
92 }
93}