augmented_playhead/
lib.rs

1// Augmented Audio: Audio libraries and applications
2// Copyright (c) 2022 Pedro Tacla Yamada
3//
4// The MIT License (MIT)
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15//
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22// THE SOFTWARE.
23use 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    // micro-second position ; might be better to use double.
62    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        // At 44100Hz each block should be roughly 1ms
205        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        // At 44100Hz, 22050 samples equals to 500ms, which is 1 beat at 120bpm
217        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    // #[test]
236    // fn test_accept_samples_loop() {
237    //     let inverse_sample_rate = 1.0 / 44100.0;
238    //     let options = PlayHeadOptions {
239    //         sample_rate: Some(44100.0),
240    //         tempo: Some(120),
241    //         ticks_per_quarter_note: Some(32),
242    //     };
243    //     let mut play_head = PlayHead::new(options);
244    //
245    //     for i in 0..100_000_000 {
246    //         let expected = (i as f32) * inverse_sample_rate;
247    //         assert!(
248    //             play_head.position_seconds() - expected < 0.02,
249    //             "(i = {}) {} != {}",
250    //             i,
251    //             play_head.position_seconds(),
252    //             expected
253    //         );
254    //         play_head.accept_samples(1);
255    //     }
256    // }
257}