diff --git a/Cargo.lock b/Cargo.lock index 3f6a7d7..18743f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,7 @@ dependencies = [ "hound", "rtrb", "simple-left-right", + "triple_buffer", ] [[package]] @@ -789,6 +790,15 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +[[package]] +name = "triple_buffer" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e66931c8eca6381f0d34656a9341f09bd462010488c1a3bc0acd3f2d08dffce" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/tracker-engine/Cargo.toml b/tracker-engine/Cargo.toml index bd3274a..671b739 100644 --- a/tracker-engine/Cargo.toml +++ b/tracker-engine/Cargo.toml @@ -25,6 +25,7 @@ cpal = "0.15.3" crossbeam-utils = "0.8.20" rtrb = "0.3.1" simple-left-right = { path = "../simple-left-right" } +triple_buffer = "8.0.0" # assert_no_alloc [dev-dependencies] diff --git a/tracker-engine/examples/live_note.rs b/tracker-engine/examples/live_note.rs index a0efc50..8b0f054 100644 --- a/tracker-engine/examples/live_note.rs +++ b/tracker-engine/examples/live_note.rs @@ -3,7 +3,7 @@ use std::{num::NonZeroU16, time::Duration}; use cpal::{traits::DeviceTrait, Sample}; use impulse_engine::{ live_audio::ToWorkerMsg, - manager::{AudioManager, AudioMsgConfig, OutputConfig}, + manager::{AudioManager, OutputConfig}, project::{ event_command::NoteCommand, note_event::{Note, NoteEvent, VolumeEffect}, @@ -43,17 +43,7 @@ fn main() { sample_rate: default_config.sample_rate().0, }; - let mut recv = manager - .init_audio( - default_device, - config, - AudioMsgConfig { - buffer_finished: true, - ..Default::default() - }, - 20, - ) - .unwrap(); + let mut recv = manager.init_audio(default_device, config).unwrap(); let note_event = NoteEvent { note: Note::new(90).unwrap(), @@ -65,7 +55,5 @@ fn main() { std::thread::sleep(Duration::from_secs(1)); manager.send_worker_msg(ToWorkerMsg::PlayEvent(note_event)); std::thread::sleep(Duration::from_secs(1)); - while let Ok(event) = recv.pop() { - println!("{event:?}"); - } + println!("{:?}", recv.read()); } diff --git a/tracker-engine/examples/pattern_playback.rs b/tracker-engine/examples/pattern_playback.rs index e92fcd0..5b5e3d9 100644 --- a/tracker-engine/examples/pattern_playback.rs +++ b/tracker-engine/examples/pattern_playback.rs @@ -3,7 +3,7 @@ use std::{num::NonZeroU16, time::Duration}; use cpal::{traits::DeviceTrait, Sample}; use impulse_engine::{ live_audio::ToWorkerMsg, - manager::{AudioManager, AudioMsgConfig, OutputConfig, PlaybackSettings}, + manager::{AudioManager, OutputConfig, PlaybackSettings}, project::{ event_command::NoteCommand, note_event::{Note, NoteEvent, VolumeEffect}, @@ -68,23 +68,11 @@ fn main() { sample_rate: default_config.sample_rate().0, }; - let mut recv = manager - .init_audio( - default_device, - config, - AudioMsgConfig { - playback_position: true, - ..Default::default() - }, - 20, - ) - .unwrap(); + let mut recv = manager.init_audio(default_device, config).unwrap(); manager.send_worker_msg(ToWorkerMsg::Playback(PlaybackSettings::default())); std::thread::sleep(Duration::from_secs(5)); manager.deinit_audio(); - // while let Ok(event) = recv.try_next() { - // println!("{event:?}"); - // } + println!("{:?}", recv.read()) } diff --git a/tracker-engine/src/audio_processing/playback.rs b/tracker-engine/src/audio_processing/playback.rs index 2c15cd9..2a91d30 100644 --- a/tracker-engine/src/audio_processing/playback.rs +++ b/tracker-engine/src/audio_processing/playback.rs @@ -6,12 +6,14 @@ use crate::{ project::song::Song, }; -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// pub struct PlaybackPosition { -// pub order: usize, -// pub pattern: usize, -// pub row: u16, -// } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PlaybackStatus { + position: PlaybackPosition, + // which sample is playing, + // which how far along is each sample + // which channel is playing + // ... +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlaybackPosition { @@ -122,8 +124,11 @@ impl<'sample, const GC: bool> PlaybackState<'sample, GC> { (samplerate * 10) / u32::from(tempo) } - pub fn get_position(&self) -> PlaybackPosition { - self.position + pub fn get_status(&self) -> PlaybackStatus { + // maybe if it gets more fields compute them while playing back and just copy out here + PlaybackStatus { + position: self.position, + } } pub fn set_samplerate(&mut self, samplerate: u32) { @@ -239,10 +244,6 @@ impl PlaybackIter<'_, '_, '_, INTERPOLA } } } - - pub fn get_position(&self) -> PlaybackPosition { - self.state.get_position() - } } /// While the Code is completely identical the types and functions are different. diff --git a/tracker-engine/src/live_audio.rs b/tracker-engine/src/live_audio.rs index 64c6da8..faff9ab 100644 --- a/tracker-engine/src/live_audio.rs +++ b/tracker-engine/src/live_audio.rs @@ -1,25 +1,27 @@ use std::fmt::Debug; use std::ops::{AddAssign, IndexMut}; -use crate::audio_processing::playback::PlaybackState; +use crate::audio_processing::playback::{PlaybackState, PlaybackStatus}; use crate::audio_processing::sample::Interpolation; use crate::audio_processing::sample::SamplePlayer; use crate::audio_processing::Frame; -use crate::manager::{AudioMsgConfig, FromWorkerMsg, OutputConfig, PlaybackSettings}; +use crate::manager::{OutputConfig, PlaybackSettings}; use crate::project::note_event::NoteEvent; use crate::project::song::Song; use cpal::{Sample, SampleFormat}; use simple_left_right::Reader; +pub type LiveAudioStatus = (Option, cpal::OutputStreamTimestamp); + pub(crate) struct LiveAudio { song: Reader>, playback_state: Option>, live_note: Option>, - // replace with something explicitly realtime safe. I think std mpsc does syscalls to sleep and wake the thread manager: rtrb::Consumer, - audio_msg_config: AudioMsgConfig, - to_app: rtrb::Producer, + // gets created in the first callback. could maybe do with an MaybeUninit + state_sender: triple_buffer::Input>, config: OutputConfig, + buffer: Box<[Frame]>, } @@ -30,8 +32,7 @@ impl LiveAudio { pub fn new( song: Reader>, manager: rtrb::Consumer, - audio_msg_config: AudioMsgConfig, - to_app: rtrb::Producer, + state_sender: triple_buffer::Input>, config: OutputConfig, ) -> Self { Self { @@ -39,13 +40,19 @@ impl LiveAudio { playback_state: None, live_note: None, manager, - audio_msg_config, - to_app, + state_sender, config, buffer: vec![Frame::default(); config.buffer_size.try_into().unwrap()].into(), } } + fn send_state(&mut self, info: &cpal::OutputCallbackInfo) { + self.state_sender.write(Some(( + self.playback_state.as_ref().map(|s| s.get_status()), + info.timestamp(), + ))); + } + #[inline] /// returns true if work was done fn fill_internal_buffer(&mut self) -> bool { @@ -97,19 +104,12 @@ impl LiveAudio { // process song playback if let Some(playback) = &mut self.playback_state { - let old_position = playback.get_position(); let playback_iter = playback.iter::<{ Self::INTERPOLATION }>(&song); self.buffer .iter_mut() .zip(playback_iter) .for_each(|(buf, frame)| buf.add_assign(frame)); - if self.audio_msg_config.playback_position && old_position != playback.get_position() { - let _ = self.to_app.push(FromWorkerMsg::CurrentPlaybackPosition( - playback.get_position(), - )); - } - if playback.is_done() { self.playback_state = None; } @@ -172,21 +172,16 @@ impl LiveAudio { _ => panic!("Sample Format not supported."), } - if self.audio_msg_config.buffer_finished { - let _ = self - .to_app - .push(FromWorkerMsg::BufferFinished(info.timestamp())); - } + self.send_state(info); } } // unsure wether i want to use this or untyped_callback // also relevant when cpal gets made into a generic that maybe this gets useful - #[expect(dead_code)] pub fn get_typed_callback>( mut self, ) -> impl FnMut(&mut [S], &cpal::OutputCallbackInfo) { - move |data, _info| { + move |data, info| { assert_eq!( data.len(), usize::try_from(self.config.buffer_size).unwrap() @@ -196,6 +191,7 @@ impl LiveAudio { if self.fill_internal_buffer() { self.fill_from_internal(data); } + self.send_state(info); } } } diff --git a/tracker-engine/src/manager.rs b/tracker-engine/src/manager.rs index fcdcfb4..9dbab03 100644 --- a/tracker-engine/src/manager.rs +++ b/tracker-engine/src/manager.rs @@ -1,6 +1,4 @@ -use std::{ - fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration -}; +use std::{fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration}; #[cfg(feature = "async")] use std::ops::ControlFlow; @@ -11,7 +9,7 @@ use simple_left_right::{WriteGuard, Writer}; use crate::{ audio_processing::playback::PlaybackPosition, - live_audio::{LiveAudio, ToWorkerMsg}, + live_audio::{LiveAudio, LiveAudioStatus, ToWorkerMsg}, project::song::{Song, SongOperation, ValidOperation}, }; @@ -41,17 +39,17 @@ fn spin(mut f: impl FnMut() -> bool, time: Duration) { #[cfg(feature = "async")] async fn async_spin(mut f: impl FnMut() -> bool, time: Duration) { let backoff = crossbeam_utils::Backoff::new(); - loop { - if f() { - return; - } + loop { + if f() { + return; + } - if backoff.is_completed() { - async_io::Timer::after(time).await; - } else { - backoff.snooze(); - } + if backoff.is_completed() { + async_io::Timer::after(time).await; + } else { + backoff.snooze(); } + } } impl ManageCollector { @@ -78,7 +76,7 @@ impl ManageCollector { #[cfg(feature = "async")] ManageCollector::External(channel, _) => { _ = channel.send_blocking(frees); - }, + } } } @@ -88,7 +86,7 @@ impl ManageCollector { ManageCollector::Internal(_, num) => *num += frees, ManageCollector::External(channel, _) => { _ = channel.send(frees).await; - }, + } } } } @@ -107,7 +105,11 @@ pub struct CollectGarbage { #[cfg(feature = "async")] impl CollectGarbage { - fn new(collector: ManuallyDrop, channel: async_channel::Receiver, to_be_freed: usize) -> Self { + fn new( + collector: ManuallyDrop, + channel: async_channel::Receiver, + to_be_freed: usize, + ) -> Self { Self { collector, channel: Some(channel), @@ -200,7 +202,7 @@ impl AudioManager { spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP); SongEdit { song: self.song.try_lock().unwrap(), - gc: &mut self.gc + gc: &mut self.gc, } } @@ -222,17 +224,14 @@ impl AudioManager { &mut self, device: cpal::Device, config: OutputConfig, - audio_msg_config: AudioMsgConfig, - msg_buffer_size: usize, - ) -> Result, cpal::BuildStreamError> { + ) -> Result>, cpal::BuildStreamError> { const TO_WORKER_CAPACITY: usize = 5; - let from_worker = rtrb::RingBuffer::new(msg_buffer_size); + let from_worker = triple_buffer::triple_buffer(&None); let to_worker = rtrb::RingBuffer::new(TO_WORKER_CAPACITY); let reader = self.song.build_reader().unwrap(); - let audio_worker = - LiveAudio::new(reader, to_worker.1, audio_msg_config, from_worker.0, config); + let audio_worker = LiveAudio::new(reader, to_worker.1, from_worker.0, config); let stream = device.build_output_stream_raw( &config.into(), @@ -309,7 +308,7 @@ impl AudioManager { async_spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP).await; SongEdit { song: self.song.try_lock().unwrap(), - gc: &mut self.gc + gc: &mut self.gc, } } @@ -331,7 +330,11 @@ impl AudioManager { /// makes the garbage collector internal again. pub fn insert_garbage_collector(&mut self, gc: CollectGarbage) { - let CollectGarbage {collector, to_be_freed, channel: _} = gc; + let CollectGarbage { + collector, + to_be_freed, + channel: _, + } = gc; self.gc = ManageCollector::Internal(collector, to_be_freed); } } @@ -351,7 +354,8 @@ impl Drop for AudioManager { self.deinit_audio(); let mut song = self.edit_song(); for i in 0..Song::::MAX_SAMPLES { - song.apply_operation(SongOperation::RemoveSample(i)).unwrap(); + song.apply_operation(SongOperation::RemoveSample(i)) + .unwrap(); } song.finish(); // lock it once more to ensure that the changes were propagated @@ -440,12 +444,6 @@ impl TryFrom for OutputConfig { } } -#[derive(Default, Debug, Clone, Copy)] -pub struct AudioMsgConfig { - pub buffer_finished: bool, - pub playback_position: bool, -} - #[derive(Debug, Clone, Copy)] pub enum PlaybackSettings { Pattern { idx: usize, should_loop: bool },