use_media_duration/
lib.rs1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct MediaDuration {
21 seconds: f64,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MediaDurationError {
26 InvalidSeconds,
27 InvalidSampleRate,
28}
29
30fn validate_seconds(seconds: f64) -> Result<f64, MediaDurationError> {
31 if !seconds.is_finite() || seconds < 0.0 {
32 Err(MediaDurationError::InvalidSeconds)
33 } else {
34 Ok(seconds)
35 }
36}
37
38fn validate_sample_rate(sample_rate_hz: f64) -> Result<f64, MediaDurationError> {
39 if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 {
40 Err(MediaDurationError::InvalidSampleRate)
41 } else {
42 Ok(sample_rate_hz)
43 }
44}
45
46impl MediaDuration {
47 pub fn new(seconds: f64) -> Result<Self, MediaDurationError> {
48 Ok(Self {
49 seconds: validate_seconds(seconds)?,
50 })
51 }
52
53 #[must_use]
54 pub fn seconds(&self) -> f64 {
55 self.seconds
56 }
57
58 #[must_use]
59 pub fn millis(&self) -> u64 {
60 (self.seconds * 1_000.0).round() as u64
61 }
62
63 #[must_use]
64 pub fn minutes(&self) -> f64 {
65 self.seconds / 60.0
66 }
67
68 #[must_use]
69 pub fn hours(&self) -> f64 {
70 self.seconds / 3_600.0
71 }
72}
73
74pub fn seconds_to_millis(seconds: f64) -> Result<u64, MediaDurationError> {
75 Ok((validate_seconds(seconds)? * 1_000.0).round() as u64)
76}
77
78#[must_use]
79pub fn millis_to_seconds(millis: u64) -> f64 {
80 millis as f64 / 1_000.0
81}
82
83pub fn format_duration_hms(seconds: f64) -> Result<String, MediaDurationError> {
84 let total_seconds = validate_seconds(seconds)?.floor() as u64;
85 let hours = total_seconds / 3_600;
86 let minutes = (total_seconds % 3_600) / 60;
87 let seconds = total_seconds % 60;
88
89 Ok(format!("{hours:02}:{minutes:02}:{seconds:02}"))
90}
91
92pub fn duration_from_samples(
93 sample_count: usize,
94 sample_rate_hz: f64,
95) -> Result<f64, MediaDurationError> {
96 Ok(sample_count as f64 / validate_sample_rate(sample_rate_hz)?)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::{
102 MediaDuration, MediaDurationError, duration_from_samples, format_duration_hms,
103 millis_to_seconds, seconds_to_millis,
104 };
105
106 #[test]
107 fn computes_duration_helpers() {
108 let duration = MediaDuration::new(90.5).unwrap();
109
110 assert_eq!(duration.seconds(), 90.5);
111 assert_eq!(duration.millis(), 90_500);
112 assert!((duration.minutes() - 1.508_333_333_333_333_3).abs() < 1.0e-12);
113 assert!((duration.hours() - 0.025_138_888_888_888_89).abs() < 1.0e-12);
114 assert_eq!(seconds_to_millis(1.5).unwrap(), 1_500);
115 assert_eq!(millis_to_seconds(1_500), 1.5);
116 }
117
118 #[test]
119 fn formats_duration_and_samples() {
120 assert_eq!(format_duration_hms(3661.0).unwrap(), "01:01:01");
121 assert!((duration_from_samples(96_000, 48_000.0).unwrap() - 2.0).abs() < 1.0e-12);
122 assert_eq!(duration_from_samples(0, 48_000.0).unwrap(), 0.0);
123 }
124
125 #[test]
126 fn rejects_invalid_duration_inputs() {
127 assert_eq!(
128 MediaDuration::new(-1.0),
129 Err(MediaDurationError::InvalidSeconds)
130 );
131 assert_eq!(
132 seconds_to_millis(f64::NAN),
133 Err(MediaDurationError::InvalidSeconds)
134 );
135 assert_eq!(
136 format_duration_hms(-0.1),
137 Err(MediaDurationError::InvalidSeconds)
138 );
139 assert_eq!(
140 duration_from_samples(10, 0.0),
141 Err(MediaDurationError::InvalidSampleRate)
142 );
143 }
144}