diff --git a/Cargo.lock b/Cargo.lock index 668bf06..9c8c588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,12 +116,6 @@ dependencies = [ "windows", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "dasp" version = "0.11.0" @@ -576,6 +570,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rt-write-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ce7f6e4b55e98feea9066f403ac0866c1d16d602d7522367151771d2800a15" + [[package]] name = "rtrb" version = "0.3.2" @@ -642,9 +642,9 @@ dependencies = [ [[package]] name = "simple-left-right" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72462f680e6aa61b8a5b31a0c98c2d0f1537d9cebab68fbf42a795e3f345b64d" +checksum = "a3a8aae2755857e854bf0aaa2024e240c39c75228789afe556f552a52dbb9d74" [[package]] name = "syn" @@ -709,24 +709,15 @@ dependencies = [ [[package]] name = "torque-tracker-engine" -version = "0.1.0" +version = "0.2.0" dependencies = [ "cpal", "dasp", "hound", + "rt-write-lock", "rtrb", "rtsan-standalone", "simple-left-right", - "triple_buffer", -] - -[[package]] -name = "triple_buffer" -version = "8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b" -dependencies = [ - "crossbeam-utils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 98225b0..0ad8434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "torque-tracker-engine" -version = "0.1.0" -edition = "2021" +version = "0.2.0" +edition = "2024" authors = ["Lucas Baumann"] rust-version = "1.87" license = "MPL-2.0" @@ -14,8 +14,9 @@ repository = "https://tangled.sh/did:plc:54jgbo4psy24qu2bk4njtpc4/torque-tracker dasp = { version = "0.11.0", default-features = false, features = ["std"] } rtrb = "0.3.1" rtsan-standalone = "0.2.0" -simple-left-right = "0.2.1" -triple_buffer = "8.0.0" +simple-left-right = "0.2.3" +# triple_buffer = "8.0.0" +rt-write-lock = "0.1.0" # assert_no_alloc [dev-dependencies] @@ -24,3 +25,6 @@ cpal = "0.16.0" [lints.rust] elided_lifetimes_in_paths = "warn" + +[lints.clippy] +uninlined_format_args = "allow" diff --git a/examples/file_load.rs b/examples/file_load.rs new file mode 100644 index 0000000..4fba477 --- /dev/null +++ b/examples/file_load.rs @@ -0,0 +1,8 @@ +use torque_tracker_engine::file; + +fn main() { + let file = include_bytes!("../test-files/test-1.it"); + let mut reader = std::io::Cursor::new(file); + let song = file::parse_song(&mut reader).unwrap(); + println!("{song:?}"); +} diff --git a/examples/live_note.rs b/examples/live_note.rs index 844560e..041ea55 100644 --- a/examples/live_note.rs +++ b/examples/live_note.rs @@ -5,8 +5,9 @@ use std::{ use cpal::traits::{DeviceTrait, HostTrait}; use torque_tracker_engine::{ + AudioManager, OutputConfig, ToWorkerMsg, + audio_processing::Interpolation, file::impulse_format::sample::VibratoWave, - manager::{AudioManager, OutputConfig, ToWorkerMsg}, project::{ event_command::NoteCommand, note_event::{Note, NoteEvent, VolumeEffect}, @@ -52,32 +53,30 @@ fn main() { buffer_size: 2048, channel_count: NonZeroU16::new(2).unwrap(), sample_rate: NonZero::new(default_config.sample_rate().0).unwrap(), + interpolation: Interpolation::Quadratic, }; - let mut audio_callback = manager.get_callback::(config); + let (mut audio_callback, _, _, mut send) = manager.get_callback::(config, ()); let stream = default_device .build_output_stream( &default_config.config(), - move |data, _| audio_callback(data), + move |data, _| audio_callback(data, ()), |e| eprintln!("{e:?}"), None, ) .unwrap(); let note_event = NoteEvent { - note: Note::new(70).unwrap(), + note: Note::new(30).unwrap(), sample_instr: 1, vol: VolumeEffect::None, command: NoteCommand::None, }; - manager - .try_msg_worker(ToWorkerMsg::PlayEvent(note_event)) + send.try_msg_worker(ToWorkerMsg::PlayEvent(note_event)) .unwrap(); std::thread::sleep(Duration::from_secs(1)); - manager - .try_msg_worker(ToWorkerMsg::PlayEvent(note_event)) + send.try_msg_worker(ToWorkerMsg::PlayEvent(note_event)) .unwrap(); std::thread::sleep(Duration::from_secs(3)); drop(stream); - manager.stream_closed(); } diff --git a/examples/pattern_playback.rs b/examples/pattern_playback.rs index c58057a..c55b2c0 100644 --- a/examples/pattern_playback.rs +++ b/examples/pattern_playback.rs @@ -5,8 +5,9 @@ use std::{ use cpal::traits::{DeviceTrait, HostTrait}; use torque_tracker_engine::{ + AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, + audio_processing::Interpolation, file::impulse_format::{header::PatternOrder, sample::VibratoWave}, - manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, project::{ event_command::NoteCommand, note_event::{Note, NoteEvent, VolumeEffect}, @@ -71,24 +72,25 @@ fn main() { buffer_size: 1024, channel_count: NonZeroU16::new(2).unwrap(), sample_rate: NonZero::new(default_config.sample_rate().0).unwrap(), + interpolation: Interpolation::Linear, }; - let mut callback = manager.get_callback::(config); - let stream = default_device + let (mut callback, _, mut status, mut send) = manager.get_callback::(config, ()); + let _stream = default_device .build_output_stream( &default_config.config(), - move |data, _| callback(data), + move |data, _| callback(data, ()), |e| eprintln!("{e:?}"), None, ) .unwrap(); - manager - .try_msg_worker(ToWorkerMsg::Playback(PlaybackSettings::default())) - .unwrap(); + send.try_msg_worker(ToWorkerMsg::Playback(PlaybackSettings::Pattern { + idx: 0, + should_loop: true, + })) + .unwrap(); std::thread::sleep(Duration::from_secs(5)); - println!("{:?}", manager.playback_status()); - drop(stream); - manager.stream_closed(); + println!("{:?}", *(status.try_get().unwrap())); } diff --git a/examples/render.rs b/examples/render.rs index e4ca5be..4d5e207 100644 --- a/examples/render.rs +++ b/examples/render.rs @@ -1,7 +1,9 @@ +use std::num::NonZero; + use torque_tracker_engine::{ - audio_processing::playback::PlaybackState, - file::impulse_format::header::PatternOrder, - manager::PlaybackSettings, + PlaybackSettings, + audio_processing::{Interpolation, playback::PlaybackState}, + file::impulse_format::{header::PatternOrder, sample::VibratoWave}, project::{ event_command::NoteCommand, note_event::{Note, NoteEvent, VolumeEffect}, @@ -22,8 +24,15 @@ fn main() { let sample = Sample::new_mono(sample_data); let meta = SampleMetaData { - sample_rate: spec.sample_rate, - ..Default::default() + sample_rate: NonZero::new(spec.sample_rate).unwrap(), + default_volume: 200, + global_volume: 200, + default_pan: None, + vibrato_speed: 0, + vibrato_depth: 0, + vibrato_rate: 0, + vibrato_waveform: VibratoWave::Sine, + base_note: Note::new(20).unwrap(), }; let mut song: Song = Song::default(); @@ -48,8 +57,16 @@ fn main() { }, ); - let mut playback = PlaybackState::new(&song, 44100, PlaybackSettings::default()).unwrap(); - let iter = playback.iter::<0>(&song); + let mut playback = PlaybackState::new( + &song, + NonZero::new(44100).unwrap(), + PlaybackSettings::Order { + idx: 0, + should_loop: true, + }, + ) + .unwrap(); + let iter = playback.iter::<{ Interpolation::Nearest as u8 }>(&song); for _ in iter.take(50) { // dbg!(frame); } diff --git a/src/audio_processing/mod.rs b/src/audio_processing/mod.rs index 738f3d2..99874df 100644 --- a/src/audio_processing/mod.rs +++ b/src/audio_processing/mod.rs @@ -1,5 +1,8 @@ use core::slice; -use std::{array, ops::IndexMut}; +use std::{ + array, + ops::{Div, DivAssign, IndexMut}, +}; use dasp::sample::ToSample; @@ -7,6 +10,8 @@ pub(crate) mod instrument; pub mod playback; pub(crate) mod sample; +pub use sample::Interpolation; + #[repr(transparent)] #[derive(Clone, Copy, Default, Debug, PartialEq)] pub struct Frame([f32; 2]); @@ -54,6 +59,30 @@ impl std::ops::Mul for Frame { } } +impl Div for Frame { + type Output = Frame; + + fn div(self, rhs: f32) -> Self::Output { + Self([self.0[0] / rhs, self.0[1] / rhs]) + } +} + +impl DivAssign for Frame { + fn div_assign(&mut self, rhs: f32) { + *self.0.index_mut(0) *= rhs; + *self.0.index_mut(1) *= rhs; + } +} + +impl crate::sample::ProcessingFrame for Frame { + fn mul_add(self, mul: f32, add: Self) -> Self { + Self([ + f32::mul_add(self.0[0], mul, add.0[0]), + f32::mul_add(self.0[1], mul, add.0[1]), + ]) + } +} + impl std::iter::Sum for Frame { fn sum>(iter: I) -> Self { iter.reduce(|acc, x| acc + x).unwrap_or_default() diff --git a/src/audio_processing/playback.rs b/src/audio_processing/playback.rs index 9b6720e..7e73e1d 100644 --- a/src/audio_processing/playback.rs +++ b/src/audio_processing/playback.rs @@ -1,10 +1,9 @@ use std::{num::NonZero, ops::ControlFlow}; use crate::{ - audio_processing::{sample::SamplePlayer, Frame}, - channel::Pan, - manager::PlaybackSettings, - project::song::Song, + PlaybackSettings, + audio_processing::{Frame, sample::SamplePlayer}, + project::song::{Pan, Song}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -59,10 +58,10 @@ impl PlaybackPosition { } } else if self.loop_active { // the row count was reset, nothing else to do - return ControlFlow::Continue(()); + ControlFlow::Continue(()) } else { // no looping, pattern is done - return ControlFlow::Break(()); + ControlFlow::Break(()) } } else { // Pattern not done yet @@ -200,7 +199,8 @@ impl Iterator for PlaybackIter<'_, '_, INTERPOLATION> { fn next(&mut self) -> Option { fn scale_vol(vol: u8) -> f32 { - (vol as f32) / (u8::MAX as f32) + // 128 is the max for the volume in schism. maybe i don't want this + (vol as f32) / (128 as f32) } /// scale from 0..=64 to 0°..=90° in radians diff --git a/src/audio_processing/sample.rs b/src/audio_processing/sample.rs index 4e137a7..6f1c14e 100644 --- a/src/audio_processing/sample.rs +++ b/src/audio_processing/sample.rs @@ -8,9 +8,12 @@ use crate::{ use super::Frame; #[repr(u8)] +// quadratic is probably enough i can't hear it anymore +#[derive(Copy, Clone, Debug)] pub enum Interpolation { Nearest = 0, Linear = 1, + Quadratic = 2, } impl From for Interpolation { @@ -26,6 +29,7 @@ impl Interpolation { match self { Interpolation::Nearest => 1, Interpolation::Linear => 1, + Interpolation::Quadratic => 2, } } @@ -33,6 +37,7 @@ impl Interpolation { match value { 0 => Self::Nearest, 1 => Self::Linear, + 2 => Self::Quadratic, _ => panic!(), } } @@ -115,8 +120,8 @@ impl SamplePlayer { self.position.0 += floor as usize; } - pub fn iter(&mut self) -> SampleIter<'_, INTERPOLATION> { - SampleIter { inner: self } + pub fn iter(&mut self) -> impl Iterator { + std::iter::from_fn(|| self.next::()) } pub fn next(&mut self) -> Option { @@ -130,6 +135,7 @@ impl SamplePlayer { let out = match interpolation { Interpolation::Nearest => self.compute_nearest(), Interpolation::Linear => self.compute_linear(), + Interpolation::Quadratic => self.compute_quadratic(), }; self.step(); @@ -140,15 +146,14 @@ impl SamplePlayer { // There are two types that implement ProcessingFrame: f32 and Frame, so stereo and mono audio data. // the compiler will monomorphize this function to both versions and depending on wether that sample is mono // or stereo the correct version will be called. - struct Linear(f32); + struct Linear; impl ProcessingFunction<2, S> for Linear { - fn process(self, data: &[S; 2]) -> S { + fn process(pos: f32, data: &[S; 2]) -> S { let diff = data[1] - data[0]; - (diff * self.0) + data[0] + (diff * pos) + data[0] } } - self.sample - .compute(self.position.0, Linear(self.position.1)) + self.sample.compute::<2, Linear>(self.position) } fn compute_nearest(&mut self) -> Frame { @@ -160,16 +165,42 @@ impl SamplePlayer { self.sample.index(load_idx) } -} - -pub struct SampleIter<'player, const INTERPOLATION: u8> { - inner: &'player mut SamplePlayer, -} -impl Iterator for SampleIter<'_, INTERPOLATION> { - type Item = Frame; + // need to hear it on a better system. With standard output i can't hear a difference. + // maybe also look at the waveforms + fn compute_quadratic(&mut self) -> Frame { + struct Quadratic; + impl ProcessingFunction<3, S> for Quadratic { + fn process(pos: f32, data: &[S; 3]) -> S { + // let y0_half = data[0] / 2.; + // let y2_half = data[2] / 2.; + // let y1 = data[1]; + + // (y0_half - y1 + y2_half) * pos * pos + (y2_half - y0_half) * pos + y1 + // (data[0] / 2 - data[1] + data[2] / 2) * pos * pos + (data[2] / 2- data[0] / 2) * pos + y1 + // https://herbie.uwplse.org/demo/e824f96dd380ac5390d6cb0362398b0e9defed73.0cc3b6492c83efca5bd11399e0830e7873c749d9/graph.html + // alternative 1, accuracy 100%, 1.3x speed + // using fused multiply add can be faster and more accurate + // S::mul_add( + // (data[2] - data[0]) / 2. - data[1], + // pos * pos, + // S::mul_add((data[2] - data[0]) * pos, 0.5, data[1]), + // ) + // + // alternative 2, accuracy 100%, 1.5x speed + // even alternative 3 with 98.5% accuracy sounds really bad + S::mul_add( + S::mul_add( + (data[2] + data[0]) * 0.5 - data[1], + pos, + (data[0] - data[2]) * -0.5, + ), + pos, + data[1], + ) + } + } - fn next(&mut self) -> Option { - self.inner.next::() + self.sample.compute::<3, Quadratic>(self.position) } } diff --git a/src/channel.rs b/src/channel.rs deleted file mode 100644 index e44b476..0000000 --- a/src/channel.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[derive(Debug, Clone, Copy)] -pub enum Pan { - /// Value ranges from 0 to 64, with 32 being center - Value(u8), - Surround, - Disabled, -} - -impl Default for Pan { - fn default() -> Self { - Self::Value(32) - } -} - -impl TryFrom for Pan { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 100 => Ok(Self::Surround), - 128 => Ok(Self::Disabled), - 0..=64 => Ok(Self::Value(value)), - _ => Err(value), - } - } -} diff --git a/src/file/err.rs b/src/file/err.rs index 7a296be..a601e14 100644 --- a/src/file/err.rs +++ b/src/file/err.rs @@ -7,8 +7,6 @@ pub enum LoadErr { CantReadFile, Invalid, BufferTooShort, - /// A defect handler function returned ControlFlow::Break - Cancelled, IO(io::Error), } @@ -30,21 +28,19 @@ impl Display for LoadErr { impl Error for LoadErr {} -/// TODO: https://users.rust-lang.org/t/validation-monad/117894/6 -/// -/// this is a way cleaner and nicer approach. Provice a lot of data about the Error, like position in file, expected value, received value, ... -/// maybe even allow to cancel the parsing via ControlFlow<(), ()> -/// -/// load was partially successful. These are the defects that are in the now loaded project -#[derive(Debug, Clone, Copy)] -#[non_exhaustive] -pub enum LoadDefect { - /// deletes the effect - UnknownEffect, - /// replaced with empty text - InvalidText, - /// tries to replace with a sane default value - OutOfBoundsValue, - /// skips loading of the pointed to value - OutOfBoundsPtr, -} +// This should be a different enum per part of file. Either return a list of these or take a callback +// TODO: https://users.rust-lang.org/t/validation-monad/117894/6 +// +// load was partially successful. These are the defects that are in the now loaded project +// #[derive(Debug, Clone, Copy)] +// #[non_exhaustive] +// pub enum LoadDefect { +// /// deletes the effect +// UnknownEffect, +// /// replaced with empty text +// InvalidText, +// /// tries to replace with a sane default value +// OutOfBoundsValue, +// /// skips loading of the pointed to value +// OutOfBoundsPtr, +// } diff --git a/src/file/impulse_format/header.rs b/src/file/impulse_format/header.rs index c7f4961..70c324d 100644 --- a/src/file/impulse_format/header.rs +++ b/src/file/impulse_format/header.rs @@ -1,19 +1,11 @@ -use crate::file::err::{self, LoadDefect}; +use crate::{ + file::err, + project::song::{Pan, PatternOrder}, +}; use std::{io::Read, num::NonZeroU32}; -use crate::channel::Pan; - use crate::file::InFilePtr; -/// maybe completely wrong -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum PatternOrder { - Number(u8), - #[default] - EndOfSong, - SkipOrder, -} - impl TryFrom for PatternOrder { type Error = u8; @@ -27,6 +19,19 @@ impl TryFrom for PatternOrder { } } +impl TryFrom for Pan { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + 100 => Ok(Self::Surround), + 128 => Ok(Self::Disabled), + 0..=64 => Ok(Self::Value(value)), + _ => Err(value), + } + } +} + #[derive(Debug)] pub struct ImpulseHeader { pub song_name: String, @@ -67,12 +72,8 @@ impl ImpulseHeader { /// Reader position needs to be at the beginning of the Header. /// /// Header is stored at the beginning of the File. length isn't constant, but at least 192 bytes - /// when unable to load specific parts the function tries its best and communicates the failures in the BitFlags return value. /// For some problems it wouldn't make sense to return an incomplete Header as so much would be missing. In those cases an Err is returned - pub fn parse( - reader: &mut R, - defect_handler: &mut H, - ) -> Result { + pub fn parse(reader: &mut R) -> Result { let base = { let mut base = [0; Self::BASE_SIZE]; reader.read_exact(&mut base)?; @@ -88,7 +89,7 @@ impl ImpulseHeader { let str = base[0x4..=0x1D].split(|b| *b == 0).next().unwrap().to_vec(); let str = String::from_utf8(str); if str.is_err() { - defect_handler(LoadDefect::InvalidText) + // defect_handler(LoadDefect::InvalidText) } str.unwrap_or_default() }; @@ -107,14 +108,14 @@ impl ImpulseHeader { let global_volume = if base[0x30] <= 128 { base[0x30] } else { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 64 }; let mix_volume = if base[0x31] <= 128 { base[0x31] } else { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 64 }; @@ -128,13 +129,14 @@ impl ImpulseHeader { // can unwrap here, because the length is already checked at the beginning let pan_vals: [u8; 64] = base[0x40..0x80].try_into().unwrap(); - let channel_pan: [Pan; 64] = pan_vals.map(|pan| match Pan::try_from(pan) { - Ok(pan) => pan, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - Pan::default() - } - }); + // let channel_pan: [Pan; 64] = pan_vals.map(|pan| match Pan::try_from(pan) { + // Ok(pan) => pan, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // Pan::default() + // } + // }); + let channel_pan: [Pan; 64] = pan_vals.map(|pan| Pan::try_from(pan).unwrap_or_default()); let channel_volume: [u8; 64] = { // can unwrap here, because the length is already checked at the beginning @@ -142,7 +144,7 @@ impl ImpulseHeader { vols.iter_mut().for_each(|vol| { if *vol > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); *vol = 64 } }); @@ -156,7 +158,7 @@ impl ImpulseHeader { .map(|order| match PatternOrder::try_from(*order) { Ok(pat_order) => pat_order, Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); PatternOrder::SkipOrder } }) @@ -170,7 +172,7 @@ impl ImpulseHeader { .map(|chunk| { let value = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); if value <= Self::BASE_SIZE as u32 { - defect_handler(LoadDefect::OutOfBoundsPtr); + // defect_handler(LoadDefect::OutOfBoundsPtr); None } else { // value is larger than Self::BASE_SIZE, so also larger than 0 @@ -187,7 +189,7 @@ impl ImpulseHeader { .map(|chunk| { let value = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); if value <= Self::BASE_SIZE as u32 { - defect_handler(LoadDefect::OutOfBoundsPtr); + // defect_handler(LoadDefect::OutOfBoundsPtr); None } else { // value is larger than Self::BASE_SIZE, so also larger than 0 @@ -207,7 +209,7 @@ impl ImpulseHeader { // None is a valid value and assumed to be an empty pattern None } else if value <= Self::BASE_SIZE as u32 { - defect_handler(LoadDefect::OutOfBoundsPtr); + // defect_handler(LoadDefect::OutOfBoundsPtr); None } else { // value is larger than Self::BASE_SIZE, so also larger than 0 diff --git a/src/file/impulse_format/instrument.rs b/src/file/impulse_format/instrument.rs index 10c4e8e..93b735b 100644 --- a/src/file/impulse_format/instrument.rs +++ b/src/file/impulse_format/instrument.rs @@ -1,7 +1,8 @@ +#![allow(dead_code)] // not nearly finished + use std::array; use crate::file::err; -use crate::file::err::LoadDefect; #[derive(Debug, Default)] pub enum NewNoteAction { @@ -100,10 +101,7 @@ pub struct ImpulseInstrument { impl ImpulseInstrument { const SIZE: usize = 554; - pub fn parse( - buf: &[u8; Self::SIZE], - defect_handler: &mut H, - ) -> Result { + pub fn parse(buf: &[u8; Self::SIZE]) -> Result { if !buf.starts_with(b"IMPI") { return Err(err::LoadErr::Invalid); } @@ -114,32 +112,35 @@ impl ImpulseInstrument { if buf[10] != 0 { return Err(err::LoadErr::Invalid); } - let new_note_action = match NewNoteAction::try_from(buf[0x11]) { - Ok(nna) => nna, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - NewNoteAction::default() - } - }; - let duplicate_check_type = match DuplicateCheckType::try_from(buf[0x12]) { - Ok(dct) => dct, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - DuplicateCheckType::default() - } - }; - let duplicate_check_action = match DuplicateCheckAction::try_from(buf[0x13]) { - Ok(dca) => dca, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - DuplicateCheckAction::default() - } - }; + // let new_note_action = match NewNoteAction::try_from(buf[0x11]) { + // Ok(nna) => nna, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // NewNoteAction::default() + // } + // }; + let new_note_action = NewNoteAction::try_from(buf[0x11]).unwrap_or_default(); + // let duplicate_check_type = match DuplicateCheckType::try_from(buf[0x12]) { + // Ok(dct) => dct, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // DuplicateCheckType::default() + // } + // }; + let duplicate_check_type = DuplicateCheckType::try_from(buf[0x12]).unwrap_or_default(); + // let duplicate_check_action = match DuplicateCheckAction::try_from(buf[0x13]) { + // Ok(dca) => dca, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // DuplicateCheckAction::default() + // } + // }; + let duplicate_check_action = DuplicateCheckAction::try_from(buf[0x13]).unwrap_or_default(); let fade_out = u16::from_le_bytes([buf[0x14], buf[0x15]]); let pitch_pan_seperation = { let tmp = i8::from_le_bytes([buf[0x16]]); if !(-32..=32).contains(&tmp) { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 0 } else { tmp @@ -148,20 +149,20 @@ impl ImpulseInstrument { let pitch_pan_center = if buf[0x17] <= 119 { buf[0x17] } else { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 59 }; let global_volume = if buf[0x18] <= 128 { buf[0x18] } else { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 64 }; let default_pan = if buf[0x19] == 128 { None } else if buf[0x19] > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); Some(32) } else { Some(buf[0x19]) diff --git a/src/file/impulse_format/pattern.rs b/src/file/impulse_format/pattern.rs index 1ee701c..bc4fadd 100644 --- a/src/file/impulse_format/pattern.rs +++ b/src/file/impulse_format/pattern.rs @@ -1,20 +1,15 @@ use crate::file::err; -use crate::file::err::LoadDefect; use crate::project::event_command::NoteCommand; use crate::project::note_event::{Note, NoteEvent, VolumeEffect}; use crate::project::pattern::{InPatternPosition, Pattern}; +use std::io::Read; /// reader should be buffered in some way and not do a syscall on every read call. /// /// This function does a lot of read calls -pub fn parse_pattern( - reader: &mut R, - defect_handler: &mut H, -) -> Result { +pub fn parse_pattern(reader: &mut R) -> Result { const PATTERN_HEADER_SIZE: usize = 8; - let read_start = reader.stream_position()?; - let (length, num_rows) = { let mut header = [0; PATTERN_HEADER_SIZE]; reader.read_exact(&mut header)?; @@ -42,7 +37,7 @@ pub fn parse_pattern( let mut scratch = [0; 1]; - while row_num < num_rows && reader.stream_position()? - read_start < length { + while row_num < num_rows { let channel_variable = scratch[0]; if channel_variable == 0 { @@ -67,13 +62,14 @@ pub fn parse_pattern( // Note if (maskvar & 0b00000001) != 0 { reader.read_exact(&mut scratch)?; - let note = match Note::new(scratch[0]) { - Ok(n) => n, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - Note::default() - } - }; + // let note = match Note::new(scratch[0]) { + // Ok(n) => n, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // Note::default() + // } + // }; + let note = Note::new(scratch[0]).unwrap_or_default(); event.note = note; last_event[channel_id].note = note; @@ -91,14 +87,14 @@ pub fn parse_pattern( // Volume if (maskvar & 0b00000100) != 0 { reader.read_exact(&mut scratch)?; - let vol_pan_raw = scratch[0]; - let vol_pan = match vol_pan_raw.try_into() { - Ok(v) => v, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - VolumeEffect::default() - } - }; + // let vol_pan = match vol_pan_raw.try_into() { + // Ok(v) => v, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // VolumeEffect::default() + // } + // }; + let vol_pan = VolumeEffect::try_from(scratch[0]).unwrap_or_default(); last_event[channel_id].vol = vol_pan; event.vol = vol_pan; @@ -111,13 +107,14 @@ pub fn parse_pattern( reader.read_exact(&mut scratch)?; let cmd_val = scratch[0]; - let cmd = match NoteCommand::try_from((command, cmd_val)) { - Ok(cmd) => cmd, - Err(_) => { - defect_handler(LoadDefect::OutOfBoundsValue); - NoteCommand::default() - } - }; + // let cmd = match NoteCommand::try_from((command, cmd_val)) { + // Ok(cmd) => cmd, + // Err(_) => { + // // defect_handler(LoadDefect::OutOfBoundsValue); + // NoteCommand::default() + // } + // }; + let cmd = NoteCommand::try_from((command, cmd_val)).unwrap_or_default(); last_event[channel_id].command = cmd; event.command = cmd; @@ -152,9 +149,5 @@ pub fn parse_pattern( ); } - if pattern.row_count() == row_num { - Ok(pattern) - } else { - Err(err::LoadErr::BufferTooShort) - } + Ok(pattern) } diff --git a/src/file/impulse_format/sample.rs b/src/file/impulse_format/sample.rs index d8d784c..909d597 100644 --- a/src/file/impulse_format/sample.rs +++ b/src/file/impulse_format/sample.rs @@ -1,11 +1,9 @@ // look at player/csndfile.c csf_read_sample +#![allow(dead_code)] // not nearly done use std::num::NonZeroU32; -use crate::file::{ - err::{LoadDefect, LoadErr}, - InFilePtr, -}; +use crate::file::{InFilePtr, err::LoadErr}; use super::header; @@ -173,10 +171,7 @@ pub struct ImpulseSampleHeader { impl ImpulseSampleHeader { const SIZE: usize = 80; - pub fn parse( - buf: &[u8; Self::SIZE], - defect_handler: &mut H, - ) -> Result { + pub fn parse(buf: &[u8; Self::SIZE]) -> Result { if !buf.starts_with(b"IMPS") { return Err(LoadErr::Invalid); } @@ -187,7 +182,7 @@ impl ImpulseSampleHeader { } let global_volume = if buf[0x11] > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 64 } else { buf[0x11] @@ -199,7 +194,7 @@ impl ImpulseSampleHeader { let str = buf[0x14..=0x2D].split(|b| *b == 0).next().unwrap().to_vec(); let str = String::from_utf8(str); if str.is_err() { - defect_handler(LoadDefect::InvalidText); + // defect_handler(LoadDefect::InvalidText); } str.unwrap_or_default() }; @@ -217,7 +212,7 @@ impl ImpulseSampleHeader { let c5_speed = { let speed = u32::from_le_bytes([buf[0x3C], buf[0x3D], buf[0x3E], buf[0x3F]]); if speed > 9999999 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); // no idea what is a good default here 9999999 / 2 } else { @@ -238,21 +233,21 @@ impl ImpulseSampleHeader { }; let vibrato_speed = if buf[0x4C] > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 32 } else { buf[0x4C] }; let vibrato_depth = if buf[0x4D] > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 32 } else { buf[0x4D] }; let vibrato_rate = if buf[0x4E] > 64 { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); 32 } else { buf[0x4E] @@ -261,7 +256,7 @@ impl ImpulseSampleHeader { let vibrato_type = { let wave = VibratoWave::try_from(buf[0x4F]); if wave.is_err() { - defect_handler(LoadDefect::OutOfBoundsValue); + // defect_handler(LoadDefect::OutOfBoundsValue); } wave.unwrap_or_default() }; diff --git a/src/file/mod.rs b/src/file/mod.rs index dd3b8a4..ff856d9 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -23,9 +23,7 @@ impl InFilePtr { /// R should be buffered in some way and not do a syscall on every read. /// If you ever find yourself using multiple different reader and/or handlers please open an issue on Github, i will change this to take &dyn. pub fn parse_song(reader: &mut R) -> Result { - //ignore defects - let mut defect_handler = |_| (); - let header = header::ImpulseHeader::parse(reader, &mut defect_handler)?; + let header = header::ImpulseHeader::parse(reader)?; let mut song = Song::default(); song.copy_values_from_header(&header); @@ -33,11 +31,13 @@ pub fn parse_song(reader: &mut R) -> Result(rt_write_lock::Reader<(Option, StreamData)>); + +// don't leak the dependency types +pub struct StatusRead<'a, StreamData>( + rt_write_lock::ReadGuard<'a, (Option, StreamData)>, +); + +impl Deref for StatusRead<'_, S> { + type Target = (Option, S); + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl StatusRecv { + pub fn try_get(&mut self) -> Option> { + self.0.try_read().map(|r| StatusRead(r)) + } +} + +pub struct StreamSend(rtrb::Producer); + +impl StreamSend { + #[expect(clippy::result_unit_err)] + pub fn try_msg_worker(&mut self, msg: ToWorkerMsg) -> Result<(), ()> { + match self.0.push(msg) { + Ok(_) => Ok(()), + Err(_) => Err(()), + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct Collector { + samples: Vec, +} + +impl Collector { + pub fn add_sample(&mut self, sample: Sample) { + self.samples.push(sample); + } + + fn collect(&mut self) { + self.samples.retain(|s| { + // only look at strong count as weak pointers are not used + s.strongcount() != 1 + }); + } +} + +pub struct AudioManager { + song: Writer, + gc: Collector, + buffer_time: Duration, +} + +impl AudioManager { + pub fn new(song: Song) -> Self { + let mut gc = Collector::default(); + for (_, sample) in song.samples.iter().flatten() { + gc.add_sample(sample.clone()); + } + let left_right = simple_left_right::Writer::new(song); + + Self { + song: left_right, + gc, + buffer_time: Duration::ZERO, + } + } + + /// If this returns None, waiting buffer_time should (weird threading issues aside) always be enough time + /// and it should return Some after that. + pub fn try_edit_song(&mut self) -> Option> { + self.song.try_lock().map(|song| SongEdit { + song, + gc: &mut self.gc, + }) + } + + pub fn get_song(&self) -> &Song { + self.song.read() + } + + pub fn collect_garbage(&mut self) { + self.gc.collect(); + } + + /// If the config specifies more than two channels only the first two will be filled with audio. + /// The rest get silence. + /// + /// The callback in for example Cpal provides an additional arguement, where a timestamp is give. + /// That should ba handled by wrapping this function in another callback, where this argument could + /// then be ignored or send somewhere for processing. This Sending needs to happen wait-free!! There are + /// a couple of libaries that can do this, i would recommend triple_buffer or rtrb. + /// + /// The OutputConfig has to match the config of the AudioStream that will call this. If for example the + /// buffer size is different Panics may occur. + /// + /// In my testing i noticed that when using Cpal with non-standard buffer sizes Cpal would just give + /// another buffer size. This may also lead to panics. + /// + /// ## Panics + /// + /// Will panic if there is an active stream. + pub fn get_callback< + Sample: dasp::sample::Sample + dasp::sample::FromSample, + StreamData: Clone, + >( + &mut self, + config: OutputConfig, + initial_stream_data: StreamData, + ) -> ( + impl FnMut(&mut [Sample], StreamData) + use, + Duration, + StatusRecv, + StreamSend, + ) { + const SEND_SIZE: usize = 10; + let mut from_worker = rt_write_lock::Reader::new((None, initial_stream_data)); + // let from_worker = triple_buffer::triple_buffer(&(None, initial_stream_data)); + let to_worker = rtrb::RingBuffer::new(SEND_SIZE); + let reader = self.song.build_reader().expect("another stream is active"); + + let audio_worker = LiveAudio::::new( + reader, + to_worker.1, + // doesn't panic because i just created the reader + from_worker.get_writer().unwrap(), + config, + ); + let buffer_time = + Duration::from_millis((config.buffer_size * 1000 / config.sample_rate).into()); + + self.buffer_time = buffer_time; + + ( + audio_worker.get_typed_callback(), + buffer_time, + StatusRecv(from_worker), + StreamSend(to_worker.0), + ) + } + + /// Buffer time of the last created callback. + pub fn last_buffer_time(&self) -> Duration { + self.buffer_time + } +} + +/// the changes made to the song will be made available to the playing live audio as soon as +/// this struct is dropped. +/// +/// With this you can load the full song without ever playing a half initialised state +/// when doing mulitple operations this object should be kept as it is +#[derive(Debug)] +pub struct SongEdit<'a> { + song: WriteGuard<'a, Song, ValidOperation>, + gc: &'a mut Collector, +} + +impl SongEdit<'_> { + pub fn apply_operation(&mut self, op: SongOperation) -> Result<(), SongOperation> { + let valid_operation = ValidOperation::new(op, self.gc, self.song.read())?; + self.song.apply_op(valid_operation); + Ok(()) + } + + pub fn song(&self) -> &Song { + self.song.read() + } + + /// Finish the changes and publish them to the live playing song. + /// Equivalent to std::mem::drop(SongEdit) + pub fn finish(self) {} +} + +#[derive(Debug, Clone, Copy)] +pub struct OutputConfig { + pub buffer_size: u32, + pub channel_count: NonZeroU16, + pub sample_rate: NonZero, + pub interpolation: Interpolation, +} + +#[derive(Debug, Clone, Copy)] +pub enum PlaybackSettings { + Pattern { idx: u8, should_loop: bool }, + Order { idx: u16, should_loop: bool }, +} diff --git a/src/live_audio.rs b/src/live_audio.rs index a430a6e..eae68be 100644 --- a/src/live_audio.rs +++ b/src/live_audio.rs @@ -1,35 +1,31 @@ use std::ops::{AddAssign, IndexMut}; 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::{OutputConfig, ToWorkerMsg}; +use crate::audio_processing::{Frame, Interpolation}; use crate::project::song::Song; use crate::sample::Sample; +use crate::{OutputConfig, ToWorkerMsg}; use dasp::sample::ToSample; use simple_left_right::Reader; -pub(crate) struct LiveAudio { +pub(crate) struct LiveAudio { song: Reader, playback_state: Option, live_note: Option, manager: rtrb::Consumer, - state_sender: triple_buffer::Input>, + state_sender: rt_write_lock::Writer<(Option, StreamData)>, config: OutputConfig, buffer: Box<[Frame]>, } -// should probabyl be made configurable at some point -const INTERPOLATION: u8 = Interpolation::Linear as u8; - -impl LiveAudio { +impl LiveAudio { /// Not realtime safe. pub fn new( song: Reader, manager: rtrb::Consumer, - state_sender: triple_buffer::Input>, + state_sender: rt_write_lock::Writer<(Option, S)>, config: OutputConfig, ) -> Self { Self { @@ -44,9 +40,11 @@ impl LiveAudio { } #[rtsan_standalone::nonblocking] - fn send_state(&mut self) { - self.state_sender - .write(self.playback_state.as_ref().map(|s| s.get_status())); + 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); } #[rtsan_standalone::nonblocking] @@ -78,6 +76,7 @@ impl LiveAudio { } } ToWorkerMsg::StopLiveNote => self.live_note = None, + ToWorkerMsg::SetInterpolation(i) => self.config.interpolation = i, } } if self.live_note.is_none() && self.playback_state.is_none() { @@ -91,11 +90,26 @@ impl LiveAudio { // process live_note if let Some(live_note) = &mut self.live_note { - let note_iter = live_note.iter::<{ INTERPOLATION }>(); - buffer - .iter_mut() - .zip(note_iter) - .for_each(|(buf, note)| buf.add_assign(note)); + fn process_note( + buffer: &mut [Frame], + note: &mut SamplePlayer, + ) { + buffer + .iter_mut() + .zip(note.iter::<{ INTERPOLATION }>()) + .for_each(|(buf, note)| buf.add_assign(note)); + } + 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; @@ -104,11 +118,27 @@ impl LiveAudio { // process song playback if let Some(playback) = &mut self.playback_state { - let playback_iter = playback.iter::<{ INTERPOLATION }>(&song); - buffer - .iter_mut() - .zip(playback_iter) - .for_each(|(buf, frame)| buf.add_assign(frame)); + 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; @@ -143,11 +173,11 @@ impl LiveAudio { // also relevant when cpal gets made into a generic that maybe this gets useful pub fn get_typed_callback>( mut self, - ) -> impl FnMut(&mut [Sample]) { - move |data| { + ) -> impl FnMut(&mut [Sample], S) { + move |audio_data, stream_data| { let channel_count = usize::from(self.config.channel_count.get()); - assert!(data.len().is_multiple_of(channel_count)); - let out_frames = data.len() / channel_count; + assert!(audio_data.len().is_multiple_of(channel_count)); + let out_frames = audio_data.len() / channel_count; assert!(self.buffer.len() > out_frames); // assert_eq!( // data.len(), @@ -156,9 +186,9 @@ impl LiveAudio { // ); if self.fill_internal_buffer(out_frames) { - self.fill_from_internal(data); + self.fill_from_internal(audio_data); } - self.send_state(); + self.send_state(stream_data); } // move |data, info| { // assert_eq!( diff --git a/src/manager.rs b/src/manager.rs deleted file mode 100644 index c56f521..0000000 --- a/src/manager.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::{ - fmt::Debug, - num::{NonZero, NonZeroU16}, - time::Duration, -}; - -use simple_left_right::{WriteGuard, Writer}; - -use crate::{ - audio_processing::playback::PlaybackStatus, - live_audio::LiveAudio, - project::{ - note_event::NoteEvent, - song::{Song, SongOperation, ValidOperation}, - }, - sample::Sample, -}; - -#[derive(Debug, Clone, Copy)] -pub enum ToWorkerMsg { - Playback(PlaybackSettings), - StopPlayback, - PlayEvent(NoteEvent), - StopLiveNote, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[must_use] -pub enum SendResult { - Success, - BufferFull, - AudioInactive, -} - -impl SendResult { - #[track_caller] - pub fn unwrap(self) { - match self { - SendResult::Success => (), - SendResult::BufferFull => panic!("Buffer full"), - SendResult::AudioInactive => panic!("Audio inactive"), - } - } - - pub fn is_success(self) -> bool { - self == Self::Success - } -} - -/// Communication to and from an active Stream -#[derive(Debug)] -struct ActiveStreamComms { - buffer_time: Duration, - send: rtrb::Producer, - status: triple_buffer::Output>, -} - -#[derive(Debug, Default)] -pub(crate) struct Collector { - samples: Vec, -} - -impl Collector { - pub fn add_sample(&mut self, sample: Sample) { - self.samples.push(sample); - } - - fn collect(&mut self) { - self.samples.retain(|s| { - // only look at strong count as weak pointers are not used - s.strongcount() != 1 - }); - } -} - -/// You will need to write your own spin loops. -/// For that you can and maybe should use AudioManager::buffer_time. -/// -/// The Stream API is not "Rusty" and not ergonimic to use, but Stream are often not Send, while the Manager is -/// suited well for being in a Global Mutex. This is why the Stream can't live inside the Manager. If you can -/// think of a better API i would love to replace this. -pub struct AudioManager { - song: Writer, - gc: Collector, - stream_comms: Option, -} - -impl AudioManager { - pub fn new(song: Song) -> Self { - let mut gc = Collector::default(); - for (_, sample) in song.samples.iter().flatten() { - gc.add_sample(sample.clone()); - } - let left_right = simple_left_right::Writer::new(song); - - Self { - song: left_right, - gc, - stream_comms: None, - } - } - - /// If this returns None, waiting buffer_time should (weird threading issues aside) always be enough time - /// and it should return Some after that. - pub fn try_edit_song(&mut self) -> Option> { - self.song.try_lock().map(|song| SongEdit { - song, - gc: &mut self.gc, - }) - } - - pub fn get_song(&self) -> &Song { - self.song.read() - } - - pub fn collect_garbage(&mut self) { - self.gc.collect(); - } - - pub fn try_msg_worker(&mut self, msg: ToWorkerMsg) -> SendResult { - if let Some(stream) = &mut self.stream_comms { - match stream.send.push(msg) { - Ok(_) => SendResult::Success, - Err(_) => SendResult::BufferFull, - } - } else { - SendResult::AudioInactive - } - } - - /// last playback status sent by the audio worker - pub fn playback_status(&mut self) -> Option<&Option> { - self.stream_comms.as_mut().map(|s| s.status.read()) - } - - /// Some if a stream is active. - /// Returns the approximate time it takes to process an audio buffer based on the used settings. - /// - /// Useful for implementing spin_loops on collect_garbage or for locking a SongEdit as every time a buffer is finished - /// garbage could be releases and a lock could be made available - pub fn buffer_time(&self) -> Option { - self.stream_comms.as_ref().map(|s| s.buffer_time) - } - - /// If the config specifies more than two channels only the first two will be filled with audio. - /// The rest gets silence. - /// - /// The callback in for example Cpal provides an additional arguement, where a timestamp is give. - /// That should ba handled by wrapping this function in another callback, where this argument could - /// then be ignored or send somewhere for processing. This Sending needs to happen wait-free!! There are - /// a couple of libaries that can do this, i would recommend triple_buffer. - /// - /// The OutputConfig has to match the config of the AudioStream that will call this. If for example the - /// buffer size is different Panics will occur. - /// - /// In my testing i noticed that when using Cpal with non-standard buffer sizes Cpal would just give - /// another buffer size. This will also lead to panics. - /// - /// The stream has to closed before dropping the Manager and the manager has to be notified by calling stream_closed. - pub fn get_callback>( - &mut self, - config: OutputConfig, - ) -> impl FnMut(&mut [Sample]) { - const TO_WORKER_CAPACITY: usize = 5; - - assert!(self.stream_comms.is_none(), "Stream already active"); - 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, from_worker.0, config); - let buffer_time = - Duration::from_millis((config.buffer_size * 1000 / config.sample_rate).into()); - - self.stream_comms = Some(ActiveStreamComms { - buffer_time, - send: to_worker.0, - status: from_worker.1, - }); - - audio_worker.get_typed_callback() - } - - /// When closing the Stream this method should be called. - pub fn stream_closed(&mut self) { - self.stream_comms = None - } -} - -impl Drop for AudioManager { - fn drop(&mut self) { - // try to stop playback if a stream is active - if let Some(stream) = &mut self.stream_comms { - eprintln!("AudioManager dropped while audio Stream still active."); - let msg1 = stream.send.push(ToWorkerMsg::StopLiveNote); - let msg2 = stream.send.push(ToWorkerMsg::StopPlayback); - if msg1.is_err() || msg2.is_err() { - // This happens when the message buffer is full - eprintln!("Audio playback couldn't be stopped completely"); - } else { - eprintln!("Audio playback was stopped"); - } - } - } -} - -/// the changes made to the song will be made available to the playing live audio as soon as -/// this struct is dropped. -/// -/// With this you can load the full song without ever playing a half initialised state -/// when doing mulitple operations this object should be kept as it is -#[derive(Debug)] -pub struct SongEdit<'a> { - song: WriteGuard<'a, Song, ValidOperation>, - gc: &'a mut Collector, -} - -impl SongEdit<'_> { - pub fn apply_operation(&mut self, op: SongOperation) -> Result<(), SongOperation> { - let valid_operation = ValidOperation::new(op, self.gc, self.song.read())?; - self.song.apply_op(valid_operation); - Ok(()) - } - - pub fn song(&self) -> &Song { - self.song.read() - } - - /// Finish the changes and publish them to the live playing song. - /// Equivalent to std::mem::drop(SongEdit) - pub fn finish(self) {} -} - -#[derive(Debug, Clone, Copy)] -pub struct OutputConfig { - pub buffer_size: u32, - pub channel_count: NonZeroU16, - pub sample_rate: NonZero, -} - -#[derive(Debug, Clone, Copy)] -pub enum PlaybackSettings { - Pattern { idx: u8, should_loop: bool }, - Order { idx: u16, should_loop: bool }, -} diff --git a/src/project/song.rs b/src/project/song.rs index efd9551..f673174 100644 --- a/src/project/song.rs +++ b/src/project/song.rs @@ -3,10 +3,8 @@ use std::fmt::{Debug, Formatter}; use std::num::NonZero; use super::pattern::{Pattern, PatternOperation}; -use crate::channel::Pan; +use crate::Collector; use crate::file::impulse_format; -use crate::file::impulse_format::header::PatternOrder; -use crate::manager::Collector; use crate::sample::{Sample, SampleMetaData}; #[derive(Clone, Debug)] @@ -21,10 +19,10 @@ pub struct Song { pub pitch_wheel_depth: u8, pub patterns: [Pattern; Song::MAX_PATTERNS], - pub pattern_order: [PatternOrder; Song::MAX_ORDERS], - pub volume: [u8; Song::MAX_CHANNELS], - pub pan: [Pan; Song::MAX_CHANNELS], - pub samples: [Option<(SampleMetaData, Sample)>; Song::MAX_SAMPLES_INSTR], + pub pattern_order: [PatternOrder; Self::MAX_ORDERS], + pub volume: [u8; Self::MAX_CHANNELS], + pub pan: [Pan; Self::MAX_CHANNELS], + pub samples: [Option<(SampleMetaData, Sample)>; Self::MAX_SAMPLES_INSTR], } impl Song { @@ -209,3 +207,25 @@ impl simple_left_right::Absorb for Song { } } } + +#[derive(Debug, Clone, Copy)] +pub enum Pan { + /// Value ranges from 0 to 64, with 32 being center + Value(u8), + Surround, + Disabled, +} + +impl Default for Pan { + fn default() -> Self { + Self::Value(32) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum PatternOrder { + Number(u8), + #[default] + EndOfSong, + SkipOrder, +} diff --git a/src/sample.rs b/src/sample.rs index 6c1408c..d585d24 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -2,7 +2,7 @@ use std::{ fmt::Debug, iter::repeat_n, num::NonZero, - ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, sync::Arc, }; @@ -10,6 +10,7 @@ use crate::{ audio_processing::Frame, file::impulse_format::sample::VibratoWave, project::note_event::Note, }; +// implemented for f32 and Frame pub(crate) trait ProcessingFrame: Add + AddAssign @@ -17,16 +18,21 @@ pub(crate) trait ProcessingFrame: + SubAssign + Mul + MulAssign + + Div + + DivAssign + Copy { + fn mul_add(self, mul: f32, add: Self) -> Self; } -impl ProcessingFrame for Frame {} - -impl ProcessingFrame for f32 {} +impl ProcessingFrame for f32 { + fn mul_add(self, mul: f32, add: Self) -> Self { + f32::mul_add(self, mul, add) + } +} pub(crate) trait ProcessingFunction { - fn process(self, data: &[Fr; N]) -> Fr; + fn process(position: f32, data: &[Fr; N]) -> Fr; } #[derive(Clone)] @@ -54,6 +60,7 @@ impl Sample { } } + /// This function also offsets the loaded sample data correctly, depending on the size of required processing data pub(crate) fn compute< const N: usize, // all implementations are generic over the ProcessingFrame type. here both possible ProcessingFrame types @@ -62,17 +69,20 @@ impl Sample { Proc: ProcessingFunction + ProcessingFunction, >( &self, - index: usize, - proc: Proc, + position: (usize, f32), ) -> Frame { + const { assert!(N / 2 <= Self::PAD_SIZE_EACH) }; + let half = const { (N - 1) / 2 }; + let start_idx = position.0 - half; + let end_idx = start_idx + N; if self.is_mono() { - let data: &[f32; N] = self.data[index..index + N].try_into().unwrap(); - Frame::from(proc.process(data)) + let data: &[f32; N] = self.data[start_idx..end_idx].try_into().unwrap(); + Frame::from(Proc::process(position.1, data)) } else { - let data: &[Frame; N] = Frame::from_interleaved(&self.data[index * 2..(index + N) * 2]) + let data: &[Frame; N] = Frame::from_interleaved(&self.data[start_idx * 2..end_idx * 2]) .try_into() .unwrap(); - proc.process(data) + Proc::process(position.1, data) } } @@ -96,6 +106,7 @@ impl Sample { ) } + /// Should only be called if the Iterator already includes enough padding pub fn new_stereo_interpolated_padded>(data: I) -> Self { Self { mono: false,