torque_tracker_engine/
manager.rs

1use std::{
2    fmt::Debug,
3    num::{NonZero, NonZeroU16},
4    time::Duration,
5};
6
7use simple_left_right::{WriteGuard, Writer};
8
9use crate::{
10    audio_processing::playback::PlaybackStatus,
11    live_audio::LiveAudio,
12    project::{
13        note_event::NoteEvent,
14        song::{Song, SongOperation, ValidOperation},
15    },
16    sample::Sample,
17};
18
19#[derive(Debug, Clone, Copy)]
20pub enum ToWorkerMsg {
21    Playback(PlaybackSettings),
22    StopPlayback,
23    PlayEvent(NoteEvent),
24    StopLiveNote,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[must_use]
29pub enum SendResult {
30    Success,
31    BufferFull,
32    AudioInactive,
33}
34
35impl SendResult {
36    #[track_caller]
37    pub fn unwrap(self) {
38        match self {
39            SendResult::Success => (),
40            SendResult::BufferFull => panic!("Buffer full"),
41            SendResult::AudioInactive => panic!("Audio inactive"),
42        }
43    }
44
45    pub fn is_success(self) -> bool {
46        self == Self::Success
47    }
48}
49
50/// Communication to and from an active Stream
51#[derive(Debug)]
52struct ActiveStreamComms {
53    buffer_time: Duration,
54    send: rtrb::Producer<ToWorkerMsg>,
55    status: triple_buffer::Output<Option<PlaybackStatus>>,
56}
57
58#[derive(Debug, Default)]
59pub(crate) struct Collector {
60    samples: Vec<Sample>,
61}
62
63impl Collector {
64    pub fn add_sample(&mut self, sample: Sample) {
65        self.samples.push(sample);
66    }
67
68    fn collect(&mut self) {
69        self.samples.retain(|s| {
70            // only look at strong count as weak pointers are not used
71            s.strongcount() != 1
72        });
73    }
74}
75
76/// You will need to write your own spin loops.
77/// For that you can and maybe should use AudioManager::buffer_time.
78///
79/// The Stream API is not "Rusty" and not ergonimic to use, but Stream are often not Send, while the Manager is
80/// suited well for being in a Global Mutex. This is why the Stream can't live inside the Manager. If you can
81/// think of a better API i would love to replace this.
82pub struct AudioManager {
83    song: Writer<Song, ValidOperation>,
84    gc: Collector,
85    stream_comms: Option<ActiveStreamComms>,
86}
87
88impl AudioManager {
89    pub fn new(song: Song) -> Self {
90        let mut gc = Collector::default();
91        for (_, sample) in song.samples.iter().flatten() {
92            gc.add_sample(sample.clone());
93        }
94        let left_right = simple_left_right::Writer::new(song);
95
96        Self {
97            song: left_right,
98            gc,
99            stream_comms: None,
100        }
101    }
102
103    /// If this returns None, waiting buffer_time should (weird threading issues aside) always be enough time
104    /// and it should return Some after that.
105    pub fn try_edit_song(&mut self) -> Option<SongEdit<'_>> {
106        self.song.try_lock().map(|song| SongEdit {
107            song,
108            gc: &mut self.gc,
109        })
110    }
111
112    pub fn get_song(&self) -> &Song {
113        self.song.read()
114    }
115
116    pub fn collect_garbage(&mut self) {
117        self.gc.collect();
118    }
119
120    pub fn try_msg_worker(&mut self, msg: ToWorkerMsg) -> SendResult {
121        if let Some(stream) = &mut self.stream_comms {
122            match stream.send.push(msg) {
123                Ok(_) => SendResult::Success,
124                Err(_) => SendResult::BufferFull,
125            }
126        } else {
127            SendResult::AudioInactive
128        }
129    }
130
131    /// last playback status sent by the audio worker
132    pub fn playback_status(&mut self) -> Option<&Option<PlaybackStatus>> {
133        self.stream_comms.as_mut().map(|s| s.status.read())
134    }
135
136    /// Some if a stream is active.
137    /// Returns the approximate time it takes to process an audio buffer based on the used settings.
138    ///
139    /// Useful for implementing spin_loops on collect_garbage or for locking a SongEdit as every time a buffer is finished
140    /// garbage could be releases and a lock could be made available
141    pub fn buffer_time(&self) -> Option<Duration> {
142        self.stream_comms.as_ref().map(|s| s.buffer_time)
143    }
144
145    /// If the config specifies more than two channels only the first two will be filled with audio.
146    /// The rest gets silence.
147    ///
148    /// The callback in for example Cpal provides an additional arguement, where a timestamp is give.
149    /// That should ba handled by wrapping this function in another callback, where this argument could
150    /// then be ignored or send somewhere for processing. This Sending needs to happen wait-free!! There are
151    /// a couple of libaries that can do this, i would recommend triple_buffer.
152    ///
153    /// The OutputConfig has to match the config of the AudioStream that will call this. If for example the
154    /// buffer size is different Panics will occur.
155    ///
156    /// In my testing i noticed that when using Cpal with non-standard buffer sizes Cpal would just give
157    /// another buffer size. This will also lead to panics.
158    ///
159    /// The stream has to closed before dropping the Manager and the manager has to be notified by calling stream_closed.
160    pub fn get_callback<Sample: dasp::sample::Sample + dasp::sample::FromSample<f32>>(
161        &mut self,
162        config: OutputConfig,
163    ) -> impl FnMut(&mut [Sample]) {
164        const TO_WORKER_CAPACITY: usize = 5;
165
166        assert!(self.stream_comms.is_none(), "Stream already active");
167        let from_worker = triple_buffer::triple_buffer(&None);
168        let to_worker = rtrb::RingBuffer::new(TO_WORKER_CAPACITY);
169        let reader = self.song.build_reader().unwrap();
170
171        let audio_worker = LiveAudio::new(reader, to_worker.1, from_worker.0, config);
172        let buffer_time =
173            Duration::from_millis((config.buffer_size * 1000 / config.sample_rate).into());
174
175        self.stream_comms = Some(ActiveStreamComms {
176            buffer_time,
177            send: to_worker.0,
178            status: from_worker.1,
179        });
180
181        audio_worker.get_typed_callback()
182    }
183
184    /// When closing the Stream this method should be called.
185    pub fn stream_closed(&mut self) {
186        self.stream_comms = None
187    }
188}
189
190impl Drop for AudioManager {
191    fn drop(&mut self) {
192        // try to stop playback if a stream is active
193        if let Some(stream) = &mut self.stream_comms {
194            eprintln!("AudioManager dropped while audio Stream still active.");
195            let msg1 = stream.send.push(ToWorkerMsg::StopLiveNote);
196            let msg2 = stream.send.push(ToWorkerMsg::StopPlayback);
197            if msg1.is_err() || msg2.is_err() {
198                // This happens when the message buffer is full
199                eprintln!("Audio playback couldn't be stopped completely");
200            } else {
201                eprintln!("Audio playback was stopped");
202            }
203        }
204    }
205}
206
207/// the changes made to the song will be made available to the playing live audio as soon as
208/// this struct is dropped.
209///
210/// With this you can load the full song without ever playing a half initialised state
211/// when doing mulitple operations this object should be kept as it is
212#[derive(Debug)]
213pub struct SongEdit<'a> {
214    song: WriteGuard<'a, Song, ValidOperation>,
215    gc: &'a mut Collector,
216}
217
218impl SongEdit<'_> {
219    pub fn apply_operation(&mut self, op: SongOperation) -> Result<(), SongOperation> {
220        let valid_operation = ValidOperation::new(op, self.gc, self.song.read())?;
221        self.song.apply_op(valid_operation);
222        Ok(())
223    }
224
225    pub fn song(&self) -> &Song {
226        self.song.read()
227    }
228
229    /// Finish the changes and publish them to the live playing song.
230    /// Equivalent to std::mem::drop(SongEdit)
231    pub fn finish(self) {}
232}
233
234#[derive(Debug, Clone, Copy)]
235pub struct OutputConfig {
236    pub buffer_size: u32,
237    pub channel_count: NonZeroU16,
238    pub sample_rate: NonZero<u32>,
239}
240
241#[derive(Debug, Clone, Copy)]
242pub enum PlaybackSettings {
243    Pattern { idx: u8, should_loop: bool },
244    Order { idx: u16, should_loop: bool },
245}