use core::f32; use core::ops::{AddAssign, IndexMut}; use crate::audio_processing::playback::{PlaybackState, PlaybackStatus}; use crate::audio_processing::sample::SamplePlayer; use crate::audio_processing::{Frame, Interpolation}; use crate::project::song::Song; use crate::sample::Sample; use crate::{OutputConfig, ToWorkerMsg}; use alloc::boxed::Box; use alloc::vec; use simple_left_right::Reader; pub struct LiveAudio { song: Reader, playback_state: Option, live_note: Option, manager: rtrb::Consumer, state_sender: rt_write_lock::Writer<(Option, StreamData)>, config: OutputConfig, buffer: Box<[Frame]>, } impl LiveAudio { /// Not realtime safe. pub(crate) fn new( song: Reader, manager: rtrb::Consumer, state_sender: rt_write_lock::Writer<(Option, S)>, config: OutputConfig, ) -> Self { Self { song, playback_state: None, live_note: None, manager, state_sender, config, buffer: vec![Frame::default(); usize::try_from(config.buffer_size).unwrap() * 2].into(), } } #[cfg_attr(feature = "rtsan", sanitize(realtime = "nonblocking"))] fn send_state(&mut self, stream_data: S) { let playback_state = self.playback_state.as_ref().map(|s| s.get_status()); let mut write_guard = self.state_sender.write(); // make this more granular once the state includes AudioData or other allocated data *write_guard = (playback_state, stream_data); } #[cfg_attr(feature = "rtsan", sanitize(realtime = "nonblocking"))] /// returns true if work was done fn fill_internal_buffer(&mut self, len: usize) -> bool { // the output buffer should be smaller than the internal buffer let buffer = &mut self.buffer[..len]; let song = self.song.lock(); // process manager events while let Ok(event) = self.manager.pop() { match event { ToWorkerMsg::StopPlayback => self.playback_state = None, ToWorkerMsg::Playback(settings) => { self.playback_state = PlaybackState::new(&song, self.config.sample_rate, settings); } ToWorkerMsg::PlayEvent(note) => { if let Some(sample) = &song.samples[usize::from(note.sample_instr)] { let sample_player = SamplePlayer::new( Sample::clone(&sample.1), sample.0, // this at some point was divided by two, if i ever figure out why, maybe put it back self.config.sample_rate, note.note, ); self.live_note = Some(sample_player); } } ToWorkerMsg::StopLiveNote => self.live_note = None, ToWorkerMsg::SetInterpolation(i) => self.config.interpolation = i, } } if self.live_note.is_none() && self.playback_state.is_none() { // no processing todo return false; } // clear buffer from past run // only happens if there is work todo buffer.fill(Frame::default()); // process live_note if let Some(live_note) = &mut self.live_note { fn process_note( buffer: &mut [Frame], note: &mut SamplePlayer, ) { // constant power panning leads to a lower volume. As the live_note isn't channel panned // increase the volume. If i change the pan law i use i need to change this here. // // This is sin(PI/4) or cos(PI/4). // TODO: sin or cos const replace this #[expect(clippy::excessive_precision)] const CENTER_VOL: f32 = 0.707106769084930419921875; buffer .iter_mut() .zip(note.iter::<{ INTERPOLATION }>()) .for_each(|(buf, note)| buf.add_assign(note * CENTER_VOL)); } match self.config.interpolation { Interpolation::Nearest => { process_note::<{ Interpolation::Nearest as u8 }>(buffer, live_note) } Interpolation::Linear => { process_note::<{ Interpolation::Linear as u8 }>(buffer, live_note) } Interpolation::Quadratic => { process_note::<{ Interpolation::Quadratic as u8 }>(buffer, live_note) } } if live_note.check_position().is_break() { self.live_note = None; } } // process song playback if let Some(playback) = &mut self.playback_state { fn process_playback( buffer: &mut [Frame], playback: &mut PlaybackState, song: &Song, ) { buffer .iter_mut() .zip(playback.iter::<{ INTERPOLATION }>(song)) .for_each(|(buf, note)| buf.add_assign(note)); } match self.config.interpolation { Interpolation::Nearest => { process_playback::<{ Interpolation::Nearest as u8 }>(buffer, playback, &song) } Interpolation::Linear => { process_playback::<{ Interpolation::Linear as u8 }>(buffer, playback, &song) } Interpolation::Quadratic => { process_playback::<{ Interpolation::Quadratic as u8 }>(buffer, playback, &song) } } if playback.is_done() { self.playback_state = None; } } true } pub fn render(&mut self, required_len: usize, stream_data: S) -> Option<&[Frame]> { assert!(required_len <= self.buffer.len()); let worked = self.fill_internal_buffer(required_len); self.send_state(stream_data); if worked { Some(&self.buffer[..required_len]) } else { None } } // // unsure wether i want to use this or untyped_callback // // also relevant when cpal gets made into a generic that maybe this gets useful // pub fn get_typed(mut self) -> impl FnMut(usize, S) -> Option<&[Frame]> { // move |required_len, stream_data| { // assert!(self.buffer.len() > required_len); // self.send_state(stream_data); // if self.fill_internal_buffer(required_len) { // return Some(&self.buffer[..required_len]); // } else { // None // } // } // } } // only used for testing // if not testing is unused #[allow(dead_code)] fn sine(output: &mut [[f32; 2]], sample_rate: f32) { let mut sample_clock = 0f32; for frame in output { sample_clock = (sample_clock + 1.) % sample_rate; let value = (sample_clock * 440. * 2. * f32::consts::PI / sample_rate).sin(); *frame.index_mut(0) = value; *frame.index_mut(1) = value; } }