old school music tracker audio backend

change AudioState communication

luca3s 09158883 f39aaee0

+79 -97
+10
Cargo.lock
··· 364 364 "hound", 365 365 "rtrb", 366 366 "simple-left-right", 367 + "triple_buffer", 367 368 ] 368 369 369 370 [[package]] ··· 788 789 version = "0.1.32" 789 790 source = "registry+https://github.com/rust-lang/crates.io-index" 790 791 checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 792 + 793 + [[package]] 794 + name = "triple_buffer" 795 + version = "8.0.0" 796 + source = "registry+https://github.com/rust-lang/crates.io-index" 797 + checksum = "9e66931c8eca6381f0d34656a9341f09bd462010488c1a3bc0acd3f2d08dffce" 798 + dependencies = [ 799 + "crossbeam-utils", 800 + ] 791 801 792 802 [[package]] 793 803 name = "unicode-ident"
+1
tracker-engine/Cargo.toml
··· 25 25 crossbeam-utils = "0.8.20" 26 26 rtrb = "0.3.1" 27 27 simple-left-right = { path = "../simple-left-right" } 28 + triple_buffer = "8.0.0" 28 29 # assert_no_alloc 29 30 30 31 [dev-dependencies]
+3 -15
tracker-engine/examples/live_note.rs
··· 3 3 use cpal::{traits::DeviceTrait, Sample}; 4 4 use impulse_engine::{ 5 5 live_audio::ToWorkerMsg, 6 - manager::{AudioManager, AudioMsgConfig, OutputConfig}, 6 + manager::{AudioManager, OutputConfig}, 7 7 project::{ 8 8 event_command::NoteCommand, 9 9 note_event::{Note, NoteEvent, VolumeEffect}, ··· 43 43 sample_rate: default_config.sample_rate().0, 44 44 }; 45 45 46 - let mut recv = manager 47 - .init_audio( 48 - default_device, 49 - config, 50 - AudioMsgConfig { 51 - buffer_finished: true, 52 - ..Default::default() 53 - }, 54 - 20, 55 - ) 56 - .unwrap(); 46 + let mut recv = manager.init_audio(default_device, config).unwrap(); 57 47 58 48 let note_event = NoteEvent { 59 49 note: Note::new(90).unwrap(), ··· 65 55 std::thread::sleep(Duration::from_secs(1)); 66 56 manager.send_worker_msg(ToWorkerMsg::PlayEvent(note_event)); 67 57 std::thread::sleep(Duration::from_secs(1)); 68 - while let Ok(event) = recv.pop() { 69 - println!("{event:?}"); 70 - } 58 + println!("{:?}", recv.read()); 71 59 }
+3 -15
tracker-engine/examples/pattern_playback.rs
··· 3 3 use cpal::{traits::DeviceTrait, Sample}; 4 4 use impulse_engine::{ 5 5 live_audio::ToWorkerMsg, 6 - manager::{AudioManager, AudioMsgConfig, OutputConfig, PlaybackSettings}, 6 + manager::{AudioManager, OutputConfig, PlaybackSettings}, 7 7 project::{ 8 8 event_command::NoteCommand, 9 9 note_event::{Note, NoteEvent, VolumeEffect}, ··· 68 68 sample_rate: default_config.sample_rate().0, 69 69 }; 70 70 71 - let mut recv = manager 72 - .init_audio( 73 - default_device, 74 - config, 75 - AudioMsgConfig { 76 - playback_position: true, 77 - ..Default::default() 78 - }, 79 - 20, 80 - ) 81 - .unwrap(); 71 + let mut recv = manager.init_audio(default_device, config).unwrap(); 82 72 83 73 manager.send_worker_msg(ToWorkerMsg::Playback(PlaybackSettings::default())); 84 74 85 75 std::thread::sleep(Duration::from_secs(5)); 86 76 manager.deinit_audio(); 87 - // while let Ok(event) = recv.try_next() { 88 - // println!("{event:?}"); 89 - // } 77 + println!("{:?}", recv.read()) 90 78 }
+13 -12
tracker-engine/src/audio_processing/playback.rs
··· 6 6 project::song::Song, 7 7 }; 8 8 9 - // #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 - // pub struct PlaybackPosition { 11 - // pub order: usize, 12 - // pub pattern: usize, 13 - // pub row: u16, 14 - // } 9 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 + pub struct PlaybackStatus { 11 + position: PlaybackPosition, 12 + // which sample is playing, 13 + // which how far along is each sample 14 + // which channel is playing 15 + // ... 16 + } 15 17 16 18 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 17 19 pub struct PlaybackPosition { ··· 122 124 (samplerate * 10) / u32::from(tempo) 123 125 } 124 126 125 - pub fn get_position(&self) -> PlaybackPosition { 126 - self.position 127 + pub fn get_status(&self) -> PlaybackStatus { 128 + // maybe if it gets more fields compute them while playing back and just copy out here 129 + PlaybackStatus { 130 + position: self.position, 131 + } 127 132 } 128 133 129 134 pub fn set_samplerate(&mut self, samplerate: u32) { ··· 238 243 false 239 244 } 240 245 } 241 - } 242 - 243 - pub fn get_position(&self) -> PlaybackPosition { 244 - self.state.get_position() 245 246 } 246 247 } 247 248
+19 -23
tracker-engine/src/live_audio.rs
··· 1 1 use std::fmt::Debug; 2 2 use std::ops::{AddAssign, IndexMut}; 3 3 4 - use crate::audio_processing::playback::PlaybackState; 4 + use crate::audio_processing::playback::{PlaybackState, PlaybackStatus}; 5 5 use crate::audio_processing::sample::Interpolation; 6 6 use crate::audio_processing::sample::SamplePlayer; 7 7 use crate::audio_processing::Frame; 8 - use crate::manager::{AudioMsgConfig, FromWorkerMsg, OutputConfig, PlaybackSettings}; 8 + use crate::manager::{OutputConfig, PlaybackSettings}; 9 9 use crate::project::note_event::NoteEvent; 10 10 use crate::project::song::Song; 11 11 use cpal::{Sample, SampleFormat}; 12 12 use simple_left_right::Reader; 13 + 14 + pub type LiveAudioStatus = (Option<PlaybackStatus>, cpal::OutputStreamTimestamp); 13 15 14 16 pub(crate) struct LiveAudio { 15 17 song: Reader<Song<true>>, 16 18 playback_state: Option<PlaybackState<'static, true>>, 17 19 live_note: Option<SamplePlayer<'static, true>>, 18 - // replace with something explicitly realtime safe. I think std mpsc does syscalls to sleep and wake the thread 19 20 manager: rtrb::Consumer<ToWorkerMsg>, 20 - audio_msg_config: AudioMsgConfig, 21 - to_app: rtrb::Producer<FromWorkerMsg>, 21 + // gets created in the first callback. could maybe do with an MaybeUninit 22 + state_sender: triple_buffer::Input<Option<LiveAudioStatus>>, 22 23 config: OutputConfig, 24 + 23 25 buffer: Box<[Frame]>, 24 26 } 25 27 ··· 30 32 pub fn new( 31 33 song: Reader<Song<true>>, 32 34 manager: rtrb::Consumer<ToWorkerMsg>, 33 - audio_msg_config: AudioMsgConfig, 34 - to_app: rtrb::Producer<FromWorkerMsg>, 35 + state_sender: triple_buffer::Input<Option<LiveAudioStatus>>, 35 36 config: OutputConfig, 36 37 ) -> Self { 37 38 Self { ··· 39 40 playback_state: None, 40 41 live_note: None, 41 42 manager, 42 - audio_msg_config, 43 - to_app, 43 + state_sender, 44 44 config, 45 45 buffer: vec![Frame::default(); config.buffer_size.try_into().unwrap()].into(), 46 46 } 47 47 } 48 48 49 + fn send_state(&mut self, info: &cpal::OutputCallbackInfo) { 50 + self.state_sender.write(Some(( 51 + self.playback_state.as_ref().map(|s| s.get_status()), 52 + info.timestamp(), 53 + ))); 54 + } 55 + 49 56 #[inline] 50 57 /// returns true if work was done 51 58 fn fill_internal_buffer(&mut self) -> bool { ··· 97 104 98 105 // process song playback 99 106 if let Some(playback) = &mut self.playback_state { 100 - let old_position = playback.get_position(); 101 107 let playback_iter = playback.iter::<{ Self::INTERPOLATION }>(&song); 102 108 self.buffer 103 109 .iter_mut() 104 110 .zip(playback_iter) 105 111 .for_each(|(buf, frame)| buf.add_assign(frame)); 106 - 107 - if self.audio_msg_config.playback_position && old_position != playback.get_position() { 108 - let _ = self.to_app.push(FromWorkerMsg::CurrentPlaybackPosition( 109 - playback.get_position(), 110 - )); 111 - } 112 112 113 113 if playback.is_done() { 114 114 self.playback_state = None; ··· 172 172 _ => panic!("Sample Format not supported."), 173 173 } 174 174 175 - if self.audio_msg_config.buffer_finished { 176 - let _ = self 177 - .to_app 178 - .push(FromWorkerMsg::BufferFinished(info.timestamp())); 179 - } 175 + self.send_state(info); 180 176 } 181 177 } 182 178 183 179 // unsure wether i want to use this or untyped_callback 184 180 // also relevant when cpal gets made into a generic that maybe this gets useful 185 - #[expect(dead_code)] 186 181 pub fn get_typed_callback<S: cpal::SizedSample + cpal::FromSample<f32>>( 187 182 mut self, 188 183 ) -> impl FnMut(&mut [S], &cpal::OutputCallbackInfo) { 189 - move |data, _info| { 184 + move |data, info| { 190 185 assert_eq!( 191 186 data.len(), 192 187 usize::try_from(self.config.buffer_size).unwrap() ··· 196 191 if self.fill_internal_buffer() { 197 192 self.fill_from_internal(data); 198 193 } 194 + self.send_state(info); 199 195 } 200 196 } 201 197 }
+30 -32
tracker-engine/src/manager.rs
··· 1 - use std::{ 2 - fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration 3 - }; 1 + use std::{fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration}; 4 2 5 3 #[cfg(feature = "async")] 6 4 use std::ops::ControlFlow; ··· 11 9 12 10 use crate::{ 13 11 audio_processing::playback::PlaybackPosition, 14 - live_audio::{LiveAudio, ToWorkerMsg}, 12 + live_audio::{LiveAudio, LiveAudioStatus, ToWorkerMsg}, 15 13 project::song::{Song, SongOperation, ValidOperation}, 16 14 }; 17 15 ··· 41 39 #[cfg(feature = "async")] 42 40 async fn async_spin(mut f: impl FnMut() -> bool, time: Duration) { 43 41 let backoff = crossbeam_utils::Backoff::new(); 44 - loop { 45 - if f() { 46 - return; 47 - } 42 + loop { 43 + if f() { 44 + return; 45 + } 48 46 49 - if backoff.is_completed() { 50 - async_io::Timer::after(time).await; 51 - } else { 52 - backoff.snooze(); 53 - } 47 + if backoff.is_completed() { 48 + async_io::Timer::after(time).await; 49 + } else { 50 + backoff.snooze(); 54 51 } 52 + } 55 53 } 56 54 57 55 impl ManageCollector { ··· 78 76 #[cfg(feature = "async")] 79 77 ManageCollector::External(channel, _) => { 80 78 _ = channel.send_blocking(frees); 81 - }, 79 + } 82 80 } 83 81 } 84 82 ··· 88 86 ManageCollector::Internal(_, num) => *num += frees, 89 87 ManageCollector::External(channel, _) => { 90 88 _ = channel.send(frees).await; 91 - }, 89 + } 92 90 } 93 91 } 94 92 } ··· 107 105 108 106 #[cfg(feature = "async")] 109 107 impl CollectGarbage { 110 - fn new(collector: ManuallyDrop<Collector>, channel: async_channel::Receiver<usize>, to_be_freed: usize) -> Self { 108 + fn new( 109 + collector: ManuallyDrop<Collector>, 110 + channel: async_channel::Receiver<usize>, 111 + to_be_freed: usize, 112 + ) -> Self { 111 113 Self { 112 114 collector, 113 115 channel: Some(channel), ··· 200 202 spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP); 201 203 SongEdit { 202 204 song: self.song.try_lock().unwrap(), 203 - gc: &mut self.gc 205 + gc: &mut self.gc, 204 206 } 205 207 } 206 208 ··· 222 224 &mut self, 223 225 device: cpal::Device, 224 226 config: OutputConfig, 225 - audio_msg_config: AudioMsgConfig, 226 - msg_buffer_size: usize, 227 - ) -> Result<rtrb::Consumer<FromWorkerMsg>, cpal::BuildStreamError> { 227 + ) -> Result<triple_buffer::Output<Option<LiveAudioStatus>>, cpal::BuildStreamError> { 228 228 const TO_WORKER_CAPACITY: usize = 5; 229 229 230 - let from_worker = rtrb::RingBuffer::new(msg_buffer_size); 230 + let from_worker = triple_buffer::triple_buffer(&None); 231 231 let to_worker = rtrb::RingBuffer::new(TO_WORKER_CAPACITY); 232 232 let reader = self.song.build_reader().unwrap(); 233 233 234 - let audio_worker = 235 - LiveAudio::new(reader, to_worker.1, audio_msg_config, from_worker.0, config); 234 + let audio_worker = LiveAudio::new(reader, to_worker.1, from_worker.0, config); 236 235 237 236 let stream = device.build_output_stream_raw( 238 237 &config.into(), ··· 309 308 async_spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP).await; 310 309 SongEdit { 311 310 song: self.song.try_lock().unwrap(), 312 - gc: &mut self.gc 311 + gc: &mut self.gc, 313 312 } 314 313 } 315 314 ··· 331 330 332 331 /// makes the garbage collector internal again. 333 332 pub fn insert_garbage_collector(&mut self, gc: CollectGarbage) { 334 - let CollectGarbage {collector, to_be_freed, channel: _} = gc; 333 + let CollectGarbage { 334 + collector, 335 + to_be_freed, 336 + channel: _, 337 + } = gc; 335 338 self.gc = ManageCollector::Internal(collector, to_be_freed); 336 339 } 337 340 } ··· 351 354 self.deinit_audio(); 352 355 let mut song = self.edit_song(); 353 356 for i in 0..Song::<true>::MAX_SAMPLES { 354 - song.apply_operation(SongOperation::RemoveSample(i)).unwrap(); 357 + song.apply_operation(SongOperation::RemoveSample(i)) 358 + .unwrap(); 355 359 } 356 360 song.finish(); 357 361 // lock it once more to ensure that the changes were propagated ··· 438 442 }), 439 443 } 440 444 } 441 - } 442 - 443 - #[derive(Default, Debug, Clone, Copy)] 444 - pub struct AudioMsgConfig { 445 - pub buffer_finished: bool, 446 - pub playback_position: bool, 447 445 } 448 446 449 447 #[derive(Debug, Clone, Copy)]