augmented_playhead/
lib.rs1use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
24
25use augmented_atomics::{AtomicF32, AtomicF64, AtomicOption, AtomicValue};
26
27pub struct PlayHeadOptions {
28 sample_rate: AtomicOption<AtomicF32>,
29 tempo: AtomicOption<AtomicF32>,
30 ticks_per_quarter_note: AtomicOption<AtomicU32>,
31}
32
33impl PlayHeadOptions {
34 pub fn new(
35 sample_rate: Option<f32>,
36 tempo: Option<f32>,
37 ticks_per_quarter_note: Option<u32>,
38 ) -> Self {
39 PlayHeadOptions {
40 sample_rate: sample_rate.into(),
41 tempo: tempo.into(),
42 ticks_per_quarter_note: ticks_per_quarter_note.into(),
43 }
44 }
45
46 pub fn sample_rate(&self) -> Option<f32> {
47 self.sample_rate.inner()
48 }
49
50 pub fn tempo(&self) -> Option<f32> {
51 self.tempo.inner()
52 }
53
54 pub fn ticks_per_quarter_note(&self) -> Option<u32> {
55 self.ticks_per_quarter_note.inner()
56 }
57}
58
59pub struct PlayHead {
60 options: PlayHeadOptions,
61 position_us: AtomicU64,
63 position_samples: AtomicU32,
64 position_ticks: AtomicU32,
65 position_beats: AtomicF64,
66}
67
68impl PlayHead {
69 pub fn new(options: PlayHeadOptions) -> Self {
70 Self {
71 options,
72 position_beats: AtomicF64::from(0.0),
73 position_samples: AtomicU32::from(0),
74 position_ticks: AtomicU32::from(0),
75 position_us: AtomicU64::from(0),
76 }
77 }
78
79 pub fn accept_samples(&self, num_samples: u32) {
80 self.position_samples
81 .fetch_add(num_samples, Ordering::Relaxed);
82 if let Some(sample_rate) = self.options.sample_rate.inner() {
83 let elapsed_secs = (1.0 / sample_rate) * (num_samples as f32);
84 let elapsed_us = elapsed_secs * 1_000_000.0;
85 self.position_us
86 .fetch_add(elapsed_us as u64, Ordering::Relaxed);
87 self.update_position_beats(elapsed_secs as f64);
88
89 if let Some((tempo, ticks_per_quarter_note)) = self
90 .options
91 .tempo
92 .inner()
93 .zip(self.options.ticks_per_quarter_note.inner())
94 {
95 let secs_per_beat = 1.0 / (tempo / 60.0);
96 let elapsed_beats = (elapsed_us / 1_000_000.0) / secs_per_beat;
97 self.position_ticks.store(
98 (ticks_per_quarter_note as f32 * elapsed_beats) as u32,
99 Ordering::Relaxed,
100 );
101 }
102 }
103 }
104
105 pub fn accept_ticks(&self, num_ticks: u32) {
106 self.position_ticks.fetch_add(num_ticks, Ordering::Relaxed);
107
108 if let Some((tempo, ticks_per_quarter_note)) = self
109 .options
110 .tempo
111 .inner()
112 .zip(self.options.ticks_per_quarter_note.inner())
113 {
114 let elapsed_beats = num_ticks as f32 / ticks_per_quarter_note as f32;
115 let secs_per_beat = 1.0 / (tempo / 60.0);
116 let elapsed_seconds = elapsed_beats * secs_per_beat;
117 let elapsed_us = (elapsed_seconds * 1_000_000.0) as u64;
118 self.position_us.fetch_add(elapsed_us, Ordering::Relaxed);
119 self.update_position_beats(elapsed_seconds as f64);
120
121 if let Some(sample_rate) = self.options.sample_rate.inner() {
122 let elapsed_samples = sample_rate * elapsed_seconds;
123 self.position_samples
124 .fetch_add(elapsed_samples as u32, Ordering::Relaxed);
125 }
126 }
127 }
128
129 pub fn set_position_seconds(&self, seconds: f32) {
130 self.position_us
131 .store((seconds * 1_000_000.0) as u64, Ordering::Relaxed);
132 self.position_beats.store(
133 self.options
134 .tempo
135 .inner()
136 .map(|tempo| {
137 let beats_per_second = tempo as f64 / 60.0;
138 beats_per_second * seconds as f64
139 })
140 .unwrap_or(0.0),
141 Ordering::Relaxed,
142 );
143 self.accept_samples(0);
144 }
145
146 pub fn set_tempo(&self, tempo: f32) {
147 self.options.tempo.set(Some(tempo));
148 }
149
150 pub fn set_sample_rate(&self, sample_rate: f32) {
151 self.options.sample_rate.set(Some(sample_rate));
152 }
153
154 pub fn options(&self) -> &PlayHeadOptions {
155 &self.options
156 }
157
158 pub fn position_seconds(&self) -> f32 {
159 self.position_us.get() as f32 / 1_000_000.0
160 }
161
162 pub fn position_beats(&self) -> f64 {
163 self.position_beats.get()
164 }
165
166 pub fn position_samples(&self) -> u32 {
167 self.position_samples.get()
168 }
169
170 pub fn position_ticks(&self) -> u32 {
171 self.position_ticks.get()
172 }
173
174 fn update_position_beats(&self, elapsed_secs: f64) {
175 let position_beats = self.position_beats.get();
176 let position_beats = position_beats
177 + self
178 .options
179 .tempo
180 .inner()
181 .map(|tempo| {
182 let beats_per_second = tempo as f64 / 60.0;
183 beats_per_second * elapsed_secs
184 })
185 .unwrap_or(0.0);
186 self.position_beats.set(position_beats);
187 }
188}
189
190#[cfg(test)]
191mod test {
192 use crate::{PlayHead, PlayHeadOptions};
193
194 #[test]
195 fn test_accept_samples() {
196 let options = PlayHeadOptions::new(Some(44100.0), Some(120.0), Some(32));
197 let play_head = PlayHead::new(options);
198 assert_eq!(play_head.position_samples(), 0);
199 assert_eq!(play_head.position_ticks(), 0);
200 assert!((play_head.position_seconds() - 0.0).abs() < f32::EPSILON);
201
202 play_head.accept_samples(512);
203 assert_eq!(play_head.position_samples(), 512);
204 assert!((play_head.position_seconds() - 0.01160998).abs() < (1.0 / 1_000_000.0));
206 }
207
208 #[test]
209 fn test_accept_samples_ticks_conversion() {
210 let options = PlayHeadOptions::new(Some(44100.0), Some(120.0), Some(32));
211 let play_head = PlayHead::new(options);
212 assert_eq!(play_head.position_samples(), 0);
213 assert_eq!(play_head.position_ticks(), 0);
214 assert!((play_head.position_seconds() - 0.0).abs() < f32::EPSILON);
215
216 play_head.accept_samples(22050);
218 assert!((play_head.position_seconds() - 0.5).abs() < f32::EPSILON);
219 assert_eq!(play_head.position_ticks(), 32);
220 }
221
222 #[test]
223 fn test_accept_many_samples() {
224 let sample_count = 5644800;
225 let options = PlayHeadOptions::new(Some(44100.0), Some(120.0), Some(32));
226 let play_head = PlayHead::new(options);
227 play_head.accept_samples(sample_count);
228 assert!((play_head.position_seconds() - 128.0).abs() < f32::EPSILON);
229 assert!((play_head.position_beats() - 256.0).abs() < f64::EPSILON);
230 play_head.accept_samples(sample_count / 2);
231 assert!((play_head.position_seconds() - 192.0).abs() < f32::EPSILON);
232 assert!((play_head.position_beats() - 384.0).abs() < f64::EPSILON);
233 }
234
235 }