old school music tracker audio backend
at main 245 lines 7.9 kB view raw
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}