From fd1586a86a8c867e6fabef1a2e81fe4ee7df289c Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 28 Jan 2024 20:11:00 -0500 Subject: [PATCH 01/87] feat: start adding flags --- Cargo.lock | 9 +- engine/Cargo.toml | 1 + engine/src/asset/sound/dat/t_song.rs | 181 ++++++++++++++++++++++++--- engine/src/asset/sound/mod.rs | 10 ++ 4 files changed, 183 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0640e2f..e2055a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "bumpalo" version = "3.14.0" @@ -157,6 +163,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" name = "engine" version = "0.1.0" dependencies = [ + "bitflags 2.4.2", "const_format", "eyre", "fixed", @@ -542,7 +549,7 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 3cd7453..c629458 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +bitflags = "2.4.2" const_format = "0.2.32" fixed = "1.24.0" flate2 = "1.0.28" diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index f465d9c..cb152e2 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -3,18 +3,19 @@ use crate::{ asset::{extension::*, sound::dat::mixer::Mixer, AssetParser}, utils::nom::*, }; +use bitflags::bitflags; use itertools::Itertools; #[derive(Debug)] pub struct TSong { - bpm: u8, - speed: u8, - restart_order: u8, - orders: Vec, + pub bpm: u8, + pub speed: u8, + pub restart_order: u8, + pub orders: Vec, /// Reusable and repeatable sequence -> Row -> Channel (`None` to play nothing) - patterns: Vec>>>, - instruments: Vec, - samples: Vec, + pub patterns: Vec>>>, + pub instruments: Vec, + pub samples: Vec, } impl TSong { @@ -28,7 +29,7 @@ impl TSong { i += 1; for event in row { if let Some(entry) = event - && let Some(note) = entry.note + && let NoteState::On(note) = entry.note // TODO(nenikitov): Find out what special instrument `0xFF` means && entry.instrument != 0xFF { @@ -219,13 +220,158 @@ impl AssetParser for TSongPointers { } #[derive(Debug)] -struct TPattern { - flags: u8, - note: Option, +pub enum NoteState { + None, + On(u8), + Off, +} + +impl From for NoteState { + fn from(value: u8) -> Self { + match value { + 0 => NoteState::None, + 1..=95 => NoteState::On(value), + 96 => NoteState::Off, + _ => unreachable!("Note should be in range 0-96"), + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct TPatternFlags: u8 { + const _ = 1 << 0; + const _ = 1 << 1; + const _ = 1 << 2; + const DoEffect1 = 1 << 3; + const DoEffect2 = 1 << 4; + } +} + +#[derive(Debug)] +pub enum PatternEffectKind { + None, + Arpegio, + PortaUp, + PortaDown, + PortaTone, + Vibrato, + PortaVolume, + VibratoVolume, + Tremolo, + Pan, + SampleOffset, + VolumeSlide, + PositionJump, + Volume, + Break, + Speed, + VolumeGlobal, + Sync, + PortaFineUp, + PortaFineDown, + NoteRetrigger, + VolumeSlideFineUp, + VolumeSlideFineDown, + NoteCut, + NoteDelay, + PatternDelay, + PortaExtraFineUp, + PortaExtraFineDown, + // TODO(nenikitov): Verify if it's referring to surround sound + SoundControlSurroundOff, + SoundControlSurroundOn, + SoundControlReverbOn, + SoundControlReverbOff, + SoundControlCentre, + SoundControlQuad, + FilterGlobal, + FilterLocal, + PlayForward, + PlayBackward, +} + +impl From for PatternEffectKind { + fn from(value: u8) -> Self { + match value { + 0x00 => Self::Arpegio, + 0x01 => Self::PortaUp, + 0x02 => Self::PortaDown, + 0x03 => Self::PortaTone, + 0x04 => Self::Vibrato, + 0x05 => Self::PortaVolume, + 0x06 => Self::VibratoVolume, + 0x07 => Self::Tremolo, + 0x08 => Self::Pan, + 0x09 => Self::SampleOffset, + 0x0A => Self::VolumeSlide, + 0x0B => Self::PositionJump, + 0x0C => Self::Volume, + 0x0D => Self::Break, + 0x0E => Self::Speed, + 0x0F => Self::VolumeGlobal, + 0x14 => Self::Sync, + 0x15 => Self::PortaFineUp, + 0x16 => Self::PortaFineDown, + 0x1D => Self::NoteRetrigger, + 0x1E => Self::VolumeSlideFineUp, + 0x1F => Self::VolumeSlideFineDown, + 0x20 => Self::NoteCut, + 0x21 => Self::NoteDelay, + 0x22 => Self::PatternDelay, + 0x24 => Self::PortaExtraFineUp, + 0x25 => Self::PortaExtraFineDown, + 0x2E => Self::SoundControlSurroundOn, + 0x2F => Self::SoundControlSurroundOff, + 0x30 => Self::SoundControlReverbOn, + 0x31 => Self::SoundControlReverbOff, + 0x32 => Self::SoundControlCentre, + 0x33 => Self::SoundControlQuad, + 0x34 => Self::FilterGlobal, + 0x35 => Self::FilterLocal, + 0x35 => Self::FilterLocal, + 0x36 => Self::PlayForward, + 0x37 => Self::PlayBackward, + _ => Self::None, + } + } +} + +#[derive(Debug)] +pub struct PatternEffect { + pub kind: PatternEffectKind, + pub value: u8, +} + +impl AssetParser for PatternEffect { + type Output = PatternEffect; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, kind) = number::le_u8(input)?; + let (input, value) = number::le_u8(input)?; + + Ok(( + input, + Self { + kind: kind.into(), + value, + }, + )) + } + } +} + +#[derive(Debug)] +pub struct TPattern { + flags: TPatternFlags, + note: NoteState, instrument: u8, volume: u8, - effect_1: u16, - effect_2: u16, + effect_1: PatternEffect, + effect_2: PatternEffect, } impl AssetParser for TPattern { @@ -239,14 +385,15 @@ impl AssetParser for TPattern { let (input, note) = number::le_u8(input)?; let (input, instrument) = number::le_u8(input)?; let (input, volume) = number::le_u8(input)?; - let (input, effect_1) = number::le_u16(input)?; - let (input, effect_2) = number::le_u16(input)?; + let (input, effect_1) = PatternEffect::parser(())(input)?; + let (input, effect_2) = PatternEffect::parser(())(input)?; Ok(( input, Self { - flags, - note: (note != 0).then_some(note), + // TODO(nenikitov): Use `Result` + flags: TPatternFlags::from_bits(flags).expect("Flags should be valid"), + note: note.into(), instrument, volume, effect_1, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 1b22227..3b1833f 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -92,6 +92,16 @@ mod tests { ) })?; + // TODO(nenikitov): Remove this debug code + let fail_music = sounds + .iter() + .filter_map(|s| match s { + Sound::Song(s) => Some(s), + Sound::Effect(_) => None, + }) + .collect::>()[0xA]; + dbg!(&fail_music.patterns[0]); + let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); sounds From 7472e27c9178525db228026a27ba87d01aa4db38 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Thu, 1 Feb 2024 20:39:19 -0500 Subject: [PATCH 02/87] wip: remake the mixer --- engine/src/asset/sound/dat/mixer.rs | 161 +++++++++++++++++++++++-- engine/src/asset/sound/dat/t_effect.rs | 2 +- engine/src/asset/sound/dat/t_song.rs | 19 +-- engine/src/asset/sound/mod.rs | 2 +- 4 files changed, 164 insertions(+), 20 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 70e53e8..88e1e06 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,8 +1,149 @@ -type Sample = i16; -type Samples = Vec; +use crate::asset::sound::dat::t_song::{NoteState, PatternEffectKind, TPatternFlags}; + +use super::{ + t_instrument::TInstrument, + t_song::{TPattern, TSong}, +}; + +type SamplePoint = i16; +type Sample = Vec; + +pub trait TSongMixer { + fn mix(&self, restart: bool) -> Sample; +} + +impl TSongMixer for TSong { + fn mix(&self, restart: bool) -> Sample { + TSongMixerUtils::mix( + self, + if restart { + self.restart_order as usize + } else { + 0 + }, + ) + } +} + +trait TSongMixerUtils { + const SAMPLE_RATE: usize = 16000; + const CHANNEL_COUNT: usize = 1; + + fn mix(&self, start: usize) -> Sample; + + fn ticks_per_beat(bpm: usize, speed: usize) -> f32; +} + +impl TSongMixerUtils for TSong { + fn mix(&self, start: usize) -> Sample { + let mut m = Mixer::new(); + + let mut channels: Vec<_> = (0..self.patterns[0].len()) + .map(|_| Channel::default()) + .collect(); + + let mut offset: usize = 0; + let mut bpm = self.bpm; + + for pattern in &self.orders[start..] { + let pattern = &self.patterns[*pattern as usize]; + for row in pattern { + // Update channels + for (c, event) in row.iter().enumerate() { + let Some(event) = event else { continue }; + let channel = &mut channels[c]; + + // Process note + match event.note { + NoteState::None => {} + NoteState::On(_) => { + // TODO(nenikitov): This will become huge with more effects, this will need to be refactored + channel.note = event.note; + channel.instrument = Some(&self.instruments[event.instrument as usize]); + channel.volume = event.volume as f32 / 255.0; + channel.sample_posion = 0; + } + NoteState::Off => { + // TODO(nenikitov): This is repeated from `NoteState::On`, somehow refactor + channel.note = event.note; + } + } + + // Process effects + let effects = [ + if event.flags.contains(TPatternFlags::DoEffect1) { + Some(&event.effect_1) + } else { + None + }, + if event.flags.contains(TPatternFlags::DoEffect2) { + Some(&event.effect_2) + } else { + None + }, + ]; + for effect in effects.into_iter().flatten() { + match effect.kind { + // TODO(nenikitov): Add effects + PatternEffectKind::Speed => { + bpm = effect.value; + } + _ => {} + } + } + } + + // Mix current tick + let sample_length = Self::ticks_per_beat(bpm as usize, self.speed as usize) + * Self::SAMPLE_RATE as f32; + let sample_length = sample_length as usize; + + for c in &channels { + m.add_sample(&c.tick(sample_length), offset); + } + + // Advance to next tick + offset += sample_length; + } + } + + m.mix() + } + + fn ticks_per_beat(bpm: usize, speed: usize) -> f32 { + (bpm * speed) as f32 / 60.0 + } +} + +#[derive(Default)] +struct Channel<'a> { + instrument: Option<&'a TInstrument>, + sample_posion: usize, + + volume: f32, + note: NoteState, +} + +impl<'a> Channel<'a> { + fn tick(&self, duration: usize) -> Sample { + todo!() + } + + fn play_event(&mut self, event: &TPattern, instruments: &'a [TInstrument]) { + if event.note != NoteState::None { + *self = Self::default(); + } + + if let NoteState::On(note) = event.note { + self.instrument = Some(&instruments[event.instrument as usize]); + } + } +} + +// TODO(nenikitov): Remove this code when new mixer is done pub struct Mixer { - samples: Samples, + samples: Sample, } impl Mixer { @@ -12,7 +153,7 @@ impl Mixer { } } - pub fn add_samples(&mut self, sample: &Samples, offset: usize) { + pub fn add_sample(&mut self, sample: &Sample, offset: usize) { let new_len = offset + sample.len(); if new_len > self.samples.len() { self.samples.resize(new_len, 0); @@ -26,22 +167,22 @@ impl Mixer { } } - pub fn mix(self) -> Samples { + pub fn mix(self) -> Sample { self.samples } } pub trait SoundEffect { - fn pitch(self, note: u8) -> Samples; - fn volume(self, volume: f32) -> Samples; + fn pitch(self, note: u8) -> Sample; + fn volume(self, volume: f32) -> Sample; } -impl SoundEffect for Samples { - fn pitch(self, note: u8) -> Samples { +impl SoundEffect for Sample { + fn pitch(self, note: u8) -> Sample { todo!("(nenikitov): Figure out how this work") } - fn volume(self, volume: f32) -> Samples { + fn volume(self, volume: f32) -> Sample { self.into_iter() .map(|s| (s as f32 * volume) as i16) .collect() diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 4413163..65e1c2f 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -17,7 +17,7 @@ pub struct TEffect { impl TEffect { pub fn mix(&self) -> Vec { let mut m = Mixer::new(); - m.add_samples(&self.sample.data, 0); + m.add_sample(&self.sample.data, 0); m.mix() } } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index cb152e2..1cb5d09 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -41,7 +41,7 @@ impl TSong { let sample = &self.samples[sample as usize]; let data = sample.data.clone(); - m.add_samples(&data.volume(Self::volume(sample.volume)), i * 1000); + m.add_sample(&data.volume(Self::volume(sample.volume)), i * 1000); } } } @@ -219,8 +219,9 @@ impl AssetParser for TSongPointers { } } -#[derive(Debug)] +#[derive(Default, Debug, PartialEq, Clone, Copy)] pub enum NoteState { + #[default] None, On(u8), Off, @@ -337,6 +338,7 @@ impl From for PatternEffectKind { } } +// TODO(nenikitov): Use enum with associated value instead of a struct #[derive(Debug)] pub struct PatternEffect { pub kind: PatternEffectKind, @@ -366,12 +368,13 @@ impl AssetParser for PatternEffect { #[derive(Debug)] pub struct TPattern { - flags: TPatternFlags, - note: NoteState, - instrument: u8, - volume: u8, - effect_1: PatternEffect, - effect_2: PatternEffect, + pub flags: TPatternFlags, + pub note: NoteState, + // TODO(nenikitov): Maybe this should be a direct reference to corresponding `TInstrument` + pub instrument: u8, + pub volume: u8, + pub effect_1: PatternEffect, + pub effect_2: PatternEffect, } impl AssetParser for TPattern { diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 3b1833f..9f37466 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -100,7 +100,7 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0xA]; - dbg!(&fail_music.patterns[0]); + //dbg!(&fail_music.patterns[0]); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From 04dbb51e2e050a0519698056d78edb05c9a801d4 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 2 Feb 2024 00:16:46 -0500 Subject: [PATCH 03/87] wip: add proper bpm --- engine/src/asset/sound/dat/mixer.rs | 64 ++++++++++++++-------- engine/src/asset/sound/dat/t_instrument.rs | 1 + engine/src/asset/sound/dat/t_song.rs | 42 +------------- engine/src/asset/sound/mod.rs | 8 ++- 4 files changed, 48 insertions(+), 67 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 88e1e06..e538339 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,8 +1,8 @@ use crate::asset::sound::dat::t_song::{NoteState, PatternEffectKind, TPatternFlags}; use super::{ - t_instrument::TInstrument, - t_song::{TPattern, TSong}, + t_instrument::{TInstrument, TSample}, + t_song::TSong, }; type SamplePoint = i16; @@ -31,19 +31,21 @@ trait TSongMixerUtils { fn mix(&self, start: usize) -> Sample; - fn ticks_per_beat(bpm: usize, speed: usize) -> f32; + fn seconds_per_tick(bpm: usize, speed: usize) -> f32; } impl TSongMixerUtils for TSong { fn mix(&self, start: usize) -> Sample { let mut m = Mixer::new(); - let mut channels: Vec<_> = (0..self.patterns[0].len()) + let mut channels: Vec<_> = (0..self.patterns[0][0].len()) .map(|_| Channel::default()) .collect(); - let mut offset: usize = 0; + let mut offset = 0; + let mut sample_length_fractional = 0.0; let mut bpm = self.bpm; + let mut speed = self.speed; for pattern in &self.orders[start..] { let pattern = &self.patterns[*pattern as usize]; @@ -59,7 +61,11 @@ impl TSongMixerUtils for TSong { NoteState::On(_) => { // TODO(nenikitov): This will become huge with more effects, this will need to be refactored channel.note = event.note; - channel.instrument = Some(&self.instruments[event.instrument as usize]); + // TODO(nenikitov): Find out what `instrument` 255 really means instead of skipping it + if event.instrument != 255 { + channel.instrument = + Some(&self.instruments[event.instrument as usize]); + } channel.volume = event.volume as f32 / 255.0; channel.sample_posion = 0; } @@ -86,7 +92,11 @@ impl TSongMixerUtils for TSong { match effect.kind { // TODO(nenikitov): Add effects PatternEffectKind::Speed => { - bpm = effect.value; + if effect.value >= 0x20 { + bpm = effect.value; + } else { + speed = effect.value; + } } _ => {} } @@ -94,12 +104,14 @@ impl TSongMixerUtils for TSong { } // Mix current tick - let sample_length = Self::ticks_per_beat(bpm as usize, self.speed as usize) - * Self::SAMPLE_RATE as f32; + let sample_length = Self::seconds_per_tick(bpm as usize, speed as usize) + * Self::SAMPLE_RATE as f32 + + sample_length_fractional; + sample_length_fractional = sample_length - sample_length.floor(); let sample_length = sample_length as usize; - for c in &channels { - m.add_sample(&c.tick(sample_length), offset); + for c in &mut channels { + m.add_sample(&c.tick(sample_length, &self.samples), offset); } // Advance to next tick @@ -110,8 +122,8 @@ impl TSongMixerUtils for TSong { m.mix() } - fn ticks_per_beat(bpm: usize, speed: usize) -> f32 { - (bpm * speed) as f32 / 60.0 + fn seconds_per_tick(bpm: usize, speed: usize) -> f32 { + 60.0 / (bpm * speed) as f32 } } @@ -125,17 +137,21 @@ struct Channel<'a> { } impl<'a> Channel<'a> { - fn tick(&self, duration: usize) -> Sample { - todo!() - } - - fn play_event(&mut self, event: &TPattern, instruments: &'a [TInstrument]) { - if event.note != NoteState::None { - *self = Self::default(); - } - - if let NoteState::On(note) = event.note { - self.instrument = Some(&instruments[event.instrument as usize]); + // TODO(nenikitov): Don not pass `samples`, it should somehow be stored in the instrument + fn tick(&mut self, duration: usize, samples: &Vec) -> Sample { + if let Some(instrument) = self.instrument + && let NoteState::On(note) = self.note + { + let sample = samples[instrument.samples[note as usize] as usize] + .sample_full() + .to_vec() + .volume(self.volume); + + self.note = NoteState::Off; + + sample + } else { + vec![] } } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index ca32f3e..f2c632f 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -26,6 +26,7 @@ pub struct TInstrument { pub fadeout: u32, pub vibrato_table: u32, + // TODO(nenikitov): Maybe this should be a direct reference to corresponding `TInstrument` pub samples: Box<[u8; 96]>, } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 1cb5d09..28256d7 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -1,6 +1,6 @@ -use super::{mixer::SoundEffect, t_instrument::*, uncompress}; +use super::{t_instrument::*, uncompress}; use crate::{ - asset::{extension::*, sound::dat::mixer::Mixer, AssetParser}, + asset::{extension::*, AssetParser}, utils::nom::*, }; use bitflags::bitflags; @@ -18,44 +18,6 @@ pub struct TSong { pub samples: Vec, } -impl TSong { - pub fn mix(&self) -> Vec { - let mut m = Mixer::new(); - - let mut i = 0; - for pattern in &self.orders { - let pattern = &self.patterns[*pattern as usize]; - for row in pattern { - i += 1; - for event in row { - if let Some(entry) = event - && let NoteState::On(note) = entry.note - // TODO(nenikitov): Find out what special instrument `0xFF` means - && entry.instrument != 0xFF - { - let instrument = &self.instruments[entry.instrument as usize]; - // TODO(nenikitov): See if doing `- 1` in parsing will look nicer - let sample = instrument.samples[(note - 1) as usize]; - // TODO(nenikitov): Find out what special sample `0xFF` means - if sample != 0xFF { - let sample = &self.samples[sample as usize]; - let data = sample.data.clone(); - - m.add_sample(&data.volume(Self::volume(sample.volume)), i * 1000); - } - } - } - } - } - - m.mix() - } - - fn volume(volume: u8) -> f32 { - volume as f32 / u8::MAX as f32 - } -} - impl AssetParser for TSong { type Output = Self; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 9f37466..0924a99 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,5 +1,7 @@ mod dat; +use self::dat::mixer::TSongMixer; + use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -17,7 +19,7 @@ pub enum Sound { impl Sound { pub fn mix(&self) -> Vec { match self { - Sound::Song(sound) => sound.mix(), + Sound::Song(sound) => sound.mix(false), Sound::Effect(effect) => effect.mix(), } } @@ -99,8 +101,8 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0xA]; - //dbg!(&fail_music.patterns[0]); + .collect::>()[0xC]; + dbg!(&fail_music.patterns[0]); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From eaa8d3eb9f8050db4a3836cda336b766d7c3535a Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 2 Feb 2024 01:58:46 -0500 Subject: [PATCH 04/87] wip: making sustain instruments --- engine/src/asset/sound/dat/mixer.rs | 77 ++++++++++++++-------- engine/src/asset/sound/dat/t_instrument.rs | 36 ++++++++-- engine/src/asset/sound/dat/t_song.rs | 6 +- engine/src/asset/sound/mod.rs | 2 +- 4 files changed, 83 insertions(+), 38 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index e538339..0c935c2 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,3 +1,5 @@ +use bitflags::Flags; + use crate::asset::sound::dat::t_song::{NoteState, PatternEffectKind, TPatternFlags}; use super::{ @@ -56,23 +58,15 @@ impl TSongMixerUtils for TSong { let channel = &mut channels[c]; // Process note - match event.note { - NoteState::None => {} - NoteState::On(_) => { - // TODO(nenikitov): This will become huge with more effects, this will need to be refactored - channel.note = event.note; - // TODO(nenikitov): Find out what `instrument` 255 really means instead of skipping it - if event.instrument != 255 { - channel.instrument = - Some(&self.instruments[event.instrument as usize]); - } - channel.volume = event.volume as f32 / 255.0; - channel.sample_posion = 0; - } - NoteState::Off => { - // TODO(nenikitov): This is repeated from `NoteState::On`, somehow refactor - channel.note = event.note; - } + if event.flags.contains(TPatternFlags::ChangeNote) { + channel.note = event.note; + } + if event.flags.contains(TPatternFlags::ChangeInstrument) { + channel.instrument = Some(&self.instruments[event.instrument as usize]); + channel.sample_posion = SamplePosition::default(); + } + if event.flags.contains(TPatternFlags::ChangeVolume) { + channel.volume = event.volume as f32 / u8::MAX as f32; } // Process effects @@ -127,10 +121,22 @@ impl TSongMixerUtils for TSong { } } +#[derive(PartialEq)] +enum SamplePosition { + Beginning, + Loop(isize), +} + +impl Default for SamplePosition { + fn default() -> Self { + Self::Beginning + } +} + #[derive(Default)] struct Channel<'a> { instrument: Option<&'a TInstrument>, - sample_posion: usize, + sample_posion: SamplePosition, volume: f32, note: NoteState, @@ -139,21 +145,36 @@ struct Channel<'a> { impl<'a> Channel<'a> { // TODO(nenikitov): Don not pass `samples`, it should somehow be stored in the instrument fn tick(&mut self, duration: usize, samples: &Vec) -> Sample { - if let Some(instrument) = self.instrument - && let NoteState::On(note) = self.note - { - let sample = samples[instrument.samples[note as usize] as usize] - .sample_full() - .to_vec() - .volume(self.volume); + if let Some(instrument) = self.instrument { + let mut m = Mixer::new(); + let mut offset = 0; + + let sample = &samples[instrument.samples[note as usize] as usize]; + + if let NoteState::On(note) = self.note { + if self.sample_posion == SamplePosition::Beginning { + let sample = sample.sample_beginning(); - self.note = NoteState::Off; + m.add_sample(&self.treat_sample(sample), 0); + + // Next sample part that should be played is loop part + self.sample_posion = + SamplePosition::Loop(duration as isize - sample.len() as isize); + offset = sample.len(); + } - sample + self.note = NoteState::None; + } + + todo!() } else { vec![] } } + + fn treat_sample(&self, sample: &[SamplePoint]) -> Sample { + sample.to_vec().volume(self.volume) + } } // TODO(nenikitov): Remove this code when new mixer is done @@ -169,7 +190,7 @@ impl Mixer { } } - pub fn add_sample(&mut self, sample: &Sample, offset: usize) { + pub fn add_sample(&mut self, sample: &[i16], offset: usize) { let new_len = offset + sample.len(); if new_len > self.samples.len() { self.samples.resize(new_len, 0); diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index f2c632f..7f723b6 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -2,10 +2,18 @@ use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; +use bitflags::bitflags; + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct TInstrumentFlags: u8 { + const _ = 1 << 0; + } +} #[derive(Debug)] pub struct TInstrument { - pub flags: u8, + pub flags: TInstrumentFlags, pub volume_begin: u16, pub volume_end: u16, @@ -67,7 +75,7 @@ impl AssetParser for TInstrument { Ok(( input, Self { - flags, + flags: TInstrumentFlags::from_bits(flags).expect("Flags should be valid"), volume_begin, volume_end, volume_sustain, @@ -90,9 +98,17 @@ impl AssetParser for TInstrument { } } +// TODO(nenikitov): I'm not sure about this flag +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct TSampleFlags: u8 { + const IsLooping = 1 << 0; + } +} + #[derive(Debug)] pub struct TSample { - pub flags: u8, + pub flags: TSampleFlags, pub volume: u8, pub panning: u8, pub align: u8, @@ -117,14 +133,14 @@ impl AssetParser for TSample { let (input, loop_end) = number::le_u32(input)?; let (input, sample_offset) = number::le_u32(input)?; - // The game uses offset for `i16`, but it's much more conventient to just use indeces + // The game uses offset for `i16`, but it's much more convenient to just use indices let loop_end = loop_end / 2; let sample_offset = sample_offset / 2; Ok(( input, Self { - flags, + flags: TSampleFlags::from_bits(flags).expect("Flags should be valid"), volume, panning, align, @@ -142,7 +158,15 @@ impl TSample { &self.data } + pub fn sample_beginning(&self) -> &[i16] { + &self.data[..self.data.len() - self.loop_length as usize] + } + pub fn sample_loop(&self) -> &[i16] { - &self.data[self.data.len() - 1 - self.loop_length as usize..] + if self.loop_length != 0 { + &self.data[self.data.len() - self.loop_length as usize..] + } else { + &[] + } } } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 28256d7..172f9a3 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -203,9 +203,9 @@ impl From for NoteState { bitflags! { #[derive(Debug, Clone, Copy)] pub struct TPatternFlags: u8 { - const _ = 1 << 0; - const _ = 1 << 1; - const _ = 1 << 2; + const ChangeNote = 1 << 0; + const ChangeInstrument = 1 << 1; + const ChangeVolume = 1 << 2; const DoEffect1 = 1 << 3; const DoEffect2 = 1 << 4; } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 0924a99..23b6385 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -101,7 +101,7 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0xC]; + .collect::>()[0xA]; dbg!(&fail_music.patterns[0]); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From 2135955a4274574d0564791eafa9d8b2e3f212c7 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 2 Feb 2024 20:06:19 -0500 Subject: [PATCH 05/87] wip: make instrument take a reference to sample --- engine/src/asset/sound/dat/mixer.rs | 45 +++++++--------------- engine/src/asset/sound/dat/t_effect.rs | 12 ++++-- engine/src/asset/sound/dat/t_instrument.rs | 37 +++++++++++++++--- engine/src/asset/sound/dat/t_song.rs | 14 ++++--- 4 files changed, 61 insertions(+), 47 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 0c935c2..2ae5060 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,9 +1,7 @@ -use bitflags::Flags; - use crate::asset::sound::dat::t_song::{NoteState, PatternEffectKind, TPatternFlags}; use super::{ - t_instrument::{TInstrument, TSample}, + t_instrument::{TInstrument, TInstrumentSampleKind}, t_song::TSong, }; @@ -62,7 +60,9 @@ impl TSongMixerUtils for TSong { channel.note = event.note; } if event.flags.contains(TPatternFlags::ChangeInstrument) { - channel.instrument = Some(&self.instruments[event.instrument as usize]); + if event.instrument != 255 { + channel.instrument = Some(&self.instruments[event.instrument as usize]); + } channel.sample_posion = SamplePosition::default(); } if event.flags.contains(TPatternFlags::ChangeVolume) { @@ -105,7 +105,7 @@ impl TSongMixerUtils for TSong { let sample_length = sample_length as usize; for c in &mut channels { - m.add_sample(&c.tick(sample_length, &self.samples), offset); + m.add_sample(&c.tick(sample_length), offset); } // Advance to next tick @@ -144,37 +144,18 @@ struct Channel<'a> { impl<'a> Channel<'a> { // TODO(nenikitov): Don not pass `samples`, it should somehow be stored in the instrument - fn tick(&mut self, duration: usize, samples: &Vec) -> Sample { - if let Some(instrument) = self.instrument { - let mut m = Mixer::new(); - let mut offset = 0; - - let sample = &samples[instrument.samples[note as usize] as usize]; - - if let NoteState::On(note) = self.note { - if self.sample_posion == SamplePosition::Beginning { - let sample = sample.sample_beginning(); - - m.add_sample(&self.treat_sample(sample), 0); - - // Next sample part that should be played is loop part - self.sample_posion = - SamplePosition::Loop(duration as isize - sample.len() as isize); - offset = sample.len(); - } - - self.note = NoteState::None; - } - - todo!() + fn tick(&mut self, duration: usize) -> Sample { + if let Some(instrument) = self.instrument + && let NoteState::On(note) = self.note + && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] + { + let sample = sample.sample_full().to_vec().volume(self.volume); + self.note = NoteState::None; + sample } else { vec![] } } - - fn treat_sample(&self, sample: &[SamplePoint]) -> Sample { - sample.to_vec().volume(self.volume) - } } // TODO(nenikitov): Remove this code when new mixer is done diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 65e1c2f..846799e 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use super::{ mixer::Mixer, t_instrument::{TInstrument, TSample}, @@ -10,7 +12,7 @@ use crate::{ pub struct TEffect { instrument: TInstrument, - sample: TSample, + sample: Rc, } // It should be separated @@ -31,10 +33,14 @@ impl AssetParser for TEffect { move |input| { let (_, pointers) = TEffectPointers::parser(())(input)?; - let (_, instrument) = TInstrument::parser(())(&input[pointers.instrument as usize..])?; - let sample = uncompress(&input[pointers.sample_data as usize..]); let (_, sample) = TSample::parser(&sample)(&input[pointers.sample as usize..])?; + let sample = [Rc::new(sample)]; + + let (_, instrument) = + TInstrument::parser(&sample)(&input[pointers.instrument as usize..])?; + + let [sample] = sample; Ok((&[], Self { instrument, sample })) } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 7f723b6..48124fb 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -11,6 +13,14 @@ bitflags! { } } +// TODO(nenikitov): Maybe make it an `AssetParser` +#[derive(Debug)] +pub enum TInstrumentSampleKind { + // TODO(nenikitov): Figure out what instrumentn `255` is + Special, + Predefined(Rc), +} + #[derive(Debug)] pub struct TInstrument { pub flags: TInstrumentFlags, @@ -34,16 +44,15 @@ pub struct TInstrument { pub fadeout: u32, pub vibrato_table: u32, - // TODO(nenikitov): Maybe this should be a direct reference to corresponding `TInstrument` - pub samples: Box<[u8; 96]>, + pub samples: Box<[TInstrumentSampleKind; 96]>, } impl AssetParser for TInstrument { type Output = Self; - type Context<'ctx> = (); + type Context<'ctx> = &'ctx [Rc]; - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(samples: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = number::le_u8(input)?; @@ -70,7 +79,7 @@ impl AssetParser for TInstrument { let (input, fadeout) = number::le_u32(input)?; let (input, vibrato_table) = number::le_u32(input)?; - let (input, samples) = multi::count!(number::le_u8)(input)?; + let (input, sample_indexes): (_, [u8; 96]) = multi::count!(number::le_u8)(input)?; Ok(( input, @@ -91,7 +100,20 @@ impl AssetParser for TInstrument { vibrato_sweep, fadeout, vibrato_table, - samples: Box::new(samples), + samples: Box::new( + sample_indexes + .into_iter() + .map(|i| { + if i == 255 { + TInstrumentSampleKind::Special + } else { + TInstrumentSampleKind::Predefined(samples[i as usize].clone()) + } + }) + .collect::>() + .try_into() + .unwrap(), + ), }, )) } @@ -136,6 +158,7 @@ impl AssetParser for TSample { // The game uses offset for `i16`, but it's much more convenient to just use indices let loop_end = loop_end / 2; let sample_offset = sample_offset / 2; + let loop_length = loop_length / 2; Ok(( input, @@ -159,6 +182,8 @@ impl TSample { } pub fn sample_beginning(&self) -> &[i16] { + dbg!(self.data.len()); + dbg!(self.loop_length); &self.data[..self.data.len() - self.loop_length as usize] } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 172f9a3..2495221 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -5,6 +5,7 @@ use crate::{ }; use bitflags::bitflags; use itertools::Itertools; +use std::rc::Rc; #[derive(Debug)] pub struct TSong { @@ -15,7 +16,7 @@ pub struct TSong { /// Reusable and repeatable sequence -> Row -> Channel (`None` to play nothing) pub patterns: Vec>>>, pub instruments: Vec, - pub samples: Vec, + pub samples: Vec>, } impl AssetParser for TSong { @@ -67,16 +68,17 @@ impl AssetParser for TSong { .collect::>()? }; - let (_, instruments) = multi::count!( - TInstrument::parser(()), - header.instrument_count as usize - )(&input[pointers.instruments as usize..])?; - let samples = uncompress(&input[pointers.sample_data as usize..]); let (_, samples) = multi::count!( TSample::parser(&samples), header.sample_count as usize )(&input[pointers.samples as usize..])?; + let samples = samples.into_iter().map(Rc::new).collect::>(); + + let (_, instruments) = multi::count!( + TInstrument::parser(&samples), + header.instrument_count as usize + )(&input[pointers.instruments as usize..])?; Ok(( input, From 642aab9df430e24156985ccabc8ace3e3901889c Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 3 Feb 2024 01:08:09 -0500 Subject: [PATCH 06/87] refactor: use reference instead of indexes --- engine/src/asset/sound/dat/mixer.rs | 18 ++--- engine/src/asset/sound/dat/t_instrument.rs | 2 +- engine/src/asset/sound/dat/t_song.rs | 81 ++++++++++++++-------- 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 2ae5060..9b55d83 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,9 +1,5 @@ -use crate::asset::sound::dat::t_song::{NoteState, PatternEffectKind, TPatternFlags}; - -use super::{ - t_instrument::{TInstrument, TInstrumentSampleKind}, - t_song::TSong, -}; +use super::{t_instrument::*, t_song::*}; +use std::ops::Deref; type SamplePoint = i16; type Sample = Vec; @@ -48,8 +44,7 @@ impl TSongMixerUtils for TSong { let mut speed = self.speed; for pattern in &self.orders[start..] { - let pattern = &self.patterns[*pattern as usize]; - for row in pattern { + for row in pattern.deref() { // Update channels for (c, event) in row.iter().enumerate() { let Some(event) = event else { continue }; @@ -60,9 +55,7 @@ impl TSongMixerUtils for TSong { channel.note = event.note; } if event.flags.contains(TPatternFlags::ChangeInstrument) { - if event.instrument != 255 { - channel.instrument = Some(&self.instruments[event.instrument as usize]); - } + channel.instrument = Some(&event.instrument); channel.sample_posion = SamplePosition::default(); } if event.flags.contains(TPatternFlags::ChangeVolume) { @@ -135,7 +128,7 @@ impl Default for SamplePosition { #[derive(Default)] struct Channel<'a> { - instrument: Option<&'a TInstrument>, + instrument: Option<&'a TPatternInstrumentKind>, sample_posion: SamplePosition, volume: f32, @@ -147,6 +140,7 @@ impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument && let NoteState::On(note) = self.note + && let TPatternInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { let sample = sample.sample_full().to_vec().volume(self.volume); diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 48124fb..57051bd 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -16,7 +16,7 @@ bitflags! { // TODO(nenikitov): Maybe make it an `AssetParser` #[derive(Debug)] pub enum TInstrumentSampleKind { - // TODO(nenikitov): Figure out what instrumentn `255` is + // TODO(nenikitov): Figure out what sample `255` is Special, Predefined(Rc), } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 2495221..504f4db 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -7,15 +7,18 @@ use bitflags::bitflags; use itertools::Itertools; use std::rc::Rc; +pub type PatternRow = Vec>; +pub type Pattern = Vec; + #[derive(Debug)] pub struct TSong { pub bpm: u8, pub speed: u8, pub restart_order: u8, - pub orders: Vec, + pub orders: Vec>, /// Reusable and repeatable sequence -> Row -> Channel (`None` to play nothing) - pub patterns: Vec>>>, - pub instruments: Vec, + pub patterns: Vec>, + pub instruments: Vec>, pub samples: Vec>, } @@ -32,9 +35,18 @@ impl AssetParser for TSong { (header, pointers) }; - let (_, orders) = multi::count!(number::le_u8, header.song_length as usize)( - &input[pointers.orders as usize..], - )?; + let samples = uncompress(&input[pointers.sample_data as usize..]); + let (_, samples) = multi::count!( + TSample::parser(&samples), + header.sample_count as usize + )(&input[pointers.samples as usize..])?; + let samples = samples.into_iter().map(Rc::new).collect::>(); + + let (_, instruments) = multi::count!( + TInstrument::parser(&samples), + header.instrument_count as usize + )(&input[pointers.instruments as usize..])?; + let instruments = instruments.into_iter().map(Rc::new).collect::>(); let patterns: Vec<_> = { let (_, lengths) = multi::count!(number::le_u8, header.pattern_count as usize)( @@ -51,7 +63,7 @@ impl AssetParser for TSong { .zip(lengths) .map(|(input, length)| { multi::count!( - >::parser(()), + >::parser(&instruments), header.channel_count as usize * length as usize )(input) }) @@ -67,18 +79,15 @@ impl AssetParser for TSong { }) .collect::>()? }; + let patterns = patterns.into_iter().map(Rc::new).collect::>(); - let samples = uncompress(&input[pointers.sample_data as usize..]); - let (_, samples) = multi::count!( - TSample::parser(&samples), - header.sample_count as usize - )(&input[pointers.samples as usize..])?; - let samples = samples.into_iter().map(Rc::new).collect::>(); - - let (_, instruments) = multi::count!( - TInstrument::parser(&samples), - header.instrument_count as usize - )(&input[pointers.instruments as usize..])?; + let (_, orders) = multi::count!(number::le_u8, header.song_length as usize)( + &input[pointers.orders as usize..], + )?; + let orders = orders + .into_iter() + .map(|o| patterns[o as usize].clone()) + .collect::>(); Ok(( input, @@ -331,26 +340,32 @@ impl AssetParser for PatternEffect { } #[derive(Debug)] -pub struct TPattern { +pub enum TPatternInstrumentKind { + // TODO(nenikitov): Figure out what instrument `255` is + Special, + Predefined(Rc), +} + +#[derive(Debug)] +pub struct PatternEvent { pub flags: TPatternFlags, pub note: NoteState, - // TODO(nenikitov): Maybe this should be a direct reference to corresponding `TInstrument` - pub instrument: u8, + pub instrument: TPatternInstrumentKind, pub volume: u8, pub effect_1: PatternEffect, pub effect_2: PatternEffect, } -impl AssetParser for TPattern { +impl AssetParser for PatternEvent { type Output = Self; - type Context<'ctx> = (); + type Context<'ctx> = &'ctx [Rc]; - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = number::le_u8(input)?; let (input, note) = number::le_u8(input)?; - let (input, instrument) = number::le_u8(input)?; + let (input, instrument_index) = number::le_u8(input)?; let (input, volume) = number::le_u8(input)?; let (input, effect_1) = PatternEffect::parser(())(input)?; let (input, effect_2) = PatternEffect::parser(())(input)?; @@ -361,7 +376,13 @@ impl AssetParser for TPattern { // TODO(nenikitov): Use `Result` flags: TPatternFlags::from_bits(flags).expect("Flags should be valid"), note: note.into(), - instrument, + instrument: if instrument_index == 255 { + TPatternInstrumentKind::Special + } else { + TPatternInstrumentKind::Predefined( + instruments[instrument_index as usize].clone(), + ) + }, volume, effect_1, effect_2, @@ -371,18 +392,18 @@ impl AssetParser for TPattern { } } -impl AssetParser for Option { +impl AssetParser for Option { type Output = Self; - type Context<'ctx> = (); + type Context<'ctx> = &'ctx [Rc]; - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (after_flags, flags) = number::le_u8(input)?; if (flags & 0x20) != 0 { Ok((after_flags, None)) } else { - let (input, pattern) = TPattern::parser(())(input)?; + let (input, pattern) = PatternEvent::parser(instruments)(input)?; Ok((input, Some(pattern))) } } From c6a6c069fcf55c6400cd5e32a6b353a17b281ddf Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 3 Feb 2024 01:30:51 -0500 Subject: [PATCH 07/87] refactor: separate event --- engine/src/asset/sound/dat/mixer.rs | 18 +- engine/src/asset/sound/dat/mod.rs | 1 + engine/src/asset/sound/dat/pattern_event.rs | 255 ++++++++++++++++++++ engine/src/asset/sound/dat/t_instrument.rs | 2 - engine/src/asset/sound/dat/t_song.rs | 222 +---------------- engine/src/asset/sound/mod.rs | 1 - 6 files changed, 266 insertions(+), 233 deletions(-) create mode 100644 engine/src/asset/sound/dat/pattern_event.rs diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 9b55d83..1393f6d 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,4 +1,4 @@ -use super::{t_instrument::*, t_song::*}; +use super::{pattern_event::*, t_instrument::*, t_song::*}; use std::ops::Deref; type SamplePoint = i16; @@ -51,25 +51,25 @@ impl TSongMixerUtils for TSong { let channel = &mut channels[c]; // Process note - if event.flags.contains(TPatternFlags::ChangeNote) { + if event.flags.contains(PatternEventFlags::ChangeNote) { channel.note = event.note; } - if event.flags.contains(TPatternFlags::ChangeInstrument) { + if event.flags.contains(PatternEventFlags::ChangeInstrument) { channel.instrument = Some(&event.instrument); channel.sample_posion = SamplePosition::default(); } - if event.flags.contains(TPatternFlags::ChangeVolume) { + if event.flags.contains(PatternEventFlags::ChangeVolume) { channel.volume = event.volume as f32 / u8::MAX as f32; } // Process effects let effects = [ - if event.flags.contains(TPatternFlags::DoEffect1) { + if event.flags.contains(PatternEventFlags::DoEffect1) { Some(&event.effect_1) } else { None }, - if event.flags.contains(TPatternFlags::DoEffect2) { + if event.flags.contains(PatternEventFlags::DoEffect2) { Some(&event.effect_2) } else { None @@ -132,19 +132,19 @@ struct Channel<'a> { sample_posion: SamplePosition, volume: f32, - note: NoteState, + note: PatternEventNote, } impl<'a> Channel<'a> { // TODO(nenikitov): Don not pass `samples`, it should somehow be stored in the instrument fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument - && let NoteState::On(note) = self.note + && let PatternEventNote::On(note) = self.note && let TPatternInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { let sample = sample.sample_full().to_vec().volume(self.volume); - self.note = NoteState::None; + self.note = PatternEventNote::None; sample } else { vec![] diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 2b19080..b24ed43 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -4,6 +4,7 @@ use std::io::Cursor; pub mod asset_header; pub mod chunk_header; pub mod mixer; +pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs new file mode 100644 index 0000000..18b1c22 --- /dev/null +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -0,0 +1,255 @@ +use super::t_instrument::*; +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; +use bitflags::bitflags; +use std::rc::Rc; + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum PatternEventNote { + #[default] + None, + On(u8), + Off, +} + +impl AssetParser for PatternEventNote { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, note) = number::le_u8(input)?; + + let note = match note { + 0 => PatternEventNote::None, + 1..=95 => PatternEventNote::On(note), + 96 => PatternEventNote::Off, + // TODO(nenikitov): Should be a `Result` + _ => unreachable!("Note should be in range 0-96"), + }; + + Ok((input, note)) + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct PatternEventFlags: u8 { + const ChangeNote = 1 << 0; + const ChangeInstrument = 1 << 1; + const ChangeVolume = 1 << 2; + const DoEffect1 = 1 << 3; + const DoEffect2 = 1 << 4; + } +} + +impl AssetParser for PatternEventFlags { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, flags) = number::le_u8(input)?; + + Ok(( + input, + // TODO(nenikitov): Should be a `Result` + PatternEventFlags::from_bits(flags).expect(&format!( + "PatternEvent flags should be valid: received: {flags:b}" + )), + )) + } + } +} + +#[derive(Debug)] +pub enum PatternEffectKind { + None, + Arpegio, + PortaUp, + PortaDown, + PortaTone, + Vibrato, + PortaVolume, + VibratoVolume, + Tremolo, + Pan, + SampleOffset, + VolumeSlide, + PositionJump, + Volume, + Break, + Speed, + VolumeGlobal, + Sync, + PortaFineUp, + PortaFineDown, + NoteRetrigger, + VolumeSlideFineUp, + VolumeSlideFineDown, + NoteCut, + NoteDelay, + PatternDelay, + PortaExtraFineUp, + PortaExtraFineDown, + // TODO(nenikitov): Verify if it's referring to surround sound + SoundControlSurroundOff, + SoundControlSurroundOn, + SoundControlReverbOn, + SoundControlReverbOff, + SoundControlCentre, + SoundControlQuad, + FilterGlobal, + FilterLocal, + PlayForward, + PlayBackward, +} + +impl From for PatternEffectKind { + fn from(value: u8) -> Self { + match value { + 0x00 => Self::Arpegio, + 0x01 => Self::PortaUp, + 0x02 => Self::PortaDown, + 0x03 => Self::PortaTone, + 0x04 => Self::Vibrato, + 0x05 => Self::PortaVolume, + 0x06 => Self::VibratoVolume, + 0x07 => Self::Tremolo, + 0x08 => Self::Pan, + 0x09 => Self::SampleOffset, + 0x0A => Self::VolumeSlide, + 0x0B => Self::PositionJump, + 0x0C => Self::Volume, + 0x0D => Self::Break, + 0x0E => Self::Speed, + 0x0F => Self::VolumeGlobal, + 0x14 => Self::Sync, + 0x15 => Self::PortaFineUp, + 0x16 => Self::PortaFineDown, + 0x1D => Self::NoteRetrigger, + 0x1E => Self::VolumeSlideFineUp, + 0x1F => Self::VolumeSlideFineDown, + 0x20 => Self::NoteCut, + 0x21 => Self::NoteDelay, + 0x22 => Self::PatternDelay, + 0x24 => Self::PortaExtraFineUp, + 0x25 => Self::PortaExtraFineDown, + 0x2E => Self::SoundControlSurroundOn, + 0x2F => Self::SoundControlSurroundOff, + 0x30 => Self::SoundControlReverbOn, + 0x31 => Self::SoundControlReverbOff, + 0x32 => Self::SoundControlCentre, + 0x33 => Self::SoundControlQuad, + 0x34 => Self::FilterGlobal, + 0x35 => Self::FilterLocal, + 0x35 => Self::FilterLocal, + 0x36 => Self::PlayForward, + 0x37 => Self::PlayBackward, + _ => Self::None, + } + } +} + +// TODO(nenikitov): Use enum with associated value instead of a struct +#[derive(Debug)] +pub struct PatternEffect { + pub kind: PatternEffectKind, + pub value: u8, +} + +impl AssetParser for PatternEffect { + type Output = PatternEffect; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, kind) = number::le_u8(input)?; + let (input, value) = number::le_u8(input)?; + + Ok(( + input, + Self { + kind: kind.into(), + value, + }, + )) + } + } +} + +#[derive(Debug)] +pub enum TPatternInstrumentKind { + // TODO(nenikitov): Figure out what instrument `255` is + Special, + Predefined(Rc), +} + +pub struct PatternEvent { + pub flags: PatternEventFlags, + pub note: PatternEventNote, + pub instrument: TPatternInstrumentKind, + pub volume: u8, + pub effect_1: PatternEffect, + pub effect_2: PatternEffect, +} + +impl AssetParser for PatternEvent { + type Output = Self; + + type Context<'ctx> = &'ctx [Rc]; + + fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, flags) = PatternEventFlags::parser(())(input)?; + let (input, note) = PatternEventNote::parser(())(input)?; + let (input, instrument_index) = number::le_u8(input)?; + let (input, volume) = number::le_u8(input)?; + let (input, effect_1) = PatternEffect::parser(())(input)?; + let (input, effect_2) = PatternEffect::parser(())(input)?; + + Ok(( + input, + Self { + // TODO(nenikitov): Use `Result` + flags, + note: note.into(), + instrument: if instrument_index == 255 { + TPatternInstrumentKind::Special + } else { + TPatternInstrumentKind::Predefined( + instruments[instrument_index as usize].clone(), + ) + }, + volume, + effect_1, + effect_2, + }, + )) + } + } +} + +impl AssetParser for Option { + type Output = Self; + + type Context<'ctx> = &'ctx [Rc]; + + fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (after_flags, flags) = number::le_u8(input)?; + if (flags & 0x20) != 0 { + Ok((after_flags, None)) + } else { + let (input, pattern) = PatternEvent::parser(instruments)(input)?; + Ok((input, Some(pattern))) + } + } + } +} diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 57051bd..850ba3b 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -182,8 +182,6 @@ impl TSample { } pub fn sample_beginning(&self) -> &[i16] { - dbg!(self.data.len()); - dbg!(self.loop_length); &self.data[..self.data.len() - self.loop_length as usize] } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 504f4db..b842880 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -1,16 +1,14 @@ -use super::{t_instrument::*, uncompress}; +use super::{pattern_event::*, t_instrument::*, uncompress}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; -use bitflags::bitflags; use itertools::Itertools; use std::rc::Rc; pub type PatternRow = Vec>; pub type Pattern = Vec; -#[derive(Debug)] pub struct TSong { pub bpm: u8, pub speed: u8, @@ -191,221 +189,3 @@ impl AssetParser for TSongPointers { } } } - -#[derive(Default, Debug, PartialEq, Clone, Copy)] -pub enum NoteState { - #[default] - None, - On(u8), - Off, -} - -impl From for NoteState { - fn from(value: u8) -> Self { - match value { - 0 => NoteState::None, - 1..=95 => NoteState::On(value), - 96 => NoteState::Off, - _ => unreachable!("Note should be in range 0-96"), - } - } -} - -bitflags! { - #[derive(Debug, Clone, Copy)] - pub struct TPatternFlags: u8 { - const ChangeNote = 1 << 0; - const ChangeInstrument = 1 << 1; - const ChangeVolume = 1 << 2; - const DoEffect1 = 1 << 3; - const DoEffect2 = 1 << 4; - } -} - -#[derive(Debug)] -pub enum PatternEffectKind { - None, - Arpegio, - PortaUp, - PortaDown, - PortaTone, - Vibrato, - PortaVolume, - VibratoVolume, - Tremolo, - Pan, - SampleOffset, - VolumeSlide, - PositionJump, - Volume, - Break, - Speed, - VolumeGlobal, - Sync, - PortaFineUp, - PortaFineDown, - NoteRetrigger, - VolumeSlideFineUp, - VolumeSlideFineDown, - NoteCut, - NoteDelay, - PatternDelay, - PortaExtraFineUp, - PortaExtraFineDown, - // TODO(nenikitov): Verify if it's referring to surround sound - SoundControlSurroundOff, - SoundControlSurroundOn, - SoundControlReverbOn, - SoundControlReverbOff, - SoundControlCentre, - SoundControlQuad, - FilterGlobal, - FilterLocal, - PlayForward, - PlayBackward, -} - -impl From for PatternEffectKind { - fn from(value: u8) -> Self { - match value { - 0x00 => Self::Arpegio, - 0x01 => Self::PortaUp, - 0x02 => Self::PortaDown, - 0x03 => Self::PortaTone, - 0x04 => Self::Vibrato, - 0x05 => Self::PortaVolume, - 0x06 => Self::VibratoVolume, - 0x07 => Self::Tremolo, - 0x08 => Self::Pan, - 0x09 => Self::SampleOffset, - 0x0A => Self::VolumeSlide, - 0x0B => Self::PositionJump, - 0x0C => Self::Volume, - 0x0D => Self::Break, - 0x0E => Self::Speed, - 0x0F => Self::VolumeGlobal, - 0x14 => Self::Sync, - 0x15 => Self::PortaFineUp, - 0x16 => Self::PortaFineDown, - 0x1D => Self::NoteRetrigger, - 0x1E => Self::VolumeSlideFineUp, - 0x1F => Self::VolumeSlideFineDown, - 0x20 => Self::NoteCut, - 0x21 => Self::NoteDelay, - 0x22 => Self::PatternDelay, - 0x24 => Self::PortaExtraFineUp, - 0x25 => Self::PortaExtraFineDown, - 0x2E => Self::SoundControlSurroundOn, - 0x2F => Self::SoundControlSurroundOff, - 0x30 => Self::SoundControlReverbOn, - 0x31 => Self::SoundControlReverbOff, - 0x32 => Self::SoundControlCentre, - 0x33 => Self::SoundControlQuad, - 0x34 => Self::FilterGlobal, - 0x35 => Self::FilterLocal, - 0x35 => Self::FilterLocal, - 0x36 => Self::PlayForward, - 0x37 => Self::PlayBackward, - _ => Self::None, - } - } -} - -// TODO(nenikitov): Use enum with associated value instead of a struct -#[derive(Debug)] -pub struct PatternEffect { - pub kind: PatternEffectKind, - pub value: u8, -} - -impl AssetParser for PatternEffect { - type Output = PatternEffect; - - type Context<'ctx> = (); - - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { - move |input| { - let (input, kind) = number::le_u8(input)?; - let (input, value) = number::le_u8(input)?; - - Ok(( - input, - Self { - kind: kind.into(), - value, - }, - )) - } - } -} - -#[derive(Debug)] -pub enum TPatternInstrumentKind { - // TODO(nenikitov): Figure out what instrument `255` is - Special, - Predefined(Rc), -} - -#[derive(Debug)] -pub struct PatternEvent { - pub flags: TPatternFlags, - pub note: NoteState, - pub instrument: TPatternInstrumentKind, - pub volume: u8, - pub effect_1: PatternEffect, - pub effect_2: PatternEffect, -} - -impl AssetParser for PatternEvent { - type Output = Self; - - type Context<'ctx> = &'ctx [Rc]; - - fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { - move |input| { - let (input, flags) = number::le_u8(input)?; - let (input, note) = number::le_u8(input)?; - let (input, instrument_index) = number::le_u8(input)?; - let (input, volume) = number::le_u8(input)?; - let (input, effect_1) = PatternEffect::parser(())(input)?; - let (input, effect_2) = PatternEffect::parser(())(input)?; - - Ok(( - input, - Self { - // TODO(nenikitov): Use `Result` - flags: TPatternFlags::from_bits(flags).expect("Flags should be valid"), - note: note.into(), - instrument: if instrument_index == 255 { - TPatternInstrumentKind::Special - } else { - TPatternInstrumentKind::Predefined( - instruments[instrument_index as usize].clone(), - ) - }, - volume, - effect_1, - effect_2, - }, - )) - } - } -} - -impl AssetParser for Option { - type Output = Self; - - type Context<'ctx> = &'ctx [Rc]; - - fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { - move |input| { - let (after_flags, flags) = number::le_u8(input)?; - if (flags & 0x20) != 0 { - Ok((after_flags, None)) - } else { - let (input, pattern) = PatternEvent::parser(instruments)(input)?; - Ok((input, Some(pattern))) - } - } - } -} diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 23b6385..ece026a 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -102,7 +102,6 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0xA]; - dbg!(&fail_music.patterns[0]); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From 7baf7352bee6b4c69f0a9e9e3ee0e09e442d05e7 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 3 Feb 2024 02:06:06 -0500 Subject: [PATCH 08/87] refactor: remove flags from pattern, replace by option --- engine/src/asset/sound/dat/mixer.rs | 32 ++--- engine/src/asset/sound/dat/pattern_event.rs | 124 +++++++++++++------- 2 files changed, 89 insertions(+), 67 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1393f6d..a78db47 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -51,31 +51,19 @@ impl TSongMixerUtils for TSong { let channel = &mut channels[c]; // Process note - if event.flags.contains(PatternEventFlags::ChangeNote) { - channel.note = event.note; + if let Some(note) = event.note { + channel.note = note; } - if event.flags.contains(PatternEventFlags::ChangeInstrument) { - channel.instrument = Some(&event.instrument); + if let Some(instrument) = &event.instrument { + channel.instrument = Some(instrument); channel.sample_posion = SamplePosition::default(); } - if event.flags.contains(PatternEventFlags::ChangeVolume) { - channel.volume = event.volume as f32 / u8::MAX as f32; + if let Some(volume) = event.volume { + channel.volume = volume as f32 / u8::MAX as f32; } // Process effects - let effects = [ - if event.flags.contains(PatternEventFlags::DoEffect1) { - Some(&event.effect_1) - } else { - None - }, - if event.flags.contains(PatternEventFlags::DoEffect2) { - Some(&event.effect_2) - } else { - None - }, - ]; - for effect in effects.into_iter().flatten() { + for effect in event.effects.iter().flatten() { match effect.kind { // TODO(nenikitov): Add effects PatternEffectKind::Speed => { @@ -128,7 +116,7 @@ impl Default for SamplePosition { #[derive(Default)] struct Channel<'a> { - instrument: Option<&'a TPatternInstrumentKind>, + instrument: Option<&'a PatternEventInstrumentKind>, sample_posion: SamplePosition, volume: f32, @@ -140,11 +128,11 @@ impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument && let PatternEventNote::On(note) = self.note - && let TPatternInstrumentKind::Predefined(instrument) = instrument + && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { let sample = sample.sample_full().to_vec().volume(self.volume); - self.note = PatternEventNote::None; + self.note = PatternEventNote::Off; sample } else { vec![] diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 18b1c22..24f778d 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -9,29 +9,30 @@ use std::rc::Rc; #[derive(Default, PartialEq, Clone, Copy)] pub enum PatternEventNote { #[default] - None, - On(u8), Off, + On(u8), } -impl AssetParser for PatternEventNote { +impl AssetParser for Option { type Output = Self; - type Context<'ctx> = (); + type Context<'ctx> = bool; - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, note) = number::le_u8(input)?; - let note = match note { - 0 => PatternEventNote::None, - 1..=95 => PatternEventNote::On(note), - 96 => PatternEventNote::Off, - // TODO(nenikitov): Should be a `Result` - _ => unreachable!("Note should be in range 0-96"), - }; - - Ok((input, note)) + Ok(( + input, + should_parse.then(|| { + match note { + 1..=95 => PatternEventNote::On(note), + 96 => PatternEventNote::Off, + // TODO(nenikitov): Should be a `Result` + _ => unreachable!("Note should be in range 0-96"), + } + }), + )) } } } @@ -42,8 +43,8 @@ bitflags! { const ChangeNote = 1 << 0; const ChangeInstrument = 1 << 1; const ChangeVolume = 1 << 2; - const DoEffect1 = 1 << 3; - const DoEffect2 = 1 << 4; + const ChangeEffect1 = 1 << 3; + const ChangeEffect2 = 1 << 4; } } @@ -163,41 +164,66 @@ pub struct PatternEffect { pub value: u8, } -impl AssetParser for PatternEffect { - type Output = PatternEffect; +impl AssetParser for Option { + type Output = Self; - type Context<'ctx> = (); + type Context<'ctx> = bool; - fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, kind) = number::le_u8(input)?; let (input, value) = number::le_u8(input)?; Ok(( input, - Self { + should_parse.then(|| PatternEffect { kind: kind.into(), value, - }, + }), )) } } } #[derive(Debug)] -pub enum TPatternInstrumentKind { +pub enum PatternEventInstrumentKind { // TODO(nenikitov): Figure out what instrument `255` is Special, Predefined(Rc), } +impl AssetParser for Option { + type Output = Self; + + type Context<'ctx> = (bool, &'ctx [Rc]); + + fn parser( + (should_parse, instruments): Self::Context<'_>, + ) -> impl Fn(Input) -> Result { + move |input| { + let (input, instrument) = number::le_u8(input)?; + + Ok(( + input, + should_parse.then(|| { + if instrument == 255 { + PatternEventInstrumentKind::Special + } else { + PatternEventInstrumentKind::Predefined( + instruments[instrument as usize].clone(), + ) + } + }), + )) + } + } +} + pub struct PatternEvent { - pub flags: PatternEventFlags, - pub note: PatternEventNote, - pub instrument: TPatternInstrumentKind, - pub volume: u8, - pub effect_1: PatternEffect, - pub effect_2: PatternEffect, + pub note: Option, + pub instrument: Option, + pub volume: Option, + pub effects: [Option; 2], } impl AssetParser for PatternEvent { @@ -208,28 +234,36 @@ impl AssetParser for PatternEvent { fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, flags) = PatternEventFlags::parser(())(input)?; - let (input, note) = PatternEventNote::parser(())(input)?; - let (input, instrument_index) = number::le_u8(input)?; + + let (input, note) = >::parser( + flags.contains(PatternEventFlags::ChangeNote), + )(input)?; + + let (input, instrument) = >::parser(( + (flags.contains(PatternEventFlags::ChangeInstrument)), + instruments, + ))(input)?; + let (input, volume) = number::le_u8(input)?; - let (input, effect_1) = PatternEffect::parser(())(input)?; - let (input, effect_2) = PatternEffect::parser(())(input)?; + let volume = flags + .contains(PatternEventFlags::ChangeVolume) + .then_some(volume); + + let (input, effect_1) = >::parser( + flags.contains(PatternEventFlags::ChangeEffect1), + )(input)?; + + let (input, effect_2) = >::parser( + flags.contains(PatternEventFlags::ChangeEffect2), + )(input)?; Ok(( input, Self { - // TODO(nenikitov): Use `Result` - flags, - note: note.into(), - instrument: if instrument_index == 255 { - TPatternInstrumentKind::Special - } else { - TPatternInstrumentKind::Predefined( - instruments[instrument_index as usize].clone(), - ) - }, + note, + instrument, volume, - effect_1, - effect_2, + effects: [effect_1, effect_2], }, )) } From 539f75ef4f17699736b98862e128eaba83be909d Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 3 Feb 2024 02:15:01 -0500 Subject: [PATCH 09/87] refactor: remove unecessary option of pattern and replace by empty pattern --- engine/src/asset/sound/dat/mixer.rs | 2 - engine/src/asset/sound/dat/pattern_event.rs | 82 +++++++++------------ engine/src/asset/sound/dat/t_song.rs | 6 +- 3 files changed, 38 insertions(+), 52 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index a78db47..17d58a1 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -47,7 +47,6 @@ impl TSongMixerUtils for TSong { for row in pattern.deref() { // Update channels for (c, event) in row.iter().enumerate() { - let Some(event) = event else { continue }; let channel = &mut channels[c]; // Process note @@ -124,7 +123,6 @@ struct Channel<'a> { } impl<'a> Channel<'a> { - // TODO(nenikitov): Don not pass `samples`, it should somehow be stored in the instrument fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument && let PatternEventNote::On(note) = self.note diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 24f778d..7c4e3e1 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -45,6 +45,7 @@ bitflags! { const ChangeVolume = 1 << 2; const ChangeEffect1 = 1 << 3; const ChangeEffect2 = 1 << 4; + const IsEmpty = 1 << 5; } } @@ -219,6 +220,7 @@ impl AssetParser for Option { } } +#[derive(Default)] pub struct PatternEvent { pub note: Option, pub instrument: Option, @@ -235,54 +237,40 @@ impl AssetParser for PatternEvent { move |input| { let (input, flags) = PatternEventFlags::parser(())(input)?; - let (input, note) = >::parser( - flags.contains(PatternEventFlags::ChangeNote), - )(input)?; - - let (input, instrument) = >::parser(( - (flags.contains(PatternEventFlags::ChangeInstrument)), - instruments, - ))(input)?; - - let (input, volume) = number::le_u8(input)?; - let volume = flags - .contains(PatternEventFlags::ChangeVolume) - .then_some(volume); - - let (input, effect_1) = >::parser( - flags.contains(PatternEventFlags::ChangeEffect1), - )(input)?; - - let (input, effect_2) = >::parser( - flags.contains(PatternEventFlags::ChangeEffect2), - )(input)?; - - Ok(( - input, - Self { - note, - instrument, - volume, - effects: [effect_1, effect_2], - }, - )) - } - } -} - -impl AssetParser for Option { - type Output = Self; - - type Context<'ctx> = &'ctx [Rc]; - - fn parser(instruments: Self::Context<'_>) -> impl Fn(Input) -> Result { - move |input| { - let (after_flags, flags) = number::le_u8(input)?; - if (flags & 0x20) != 0 { - Ok((after_flags, None)) + if flags.contains(PatternEventFlags::IsEmpty) { + Ok((input, Self::default())) } else { - let (input, pattern) = PatternEvent::parser(instruments)(input)?; - Ok((input, Some(pattern))) + let (input, note) = >::parser( + flags.contains(PatternEventFlags::ChangeNote), + )(input)?; + + let (input, instrument) = >::parser(( + (flags.contains(PatternEventFlags::ChangeInstrument)), + instruments, + ))(input)?; + + let (input, volume) = number::le_u8(input)?; + let volume = flags + .contains(PatternEventFlags::ChangeVolume) + .then_some(volume); + + let (input, effect_1) = >::parser( + flags.contains(PatternEventFlags::ChangeEffect1), + )(input)?; + + let (input, effect_2) = >::parser( + flags.contains(PatternEventFlags::ChangeEffect2), + )(input)?; + + Ok(( + input, + Self { + note, + instrument, + volume, + effects: [effect_1, effect_2], + }, + )) } } } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index b842880..1013395 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -6,7 +6,7 @@ use crate::{ use itertools::Itertools; use std::rc::Rc; -pub type PatternRow = Vec>; +pub type PatternRow = Vec; pub type Pattern = Vec; pub struct TSong { @@ -14,7 +14,7 @@ pub struct TSong { pub speed: u8, pub restart_order: u8, pub orders: Vec>, - /// Reusable and repeatable sequence -> Row -> Channel (`None` to play nothing) + /// Reusable and repeatable sequence -> Row -> Channel pub patterns: Vec>, pub instruments: Vec>, pub samples: Vec>, @@ -61,7 +61,7 @@ impl AssetParser for TSong { .zip(lengths) .map(|(input, length)| { multi::count!( - >::parser(&instruments), + PatternEvent::parser(&instruments), header.channel_count as usize * length as usize )(input) }) From 2e41b883560c8ab6ead3e1a3ccef8affff08bc0f Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 3 Feb 2024 22:18:11 -0500 Subject: [PATCH 10/87] feat: make samples loop properly --- engine/src/asset/sound/dat/mixer.rs | 32 ++++++++++------------ engine/src/asset/sound/dat/t_instrument.rs | 11 +++----- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 17d58a1..b553690 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -52,10 +52,10 @@ impl TSongMixerUtils for TSong { // Process note if let Some(note) = event.note { channel.note = note; + channel.sample_posion = 0; } if let Some(instrument) = &event.instrument { channel.instrument = Some(instrument); - channel.sample_posion = SamplePosition::default(); } if let Some(volume) = event.volume { channel.volume = volume as f32 / u8::MAX as f32; @@ -101,22 +101,11 @@ impl TSongMixerUtils for TSong { } } -#[derive(PartialEq)] -enum SamplePosition { - Beginning, - Loop(isize), -} - -impl Default for SamplePosition { - fn default() -> Self { - Self::Beginning - } -} - #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, - sample_posion: SamplePosition, + + sample_posion: usize, volume: f32, note: PatternEventNote, @@ -129,9 +118,18 @@ impl<'a> Channel<'a> { && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { - let sample = sample.sample_full().to_vec().volume(self.volume); - self.note = PatternEventNote::Off; - sample + let data = sample + .sample_beginning() + .iter() + .chain(sample.sample_loop().iter().cycle()) + .skip(self.sample_posion) + .take(duration) + .copied() + .collect::>(); + + self.sample_posion += duration; + + data } else { vec![] } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 850ba3b..f49f908 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -1,15 +1,16 @@ -use std::rc::Rc; - use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; use bitflags::bitflags; +use std::rc::Rc; +// TODO(nenikitov): Double check these flags bitflags! { #[derive(Debug, Clone, Copy)] pub struct TInstrumentFlags: u8 { - const _ = 1 << 0; + const HasVolumeEnveloppe = 1 << 0; + const HasPanEnveloppe = 1 << 1; } } @@ -177,10 +178,6 @@ impl AssetParser for TSample { } impl TSample { - pub fn sample_full(&self) -> &[i16] { - &self.data - } - pub fn sample_beginning(&self) -> &[i16] { &self.data[..self.data.len() - self.loop_length as usize] } From 7dd40a9055c5a08a48e8adc17544a31826d155af Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 5 Feb 2024 17:42:43 -0500 Subject: [PATCH 11/87] wip: make event pitch --- engine/src/asset/sound/dat/mixer.rs | 36 +++++++++++++++------ engine/src/asset/sound/dat/pattern_event.rs | 2 +- engine/src/asset/sound/mod.rs | 6 ++-- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index b553690..3ab3e50 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -52,7 +52,7 @@ impl TSongMixerUtils for TSong { // Process note if let Some(note) = event.note { channel.note = note; - channel.sample_posion = 0; + channel.sample_position = 0; } if let Some(instrument) = &event.instrument { channel.instrument = Some(instrument); @@ -105,12 +105,18 @@ impl TSongMixerUtils for TSong { struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, - sample_posion: usize, + sample_position: usize, volume: f32, note: PatternEventNote, } +fn note_to_pitch(note: u8) -> f32 { + 440.0 * 2.0f32.powf((note as f32 - 49.0) / 12.0) +} + +const BASE_NOTE: u8 = 48; + impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument @@ -118,18 +124,23 @@ impl<'a> Channel<'a> { && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { + let pitch_factor = note_to_pitch(BASE_NOTE) / note_to_pitch(note); + + let duration_scaled = (duration as f32 / pitch_factor).round() as usize; + let data = sample .sample_beginning() .iter() .chain(sample.sample_loop().iter().cycle()) - .skip(self.sample_posion) - .take(duration) + .skip(self.sample_position) + .take(duration_scaled) .copied() .collect::>(); - self.sample_posion += duration; + self.sample_position += duration_scaled; - data + let factor = duration as f32 / data.len() as f32; + data.volume(self.volume).pitch_with_time_stretch(factor) } else { vec![] } @@ -169,13 +180,20 @@ impl Mixer { } pub trait SoundEffect { - fn pitch(self, note: u8) -> Sample; + fn pitch_with_time_stretch(self, note: f32) -> Sample; fn volume(self, volume: f32) -> Sample; } impl SoundEffect for Sample { - fn pitch(self, note: u8) -> Sample { - todo!("(nenikitov): Figure out how this work") + fn pitch_with_time_stretch(self, factor: f32) -> Sample { + let len = (self.len() as f32 * factor).floor() as usize; + let mut result = Vec::with_capacity(len); + + for i in 0..len { + result.push(self[(i as f32 / factor).floor() as usize]); + } + + result } fn volume(self, volume: f32) -> Sample { diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 7c4e3e1..fc40b38 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -6,7 +6,7 @@ use crate::{ use bitflags::bitflags; use std::rc::Rc; -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Default, PartialEq, Clone, Copy, Debug)] pub enum PatternEventNote { #[default] Off, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index ece026a..9b1d2a7 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,7 +1,6 @@ mod dat; use self::dat::mixer::TSongMixer; - use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -95,13 +94,14 @@ mod tests { })?; // TODO(nenikitov): Remove this debug code - let fail_music = sounds + let test_music = sounds .iter() .filter_map(|s| match s { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0xA]; + .collect::>()[0xC]; + dbg!(&test_music.patterns[1][0][6].note); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From b6adcc3127f024459e49fbccf563281195aa123c Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 5 Feb 2024 19:58:53 -0500 Subject: [PATCH 12/87] fix: proper bpm and note pitch --- engine/src/asset/sound/dat/mixer.rs | 21 +++++++++------------ engine/src/asset/sound/dat/t_instrument.rs | 4 ++-- engine/src/asset/sound/mod.rs | 7 ++++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 3ab3e50..8e3fca8 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -52,10 +52,10 @@ impl TSongMixerUtils for TSong { // Process note if let Some(note) = event.note { channel.note = note; - channel.sample_position = 0; } if let Some(instrument) = &event.instrument { channel.instrument = Some(instrument); + channel.sample_position = 0; } if let Some(volume) = event.volume { channel.volume = volume as f32 / u8::MAX as f32; @@ -97,7 +97,8 @@ impl TSongMixerUtils for TSong { } fn seconds_per_tick(bpm: usize, speed: usize) -> f32 { - 60.0 / (bpm * speed) as f32 + // TODO(nenikitov): Figure out what constant `24` means (maybe `6` default speed * `4` beats/measure???) + 60.0 / (bpm * 24 / speed) as f32 } } @@ -115,7 +116,7 @@ fn note_to_pitch(note: u8) -> f32 { 440.0 * 2.0f32.powf((note as f32 - 49.0) / 12.0) } -const BASE_NOTE: u8 = 48; +const BASE_NOTE: u8 = 60; impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { @@ -124,7 +125,8 @@ impl<'a> Channel<'a> { && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] { - let pitch_factor = note_to_pitch(BASE_NOTE) / note_to_pitch(note); + let pitch_factor = note_to_pitch((BASE_NOTE as i32 - sample.finetune / 128) as u8) + / note_to_pitch(note); let duration_scaled = (duration as f32 / pitch_factor).round() as usize; @@ -187,13 +189,10 @@ pub trait SoundEffect { impl SoundEffect for Sample { fn pitch_with_time_stretch(self, factor: f32) -> Sample { let len = (self.len() as f32 * factor).floor() as usize; - let mut result = Vec::with_capacity(len); - - for i in 0..len { - result.push(self[(i as f32 / factor).floor() as usize]); - } - result + (0..len) + .map(|i| self[(i as f32 / factor).floor() as usize]) + .collect() } fn volume(self, volume: f32) -> Sample { @@ -202,5 +201,3 @@ impl SoundEffect for Sample { .collect() } } - -fn note_frequency(note: u8) {} diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index f49f908..1f38c0e 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -135,7 +135,7 @@ pub struct TSample { pub volume: u8, pub panning: u8, pub align: u8, - pub finetune: u32, + pub finetune: i32, pub loop_length: u32, pub data: Vec, } @@ -151,7 +151,7 @@ impl AssetParser for TSample { let (input, volume) = number::le_u8(input)?; let (input, panning) = number::le_u8(input)?; let (input, align) = number::le_u8(input)?; - let (input, finetune) = number::le_u32(input)?; + let (input, finetune) = number::le_i32(input)?; let (input, loop_length) = number::le_u32(input)?; let (input, loop_end) = number::le_u32(input)?; let (input, sample_offset) = number::le_u32(input)?; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 9b1d2a7..fa7ed7d 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -101,7 +101,12 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0xC]; - dbg!(&test_music.patterns[1][0][6].note); + + dbg!(&test_music + .instruments + .iter() + .map(|s| s.volume_end) + .collect::>()); let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From 3649f23542aba83f8b4607a0e5559aa095541dad Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 11:20:10 -0500 Subject: [PATCH 13/87] refactor: move all note adding and frequency ratio computations into a separate finetune struct --- Cargo.lock | 7 + engine/Cargo.toml | 1 + engine/src/asset/sound/dat/finetune.rs | 151 ++++++++++++++++++++ engine/src/asset/sound/dat/mixer.rs | 14 +- engine/src/asset/sound/dat/mod.rs | 1 + engine/src/asset/sound/dat/pattern_event.rs | 6 +- engine/src/asset/sound/dat/t_instrument.rs | 6 +- 7 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 engine/src/asset/sound/dat/finetune.rs diff --git a/Cargo.lock b/Cargo.lock index e2055a5..d21b5aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "engine", ] +[[package]] +name = "assert_approx_eq" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" + [[package]] name = "autocfg" version = "1.1.0" @@ -163,6 +169,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" name = "engine" version = "0.1.0" dependencies = [ + "assert_approx_eq", "bitflags 2.4.2", "const_format", "eyre", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index c629458..f4fd223 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -17,6 +17,7 @@ sealed = "0.5.0" thiserror = "1.0.56" [dev-dependencies] +assert_approx_eq = "1.1.0" eyre = "0.6.8" image = "0.24.8" diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs new file mode 100644 index 0000000..6424373 --- /dev/null +++ b/engine/src/asset/sound/dat/finetune.rs @@ -0,0 +1,151 @@ +use std::ops::{Add, Div, Sub}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FineTune { + cents: i32, +} + +impl FineTune { + const BASE_NOTE: FineTune = FineTune::new_from_note(49); + const BASE_FREQUENCY: f32 = 440.0; + + const CENTS_PER_NOTE: i32 = 128; + + pub const fn new(cents: i32) -> Self { + Self { cents } + } + + pub const fn new_from_note(note: i32) -> Self { + FineTune::new(note * Self::CENTS_PER_NOTE) + } + + pub fn frequency(&self) -> f32 { + Self::BASE_FREQUENCY + * 2.0f32 + .powf((*self - Self::BASE_NOTE).cents as f32 / (12 * Self::CENTS_PER_NOTE) as f32) + } + + pub fn cents(&self) -> i32 { + self.cents + } + + pub fn note(&self) -> i32 { + self.cents / Self::CENTS_PER_NOTE + } +} + +impl Add for FineTune { + type Output = FineTune; + + fn add(self, rhs: Self) -> Self::Output { + FineTune::new(self.cents.saturating_add(rhs.cents)) + } +} + +impl Sub for FineTune { + type Output = FineTune; + + fn sub(self, rhs: Self) -> Self::Output { + FineTune::new(self.cents.saturating_sub(rhs.cents)) + } +} + +impl Div for FineTune { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.frequency() / rhs.frequency() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_approx_eq::assert_approx_eq; + + #[test] + fn new_works() { + assert_eq!(FineTune { cents: 1000 }, FineTune::new(1000)); + } + + #[test] + fn new_from_note_works() { + assert_eq!( + FineTune { + cents: 30 * FineTune::CENTS_PER_NOTE + }, + FineTune::new_from_note(30) + ); + } + + #[test] + fn new_cent_to_note_relation_works() { + assert_eq!( + FineTune::new(FineTune::CENTS_PER_NOTE), + FineTune::new_from_note(1) + ); + } + + #[test] + fn cents_works() { + assert_eq!(100, FineTune::new(100).cents()) + } + + #[test] + fn note_works() { + assert_eq!(5, FineTune::new_from_note(5).cents()) + } + + #[test] + fn frequency_works() { + assert_eq!(440.0, FineTune::BASE_NOTE.frequency()); + assert_eq!( + 880.0, + (FineTune::BASE_NOTE + FineTune::new_from_note(12)).frequency() + ); + assert_eq!( + 220.0, + (FineTune::BASE_NOTE - FineTune::new_from_note(12)).frequency() + ); + assert_approx_eq!( + 392.0, + (FineTune::BASE_NOTE - FineTune::new_from_note(2)).frequency(), + 0.25 + ); + } + + #[test] + fn add_works() { + assert_eq!( + FineTune::new_from_note(54), + FineTune::new_from_note(49) + FineTune::new_from_note(5), + ); + } + + #[test] + fn sub_works() { + assert_eq!( + FineTune::new_from_note(32), + FineTune::new_from_note(40) - FineTune::new_from_note(8), + ); + } + + #[test] + fn div_works() { + assert_approx_eq!( + 2.0, + FineTune::new_from_note(30) / FineTune::new_from_note(18), + 0.01 + ); + assert_approx_eq!( + 0.5, + FineTune::new_from_note(18) / FineTune::new_from_note(30), + 0.01 + ); + assert_approx_eq!( + 1.5, + FineTune::new_from_note(17) / FineTune::new_from_note(10), + 0.01 + ); + } +} diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 8e3fca8..037dafe 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,3 +1,5 @@ +use crate::asset::sound::dat::finetune::FineTune; + use super::{pattern_event::*, t_instrument::*, t_song::*}; use std::ops::Deref; @@ -112,21 +114,17 @@ struct Channel<'a> { note: PatternEventNote, } -fn note_to_pitch(note: u8) -> f32 { - 440.0 * 2.0f32.powf((note as f32 - 49.0) / 12.0) -} - -const BASE_NOTE: u8 = 60; +// TODO(nenikitov): Double check that the game actually uses C_5 as a base note for all samples +const BASE_NOTE: FineTune = FineTune::new_from_note(60); impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument && let PatternEventNote::On(note) = self.note && let PatternEventInstrumentKind::Predefined(instrument) = instrument - && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note as usize] + && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note.note() as usize] { - let pitch_factor = note_to_pitch((BASE_NOTE as i32 - sample.finetune / 128) as u8) - / note_to_pitch(note); + let pitch_factor = (BASE_NOTE - sample.finetune) / note; let duration_scaled = (duration as f32 / pitch_factor).round() as usize; diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index b24ed43..1e6e152 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -8,6 +8,7 @@ pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; +pub mod finetune; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index fc40b38..7a6c2c3 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -1,4 +1,4 @@ -use super::t_instrument::*; +use super::{finetune::FineTune, t_instrument::*}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -10,7 +10,7 @@ use std::rc::Rc; pub enum PatternEventNote { #[default] Off, - On(u8), + On(FineTune), } impl AssetParser for Option { @@ -26,7 +26,7 @@ impl AssetParser for Option { input, should_parse.then(|| { match note { - 1..=95 => PatternEventNote::On(note), + 1..=95 => PatternEventNote::On(FineTune::new_from_note(note as i32)), 96 => PatternEventNote::Off, // TODO(nenikitov): Should be a `Result` _ => unreachable!("Note should be in range 0-96"), diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 1f38c0e..8054644 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -5,6 +5,8 @@ use crate::{ use bitflags::bitflags; use std::rc::Rc; +use super::finetune::FineTune; + // TODO(nenikitov): Double check these flags bitflags! { #[derive(Debug, Clone, Copy)] @@ -135,7 +137,7 @@ pub struct TSample { pub volume: u8, pub panning: u8, pub align: u8, - pub finetune: i32, + pub finetune: FineTune, pub loop_length: u32, pub data: Vec, } @@ -168,7 +170,7 @@ impl AssetParser for TSample { volume, panning, align, - finetune, + finetune: FineTune::new(finetune), loop_length, data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), }, From 441ec03eda584a6ffb88b76f5d9aec667db340b0 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 11:59:49 -0500 Subject: [PATCH 14/87] feat: make audio stretching linearly interpolate --- engine/src/asset/sound/dat/finetune.rs | 2 +- engine/src/asset/sound/dat/mixer.rs | 71 ++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 6424373..63b435d 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -93,7 +93,7 @@ mod tests { #[test] fn note_works() { - assert_eq!(5, FineTune::new_from_note(5).cents()) + assert_eq!(5, FineTune::new_from_note(5).note()) } #[test] diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 037dafe..f16ae77 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -122,7 +122,8 @@ impl<'a> Channel<'a> { if let Some(instrument) = self.instrument && let PatternEventNote::On(note) = self.note && let PatternEventInstrumentKind::Predefined(instrument) = instrument - && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note.note() as usize] + && let TInstrumentSampleKind::Predefined(sample) = + &instrument.samples[note.note() as usize] { let pitch_factor = (BASE_NOTE - sample.finetune) / note; @@ -180,22 +181,72 @@ impl Mixer { } pub trait SoundEffect { - fn pitch_with_time_stretch(self, note: f32) -> Sample; - fn volume(self, volume: f32) -> Sample; + fn volume(self, volume: f32) -> Self; + fn pitch_with_time_stretch(self, factor: f32) -> Self; } impl SoundEffect for Sample { - fn pitch_with_time_stretch(self, factor: f32) -> Sample { - let len = (self.len() as f32 * factor).floor() as usize; + fn volume(self, volume: f32) -> Self { + self.into_iter() + .map(|s| (s as f32 * volume) as i16) + .collect() + } + fn pitch_with_time_stretch(self, factor: f32) -> Self { + let len = (self.len() as f32 * factor as f32).round() as usize; + (0..len) - .map(|i| self[(i as f32 / factor).floor() as usize]) + .map(|i| { + let frac = i as f32 / factor; + let index = (frac).floor() as usize; + let frac = frac - index as f32; + + let sample_1 = self[index]; + let sample_2 = if index + 1 < self.len() { + self[index + 1] + } else { + self[index] + }; + + sample_1 + ((sample_2 - sample_1) as f32 * frac) as i16 + }) .collect() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sound_effect_volume_works() { + assert_eq!( + vec![-10, 20, 40, 30, -78], + vec![-10, 20, 40, 30, -78].volume(1.0), + ); + assert_eq!( + vec![-40, 80, 160, 120, -312], + vec![-20, 40, 80, 60, -156].volume(2.0) + ); + assert_eq!( + vec![-10, 20, 40, 30, -78], + vec![-20, 40, 80, 60, -156].volume(0.5) + ); + } - fn volume(self, volume: f32) -> Sample { - self.into_iter() - .map(|s| (s as f32 * volume) as i16) - .collect() + #[test] + fn pitch_with_time_stretch_works() { + assert_eq!( + vec![-10, 20, 40, 30, -78], + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0), + ); + assert_eq!( + vec![-10, 5, 20, 30, 40, 35, 30, -24, -78, -78], + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0), + ); + assert_eq!( + vec![-10, 40, -78], + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5), + ); } } From 70da45dd5f2f6a69fd444631785432aaab238f41 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 12:23:57 -0500 Subject: [PATCH 15/87] fix: clicks with linear interpolation --- engine/src/asset/sound/dat/mixer.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index f16ae77..3d816b1 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -134,14 +134,26 @@ impl<'a> Channel<'a> { .iter() .chain(sample.sample_loop().iter().cycle()) .skip(self.sample_position) - .take(duration_scaled) + // TODO(nenikitov): This `+1` is necessary for linear interpolation. + // If not present, linear interpolation can produce clicks. + // If interpolation is reverted, this magic shouldn't be here + .take(duration_scaled + 1) .copied() .collect::>(); self.sample_position += duration_scaled; - let factor = duration as f32 / data.len() as f32; - data.volume(self.volume).pitch_with_time_stretch(factor) + // TODO(nenikitov): Same here, these `+1` and `pop` are necessary for linear interpolation. + // If interpolation is reverted, this magic shouldn't be here + let pitch_factor = (duration + 1) as f32 / data.len() as f32; + let mut data = data + .volume(self.volume) + .pitch_with_time_stretch(pitch_factor); + if (data.len() != duration) { + data.pop(); + } + + data } else { vec![] } @@ -193,21 +205,24 @@ impl SoundEffect for Sample { } fn pitch_with_time_stretch(self, factor: f32) -> Self { + // TODO(nenikitov): Linear interpolation sounds nicer and less crusty, but + // Introduces occasional clicks. + // Maybe it should be removed. let len = (self.len() as f32 * factor as f32).round() as usize; - + (0..len) .map(|i| { let frac = i as f32 / factor; let index = (frac).floor() as usize; let frac = frac - index as f32; - + let sample_1 = self[index]; let sample_2 = if index + 1 < self.len() { self[index + 1] } else { self[index] }; - + sample_1 + ((sample_2 - sample_1) as f32 * frac) as i16 }) .collect() From b06854e1c9dd05af5eded58118f54ad0e8c61b40 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 12:37:49 -0500 Subject: [PATCH 16/87] fix: overflow problems --- engine/src/asset/sound/dat/mixer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 3d816b1..84fac2d 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -223,7 +223,7 @@ impl SoundEffect for Sample { self[index] }; - sample_1 + ((sample_2 - sample_1) as f32 * frac) as i16 + ((1.0 - frac) * sample_1 as f32 + frac * sample_2 as f32) as i16 }) .collect() } From b395bf1800089b7ea408223a204f84aa23d64e0f Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 13:02:22 -0500 Subject: [PATCH 17/87] feat: make audio samples resampled to 48khz --- engine/src/asset/sound/dat/mixer.rs | 22 ++++++++++++---------- engine/src/asset/sound/dat/t_instrument.rs | 11 +++++++---- engine/src/asset/sound/mod.rs | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 84fac2d..636f0c6 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -24,7 +24,7 @@ impl TSongMixer for TSong { } trait TSongMixerUtils { - const SAMPLE_RATE: usize = 16000; + const SAMPLE_RATE: usize = 48000; const CHANNEL_COUNT: usize = 1; fn mix(&self, start: usize) -> Sample; @@ -146,9 +146,9 @@ impl<'a> Channel<'a> { // TODO(nenikitov): Same here, these `+1` and `pop` are necessary for linear interpolation. // If interpolation is reverted, this magic shouldn't be here let pitch_factor = (duration + 1) as f32 / data.len() as f32; - let mut data = data - .volume(self.volume) - .pitch_with_time_stretch(pitch_factor); + let mut data = + data.volume(self.volume) + .pitch_with_time_stretch(pitch_factor, true, false); if (data.len() != duration) { data.pop(); } @@ -194,7 +194,7 @@ impl Mixer { pub trait SoundEffect { fn volume(self, volume: f32) -> Self; - fn pitch_with_time_stretch(self, factor: f32) -> Self; + fn pitch_with_time_stretch(self, factor: f32, interpolate: bool, loop_end: bool) -> Self; } impl SoundEffect for Sample { @@ -204,7 +204,7 @@ impl SoundEffect for Sample { .collect() } - fn pitch_with_time_stretch(self, factor: f32) -> Self { + fn pitch_with_time_stretch(self, factor: f32, interpolate: bool, loop_end: bool) -> Self { // TODO(nenikitov): Linear interpolation sounds nicer and less crusty, but // Introduces occasional clicks. // Maybe it should be removed. @@ -214,11 +214,13 @@ impl SoundEffect for Sample { .map(|i| { let frac = i as f32 / factor; let index = (frac).floor() as usize; - let frac = frac - index as f32; + let frac = if interpolate { frac - index as f32 } else { 0.0 }; let sample_1 = self[index]; let sample_2 = if index + 1 < self.len() { self[index + 1] + } else if loop_end { + self[0] } else { self[index] }; @@ -253,15 +255,15 @@ mod tests { fn pitch_with_time_stretch_works() { assert_eq!( vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, true, false), ); assert_eq!( vec![-10, 5, 20, 30, 40, 35, 30, -24, -78, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, true, false), ); assert_eq!( vec![-10, 40, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, true, false), ); } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 8054644..e217ca1 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -5,7 +5,7 @@ use crate::{ use bitflags::bitflags; use std::rc::Rc; -use super::finetune::FineTune; +use super::{finetune::FineTune, mixer::SoundEffect}; // TODO(nenikitov): Double check these flags bitflags! { @@ -158,7 +158,7 @@ impl AssetParser for TSample { let (input, loop_end) = number::le_u32(input)?; let (input, sample_offset) = number::le_u32(input)?; - // The game uses offset for `i16`, but it's much more convenient to just use indices + // The game uses offset for `i16`, but it's much more convenient to just use indices, so that's why `/ 2` let loop_end = loop_end / 2; let sample_offset = sample_offset / 2; let loop_length = loop_length / 2; @@ -171,8 +171,11 @@ impl AssetParser for TSample { panning, align, finetune: FineTune::new(finetune), - loop_length, - data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), + // I resample sample data from 16k to 48k Hz, so offset should be `* 3` + loop_length: loop_length * 3, + data: sample_data[sample_offset as usize..loop_end as usize] + .to_vec() + .pitch_with_time_stretch(3.0, true, false), }, )) } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index fa7ed7d..9d8d1bb 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -27,7 +27,7 @@ impl Sound { pub struct SoundCollection; impl SoundCollection { - pub const SAMPLE_RATE: usize = 16000; + pub const SAMPLE_RATE: usize = 48000; pub const CHANNEL_COUNT: usize = 1; } From d4ee7b2f70ea6a1e4898bf63ffad4fa4832f7076 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 15:38:12 -0500 Subject: [PATCH 18/87] refactor: remove unecessary parameter --- engine/src/asset/sound/dat/mixer.rs | 24 ++++++++++++---------- engine/src/asset/sound/dat/t_instrument.rs | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 636f0c6..40048ef 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -146,9 +146,9 @@ impl<'a> Channel<'a> { // TODO(nenikitov): Same here, these `+1` and `pop` are necessary for linear interpolation. // If interpolation is reverted, this magic shouldn't be here let pitch_factor = (duration + 1) as f32 / data.len() as f32; - let mut data = - data.volume(self.volume) - .pitch_with_time_stretch(pitch_factor, true, false); + let mut data = data + .volume(self.volume) + .pitch_with_time_stretch(pitch_factor, true); if (data.len() != duration) { data.pop(); } @@ -194,7 +194,7 @@ impl Mixer { pub trait SoundEffect { fn volume(self, volume: f32) -> Self; - fn pitch_with_time_stretch(self, factor: f32, interpolate: bool, loop_end: bool) -> Self; + fn pitch_with_time_stretch(self, factor: f32, interpolate: bool) -> Self; } impl SoundEffect for Sample { @@ -204,7 +204,7 @@ impl SoundEffect for Sample { .collect() } - fn pitch_with_time_stretch(self, factor: f32, interpolate: bool, loop_end: bool) -> Self { + fn pitch_with_time_stretch(self, factor: f32, interpolate: bool) -> Self { // TODO(nenikitov): Linear interpolation sounds nicer and less crusty, but // Introduces occasional clicks. // Maybe it should be removed. @@ -214,13 +214,15 @@ impl SoundEffect for Sample { .map(|i| { let frac = i as f32 / factor; let index = (frac).floor() as usize; - let frac = if interpolate { frac - index as f32 } else { 0.0 }; + let frac = if interpolate { + frac - index as f32 + } else { + 0.0 + }; let sample_1 = self[index]; let sample_2 = if index + 1 < self.len() { self[index + 1] - } else if loop_end { - self[0] } else { self[index] }; @@ -255,15 +257,15 @@ mod tests { fn pitch_with_time_stretch_works() { assert_eq!( vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, true, false), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, true), ); assert_eq!( vec![-10, 5, 20, 30, 40, 35, 30, -24, -78, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, true, false), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, true), ); assert_eq!( vec![-10, 40, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, true, false), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, true), ); } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index e217ca1..d9d1161 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -175,7 +175,7 @@ impl AssetParser for TSample { loop_length: loop_length * 3, data: sample_data[sample_offset as usize..loop_end as usize] .to_vec() - .pitch_with_time_stretch(3.0, true, false), + .pitch_with_time_stretch(3.0, true), }, )) } From def40725c73cd83351299f3ecbd39b224c56ff22 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 15:47:23 -0500 Subject: [PATCH 19/87] fix: remove interpolation for now --- engine/src/asset/sound/dat/mixer.rs | 5 +++-- engine/src/asset/sound/dat/t_instrument.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 40048ef..47a3819 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -147,8 +147,8 @@ impl<'a> Channel<'a> { // If interpolation is reverted, this magic shouldn't be here let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(self.volume) - .pitch_with_time_stretch(pitch_factor, true); + .volume(self.volume * 0.5) + .pitch_with_time_stretch(pitch_factor, false); if (data.len() != duration) { data.pop(); } @@ -208,6 +208,7 @@ impl SoundEffect for Sample { // TODO(nenikitov): Linear interpolation sounds nicer and less crusty, but // Introduces occasional clicks. // Maybe it should be removed. + // Or improved... let len = (self.len() as f32 * factor as f32).round() as usize; (0..len) diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index d9d1161..5cda648 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -175,7 +175,7 @@ impl AssetParser for TSample { loop_length: loop_length * 3, data: sample_data[sample_offset as usize..loop_end as usize] .to_vec() - .pitch_with_time_stretch(3.0, true), + .pitch_with_time_stretch(3.0, false), }, )) } From edc4a8666806247f8d762bc7ebfbf62586cce563 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 17:26:08 -0500 Subject: [PATCH 20/87] fix: volume of the instrument --- engine/src/asset/sound/dat/mixer.rs | 32 +++++++++++++++++++++++++---- engine/src/asset/sound/mod.rs | 20 ++++++++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 47a3819..57fc062 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -60,7 +60,11 @@ impl TSongMixerUtils for TSong { channel.sample_position = 0; } if let Some(volume) = event.volume { - channel.volume = volume as f32 / u8::MAX as f32; + if volume == u8::MAX { + channel.volume = ChannelVolume::Sample; + } else { + channel.volume = ChannelVolume::Value(volume); + } } // Process effects @@ -74,6 +78,9 @@ impl TSongMixerUtils for TSong { speed = effect.value; } } + PatternEffectKind::SampleOffset => { + channel.sample_position = effect.value as usize * 256 * 3 + } _ => {} } } @@ -86,7 +93,7 @@ impl TSongMixerUtils for TSong { sample_length_fractional = sample_length - sample_length.floor(); let sample_length = sample_length as usize; - for c in &mut channels { + for (i, c) in channels.iter_mut().enumerate() { m.add_sample(&c.tick(sample_length), offset); } @@ -104,13 +111,24 @@ impl TSongMixerUtils for TSong { } } +enum ChannelVolume { + Sample, + Value(u8), +} + +impl Default for ChannelVolume { + fn default() -> Self { + ChannelVolume::Value(0) + } +} + #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, sample_position: usize, - volume: f32, + volume: ChannelVolume, note: PatternEventNote, } @@ -125,6 +143,12 @@ impl<'a> Channel<'a> { && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note.note() as usize] { + let volume = match self.volume { + ChannelVolume::Sample => sample.volume, + ChannelVolume::Value(value) => value, + } as f32 + / u8::MAX as f32; + let pitch_factor = (BASE_NOTE - sample.finetune) / note; let duration_scaled = (duration as f32 / pitch_factor).round() as usize; @@ -147,7 +171,7 @@ impl<'a> Channel<'a> { // If interpolation is reverted, this magic shouldn't be here let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(self.volume * 0.5) + .volume(volume) .pitch_with_time_stretch(pitch_factor, false); if (data.len() != duration) { data.pop(); diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 9d8d1bb..22125f6 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -100,14 +100,26 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0xC]; + .collect::>()[0x8]; - dbg!(&test_music - .instruments + dbg!(&test_music.patterns[1] .iter() - .map(|s| s.volume_end) + .map(|r| &r[2].effects) .collect::>()); + test_music + .samples + .iter() + .enumerate() + .try_for_each(|(i, s)| { + let file = output_dir.join(format!("sample-{i}.wav")); + output_file( + file, + s.data + .to_wave(SoundCollection::SAMPLE_RATE, SoundCollection::CHANNEL_COUNT), + ) + })?; + let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); sounds From b34c4fa2e6cb30769d9838b2f9217b29f6377a37 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 17:57:28 -0500 Subject: [PATCH 21/87] format --- engine/src/asset/color_map.rs | 6 +++-- engine/src/asset/gamma_table.rs | 6 +++-- engine/src/asset/model/mod.rs | 25 ++++++++++++--------- engine/src/asset/pack_file.rs | 3 ++- engine/src/asset/pack_info.rs | 3 ++- engine/src/asset/skybox.rs | 3 ++- engine/src/asset/sound/dat/finetune.rs | 3 ++- engine/src/asset/sound/dat/mixer.rs | 4 ++-- engine/src/asset/sound/dat/mod.rs | 5 +++-- engine/src/asset/sound/dat/pattern_event.rs | 6 +++-- engine/src/asset/sound/dat/t_instrument.rs | 9 ++++---- engine/src/asset/sound/dat/t_song.rs | 6 +++-- engine/src/asset/sound/mod.rs | 3 ++- engine/src/asset/string_table.rs | 3 ++- engine/src/asset/texture/dat/texture.rs | 3 ++- engine/src/asset/texture/mod.rs | 6 +++-- engine/src/error.rs | 6 +++-- engine/src/utils/compression.rs | 3 ++- engine/src/utils/format.rs | 6 +++-- engine/src/utils/nom.rs | 4 ++-- engine/src/utils/test.rs | 4 +++- rustfmt.toml | 3 +++ 22 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 rustfmt.toml diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 495145b..79053ff 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -1,6 +1,7 @@ +use std::{mem, ops::Deref}; + use super::{extension::*, AssetParser}; use crate::{error, utils::nom::*}; -use std::{mem, ops::Deref}; const COLORS_COUNT: usize = 256; const SHADES_COUNT: usize = 32; @@ -90,9 +91,10 @@ impl AssetParser for ColorMap { #[cfg(test)] mod tests { + use std::cell::LazyCell; + use super::*; use crate::utils::{format::*, test::*}; - use std::cell::LazyCell; #[test] fn shade_works() -> eyre::Result<()> { diff --git a/engine/src/asset/gamma_table.rs b/engine/src/asset/gamma_table.rs index cadb9d9..050536e 100644 --- a/engine/src/asset/gamma_table.rs +++ b/engine/src/asset/gamma_table.rs @@ -1,6 +1,7 @@ +use std::mem; + use super::{extension::*, AssetParser}; use crate::{error, utils::nom::*}; -use std::mem; const ROWS_COUNT: usize = 256; const COLS_COUNT: usize = 101; @@ -47,12 +48,13 @@ impl AssetParser for GammaTable { #[cfg(test)] mod tests { + use std::cell::LazyCell; + use super::*; use crate::{ asset::color_map::Color, utils::{format::*, test::*}, }; - use std::cell::LazyCell; const GAMMA_TABLE_DATA: LazyCell> = deflated_file!("00.dat"); diff --git a/engine/src/asset/model/mod.rs b/engine/src/asset/model/mod.rs index af4a079..d91e57a 100644 --- a/engine/src/asset/model/mod.rs +++ b/engine/src/asset/model/mod.rs @@ -1,15 +1,18 @@ mod dat; +use dat::{ + frame::{ModelFrame, ModelSpecs}, + header::ModelHeader, + sequence::ModelSequence, + triangle::{ModelTriangle, TextureDimensions}, +}; + use super::{ extension::*, texture::dat::{size::TextureSize, texture::Texture}, AssetParser, }; use crate::utils::nom::*; -use dat::{ - frame::ModelFrame, frame::ModelSpecs, header::ModelHeader, sequence::ModelSequence, - triangle::ModelTriangle, triangle::TextureDimensions, -}; pub struct Model { pub texture: Texture, @@ -69,6 +72,14 @@ impl AssetParser for Model { #[cfg(test)] mod tests { + use std::{ + cell::LazyCell, + fmt::{Display, Formatter}, + path::PathBuf, + }; + + use itertools::Itertools; + use super::{ dat::{frame::ModelVertex, triangle::ModelPoint}, *, @@ -77,12 +88,6 @@ mod tests { asset::color_map::{Color, ColorMap, PaletteTexture}, utils::test::*, }; - use itertools::Itertools; - use std::{ - cell::LazyCell, - fmt::{Display, Formatter}, - path::PathBuf, - }; const COLOR_MAP_DATA: LazyCell> = deflated_file!("01.dat"); const MODEL_DATA: LazyCell> = deflated_file!("0E-deflated.dat"); diff --git a/engine/src/asset/pack_file.rs b/engine/src/asset/pack_file.rs index 996bb44..5e72455 100644 --- a/engine/src/asset/pack_file.rs +++ b/engine/src/asset/pack_file.rs @@ -86,9 +86,10 @@ impl PackFile { #[cfg(test)] mod tests { + use std::{cell::LazyCell, io, path::PathBuf}; + use super::*; use crate::utils::{compression::decompress, test::*}; - use std::{cell::LazyCell, io, path::PathBuf}; #[test] fn header_works() -> eyre::Result<()> { diff --git a/engine/src/asset/pack_info.rs b/engine/src/asset/pack_info.rs index c357f23..1906356 100644 --- a/engine/src/asset/pack_info.rs +++ b/engine/src/asset/pack_info.rs @@ -1,6 +1,7 @@ +use std::ops::Index; + use super::{extension::*, AssetParser}; use crate::utils::nom::*; -use std::ops::Index; #[derive(Debug)] pub struct PackInfo { diff --git a/engine/src/asset/skybox.rs b/engine/src/asset/skybox.rs index d347b10..b69d591 100644 --- a/engine/src/asset/skybox.rs +++ b/engine/src/asset/skybox.rs @@ -38,12 +38,13 @@ impl AssetParser for Skybox { #[cfg(test)] mod tests { + use std::cell::LazyCell; + use super::*; use crate::{ asset::color_map::PaletteTexture, utils::{format::*, test::*}, }; - use std::cell::LazyCell; const SKYBOX_DATA: LazyCell> = deflated_file!("3C.dat"); diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 63b435d..55a7ab5 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -60,9 +60,10 @@ impl Div for FineTune { #[cfg(test)] mod tests { - use super::*; use assert_approx_eq::assert_approx_eq; + use super::*; + #[test] fn new_works() { assert_eq!(FineTune { cents: 1000 }, FineTune::new(1000)); diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 57fc062..318121e 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,7 +1,7 @@ -use crate::asset::sound::dat::finetune::FineTune; +use std::ops::Deref; use super::{pattern_event::*, t_instrument::*, t_song::*}; -use std::ops::Deref; +use crate::asset::sound::dat::finetune::FineTune; type SamplePoint = i16; type Sample = Vec; diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 1e6e152..21ce860 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -1,14 +1,15 @@ -use lewton::inside_ogg::OggStreamReader; use std::io::Cursor; +use lewton::inside_ogg::OggStreamReader; + pub mod asset_header; pub mod chunk_header; +pub mod finetune; pub mod mixer; pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; -pub mod finetune; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 7a6c2c3..0def910 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -1,10 +1,12 @@ +use std::rc::Rc; + +use bitflags::bitflags; + use super::{finetune::FineTune, t_instrument::*}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; -use bitflags::bitflags; -use std::rc::Rc; #[derive(Default, PartialEq, Clone, Copy, Debug)] pub enum PatternEventNote { diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 5cda648..7a7fca6 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -1,11 +1,12 @@ +use std::rc::Rc; + +use bitflags::bitflags; + +use super::{finetune::FineTune, mixer::SoundEffect}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; -use bitflags::bitflags; -use std::rc::Rc; - -use super::{finetune::FineTune, mixer::SoundEffect}; // TODO(nenikitov): Double check these flags bitflags! { diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 1013395..c536299 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -1,10 +1,12 @@ +use std::rc::Rc; + +use itertools::Itertools; + use super::{pattern_event::*, t_instrument::*, uncompress}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; -use itertools::Itertools; -use std::rc::Rc; pub type PatternRow = Vec; pub type Pattern = Vec; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 22125f6..37477ea 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -67,9 +67,10 @@ impl AssetParser for SoundCollection { #[cfg(test)] mod tests { + use std::{cell::LazyCell, path::PathBuf}; + use super::*; use crate::utils::{format::*, test::*}; - use std::{cell::LazyCell, path::PathBuf}; const SOUND_DATA: LazyCell> = deflated_file!("97.dat"); diff --git a/engine/src/asset/string_table.rs b/engine/src/asset/string_table.rs index 577fdeb..270d723 100644 --- a/engine/src/asset/string_table.rs +++ b/engine/src/asset/string_table.rs @@ -32,9 +32,10 @@ impl AssetParser for StringTable { #[cfg(test)] mod tests { + use std::cell::LazyCell; + use super::*; use crate::utils::test::*; - use std::cell::LazyCell; const STRING_TABLE_DATA: LazyCell> = deflated_file!("98-deflated.dat"); diff --git a/engine/src/asset/texture/dat/texture.rs b/engine/src/asset/texture/dat/texture.rs index 0aaa782..dcb4998 100644 --- a/engine/src/asset/texture/dat/texture.rs +++ b/engine/src/asset/texture/dat/texture.rs @@ -1,3 +1,5 @@ +use itertools::Itertools; + use super::size::TextureSize; use crate::{ asset::{ @@ -7,7 +9,6 @@ use crate::{ }, utils::nom::*, }; -use itertools::Itertools; // TODO(nenikitov): Move this to a separate public module later #[derive(Clone)] diff --git a/engine/src/asset/texture/mod.rs b/engine/src/asset/texture/mod.rs index bbcc4c1..7f34ca6 100644 --- a/engine/src/asset/texture/mod.rs +++ b/engine/src/asset/texture/mod.rs @@ -2,10 +2,11 @@ // this `pub(crate)` could be deleted pub(crate) mod dat; +use dat::{offset::TextureOffset, size::TextureSize, texture::MippedTexture}; + use self::dat::texture::Texture; use super::{extension::*, AssetParser}; use crate::utils::{compression::decompress, nom::*}; -use dat::{offset::TextureOffset, size::TextureSize, texture::MippedTexture}; pub enum TextureMipKind { NonMipped(Texture), @@ -85,12 +86,13 @@ impl AssetParser for MippedTextureCollection { #[cfg(test)] mod tests { + use std::{cell::LazyCell, path::PathBuf}; + use super::*; use crate::{ asset::color_map::{ColorMap, PaletteTexture}, utils::{format::*, test::*}, }; - use std::{cell::LazyCell, path::PathBuf}; const COLOR_MAP_DATA: LazyCell> = deflated_file!("4F.dat"); const TEXTURE_INFO_DATA: LazyCell> = deflated_file!("93.dat"); diff --git a/engine/src/error.rs b/engine/src/error.rs index 8d77a62..30c54f9 100644 --- a/engine/src/error.rs +++ b/engine/src/error.rs @@ -1,7 +1,9 @@ -use crate::utils::nom::Input; -use nom::error::ErrorKind as NomErrorKind; use std::num::NonZeroUsize; +use nom::error::ErrorKind as NomErrorKind; + +use crate::utils::nom::Input; + // TODO(Unavailable): implement Error and Display. // // I really don't know how the output should look like, can be done later. When diff --git a/engine/src/utils/compression.rs b/engine/src/utils/compression.rs index 71b90b7..13f2f28 100644 --- a/engine/src/utils/compression.rs +++ b/engine/src/utils/compression.rs @@ -1,6 +1,7 @@ -use flate2::bufread::ZlibDecoder; use std::io::Read; +use flate2::bufread::ZlibDecoder; + // TODO(nenikitov): Return `Result` pub fn decompress(bytes: &[u8]) -> Vec { match bytes { diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index 6f38cae..a0b6098 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -1,4 +1,5 @@ -use crate::asset::color_map::Color; +use std::ops::Deref; + use image::{ codecs::{ gif::{GifEncoder, Repeat}, @@ -6,7 +7,8 @@ use image::{ }, Frame, ImageEncoder, RgbaImage, }; -use std::ops::Deref; + +use crate::asset::color_map::Color; pub trait PngFile { fn to_png(&self) -> Vec; diff --git a/engine/src/utils/nom.rs b/engine/src/utils/nom.rs index 560fa4b..b759297 100644 --- a/engine/src/utils/nom.rs +++ b/engine/src/utils/nom.rs @@ -14,11 +14,11 @@ macro_rules! re_export { /// Re-exports all `nom::number` items. pub mod number { + use nom::number; pub use nom::number::complete::*; + use paste::paste; use super::Result; - use nom::number; - use paste::paste; macro_rules! parser_for_fixed { ($type: ty, $bits: expr) => { diff --git a/engine/src/utils/test.rs b/engine/src/utils/test.rs index ab01e90..c2410c9 100644 --- a/engine/src/utils/test.rs +++ b/engine/src/utils/test.rs @@ -58,4 +58,6 @@ where inner(path.as_ref(), contents.as_ref()) } -pub(crate) use {deflated_file, parsed_file_path, workspace_file_path}; +pub(crate) use deflated_file; +pub(crate) use parsed_file_path; +pub(crate) use workspace_file_path; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..331866b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +group_imports = "StdExternalCrate" +hex_literal_case = "Upper" +imports_granularity = "Crate" From ccacd950acc5fac219dc4329c0937183c3d1fc1f Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 6 Feb 2024 23:00:36 -0500 Subject: [PATCH 22/87] wip: remove interpolation and upsampling --- engine/src/asset/sound/dat/mixer.rs | 58 +++++----------------- engine/src/asset/sound/dat/t_instrument.rs | 10 ++-- engine/src/asset/sound/mod.rs | 2 +- 3 files changed, 18 insertions(+), 52 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 318121e..1004d2f 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -24,7 +24,7 @@ impl TSongMixer for TSong { } trait TSongMixerUtils { - const SAMPLE_RATE: usize = 48000; + const SAMPLE_RATE: usize = 16000; const CHANNEL_COUNT: usize = 1; fn mix(&self, start: usize) -> Sample; @@ -158,26 +158,14 @@ impl<'a> Channel<'a> { .iter() .chain(sample.sample_loop().iter().cycle()) .skip(self.sample_position) - // TODO(nenikitov): This `+1` is necessary for linear interpolation. - // If not present, linear interpolation can produce clicks. - // If interpolation is reverted, this magic shouldn't be here - .take(duration_scaled + 1) + .take(duration_scaled) .copied() .collect::>(); self.sample_position += duration_scaled; - // TODO(nenikitov): Same here, these `+1` and `pop` are necessary for linear interpolation. - // If interpolation is reverted, this magic shouldn't be here - let pitch_factor = (duration + 1) as f32 / data.len() as f32; - let mut data = data - .volume(volume) - .pitch_with_time_stretch(pitch_factor, false); - if (data.len() != duration) { - data.pop(); - } - - data + let pitch_factor = duration as f32 / data.len() as f32; + data.volume(volume).pitch_with_time_stretch(pitch_factor) } else { vec![] } @@ -218,7 +206,7 @@ impl Mixer { pub trait SoundEffect { fn volume(self, volume: f32) -> Self; - fn pitch_with_time_stretch(self, factor: f32, interpolate: bool) -> Self; + fn pitch_with_time_stretch(self, factor: f32) -> Self; } impl SoundEffect for Sample { @@ -228,32 +216,12 @@ impl SoundEffect for Sample { .collect() } - fn pitch_with_time_stretch(self, factor: f32, interpolate: bool) -> Self { - // TODO(nenikitov): Linear interpolation sounds nicer and less crusty, but - // Introduces occasional clicks. - // Maybe it should be removed. - // Or improved... - let len = (self.len() as f32 * factor as f32).round() as usize; + fn pitch_with_time_stretch(self, factor: f32) -> Self { + // TODO(nenikitov): Look into linear interpolation + let len = (self.len() as f32 * factor).round() as usize; (0..len) - .map(|i| { - let frac = i as f32 / factor; - let index = (frac).floor() as usize; - let frac = if interpolate { - frac - index as f32 - } else { - 0.0 - }; - - let sample_1 = self[index]; - let sample_2 = if index + 1 < self.len() { - self[index + 1] - } else { - self[index] - }; - - ((1.0 - frac) * sample_1 as f32 + frac * sample_2 as f32) as i16 - }) + .map(|i| self[(i as f32 / factor).floor() as usize]) .collect() } } @@ -282,15 +250,15 @@ mod tests { fn pitch_with_time_stretch_works() { assert_eq!( vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, true), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0), ); assert_eq!( - vec![-10, 5, 20, 30, 40, 35, 30, -24, -78, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, true), + vec![-10, -10, 20, 20, 40, 40, 30, 30, -78, -78], + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0), ); assert_eq!( vec![-10, 40, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, true), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5), ); } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 7a7fca6..4fed902 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use bitflags::bitflags; -use super::{finetune::FineTune, mixer::SoundEffect}; +use super::finetune::FineTune; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -172,11 +172,9 @@ impl AssetParser for TSample { panning, align, finetune: FineTune::new(finetune), - // I resample sample data from 16k to 48k Hz, so offset should be `* 3` - loop_length: loop_length * 3, - data: sample_data[sample_offset as usize..loop_end as usize] - .to_vec() - .pitch_with_time_stretch(3.0, false), + // TODO(nenikitov): Look into resampling the sample to 48 KHz + loop_length, + data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), }, )) } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 37477ea..0554f52 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -27,7 +27,7 @@ impl Sound { pub struct SoundCollection; impl SoundCollection { - pub const SAMPLE_RATE: usize = 48000; + pub const SAMPLE_RATE: usize = 16000; pub const CHANNEL_COUNT: usize = 1; } From a7686694ab6fc77153bb6a9c8d08655214881a0c Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 7 Feb 2024 00:30:12 -0500 Subject: [PATCH 23/87] wip: experimenting with linear interpolation --- engine/src/asset/sound/dat/finetune.rs | 10 ++--- engine/src/asset/sound/dat/mixer.rs | 54 +++++++++++++++++++------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 55a7ab5..4d58f31 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -7,7 +7,7 @@ pub struct FineTune { impl FineTune { const BASE_NOTE: FineTune = FineTune::new_from_note(49); - const BASE_FREQUENCY: f32 = 440.0; + const BASE_FREQUENCY: f64 = 440.0; const CENTS_PER_NOTE: i32 = 128; @@ -19,10 +19,10 @@ impl FineTune { FineTune::new(note * Self::CENTS_PER_NOTE) } - pub fn frequency(&self) -> f32 { + pub fn frequency(&self) -> f64 { Self::BASE_FREQUENCY - * 2.0f32 - .powf((*self - Self::BASE_NOTE).cents as f32 / (12 * Self::CENTS_PER_NOTE) as f32) + * 2.0f64 + .powf((*self - Self::BASE_NOTE).cents as f64 / (12 * Self::CENTS_PER_NOTE) as f64) } pub fn cents(&self) -> i32 { @@ -51,7 +51,7 @@ impl Sub for FineTune { } impl Div for FineTune { - type Output = f32; + type Output = f64; fn div(self, rhs: Self) -> Self::Output { self.frequency() / rhs.frequency() diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1004d2f..885cd7a 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -53,7 +53,10 @@ impl TSongMixerUtils for TSong { // Process note if let Some(note) = event.note { - channel.note = note; + channel.note = match note { + PatternEventNote::Off => note, + PatternEventNote::On(note) => PatternEventNote::On(note - FineTune::new(12)), + }; } if let Some(instrument) = &event.instrument { channel.instrument = Some(instrument); @@ -94,7 +97,10 @@ impl TSongMixerUtils for TSong { let sample_length = sample_length as usize; for (i, c) in channels.iter_mut().enumerate() { - m.add_sample(&c.tick(sample_length), offset); + let data = c.tick(sample_length); + if i != 255 { + m.add_sample(&data, offset); + } } // Advance to next tick @@ -149,23 +155,28 @@ impl<'a> Channel<'a> { } as f32 / u8::MAX as f32; - let pitch_factor = (BASE_NOTE - sample.finetune) / note; + let pitch_factor = BASE_NOTE / (note + sample.finetune); - let duration_scaled = (duration as f32 / pitch_factor).round() as usize; + let duration_scaled = (duration as f64 / pitch_factor).round() as usize; - let data = sample + let mut data = sample .sample_beginning() .iter() .chain(sample.sample_loop().iter().cycle()) - .skip(self.sample_position) + .skip(self.sample_position + 1) .take(duration_scaled) .copied() .collect::>(); self.sample_position += duration_scaled; - let pitch_factor = duration as f32 / data.len() as f32; - data.volume(volume).pitch_with_time_stretch(pitch_factor) + let pitch_factor = (duration + 1) as f32 / data.len() as f32; + let mut data = data + .volume(volume) + .pitch_with_time_stretch(pitch_factor, None); + data.truncate(duration); + + data } else { vec![] } @@ -206,7 +217,7 @@ impl Mixer { pub trait SoundEffect { fn volume(self, volume: f32) -> Self; - fn pitch_with_time_stretch(self, factor: f32) -> Self; + fn pitch_with_time_stretch(self, factor: f32, next_sample: Option) -> Self; } impl SoundEffect for Sample { @@ -216,12 +227,27 @@ impl SoundEffect for Sample { .collect() } - fn pitch_with_time_stretch(self, factor: f32) -> Self { + fn pitch_with_time_stretch(self, factor: f32, next_sample: Option) -> Self { // TODO(nenikitov): Look into linear interpolation let len = (self.len() as f32 * factor).round() as usize; (0..len) - .map(|i| self[(i as f32 / factor).floor() as usize]) + .map(|i| { + let frac = i as f32 / factor; + let index = frac.floor() as usize; + let frac = frac - index as f32; + + let sample_1 = self[index] as f32; + let sample_2 = if self.len() > index + 1 { + self[index + 1] + } else if let Some(next_sample) = next_sample { + next_sample + } else { + self[index] + } as f32; + + ((1.0 - frac) * sample_1 + frac * sample_2).floor() as i16 + }) .collect() } } @@ -250,15 +276,15 @@ mod tests { fn pitch_with_time_stretch_works() { assert_eq!( vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, None), ); assert_eq!( vec![-10, -10, 20, 20, 40, 40, 30, 30, -78, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, None), ); assert_eq!( vec![-10, 40, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5), + vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, None), ); } } From 5ff29bb2de2e65d4da2c4d4d655778fd2558316a Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 10 Feb 2024 00:26:14 -0500 Subject: [PATCH 24/87] feat: made a more faithful frequency calculator --- engine/src/asset/sound/dat/finetune.rs | 65 ++++++-------------------- engine/src/asset/sound/dat/mixer.rs | 5 +- 2 files changed, 16 insertions(+), 54 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 4d58f31..c45e1e9 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, Div, Sub}; +use std::ops::{Add, Sub}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FineTune { @@ -6,9 +6,6 @@ pub struct FineTune { } impl FineTune { - const BASE_NOTE: FineTune = FineTune::new_from_note(49); - const BASE_FREQUENCY: f64 = 440.0; - const CENTS_PER_NOTE: i32 = 128; pub const fn new(cents: i32) -> Self { @@ -19,10 +16,16 @@ impl FineTune { FineTune::new(note * Self::CENTS_PER_NOTE) } - pub fn frequency(&self) -> f64 { - Self::BASE_FREQUENCY - * 2.0f64 - .powf((*self - Self::BASE_NOTE).cents as f64 / (12 * Self::CENTS_PER_NOTE) as f64) + pub fn pitch_factor(&self) -> f64 { + // TODO(nenikitov): This formula is from the game + // And it's very magic. + // Maybe simplify it or at least name constants. + 1.0 / (2f64.powf((self.cents as f64 + 1.0) / (12.0 * FineTune::CENTS_PER_NOTE as f64)) + * 8363.0 + * 1048576.0 + / 16000.0 + / 2048.0 + / 8192.0) } pub fn cents(&self) -> i32 { @@ -50,14 +53,6 @@ impl Sub for FineTune { } } -impl Div for FineTune { - type Output = f64; - - fn div(self, rhs: Self) -> Self::Output { - self.frequency() / rhs.frequency() - } -} - #[cfg(test)] mod tests { use assert_approx_eq::assert_approx_eq; @@ -98,21 +93,10 @@ mod tests { } #[test] - fn frequency_works() { - assert_eq!(440.0, FineTune::BASE_NOTE.frequency()); - assert_eq!( - 880.0, - (FineTune::BASE_NOTE + FineTune::new_from_note(12)).frequency() - ); - assert_eq!( - 220.0, - (FineTune::BASE_NOTE - FineTune::new_from_note(12)).frequency() - ); - assert_approx_eq!( - 392.0, - (FineTune::BASE_NOTE - FineTune::new_from_note(2)).frequency(), - 0.25 - ); + fn pitch_factor_works() { + assert_approx_eq!(2.0, FineTune::new_from_note(47).pitch_factor(), 0.030); + assert_approx_eq!(1.0, FineTune::new_from_note(59).pitch_factor(), 0.015); + assert_approx_eq!(0.5, FineTune::new_from_note(71).pitch_factor(), 0.008); } #[test] @@ -130,23 +114,4 @@ mod tests { FineTune::new_from_note(40) - FineTune::new_from_note(8), ); } - - #[test] - fn div_works() { - assert_approx_eq!( - 2.0, - FineTune::new_from_note(30) / FineTune::new_from_note(18), - 0.01 - ); - assert_approx_eq!( - 0.5, - FineTune::new_from_note(18) / FineTune::new_from_note(30), - 0.01 - ); - assert_approx_eq!( - 1.5, - FineTune::new_from_note(17) / FineTune::new_from_note(10), - 0.01 - ); - } } diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 885cd7a..b3287e0 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -138,9 +138,6 @@ struct Channel<'a> { note: PatternEventNote, } -// TODO(nenikitov): Double check that the game actually uses C_5 as a base note for all samples -const BASE_NOTE: FineTune = FineTune::new_from_note(60); - impl<'a> Channel<'a> { fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument @@ -155,7 +152,7 @@ impl<'a> Channel<'a> { } as f32 / u8::MAX as f32; - let pitch_factor = BASE_NOTE / (note + sample.finetune); + let pitch_factor = (note + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; From 6761ff28fa492f431dc8c962a0333fde76fd5528 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 16 Feb 2024 20:41:03 -0500 Subject: [PATCH 25/87] fix: clippy --- engine/src/asset/sound/dat/finetune.rs | 50 ++++++--------------- engine/src/asset/sound/dat/mixer.rs | 8 ++-- engine/src/asset/sound/dat/pattern_event.rs | 3 +- engine/src/lib.rs | 1 + 4 files changed, 19 insertions(+), 43 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index c45e1e9..e03f79c 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -12,27 +12,28 @@ impl FineTune { Self { cents } } - pub const fn new_from_note(note: i32) -> Self { + pub const fn from_note(note: i32) -> Self { FineTune::new(note * Self::CENTS_PER_NOTE) } - pub fn pitch_factor(&self) -> f64 { + pub fn pitch_factor(self) -> f64 { // TODO(nenikitov): This formula is from the game // And it's very magic. // Maybe simplify it or at least name constants. 1.0 / (2f64.powf((self.cents as f64 + 1.0) / (12.0 * FineTune::CENTS_PER_NOTE as f64)) * 8363.0 + // TODO(nenikitov): This is `2^20`, which is divided by `2048` and `8192` results in `1/16` * 1048576.0 / 16000.0 / 2048.0 / 8192.0) } - pub fn cents(&self) -> i32 { + pub fn cents(self) -> i32 { self.cents } - pub fn note(&self) -> i32 { + pub fn note(self) -> i32 { self.cents / Self::CENTS_PER_NOTE } } @@ -60,58 +61,35 @@ mod tests { use super::*; #[test] - fn new_works() { - assert_eq!(FineTune { cents: 1000 }, FineTune::new(1000)); - } - - #[test] - fn new_from_note_works() { + fn from_note_works() { assert_eq!( FineTune { cents: 30 * FineTune::CENTS_PER_NOTE }, - FineTune::new_from_note(30) - ); - } - - #[test] - fn new_cent_to_note_relation_works() { - assert_eq!( - FineTune::new(FineTune::CENTS_PER_NOTE), - FineTune::new_from_note(1) + FineTune::from_note(30) ); } - #[test] - fn cents_works() { - assert_eq!(100, FineTune::new(100).cents()) - } - - #[test] - fn note_works() { - assert_eq!(5, FineTune::new_from_note(5).note()) - } - #[test] fn pitch_factor_works() { - assert_approx_eq!(2.0, FineTune::new_from_note(47).pitch_factor(), 0.030); - assert_approx_eq!(1.0, FineTune::new_from_note(59).pitch_factor(), 0.015); - assert_approx_eq!(0.5, FineTune::new_from_note(71).pitch_factor(), 0.008); + assert_approx_eq!(2.0, FineTune::from_note(47).pitch_factor(), 0.030); + assert_approx_eq!(1.0, FineTune::from_note(59).pitch_factor(), 0.015); + assert_approx_eq!(0.5, FineTune::from_note(71).pitch_factor(), 0.008); } #[test] fn add_works() { assert_eq!( - FineTune::new_from_note(54), - FineTune::new_from_note(49) + FineTune::new_from_note(5), + FineTune::from_note(54), + FineTune::from_note(49) + FineTune::from_note(5), ); } #[test] fn sub_works() { assert_eq!( - FineTune::new_from_note(32), - FineTune::new_from_note(40) - FineTune::new_from_note(8), + FineTune::from_note(32), + FineTune::from_note(40) - FineTune::from_note(8), ); } } diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 37ae840..f12fbbe 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; - use super::{pattern_event::*, t_instrument::*, t_song::*}; use crate::asset::sound::dat::finetune::FineTune; @@ -46,7 +44,7 @@ impl TSongMixerUtils for TSong { let mut speed = self.speed; for pattern in &self.orders[start..] { - for row in pattern.deref() { + for row in &**pattern { // Update channels for (c, event) in row.iter().enumerate() { let channel = &mut channels[c]; @@ -84,10 +82,10 @@ impl TSongMixerUtils for TSong { } } PatternEffectKind::SampleOffset => { - channel.sample_position = effect.value as usize * 256 * 3 + channel.sample_position = effect.value as usize * 256; } _ => {} - } + }; } } diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 0def910..501ce12 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -28,7 +28,7 @@ impl AssetParser for Option { input, should_parse.then(|| { match note { - 1..=95 => PatternEventNote::On(FineTune::new_from_note(note as i32)), + 1..=95 => PatternEventNote::On(FineTune::from_note(note as i32)), 96 => PatternEventNote::Off, // TODO(nenikitov): Should be a `Result` _ => unreachable!("Note should be in range 0-96"), @@ -152,7 +152,6 @@ impl From for PatternEffectKind { 0x33 => Self::SoundControlQuad, 0x34 => Self::FilterGlobal, 0x35 => Self::FilterLocal, - 0x35 => Self::FilterLocal, 0x36 => Self::PlayForward, 0x37 => Self::PlayBackward, _ => Self::None, diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 3a4394f..6844758 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -9,6 +9,7 @@ clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_precision_loss, + clippy::unreadable_literal, )] #![warn(unused_imports)] #![feature( From 84aa652718a648c488cb706fbf4b8d92e643ea92 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 16 Feb 2024 21:05:30 -0500 Subject: [PATCH 26/87] refactor: volume --- engine/src/asset/sound/dat/mixer.rs | 26 +++--------- engine/src/asset/sound/dat/pattern_event.rs | 44 ++++++++++++++++++--- engine/src/asset/sound/dat/t_instrument.rs | 4 +- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index f12fbbe..78653de 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -63,11 +63,7 @@ impl TSongMixerUtils for TSong { channel.sample_position = 0; } if let Some(volume) = event.volume { - if volume == u8::MAX { - channel.volume = ChannelVolume::Sample; - } else { - channel.volume = ChannelVolume::Value(volume); - } + channel.volume = volume } // Process effects @@ -117,24 +113,13 @@ impl TSongMixerUtils for TSong { } } -enum ChannelVolume { - Sample, - Value(u8), -} - -impl Default for ChannelVolume { - fn default() -> Self { - ChannelVolume::Value(0) - } -} - #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, sample_position: usize, - volume: ChannelVolume, + volume: PatternEventVolume, note: PatternEventNote, } @@ -147,10 +132,9 @@ impl<'a> Channel<'a> { &instrument.samples[note.note() as usize] { let volume = match self.volume { - ChannelVolume::Sample => sample.volume, - ChannelVolume::Value(value) => value, - } as f32 - / u8::MAX as f32; + PatternEventVolume::Sample => sample.volume, + PatternEventVolume::Value(value) => value, + }; let pitch_factor = (note + sample.finetune).pitch_factor(); diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 501ce12..056b8f3 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -221,11 +221,46 @@ impl AssetParser for Option { } } +#[derive(Clone, Copy)] +pub enum PatternEventVolume { + Sample, + Value(f32), +} + +impl Default for PatternEventVolume { + fn default() -> Self { + PatternEventVolume::Value(0.0) + } +} + +impl AssetParser for Option { + type Output = Self; + + type Context<'ctx> = bool; + + fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, volume) = number::le_u8(input)?; + + Ok(( + input, + should_parse.then(|| { + if volume == 255 { + PatternEventVolume::Sample + } else { + PatternEventVolume::Value(volume as f32 / u8::MAX as f32) + } + }), + )) + } + } +} + #[derive(Default)] pub struct PatternEvent { pub note: Option, pub instrument: Option, - pub volume: Option, + pub volume: Option, pub effects: [Option; 2], } @@ -250,10 +285,9 @@ impl AssetParser for PatternEvent { instruments, ))(input)?; - let (input, volume) = number::le_u8(input)?; - let volume = flags - .contains(PatternEventFlags::ChangeVolume) - .then_some(volume); + let (input, volume) = >::parser( + flags.contains(PatternEventFlags::ChangeVolume), + )(input)?; let (input, effect_1) = >::parser( flags.contains(PatternEventFlags::ChangeEffect1), diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 4fed902..100d952 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -135,7 +135,7 @@ bitflags! { #[derive(Debug)] pub struct TSample { pub flags: TSampleFlags, - pub volume: u8, + pub volume: f32, pub panning: u8, pub align: u8, pub finetune: FineTune, @@ -168,7 +168,7 @@ impl AssetParser for TSample { input, Self { flags: TSampleFlags::from_bits(flags).expect("Flags should be valid"), - volume, + volume: volume as f32 / u8::MAX as f32, panning, align, finetune: FineTune::new(finetune), From f0a998a1c80cccf8add9762a4d5fbde338f662da Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 17 Feb 2024 03:02:18 -0500 Subject: [PATCH 27/87] refactor: volume envelope --- engine/src/asset/sound/dat/mixer.rs | 2 +- engine/src/asset/sound/dat/pattern_event.rs | 2 +- engine/src/asset/sound/dat/t_instrument.rs | 122 ++++++++++++++++---- engine/src/asset/sound/mod.rs | 19 ++- 4 files changed, 115 insertions(+), 30 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 78653de..6c769f9 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -94,7 +94,7 @@ impl TSongMixerUtils for TSong { for (i, c) in channels.iter_mut().enumerate() { let data = c.tick(sample_length); - if i != 255 { + if i == 0 { m.add_sample(&data, offset); } } diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 056b8f3..380b6d3 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -245,7 +245,7 @@ impl AssetParser for Option { Ok(( input, should_parse.then(|| { - if volume == 255 { + if volume == u8::MAX { PatternEventVolume::Sample } else { PatternEventVolume::Value(volume as f32 / u8::MAX as f32) diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 100d952..1e70772 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{cmp, rc::Rc}; use bitflags::bitflags; @@ -12,8 +12,28 @@ use crate::{ bitflags! { #[derive(Debug, Clone, Copy)] pub struct TInstrumentFlags: u8 { - const HasVolumeEnveloppe = 1 << 0; - const HasPanEnveloppe = 1 << 1; + const HasVolumeEnvelope = 1 << 0; + const HasPanEnvelope = 1 << 1; + } +} + +impl AssetParser for TInstrumentFlags { + type Output = Self; + + type Context<'ctx> = (); + + fn parser((): Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, flags) = number::le_u8(input)?; + + Ok(( + input, + // TODO(nenikitov): Should be a `Result` + TInstrumentFlags::from_bits(flags).expect(&format!( + "PatternEvent flags should be valid: received: {flags:b}" + )), + )) + } } } @@ -25,15 +45,79 @@ pub enum TInstrumentSampleKind { Predefined(Rc), } +#[derive(Debug)] +pub struct TInstrumentVolumeEnvelope { + data: Vec, + sustain: Option, +} + +impl TInstrumentVolumeEnvelope { + pub fn volume_beginning(&self) -> &[f32] { + if let Some(sustain) = self.sustain { + &self.data[0..sustain] + } else { + &self.data + } + } + + pub fn volume_loop(&self) -> f32 { + if let Some(sustain) = self.sustain { + self.data[sustain] + } else { + 64.0 / u8::MAX as f32 + } + } + + pub fn volume_end(&self) -> &[f32] { + if let Some(sustain) = self.sustain { + &self.data[sustain + 1..] + } else { + &[] + } + } +} + +impl AssetParser for Option { + type Output = Self; + + type Context<'ctx> = bool; + + fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, begin) = number::le_u16(input)?; + let (input, end) = number::le_u16(input)?; + let (input, sustain) = number::le_u16(input)?; + let (input, end_total) = number::le_u16(input)?; + let (input, data) = multi::count!(number::le_u8, 325)(input)?; + + Ok(( + input, + should_parse.then(|| { + let data = data + .into_iter() + .skip(begin as usize) + .take(cmp::min(cmp::min(end, end_total), 325) as usize) + .map(|v| v as f32 / u8::MAX as f32) + .collect::>(); + TInstrumentVolumeEnvelope { + data, + sustain: if sustain == u16::MAX { + None + } else { + Some((sustain - begin) as usize) + }, + } + }), + )) + } + } +} + #[derive(Debug)] pub struct TInstrument { pub flags: TInstrumentFlags, - pub volume_begin: u16, - pub volume_end: u16, - pub volume_sustain: u16, - pub volume_envelope_border: u16, - pub volume_envelope: Box<[u8; 325]>, + pub volume_envelope: Option, pub pan_begin: u16, pub pan_end: u16, @@ -58,15 +142,13 @@ impl AssetParser for TInstrument { fn parser(samples: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { - let (input, flags) = number::le_u8(input)?; + let (input, flags) = TInstrumentFlags::parser(())(input)?; let (input, _) = bytes::take(1usize)(input)?; - let (input, volume_begin) = number::le_u16(input)?; - let (input, volume_end) = number::le_u16(input)?; - let (input, volume_sustain) = number::le_u16(input)?; - let (input, volume_envelope_border) = number::le_u16(input)?; - let (input, volume_envelope) = multi::count!(number::le_u8)(input)?; + let (input, volume_envelope) = >::parser( + flags.contains(TInstrumentFlags::HasVolumeEnvelope), + )(input)?; let (input, pan_begin) = number::le_u16(input)?; let (input, pan_end) = number::le_u16(input)?; @@ -83,17 +165,13 @@ impl AssetParser for TInstrument { let (input, fadeout) = number::le_u32(input)?; let (input, vibrato_table) = number::le_u32(input)?; - let (input, sample_indexes): (_, [u8; 96]) = multi::count!(number::le_u8)(input)?; + let (input, sample_indexes): (_, [_; 96]) = multi::count!(number::le_u8)(input)?; Ok(( input, Self { - flags: TInstrumentFlags::from_bits(flags).expect("Flags should be valid"), - volume_begin, - volume_end, - volume_sustain, - volume_envelope_border, - volume_envelope: Box::new(volume_envelope), + flags, + volume_envelope, pan_begin, pan_end, pan_sustain, @@ -108,7 +186,7 @@ impl AssetParser for TInstrument { sample_indexes .into_iter() .map(|i| { - if i == 255 { + if i == u8::MAX { TInstrumentSampleKind::Special } else { TInstrumentSampleKind::Predefined(samples[i as usize].clone()) diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 0554f52..9293d98 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -101,12 +101,19 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0x8]; - - dbg!(&test_music.patterns[1] - .iter() - .map(|r| &r[2].effects) - .collect::>()); + .collect::>()[0xC]; + + // dbg!(&test_music + // .instruments + // .iter() + // .map(|i| ( + // i.flags, + // i.volume_envelope_border, + // i.volume_begin, + // i.volume_end, + // i.volume_sustain, + // )) + // .collect::>()); test_music .samples From 7831da22ab9ec3f47bda74404efac7db69699d2f Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 17 Feb 2024 10:54:27 -0500 Subject: [PATCH 28/87] wip: store previous note --- engine/src/asset/sound/dat/mixer.rs | 43 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 6c769f9..6eea193 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -51,12 +51,7 @@ impl TSongMixerUtils for TSong { // Process note if let Some(note) = event.note { - channel.note = match note { - PatternEventNote::Off => note, - PatternEventNote::On(note) => { - PatternEventNote::On(note - FineTune::new(12)) - } - }; + channel.change_note(note); } if let Some(instrument) = &event.instrument { channel.instrument = Some(instrument); @@ -113,6 +108,11 @@ impl TSongMixerUtils for TSong { } } +struct ChannelNote { + finetune: FineTune, + on: bool, +} + #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, @@ -120,23 +120,46 @@ struct Channel<'a> { sample_position: usize, volume: PatternEventVolume, - note: PatternEventNote, + volume_envelope_position: usize, + note: Option, } impl<'a> Channel<'a> { + fn change_note(&mut self, note: PatternEventNote) { + match (&mut self.note, note) { + (None, PatternEventNote::Off) => { + self.note = None; + } + (None, PatternEventNote::On(target)) => { + self.note = Some(ChannelNote { + finetune: target, + on: true, + }); + } + (Some(current), PatternEventNote::Off) => { + current.on = false; + } + (Some(current), PatternEventNote::On(target)) => { + current.finetune = target; + current.on = true; + } + } + } + fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument - && let PatternEventNote::On(note) = self.note + && let Some(note) = &self.note + && note.on && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = - &instrument.samples[note.note() as usize] + &instrument.samples[note.finetune.note() as usize] { let volume = match self.volume { PatternEventVolume::Sample => sample.volume, PatternEventVolume::Value(value) => value, }; - let pitch_factor = (note + sample.finetune).pitch_factor(); + let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; From c39b9bd41b40c7d0f1dee90c410695777a5b2dbe Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 00:18:26 -0500 Subject: [PATCH 29/87] wip: add volume envelope --- engine/src/asset/sound/dat/mixer.rs | 80 ++++++++++++++++------ engine/src/asset/sound/dat/t_instrument.rs | 28 +++++--- engine/src/asset/sound/mod.rs | 16 ++--- 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 6eea193..e27f645 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -27,7 +27,7 @@ trait TSongMixerUtils { fn mix(&self, start: usize) -> Sample; - fn seconds_per_tick(bpm: usize, speed: usize) -> f32; + fn seconds_per_row(bpm: usize, speed: usize) -> f32; } impl TSongMixerUtils for TSong { @@ -40,8 +40,8 @@ impl TSongMixerUtils for TSong { let mut offset = 0; let mut sample_length_fractional = 0.0; - let mut bpm = self.bpm; - let mut speed = self.speed; + let mut bpm = self.bpm as usize; + let mut speed = self.speed as usize; for pattern in &self.orders[start..] { for row in &**pattern { @@ -54,11 +54,10 @@ impl TSongMixerUtils for TSong { channel.change_note(note); } if let Some(instrument) = &event.instrument { - channel.instrument = Some(instrument); - channel.sample_position = 0; + channel.change_instrument(instrument); } if let Some(volume) = event.volume { - channel.volume = volume + channel.change_volume(volume); } // Process effects @@ -67,9 +66,9 @@ impl TSongMixerUtils for TSong { // TODO(nenikitov): Add effects PatternEffectKind::Speed => { if effect.value >= 0x20 { - bpm = effect.value; + bpm = effect.value as usize; } else { - speed = effect.value; + speed = effect.value as usize; } } PatternEffectKind::SampleOffset => { @@ -80,16 +79,23 @@ impl TSongMixerUtils for TSong { } } - // Mix current tick - let sample_length = Self::seconds_per_tick(bpm as usize, speed as usize) - * Self::SAMPLE_RATE as f32 + // Mix current row + let sample_length = Self::seconds_per_row(bpm, speed) * Self::SAMPLE_RATE as f32 + sample_length_fractional; sample_length_fractional = sample_length - sample_length.floor(); let sample_length = sample_length as usize; + let tick_length = sample_length / speed; for (i, c) in channels.iter_mut().enumerate() { - let data = c.tick(sample_length); - if i == 0 { + for j in 0..speed { + let offset = offset + j * tick_length; + let tick_length = if (j + 1) != speed { + tick_length + } else { + sample_length - j * tick_length + }; + + let data = c.tick(tick_length); m.add_sample(&data, offset); } } @@ -102,9 +108,9 @@ impl TSongMixerUtils for TSong { m.mix() } - fn seconds_per_tick(bpm: usize, speed: usize) -> f32 { - // TODO(nenikitov): Figure out what constant `24` means (maybe `6` default speed * `4` beats/measure???) - 60.0 / (bpm * 24 / speed) as f32 + fn seconds_per_row(bpm: usize, speed: usize) -> f32 { + // TODO(nenikitov): The formula from the game is `5 / 2 / BPM * SPEED`, not sure why + 2.5 / bpm as f32 * speed as f32 } } @@ -138,6 +144,7 @@ impl<'a> Channel<'a> { } (Some(current), PatternEventNote::Off) => { current.on = false; + self.volume_envelope_position = 0; } (Some(current), PatternEventNote::On(target)) => { current.finetune = target; @@ -146,18 +153,46 @@ impl<'a> Channel<'a> { } } + fn change_instrument(&mut self, instrument: &'a PatternEventInstrumentKind) { + self.instrument = Some(instrument); + self.sample_position = 0; + self.volume_envelope_position = 0; + } + + fn change_volume(&mut self, volume: PatternEventVolume) { + self.volume = volume; + } + fn tick(&mut self, duration: usize) -> Sample { if let Some(instrument) = self.instrument && let Some(note) = &self.note - && note.on && let PatternEventInstrumentKind::Predefined(instrument) = instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note.finetune.note() as usize] { - let volume = match self.volume { + // Generate data + let volume_note = match self.volume { PatternEventVolume::Sample => sample.volume, PatternEventVolume::Value(value) => value, }; + let volume_envelope = match &instrument.volume { + TInstrumentVolume::Envelope(envelope) => { + if note.on { + envelope + .volume_beginning() + .get(self.volume_envelope_position) + .map(ToOwned::to_owned) + .unwrap_or(envelope.volume_loop()) + } else { + envelope + .volume_end() + .get(self.volume_envelope_position) + .map(ToOwned::to_owned) + .unwrap_or_default() + } + } + TInstrumentVolume::Constant(volume) => *volume, + }; let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); @@ -172,14 +207,17 @@ impl<'a> Channel<'a> { .copied() .collect::>(); - self.sample_position += duration_scaled; - let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(volume) + .volume(volume_note * volume_envelope) .pitch_with_time_stretch(pitch_factor, None); data.truncate(duration); + // Update + self.sample_position += duration_scaled; + self.volume_envelope_position += 1; + + // Return data } else { vec![] diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 1e70772..ddac3be 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -64,7 +64,7 @@ impl TInstrumentVolumeEnvelope { if let Some(sustain) = self.sustain { self.data[sustain] } else { - 64.0 / u8::MAX as f32 + 0.0 } } @@ -77,12 +77,18 @@ impl TInstrumentVolumeEnvelope { } } -impl AssetParser for Option { +#[derive(Debug)] +pub enum TInstrumentVolume { + Envelope(TInstrumentVolumeEnvelope), + Constant(f32), +} + +impl AssetParser for TInstrumentVolume { type Output = Self; type Context<'ctx> = bool; - fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { + fn parser(has_envelope: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { let (input, begin) = number::le_u16(input)?; let (input, end) = number::le_u16(input)?; @@ -92,22 +98,24 @@ impl AssetParser for Option { Ok(( input, - should_parse.then(|| { + if has_envelope { let data = data .into_iter() .skip(begin as usize) .take(cmp::min(cmp::min(end, end_total), 325) as usize) .map(|v| v as f32 / u8::MAX as f32) .collect::>(); - TInstrumentVolumeEnvelope { + TInstrumentVolume::Envelope(TInstrumentVolumeEnvelope { data, sustain: if sustain == u16::MAX { None } else { Some((sustain - begin) as usize) }, - } - }), + }) + } else { + TInstrumentVolume::Constant(0.25) + }, )) } } @@ -117,7 +125,7 @@ impl AssetParser for Option { pub struct TInstrument { pub flags: TInstrumentFlags, - pub volume_envelope: Option, + pub volume: TInstrumentVolume, pub pan_begin: u16, pub pan_end: u16, @@ -146,7 +154,7 @@ impl AssetParser for TInstrument { let (input, _) = bytes::take(1usize)(input)?; - let (input, volume_envelope) = >::parser( + let (input, volume_envelope) = TInstrumentVolume::parser( flags.contains(TInstrumentFlags::HasVolumeEnvelope), )(input)?; @@ -171,7 +179,7 @@ impl AssetParser for TInstrument { input, Self { flags, - volume_envelope, + volume: volume_envelope, pan_begin, pan_end, pan_sustain, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 9293d98..4ca14ae 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -103,17 +103,11 @@ mod tests { }) .collect::>()[0xC]; - // dbg!(&test_music - // .instruments - // .iter() - // .map(|i| ( - // i.flags, - // i.volume_envelope_border, - // i.volume_begin, - // i.volume_end, - // i.volume_sustain, - // )) - // .collect::>()); + dbg!(&test_music + .instruments + .iter() + .map(|i| (i.flags, i.fadeout, &i.volume)) + .collect::>()); test_music .samples From 9dee52240eb8ac0c434e4cc3559dbfcd752cfd71 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 00:45:34 -0500 Subject: [PATCH 30/87] refactor: note volume computation --- engine/src/asset/sound/dat/mod.rs | 4 ++++ engine/src/asset/sound/dat/pattern_event.rs | 4 ++-- engine/src/asset/sound/dat/t_instrument.rs | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 21ce860..699ea98 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -49,3 +49,7 @@ fn uncompress(bytes: &[u8]) -> Vec { .collect() } } + +fn convert_volume(volume: u8) -> f32 { + volume as f32 / 64.0 +} diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 380b6d3..2fa5475 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use bitflags::bitflags; -use super::{finetune::FineTune, t_instrument::*}; +use super::{convert_volume, finetune::FineTune, t_instrument::*}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -248,7 +248,7 @@ impl AssetParser for Option { if volume == u8::MAX { PatternEventVolume::Sample } else { - PatternEventVolume::Value(volume as f32 / u8::MAX as f32) + PatternEventVolume::Value(convert_volume(volume)) } }), )) diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index ddac3be..ee066e6 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -2,7 +2,7 @@ use std::{cmp, rc::Rc}; use bitflags::bitflags; -use super::finetune::FineTune; +use super::{convert_volume, finetune::FineTune}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -103,7 +103,7 @@ impl AssetParser for TInstrumentVolume { .into_iter() .skip(begin as usize) .take(cmp::min(cmp::min(end, end_total), 325) as usize) - .map(|v| v as f32 / u8::MAX as f32) + .map(|v| convert_volume(v)) .collect::>(); TInstrumentVolume::Envelope(TInstrumentVolumeEnvelope { data, @@ -254,7 +254,7 @@ impl AssetParser for TSample { input, Self { flags: TSampleFlags::from_bits(flags).expect("Flags should be valid"), - volume: volume as f32 / u8::MAX as f32, + volume: convert_volume(volume), panning, align, finetune: FineTune::new(finetune), From c3e8c8dcfd8fa9a9c08718cc7311b8c154ac48fd Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 11:15:21 -0500 Subject: [PATCH 31/87] wip: add separate volume control --- engine/src/asset/sound/dat/mixer.rs | 42 ++++++++++++++++++++--------- engine/src/asset/sound/mod.rs | 2 +- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index e27f645..82ed46f 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -119,15 +119,29 @@ struct ChannelNote { on: bool, } +struct ChannelVolume { + initial: PatternEventVolume, + factor: f32, +} + +impl Default for ChannelVolume { + fn default() -> Self { + Self { + initial: PatternEventVolume::Value(0.0), + factor: 1.0, + } + } +} + #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, + note: Option, sample_position: usize, - volume: PatternEventVolume, - volume_envelope_position: usize, - note: Option, + volume: ChannelVolume, + volume_evelope_position: usize, } impl<'a> Channel<'a> { @@ -144,7 +158,7 @@ impl<'a> Channel<'a> { } (Some(current), PatternEventNote::Off) => { current.on = false; - self.volume_envelope_position = 0; + self.volume_evelope_position = 0; } (Some(current), PatternEventNote::On(target)) => { current.finetune = target; @@ -156,11 +170,12 @@ impl<'a> Channel<'a> { fn change_instrument(&mut self, instrument: &'a PatternEventInstrumentKind) { self.instrument = Some(instrument); self.sample_position = 0; - self.volume_envelope_position = 0; + self.volume_evelope_position = 0; } fn change_volume(&mut self, volume: PatternEventVolume) { - self.volume = volume; + self.volume.initial = volume; + self.volume.factor = 1.0; } fn tick(&mut self, duration: usize) -> Sample { @@ -171,22 +186,23 @@ impl<'a> Channel<'a> { &instrument.samples[note.finetune.note() as usize] { // Generate data - let volume_note = match self.volume { - PatternEventVolume::Sample => sample.volume, - PatternEventVolume::Value(value) => value, - }; + let volume_note = self.volume.factor + * match self.volume.initial { + PatternEventVolume::Sample => sample.volume, + PatternEventVolume::Value(volume) => volume, + }; let volume_envelope = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { if note.on { envelope .volume_beginning() - .get(self.volume_envelope_position) + .get(self.volume_evelope_position) .map(ToOwned::to_owned) .unwrap_or(envelope.volume_loop()) } else { envelope .volume_end() - .get(self.volume_envelope_position) + .get(self.volume_evelope_position) .map(ToOwned::to_owned) .unwrap_or_default() } @@ -215,7 +231,7 @@ impl<'a> Channel<'a> { // Update self.sample_position += duration_scaled; - self.volume_envelope_position += 1; + self.volume_evelope_position += 1; // Return data diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 4ca14ae..9cb1ba9 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -101,7 +101,7 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0xC]; + .collect::>()[0x1]; dbg!(&test_music .instruments From a69e866221a5303470d33234f5715b4ea18b1069 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 11:53:22 -0500 Subject: [PATCH 32/87] refactor: move effects --- engine/src/asset/sound/dat/mixer.rs | 27 +++-- engine/src/asset/sound/dat/mod.rs | 1 + engine/src/asset/sound/dat/pattern_effect.rs | 83 +++++++++++++ engine/src/asset/sound/dat/pattern_event.rs | 118 +------------------ 4 files changed, 99 insertions(+), 130 deletions(-) create mode 100644 engine/src/asset/sound/dat/pattern_effect.rs diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 82ed46f..6e217d7 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,4 +1,9 @@ -use super::{pattern_event::*, t_instrument::*, t_song::*}; +use super::{ + pattern_effect::{PatternEffect, PatternEffectSpeed}, + pattern_event::*, + t_instrument::*, + t_song::*, +}; use crate::asset::sound::dat::finetune::FineTune; type SamplePoint = i16; @@ -62,19 +67,15 @@ impl TSongMixerUtils for TSong { // Process effects for effect in event.effects.iter().flatten() { - match effect.kind { - // TODO(nenikitov): Add effects - PatternEffectKind::Speed => { - if effect.value >= 0x20 { - bpm = effect.value as usize; - } else { - speed = effect.value as usize; - } + match effect { + PatternEffect::Dummy => {} + PatternEffect::Speed(s) => match s { + PatternEffectSpeed::Bpm(s) => bpm = *s, + PatternEffectSpeed::TicksPerRow(s) => speed = *s, + }, + PatternEffect::SampleOffset(offset) => { + channel.sample_position = *offset; } - PatternEffectKind::SampleOffset => { - channel.sample_position = effect.value as usize * 256; - } - _ => {} }; } } diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 699ea98..884f0ad 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -10,6 +10,7 @@ pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; +pub mod pattern_effect; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs new file mode 100644 index 0000000..e8e3ccf --- /dev/null +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -0,0 +1,83 @@ +use crate::{ + asset::{extension::*, AssetParser}, + utils::nom::*, +}; + +pub enum PatternEffectSpeed { + TicksPerRow(usize), + Bpm(usize), +} + +pub enum PatternEffect { + Dummy, + Speed(PatternEffectSpeed), + SampleOffset(usize), +} + +impl AssetParser for Option { + type Output = Self; + + type Context<'ctx> = bool; + + fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { + move |input| { + let (input, kind) = number::le_u8(input)?; + let (input, value) = number::le_u8(input)?; + + Ok(( + input, + should_parse.then(|| match kind { + 0x09 => PatternEffect::SampleOffset(value as usize * 256), + 0x0E => PatternEffect::Speed(if value >= 0x20 { + PatternEffectSpeed::Bpm(value as usize) + } else { + PatternEffectSpeed::TicksPerRow(value as usize) + }), + // TODO(nenikitov): Remove dummy effect + 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B + | 0x0C | 0x0D | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 + | 0x21 | 0x22 | 0x24 | 0x25 | 0x2E | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 + | 0x34 | 0x35 | 0x36 | 0x37 => PatternEffect::Dummy, + // TODO(nenikitov): Add support for other effects + // 0x00 => Self::Arpegio, + // 0x01 => Self::PortaUp, + // 0x02 => Self::PortaDown, + // 0x03 => Self::PortaTone, + // 0x04 => Self::Vibrato, + // 0x05 => Self::PortaVolume, + // 0x06 => Self::VibratoVolume, + // 0x07 => Self::Tremolo, + // 0x08 => Self::Pan, + // 0x0A => Self::VolumeSlide, + // 0x0B => Self::PositionJump, + // 0x0C => Self::Volume, + // 0x0D => Self::Break, + // 0x0F => Self::VolumeGlobal, + // 0x14 => Self::Sync, + // 0x15 => Self::PortaFineUp, + // 0x16 => Self::PortaFineDown, + // 0x1D => Self::NoteRetrigger, + // 0x1E => Self::VolumeSlideFineUp, + // 0x1F => Self::VolumeSlideFineDown, + // 0x20 => Self::NoteCut, + // 0x21 => ???, + // 0x22 => Self::PatternDelay, + // 0x24 => Self::PortaExtraFineUp, + // 0x25 => Self::PortaExtraFineDown, + // 0x2E => Self::SoundControlSurroundOn, + // 0x2F => Self::SoundControlSurroundOff, + // 0x30 => Self::SoundControlReverbOn, + // 0x31 => Self::SoundControlReverbOff, + // 0x32 => Self::SoundControlCentre, + // 0x33 => Self::SoundControlQuad, + // 0x34 => Self::FilterGlobal, + // 0x35 => Self::FilterLocal, + // 0x36 => Self::PlayForward, + // 0x37 => Self::PlayBackward, + // TODO(nenikitov): Should be a `Result` + a => unreachable!("Effect is outside the range {a}"), + }), + )) + } + } +} diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 2fa5475..3ef77f2 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use bitflags::bitflags; -use super::{convert_volume, finetune::FineTune, t_instrument::*}; +use super::{convert_volume, finetune::FineTune, pattern_effect::PatternEffect, t_instrument::*}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -71,122 +71,6 @@ impl AssetParser for PatternEventFlags { } } -#[derive(Debug)] -pub enum PatternEffectKind { - None, - Arpegio, - PortaUp, - PortaDown, - PortaTone, - Vibrato, - PortaVolume, - VibratoVolume, - Tremolo, - Pan, - SampleOffset, - VolumeSlide, - PositionJump, - Volume, - Break, - Speed, - VolumeGlobal, - Sync, - PortaFineUp, - PortaFineDown, - NoteRetrigger, - VolumeSlideFineUp, - VolumeSlideFineDown, - NoteCut, - NoteDelay, - PatternDelay, - PortaExtraFineUp, - PortaExtraFineDown, - // TODO(nenikitov): Verify if it's referring to surround sound - SoundControlSurroundOff, - SoundControlSurroundOn, - SoundControlReverbOn, - SoundControlReverbOff, - SoundControlCentre, - SoundControlQuad, - FilterGlobal, - FilterLocal, - PlayForward, - PlayBackward, -} - -impl From for PatternEffectKind { - fn from(value: u8) -> Self { - match value { - 0x00 => Self::Arpegio, - 0x01 => Self::PortaUp, - 0x02 => Self::PortaDown, - 0x03 => Self::PortaTone, - 0x04 => Self::Vibrato, - 0x05 => Self::PortaVolume, - 0x06 => Self::VibratoVolume, - 0x07 => Self::Tremolo, - 0x08 => Self::Pan, - 0x09 => Self::SampleOffset, - 0x0A => Self::VolumeSlide, - 0x0B => Self::PositionJump, - 0x0C => Self::Volume, - 0x0D => Self::Break, - 0x0E => Self::Speed, - 0x0F => Self::VolumeGlobal, - 0x14 => Self::Sync, - 0x15 => Self::PortaFineUp, - 0x16 => Self::PortaFineDown, - 0x1D => Self::NoteRetrigger, - 0x1E => Self::VolumeSlideFineUp, - 0x1F => Self::VolumeSlideFineDown, - 0x20 => Self::NoteCut, - 0x21 => Self::NoteDelay, - 0x22 => Self::PatternDelay, - 0x24 => Self::PortaExtraFineUp, - 0x25 => Self::PortaExtraFineDown, - 0x2E => Self::SoundControlSurroundOn, - 0x2F => Self::SoundControlSurroundOff, - 0x30 => Self::SoundControlReverbOn, - 0x31 => Self::SoundControlReverbOff, - 0x32 => Self::SoundControlCentre, - 0x33 => Self::SoundControlQuad, - 0x34 => Self::FilterGlobal, - 0x35 => Self::FilterLocal, - 0x36 => Self::PlayForward, - 0x37 => Self::PlayBackward, - _ => Self::None, - } - } -} - -// TODO(nenikitov): Use enum with associated value instead of a struct -#[derive(Debug)] -pub struct PatternEffect { - pub kind: PatternEffectKind, - pub value: u8, -} - -impl AssetParser for Option { - type Output = Self; - - type Context<'ctx> = bool; - - fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { - move |input| { - let (input, kind) = number::le_u8(input)?; - let (input, value) = number::le_u8(input)?; - - Ok(( - input, - should_parse.then(|| PatternEffect { - kind: kind.into(), - value, - }), - )) - } - } -} - #[derive(Debug)] pub enum PatternEventInstrumentKind { // TODO(nenikitov): Figure out what instrument `255` is From efd67e74a0dfcf7d057feb02b84d5926682fe287 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 12:23:30 -0500 Subject: [PATCH 33/87] refactor: even simpler volume --- engine/src/asset/sound/dat/mixer.rs | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 6e217d7..301fc32 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use super::{ pattern_effect::{PatternEffect, PatternEffectSpeed}, pattern_event::*, @@ -29,6 +31,7 @@ impl TSongMixer for TSong { trait TSongMixerUtils { const SAMPLE_RATE: usize = 16000; const CHANNEL_COUNT: usize = 1; + const GLOBAL_VOLUME: f32 = 0.5; fn mix(&self, start: usize) -> Sample; @@ -96,7 +99,7 @@ impl TSongMixerUtils for TSong { sample_length - j * tick_length }; - let data = c.tick(tick_length); + let data = c.tick(tick_length, Self::GLOBAL_VOLUME); m.add_sample(&data, offset); } } @@ -120,20 +123,6 @@ struct ChannelNote { on: bool, } -struct ChannelVolume { - initial: PatternEventVolume, - factor: f32, -} - -impl Default for ChannelVolume { - fn default() -> Self { - Self { - initial: PatternEventVolume::Value(0.0), - factor: 1.0, - } - } -} - #[derive(Default)] struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, @@ -141,7 +130,7 @@ struct Channel<'a> { sample_position: usize, - volume: ChannelVolume, + volume: f32, volume_evelope_position: usize, } @@ -175,23 +164,34 @@ impl<'a> Channel<'a> { } fn change_volume(&mut self, volume: PatternEventVolume) { - self.volume.initial = volume; - self.volume.factor = 1.0; + self.volume = match volume { + PatternEventVolume::Sample => { + if let Some((_, _, sample)) = self.get_note_instrument_sample() { + sample.volume + } else { + 0.0 + } + } + + PatternEventVolume::Value(value) => value, + }; } - fn tick(&mut self, duration: usize) -> Sample { - if let Some(instrument) = self.instrument - && let Some(note) = &self.note - && let PatternEventInstrumentKind::Predefined(instrument) = instrument + fn get_note_instrument_sample(&self) -> Option<(&ChannelNote, &Rc, &Rc)> { + if let Some(note) = &self.note + && let Some(PatternEventInstrumentKind::Predefined(instrument)) = self.instrument && let TInstrumentSampleKind::Predefined(sample) = &instrument.samples[note.finetune.note() as usize] { + Some((note, instrument, sample)) + } else { + None + } + } + + fn tick(&mut self, duration: usize, global_volume: f32) -> Sample { + if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { // Generate data - let volume_note = self.volume.factor - * match self.volume.initial { - PatternEventVolume::Sample => sample.volume, - PatternEventVolume::Value(volume) => volume, - }; let volume_envelope = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { if note.on { @@ -226,7 +226,7 @@ impl<'a> Channel<'a> { let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(volume_note * volume_envelope) + .volume(global_volume * self.volume * volume_envelope) .pitch_with_time_stretch(pitch_factor, None); data.truncate(duration); From e7e8ecdc90f7828360baf73d5adbaa2d3d0857d6 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 20 Feb 2024 16:49:40 -0500 Subject: [PATCH 34/87] wip: implement volume slide --- engine/src/asset/sound/dat/mixer.rs | 54 ++++++++++++++++---- engine/src/asset/sound/dat/pattern_effect.rs | 26 ++++++++++ engine/src/asset/sound/mod.rs | 12 +++-- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 301fc32..f953667 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,11 +1,8 @@ use std::rc::Rc; -use super::{ - pattern_effect::{PatternEffect, PatternEffectSpeed}, - pattern_event::*, - t_instrument::*, - t_song::*, -}; +use itertools::Itertools; + +use super::{pattern_effect::*, pattern_event::*, t_instrument::*, t_song::*}; use crate::asset::sound::dat::finetune::FineTune; type SamplePoint = i16; @@ -31,7 +28,7 @@ impl TSongMixer for TSong { trait TSongMixerUtils { const SAMPLE_RATE: usize = 16000; const CHANNEL_COUNT: usize = 1; - const GLOBAL_VOLUME: f32 = 0.5; + const VOLUME_SCALE: f32 = 0.5; fn mix(&self, start: usize) -> Sample; @@ -67,8 +64,13 @@ impl TSongMixerUtils for TSong { if let Some(volume) = event.volume { channel.change_volume(volume); } + for (i, effect) in event.effects.iter().enumerate() { + if let Some(effect) = effect { + channel.change_effect(i, effect); + } + } - // Process effects + // Init effects for effect in event.effects.iter().flatten() { match effect { PatternEffect::Dummy => {} @@ -76,11 +78,30 @@ impl TSongMixerUtils for TSong { PatternEffectSpeed::Bpm(s) => bpm = *s, PatternEffectSpeed::TicksPerRow(s) => speed = *s, }, + PatternEffect::Volume(v) => match v { + PatternEffectVolume::Value(v) => channel.volume = *v, + PatternEffectVolume::Slide(v) => { + if let Some(v) = v { + channel.volume_slide = *v; + } + } + }, PatternEffect::SampleOffset(offset) => { channel.sample_position = *offset; } }; } + + // Process repeatable effects + for effect in channel.effects.iter().flatten() { + match effect { + PatternEffect::Volume(PatternEffectVolume::Slide(_)) => { + channel.volume = + (channel.volume + channel.volume_slide).clamp(0.0, 4.0); + } + _ => {} + } + } } // Mix current row @@ -90,6 +111,11 @@ impl TSongMixerUtils for TSong { let sample_length = sample_length as usize; let tick_length = sample_length / speed; + let volumes = channels + .iter() + .map(|c| format!("{: >10}", c.volume)) + .join(" "); + for (i, c) in channels.iter_mut().enumerate() { for j in 0..speed { let offset = offset + j * tick_length; @@ -99,7 +125,7 @@ impl TSongMixerUtils for TSong { sample_length - j * tick_length }; - let data = c.tick(tick_length, Self::GLOBAL_VOLUME); + let data = c.tick(tick_length, Self::VOLUME_SCALE); m.add_sample(&data, offset); } } @@ -127,11 +153,13 @@ struct ChannelNote { struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, note: Option, + effects: [Option<&'a PatternEffect>; 2], sample_position: usize, volume: f32, volume_evelope_position: usize, + volume_slide: f32, } impl<'a> Channel<'a> { @@ -189,7 +217,7 @@ impl<'a> Channel<'a> { } } - fn tick(&mut self, duration: usize, global_volume: f32) -> Sample { + fn tick(&mut self, duration: usize, volume_scale: f32) -> Sample { if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { // Generate data let volume_envelope = match &instrument.volume { @@ -226,7 +254,7 @@ impl<'a> Channel<'a> { let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(global_volume * self.volume * volume_envelope) + .volume(volume_scale * self.volume * volume_envelope) .pitch_with_time_stretch(pitch_factor, None); data.truncate(duration); @@ -240,6 +268,10 @@ impl<'a> Channel<'a> { vec![] } } + + fn change_effect(&mut self, i: usize, effect: &'a PatternEffect) { + self.effects[i] = Some(effect); + } } // TODO(nenikitov): Remove this code when new mixer is done diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index e8e3ccf..190046a 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,16 +1,26 @@ +use super::convert_volume; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, }; +#[derive(Debug)] pub enum PatternEffectSpeed { TicksPerRow(usize), Bpm(usize), } +#[derive(Debug)] +pub enum PatternEffectVolume { + Value(f32), + Slide(Option), +} + +#[derive(Debug)] pub enum PatternEffect { Dummy, Speed(PatternEffectSpeed), + Volume(PatternEffectVolume), SampleOffset(usize), } @@ -33,6 +43,22 @@ impl AssetParser for Option { } else { PatternEffectSpeed::TicksPerRow(value as usize) }), + 0x0C => { + PatternEffect::Volume(PatternEffectVolume::Value(convert_volume(value))) + } + 0x0A => { + if value == 0 { + PatternEffect::Volume(PatternEffectVolume::Slide(None)) + } else { + PatternEffect::Volume(PatternEffectVolume::Slide(Some( + if value >= 16 { + convert_volume(value / 16) + } else { + -convert_volume(value) + }, + ))) + } + } // TODO(nenikitov): Remove dummy effect 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 9cb1ba9..45d339c 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -102,12 +102,14 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0x1]; + let file = output_dir.join("test.wav"); - dbg!(&test_music - .instruments - .iter() - .map(|i| (i.flags, i.fadeout, &i.volume)) - .collect::>()); + dbg!(&test_music.patterns[5][0x32][1].effects); + + // dbg!(&test_music.patterns[2] + // .iter() + // .map(|p| p.iter().map(|r| &r.effects).collect::>()) + // .collect::>()); test_music .samples From f02b59751e546f2654e7399e178b26239ef87b0e Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 8 Mar 2024 23:32:23 -0500 Subject: [PATCH 35/87] wip: effect memory --- engine/src/asset/sound/dat/mixer.rs | 10 +- engine/src/asset/sound/dat/mod.rs | 2 +- engine/src/asset/sound/dat/pattern_effect.rs | 128 ++++++++++++++++--- 3 files changed, 117 insertions(+), 23 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index f953667..407a318 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -75,12 +75,12 @@ impl TSongMixerUtils for TSong { match effect { PatternEffect::Dummy => {} PatternEffect::Speed(s) => match s { - PatternEffectSpeed::Bpm(s) => bpm = *s, - PatternEffectSpeed::TicksPerRow(s) => speed = *s, + Speed::Bpm(s) => bpm = *s, + Speed::TicksPerRow(s) => speed = *s, }, PatternEffect::Volume(v) => match v { - PatternEffectVolume::Value(v) => channel.volume = *v, - PatternEffectVolume::Slide(v) => { + Volume::Value(v) => channel.volume = *v, + Volume::Slide(v) => { if let Some(v) = v { channel.volume_slide = *v; } @@ -95,7 +95,7 @@ impl TSongMixerUtils for TSong { // Process repeatable effects for effect in channel.effects.iter().flatten() { match effect { - PatternEffect::Volume(PatternEffectVolume::Slide(_)) => { + PatternEffect::Volume(Volume::Slide(_)) => { channel.volume = (channel.volume + channel.volume_slide).clamp(0.0, 4.0); } diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 884f0ad..f086031 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -6,11 +6,11 @@ pub mod asset_header; pub mod chunk_header; pub mod finetune; pub mod mixer; +pub mod pattern_effect; pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; -pub mod pattern_effect; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 190046a..e3f752d 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,3 +1,5 @@ +use std::hash::Hash; + use super::convert_volume; use crate::{ asset::{extension::*, AssetParser}, @@ -5,25 +7,49 @@ use crate::{ }; #[derive(Debug)] -pub enum PatternEffectSpeed { +pub enum Speed { TicksPerRow(usize), Bpm(usize), } #[derive(Debug)] -pub enum PatternEffectVolume { +pub enum Volume { Value(f32), Slide(Option), } +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum PatternEffectMemoryKey { + VolumeValue, + VolumeSlide, + SampleOffset, +} + #[derive(Debug)] pub enum PatternEffect { Dummy, - Speed(PatternEffectSpeed), - Volume(PatternEffectVolume), + Speed(Speed), + Volume(Volume), SampleOffset(usize), } +impl PatternEffect { + fn memory_key(&self) -> Option { + match self { + PatternEffect::Volume(Volume::Value(_)) => Some(PatternEffectMemoryKey::VolumeValue), + PatternEffect::Volume(Volume::Slide(_)) => Some(PatternEffectMemoryKey::VolumeSlide), + PatternEffect::SampleOffset(_) => Some(PatternEffectMemoryKey::SampleOffset), + _ => None, + } + } +} + +impl Hash for PatternEffect { + fn hash(&self, state: &mut H) { + self.memory_key().hash(state) + } +} + impl AssetParser for Option { type Output = Self; @@ -39,24 +65,20 @@ impl AssetParser for Option { should_parse.then(|| match kind { 0x09 => PatternEffect::SampleOffset(value as usize * 256), 0x0E => PatternEffect::Speed(if value >= 0x20 { - PatternEffectSpeed::Bpm(value as usize) + Speed::Bpm(value as usize) } else { - PatternEffectSpeed::TicksPerRow(value as usize) + Speed::TicksPerRow(value as usize) }), - 0x0C => { - PatternEffect::Volume(PatternEffectVolume::Value(convert_volume(value))) - } + 0x0C => PatternEffect::Volume(Volume::Value(convert_volume(value))), 0x0A => { if value == 0 { - PatternEffect::Volume(PatternEffectVolume::Slide(None)) + PatternEffect::Volume(Volume::Slide(None)) } else { - PatternEffect::Volume(PatternEffectVolume::Slide(Some( - if value >= 16 { - convert_volume(value / 16) - } else { - -convert_volume(value) - }, - ))) + PatternEffect::Volume(Volume::Slide(Some(if value >= 16 { + convert_volume(value / 16) + } else { + -convert_volume(value) + }))) } } // TODO(nenikitov): Remove dummy effect @@ -107,3 +129,75 @@ impl AssetParser for Option { } } } + +/* +use std::{collections::HashMap, hash::Hash}; + +struct Channel; +struct Song; + +#[derive(Hash, Debug, PartialEq, Eq)] +enum PanKind { + Set, + Slide, +} + +#[derive(Hash, Debug, PartialEq, Eq)] +enum EffectKind { + Bpm, + Volume, + Pan(PanKind), +} + +#[derive(Debug)] +enum Pan { + Set(f32), + Slide(f32), +} + +#[derive(Debug)] +enum Effect { + Bpm(usize), + Volume(f32), + Pan(Pan), +} + +impl Effect { + fn kind(&self) -> EffectKind { + match self { + Effect::Bpm(_) => EffectKind::Bpm, + Effect::Volume(_) => EffectKind::Volume, + Effect::Pan(Pan::Set(_)) => EffectKind::Pan(PanKind::Set), + Effect::Pan(Pan::Slide(_)) => EffectKind::Pan(PanKind::Slide), + } + } +} + +impl Hash for Effect { + fn hash(&self, state: &mut H) { + self.kind().hash(state) + } +} + +fn main() { + let mut effects = HashMap::new(); + + let effect_1 = Effect::Volume(0.15); + let effect_2 = Effect::Bpm(120); + let effect_3 = Effect::Volume(0.25); + let effect_4 = Effect::Pan(Pan::Set(0.25)); + let effect_5 = Effect::Pan(Pan::Set(0.5)); + let effect_6 = Effect::Pan(Pan::Slide(-0.25)); + let effect_7 = Effect::Pan(Pan::Slide(-0.5)); + + effects.insert(effect_1.kind(), &effect_1); + effects.insert(effect_2.kind(), &effect_2); + effects.insert(effect_3.kind(), &effect_3); + effects.insert(effect_4.kind(), &effect_4); + effects.insert(effect_5.kind(), &effect_5); + effects.insert(effect_6.kind(), &effect_6); + effects.insert(effect_7.kind(), &effect_7); + + dbg!(effects); +} +*/ From 713b979e6800d86a7172309d7bc9292d8fd02396 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 9 Mar 2024 11:25:44 -0500 Subject: [PATCH 36/87] refactor: effects --- engine/src/asset/sound/dat/pattern_effect.rs | 102 +++---------------- 1 file changed, 13 insertions(+), 89 deletions(-) diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index e3f752d..b714808 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -6,6 +6,13 @@ use crate::{ utils::nom::*, }; +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum PatternEffectMemoryKey { + VolumeValue, + VolumeSlide, + SampleOffset, +} + #[derive(Debug)] pub enum Speed { TicksPerRow(usize), @@ -18,13 +25,6 @@ pub enum Volume { Slide(Option), } -#[derive(Debug, Hash, PartialEq, Eq)] -pub enum PatternEffectMemoryKey { - VolumeValue, - VolumeSlide, - SampleOffset, -} - #[derive(Debug)] pub enum PatternEffect { Dummy, @@ -70,17 +70,13 @@ impl AssetParser for Option { Speed::TicksPerRow(value as usize) }), 0x0C => PatternEffect::Volume(Volume::Value(convert_volume(value))), - 0x0A => { - if value == 0 { - PatternEffect::Volume(Volume::Slide(None)) + 0x0A => PatternEffect::Volume(Volume::Slide((value != 0).then(|| { + if value >= 16 { + convert_volume(value / 16) } else { - PatternEffect::Volume(Volume::Slide(Some(if value >= 16 { - convert_volume(value / 16) - } else { - -convert_volume(value) - }))) + -convert_volume(value) } - } + }))), // TODO(nenikitov): Remove dummy effect 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 @@ -123,81 +119,9 @@ impl AssetParser for Option { // 0x36 => Self::PlayForward, // 0x37 => Self::PlayBackward, // TODO(nenikitov): Should be a `Result` - a => unreachable!("Effect is outside the range {a}"), + kind => unreachable!("Effect is outside the range {kind}"), }), )) } } } - -/* -use std::{collections::HashMap, hash::Hash}; - -struct Channel; -struct Song; - -#[derive(Hash, Debug, PartialEq, Eq)] -enum PanKind { - Set, - Slide, -} - -#[derive(Hash, Debug, PartialEq, Eq)] -enum EffectKind { - Bpm, - Volume, - Pan(PanKind), -} - -#[derive(Debug)] -enum Pan { - Set(f32), - Slide(f32), -} - -#[derive(Debug)] -enum Effect { - Bpm(usize), - Volume(f32), - Pan(Pan), -} - -impl Effect { - fn kind(&self) -> EffectKind { - match self { - Effect::Bpm(_) => EffectKind::Bpm, - Effect::Volume(_) => EffectKind::Volume, - Effect::Pan(Pan::Set(_)) => EffectKind::Pan(PanKind::Set), - Effect::Pan(Pan::Slide(_)) => EffectKind::Pan(PanKind::Slide), - } - } -} - -impl Hash for Effect { - fn hash(&self, state: &mut H) { - self.kind().hash(state) - } -} - -fn main() { - let mut effects = HashMap::new(); - - let effect_1 = Effect::Volume(0.15); - let effect_2 = Effect::Bpm(120); - let effect_3 = Effect::Volume(0.25); - let effect_4 = Effect::Pan(Pan::Set(0.25)); - let effect_5 = Effect::Pan(Pan::Set(0.5)); - let effect_6 = Effect::Pan(Pan::Slide(-0.25)); - let effect_7 = Effect::Pan(Pan::Slide(-0.5)); - - effects.insert(effect_1.kind(), &effect_1); - effects.insert(effect_2.kind(), &effect_2); - effects.insert(effect_3.kind(), &effect_3); - effects.insert(effect_4.kind(), &effect_4); - effects.insert(effect_5.kind(), &effect_5); - effects.insert(effect_6.kind(), &effect_6); - effects.insert(effect_7.kind(), &effect_7); - - dbg!(effects); -} -*/ From 541a37a585a16b180a817228fc803fbdd367acb3 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 29 Apr 2024 22:41:06 -0400 Subject: [PATCH 37/87] feat(sample): write resampling logic --- engine/src/asset/sound/dat/mixer.rs | 56 +++--- engine/src/asset/sound/dat/pattern_effect.rs | 45 +++-- engine/src/asset/sound/mod.rs | 1 + engine/src/asset/sound/sample.rs | 187 +++++++++++++++++++ 4 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 engine/src/asset/sound/sample.rs diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 407a318..a39b161 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; use itertools::Itertools; @@ -66,29 +66,31 @@ impl TSongMixerUtils for TSong { } for (i, effect) in event.effects.iter().enumerate() { if let Some(effect) = effect { - channel.change_effect(i, effect); + channel.change_effect(i, *effect); } } // Init effects + // Efffects from now on have their memory initialized for effect in event.effects.iter().flatten() { match effect { PatternEffect::Dummy => {} - PatternEffect::Speed(s) => match s { - Speed::Bpm(s) => bpm = *s, - Speed::TicksPerRow(s) => speed = *s, - }, - PatternEffect::Volume(v) => match v { - Volume::Value(v) => channel.volume = *v, - Volume::Slide(v) => { - if let Some(v) = v { - channel.volume_slide = *v; - } - } - }, - PatternEffect::SampleOffset(offset) => { + PatternEffect::Speed(Speed::Bpm(s)) => { + bpm = *s; + } + PatternEffect::Speed(Speed::TicksPerRow(s)) => { + speed = *s; + } + PatternEffect::Volume(Volume::Value(volume)) => { + channel.volume = *volume; + } + PatternEffect::Volume(Volume::Slide(Some(volume))) => { + channel.volume_slide = *volume; + } + PatternEffect::SampleOffset(Some(offset)) => { channel.sample_position = *offset; } + _ => (), }; } @@ -96,8 +98,7 @@ impl TSongMixerUtils for TSong { for effect in channel.effects.iter().flatten() { match effect { PatternEffect::Volume(Volume::Slide(_)) => { - channel.volume = - (channel.volume + channel.volume_slide).clamp(0.0, 4.0); + channel.volume = channel.volume + channel.volume_slide; } _ => {} } @@ -153,7 +154,8 @@ struct ChannelNote { struct Channel<'a> { instrument: Option<&'a PatternEventInstrumentKind>, note: Option, - effects: [Option<&'a PatternEffect>; 2], + effects: [Option; 2], + effects_memory: HashMap, sample_position: usize, @@ -254,7 +256,7 @@ impl<'a> Channel<'a> { let pitch_factor = (duration + 1) as f32 / data.len() as f32; let mut data = data - .volume(volume_scale * self.volume * volume_envelope) + .volume(volume_scale * self.volume.clamp(0.0, 4.0) * volume_envelope) .pitch_with_time_stretch(pitch_factor, None); data.truncate(duration); @@ -269,8 +271,20 @@ impl<'a> Channel<'a> { } } - fn change_effect(&mut self, i: usize, effect: &'a PatternEffect) { - self.effects[i] = Some(effect); + pub fn recall_effect_with_memory(&mut self, effect: PatternEffect) -> PatternEffect { + if let Some(key) = effect.memory_key() { + if !effect.is_empty() { + self.effects_memory.insert(key, effect); + } + + self.effects_memory[&key] + } else { + effect + } + } + + fn change_effect(&mut self, i: usize, effect: PatternEffect) { + self.effects[i] = Some(self.recall_effect_with_memory(effect)); } } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index b714808..d989bdc 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,4 +1,4 @@ -use std::hash::Hash; +use std::{collections::HashMap, hash::Hash}; use super::convert_volume; use crate::{ @@ -6,47 +6,50 @@ use crate::{ utils::nom::*, }; -#[derive(Debug, Hash, PartialEq, Eq)] -pub enum PatternEffectMemoryKey { - VolumeValue, - VolumeSlide, - SampleOffset, -} - -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Speed { TicksPerRow(usize), Bpm(usize), } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Volume { Value(f32), Slide(Option), } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum PatternEffectMemoryKey { + VolumeSlide, + SampleOffset, +} + +#[derive(Debug, Clone, Copy)] pub enum PatternEffect { Dummy, Speed(Speed), Volume(Volume), - SampleOffset(usize), + SampleOffset(Option), } impl PatternEffect { - fn memory_key(&self) -> Option { + pub fn memory_key(&self) -> Option { match self { - PatternEffect::Volume(Volume::Value(_)) => Some(PatternEffectMemoryKey::VolumeValue), PatternEffect::Volume(Volume::Slide(_)) => Some(PatternEffectMemoryKey::VolumeSlide), PatternEffect::SampleOffset(_) => Some(PatternEffectMemoryKey::SampleOffset), _ => None, } } -} -impl Hash for PatternEffect { - fn hash(&self, state: &mut H) { - self.memory_key().hash(state) + pub fn has_memory(&self) -> bool { + self.memory_key().is_some() + } + + pub fn is_empty(&self) -> bool { + matches!( + self, + PatternEffect::Volume(Volume::Slide(None)) | PatternEffect::SampleOffset(None) + ) } } @@ -63,7 +66,11 @@ impl AssetParser for Option { Ok(( input, should_parse.then(|| match kind { - 0x09 => PatternEffect::SampleOffset(value as usize * 256), + 0x09 => PatternEffect::SampleOffset(if value != 0 { + Some(value as usize * 256) + } else { + None + }), 0x0E => PatternEffect::Speed(if value >= 0x20 { Speed::Bpm(value as usize) } else { diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 45d339c..b8efb1a 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,4 +1,5 @@ mod dat; +mod sample; use self::dat::mixer::TSongMixer; use super::{extension::*, AssetParser}; diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs new file mode 100644 index 0000000..fc6d9ee --- /dev/null +++ b/engine/src/asset/sound/sample.rs @@ -0,0 +1,187 @@ +use std::{fmt::Debug, slice::Iter}; + +use itertools::Itertools; + +pub trait IntoFromF32 { + fn into_f32(self) -> f32; + fn from_f32(value: f32) -> Self; +} +macro_rules! impl_into_from_f32 { + ($type: ty) => { + impl IntoFromF32 for $type { + fn into_f32(self) -> f32 { + self as f32 + } + + fn from_f32(value: f32) -> Self { + value as Self + } + } + }; +} +macro_rules! impl_into_from_f32_round { + ($type: ty) => { + impl IntoFromF32 for $type { + fn into_f32(self) -> f32 { + self as f32 + } + + fn from_f32(value: f32) -> Self { + value.round() as Self + } + } + }; +} + +impl_into_from_f32!(f32); +impl_into_from_f32_round!(i16); +impl_into_from_f32_round!(i32); + +pub trait SamplePoint: Copy + Clone + IntoFromF32 + Debug {} + +impl SamplePoint for i16 {} +impl SamplePoint for i32 {} +impl SamplePoint for f32 {} + +pub enum Interpolation { + Nearest, + Linear, +} + +pub struct Sample { + data: [Vec; CHANNELS], + sample_rate: usize, +} + +impl Sample { + fn index(&self, index: usize) -> [S; CHANNELS] { + self.data + .iter() + .map(|channel| channel[index]) + .collect_vec() + .try_into() + .unwrap() + } + + fn at_time(&self, time: f32) -> [S; CHANNELS] { + self.index((time * self.sample_rate as f32) as usize) + } + + pub fn resample(&self, sample_rate: usize, interpolation: Interpolation) -> Self { + let mut sample = self.stretch( + sample_rate as f32 / self.sample_rate as f32, + Some([S::from_f32(0.0); CHANNELS]), + interpolation, + ); + sample.sample_rate = sample_rate; + + sample + } + + pub fn volume(&self, volume: f32) -> Self { + let data = self + .data + .iter() + .map(|channel| { + channel + .into_iter() + .map(|&sample| (sample).into_f32() * volume) + .map(|sample| S::from_f32(sample)) + .collect_vec() + }) + .collect_vec(); + + Sample { + data: data.try_into().unwrap(), + ..*self + } + } + + pub fn stretch( + &self, + factor: f32, + last_sample: Option<[S; CHANNELS]>, + interpolation: Interpolation, + ) -> Self { + let len = (self.data[0].len() as f32 * factor).round() as usize; + + let data = self + .data + .iter() + .enumerate() + .map(|(i_channel, channel)| { + (0..len) + .map(|i_sample| match interpolation { + Interpolation::Nearest => { + channel[(i_sample as f32 / factor).floor() as usize] + } + Interpolation::Linear => { + let frac = i_sample as f32 / factor; + let index = frac.floor() as usize; + let frac = frac - index as f32; + + let sample_1 = channel[index].into_f32(); + let sample_2 = if channel.len() > index + 1 { + channel[index + 1] + } else { + last_sample.map(|s| s[i_channel]).unwrap_or(channel[index]) + } + .into_f32(); + + S::from_f32((1.0 - frac) * sample_1 + frac * sample_2) + } + }) + .collect_vec() + }) + .collect_vec(); + + Self { + data: data.try_into().unwrap(), + ..*self + } + } +} + +impl Sample { + pub fn mono(sample_rate: usize) -> Self { + Self { + data: Default::default(), + sample_rate, + } + } + + pub fn to_stereo(&self) -> Sample { + let data = [self.data[0].clone(), self.data[0].clone()]; + + Sample:: { + data, + sample_rate: self.sample_rate, + } + } +} + +impl Sample { + pub fn stereo(sample_rate: usize) -> Self { + Self { + data: Default::default(), + sample_rate, + } + } + + pub fn to_mono(&self) -> Sample { + let data = Iter::zip(self.data[0].iter(), self.data[1].iter()) + .map(|(sample1, sample2)| { + let sample1 = sample1.into_f32(); + let sample2 = sample2.into_f32(); + + sample1 * 0.5 + sample2 * 0.5 + }) + .map(|sample| S::from_f32(sample)) + .collect_vec(); + + Sample:: { + data: [data].try_into().unwrap(), + sample_rate: self.sample_rate, + } + } +} From 287807da87e0c83e0ab2aa86fc2a66b62e0e14f1 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 30 Apr 2024 01:27:19 -0400 Subject: [PATCH 38/87] feat(sample): write new wav converter --- engine/src/asset/sound/dat/pattern_effect.rs | 2 +- engine/src/asset/sound/dat/t_instrument.rs | 2 +- engine/src/asset/sound/mod.rs | 2 +- engine/src/asset/sound/sample.rs | 79 +++++++++++++++----- engine/src/utils/format.rs | 45 ++++++++++- 5 files changed, 105 insertions(+), 25 deletions(-) diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index d989bdc..0d58c15 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, hash::Hash}; +use std::hash::Hash; use super::convert_volume; use crate::{ diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index ee066e6..43e3212 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -103,7 +103,7 @@ impl AssetParser for TInstrumentVolume { .into_iter() .skip(begin as usize) .take(cmp::min(cmp::min(end, end_total), 325) as usize) - .map(|v| convert_volume(v)) + .map(convert_volume) .collect::>(); TInstrumentVolume::Envelope(TInstrumentVolumeEnvelope { data, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index b8efb1a..946387c 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,5 +1,5 @@ mod dat; -mod sample; +pub(crate) mod sample; use self::dat::mixer::TSongMixer; use super::{extension::*, AssetParser}; diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index fc6d9ee..d656071 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,14 +1,20 @@ -use std::{fmt::Debug, slice::Iter}; +use std::fmt::Debug; use itertools::Itertools; -pub trait IntoFromF32 { +pub trait SamplePointConversions { + const BITS: usize; + fn into_f32(self) -> f32; fn from_f32(value: f32) -> Self; + fn to_le_bytes(self) -> Vec; } -macro_rules! impl_into_from_f32 { - ($type: ty) => { - impl IntoFromF32 for $type { + +macro_rules! impl_sample_point_conversions_other { + ($type: ty, $size: literal) => { + impl SamplePointConversions for $type { + const BITS: usize = $size as usize; + fn into_f32(self) -> f32 { self as f32 } @@ -16,12 +22,18 @@ macro_rules! impl_into_from_f32 { fn from_f32(value: f32) -> Self { value as Self } + + fn to_le_bytes(self) -> Vec { + self.to_le_bytes().to_vec() + } } }; } -macro_rules! impl_into_from_f32_round { - ($type: ty) => { - impl IntoFromF32 for $type { +macro_rules! impl_sample_point_conversions_integer { + ($type: tt) => { + impl SamplePointConversions for $type { + const BITS: usize = $type::BITS as usize; + fn into_f32(self) -> f32 { self as f32 } @@ -29,32 +41,37 @@ macro_rules! impl_into_from_f32_round { fn from_f32(value: f32) -> Self { value.round() as Self } + + fn to_le_bytes(self) -> Vec { + self.to_le_bytes().to_vec() + } } }; } -impl_into_from_f32!(f32); -impl_into_from_f32_round!(i16); -impl_into_from_f32_round!(i32); +impl_sample_point_conversions_other!(f32, 32); +impl_sample_point_conversions_integer!(i16); +impl_sample_point_conversions_integer!(i32); -pub trait SamplePoint: Copy + Clone + IntoFromF32 + Debug {} +pub trait SamplePoint: Copy + Clone + SamplePointConversions + Debug {} impl SamplePoint for i16 {} impl SamplePoint for i32 {} impl SamplePoint for f32 {} +#[derive(Debug, Clone, Copy)] pub enum Interpolation { Nearest, Linear, } pub struct Sample { - data: [Vec; CHANNELS], - sample_rate: usize, + pub data: [Vec; CHANNELS], + pub sample_rate: usize, } impl Sample { - fn index(&self, index: usize) -> [S; CHANNELS] { + pub fn index(&self, index: usize) -> [S; CHANNELS] { self.data .iter() .map(|channel| channel[index]) @@ -63,7 +80,11 @@ impl Sample { .unwrap() } - fn at_time(&self, time: f32) -> [S; CHANNELS] { + pub fn len(&self) -> usize { + self.data[0].len() + } + + pub fn at_time(&self, time: f32) -> [S; CHANNELS] { self.index((time * self.sample_rate as f32) as usize) } @@ -78,13 +99,31 @@ impl Sample { sample } + pub fn add_sample(&mut self, other: Sample, offset: usize) { + let new_len = offset + other.len(); + + if new_len > self.len() { + for channel in self.data.iter_mut() { + channel.resize(new_len, S::from_f32(0.0)); + } + } + + for (channel_self, channel_other) in Iterator::zip(self.data.iter_mut(), other.data.iter()) + { + for (i, s) in channel_other.iter().enumerate() { + let i = i + offset; + channel_self[i] = S::from_f32(channel_self[i].into_f32() + s.into_f32()); + } + } + } + pub fn volume(&self, volume: f32) -> Self { let data = self .data .iter() .map(|channel| { channel - .into_iter() + .iter() .map(|&sample| (sample).into_f32() * volume) .map(|sample| S::from_f32(sample)) .collect_vec() @@ -124,7 +163,7 @@ impl Sample { let sample_2 = if channel.len() > index + 1 { channel[index + 1] } else { - last_sample.map(|s| s[i_channel]).unwrap_or(channel[index]) + last_sample.map_or(channel[index], |s| s[i_channel]) } .into_f32(); @@ -169,7 +208,7 @@ impl Sample { } pub fn to_mono(&self) -> Sample { - let data = Iter::zip(self.data[0].iter(), self.data[1].iter()) + let data = Iterator::zip(self.data[0].iter(), self.data[1].iter()) .map(|(sample1, sample2)| { let sample1 = sample1.into_f32(); let sample2 = sample2.into_f32(); @@ -180,7 +219,7 @@ impl Sample { .collect_vec(); Sample:: { - data: [data].try_into().unwrap(), + data: [data], sample_rate: self.sample_rate, } } diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index a0b6098..2fce80b 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -7,8 +7,12 @@ use image::{ }, Frame, ImageEncoder, RgbaImage, }; +use itertools::Itertools; -use crate::asset::color_map::Color; +use crate::asset::{ + color_map::Color, + sound::sample::{Sample, SamplePoint}, +}; pub trait PngFile { fn to_png(&self) -> Vec; @@ -96,10 +100,47 @@ where } pub trait WaveFile { + fn to_wave(&self) -> Vec; +} + +impl WaveFile for Sample { + fn to_wave(&self) -> Vec { + let bits_per_sample: usize = S::BITS; + let bytes_per_sample: usize = bits_per_sample / 8; + + let size = self.len() * CHANNELS * bytes_per_sample; + + "RIFF" + .bytes() + .chain(u32::to_le_bytes((36 + size) as u32)) + .chain("WAVE".bytes()) + .chain("fmt ".bytes()) + .chain(u32::to_le_bytes(16)) + .chain(u16::to_le_bytes(1)) + .chain(u16::to_le_bytes(CHANNELS as u16)) + .chain(u32::to_le_bytes(self.sample_rate as u32)) + .chain(u32::to_le_bytes( + (self.sample_rate * CHANNELS * bytes_per_sample) as u32, + )) + .chain(u16::to_le_bytes((CHANNELS * bytes_per_sample) as u16)) + .chain(u16::to_le_bytes(bits_per_sample as u16)) + .chain("data".bytes()) + .chain(u32::to_le_bytes(size as u32)) + .chain((0..self.data.len()).into_iter().flat_map(|i| { + self.data + .iter() + .flat_map(|channel| channel[i].to_le_bytes()) + .collect_vec() + })) + .collect() + } +} + +pub trait WaveFileOld { fn to_wave(&self, sample_rate: usize, channel_count: usize) -> Vec; } -impl WaveFile for Vec { +impl WaveFileOld for Vec { fn to_wave(&self, sample_rate: usize, channel_count: usize) -> Vec { const BITS_PER_SAMPLE: usize = 16; const BYTES_PER_SAMPLE: usize = BITS_PER_SAMPLE / 8; From dc6b947df44f1f357da93ef95b4de7cc408405b3 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Thu, 2 May 2024 13:20:54 -0400 Subject: [PATCH 39/87] feat(sample): integrate new sample and fix noise --- engine/src/asset/sound/dat/mixer.rs | 149 ++------ engine/src/asset/sound/dat/t_effect.rs | 9 +- engine/src/asset/sound/dat/t_instrument.rs | 34 +- engine/src/asset/sound/mod.rs | 23 +- engine/src/asset/sound/sample.rs | 390 +++++++++++++++++---- engine/src/lib.rs | 6 +- engine/src/utils/format.rs | 12 +- 7 files changed, 374 insertions(+), 249 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index a39b161..1a340e1 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -3,17 +3,17 @@ use std::{collections::HashMap, rc::Rc}; use itertools::Itertools; use super::{pattern_effect::*, pattern_event::*, t_instrument::*, t_song::*}; -use crate::asset::sound::dat::finetune::FineTune; - -type SamplePoint = i16; -type Sample = Vec; +use crate::asset::sound::{ + dat::finetune::FineTune, + sample::{Interpolation, Sample, SampleDataProcessing}, +}; pub trait TSongMixer { - fn mix(&self, restart: bool) -> Sample; + fn mix(&self, restart: bool) -> Sample; } impl TSongMixer for TSong { - fn mix(&self, restart: bool) -> Sample { + fn mix(&self, restart: bool) -> Sample { TSongMixerUtils::mix( self, if restart { @@ -26,18 +26,17 @@ impl TSongMixer for TSong { } trait TSongMixerUtils { - const SAMPLE_RATE: usize = 16000; - const CHANNEL_COUNT: usize = 1; + const SAMPLE_RATE: usize = 16_000; const VOLUME_SCALE: f32 = 0.5; - fn mix(&self, start: usize) -> Sample; + fn mix(&self, start: usize) -> Sample; fn seconds_per_row(bpm: usize, speed: usize) -> f32; } impl TSongMixerUtils for TSong { - fn mix(&self, start: usize) -> Sample { - let mut m = Mixer::new(); + fn mix(&self, start: usize) -> Sample { + let mut song = Sample::mono(Self::SAMPLE_RATE); let mut channels: Vec<_> = (0..self.patterns[0][0].len()) .map(|_| Channel::default()) @@ -127,7 +126,7 @@ impl TSongMixerUtils for TSong { }; let data = c.tick(tick_length, Self::VOLUME_SCALE); - m.add_sample(&data, offset); + song.data.add_sample(&data, offset); } } @@ -136,7 +135,7 @@ impl TSongMixerUtils for TSong { } } - m.mix() + song } fn seconds_per_row(bpm: usize, speed: usize) -> f32 { @@ -219,7 +218,7 @@ impl<'a> Channel<'a> { } } - fn tick(&mut self, duration: usize, volume_scale: f32) -> Sample { + fn tick(&mut self, duration: usize, volume_scale: f32) -> Vec<[i16; 1]> { if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { // Generate data let volume_envelope = match &instrument.volume { @@ -245,19 +244,21 @@ impl<'a> Channel<'a> { let duration_scaled = (duration as f64 / pitch_factor).round() as usize; - let mut data = sample + let mut sample = sample .sample_beginning() .iter() .chain(sample.sample_loop().iter().cycle()) - .skip(self.sample_position + 1) - .take(duration_scaled) + .skip(self.sample_position) + .take(duration_scaled + 1) .copied() .collect::>(); - let pitch_factor = (duration + 1) as f32 / data.len() as f32; - let mut data = data - .volume(volume_scale * self.volume.clamp(0.0, 4.0) * volume_envelope) - .pitch_with_time_stretch(pitch_factor, None); + let first_sample_after = sample.pop(); + + let pitch_factor = duration as f32 / sample.len() as f32; + let mut data = sample + .stretch(pitch_factor, first_sample_after, Interpolation::Linear) + .volume(volume_scale * self.volume.clamp(0.0, 4.0)); data.truncate(duration); // Update @@ -287,109 +288,3 @@ impl<'a> Channel<'a> { self.effects[i] = Some(self.recall_effect_with_memory(effect)); } } - -// TODO(nenikitov): Remove this code when new mixer is done - -pub struct Mixer { - samples: Sample, -} - -impl Mixer { - pub fn new() -> Self { - Self { - samples: Vec::new(), - } - } - - pub fn add_sample(&mut self, sample: &[i16], offset: usize) { - let new_len = offset + sample.len(); - if new_len > self.samples.len() { - self.samples.resize(new_len, 0); - } - - for (i, s) in sample.iter().enumerate() { - let i = i + offset; - if i < self.samples.len() { - self.samples[i] = self.samples[i].saturating_add(*s); - } - } - } - - pub fn mix(self) -> Sample { - self.samples - } -} - -pub trait SoundEffect { - fn volume(self, volume: f32) -> Self; - fn pitch_with_time_stretch(self, factor: f32, next_sample: Option) -> Self; -} - -impl SoundEffect for Sample { - fn volume(self, volume: f32) -> Self { - self.into_iter() - .map(|s| (s as f32 * volume) as i16) - .collect() - } - - fn pitch_with_time_stretch(self, factor: f32, next_sample: Option) -> Self { - // TODO(nenikitov): Look into linear interpolation - let len = (self.len() as f32 * factor).round() as usize; - - (0..len) - .map(|i| { - let frac = i as f32 / factor; - let index = frac.floor() as usize; - let frac = frac - index as f32; - - let sample_1 = self[index] as f32; - let sample_2 = if self.len() > index + 1 { - self[index + 1] - } else if let Some(next_sample) = next_sample { - next_sample - } else { - self[index] - } as f32; - - ((1.0 - frac) * sample_1 + frac * sample_2).floor() as i16 - }) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sound_effect_volume_works() { - assert_eq!( - vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].volume(1.0), - ); - assert_eq!( - vec![-40, 80, 160, 120, -312], - vec![-20, 40, 80, 60, -156].volume(2.0) - ); - assert_eq!( - vec![-10, 20, 40, 30, -78], - vec![-20, 40, 80, 60, -156].volume(0.5) - ); - } - - #[test] - fn pitch_with_time_stretch_works() { - assert_eq!( - vec![-10, 20, 40, 30, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(1.0, None), - ); - assert_eq!( - vec![-10, -10, 20, 20, 40, 40, 30, 30, -78, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(2.0, None), - ); - assert_eq!( - vec![-10, 40, -78], - vec![-10, 20, 40, 30, -78].pitch_with_time_stretch(0.5, None), - ); - } -} diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 846799e..b32114e 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -1,12 +1,11 @@ use std::rc::Rc; use super::{ - mixer::Mixer, t_instrument::{TInstrument, TSample}, uncompress, }; use crate::{ - asset::{extension::*, AssetParser}, + asset::{extension::*, sound::sample::Sample, AssetParser}, utils::nom::*, }; @@ -17,10 +16,8 @@ pub struct TEffect { // It should be separated impl TEffect { - pub fn mix(&self) -> Vec { - let mut m = Mixer::new(); - m.add_sample(&self.sample.data, 0); - m.mix() + pub fn mix(&self) -> Sample { + self.sample.data.clone() } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 43e3212..8d527f9 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -4,7 +4,7 @@ use bitflags::bitflags; use super::{convert_volume, finetune::FineTune}; use crate::{ - asset::{extension::*, AssetParser}, + asset::{extension::*, sound::sample::Sample, AssetParser}, utils::nom::*, }; @@ -225,8 +225,12 @@ pub struct TSample { pub panning: u8, pub align: u8, pub finetune: FineTune, - pub loop_length: u32, - pub data: Vec, + pub loop_length: f32, + pub data: Sample, +} + +impl TSample { + const SAMPLE_RATE: usize = 16_000; } impl AssetParser for TSample { @@ -245,10 +249,10 @@ impl AssetParser for TSample { let (input, loop_end) = number::le_u32(input)?; let (input, sample_offset) = number::le_u32(input)?; - // The game uses offset for `i16`, but it's much more convenient to just use indices, so that's why `/ 2` + // The game uses offset for `i16`, but it's much more convenient to just use time, so that's why `/ 2` (`i16` is 2 bytes) let loop_end = loop_end / 2; let sample_offset = sample_offset / 2; - let loop_length = loop_length / 2; + let loop_length = loop_length as f32 / 2.0 / Self::SAMPLE_RATE as f32; Ok(( input, @@ -260,7 +264,15 @@ impl AssetParser for TSample { finetune: FineTune::new(finetune), // TODO(nenikitov): Look into resampling the sample to 48 KHz loop_length, - data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), + data: Sample { + data: Box::new( + sample_data[sample_offset as usize..loop_end as usize] + .into_iter() + .map(|&s| [s]) + .collect(), + ), + sample_rate: Self::SAMPLE_RATE, + }, }, )) } @@ -268,13 +280,13 @@ impl AssetParser for TSample { } impl TSample { - pub fn sample_beginning(&self) -> &[i16] { - &self.data[..self.data.len() - self.loop_length as usize] + pub fn sample_beginning(&self) -> &[[i16; 1]] { + &self.data[..self.data.len_seconds() - self.loop_length] } - pub fn sample_loop(&self) -> &[i16] { - if self.loop_length != 0 { - &self.data[self.data.len() - self.loop_length as usize..] + pub fn sample_loop(&self) -> &[[i16; 1]] { + if self.loop_length != 0.0 { + &self.data[self.data.len_seconds() - self.loop_length..] } else { &[] } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 946387c..26f806e 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,7 +1,7 @@ mod dat; pub(crate) mod sample; -use self::dat::mixer::TSongMixer; +use self::{dat::mixer::TSongMixer, sample::Sample}; use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -17,7 +17,7 @@ pub enum Sound { } impl Sound { - pub fn mix(&self) -> Vec { + pub fn mix(&self) -> Sample { match self { Sound::Song(sound) => sound.mix(false), Sound::Effect(effect) => effect.mix(), @@ -88,11 +88,7 @@ mod tests { .enumerate() .try_for_each(|(i, song)| { let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file( - file, - song.mix() - .to_wave(SoundCollection::SAMPLE_RATE, SoundCollection::CHANNEL_COUNT), - ) + output_file(file, song.mix().to_wave()) })?; // TODO(nenikitov): Remove this debug code @@ -118,11 +114,7 @@ mod tests { .enumerate() .try_for_each(|(i, s)| { let file = output_dir.join(format!("sample-{i}.wav")); - output_file( - file, - s.data - .to_wave(SoundCollection::SAMPLE_RATE, SoundCollection::CHANNEL_COUNT), - ) + output_file(file, s.data.to_wave()) })?; let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); @@ -133,12 +125,7 @@ mod tests { .enumerate() .try_for_each(|(i, effect)| { let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file( - file, - effect - .mix() - .to_wave(SoundCollection::SAMPLE_RATE, SoundCollection::CHANNEL_COUNT), - ) + output_file(file, effect.mix().to_wave()) })?; Ok(()) diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index d656071..a9eb0e1 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,19 +1,26 @@ -use std::fmt::Debug; +use std::{ + fmt::Debug, + ops::{Index, Range, RangeFrom, RangeTo}, +}; use itertools::Itertools; -pub trait SamplePointConversions { - const BITS: usize; +pub trait SamplePointConversions +where + Self: Sized, +{ + const SIZE_BITS: usize; fn into_f32(self) -> f32; fn from_f32(value: f32) -> Self; - fn to_le_bytes(self) -> Vec; + + fn to_integer_le_bytes(self) -> Vec; } macro_rules! impl_sample_point_conversions_other { ($type: ty, $size: literal) => { impl SamplePointConversions for $type { - const BITS: usize = $size as usize; + const SIZE_BITS: usize = $size as usize; fn into_f32(self) -> f32 { self as f32 @@ -23,8 +30,10 @@ macro_rules! impl_sample_point_conversions_other { value as Self } - fn to_le_bytes(self) -> Vec { - self.to_le_bytes().to_vec() + fn to_integer_le_bytes(self) -> Vec { + ((self.clamp(-1.0, 1.0) * i32::MAX as f32).round() as i32) + .to_le_bytes() + .to_vec() } } }; @@ -32,17 +41,17 @@ macro_rules! impl_sample_point_conversions_other { macro_rules! impl_sample_point_conversions_integer { ($type: tt) => { impl SamplePointConversions for $type { - const BITS: usize = $type::BITS as usize; + const SIZE_BITS: usize = $type::BITS as usize; fn into_f32(self) -> f32 { - self as f32 + self as f32 / $type::MAX as f32 } fn from_f32(value: f32) -> Self { - value.round() as Self + (value * $type::MAX as f32).round() as Self } - fn to_le_bytes(self) -> Vec { + fn to_integer_le_bytes(self) -> Vec { self.to_le_bytes().to_vec() } } @@ -65,105 +74,176 @@ pub enum Interpolation { Linear, } +#[derive(Debug, Clone)] pub struct Sample { - pub data: [Vec; CHANNELS], + pub data: Box>, pub sample_rate: usize, } +impl Index for Sample { + type Output = [S; CHANNELS]; + + fn index(&self, index: usize) -> &Self::Output { + self.data.index(index) + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: Range) -> &Self::Output { + &self.data[range] + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: RangeFrom) -> &Self::Output { + &self.data[range] + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: RangeTo) -> &Self::Output { + &self.data[range] + } +} + +impl Index for Sample { + type Output = [S; CHANNELS]; + + fn index(&self, index: f32) -> &Self::Output { + self.data.index(self.time_to_index(index)) + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: Range) -> &Self::Output { + &self.data[self.time_to_index(range.start)..self.time_to_index(range.end)] + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: RangeFrom) -> &Self::Output { + &self.data[self.time_to_index(range.start)..] + } +} + +impl Index> for Sample { + type Output = [[S; CHANNELS]]; + + fn index(&self, range: RangeTo) -> &Self::Output { + &self.data[..self.time_to_index(range.end)] + } +} + impl Sample { - pub fn index(&self, index: usize) -> [S; CHANNELS] { - self.data - .iter() - .map(|channel| channel[index]) - .collect_vec() - .try_into() - .unwrap() + fn time_to_index(&self, time: f32) -> usize { + (time * self.sample_rate as f32) as usize } - pub fn len(&self) -> usize { - self.data[0].len() + pub fn len_samples(&self) -> usize { + self.data.len() } - pub fn at_time(&self, time: f32) -> [S; CHANNELS] { - self.index((time * self.sample_rate as f32) as usize) + pub fn len_seconds(&self) -> f32 { + self.data.len() as f32 / self.sample_rate as f32 } pub fn resample(&self, sample_rate: usize, interpolation: Interpolation) -> Self { - let mut sample = self.stretch( + let data = self.data.stretch( sample_rate as f32 / self.sample_rate as f32, Some([S::from_f32(0.0); CHANNELS]), interpolation, ); - sample.sample_rate = sample_rate; - sample + Self { + data: Box::new(data), + sample_rate: self.sample_rate, + } } +} - pub fn add_sample(&mut self, other: Sample, offset: usize) { +pub trait SampleDataProcessing { + fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize); + fn volume(&self, volume: f32) -> Self; + fn stretch( + &self, + factor: f32, + last_sample: Option<[S; CHANNELS]>, + interpolation: Interpolation, + ) -> Self; +} + +impl SampleDataProcessing + for Vec<[S; CHANNELS]> +{ + fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize) { let new_len = offset + other.len(); if new_len > self.len() { - for channel in self.data.iter_mut() { - channel.resize(new_len, S::from_f32(0.0)); - } + self.resize(new_len, [S::from_f32(0.0); CHANNELS]); } - for (channel_self, channel_other) in Iterator::zip(self.data.iter_mut(), other.data.iter()) - { - for (i, s) in channel_other.iter().enumerate() { - let i = i + offset; - channel_self[i] = S::from_f32(channel_self[i].into_f32() + s.into_f32()); + for (i, samples_other) in other.iter().enumerate() { + let i = i + offset; + + for channel_i in 0..self[i].len() { + self[i][channel_i] = S::from_f32( + self[i][channel_i].into_f32() + samples_other[channel_i].into_f32(), + ); } } } - pub fn volume(&self, volume: f32) -> Self { - let data = self - .data - .iter() - .map(|channel| { - channel + fn volume(&self, volume: f32) -> Self { + self.iter() + .map(|sample_channels| { + sample_channels .iter() .map(|&sample| (sample).into_f32() * volume) .map(|sample| S::from_f32(sample)) .collect_vec() + .try_into() + .unwrap() }) - .collect_vec(); - - Sample { - data: data.try_into().unwrap(), - ..*self - } + .collect() } - pub fn stretch( + fn stretch( &self, factor: f32, last_sample: Option<[S; CHANNELS]>, interpolation: Interpolation, ) -> Self { - let len = (self.data[0].len() as f32 * factor).round() as usize; - - let data = self - .data - .iter() - .enumerate() - .map(|(i_channel, channel)| { - (0..len) - .map(|i_sample| match interpolation { + let len = (self.len() as f32 * factor).round() as usize; + + (0..len) + .into_iter() + .map(|(i_sample)| { + (0..CHANNELS) + .into_iter() + .map(|i_channel| match interpolation { Interpolation::Nearest => { - channel[(i_sample as f32 / factor).floor() as usize] + self[(i_sample as f32 / factor).floor() as usize][i_channel] } Interpolation::Linear => { let frac = i_sample as f32 / factor; let index = frac.floor() as usize; let frac = frac - index as f32; - let sample_1 = channel[index].into_f32(); - let sample_2 = if channel.len() > index + 1 { - channel[index + 1] + let sample_1 = self[index][i_channel].into_f32(); + let sample_2 = if self.len() > index + 1 { + self[index + 1][i_channel] } else { - last_sample.map_or(channel[index], |s| s[i_channel]) + last_sample.map_or(self[index][i_channel], |s| s[i_channel]) } .into_f32(); @@ -171,13 +251,10 @@ impl Sample { } }) .collect_vec() + .try_into() + .unwrap() }) - .collect_vec(); - - Self { - data: data.try_into().unwrap(), - ..*self - } + .collect() } } @@ -190,10 +267,10 @@ impl Sample { } pub fn to_stereo(&self) -> Sample { - let data = [self.data[0].clone(), self.data[0].clone()]; + let data = self.data.iter().map(|[s]| [*s, *s]).collect(); Sample:: { - data, + data: Box::new(data), sample_rate: self.sample_rate, } } @@ -208,19 +285,178 @@ impl Sample { } pub fn to_mono(&self) -> Sample { - let data = Iterator::zip(self.data[0].iter(), self.data[1].iter()) - .map(|(sample1, sample2)| { - let sample1 = sample1.into_f32(); - let sample2 = sample2.into_f32(); + let data = self + .data + .iter() + .map(|[sample_1, sample_2]| { + let sample_1 = sample_1.into_f32(); + let sample_2 = sample_2.into_f32(); - sample1 * 0.5 + sample2 * 0.5 + [S::from_f32(sample_1 * 0.5 + sample_2 * 0.5)] }) - .map(|sample| S::from_f32(sample)) .collect_vec(); Sample:: { - data: [data], + data: Box::new(data), sample_rate: self.sample_rate, } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn volume() { + let samples = vec![ + [11, 20], + [-20, -40], + [30, 60], + [-40, -80], + [-50, -100], + [-80, -160], + ]; + + assert_eq!( + samples.volume(1.47), + vec![ + [16, 29], + [-29, -59], + [44, 88], + [-59, -118], + [-74, -147], + [-118, -235], + ] + ); + + assert_eq!( + samples.volume(0.5), + vec![ + [6, 10], + [-10, -20], + [15, 30], + [-20, -40], + [-25, -50], + [-40, -80], + ] + ); + } + + #[test] + fn stretch_nearest_integer() { + let samples = vec![ + [10, 20], + [-20, -40], + [30, 60], + [-40, -80], + [-50, -100], + [-80, -160], + ]; + + assert_eq!( + samples.stretch(2.0, None, Interpolation::Nearest), + vec![ + [10, 20], + [10, 20], + [-20, -40], + [-20, -40], + [30, 60], + [30, 60], + [-40, -80], + [-40, -80], + [-50, -100], + [-50, -100], + [-80, -160], + [-80, -160], + ] + ); + } + + #[test] + fn stretch_nearest_frac() { + let samples = vec![ + [10, 20], + [-20, -40], + [30, 60], + [-40, -80], + [-50, -100], + [-80, -160], + ]; + + assert_eq!( + samples.stretch(1.5, None, Interpolation::Nearest), + vec![ + [10, 20], + [10, 20], + [-20, -40], + [30, 60], + [30, 60], + [-40, -80], + [-50, -100], + [-50, -100], + [-80, -160], + ] + ); + } + + #[test] + fn stretch_linear_integer_without_last() { + let samples = vec![ + [10, 20], + [-20, -40], + [30, 60], + [-40, -80], + [-50, -100], + [-80, -160], + ]; + + assert_eq!( + samples.stretch(2.0, None, Interpolation::Linear), + vec![ + [10, 20], + [-5, -10], + [-20, -40], + [5, 10], + [30, 60], + [-5, -10], + [-40, -80], + [-45, -90], + [-50, -100], + [-65, -130], + [-80, -160], + [-80, -160], + ] + ); + } + + #[test] + fn stretch_linear_integer_with_last() { + let samples = vec![ + [10, 20], + [-20, -40], + [30, 60], + [-40, -80], + [-50, -100], + [-80, -160], + ]; + + assert_eq!( + samples.stretch(2.0, Some([500, 500]), Interpolation::Linear), + vec![ + [10, 20], + [-5, -10], + [-20, -40], + [5, 10], + [30, 60], + [-5, -10], + [-40, -80], + [-45, -90], + [-50, -100], + [-65, -130], + [-80, -160], + [210, 170], + ] + ); + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 6844758..400f2b6 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -15,10 +15,10 @@ #![feature( // Discussion about possible future alternatives: // https://github.com/rust-lang/rust/pull/101179 - maybe_uninit_uninit_array_transpose, - let_chains, - lazy_cell, io_error_more, + lazy_cell, + let_chains, + maybe_uninit_uninit_array_transpose, )] pub mod asset; diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index 2fce80b..721f545 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -7,7 +7,6 @@ use image::{ }, Frame, ImageEncoder, RgbaImage, }; -use itertools::Itertools; use crate::asset::{ color_map::Color, @@ -105,10 +104,10 @@ pub trait WaveFile { impl WaveFile for Sample { fn to_wave(&self) -> Vec { - let bits_per_sample: usize = S::BITS; + let bits_per_sample: usize = S::SIZE_BITS; let bytes_per_sample: usize = bits_per_sample / 8; - let size = self.len() * CHANNELS * bytes_per_sample; + let size = self.len_samples() * CHANNELS * bytes_per_sample; "RIFF" .bytes() @@ -126,12 +125,11 @@ impl WaveFile for Sample { .chain(u16::to_le_bytes(bits_per_sample as u16)) .chain("data".bytes()) .chain(u32::to_le_bytes(size as u32)) - .chain((0..self.data.len()).into_iter().flat_map(|i| { + .chain( self.data .iter() - .flat_map(|channel| channel[i].to_le_bytes()) - .collect_vec() - })) + .flat_map(|s| s.into_iter().flat_map(|s| s.to_integer_le_bytes())), + ) .collect() } } From 30abf62a721d9af62a0f27802e8ac6be4efc328a Mon Sep 17 00:00:00 2001 From: nenikitov Date: Thu, 2 May 2024 13:53:41 -0400 Subject: [PATCH 40/87] feat(sample): move volume before stretching --- engine/src/asset/sound/dat/mixer.rs | 35 ++++++----- engine/src/asset/sound/sample.rs | 92 ++++++++++++++++------------- 2 files changed, 69 insertions(+), 58 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1a340e1..d37ec23 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use super::{pattern_effect::*, pattern_event::*, t_instrument::*, t_song::*}; use crate::asset::sound::{ dat::finetune::FineTune, - sample::{Interpolation, Sample, SampleDataProcessing}, + sample::{Interpolation, Sample, SampleDataProcessing, SamplePointProcessing}, }; pub trait TSongMixer { @@ -223,25 +223,20 @@ impl<'a> Channel<'a> { // Generate data let volume_envelope = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { - if note.on { - envelope - .volume_beginning() - .get(self.volume_evelope_position) - .map(ToOwned::to_owned) - .unwrap_or(envelope.volume_loop()) + let (envelope, default) = if note.on { + (envelope.volume_beginning(), envelope.volume_loop()) } else { - envelope - .volume_end() - .get(self.volume_evelope_position) - .map(ToOwned::to_owned) - .unwrap_or_default() - } + (envelope.volume_end(), 0.0) + }; + envelope + .get(self.volume_evelope_position) + .map(ToOwned::to_owned) + .unwrap_or(default) } TInstrumentVolume::Constant(volume) => *volume, }; let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); - let duration_scaled = (duration as f64 / pitch_factor).round() as usize; let mut sample = sample @@ -254,11 +249,15 @@ impl<'a> Channel<'a> { .collect::>(); let first_sample_after = sample.pop(); - let pitch_factor = duration as f32 / sample.len() as f32; - let mut data = sample - .stretch(pitch_factor, first_sample_after, Interpolation::Linear) - .volume(volume_scale * self.volume.clamp(0.0, 4.0)); + let volume = volume_scale * self.volume.clamp(0.0, 4.0); + + let mut data = sample.volume(volume).stretch( + pitch_factor, + Interpolation::Linear { + first_sample_after: first_sample_after.map(|s| s.volume(volume)), + }, + ); data.truncate(duration); // Update diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index a9eb0e1..257b6f9 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -69,9 +69,11 @@ impl SamplePoint for i32 {} impl SamplePoint for f32 {} #[derive(Debug, Clone, Copy)] -pub enum Interpolation { +pub enum Interpolation { Nearest, - Linear, + Linear { + first_sample_after: Option<[S; CHANNELS]>, + }, } #[derive(Debug, Clone)] @@ -157,12 +159,10 @@ impl Sample { self.data.len() as f32 / self.sample_rate as f32 } - pub fn resample(&self, sample_rate: usize, interpolation: Interpolation) -> Self { - let data = self.data.stretch( - sample_rate as f32 / self.sample_rate as f32, - Some([S::from_f32(0.0); CHANNELS]), - interpolation, - ); + pub fn resample(&self, sample_rate: usize, interpolation: Interpolation) -> Self { + let data = self + .data + .stretch(sample_rate as f32 / self.sample_rate as f32, interpolation); Self { data: Box::new(data), @@ -171,15 +171,32 @@ impl Sample { } } +pub trait SamplePointProcessing { + fn add_sample(&mut self, other: &[S; CHANNELS]); + fn volume(&self, volume: f32) -> Self; +} + +impl SamplePointProcessing for [S; CHANNELS] { + fn add_sample(&mut self, other: &[S; CHANNELS]) { + for channel_i in 0..CHANNELS { + self[channel_i] = S::from_f32(self[channel_i].into_f32() + other[channel_i].into_f32()); + } + } + + fn volume(&self, volume: f32) -> Self { + self.iter() + .map(|sample| sample.into_f32() * volume) + .map(S::from_f32) + .collect_vec() + .try_into() + .unwrap() + } +} + pub trait SampleDataProcessing { fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize); fn volume(&self, volume: f32) -> Self; - fn stretch( - &self, - factor: f32, - last_sample: Option<[S; CHANNELS]>, - interpolation: Interpolation, - ) -> Self; + fn stretch(&self, factor: f32, interpolation: Interpolation) -> Self; } impl SampleDataProcessing @@ -195,34 +212,17 @@ impl SampleDataProcessing for (i, samples_other) in other.iter().enumerate() { let i = i + offset; - for channel_i in 0..self[i].len() { - self[i][channel_i] = S::from_f32( - self[i][channel_i].into_f32() + samples_other[channel_i].into_f32(), - ); + for channel_i in 0..CHANNELS { + self[i].add_sample(samples_other); } } } fn volume(&self, volume: f32) -> Self { - self.iter() - .map(|sample_channels| { - sample_channels - .iter() - .map(|&sample| (sample).into_f32() * volume) - .map(|sample| S::from_f32(sample)) - .collect_vec() - .try_into() - .unwrap() - }) - .collect() + self.iter().map(|samples| samples.volume(volume)).collect() } - fn stretch( - &self, - factor: f32, - last_sample: Option<[S; CHANNELS]>, - interpolation: Interpolation, - ) -> Self { + fn stretch(&self, factor: f32, interpolation: Interpolation) -> Self { let len = (self.len() as f32 * factor).round() as usize; (0..len) @@ -234,7 +234,9 @@ impl SampleDataProcessing Interpolation::Nearest => { self[(i_sample as f32 / factor).floor() as usize][i_channel] } - Interpolation::Linear => { + Interpolation::Linear { + first_sample_after: last_sample, + } => { let frac = i_sample as f32 / factor; let index = frac.floor() as usize; let frac = frac - index as f32; @@ -355,7 +357,7 @@ mod tests { ]; assert_eq!( - samples.stretch(2.0, None, Interpolation::Nearest), + samples.stretch(2.0, Interpolation::Nearest), vec![ [10, 20], [10, 20], @@ -385,7 +387,7 @@ mod tests { ]; assert_eq!( - samples.stretch(1.5, None, Interpolation::Nearest), + samples.stretch(1.5, Interpolation::Nearest), vec![ [10, 20], [10, 20], @@ -412,7 +414,12 @@ mod tests { ]; assert_eq!( - samples.stretch(2.0, None, Interpolation::Linear), + samples.stretch( + 2.0, + Interpolation::Linear { + first_sample_after: None + } + ), vec![ [10, 20], [-5, -10], @@ -442,7 +449,12 @@ mod tests { ]; assert_eq!( - samples.stretch(2.0, Some([500, 500]), Interpolation::Linear), + samples.stretch( + 2.0, + Interpolation::Linear { + first_sample_after: Some([500, 500]) + } + ), vec![ [10, 20], [-5, -10], From dae3928f18a9f77a80a371332a4bfdaf308f93c3 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 3 May 2024 15:53:27 -0400 Subject: [PATCH 41/87] refactor(effects): improve effect memory --- engine/src/asset/sound/dat/mixer.rs | 53 +++++++++++++++++------------ engine/src/asset/sound/mod.rs | 30 ++++------------ 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index d37ec23..4ad2469 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -27,7 +27,7 @@ impl TSongMixer for TSong { trait TSongMixerUtils { const SAMPLE_RATE: usize = 16_000; - const VOLUME_SCALE: f32 = 0.5; + const VOLUME_SCALE: f32 = 1.0; fn mix(&self, start: usize) -> Sample; @@ -50,9 +50,7 @@ impl TSongMixerUtils for TSong { for pattern in &self.orders[start..] { for row in &**pattern { // Update channels - for (c, event) in row.iter().enumerate() { - let channel = &mut channels[c]; - + for (event, channel) in Iterator::zip(row.iter(), channels.iter_mut()) { // Process note if let Some(note) = event.note { channel.change_note(note); @@ -63,41 +61,55 @@ impl TSongMixerUtils for TSong { if let Some(volume) = event.volume { channel.change_volume(volume); } - for (i, effect) in event.effects.iter().enumerate() { - if let Some(effect) = effect { - channel.change_effect(i, *effect); - } - } + + let effects: Vec<_> = event + .effects + .iter() + .enumerate() + .filter_map(|(i, effect)| { + if let Some(effect) = effect { + channel.change_effect(i, *effect); + Some(i) + } else { + None + } + }) + .collect(); // Init effects // Efffects from now on have their memory initialized - for effect in event.effects.iter().flatten() { + for effect in effects.into_iter().map(|e| { + channel.effects[e].expect("effect is initialized after assignment") + }) { match effect { PatternEffect::Dummy => {} PatternEffect::Speed(Speed::Bpm(s)) => { - bpm = *s; + bpm = s; } PatternEffect::Speed(Speed::TicksPerRow(s)) => { - speed = *s; + speed = s; } PatternEffect::Volume(Volume::Value(volume)) => { - channel.volume = *volume; + channel.volume = volume; } PatternEffect::Volume(Volume::Slide(Some(volume))) => { - channel.volume_slide = *volume; + channel.volume_slide = volume; } PatternEffect::SampleOffset(Some(offset)) => { - channel.sample_position = *offset; + channel.sample_position = offset; + } + PatternEffect::Volume(Volume::Slide(None)) + | PatternEffect::SampleOffset(None) => { + unreachable!("effect memory should already be initialized") } - _ => (), }; } // Process repeatable effects for effect in channel.effects.iter().flatten() { match effect { - PatternEffect::Volume(Volume::Slide(_)) => { - channel.volume = channel.volume + channel.volume_slide; + PatternEffect::Volume(Volume::Slide(Some(volume))) => { + channel.volume += volume; } _ => {} } @@ -235,10 +247,10 @@ impl<'a> Channel<'a> { } TInstrumentVolume::Constant(volume) => *volume, }; + let volume = volume_scale * volume_envelope * self.volume.clamp(0.0, 4.0); let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; - let mut sample = sample .sample_beginning() .iter() @@ -247,10 +259,8 @@ impl<'a> Channel<'a> { .take(duration_scaled + 1) .copied() .collect::>(); - let first_sample_after = sample.pop(); let pitch_factor = duration as f32 / sample.len() as f32; - let volume = volume_scale * self.volume.clamp(0.0, 4.0); let mut data = sample.volume(volume).stretch( pitch_factor, @@ -258,7 +268,6 @@ impl<'a> Channel<'a> { first_sample_after: first_sample_after.map(|s| s.volume(volume)), }, ); - data.truncate(duration); // Update self.sample_position += duration_scaled; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 26f806e..8f95baf 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -82,15 +82,6 @@ mod tests { let output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); - sounds - .iter() - .filter(|s| matches!(s, Sound::Song(_))) - .enumerate() - .try_for_each(|(i, song)| { - let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file(file, song.mix().to_wave()) - })?; - // TODO(nenikitov): Remove this debug code let test_music = sounds .iter() @@ -98,23 +89,16 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0x1]; - let file = output_dir.join("test.wav"); - - dbg!(&test_music.patterns[5][0x32][1].effects); + .collect::>()[0x0]; + dbg!(&test_music.patterns[0][0x17][2].effects); - // dbg!(&test_music.patterns[2] - // .iter() - // .map(|p| p.iter().map(|r| &r.effects).collect::>()) - // .collect::>()); - - test_music - .samples + sounds .iter() + .filter(|s| matches!(s, Sound::Song(_))) .enumerate() - .try_for_each(|(i, s)| { - let file = output_dir.join(format!("sample-{i}.wav")); - output_file(file, s.data.to_wave()) + .try_for_each(|(i, song)| { + let file = output_dir.join(format!("{i:0>2X}.wav")); + output_file(file, song.mix().to_wave()) })?; let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From 8a30caccbdfa69731c617532af46368802b56c88 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 3 May 2024 16:18:32 -0400 Subject: [PATCH 42/87] feat(effect): start playback direction --- engine/src/asset/sound/dat/mixer.rs | 12 +++++++----- engine/src/asset/sound/dat/pattern_effect.rs | 16 ++++++++++++---- engine/src/asset/sound/dat/t_instrument.rs | 10 ++++------ engine/src/asset/sound/mod.rs | 4 ++-- engine/src/asset/sound/sample.rs | 10 ++++------ 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 4ad2469..fafb046 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -82,7 +82,7 @@ impl TSongMixerUtils for TSong { channel.effects[e].expect("effect is initialized after assignment") }) { match effect { - PatternEffect::Dummy => {} + PatternEffect::Dummy(_) => {} PatternEffect::Speed(Speed::Bpm(s)) => { bpm = s; } @@ -92,16 +92,17 @@ impl TSongMixerUtils for TSong { PatternEffect::Volume(Volume::Value(volume)) => { channel.volume = volume; } - PatternEffect::Volume(Volume::Slide(Some(volume))) => { - channel.volume_slide = volume; - } PatternEffect::SampleOffset(Some(offset)) => { channel.sample_position = offset; } + PatternEffect::PlaybackDirection(direction) => { + channel.playback_direaction = direction + } PatternEffect::Volume(Volume::Slide(None)) | PatternEffect::SampleOffset(None) => { unreachable!("effect memory should already be initialized") } + _ => {} }; } @@ -172,7 +173,8 @@ struct Channel<'a> { volume: f32, volume_evelope_position: usize, - volume_slide: f32, + + playback_direaction: PlaybackDirection, } impl<'a> Channel<'a> { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 0d58c15..7c3fedf 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -18,6 +18,13 @@ pub enum Volume { Slide(Option), } +#[derive(Debug, Default, Clone, Copy)] +pub enum PlaybackDirection { + #[default] + Forwards, + Backwards, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum PatternEffectMemoryKey { VolumeSlide, @@ -26,10 +33,11 @@ pub enum PatternEffectMemoryKey { #[derive(Debug, Clone, Copy)] pub enum PatternEffect { - Dummy, + Dummy(u8), Speed(Speed), Volume(Volume), SampleOffset(Option), + PlaybackDirection(PlaybackDirection), } impl PatternEffect { @@ -88,7 +96,7 @@ impl AssetParser for Option { 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x21 | 0x22 | 0x24 | 0x25 | 0x2E | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 - | 0x34 | 0x35 | 0x36 | 0x37 => PatternEffect::Dummy, + | 0x34 | 0x35 => PatternEffect::Dummy(kind), // TODO(nenikitov): Add support for other effects // 0x00 => Self::Arpegio, // 0x01 => Self::PortaUp, @@ -123,8 +131,8 @@ impl AssetParser for Option { // 0x33 => Self::SoundControlQuad, // 0x34 => Self::FilterGlobal, // 0x35 => Self::FilterLocal, - // 0x36 => Self::PlayForward, - // 0x37 => Self::PlayBackward, + 0x36 => PatternEffect::PlaybackDirection(PlaybackDirection::Forwards), + 0x37 => PatternEffect::PlaybackDirection(PlaybackDirection::Backwards), // TODO(nenikitov): Should be a `Result` kind => unreachable!("Effect is outside the range {kind}"), }), diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 8d527f9..8723acb 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -265,12 +265,10 @@ impl AssetParser for TSample { // TODO(nenikitov): Look into resampling the sample to 48 KHz loop_length, data: Sample { - data: Box::new( - sample_data[sample_offset as usize..loop_end as usize] - .into_iter() - .map(|&s| [s]) - .collect(), - ), + data: sample_data[sample_offset as usize..loop_end as usize] + .into_iter() + .map(|&s| [s]) + .collect(), sample_rate: Self::SAMPLE_RATE, }, }, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 8f95baf..4ffa6f8 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -89,8 +89,8 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0x0]; - dbg!(&test_music.patterns[0][0x17][2].effects); + .collect::>()[0x9]; + dbg!(&test_music.patterns[0][0x29][5].effects); sounds .iter() diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 257b6f9..4695d92 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -78,7 +78,7 @@ pub enum Interpolation { #[derive(Debug, Clone)] pub struct Sample { - pub data: Box>, + pub data: Vec<[S; CHANNELS]>, pub sample_rate: usize, } @@ -165,7 +165,7 @@ impl Sample { .stretch(sample_rate as f32 / self.sample_rate as f32, interpolation); Self { - data: Box::new(data), + data, sample_rate: self.sample_rate, } } @@ -226,10 +226,8 @@ impl SampleDataProcessing let len = (self.len() as f32 * factor).round() as usize; (0..len) - .into_iter() .map(|(i_sample)| { (0..CHANNELS) - .into_iter() .map(|i_channel| match interpolation { Interpolation::Nearest => { self[(i_sample as f32 / factor).floor() as usize][i_channel] @@ -272,7 +270,7 @@ impl Sample { let data = self.data.iter().map(|[s]| [*s, *s]).collect(); Sample:: { - data: Box::new(data), + data, sample_rate: self.sample_rate, } } @@ -299,7 +297,7 @@ impl Sample { .collect_vec(); Sample:: { - data: Box::new(data), + data, sample_rate: self.sample_rate, } } From 0ddc5f0772e2c2e1fc96f86565d6ef7c4f64067b Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 4 May 2024 21:54:57 -0400 Subject: [PATCH 43/87] perf(sample): majorly optimized audio generation --- engine/src/asset/sound/dat/mixer.rs | 10 +++++----- engine/src/asset/sound/dat/t_instrument.rs | 6 ++---- engine/src/asset/sound/sample.rs | 15 +++++++------- engine/src/utils/iterator.rs | 23 ++++++++++++++++++++++ engine/src/utils/mod.rs | 1 + 5 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 engine/src/utils/iterator.rs diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index fafb046..a00d2a6 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -172,7 +172,7 @@ struct Channel<'a> { sample_position: usize, volume: f32, - volume_evelope_position: usize, + volume_envelope_position: usize, playback_direaction: PlaybackDirection, } @@ -191,7 +191,7 @@ impl<'a> Channel<'a> { } (Some(current), PatternEventNote::Off) => { current.on = false; - self.volume_evelope_position = 0; + self.volume_envelope_position = 0; } (Some(current), PatternEventNote::On(target)) => { current.finetune = target; @@ -203,7 +203,7 @@ impl<'a> Channel<'a> { fn change_instrument(&mut self, instrument: &'a PatternEventInstrumentKind) { self.instrument = Some(instrument); self.sample_position = 0; - self.volume_evelope_position = 0; + self.volume_envelope_position = 0; } fn change_volume(&mut self, volume: PatternEventVolume) { @@ -243,7 +243,7 @@ impl<'a> Channel<'a> { (envelope.volume_end(), 0.0) }; envelope - .get(self.volume_evelope_position) + .get(self.volume_envelope_position) .map(ToOwned::to_owned) .unwrap_or(default) } @@ -273,7 +273,7 @@ impl<'a> Channel<'a> { // Update self.sample_position += duration_scaled; - self.volume_evelope_position += 1; + self.volume_envelope_position += 1; // Return data diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 8723acb..6a2f825 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -5,7 +5,7 @@ use bitflags::bitflags; use super::{convert_volume, finetune::FineTune}; use crate::{ asset::{extension::*, sound::sample::Sample, AssetParser}, - utils::nom::*, + utils::{iterator::CollectArray, nom::*}, }; // TODO(nenikitov): Double check these flags @@ -200,9 +200,7 @@ impl AssetParser for TInstrument { TInstrumentSampleKind::Predefined(samples[i as usize].clone()) } }) - .collect::>() - .try_into() - .unwrap(), + .collect_array(), ), }, )) diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 4695d92..ce0c9b3 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,3 +1,4 @@ +// TODO(nenikitov): Remove this test code use std::{ fmt::Debug, ops::{Index, Range, RangeFrom, RangeTo}, @@ -5,6 +6,8 @@ use std::{ use itertools::Itertools; +use crate::utils::iterator::CollectArray; + pub trait SamplePointConversions where Self: Sized, @@ -31,7 +34,7 @@ macro_rules! impl_sample_point_conversions_other { } fn to_integer_le_bytes(self) -> Vec { - ((self.clamp(-1.0, 1.0) * i32::MAX as f32).round() as i32) + ((self.clamp(-1.0, 1.0) * i32::MAX as f32) as i32) .to_le_bytes() .to_vec() } @@ -48,7 +51,7 @@ macro_rules! impl_sample_point_conversions_integer { } fn from_f32(value: f32) -> Self { - (value * $type::MAX as f32).round() as Self + (value * $type::MAX as f32) as Self } fn to_integer_le_bytes(self) -> Vec { @@ -187,9 +190,7 @@ impl SamplePointProcessing f self.iter() .map(|sample| sample.into_f32() * volume) .map(S::from_f32) - .collect_vec() - .try_into() - .unwrap() + .collect_array::() } } @@ -250,9 +251,7 @@ impl SampleDataProcessing S::from_f32((1.0 - frac) * sample_1 + frac * sample_2) } }) - .collect_vec() - .try_into() - .unwrap() + .collect_array::() }) .collect() } diff --git a/engine/src/utils/iterator.rs b/engine/src/utils/iterator.rs new file mode 100644 index 0000000..e25531b --- /dev/null +++ b/engine/src/utils/iterator.rs @@ -0,0 +1,23 @@ +use std::mem::{self, MaybeUninit}; + +// Code from [this post](https://www.reddit.com/r/learnrust/comments/lfw6uy/comment/gn16m4o) +pub trait CollectArray: Sized + Iterator { + fn collect_array(self) -> [Self::Item; N] { + assert!(N > 0 && mem::size_of::() > 0); + let mut array = MaybeUninit::uninit(); + let array_ptr = array.as_mut_ptr() as *mut Self::Item; + + let mut i = 0; + unsafe { + for item in self { + assert!(i < N); + array_ptr.add(i).write(item); + i += 1; + } + assert!(i == N); + array.assume_init() + } + } +} + +impl CollectArray for T where T: Iterator {} diff --git a/engine/src/utils/mod.rs b/engine/src/utils/mod.rs index 91e6738..8474c3c 100644 --- a/engine/src/utils/mod.rs +++ b/engine/src/utils/mod.rs @@ -4,3 +4,4 @@ pub mod format; pub mod nom; #[cfg(test)] pub mod test; +pub mod iterator; From 13ce305a6d6a25651c9d0c91cd0267965d814743 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 4 May 2024 23:59:54 -0400 Subject: [PATCH 44/87] feat(effect): playback direction --- engine/src/asset/sound/dat/mixer.rs | 53 +++++++++++++++++++++-------- engine/src/asset/sound/mod.rs | 2 +- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index a00d2a6..e3daf72 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -47,10 +47,13 @@ impl TSongMixerUtils for TSong { let mut bpm = self.bpm as usize; let mut speed = self.speed as usize; - for pattern in &self.orders[start..] { - for row in &**pattern { + // TODO!(nenikitov): Remove all `enumerate` + for (p, pattern) in self.orders[start..].iter().enumerate() { + for (r, row) in pattern.iter().enumerate() { // Update channels - for (event, channel) in Iterator::zip(row.iter(), channels.iter_mut()) { + for (c, (event, channel)) in + Iterator::zip(row.iter(), channels.iter_mut()).enumerate() + { // Process note if let Some(note) = event.note { channel.change_note(note); @@ -95,8 +98,18 @@ impl TSongMixerUtils for TSong { PatternEffect::SampleOffset(Some(offset)) => { channel.sample_position = offset; } - PatternEffect::PlaybackDirection(direction) => { - channel.playback_direaction = direction + PatternEffect::PlaybackDirection(PlaybackDirection::Forwards) => { + channel.playback_direction = PlaybackDirection::Forwards + } + PatternEffect::PlaybackDirection(PlaybackDirection::Backwards) => { + channel.playback_direction = PlaybackDirection::Backwards; + if channel.sample_position == 0 + && let Some((_, _, sample)) = + channel.get_note_instrument_sample() + { + channel.sample_position = sample.sample_beginning().len() + + sample.sample_loop().len(); + } } PatternEffect::Volume(Volume::Slide(None)) | PatternEffect::SampleOffset(None) => { @@ -174,7 +187,7 @@ struct Channel<'a> { volume: f32, volume_envelope_position: usize, - playback_direaction: PlaybackDirection, + playback_direction: PlaybackDirection, } impl<'a> Channel<'a> { @@ -204,6 +217,7 @@ impl<'a> Channel<'a> { self.instrument = Some(instrument); self.sample_position = 0; self.volume_envelope_position = 0; + self.playback_direction = PlaybackDirection::Forwards; } fn change_volume(&mut self, volume: PatternEventVolume) { @@ -253,14 +267,25 @@ impl<'a> Channel<'a> { let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; - let mut sample = sample - .sample_beginning() - .iter() - .chain(sample.sample_loop().iter().cycle()) - .skip(self.sample_position) - .take(duration_scaled + 1) - .copied() - .collect::>(); + let mut sample = match self.playback_direction { + PlaybackDirection::Forwards => sample + .sample_beginning() + .iter() + .chain(sample.sample_loop().iter().cycle()) + .skip(self.sample_position) + .take(duration_scaled + 1) + .copied() + .collect::>(), + PlaybackDirection::Backwards => sample + .sample_beginning() + .iter() + .chain(sample.sample_loop()) + .rev() + .skip(self.sample_position) + .take(duration_scaled + 1) + .copied() + .collect::>(), + }; let first_sample_after = sample.pop(); let pitch_factor = duration as f32 / sample.len() as f32; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 4ffa6f8..6b2c91e 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -90,7 +90,7 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0x9]; - dbg!(&test_music.patterns[0][0x29][5].effects); + //dbg!(&test_music.patterns[0][0x29][5].effects); sounds .iter() From 89f67a68d7a3f9716bf499b3d498c48926e74d78 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 5 May 2024 16:03:46 -0400 Subject: [PATCH 45/87] feat(effect): porta slide and bump --- engine/src/asset/sound/dat/finetune.rs | 16 ++- engine/src/asset/sound/dat/mixer.rs | 21 +++- engine/src/asset/sound/dat/pattern_effect.rs | 122 ++++++++++++++++--- engine/src/asset/sound/mod.rs | 8 +- 4 files changed, 148 insertions(+), 19 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index e03f79c..c183bf6 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, Sub}; +use std::ops::{Add, AddAssign, Neg, Sub}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FineTune { @@ -46,6 +46,12 @@ impl Add for FineTune { } } +impl AddAssign for FineTune { + fn add_assign(&mut self, rhs: Self) { + self.cents = self.cents.saturating_add(rhs.cents) + } +} + impl Sub for FineTune { type Output = FineTune; @@ -54,6 +60,14 @@ impl Sub for FineTune { } } +impl Neg for FineTune { + type Output = FineTune; + + fn neg(self) -> Self::Output { + FineTune::new(-self.cents) + } +} + #[cfg(test)] mod tests { use assert_approx_eq::assert_approx_eq; diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index e3daf72..1b4fae4 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -92,9 +92,18 @@ impl TSongMixerUtils for TSong { PatternEffect::Speed(Speed::TicksPerRow(s)) => { speed = s; } - PatternEffect::Volume(Volume::Value(volume)) => { + PatternEffect::Volume(Volume::Set(volume)) => { channel.volume = volume; } + PatternEffect::Porta(Porta::Bump { + up: _, + small: _, + finetune: Some(finetune), + }) => { + if let Some(note) = &mut channel.note { + note.finetune += finetune; + } + } PatternEffect::SampleOffset(Some(offset)) => { channel.sample_position = offset; } @@ -125,6 +134,14 @@ impl TSongMixerUtils for TSong { PatternEffect::Volume(Volume::Slide(Some(volume))) => { channel.volume += volume; } + PatternEffect::Porta(Porta::Slide { + up: _, + finetune: Some(finetune), + }) => { + if let Some(note) = &mut channel.note { + note.finetune += *finetune; + } + } _ => {} } } @@ -238,7 +255,7 @@ impl<'a> Channel<'a> { if let Some(note) = &self.note && let Some(PatternEventInstrumentKind::Predefined(instrument)) = self.instrument && let TInstrumentSampleKind::Predefined(sample) = - &instrument.samples[note.finetune.note() as usize] + &instrument.samples[note.finetune.note().clamp(0, 95) as usize] { Some((note, instrument, sample)) } else { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 7c3fedf..70227c6 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,6 +1,6 @@ use std::hash::Hash; -use super::convert_volume; +use super::{convert_volume, finetune::FineTune}; use crate::{ asset::{extension::*, AssetParser}, utils::nom::*, @@ -14,8 +14,23 @@ pub enum Speed { #[derive(Debug, Clone, Copy)] pub enum Volume { - Value(f32), + Set(f32), Slide(Option), + Bump { up: bool, volume: Option }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Porta { + Tone(Option), + Slide { + up: bool, + finetune: Option, + }, + Bump { + up: bool, + small: bool, + finetune: Option, + }, } #[derive(Debug, Default, Clone, Copy)] @@ -28,7 +43,16 @@ pub enum PlaybackDirection { #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum PatternEffectMemoryKey { VolumeSlide, + VolumeBumpUp, + VolumeBumpDown, SampleOffset, + PortaTone, + PortaSlideUp, + PortaSlideDown, + PortaBumpUp, + PortaBumpDown, + PortaBumpSmallUp, + PortaBumpSmallDown, } #[derive(Debug, Clone, Copy)] @@ -36,6 +60,7 @@ pub enum PatternEffect { Dummy(u8), Speed(Speed), Volume(Volume), + Porta(Porta), SampleOffset(Option), PlaybackDirection(PlaybackDirection), } @@ -43,7 +68,44 @@ pub enum PatternEffect { impl PatternEffect { pub fn memory_key(&self) -> Option { match self { + PatternEffect::Porta(Porta::Tone(_)) => Some(PatternEffectMemoryKey::PortaTone), + PatternEffect::Porta(Porta::Slide { + up: true, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaSlideUp), + PatternEffect::Porta(Porta::Slide { + up: false, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaSlideDown), + PatternEffect::Porta(Porta::Bump { + up: true, + small: false, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaBumpUp), + PatternEffect::Porta(Porta::Bump { + up: false, + small: false, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaBumpDown), + PatternEffect::Porta(Porta::Bump { + up: true, + small: true, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaBumpSmallUp), + PatternEffect::Porta(Porta::Bump { + up: false, + small: true, + finetune: _, + }) => Some(PatternEffectMemoryKey::PortaBumpSmallDown), PatternEffect::Volume(Volume::Slide(_)) => Some(PatternEffectMemoryKey::VolumeSlide), + PatternEffect::Volume(Volume::Bump { + up: true, + volume: _, + }) => Some(PatternEffectMemoryKey::VolumeBumpUp), + PatternEffect::Volume(Volume::Bump { + up: down, + volume: _, + }) => Some(PatternEffectMemoryKey::VolumeBumpDown), PatternEffect::SampleOffset(_) => Some(PatternEffectMemoryKey::SampleOffset), _ => None, } @@ -73,30 +135,60 @@ impl AssetParser for Option { Ok(( input, - should_parse.then(|| match kind { - 0x09 => PatternEffect::SampleOffset(if value != 0 { - Some(value as usize * 256) - } else { - None + should_parse.then_some(match kind { + 0x01 => PatternEffect::Porta(Porta::Slide { + up: true, + finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), + }), + 0x02 => PatternEffect::Porta(Porta::Slide { + up: false, + finetune: (value != 0).then_some(-FineTune::new(8 * value as i32)), + }), + 0x03 => PatternEffect::Porta(Porta::Tone( + (value != 0).then_some(FineTune::new(8 * value as i32)), + )), + 0x15 => PatternEffect::Porta(Porta::Bump { + up: true, + small: false, + finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), + }), + 0x16 => PatternEffect::Porta(Porta::Bump { + up: false, + small: false, + finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), + }), + 0x24 => PatternEffect::Porta(Porta::Bump { + up: true, + small: true, + finetune: (value != 0).then_some(FineTune::new(2 * value as i32)), + }), + 0x25 => PatternEffect::Porta(Porta::Bump { + up: false, + small: true, + finetune: (value != 0).then_some(FineTune::new(2 * value as i32)), }), + 0x09 => { + PatternEffect::SampleOffset((value != 0).then_some(value as usize * 256)) + } 0x0E => PatternEffect::Speed(if value >= 0x20 { Speed::Bpm(value as usize) } else { Speed::TicksPerRow(value as usize) }), - 0x0C => PatternEffect::Volume(Volume::Value(convert_volume(value))), - 0x0A => PatternEffect::Volume(Volume::Slide((value != 0).then(|| { + 0x0C => PatternEffect::Volume(Volume::Set(convert_volume(value))), + 0x0A => PatternEffect::Volume(Volume::Slide((value != 0).then_some( if value >= 16 { convert_volume(value / 16) } else { -convert_volume(value) - } - }))), + }, + ))), // TODO(nenikitov): Remove dummy effect - 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B - | 0x0C | 0x0D | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 - | 0x21 | 0x22 | 0x24 | 0x25 | 0x2E | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 - | 0x34 | 0x35 => PatternEffect::Dummy(kind), + 0x00 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D + | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x21 | 0x22 + | 0x2E | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => { + PatternEffect::Dummy(kind) + } // TODO(nenikitov): Add support for other effects // 0x00 => Self::Arpegio, // 0x01 => Self::PortaUp, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 6b2c91e..7e372bb 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -90,7 +90,13 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[0x9]; - //dbg!(&test_music.patterns[0][0x29][5].effects); + let effects = test_music + .orders + .iter() + .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) + .flatten() + .collect::>(); + //dbg!(effects); sounds .iter() From 830668a21cd1397049a489e29fd8bc1dba7101b6 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 6 May 2024 21:07:55 -0400 Subject: [PATCH 46/87] feat(effect): porta tone --- Cargo.toml | 9 +++ engine/src/asset/sound/dat/finetune.rs | 8 ++- engine/src/asset/sound/dat/mixer.rs | 57 ++++++++++++------ engine/src/asset/sound/dat/pattern_effect.rs | 63 ++++++++++---------- engine/src/asset/sound/dat/pattern_event.rs | 2 +- engine/src/asset/sound/mod.rs | 15 +++-- engine/src/utils/iterator.rs | 24 ++++---- 7 files changed, 108 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62c5a0f..ca258b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,12 @@ members = [ "ashen", "engine", ] + + +[profile.perf] +inherits = "release" +codegen-units = 1 +incremental = false +lto = "fat" +opt-level = 3 +panic = "abort" diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index c183bf6..7f7cd11 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,6 +1,6 @@ use std::ops::{Add, AddAssign, Neg, Sub}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord)] pub struct FineTune { cents: i32, } @@ -68,6 +68,12 @@ impl Neg for FineTune { } } +impl PartialOrd for FineTune { + fn partial_cmp(&self, other: &Self) -> Option { + self.cents.partial_cmp(&other.cents) + } +} + #[cfg(test)] mod tests { use assert_approx_eq::assert_approx_eq; diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1b4fae4..70541e3 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,7 +1,5 @@ use std::{collections::HashMap, rc::Rc}; -use itertools::Itertools; - use super::{pattern_effect::*, pattern_event::*, t_instrument::*, t_song::*}; use crate::asset::sound::{ dat::finetune::FineTune, @@ -92,18 +90,23 @@ impl TSongMixerUtils for TSong { PatternEffect::Speed(Speed::TicksPerRow(s)) => { speed = s; } - PatternEffect::Volume(Volume::Set(volume)) => { - channel.volume = volume; - } PatternEffect::Porta(Porta::Bump { - up: _, - small: _, finetune: Some(finetune), + .. }) => { if let Some(note) = &mut channel.note { note.finetune += finetune; } } + PatternEffect::Volume(Volume::Set(volume)) => { + channel.volume = volume; + } + PatternEffect::Volume(Volume::Bump { + volume: Some(volume), + .. + }) => { + channel.volume += volume; + } PatternEffect::SampleOffset(Some(offset)) => { channel.sample_position = offset; } @@ -120,9 +123,13 @@ impl TSongMixerUtils for TSong { + sample.sample_loop().len(); } } - PatternEffect::Volume(Volume::Slide(None)) + PatternEffect::Porta(Porta::Tone(None)) + | PatternEffect::Porta(Porta::Slide { finetune: None, .. }) + | PatternEffect::Porta(Porta::Bump { finetune: None, .. }) + | PatternEffect::Volume(Volume::Slide(None)) + | PatternEffect::Volume(Volume::Bump { volume: None, .. }) | PatternEffect::SampleOffset(None) => { - unreachable!("effect memory should already be initialized") + unreachable!("effect {effect:?} ({:?}) at ({p} {r} {c}) memory should already be initialized", effect.memory_key()) } _ => {} }; @@ -131,17 +138,33 @@ impl TSongMixerUtils for TSong { // Process repeatable effects for effect in channel.effects.iter().flatten() { match effect { - PatternEffect::Volume(Volume::Slide(Some(volume))) => { - channel.volume += volume; + PatternEffect::Porta(Porta::Tone(Some(step))) => { + if let Some(note) = &mut channel.note { + note.finetune = match note.finetune.cmp(¬e.finetune_initial) + { + std::cmp::Ordering::Less => FineTune::min( + note.finetune + *step, + note.finetune_initial, + ), + std::cmp::Ordering::Greater => FineTune::max( + note.finetune - *step, + note.finetune_initial, + ), + std::cmp::Ordering::Equal => note.finetune, + } + } } PatternEffect::Porta(Porta::Slide { - up: _, finetune: Some(finetune), + .. }) => { if let Some(note) = &mut channel.note { note.finetune += *finetune; } } + PatternEffect::Volume(Volume::Slide(Some(volume))) => { + channel.volume += volume; + } _ => {} } } @@ -154,11 +177,6 @@ impl TSongMixerUtils for TSong { let sample_length = sample_length as usize; let tick_length = sample_length / speed; - let volumes = channels - .iter() - .map(|c| format!("{: >10}", c.volume)) - .join(" "); - for (i, c) in channels.iter_mut().enumerate() { for j in 0..speed { let offset = offset + j * tick_length; @@ -187,8 +205,10 @@ impl TSongMixerUtils for TSong { } } +#[derive(Clone)] struct ChannelNote { finetune: FineTune, + finetune_initial: FineTune, on: bool, } @@ -216,6 +236,7 @@ impl<'a> Channel<'a> { (None, PatternEventNote::On(target)) => { self.note = Some(ChannelNote { finetune: target, + finetune_initial: target, on: true, }); } @@ -266,6 +287,8 @@ impl<'a> Channel<'a> { fn tick(&mut self, duration: usize, volume_scale: f32) -> Vec<[i16; 1]> { if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { // Generate data + // TODO(nenikitov): If `volume_envelope` is `0`, this means that the sample already finished playing + // and there is no reason to keep `note.on`. let volume_envelope = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { let (envelope, default) = if note.on { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 70227c6..d193a6a 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -12,13 +12,6 @@ pub enum Speed { Bpm(usize), } -#[derive(Debug, Clone, Copy)] -pub enum Volume { - Set(f32), - Slide(Option), - Bump { up: bool, volume: Option }, -} - #[derive(Debug, Clone, Copy)] pub enum Porta { Tone(Option), @@ -33,6 +26,13 @@ pub enum Porta { }, } +#[derive(Debug, Clone, Copy)] +pub enum Volume { + Set(f32), + Slide(Option), + Bump { up: bool, volume: Option }, +} + #[derive(Debug, Default, Clone, Copy)] pub enum PlaybackDirection { #[default] @@ -69,43 +69,39 @@ impl PatternEffect { pub fn memory_key(&self) -> Option { match self { PatternEffect::Porta(Porta::Tone(_)) => Some(PatternEffectMemoryKey::PortaTone), - PatternEffect::Porta(Porta::Slide { - up: true, - finetune: _, - }) => Some(PatternEffectMemoryKey::PortaSlideUp), - PatternEffect::Porta(Porta::Slide { - up: false, - finetune: _, - }) => Some(PatternEffectMemoryKey::PortaSlideDown), + PatternEffect::Porta(Porta::Slide { up: true, .. }) => { + Some(PatternEffectMemoryKey::PortaSlideUp) + } + PatternEffect::Porta(Porta::Slide { up: false, .. }) => { + Some(PatternEffectMemoryKey::PortaSlideDown) + } PatternEffect::Porta(Porta::Bump { up: true, small: false, - finetune: _, + .. }) => Some(PatternEffectMemoryKey::PortaBumpUp), PatternEffect::Porta(Porta::Bump { up: false, small: false, - finetune: _, + .. }) => Some(PatternEffectMemoryKey::PortaBumpDown), PatternEffect::Porta(Porta::Bump { up: true, small: true, - finetune: _, + .. }) => Some(PatternEffectMemoryKey::PortaBumpSmallUp), PatternEffect::Porta(Porta::Bump { up: false, small: true, - finetune: _, + .. }) => Some(PatternEffectMemoryKey::PortaBumpSmallDown), PatternEffect::Volume(Volume::Slide(_)) => Some(PatternEffectMemoryKey::VolumeSlide), - PatternEffect::Volume(Volume::Bump { - up: true, - volume: _, - }) => Some(PatternEffectMemoryKey::VolumeBumpUp), - PatternEffect::Volume(Volume::Bump { - up: down, - volume: _, - }) => Some(PatternEffectMemoryKey::VolumeBumpDown), + PatternEffect::Volume(Volume::Bump { up: true, .. }) => { + Some(PatternEffectMemoryKey::VolumeBumpUp) + } + PatternEffect::Volume(Volume::Bump { up: down, .. }) => { + Some(PatternEffectMemoryKey::VolumeBumpDown) + } PatternEffect::SampleOffset(_) => Some(PatternEffectMemoryKey::SampleOffset), _ => None, } @@ -118,7 +114,12 @@ impl PatternEffect { pub fn is_empty(&self) -> bool { matches!( self, - PatternEffect::Volume(Volume::Slide(None)) | PatternEffect::SampleOffset(None) + PatternEffect::Porta(Porta::Tone(None)) + | PatternEffect::Porta(Porta::Slide { finetune: None, .. }) + | PatternEffect::Porta(Porta::Bump { finetune: None, .. }) + | PatternEffect::Volume(Volume::Slide(None)) + | PatternEffect::Volume(Volume::Bump { volume: None, .. }) + | PatternEffect::SampleOffset(None) ) } } @@ -155,17 +156,17 @@ impl AssetParser for Option { 0x16 => PatternEffect::Porta(Porta::Bump { up: false, small: false, - finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), + finetune: (value != 0).then_some(-FineTune::new(8 * value as i32)), }), 0x24 => PatternEffect::Porta(Porta::Bump { up: true, small: true, - finetune: (value != 0).then_some(FineTune::new(2 * value as i32)), + finetune: (value != 0).then_some(FineTune::new(2 * (value & 0xF) as i32)), }), 0x25 => PatternEffect::Porta(Porta::Bump { up: false, small: true, - finetune: (value != 0).then_some(FineTune::new(2 * value as i32)), + finetune: (value != 0).then_some(-FineTune::new(2 * (value & 0xF) as i32)), }), 0x09 => { PatternEffect::SampleOffset((value != 0).then_some(value as usize * 256)) diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 3ef77f2..066c85b 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -105,7 +105,7 @@ impl AssetParser for Option { } } -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub enum PatternEventVolume { Sample, Value(f32), diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 7e372bb..c6dc6c9 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -89,14 +89,13 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0x9]; - let effects = test_music - .orders - .iter() - .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) - .flatten() - .collect::>(); - //dbg!(effects); + .collect::>()[0x1]; + // let effects = test_music + // .orders + // .iter() + // .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) + // .flatten() + // .collect::>(); sounds .iter() diff --git a/engine/src/utils/iterator.rs b/engine/src/utils/iterator.rs index e25531b..dd032ab 100644 --- a/engine/src/utils/iterator.rs +++ b/engine/src/utils/iterator.rs @@ -1,23 +1,23 @@ use std::mem::{self, MaybeUninit}; +use itertools::Itertools; + // Code from [this post](https://www.reddit.com/r/learnrust/comments/lfw6uy/comment/gn16m4o) pub trait CollectArray: Sized + Iterator { fn collect_array(self) -> [Self::Item; N] { + // TODO(nenikitov): Replace with compile-time assertions or const generic expressions + // When it will be supported. assert!(N > 0 && mem::size_of::() > 0); - let mut array = MaybeUninit::uninit(); - let array_ptr = array.as_mut_ptr() as *mut Self::Item; - let mut i = 0; - unsafe { - for item in self { - assert!(i < N); - array_ptr.add(i).write(item); - i += 1; - } - assert!(i == N); - array.assume_init() - } + let mut array = MaybeUninit::<[Self::Item; N]>::uninit().transpose(); + + Itertools::zip_eq(array.iter_mut(), self).for_each(|(dest, item)| _ = dest.write(item)); + + // SAFETY: Every single element in the array is initialized + // because we wrote a valid iterator element into it. + unsafe { array.transpose().assume_init() } } } impl CollectArray for T where T: Iterator {} + From 5bf350b9a01cbae88227e5a446a9f52cc8af73de Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 6 May 2024 22:05:21 -0400 Subject: [PATCH 47/87] feat(effect): global volume --- engine/src/asset/sound/dat/mixer.rs | 11 +++++++---- engine/src/asset/sound/dat/mod.rs | 1 + engine/src/asset/sound/dat/pattern_effect.rs | 9 ++++----- engine/src/asset/sound/mod.rs | 4 +++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 70541e3..1ebd93f 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -25,7 +25,6 @@ impl TSongMixer for TSong { trait TSongMixerUtils { const SAMPLE_RATE: usize = 16_000; - const VOLUME_SCALE: f32 = 1.0; fn mix(&self, start: usize) -> Sample; @@ -44,6 +43,7 @@ impl TSongMixerUtils for TSong { let mut sample_length_fractional = 0.0; let mut bpm = self.bpm as usize; let mut speed = self.speed as usize; + let mut volume_global = 1.0; // TODO!(nenikitov): Remove all `enumerate` for (p, pattern) in self.orders[start..].iter().enumerate() { @@ -123,6 +123,9 @@ impl TSongMixerUtils for TSong { + sample.sample_loop().len(); } } + PatternEffect::GlobalVolume(volume) => { + volume_global = volume; + } PatternEffect::Porta(Porta::Tone(None)) | PatternEffect::Porta(Porta::Slide { finetune: None, .. }) | PatternEffect::Porta(Porta::Bump { finetune: None, .. }) @@ -186,7 +189,7 @@ impl TSongMixerUtils for TSong { sample_length - j * tick_length }; - let data = c.tick(tick_length, Self::VOLUME_SCALE); + let data = c.tick(tick_length, volume_global); song.data.add_sample(&data, offset); } } @@ -284,7 +287,7 @@ impl<'a> Channel<'a> { } } - fn tick(&mut self, duration: usize, volume_scale: f32) -> Vec<[i16; 1]> { + fn tick(&mut self, duration: usize, volume_global: f32) -> Vec<[i16; 1]> { if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { // Generate data // TODO(nenikitov): If `volume_envelope` is `0`, this means that the sample already finished playing @@ -303,7 +306,7 @@ impl<'a> Channel<'a> { } TInstrumentVolume::Constant(volume) => *volume, }; - let volume = volume_scale * volume_envelope * self.volume.clamp(0.0, 4.0); + let volume = volume_global * volume_envelope * self.volume.clamp(0.0, 4.0); let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index f086031..cb02826 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -52,5 +52,6 @@ fn uncompress(bytes: &[u8]) -> Vec { } fn convert_volume(volume: u8) -> f32 { + // TODO(nenikitov): Check if the volume should be limited to `1` max volume as f32 / 64.0 } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index d193a6a..113cb4f 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -63,6 +63,7 @@ pub enum PatternEffect { Porta(Porta), SampleOffset(Option), PlaybackDirection(PlaybackDirection), + GlobalVolume(f32), } impl PatternEffect { @@ -184,12 +185,11 @@ impl AssetParser for Option { -convert_volume(value) }, ))), + 0x0F => PatternEffect::GlobalVolume(convert_volume(value)), // TODO(nenikitov): Remove dummy effect 0x00 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D - | 0x0F | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x21 | 0x22 - | 0x2E | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => { - PatternEffect::Dummy(kind) - } + | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x21 | 0x22 | 0x2E + | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => PatternEffect::Dummy(kind), // TODO(nenikitov): Add support for other effects // 0x00 => Self::Arpegio, // 0x01 => Self::PortaUp, @@ -204,7 +204,6 @@ impl AssetParser for Option { // 0x0B => Self::PositionJump, // 0x0C => Self::Volume, // 0x0D => Self::Break, - // 0x0F => Self::VolumeGlobal, // 0x14 => Self::Sync, // 0x15 => Self::PortaFineUp, // 0x16 => Self::PortaFineDown, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index c6dc6c9..d31a98d 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -89,13 +89,15 @@ mod tests { Sound::Song(s) => Some(s), Sound::Effect(_) => None, }) - .collect::>()[0x1]; + .collect::>()[0xA]; // let effects = test_music // .orders // .iter() // .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) // .flatten() // .collect::>(); + dbg!(test_music.orders[0][0x2][9].note); + dbg!(test_music.orders[0][0x8][9].note); sounds .iter() From afda0a27f59130ce62d32c578b167f0f9f079f11 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 11 May 2024 20:33:50 -0400 Subject: [PATCH 48/87] feat(debug): added printing to tsong --- engine/src/asset/sound/dat/mixer.rs | 4 +- engine/src/asset/sound/dat/pattern_event.rs | 9 +++ engine/src/asset/sound/dat/t_song.rs | 74 +++++++++++++++++++++ engine/src/asset/sound/mod.rs | 72 ++++++++++---------- engine/src/lib.rs | 1 + engine/src/utils/iterator.rs | 1 - engine/src/utils/mod.rs | 2 +- 7 files changed, 126 insertions(+), 37 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1ebd93f..899c164 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -190,7 +190,9 @@ impl TSongMixerUtils for TSong { }; let data = c.tick(tick_length, volume_global); - song.data.add_sample(&data, offset); + if i == 0 { + song.data.add_sample(&data, offset); + } } } diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 066c85b..5b582f1 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -148,6 +148,15 @@ pub struct PatternEvent { pub effects: [Option; 2], } +impl PatternEvent { + pub fn has_content(&self) -> bool { + self.note.is_some() + || self.volume.is_some() + || self.instrument.is_some() + || self.effects.iter().any(Option::is_some) + } +} + impl AssetParser for PatternEvent { type Output = Self; diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index c536299..2cd05c8 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -22,6 +22,80 @@ pub struct TSong { pub samples: Vec>, } +impl std::fmt::Debug for TSong { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TSong") + .field("bpm", &self.bpm) + .field("speed", &self.speed) + .field("restart_order", &self.restart_order) + .field_with("events", |f| { + let mut d = f.debug_map(); + for (p, pattern) in self.orders.iter().enumerate() { + d.key(&format!("P 0x{p:X}")); + d.value_with(|f| { + let mut d = f.debug_map(); + for (r, row) in pattern.iter().enumerate() { + if !row.iter().any(|c| c.has_content()) { + continue; + } + + d.key(&format!("R 0x{r:X}")); + d.value_with(|f| { + let mut d = f.debug_map(); + for (e, event) in row.iter().enumerate() { + if !event.has_content() { + continue; + } + + d.key(&format!("C 0x{e:X}")); + d.value_with(|f| { + let mut d = f.debug_struct("Event"); + event.note.map(|note| { + d.field_with("note", |f| { + f.write_fmt(format_args!("{:?}", note)) + }); + }); + event.volume.map(|volume| { + d.field_with("volume", |f| { + f.write_fmt(format_args!("{:?}", volume)) + }); + }); + event.instrument.as_ref().map(|instrument| { + d.field_with("instrument", |f| match instrument { + PatternEventInstrumentKind::Special => { + f.write_fmt(format_args!("Special")) + } + PatternEventInstrumentKind::Predefined( + instrument, + ) => f.write_fmt(format_args!( + "Predefined({})", + self.instruments + .iter() + .position(|i| Rc::ptr_eq(i, instrument)) + .unwrap() + )), + }); + }); + if event.effects.iter().any(Option::is_some) { + d.field_with("effects", |f| { + f.write_fmt(format_args!("{:?}", event.effects)) + }); + } + d.finish() + }); + } + d.finish() + }); + } + d.finish() + }); + } + d.finish() + }) + .finish() + } +} + impl AssetParser for TSong { type Output = Self; diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index d31a98d..a31f6b7 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -83,41 +83,45 @@ mod tests { let output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); // TODO(nenikitov): Remove this debug code - let test_music = sounds - .iter() - .filter_map(|s| match s { - Sound::Song(s) => Some(s), - Sound::Effect(_) => None, - }) - .collect::>()[0xA]; - // let effects = test_music - // .orders + { + let i = 0xA; + let song = sounds + .iter() + .filter_map(|s| match s { + Sound::Song(s) => Some(s), + Sound::Effect(_) => None, + }) + .collect::>()[i]; + // let effects = test_music + // .orders + // .iter() + // .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) + // .flatten() + // .collect::>(); + dbg!(song); + let file = output_dir.join(format!("{i:0>2X}.wav")); + output_file(file, song.mix(false).to_wave()); + } + + // sounds + // .iter() + // .filter(|s| matches!(s, Sound::Song(_))) + // .enumerate() + // .try_for_each(|(i, song)| { + // let file = output_dir.join(format!("{i:0>2X}.wav")); + // output_file(file, song.mix().to_wave()) + // })?; + // + // let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); + // + // sounds // .iter() - // .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) - // .flatten() - // .collect::>(); - dbg!(test_music.orders[0][0x2][9].note); - dbg!(test_music.orders[0][0x8][9].note); - - sounds - .iter() - .filter(|s| matches!(s, Sound::Song(_))) - .enumerate() - .try_for_each(|(i, song)| { - let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file(file, song.mix().to_wave()) - })?; - - let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); - - sounds - .iter() - .filter(|s| matches!(s, Sound::Effect(_))) - .enumerate() - .try_for_each(|(i, effect)| { - let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file(file, effect.mix().to_wave()) - })?; + // .filter(|s| matches!(s, Sound::Effect(_))) + // .enumerate() + // .try_for_each(|(i, effect)| { + // let file = output_dir.join(format!("{i:0>2X}.wav")); + // output_file(file, effect.mix().to_wave()) + // })?; Ok(()) } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 400f2b6..aeadcd9 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -19,6 +19,7 @@ lazy_cell, let_chains, maybe_uninit_uninit_array_transpose, + debug_closure_helpers, )] pub mod asset; diff --git a/engine/src/utils/iterator.rs b/engine/src/utils/iterator.rs index dd032ab..2f620cf 100644 --- a/engine/src/utils/iterator.rs +++ b/engine/src/utils/iterator.rs @@ -20,4 +20,3 @@ pub trait CollectArray: Sized + Iterator { } impl CollectArray for T where T: Iterator {} - diff --git a/engine/src/utils/mod.rs b/engine/src/utils/mod.rs index 8474c3c..3a507c4 100644 --- a/engine/src/utils/mod.rs +++ b/engine/src/utils/mod.rs @@ -1,7 +1,7 @@ pub mod compression; #[cfg(test)] pub mod format; +pub mod iterator; pub mod nom; #[cfg(test)] pub mod test; -pub mod iterator; From 84e400d0eb145b2d7a529505b4686b1cdbd5c3c0 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 14 May 2024 15:56:50 -0400 Subject: [PATCH 49/87] fix(sound): fixed default instrument volume) --- engine/src/asset/sound/dat/mixer.rs | 8 ++-- engine/src/asset/sound/dat/t_instrument.rs | 8 +--- engine/src/asset/sound/mod.rs | 47 +++++++++------------- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 899c164..adf1058 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -190,9 +190,7 @@ impl TSongMixerUtils for TSong { }; let data = c.tick(tick_length, volume_global); - if i == 0 { - song.data.add_sample(&data, offset); - } + song.data.add_sample(&data, offset); } } @@ -294,7 +292,7 @@ impl<'a> Channel<'a> { // Generate data // TODO(nenikitov): If `volume_envelope` is `0`, this means that the sample already finished playing // and there is no reason to keep `note.on`. - let volume_envelope = match &instrument.volume { + let volume_instrument = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { let (envelope, default) = if note.on { (envelope.volume_beginning(), envelope.volume_loop()) @@ -308,7 +306,7 @@ impl<'a> Channel<'a> { } TInstrumentVolume::Constant(volume) => *volume, }; - let volume = volume_global * volume_envelope * self.volume.clamp(0.0, 4.0); + let volume = (volume_global * volume_instrument * self.volume).clamp(0.0, 4.0); let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 6a2f825..3c22eb8 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -107,14 +107,10 @@ impl AssetParser for TInstrumentVolume { .collect::>(); TInstrumentVolume::Envelope(TInstrumentVolumeEnvelope { data, - sustain: if sustain == u16::MAX { - None - } else { - Some((sustain - begin) as usize) - }, + sustain: (sustain != u16::MAX).then_some((sustain - begin) as usize), }) } else { - TInstrumentVolume::Constant(0.25) + TInstrumentVolume::Constant(1.0) }, )) } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index a31f6b7..8445d81 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -92,36 +92,27 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; - // let effects = test_music - // .orders - // .iter() - // .flat_map(|p| p.iter().flat_map(|p| p.iter().flat_map(|p| p.effects))) - // .flatten() - // .collect::>(); - dbg!(song); - let file = output_dir.join(format!("{i:0>2X}.wav")); - output_file(file, song.mix(false).to_wave()); } - // sounds - // .iter() - // .filter(|s| matches!(s, Sound::Song(_))) - // .enumerate() - // .try_for_each(|(i, song)| { - // let file = output_dir.join(format!("{i:0>2X}.wav")); - // output_file(file, song.mix().to_wave()) - // })?; - // - // let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); - // - // sounds - // .iter() - // .filter(|s| matches!(s, Sound::Effect(_))) - // .enumerate() - // .try_for_each(|(i, effect)| { - // let file = output_dir.join(format!("{i:0>2X}.wav")); - // output_file(file, effect.mix().to_wave()) - // })?; + sounds + .iter() + .filter(|s| matches!(s, Sound::Song(_))) + .enumerate() + .try_for_each(|(i, song)| { + let file = output_dir.join(format!("{i:0>2X}.wav")); + output_file(file, song.mix().to_wave()) + })?; + + let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); + + sounds + .iter() + .filter(|s| matches!(s, Sound::Effect(_))) + .enumerate() + .try_for_each(|(i, effect)| { + let file = output_dir.join(format!("{i:0>2X}.wav")); + output_file(file, effect.mix().to_wave()) + })?; Ok(()) } From 8826d18131e7a8e4dc62d28c51066065c6db12a1 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 14 May 2024 18:06:56 -0400 Subject: [PATCH 50/87] feat(sound): add global volume --- engine/src/asset/sound/dat/mixer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index adf1058..9677ce6 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -6,6 +6,8 @@ use crate::asset::sound::{ sample::{Interpolation, Sample, SampleDataProcessing, SamplePointProcessing}, }; +const GLOBAL_VOLUME: f32 = 0.3125; + pub trait TSongMixer { fn mix(&self, restart: bool) -> Sample; } @@ -306,7 +308,8 @@ impl<'a> Channel<'a> { } TInstrumentVolume::Constant(volume) => *volume, }; - let volume = (volume_global * volume_instrument * self.volume).clamp(0.0, 4.0); + let volume = + (GLOBAL_VOLUME * volume_global * volume_instrument * self.volume).clamp(0.0, 4.0); let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); let duration_scaled = (duration as f64 / pitch_factor).round() as usize; From bb58d7c10cb4fc5fbf78e9398069dccb2af2695f Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 22 Sep 2024 14:41:53 -0400 Subject: [PATCH 51/87] feat: remove predefined instruments, they are null --- engine/src/asset/sound/dat/mixer.rs | 29 ++++++++++++--------- engine/src/asset/sound/dat/pattern_event.rs | 25 ++++-------------- engine/src/asset/sound/dat/t_instrument.rs | 18 ++----------- engine/src/asset/sound/dat/t_song.rs | 10 +++---- engine/src/asset/sound/mod.rs | 3 ++- engine/src/asset/sound/sample.rs | 11 ++++++++ 6 files changed, 40 insertions(+), 56 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 9677ce6..00c7364 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -219,7 +219,7 @@ struct ChannelNote { #[derive(Default)] struct Channel<'a> { - instrument: Option<&'a PatternEventInstrumentKind>, + instrument: Option<&'a Option>>, note: Option, effects: [Option; 2], effects_memory: HashMap, @@ -227,6 +227,7 @@ struct Channel<'a> { sample_position: usize, volume: f32, + volume_last: f32, volume_envelope_position: usize, playback_direction: PlaybackDirection, @@ -256,7 +257,7 @@ impl<'a> Channel<'a> { } } - fn change_instrument(&mut self, instrument: &'a PatternEventInstrumentKind) { + fn change_instrument(&mut self, instrument: &'a Option>) { self.instrument = Some(instrument); self.sample_position = 0; self.volume_envelope_position = 0; @@ -279,9 +280,8 @@ impl<'a> Channel<'a> { fn get_note_instrument_sample(&self) -> Option<(&ChannelNote, &Rc, &Rc)> { if let Some(note) = &self.note - && let Some(PatternEventInstrumentKind::Predefined(instrument)) = self.instrument - && let TInstrumentSampleKind::Predefined(sample) = - &instrument.samples[note.finetune.note().clamp(0, 95) as usize] + && let Some(Some(instrument)) = self.instrument + && let Some(sample) = &instrument.samples[note.finetune.note().clamp(0, 95) as usize] { Some((note, instrument, sample)) } else { @@ -294,19 +294,20 @@ impl<'a> Channel<'a> { // Generate data // TODO(nenikitov): If `volume_envelope` is `0`, this means that the sample already finished playing // and there is no reason to keep `note.on`. - let volume_instrument = match &instrument.volume { + let (volume_instrument, should_increment_volume_envelope) = match &instrument.volume { TInstrumentVolume::Envelope(envelope) => { let (envelope, default) = if note.on { (envelope.volume_beginning(), envelope.volume_loop()) } else { (envelope.volume_end(), 0.0) }; - envelope + let envelope = envelope .get(self.volume_envelope_position) - .map(ToOwned::to_owned) - .unwrap_or(default) + .map(ToOwned::to_owned); + + (envelope.unwrap_or(default), envelope.is_some()) } - TInstrumentVolume::Constant(volume) => *volume, + TInstrumentVolume::Constant(volume) => (*volume, false), }; let volume = (GLOBAL_VOLUME * volume_global * volume_instrument * self.volume).clamp(0.0, 4.0); @@ -335,7 +336,8 @@ impl<'a> Channel<'a> { let first_sample_after = sample.pop(); let pitch_factor = duration as f32 / sample.len() as f32; - let mut data = sample.volume(volume).stretch( + let mut data = sample.volume_range(self.volume_last..volume).stretch( + //let mut data = sample.volume(volume).stretch( pitch_factor, Interpolation::Linear { first_sample_after: first_sample_after.map(|s| s.volume(volume)), @@ -344,7 +346,10 @@ impl<'a> Channel<'a> { // Update self.sample_position += duration_scaled; - self.volume_envelope_position += 1; + if should_increment_volume_envelope { + self.volume_envelope_position += 1; + } + self.volume_last = volume; // Return data diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 5b582f1..8170eec 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -71,14 +71,7 @@ impl AssetParser for PatternEventFlags { } } -#[derive(Debug)] -pub enum PatternEventInstrumentKind { - // TODO(nenikitov): Figure out what instrument `255` is - Special, - Predefined(Rc), -} - -impl AssetParser for Option { +impl AssetParser for Option>> { type Output = Self; type Context<'ctx> = (bool, &'ctx [Rc]); @@ -91,15 +84,7 @@ impl AssetParser for Option { Ok(( input, - should_parse.then(|| { - if instrument == 255 { - PatternEventInstrumentKind::Special - } else { - PatternEventInstrumentKind::Predefined( - instruments[instrument as usize].clone(), - ) - } - }), + should_parse.then(|| instruments.get(instrument as usize).map(Rc::clone)), )) } } @@ -140,10 +125,10 @@ impl AssetParser for Option { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct PatternEvent { pub note: Option, - pub instrument: Option, + pub instrument: Option>>, pub volume: Option, pub effects: [Option; 2], } @@ -173,7 +158,7 @@ impl AssetParser for PatternEvent { flags.contains(PatternEventFlags::ChangeNote), )(input)?; - let (input, instrument) = >::parser(( + let (input, instrument) = >>>::parser(( (flags.contains(PatternEventFlags::ChangeInstrument)), instruments, ))(input)?; diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 3c22eb8..01ef787 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -37,14 +37,6 @@ impl AssetParser for TInstrumentFlags { } } -// TODO(nenikitov): Maybe make it an `AssetParser` -#[derive(Debug)] -pub enum TInstrumentSampleKind { - // TODO(nenikitov): Figure out what sample `255` is - Special, - Predefined(Rc), -} - #[derive(Debug)] pub struct TInstrumentVolumeEnvelope { data: Vec, @@ -136,7 +128,7 @@ pub struct TInstrument { pub fadeout: u32, pub vibrato_table: u32, - pub samples: Box<[TInstrumentSampleKind; 96]>, + pub samples: Box<[Option>; 96]>, } impl AssetParser for TInstrument { @@ -189,13 +181,7 @@ impl AssetParser for TInstrument { samples: Box::new( sample_indexes .into_iter() - .map(|i| { - if i == u8::MAX { - TInstrumentSampleKind::Special - } else { - TInstrumentSampleKind::Predefined(samples[i as usize].clone()) - } - }) + .map(|i| samples.get(i as usize).map(Rc::clone)) .collect_array(), ), }, diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 2cd05c8..527fe5a 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -62,13 +62,9 @@ impl std::fmt::Debug for TSong { }); event.instrument.as_ref().map(|instrument| { d.field_with("instrument", |f| match instrument { - PatternEventInstrumentKind::Special => { - f.write_fmt(format_args!("Special")) - } - PatternEventInstrumentKind::Predefined( - instrument, - ) => f.write_fmt(format_args!( - "Predefined({})", + None => f.write_fmt(format_args!("None")), + Some(instrument) => f.write_fmt(format_args!( + "Some({})", self.instruments .iter() .position(|i| Rc::ptr_eq(i, instrument)) diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 8445d81..df723dd 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0xA; + let i = 0x3; let song = sounds .iter() .filter_map(|s| match s { @@ -92,6 +92,7 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; + dbg!(&song.patterns[0][0x28][7]); } sounds diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index ce0c9b3..9fb1f2c 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -197,6 +197,7 @@ impl SamplePointProcessing f pub trait SampleDataProcessing { fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize); fn volume(&self, volume: f32) -> Self; + fn volume_range(&self, volume: Range) -> Self; fn stretch(&self, factor: f32, interpolation: Interpolation) -> Self; } @@ -255,6 +256,16 @@ impl SampleDataProcessing }) .collect() } + + fn volume_range(&self, volume: Range) -> Self { + let len = self.iter().len(); + self.iter() + .enumerate() + .map(|(i, samples)| { + samples.volume(volume.start + (i as f32 / len as f32) * (volume.end - volume.start)) + }) + .collect() + } } impl Sample { From 0f5dea7b10978696bbd900dec6958c5e6f42c927 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 22 Sep 2024 17:58:44 -0400 Subject: [PATCH 52/87] wip: new new mixer --- engine/src/asset/sound/dat/mixer_new.rs | 57 +++++++++++++++++++++++++ engine/src/asset/sound/dat/mod.rs | 1 + engine/src/lib.rs | 1 - 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 engine/src/asset/sound/dat/mixer_new.rs diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs new file mode 100644 index 0000000..cd67789 --- /dev/null +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -0,0 +1,57 @@ +trait AudioSamplePoint { + type Bytes: IntoIterator; + + fn into_normalized_f32(&self) -> f32; + fn from_normalized_f32(value: f32) -> Self; + fn into_bytes(&self) -> Self::Bytes; +} + +impl AudioSamplePoint for i16 { + type Bytes = [u8; 2]; + + fn into_normalized_f32(&self) -> f32 { + if *self < 0 { + -(*self as f32 / Self::MIN as f32) + } else { + (*self as f32 / Self::MAX as f32) + } + } + + fn from_normalized_f32(value: f32) -> Self { + if value < 0.0 { + -(value * i16::MIN as f32) as i16 + } else { + (value * i16::MAX as f32) as i16 + } + } + + fn into_bytes(&self) -> Self::Bytes { + self.to_le_bytes() + } +} + +struct Player { + loop_count: usize, +} + +impl Player { + fn new() -> Self { + Player { loop_count: 0 } + } + + fn generate_sample(&mut self, sample_rate: usize) -> T { + self.loop_count += 1; + todo!() + } +} + +fn main() { + let mut player = Player::new(); + + let samples: Vec<_> = std::iter::from_fn(|| { + (player.loop_count == 0).then(|| player.generate_sample::(16000)) + }) + .collect(); + + println!("Generated {} samples", samples.len()); +} diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index cb02826..1e68454 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -11,6 +11,7 @@ pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; +mod mixer_new; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index aeadcd9..d0e06f8 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -16,7 +16,6 @@ // Discussion about possible future alternatives: // https://github.com/rust-lang/rust/pull/101179 io_error_more, - lazy_cell, let_chains, maybe_uninit_uninit_array_transpose, debug_closure_helpers, From 269d9318a9ad2118453ff16fefc4fa3eb0a5d4ca Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 23 Sep 2024 00:31:19 -0400 Subject: [PATCH 53/87] wip: new mixer is finally playing something :D --- Cargo.toml | 3 + engine/src/asset/sound/dat/mixer_new.rs | 218 ++++++++++++++++++-- engine/src/asset/sound/dat/mod.rs | 2 +- engine/src/asset/sound/dat/pattern_event.rs | 2 +- engine/src/asset/sound/dat/t_instrument.rs | 40 +++- engine/src/asset/sound/mod.rs | 9 +- engine/src/asset/sound/sample.rs | 20 +- 7 files changed, 261 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ca258b0..e9140be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,6 @@ incremental = false lto = "fat" opt-level = 3 panic = "abort" + +[profile.release] +debug = true diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index cd67789..6b354d6 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -1,4 +1,14 @@ -trait AudioSamplePoint { +use std::{iter::Sum, rc::Rc}; + +use super::{ + finetune::FineTune, + pattern_event::{PatternEventNote, PatternEventVolume}, + t_instrument::{TInstrument, TSample}, + t_song::TSong, +}; +use crate::asset::sound::sample::Sample; + +trait AudioSamplePoint: Sum { type Bytes: IntoIterator; fn into_normalized_f32(&self) -> f32; @@ -18,7 +28,7 @@ impl AudioSamplePoint for i16 { } fn from_normalized_f32(value: f32) -> Self { - if value < 0.0 { + if value < 0. { -(value * i16::MIN as f32) as i16 } else { (value * i16::MAX as f32) as i16 @@ -30,28 +40,202 @@ impl AudioSamplePoint for i16 { } } -struct Player { - loop_count: usize, +struct PlayerChannelNote { + finetune: FineTune, + finetune_initial: FineTune, + on: bool, +} + +#[derive(Default)] +struct PlayerChannel { + instrument: Option>, + sample: Option>, + note: Option, + volume: Option, + pos_sample: f64, + pos_volume_envelope: usize, } -impl Player { - fn new() -> Self { - Player { loop_count: 0 } +impl PlayerChannel { + fn note_cut(&mut self) { + self.volume = Some(PatternEventVolume::Value(0.)); } - fn generate_sample(&mut self, sample_rate: usize) -> T { - self.loop_count += 1; - todo!() + fn note_trigger(&mut self) { + self.pos_sample = 0f64; + self.pos_volume_envelope = 0; } + + fn generate_sample(&mut self, sample_length: f64) -> T { + if let Some(instrument) = &self.instrument + && let Some(sample) = &self.sample + && let Some(volume) = &self.volume + && let Some(note) = &self.note + { + let value = sample.get(self.pos_sample).into_normalized_f32(); + let volume = match volume { + PatternEventVolume::Sample => sample.volume, + PatternEventVolume::Value(volume) => *volume, + }; + + let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); + self.pos_sample += sample_length / pitch_factor; + + T::from_normalized_f32(value * volume) + } else { + T::from_normalized_f32(0f32) + } + } +} + +struct Player<'a> { + song: &'a TSong, + + time_in_tick: f64, + + pos_loop: usize, + pos_pattern: usize, + pos_row: usize, + pos_tick: usize, + + tempo: usize, + bpm: usize, + volume_global: f32, + + channels: Vec, } -fn main() { - let mut player = Player::new(); +impl<'a> Player<'a> { + fn new(song: &'a TSong) -> Self { + Self { + song, + time_in_tick: 0., + pos_loop: 0, + pos_pattern: 0, + pos_row: 0, + pos_tick: 0, + tempo: song.speed as usize, + bpm: song.bpm as usize, + volume_global: 0.25, + channels: (0..song.patterns[0][0].len()) + .map(|_| PlayerChannel::default()) + .collect(), + } + } - let samples: Vec<_> = std::iter::from_fn(|| { - (player.loop_count == 0).then(|| player.generate_sample::(16000)) - }) - .collect(); + fn generate_sample(&mut self, sample_rate: usize) -> S { + if self.time_in_tick <= 0f64 { + self.tick(); + } + let sample_length = 1. / sample_rate as f64; + self.time_in_tick -= sample_length; - println!("Generated {} samples", samples.len()); + let sample = self + .channels + .iter_mut() + .map(|c| c.generate_sample::(sample_length)) + .map(|c| c.into_normalized_f32()) + .sum::(); + S::from_normalized_f32(sample * self.volume_global) + } + + fn tick(&mut self) { + if self.pos_tick == 0 { + self.row(); + } + + self.pos_tick += 1; + + if self.pos_tick >= self.tempo { + self.pos_tick = 0; + self.pos_row += 1; + } + if let Some(pattern) = self.song.patterns.get(self.pos_pattern) + && self.pos_row >= pattern.len() + { + self.pos_pattern += 1; + self.pos_row = 0; + }; + if self.pos_pattern >= self.song.patterns.len() { + self.pos_loop += 1; + self.pos_pattern = self.song.restart_order as usize; + } + + self.time_in_tick += 2.5 / (self.bpm as f64); + } + + fn row(&mut self) { + let Some(row) = self + .song + .patterns + .get(self.pos_pattern) + .and_then(|p| p.get(self.pos_row)) + else { + return; + }; + + for (channel, event) in self.channels.iter_mut().zip(row) { + if let Some(instrument) = &event.instrument { + if let Some(instrument) = instrument { + channel.instrument = Some(instrument.clone()); + } else { + // TODO(nenikitov): Idk honestly, figure this out + channel.note_cut(); + channel.instrument = None; + channel.sample = None; + } + } + + if let Some(note) = &event.note { + if let Some(instrument) = &channel.instrument { + match note { + PatternEventNote::Off => { + if let Some(note) = &mut channel.note { + note.on = false; + } + } + PatternEventNote::On(note) => { + channel.note = Some(PlayerChannelNote { + finetune: *note, + finetune_initial: *note, + on: true, + }); + channel.sample = instrument.samples[note.note() as usize].clone(); + channel.note_trigger(); + } + } + } else { + // TODO(nenikitov): Idk honestly, figure this out + channel.note_cut(); + } + } + + if let Some(volume) = &event.volume { + channel.volume = Some(volume.clone()); + } + + // TODO(nenikitov): Do effects + } + } +} + +pub trait TSongMixerNew { + fn mix_new(&self) -> Sample; +} + +impl TSongMixerNew for TSong { + fn mix_new(&self) -> Sample { + let mut player = Player::new(self); + + let samples: Vec<_> = std::iter::from_fn(|| { + (player.pos_loop == 0).then(|| player.generate_sample::(48000)) + }) + .map(|s| [s]) + .collect(); + + Sample { + data: samples, + sample_rate: 48000, + } + } } diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index 1e68454..ca288e5 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -11,7 +11,7 @@ pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; -mod mixer_new; +pub mod mixer_new; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 8170eec..02cdc1a 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -31,7 +31,7 @@ impl AssetParser for Option { 1..=95 => PatternEventNote::On(FineTune::from_note(note as i32)), 96 => PatternEventNote::Off, // TODO(nenikitov): Should be a `Result` - _ => unreachable!("Note should be in range 0-96"), + _ => unreachable!("Note should be in range 1-96"), } }), )) diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 01ef787..519cbef 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -209,10 +209,6 @@ pub struct TSample { pub data: Sample, } -impl TSample { - const SAMPLE_RATE: usize = 16_000; -} - impl AssetParser for TSample { type Output = Self; @@ -258,6 +254,8 @@ impl AssetParser for TSample { } impl TSample { + const SAMPLE_RATE: usize = 16_000; + pub fn sample_beginning(&self) -> &[[i16; 1]] { &self.data[..self.data.len_seconds() - self.loop_length] } @@ -266,7 +264,39 @@ impl TSample { if self.loop_length != 0.0 { &self.data[self.data.len_seconds() - self.loop_length..] } else { - &[] + &[[0; 1]] + } + } + + // TODO(nenikitov): I think the whole `Sample` will need to be removed + pub fn get(&self, position: f64) -> i16 { + let position = Self::SAMPLE_RATE as f64 * position; + + let frac = position.fract() as f32; + let Some(prev) = self.normalize(position as usize) else { + return 0; + }; + let Some(next) = self.normalize(position as usize + 1) else { + return 0; + }; + + let prev = self.data[prev][0] as f32; + let next = self.data[next][0] as f32; + + (prev + frac * (next - prev)) as i16 + } + + fn normalize(&self, position: usize) -> Option { + if position >= self.data.data.len() && self.loop_length == 0. { + None + } else { + let mut position = position; + + while position >= self.data.data.len() { + position -= (self.loop_length * Self::SAMPLE_RATE as f32) as usize; + } + + Some(position) } } } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index df723dd..002ca16 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,7 +1,10 @@ mod dat; pub(crate) mod sample; -use self::{dat::mixer::TSongMixer, sample::Sample}; +use self::{ + dat::{mixer::TSongMixer, mixer_new::TSongMixerNew}, + sample::Sample, +}; use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -19,7 +22,7 @@ pub enum Sound { impl Sound { pub fn mix(&self) -> Sample { match self { - Sound::Song(sound) => sound.mix(false), + Sound::Song(sound) => sound.mix_new(), Sound::Effect(effect) => effect.mix(), } } @@ -92,7 +95,6 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; - dbg!(&song.patterns[0][0x28][7]); } sounds @@ -101,6 +103,7 @@ mod tests { .enumerate() .try_for_each(|(i, song)| { let file = output_dir.join(format!("{i:0>2X}.wav")); + println!("# SONG {i}"); output_file(file, song.mix().to_wave()) })?; diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 9fb1f2c..09e50ce 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -121,7 +121,7 @@ impl Index for Sample { type Output = [S; CHANNELS]; fn index(&self, index: f32) -> &Self::Output { - self.data.index(self.time_to_index(index)) + self.data.index(self.time_to_index(index as f64)) } } @@ -129,7 +129,7 @@ impl Index> for Sample) -> &Self::Output { - &self.data[self.time_to_index(range.start)..self.time_to_index(range.end)] + &self.data[self.time_to_index(range.start as f64)..self.time_to_index(range.end as f64)] } } @@ -137,7 +137,7 @@ impl Index> for Sample) -> &Self::Output { - &self.data[self.time_to_index(range.start)..] + &self.data[self.time_to_index(range.start as f64)..] } } @@ -145,13 +145,21 @@ impl Index> for Sample) -> &Self::Output { - &self.data[..self.time_to_index(range.end)] + &self.data[..self.time_to_index(range.end as f64)] + } +} + +impl Index for Sample { + type Output = [S; CHANNELS]; + + fn index(&self, index: f64) -> &Self::Output { + self.data.index(self.time_to_index(index)) } } impl Sample { - fn time_to_index(&self, time: f32) -> usize { - (time * self.sample_rate as f32) as usize + fn time_to_index(&self, time: f64) -> usize { + (time * self.sample_rate as f64) as usize } pub fn len_samples(&self) -> usize { From bac205448d88b2949868c14df3e93aac36da7667 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 23 Sep 2024 01:08:10 -0400 Subject: [PATCH 54/87] wip --- engine/src/asset/sound/dat/t_instrument.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 519cbef..1498dde 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -205,7 +205,7 @@ pub struct TSample { pub panning: u8, pub align: u8, pub finetune: FineTune, - pub loop_length: f32, + pub loop_length: usize, pub data: Sample, } @@ -228,7 +228,7 @@ impl AssetParser for TSample { // The game uses offset for `i16`, but it's much more convenient to just use time, so that's why `/ 2` (`i16` is 2 bytes) let loop_end = loop_end / 2; let sample_offset = sample_offset / 2; - let loop_length = loop_length as f32 / 2.0 / Self::SAMPLE_RATE as f32; + let loop_length = loop_length / 2; Ok(( input, @@ -238,8 +238,7 @@ impl AssetParser for TSample { panning, align, finetune: FineTune::new(finetune), - // TODO(nenikitov): Look into resampling the sample to 48 KHz - loop_length, + loop_length: loop_length as usize, data: Sample { data: sample_data[sample_offset as usize..loop_end as usize] .into_iter() @@ -257,12 +256,13 @@ impl TSample { const SAMPLE_RATE: usize = 16_000; pub fn sample_beginning(&self) -> &[[i16; 1]] { - &self.data[..self.data.len_seconds() - self.loop_length] + &self.data[..self.data.len_seconds() - (self.loop_length as f32 / Self::SAMPLE_RATE as f32)] } pub fn sample_loop(&self) -> &[[i16; 1]] { - if self.loop_length != 0.0 { - &self.data[self.data.len_seconds() - self.loop_length..] + if self.loop_length != 0 { + &self.data + [self.data.len_seconds() - (self.loop_length as f32 / Self::SAMPLE_RATE as f32)..] } else { &[[0; 1]] } @@ -287,13 +287,12 @@ impl TSample { } fn normalize(&self, position: usize) -> Option { - if position >= self.data.data.len() && self.loop_length == 0. { + if position >= self.data.data.len() && self.loop_length == 0 { None } else { let mut position = position; - while position >= self.data.data.len() { - position -= (self.loop_length * Self::SAMPLE_RATE as f32) as usize; + position -= self.loop_length; } Some(position) From d8e08c3bdfbd177b024400349157efc7f4132473 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 23 Sep 2024 12:30:34 -0400 Subject: [PATCH 55/87] wip: effects --- engine/src/asset/sound/dat/mixer_new.rs | 144 ++++++++++++++++-------- 1 file changed, 98 insertions(+), 46 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 6b354d6..b82cafa 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -1,7 +1,8 @@ -use std::{iter::Sum, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, iter::Sum, rc::Rc}; use super::{ finetune::FineTune, + pattern_effect::{PatternEffect, PatternEffectMemoryKey}, pattern_event::{PatternEventNote, PatternEventVolume}, t_instrument::{TInstrument, TSample}, t_song::TSong, @@ -40,9 +41,25 @@ impl AudioSamplePoint for i16 { } } +trait PatternEffectLogic { + fn init(&self, player: &mut Player, channel: &mut PlayerChannel); + fn tick(&self, player: &mut Player, channel: &mut PlayerChannel); +} + +impl PatternEffectLogic for PatternEffect { + fn init(&self, player: &mut Player, channel: &mut PlayerChannel) { + todo!() + } + + fn tick(&self, player: &mut Player, channel: &mut PlayerChannel) { + todo!() + } +} + +#[derive(Default)] struct PlayerChannelNote { - finetune: FineTune, - finetune_initial: FineTune, + finetune: Option, + finetune_initial: Option, on: bool, } @@ -50,8 +67,11 @@ struct PlayerChannelNote { struct PlayerChannel { instrument: Option>, sample: Option>, - note: Option, + note: PlayerChannelNote, volume: Option, + effects: [Option; 2], + effects_memory: HashMap, + pos_sample: f64, pos_volume_envelope: usize, } @@ -61,16 +81,16 @@ impl PlayerChannel { self.volume = Some(PatternEventVolume::Value(0.)); } - fn note_trigger(&mut self) { - self.pos_sample = 0f64; + fn pos_reset(&mut self) { + self.pos_sample = 0.; self.pos_volume_envelope = 0; } - fn generate_sample(&mut self, sample_length: f64) -> T { + fn generate_sample(&mut self, step: f64) -> T { if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(volume) = &self.volume - && let Some(note) = &self.note + && let Some(note) = self.note.finetune { let value = sample.get(self.pos_sample).into_normalized_f32(); let volume = match volume { @@ -78,14 +98,62 @@ impl PlayerChannel { PatternEventVolume::Value(volume) => *volume, }; - let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); - self.pos_sample += sample_length / pitch_factor; + let pitch_factor = (note + sample.finetune).pitch_factor(); + self.pos_sample += step / pitch_factor; T::from_normalized_f32(value * volume) } else { T::from_normalized_f32(0f32) } } + + fn change_instrument(&mut self, instrument: Option>) { + if let Some(instrument) = instrument { + self.instrument = Some(instrument); + self.pos_reset(); + } else { + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); + self.instrument = None; + self.sample = None; + } + } + + fn change_note(&mut self, note: PatternEventNote) { + if let Some(instrument) = &self.instrument { + match note { + PatternEventNote::Off => { + self.note.on = false; + } + PatternEventNote::On(note) => { + self.note.finetune = Some(note); + self.note.finetune_initial = Some(note); + self.note.on = true; + self.sample = instrument.samples[note.note() as usize].clone(); + } + } + } else { + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); + } + } + + fn change_volume(&mut self, volume: PatternEventVolume) { + self.volume = Some(volume); + } + + fn change_effect(&mut self, i: usize, effect: PatternEffect) { + // Recall from memory + let effect = if let Some(key) = effect.memory_key() { + if !effect.is_empty() { + self.effects_memory.insert(key, effect); + } + + self.effects_memory[&key] + } else { + effect + }; + } } struct Player<'a> { @@ -116,7 +184,7 @@ impl<'a> Player<'a> { pos_tick: 0, tempo: song.speed as usize, bpm: song.bpm as usize, - volume_global: 0.25, + volume_global: 0.375, channels: (0..song.patterns[0][0].len()) .map(|_| PlayerChannel::default()) .collect(), @@ -127,13 +195,13 @@ impl<'a> Player<'a> { if self.time_in_tick <= 0f64 { self.tick(); } - let sample_length = 1. / sample_rate as f64; - self.time_in_tick -= sample_length; + let step = 1. / sample_rate as f64; + self.time_in_tick -= step; let sample = self .channels .iter_mut() - .map(|c| c.generate_sample::(sample_length)) + .map(|c| c.generate_sample::(step)) .map(|c| c.into_normalized_f32()) .sum::(); S::from_normalized_f32(sample * self.volume_global) @@ -176,42 +244,24 @@ impl<'a> Player<'a> { for (channel, event) in self.channels.iter_mut().zip(row) { if let Some(instrument) = &event.instrument { - if let Some(instrument) = instrument { - channel.instrument = Some(instrument.clone()); - } else { - // TODO(nenikitov): Idk honestly, figure this out - channel.note_cut(); - channel.instrument = None; - channel.sample = None; - } + channel.change_instrument(instrument.clone()); } if let Some(note) = &event.note { - if let Some(instrument) = &channel.instrument { - match note { - PatternEventNote::Off => { - if let Some(note) = &mut channel.note { - note.on = false; - } - } - PatternEventNote::On(note) => { - channel.note = Some(PlayerChannelNote { - finetune: *note, - finetune_initial: *note, - on: true, - }); - channel.sample = instrument.samples[note.note() as usize].clone(); - channel.note_trigger(); - } - } - } else { - // TODO(nenikitov): Idk honestly, figure this out - channel.note_cut(); - } + channel.change_note(note.clone()); } if let Some(volume) = &event.volume { - channel.volume = Some(volume.clone()); + channel.change_volume(volume.clone()); + } + + for (i, effect) in event.effects.iter().enumerate() { + if let Some(effect) = effect { + channel.change_effect(i, effect.clone()); + // channel.effects[i] + // .expect("Effect was initialized") + // .init(self, channel); + } } // TODO(nenikitov): Do effects @@ -225,17 +275,19 @@ pub trait TSongMixerNew { impl TSongMixerNew for TSong { fn mix_new(&self) -> Sample { + const SAMPLE_RATE: usize = 16000; + let mut player = Player::new(self); let samples: Vec<_> = std::iter::from_fn(|| { - (player.pos_loop == 0).then(|| player.generate_sample::(48000)) + (player.pos_loop == 0).then(|| player.generate_sample::(SAMPLE_RATE)) }) .map(|s| [s]) .collect(); Sample { data: samples, - sample_rate: 48000, + sample_rate: SAMPLE_RATE, } } } From 0e7aa347b862ebab730d6eb2a4de284fb23b5230 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 27 Sep 2024 16:29:11 -0400 Subject: [PATCH 56/87] fix: pattern orders --- engine/src/asset/sound/dat/mixer_new.rs | 31 +++++++++++++------------ engine/src/asset/sound/dat/t_song.rs | 2 +- engine/src/asset/sound/mod.rs | 5 +--- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index b82cafa..b56fc75 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashMap, iter::Sum, rc::Rc}; +use std::{collections::HashMap, iter::Sum, rc::Rc}; use super::{ finetune::FineTune, @@ -158,6 +158,7 @@ impl PlayerChannel { struct Player<'a> { song: &'a TSong, + sample_rate: usize, time_in_tick: f64, @@ -174,9 +175,10 @@ struct Player<'a> { } impl<'a> Player<'a> { - fn new(song: &'a TSong) -> Self { + fn new(song: &'a TSong, sample_rate: usize) -> Self { Self { song, + sample_rate, time_in_tick: 0., pos_loop: 0, pos_pattern: 0, @@ -185,17 +187,17 @@ impl<'a> Player<'a> { tempo: song.speed as usize, bpm: song.bpm as usize, volume_global: 0.375, - channels: (0..song.patterns[0][0].len()) + channels: (0..song.orders[0][0].len()) .map(|_| PlayerChannel::default()) .collect(), } } - fn generate_sample(&mut self, sample_rate: usize) -> S { + fn generate_sample(&mut self) -> S { if self.time_in_tick <= 0f64 { self.tick(); } - let step = 1. / sample_rate as f64; + let step = 1. / self.sample_rate as f64; self.time_in_tick -= step; let sample = self @@ -218,13 +220,13 @@ impl<'a> Player<'a> { self.pos_tick = 0; self.pos_row += 1; } - if let Some(pattern) = self.song.patterns.get(self.pos_pattern) + if let Some(pattern) = self.song.orders.get(self.pos_pattern) && self.pos_row >= pattern.len() { self.pos_pattern += 1; self.pos_row = 0; }; - if self.pos_pattern >= self.song.patterns.len() { + if self.pos_pattern >= self.song.orders.len() { self.pos_loop += 1; self.pos_pattern = self.song.restart_order as usize; } @@ -235,7 +237,7 @@ impl<'a> Player<'a> { fn row(&mut self) { let Some(row) = self .song - .patterns + .orders .get(self.pos_pattern) .and_then(|p| p.get(self.pos_row)) else { @@ -277,17 +279,16 @@ impl TSongMixerNew for TSong { fn mix_new(&self) -> Sample { const SAMPLE_RATE: usize = 16000; - let mut player = Player::new(self); + let mut player = Player::new(self, SAMPLE_RATE); - let samples: Vec<_> = std::iter::from_fn(|| { - (player.pos_loop == 0).then(|| player.generate_sample::(SAMPLE_RATE)) - }) - .map(|s| [s]) - .collect(); + let samples: Vec<_> = + std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) + .map(|s| [s]) + .collect(); Sample { data: samples, - sample_rate: SAMPLE_RATE, + sample_rate: player.sample_rate, } } } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 527fe5a..c5f424c 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -15,8 +15,8 @@ pub struct TSong { pub bpm: u8, pub speed: u8, pub restart_order: u8, - pub orders: Vec>, /// Reusable and repeatable sequence -> Row -> Channel + pub orders: Vec>, pub patterns: Vec>, pub instruments: Vec>, pub samples: Vec>, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 002ca16..0e56304 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,10 +1,7 @@ mod dat; pub(crate) mod sample; -use self::{ - dat::{mixer::TSongMixer, mixer_new::TSongMixerNew}, - sample::Sample, -}; +use self::{dat::mixer_new::TSongMixerNew, sample::Sample}; use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ From a53917fd1d92333aed06ebd68dd1f70dc5538476 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 27 Sep 2024 18:51:32 -0400 Subject: [PATCH 57/87] feat: made sample blending work, f*cking finally --- engine/src/asset/sound/dat/mixer_new.rs | 38 ++++++++++++++++++++++--- engine/src/asset/sound/mod.rs | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index b56fc75..064cf8a 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -56,14 +56,14 @@ impl PatternEffectLogic for PatternEffect { } } -#[derive(Default)] +#[derive(Default, Clone)] struct PlayerChannelNote { finetune: Option, finetune_initial: Option, on: bool, } -#[derive(Default)] +#[derive(Default, Clone)] struct PlayerChannel { instrument: Option>, sample: Option>, @@ -72,11 +72,21 @@ struct PlayerChannel { effects: [Option; 2], effects_memory: HashMap, + previous: Option<(Box, f64)>, + pos_sample: f64, pos_volume_envelope: usize, } impl PlayerChannel { + // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. + // Too little and transitions between notes will click. + // This is 800 microseconds, which amounts to + // - 13 samples at 16000 + // - 35 samples at 44100 + // - 38 samples at 48000 + const SAMPLE_BLEND: f64 = 0.0008; + fn note_cut(&mut self) { self.volume = Some(PatternEventVolume::Value(0.)); } @@ -87,7 +97,7 @@ impl PlayerChannel { } fn generate_sample(&mut self, step: f64) -> T { - if let Some(instrument) = &self.instrument + let current_sample = if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(volume) = &self.volume && let Some(note) = self.note.finetune @@ -103,11 +113,31 @@ impl PlayerChannel { T::from_normalized_f32(value * volume) } else { - T::from_normalized_f32(0f32) + T::from_normalized_f32(0.) + }; + + if let Some((previous, position)) = &mut self.previous { + let factor = (*position / Self::SAMPLE_BLEND).min(1.) as f32; + let current_sample = current_sample.into_normalized_f32(); + let previous_sample = previous.generate_sample::(step).into_normalized_f32(); + + *position += step; + if *position >= Self::SAMPLE_BLEND { + self.previous = None + } + + T::from_normalized_f32(previous_sample + factor * (current_sample - previous_sample)) + } else { + current_sample } } fn change_instrument(&mut self, instrument: Option>) { + // In tracker music, every instrument change is a state reset + // Previous state is kept to subtly blend in notes to remove clicks. + self.previous = None; + self.previous = Some((Box::new(self.clone()), 0.)); + if let Some(instrument) = instrument { self.instrument = Some(instrument); self.pos_reset(); diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 0e56304..cc4886a 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0x3; + let i = 0x8; let song = sounds .iter() .filter_map(|s| match s { From 6e2a9c14e2a2731e4ac42d31560b04933c3f1d35 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 28 Sep 2024 01:49:05 -0400 Subject: [PATCH 58/87] wip: small tweaks --- Cargo.lock | 346 +++++++++++++++++++++++- engine/Cargo.toml | 1 + engine/src/asset/sound/dat/mixer_new.rs | 29 +- 3 files changed, 349 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d21b5aa..2976078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,41 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.9.0" @@ -176,15 +211,26 @@ dependencies = [ "fixed", "flate2", "image", - "itertools", + "itertools 0.12.0", "lewton", "loom", "nom", + "partial-borrow", "paste", "sealed", "thiserror", ] +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "exr" version = "1.6.4" @@ -211,6 +257,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "fdeflate" version = "0.3.4" @@ -220,6 +272,26 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fehler" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635" +dependencies = [ + "fehler-macros", +] + +[[package]] +name = "fehler-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fixed" version = "1.24.0" @@ -255,6 +327,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures-core" version = "0.3.30" @@ -313,12 +391,24 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" version = "0.24.8" @@ -343,6 +433,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.0" @@ -399,6 +508,12 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.11" @@ -443,6 +558,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -518,6 +642,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "partial-borrow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4841b3043764a29a834246d1faf87fac8f70fa37a53c503af5785c6a2e98af8a" +dependencies = [ + "memoffset", + "partial-borrow-macros", +] + +[[package]] +name = "partial-borrow-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c561c7c8fb375e70b41f324c284f2489b69b5a6f669ae9d1ce4267e7da59d581" +dependencies = [ + "darling", + "either", + "fehler", + "indexmap", + "itertools 0.10.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "tempfile", +] + [[package]] name = "paste" version = "1.0.14" @@ -541,7 +693,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -563,6 +715,30 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.76" @@ -610,6 +786,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -654,6 +839,19 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -681,7 +879,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -714,6 +912,23 @@ dependencies = [ "lock_api", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -725,6 +940,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.56" @@ -742,7 +970,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -800,7 +1028,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -866,6 +1094,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -893,7 +1127,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -915,7 +1149,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -960,7 +1194,25 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -969,13 +1221,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -984,42 +1252,90 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/engine/Cargo.toml b/engine/Cargo.toml index f4fd223..0eb4729 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -12,6 +12,7 @@ itertools = "0.12.0" lewton = "0.10.2" loom = { version = "0.7.1", optional = true } nom = "7.1.3" +partial-borrow = "1.0.1" paste = "1.0.14" sealed = "0.5.0" thiserror = "1.0.56" diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 064cf8a..3720f79 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -1,4 +1,6 @@ -use std::{collections::HashMap, iter::Sum, rc::Rc}; +use std::{collections::HashMap, rc::Rc}; + +use itertools::Itertools; use super::{ finetune::FineTune, @@ -9,7 +11,7 @@ use super::{ }; use crate::asset::sound::sample::Sample; -trait AudioSamplePoint: Sum { +trait AudioSamplePoint { type Bytes: IntoIterator; fn into_normalized_f32(&self) -> f32; @@ -81,7 +83,8 @@ struct PlayerChannel { impl PlayerChannel { // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. // Too little and transitions between notes will click. - // This is 800 microseconds, which amounts to + // This is 800 microseconds, which is a bit of an arbitrary value that i found sounds nice. + // It amounts to: // - 13 samples at 16000 // - 35 samples at 44100 // - 38 samples at 48000 @@ -117,7 +120,7 @@ impl PlayerChannel { }; if let Some((previous, position)) = &mut self.previous { - let factor = (*position / Self::SAMPLE_BLEND).min(1.) as f32; + let factor = (*position / Self::SAMPLE_BLEND) as f32; let current_sample = current_sample.into_normalized_f32(); let previous_sample = previous.generate_sample::(step).into_normalized_f32(); @@ -135,6 +138,8 @@ impl PlayerChannel { fn change_instrument(&mut self, instrument: Option>) { // In tracker music, every instrument change is a state reset // Previous state is kept to subtly blend in notes to remove clicks. + + // Disregard previous state before `self.clone` so we don't have a fully recursive structure. self.previous = None; self.previous = Some((Box::new(self.clone()), 0.)); @@ -224,7 +229,7 @@ impl<'a> Player<'a> { } fn generate_sample(&mut self) -> S { - if self.time_in_tick <= 0f64 { + if self.time_in_tick <= 0. { self.tick(); } let step = 1. / self.sample_rate as f64; @@ -253,12 +258,12 @@ impl<'a> Player<'a> { if let Some(pattern) = self.song.orders.get(self.pos_pattern) && self.pos_row >= pattern.len() { - self.pos_pattern += 1; self.pos_row = 0; + self.pos_pattern += 1; }; if self.pos_pattern >= self.song.orders.len() { - self.pos_loop += 1; self.pos_pattern = self.song.restart_order as usize; + self.pos_loop += 1; } self.time_in_tick += 2.5 / (self.bpm as f64); @@ -274,7 +279,7 @@ impl<'a> Player<'a> { return; }; - for (channel, event) in self.channels.iter_mut().zip(row) { + for (channel, event) in self.channels.iter_mut().zip_eq(row) { if let Some(instrument) = &event.instrument { channel.change_instrument(instrument.clone()); } @@ -289,10 +294,10 @@ impl<'a> Player<'a> { for (i, effect) in event.effects.iter().enumerate() { if let Some(effect) = effect { - channel.change_effect(i, effect.clone()); - // channel.effects[i] - // .expect("Effect was initialized") - // .init(self, channel); + //channel.change_effect(i, effect.clone()); + //channel.effects[i] + // .expect("Effect was initialized") + // .init(self, channel); } } From 7f3ae3a9117e6538c0bbb22b7fe8fa69b1e176e4 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 2 Oct 2024 19:34:19 -0400 Subject: [PATCH 59/87] feat: more effects --- engine/src/asset/sound/dat/mixer_new.rs | 131 ++++++++++++++++--- engine/src/asset/sound/dat/pattern_effect.rs | 6 +- 2 files changed, 116 insertions(+), 21 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 3720f79..3fd90c3 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -3,11 +3,7 @@ use std::{collections::HashMap, rc::Rc}; use itertools::Itertools; use super::{ - finetune::FineTune, - pattern_effect::{PatternEffect, PatternEffectMemoryKey}, - pattern_event::{PatternEventNote, PatternEventVolume}, - t_instrument::{TInstrument, TSample}, - t_song::TSong, + finetune::FineTune, pattern_effect::*, pattern_event::*, t_instrument::*, t_song::TSong, }; use crate::asset::sound::sample::Sample; @@ -70,7 +66,7 @@ struct PlayerChannel { instrument: Option>, sample: Option>, note: PlayerChannelNote, - volume: Option, + volume: f32, effects: [Option; 2], effects_memory: HashMap, @@ -91,7 +87,7 @@ impl PlayerChannel { const SAMPLE_BLEND: f64 = 0.0008; fn note_cut(&mut self) { - self.volume = Some(PatternEventVolume::Value(0.)); + self.volume = 0.; } fn pos_reset(&mut self) { @@ -102,19 +98,14 @@ impl PlayerChannel { fn generate_sample(&mut self, step: f64) -> T { let current_sample = if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample - && let Some(volume) = &self.volume && let Some(note) = self.note.finetune { let value = sample.get(self.pos_sample).into_normalized_f32(); - let volume = match volume { - PatternEventVolume::Sample => sample.volume, - PatternEventVolume::Value(volume) => *volume, - }; let pitch_factor = (note + sample.finetune).pitch_factor(); self.pos_sample += step / pitch_factor; - T::from_normalized_f32(value * volume) + T::from_normalized_f32(value * self.volume) } else { T::from_normalized_f32(0.) }; @@ -174,7 +165,17 @@ impl PlayerChannel { } fn change_volume(&mut self, volume: PatternEventVolume) { - self.volume = Some(volume); + self.volume = match volume { + PatternEventVolume::Sample => { + if let Some(sample) = &self.sample { + sample.volume + } else { + self.note_cut(); + return; + } + } + PatternEventVolume::Value(volume) => volume, + }; } fn change_effect(&mut self, i: usize, effect: PatternEffect) { @@ -188,6 +189,7 @@ impl PlayerChannel { } else { effect }; + self.effects[i] = Some(effect); } } @@ -294,14 +296,107 @@ impl<'a> Player<'a> { for (i, effect) in event.effects.iter().enumerate() { if let Some(effect) = effect { - //channel.change_effect(i, effect.clone()); - //channel.effects[i] - // .expect("Effect was initialized") - // .init(self, channel); + channel.change_effect(i, effect.clone()); + + use PatternEffect as E; + match channel.effects[i].unwrap() { + // Init effects + E::Speed(Speed::Bpm(bpm)) => { + self.bpm = bpm; + } + E::Speed(Speed::TicksPerRow(ticks_per_row)) => { + self.tempo = ticks_per_row; + } + E::GlobalVolume(volume) => { + self.volume_global = volume; + } + E::Volume(Volume::Set(volume)) => { + channel.volume = volume; + } + E::Volume(Volume::Bump { + volume: Some(volume), + .. + }) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Bump { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel.note.finetune.map(|f| f + finetune); + } + E::PlaybackDirection(..) => {} + E::SampleOffset(Some(offset)) => { + // TODO(nenikitov): Remove this hardcoded value + channel.pos_sample = 1. / 16_000. * offset as f64; + } + // Noops - no init + E::Volume(Volume::Slide(..)) => {} + E::Porta(Porta::Tone(..)) => {} + E::Porta(Porta::Slide { .. }) => {} + // TODO(nenikitov): To implement + E::Dummy(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Bump { volume: None, .. }) + | E::Porta(Porta::Tone(None)) + | E::Porta(Porta::Bump { finetune: None, .. }) + | E::SampleOffset(None) => { + unreachable!("Effects should have their memory initialized") + } + } } } // TODO(nenikitov): Do effects + for effect in channel.effects.iter() { + if let Some(effect) = effect { + use PatternEffect as E; + match *effect { + // Tick effects + E::Volume(Volume::Slide(Some(volume))) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Tone(Some(step))) => { + if let Some(finetune_initial) = channel.note.finetune_initial { + channel.note.finetune = channel.note.finetune.map(|finetune| { + use std::cmp::Ordering; + match finetune.cmp(&finetune_initial) { + Ordering::Less => { + FineTune::min(finetune + step, finetune_initial) + } + Ordering::Greater => { + FineTune::max(finetune - step, finetune_initial) + } + Ordering::Equal => finetune, + } + }); + } + } + E::Porta(Porta::Slide { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel.note.finetune.map(|f| f + finetune); + } + // Noops - no tick + E::Volume(Volume::Set(..)) + | E::Volume(Volume::Bump { .. }) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Bump { .. }) + | E::Speed(..) + | E::GlobalVolume(..) + | E::SampleOffset(..) + | E::PlaybackDirection(..) => {} + // TODO(nenikitov): Unemplemented + E::Dummy(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Slide(None)) + | E::Porta(Porta::Slide { finetune: None, .. }) => { + unreachable!("Effects should have their memory initialized") + } + } + } + } } } } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 113cb4f..d389123 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -69,7 +69,7 @@ pub enum PatternEffect { impl PatternEffect { pub fn memory_key(&self) -> Option { match self { - PatternEffect::Porta(Porta::Tone(_)) => Some(PatternEffectMemoryKey::PortaTone), + PatternEffect::Porta(Porta::Tone(..)) => Some(PatternEffectMemoryKey::PortaTone), PatternEffect::Porta(Porta::Slide { up: true, .. }) => { Some(PatternEffectMemoryKey::PortaSlideUp) } @@ -96,14 +96,14 @@ impl PatternEffect { small: true, .. }) => Some(PatternEffectMemoryKey::PortaBumpSmallDown), - PatternEffect::Volume(Volume::Slide(_)) => Some(PatternEffectMemoryKey::VolumeSlide), + PatternEffect::Volume(Volume::Slide(..)) => Some(PatternEffectMemoryKey::VolumeSlide), PatternEffect::Volume(Volume::Bump { up: true, .. }) => { Some(PatternEffectMemoryKey::VolumeBumpUp) } PatternEffect::Volume(Volume::Bump { up: down, .. }) => { Some(PatternEffectMemoryKey::VolumeBumpDown) } - PatternEffect::SampleOffset(_) => Some(PatternEffectMemoryKey::SampleOffset), + PatternEffect::SampleOffset(..) => Some(PatternEffectMemoryKey::SampleOffset), _ => None, } } From e833fa68afd4880f3a6d4f02399d11488c2e382b Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 4 Oct 2024 20:04:05 -0400 Subject: [PATCH 60/87] feat: implement volume envelope --- Cargo.toml | 1 + engine/src/asset/sound/dat/mixer_new.rs | 193 ++++++++++++------- engine/src/asset/sound/dat/pattern_effect.rs | 2 +- engine/src/asset/sound/dat/t_instrument.rs | 4 +- engine/src/asset/sound/mod.rs | 3 +- 5 files changed, 128 insertions(+), 75 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e9140be..a8f57e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ panic = "abort" [profile.release] debug = true +overflow-checks = true diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 3fd90c3..c18a571 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -39,21 +39,6 @@ impl AudioSamplePoint for i16 { } } -trait PatternEffectLogic { - fn init(&self, player: &mut Player, channel: &mut PlayerChannel); - fn tick(&self, player: &mut Player, channel: &mut PlayerChannel); -} - -impl PatternEffectLogic for PatternEffect { - fn init(&self, player: &mut Player, channel: &mut PlayerChannel) { - todo!() - } - - fn tick(&self, player: &mut Player, channel: &mut PlayerChannel) { - todo!() - } -} - #[derive(Default, Clone)] struct PlayerChannelNote { finetune: Option, @@ -74,6 +59,7 @@ struct PlayerChannel { pos_sample: f64, pos_volume_envelope: usize, + direction: PlaybackDirection, } impl PlayerChannel { @@ -93,19 +79,51 @@ impl PlayerChannel { fn pos_reset(&mut self) { self.pos_sample = 0.; self.pos_volume_envelope = 0; + self.direction = PlaybackDirection::Forwards; } fn generate_sample(&mut self, step: f64) -> T { let current_sample = if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(note) = self.note.finetune + && self.pos_sample >= 0. { let value = sample.get(self.pos_sample).into_normalized_f32(); let pitch_factor = (note + sample.finetune).pitch_factor(); - self.pos_sample += step / pitch_factor; + let step = step / pitch_factor; + match self.direction { + PlaybackDirection::Forwards => { + self.pos_sample += step; + } + PlaybackDirection::Backwards => { + self.pos_sample -= step; + } + } + + let volume = self.volume + * match &instrument.volume { + TInstrumentVolume::Envelope(envelope) => { + if self.note.on { + envelope + .volume_beginning() + .get(self.pos_volume_envelope) + .cloned() + .unwrap_or(envelope.volume_loop()) + } else { + envelope + .volume_end() + .get(self.pos_volume_envelope.saturating_sub( + envelope.volume_beginning().len().saturating_sub(1), + )) + .cloned() + .unwrap_or(0.) + } + } + TInstrumentVolume::Constant(_) => 1., + }; - T::from_normalized_f32(value * self.volume) + T::from_normalized_f32(value * volume) } else { T::from_normalized_f32(0.) }; @@ -191,6 +209,23 @@ impl PlayerChannel { }; self.effects[i] = Some(effect); } + + fn advance_envelopes(&mut self) { + if let Some(instrument) = &self.instrument + && let TInstrumentVolume::Envelope(envelope) = &instrument.volume + { + if (self.note.on + && if let Some(sustain) = envelope.sustain { + self.pos_volume_envelope < sustain + } else { + true + }) + || !self.note.on + { + self.pos_volume_envelope += 1; + } + } + } } struct Player<'a> { @@ -207,6 +242,7 @@ struct Player<'a> { tempo: usize, bpm: usize, volume_global: f32, + volume_amplification: f32, channels: Vec, } @@ -223,7 +259,8 @@ impl<'a> Player<'a> { pos_tick: 0, tempo: song.speed as usize, bpm: song.bpm as usize, - volume_global: 0.375, + volume_global: 1., + volume_amplification: 0.25, channels: (0..song.orders[0][0].len()) .map(|_| PlayerChannel::default()) .collect(), @@ -242,8 +279,10 @@ impl<'a> Player<'a> { .iter_mut() .map(|c| c.generate_sample::(step)) .map(|c| c.into_normalized_f32()) + //.enumerate() + //.filter_map(|(i, s)| (i == 4).then_some(s)) .sum::(); - S::from_normalized_f32(sample * self.volume_global) + S::from_normalized_f32(sample * self.volume_global * self.volume_amplification) } fn tick(&mut self) { @@ -251,6 +290,62 @@ impl<'a> Player<'a> { self.row(); } + for channel in self.channels.iter_mut() { + channel.advance_envelopes(); + + for effect in channel.effects.iter() { + if let Some(effect) = effect { + use PatternEffect as E; + match *effect { + // Tick effects + E::Volume(Volume::Slide(Some(volume))) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Tone(Some(step))) => { + if let Some(finetune_initial) = channel.note.finetune_initial { + channel.note.finetune = channel.note.finetune.map(|finetune| { + use std::cmp::Ordering; + match finetune.cmp(&finetune_initial) { + Ordering::Less => { + FineTune::min(finetune + step, finetune_initial) + } + Ordering::Greater => { + FineTune::max(finetune - step, finetune_initial) + } + Ordering::Equal => finetune, + } + }); + } + } + E::Porta(Porta::Slide { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel.note.finetune.map(|f| { + (f + finetune).clamp(FineTune::new(0), FineTune::new(15488)) + }); + } + // Noops - no tick + E::Volume(Volume::Set(..)) + | E::Volume(Volume::Bump { .. }) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Bump { .. }) + | E::Speed(..) + | E::GlobalVolume(..) + | E::SampleOffset(..) + | E::PlaybackDirection(..) => {} + // TODO(nenikitov): Unemplemented + E::Dummy(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Slide(None)) + | E::Porta(Porta::Slide { finetune: None, .. }) => { + unreachable!("Effects should have their memory initialized") + } + } + } + } + } + self.pos_tick += 1; if self.pos_tick >= self.tempo { @@ -325,7 +420,14 @@ impl<'a> Player<'a> { }) => { channel.note.finetune = channel.note.finetune.map(|f| f + finetune); } - E::PlaybackDirection(..) => {} + E::PlaybackDirection(direction) => { + channel.direction = direction; + if let Some(sample) = &channel.sample + && direction == PlaybackDirection::Backwards + { + channel.pos_sample = sample.data.len_seconds() as f64 + } + } E::SampleOffset(Some(offset)) => { // TODO(nenikitov): Remove this hardcoded value channel.pos_sample = 1. / 16_000. * offset as f64; @@ -346,57 +448,6 @@ impl<'a> Player<'a> { } } } - - // TODO(nenikitov): Do effects - for effect in channel.effects.iter() { - if let Some(effect) = effect { - use PatternEffect as E; - match *effect { - // Tick effects - E::Volume(Volume::Slide(Some(volume))) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); - } - E::Porta(Porta::Tone(Some(step))) => { - if let Some(finetune_initial) = channel.note.finetune_initial { - channel.note.finetune = channel.note.finetune.map(|finetune| { - use std::cmp::Ordering; - match finetune.cmp(&finetune_initial) { - Ordering::Less => { - FineTune::min(finetune + step, finetune_initial) - } - Ordering::Greater => { - FineTune::max(finetune - step, finetune_initial) - } - Ordering::Equal => finetune, - } - }); - } - } - E::Porta(Porta::Slide { - finetune: Some(finetune), - .. - }) => { - channel.note.finetune = channel.note.finetune.map(|f| f + finetune); - } - // Noops - no tick - E::Volume(Volume::Set(..)) - | E::Volume(Volume::Bump { .. }) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Bump { .. }) - | E::Speed(..) - | E::GlobalVolume(..) - | E::SampleOffset(..) - | E::PlaybackDirection(..) => {} - // TODO(nenikitov): Unemplemented - E::Dummy(..) => {} - // Unreachable because memory has to be initialized - E::Volume(Volume::Slide(None)) - | E::Porta(Porta::Slide { finetune: None, .. }) => { - unreachable!("Effects should have their memory initialized") - } - } - } - } } } } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index d389123..2833e0f 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -33,7 +33,7 @@ pub enum Volume { Bump { up: bool, volume: Option }, } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum PlaybackDirection { #[default] Forwards, diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 1498dde..c01b261 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -39,8 +39,8 @@ impl AssetParser for TInstrumentFlags { #[derive(Debug)] pub struct TInstrumentVolumeEnvelope { - data: Vec, - sustain: Option, + pub data: Vec, + pub sustain: Option, } impl TInstrumentVolumeEnvelope { diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index cc4886a..963aaf2 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0x8; + let i = 0x1; let song = sounds .iter() .filter_map(|s| match s { @@ -92,6 +92,7 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; + dbg!(song); } sounds From 4aaf12b562711534249dcbea2a85249665179944 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 4 Oct 2024 22:49:32 -0400 Subject: [PATCH 61/87] feat(effect): note delay --- engine/src/asset/sound/dat/mixer_new.rs | 278 ++++++++++--------- engine/src/asset/sound/dat/pattern_effect.rs | 6 +- engine/src/asset/sound/dat/t_instrument.rs | 16 +- engine/src/asset/sound/mod.rs | 2 +- 4 files changed, 155 insertions(+), 147 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index c18a571..6aff85d 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -54,6 +54,7 @@ struct PlayerChannel { volume: f32, effects: [Option; 2], effects_memory: HashMap, + note_delay: usize, previous: Option<(Box, f64)>, @@ -86,51 +87,44 @@ impl PlayerChannel { let current_sample = if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(note) = self.note.finetune - && self.pos_sample >= 0. + && self.note_delay == 0 + && let Some(value) = sample.get(self.pos_sample) { - let value = sample.get(self.pos_sample).into_normalized_f32(); - let pitch_factor = (note + sample.finetune).pitch_factor(); let step = step / pitch_factor; - match self.direction { - PlaybackDirection::Forwards => { - self.pos_sample += step; - } - PlaybackDirection::Backwards => { - self.pos_sample -= step; - } - } - - let volume = self.volume - * match &instrument.volume { - TInstrumentVolume::Envelope(envelope) => { - if self.note.on { - envelope - .volume_beginning() - .get(self.pos_volume_envelope) - .cloned() - .unwrap_or(envelope.volume_loop()) - } else { - envelope - .volume_end() - .get(self.pos_volume_envelope.saturating_sub( - envelope.volume_beginning().len().saturating_sub(1), - )) - .cloned() - .unwrap_or(0.) - } + self.pos_sample += match self.direction { + PlaybackDirection::Forwards => step, + PlaybackDirection::Backwards => -step, + }; + + let volume_envelope = match &instrument.volume { + TInstrumentVolume::Envelope(envelope) => { + if self.note.on { + envelope + .volume_beginning() + .get(self.pos_volume_envelope) + .cloned() + .unwrap_or(envelope.volume_loop()) + } else { + envelope + .volume_end() + .get(self.pos_volume_envelope.saturating_sub( + envelope.volume_beginning().len().saturating_sub(1), + )) + .cloned() + .unwrap_or(0.) } - TInstrumentVolume::Constant(_) => 1., - }; + } + TInstrumentVolume::Constant(_) => 1., + }; - T::from_normalized_f32(value * volume) + value.into_normalized_f32() * volume_envelope * self.volume } else { - T::from_normalized_f32(0.) + 0. }; - if let Some((previous, position)) = &mut self.previous { + let current_sample = if let Some((previous, position)) = &mut self.previous { let factor = (*position / Self::SAMPLE_BLEND) as f32; - let current_sample = current_sample.into_normalized_f32(); let previous_sample = previous.generate_sample::(step).into_normalized_f32(); *position += step; @@ -138,10 +132,12 @@ impl PlayerChannel { self.previous = None } - T::from_normalized_f32(previous_sample + factor * (current_sample - previous_sample)) + previous_sample + factor * (current_sample - previous_sample) } else { current_sample - } + }; + + T::from_normalized_f32(current_sample) } fn change_instrument(&mut self, instrument: Option>) { @@ -293,54 +289,56 @@ impl<'a> Player<'a> { for channel in self.channels.iter_mut() { channel.advance_envelopes(); - for effect in channel.effects.iter() { - if let Some(effect) = effect { - use PatternEffect as E; - match *effect { - // Tick effects - E::Volume(Volume::Slide(Some(volume))) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); - } - E::Porta(Porta::Tone(Some(step))) => { - if let Some(finetune_initial) = channel.note.finetune_initial { - channel.note.finetune = channel.note.finetune.map(|finetune| { - use std::cmp::Ordering; - match finetune.cmp(&finetune_initial) { - Ordering::Less => { - FineTune::min(finetune + step, finetune_initial) - } - Ordering::Greater => { - FineTune::max(finetune - step, finetune_initial) - } - Ordering::Equal => finetune, + for effect in channel.effects.iter().flatten() { + use PatternEffect as E; + match *effect { + // Tick effects + E::Volume(Volume::Slide(Some(volume))) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Tone(Some(step))) => { + if let Some(finetune_initial) = channel.note.finetune_initial { + channel.note.finetune = channel.note.finetune.map(|finetune| { + use std::cmp::Ordering; + match finetune.cmp(&finetune_initial) { + Ordering::Less => { + FineTune::min(finetune + step, finetune_initial) } - }); - } - } - E::Porta(Porta::Slide { - finetune: Some(finetune), - .. - }) => { - channel.note.finetune = channel.note.finetune.map(|f| { - (f + finetune).clamp(FineTune::new(0), FineTune::new(15488)) + Ordering::Greater => { + FineTune::max(finetune - step, finetune_initial) + } + Ordering::Equal => finetune, + } }); } - // Noops - no tick - E::Volume(Volume::Set(..)) - | E::Volume(Volume::Bump { .. }) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Bump { .. }) - | E::Speed(..) - | E::GlobalVolume(..) - | E::SampleOffset(..) - | E::PlaybackDirection(..) => {} - // TODO(nenikitov): Unemplemented - E::Dummy(..) => {} - // Unreachable because memory has to be initialized - E::Volume(Volume::Slide(None)) - | E::Porta(Porta::Slide { finetune: None, .. }) => { - unreachable!("Effects should have their memory initialized") - } + } + E::Porta(Porta::Slide { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel + .note + .finetune + .map(|f| (f + finetune).clamp(FineTune::new(0), FineTune::new(15488))); + } + E::NoteDelay(_) => { + channel.note_delay = channel.note_delay.saturating_sub(1); + } + // Noops - no tick + E::Volume(Volume::Set(..)) + | E::Volume(Volume::Bump { .. }) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Bump { .. }) + | E::Speed(..) + | E::GlobalVolume(..) + | E::SampleOffset(..) + | E::PlaybackDirection(..) => {} + // TODO(nenikitov): Unemplemented + E::Dummy(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Slide(None)) + | E::Porta(Porta::Slide { finetune: None, .. }) => { + unreachable!("Effects should have their memory initialized") } } } @@ -389,63 +387,69 @@ impl<'a> Player<'a> { channel.change_volume(volume.clone()); } - for (i, effect) in event.effects.iter().enumerate() { - if let Some(effect) = effect { - channel.change_effect(i, effect.clone()); + for (i, effect) in event + .effects + .iter() + .enumerate() + .filter_map(|(i, e)| e.map(|e| (i, e))) + { + channel.change_effect(i, effect.clone()); - use PatternEffect as E; - match channel.effects[i].unwrap() { - // Init effects - E::Speed(Speed::Bpm(bpm)) => { - self.bpm = bpm; - } - E::Speed(Speed::TicksPerRow(ticks_per_row)) => { - self.tempo = ticks_per_row; - } - E::GlobalVolume(volume) => { - self.volume_global = volume; - } - E::Volume(Volume::Set(volume)) => { - channel.volume = volume; - } - E::Volume(Volume::Bump { - volume: Some(volume), - .. - }) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); - } - E::Porta(Porta::Bump { - finetune: Some(finetune), - .. - }) => { - channel.note.finetune = channel.note.finetune.map(|f| f + finetune); - } - E::PlaybackDirection(direction) => { - channel.direction = direction; - if let Some(sample) = &channel.sample - && direction == PlaybackDirection::Backwards - { - channel.pos_sample = sample.data.len_seconds() as f64 - } - } - E::SampleOffset(Some(offset)) => { - // TODO(nenikitov): Remove this hardcoded value - channel.pos_sample = 1. / 16_000. * offset as f64; - } - // Noops - no init - E::Volume(Volume::Slide(..)) => {} - E::Porta(Porta::Tone(..)) => {} - E::Porta(Porta::Slide { .. }) => {} - // TODO(nenikitov): To implement - E::Dummy(..) => {} - // Unreachable because memory has to be initialized - E::Volume(Volume::Bump { volume: None, .. }) - | E::Porta(Porta::Tone(None)) - | E::Porta(Porta::Bump { finetune: None, .. }) - | E::SampleOffset(None) => { - unreachable!("Effects should have their memory initialized") + use PatternEffect as E; + match channel.effects[i].unwrap() { + // Init effects + E::Speed(Speed::Bpm(bpm)) => { + self.bpm = bpm; + } + E::Speed(Speed::TicksPerRow(ticks_per_row)) => { + self.tempo = ticks_per_row; + } + E::GlobalVolume(volume) => { + self.volume_global = volume; + } + E::Volume(Volume::Set(volume)) => { + channel.volume = volume; + } + E::Volume(Volume::Bump { + volume: Some(volume), + .. + }) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Bump { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel.note.finetune.map(|f| f + finetune); + } + E::PlaybackDirection(direction) => { + channel.direction = direction; + if let Some(sample) = &channel.sample + && direction == PlaybackDirection::Backwards + { + channel.pos_sample = sample.data.len_seconds() as f64 } } + E::SampleOffset(Some(offset)) => { + // TODO(nenikitov): Remove this hardcoded value + channel.pos_sample = 1. / 16_000. * offset as f64; + } + E::NoteDelay(delay) => { + channel.note_delay = delay; + } + // Noops - no init + E::Volume(Volume::Slide(..)) => {} + E::Porta(Porta::Tone(..)) => {} + E::Porta(Porta::Slide { .. }) => {} + // TODO(nenikitov): To implement + E::Dummy(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Bump { volume: None, .. }) + | E::Porta(Porta::Tone(None)) + | E::Porta(Porta::Bump { finetune: None, .. }) + | E::SampleOffset(None) => { + unreachable!("Effects should have their memory initialized") + } } } } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 2833e0f..68fc4c5 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -64,6 +64,7 @@ pub enum PatternEffect { SampleOffset(Option), PlaybackDirection(PlaybackDirection), GlobalVolume(f32), + NoteDelay(usize), } impl PatternEffect { @@ -186,10 +187,11 @@ impl AssetParser for Option { }, ))), 0x0F => PatternEffect::GlobalVolume(convert_volume(value)), + 0x21 => PatternEffect::NoteDelay(value as usize), // TODO(nenikitov): Remove dummy effect 0x00 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D - | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x21 | 0x22 | 0x2E - | 0x2F | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => PatternEffect::Dummy(kind), + | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x22 | 0x2E | 0x2F + | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => PatternEffect::Dummy(kind), // TODO(nenikitov): Add support for other effects // 0x00 => Self::Arpegio, // 0x01 => Self::PortaUp, diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index c01b261..ccb2c71 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -269,21 +269,23 @@ impl TSample { } // TODO(nenikitov): I think the whole `Sample` will need to be removed - pub fn get(&self, position: f64) -> i16 { + pub fn get(&self, position: f64) -> Option { + if position < 0. { + return None; + } + let position = Self::SAMPLE_RATE as f64 * position; let frac = position.fract() as f32; let Some(prev) = self.normalize(position as usize) else { - return 0; - }; - let Some(next) = self.normalize(position as usize + 1) else { - return 0; + return None; }; + let next = self.normalize(position as usize + 1); let prev = self.data[prev][0] as f32; - let next = self.data[next][0] as f32; + let next = next.map(|next| self.data[next][0] as f32).unwrap_or(0.); - (prev + frac * (next - prev)) as i16 + Some((prev + frac * (next - prev)) as i16) } fn normalize(&self, position: usize) -> Option { diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 963aaf2..22368d0 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0x1; + let i = 0x8; let song = sounds .iter() .filter_map(|s| match s { From 13b5f2aa54f47cde3cd5ef0f6e500299c39791cb Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 4 Oct 2024 23:40:45 -0400 Subject: [PATCH 62/87] fix(instrument): retrigger on special 255 instrument --- engine/src/asset/sound/dat/mixer_new.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 6aff85d..25f8df1 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -39,7 +39,7 @@ impl AudioSamplePoint for i16 { } } -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] struct PlayerChannelNote { finetune: Option, finetune_initial: Option, @@ -140,22 +140,28 @@ impl PlayerChannel { T::from_normalized_f32(current_sample) } - fn change_instrument(&mut self, instrument: Option>) { - // In tracker music, every instrument change is a state reset + fn trigger_note(&mut self) { // Previous state is kept to subtly blend in notes to remove clicks. // Disregard previous state before `self.clone` so we don't have a fully recursive structure. self.previous = None; self.previous = Some((Box::new(self.clone()), 0.)); + self.pos_reset(); + } + + fn change_instrument(&mut self, instrument: Option>) { if let Some(instrument) = instrument { self.instrument = Some(instrument); - self.pos_reset(); + + self.trigger_note(); } else { - // TODO(nenikitov): Idk honestly, figure this out - self.note_cut(); - self.instrument = None; - self.sample = None; + if self.instrument.is_none() { + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); + self.instrument = None; + self.sample = None; + } } } @@ -276,7 +282,7 @@ impl<'a> Player<'a> { .map(|c| c.generate_sample::(step)) .map(|c| c.into_normalized_f32()) //.enumerate() - //.filter_map(|(i, s)| (i == 4).then_some(s)) + //.filter_map(|(i, s)| (i == 8).then_some(s)) .sum::(); S::from_normalized_f32(sample * self.volume_global * self.volume_amplification) } From 07926070ef5319049fd1e42d316317adc533d0a7 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 5 Oct 2024 11:17:37 -0400 Subject: [PATCH 63/87] fix(instrument): retrigger on special 255 instrument --- engine/src/asset/sound/dat/mixer_new.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 25f8df1..0d855e4 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -153,15 +153,15 @@ impl PlayerChannel { fn change_instrument(&mut self, instrument: Option>) { if let Some(instrument) = instrument { self.instrument = Some(instrument); + } + if self.instrument.is_some() { self.trigger_note(); } else { - if self.instrument.is_none() { - // TODO(nenikitov): Idk honestly, figure this out - self.note_cut(); - self.instrument = None; - self.sample = None; - } + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); + self.instrument = None; + self.sample = None; } } From 9aa2cb68aedd6110bb4c72c471546e32fce0867e Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sat, 5 Oct 2024 12:24:34 -0400 Subject: [PATCH 64/87] fix(effects): repeated effects should only apply in the current row --- engine/src/asset/sound/dat/mixer_new.rs | 7 ++++++- engine/src/asset/sound/mod.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 0d855e4..faf82fc 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -228,6 +228,10 @@ impl PlayerChannel { } } } + + fn clear_effects(&mut self) { + self.effects = [None, None]; + } } struct Player<'a> { @@ -282,7 +286,7 @@ impl<'a> Player<'a> { .map(|c| c.generate_sample::(step)) .map(|c| c.into_normalized_f32()) //.enumerate() - //.filter_map(|(i, s)| (i == 8).then_some(s)) + //.filter_map(|(i, s)| (i == 4).then_some(s)) .sum::(); S::from_normalized_f32(sample * self.volume_global * self.volume_amplification) } @@ -393,6 +397,7 @@ impl<'a> Player<'a> { channel.change_volume(volume.clone()); } + channel.clear_effects(); for (i, effect) in event .effects .iter() diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 22368d0..963aaf2 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0x8; + let i = 0x1; let song = sounds .iter() .filter_map(|s| match s { From 3c36cc870eed7d5f1656a6c2deaded0238c32abe Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 6 Oct 2024 13:58:14 -0400 Subject: [PATCH 65/87] feat: add volume ramping --- engine/src/asset/sound/dat/mixer_new.rs | 87 +++++++++++++++++++------ engine/src/lib.rs | 3 +- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index faf82fc..0fa3ab3 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -1,4 +1,9 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{ + collections::HashMap, + ops::{Add, Sub}, + rc::Rc, + time::Duration, +}; use itertools::Itertools; @@ -52,6 +57,8 @@ struct PlayerChannel { sample: Option>, note: PlayerChannelNote, volume: f32, + volume_target: f32, + volume_actual: f32, effects: [Option; 2], effects_memory: HashMap, note_delay: usize, @@ -64,17 +71,23 @@ struct PlayerChannel { } impl PlayerChannel { + // Length of a transition between the current and the next sample in seconds // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. // Too little and transitions between notes will click. - // This is 800 microseconds, which is a bit of an arbitrary value that i found sounds nice. + // Chosen value is a bit of an arbitrary value that I found sounds nice. // It amounts to: // - 13 samples at 16000 // - 35 samples at 44100 // - 38 samples at 48000 - const SAMPLE_BLEND: f64 = 0.0008; + const SAMPLE_BLEND: f64 = Duration::from_micros(800).as_secs_f64(); + // Maximum difference in volume between 2 audio samples + // Volume as in channels volume, does not account for samples + // A bit of an arbitrary amount too + const MAX_VOLUME_CHANGE: f32 = 1. / 128.; fn note_cut(&mut self) { self.volume = 0.; + self.volume_actual = 0.; } fn pos_reset(&mut self) { @@ -108,17 +121,24 @@ impl PlayerChannel { } else { envelope .volume_end() - .get(self.pos_volume_envelope.saturating_sub( - envelope.volume_beginning().len().saturating_sub(1), - )) + .get( + self.pos_volume_envelope + .saturating_sub(envelope.volume_beginning().len()), + ) .cloned() .unwrap_or(0.) } } TInstrumentVolume::Constant(_) => 1., }; - - value.into_normalized_f32() * volume_envelope * self.volume + self.volume_target = volume_envelope * self.volume; + self.volume_actual = advance_to( + self.volume_actual, + self.volume_target, + Self::MAX_VOLUME_CHANGE, + ); + + value.into_normalized_f32() * self.volume_actual } else { 0. }; @@ -234,6 +254,39 @@ impl PlayerChannel { } } +trait MinMax { + fn generic_min(a: Self, b: Self) -> Self; + fn generic_max(a: Self, b: Self) -> Self; +} + +macro_rules! impl_min_max { + ($($ty:tt),*) => { + $(impl MinMax for $ty { + fn generic_min(a: Self, b: Self) -> Self { + $ty::min(a, b) + } + + fn generic_max(a: Self, b: Self) -> Self { + $ty::max(a, b) + } + })* + }; +} + +impl_min_max!(f32, FineTune); + +fn advance_to(from: T, to: T, step: T) -> T +where + T: PartialOrd + Add + Sub + MinMax, +{ + use std::cmp::Ordering; + match from.partial_cmp(&to) { + Some(Ordering::Less) => T::generic_min(from + step, to), + Some(Ordering::Greater) => T::generic_max(from - step, to), + Some(Ordering::Equal) | None => from, + } +} + struct Player<'a> { song: &'a TSong, sample_rate: usize, @@ -286,7 +339,7 @@ impl<'a> Player<'a> { .map(|c| c.generate_sample::(step)) .map(|c| c.into_normalized_f32()) //.enumerate() - //.filter_map(|(i, s)| (i == 4).then_some(s)) + //.filter_map(|(i, s)| (i == 0).then_some(s)) .sum::(); S::from_normalized_f32(sample * self.volume_global * self.volume_amplification) } @@ -308,18 +361,10 @@ impl<'a> Player<'a> { } E::Porta(Porta::Tone(Some(step))) => { if let Some(finetune_initial) = channel.note.finetune_initial { - channel.note.finetune = channel.note.finetune.map(|finetune| { - use std::cmp::Ordering; - match finetune.cmp(&finetune_initial) { - Ordering::Less => { - FineTune::min(finetune + step, finetune_initial) - } - Ordering::Greater => { - FineTune::max(finetune - step, finetune_initial) - } - Ordering::Equal => finetune, - } - }); + channel.note.finetune = channel + .note + .finetune + .map(|finetune| advance_to(finetune, finetune_initial, step)); } } E::Porta(Porta::Slide { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index d0e06f8..1c89dbc 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -15,10 +15,11 @@ #![feature( // Discussion about possible future alternatives: // https://github.com/rust-lang/rust/pull/101179 + debug_closure_helpers, + duration_consts_float, io_error_more, let_chains, maybe_uninit_uninit_array_transpose, - debug_closure_helpers, )] pub mod asset; From 58cdec8262e8ee1cf30a5ca0e114e2e761f2e413 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Sun, 6 Oct 2024 23:34:54 -0400 Subject: [PATCH 66/87] feat(effect): pattern break and wip pattern jump --- engine/src/asset/sound/dat/mixer_new.rs | 27 ++++++++- engine/src/asset/sound/dat/pattern_effect.rs | 60 ++++++++++++-------- engine/src/asset/sound/mod.rs | 4 +- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 0fa3ab3..a7c8a59 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -297,6 +297,7 @@ struct Player<'a> { pos_pattern: usize, pos_row: usize, pos_tick: usize, + jump: Option<(usize, usize)>, tempo: usize, bpm: usize, @@ -316,6 +317,7 @@ impl<'a> Player<'a> { pos_pattern: 0, pos_row: 0, pos_tick: 0, + jump: None, tempo: song.speed as usize, bpm: song.bpm as usize, volume_global: 1., @@ -380,11 +382,13 @@ impl<'a> Player<'a> { channel.note_delay = channel.note_delay.saturating_sub(1); } // Noops - no tick - E::Volume(Volume::Set(..)) + E::Speed(..) + | E::PatternBreak + | E::PatternJump(..) + | E::Volume(Volume::Set(..)) | E::Volume(Volume::Bump { .. }) | E::Porta(Porta::Tone(..)) | E::Porta(Porta::Bump { .. }) - | E::Speed(..) | E::GlobalVolume(..) | E::SampleOffset(..) | E::PlaybackDirection(..) => {} @@ -420,6 +424,11 @@ impl<'a> Player<'a> { } fn row(&mut self) { + if let Some((pos_pattern, pos_row)) = self.jump.take() { + self.pos_pattern = pos_pattern; + self.pos_row = pos_row; + } + let Some(row) = self .song .orders @@ -460,6 +469,16 @@ impl<'a> Player<'a> { E::Speed(Speed::TicksPerRow(ticks_per_row)) => { self.tempo = ticks_per_row; } + E::PatternBreak => { + self.jump = Some((self.pos_pattern + 1, 0)); + } + E::PatternJump(position) => { + println!( + "On pat {pat:x} row {row:x} -> {position:x}", + pat = self.pos_pattern, + row = self.pos_row + ) + } E::GlobalVolume(volume) => { self.volume_global = volume; } @@ -498,7 +517,9 @@ impl<'a> Player<'a> { E::Porta(Porta::Tone(..)) => {} E::Porta(Porta::Slide { .. }) => {} // TODO(nenikitov): To implement - E::Dummy(..) => {} + E::Dummy(code) => { + //println!("{code:x}"); + } // Unreachable because memory has to be initialized E::Volume(Volume::Bump { volume: None, .. }) | E::Porta(Porta::Tone(None)) diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 68fc4c5..975ab3e 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -65,6 +65,8 @@ pub enum PatternEffect { PlaybackDirection(PlaybackDirection), GlobalVolume(f32), NoteDelay(usize), + PatternBreak, + PatternJump(usize), } impl PatternEffect { @@ -136,62 +138,69 @@ impl AssetParser for Option { let (input, kind) = number::le_u8(input)?; let (input, value) = number::le_u8(input)?; + use PatternEffect as E; Ok(( input, should_parse.then_some(match kind { - 0x01 => PatternEffect::Porta(Porta::Slide { + 0x01 => E::Porta(Porta::Slide { up: true, finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), }), - 0x02 => PatternEffect::Porta(Porta::Slide { + 0x02 => E::Porta(Porta::Slide { up: false, finetune: (value != 0).then_some(-FineTune::new(8 * value as i32)), }), - 0x03 => PatternEffect::Porta(Porta::Tone( + 0x03 => E::Porta(Porta::Tone( (value != 0).then_some(FineTune::new(8 * value as i32)), )), - 0x15 => PatternEffect::Porta(Porta::Bump { + 0x15 => E::Porta(Porta::Bump { up: true, small: false, finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), }), - 0x16 => PatternEffect::Porta(Porta::Bump { + 0x16 => E::Porta(Porta::Bump { up: false, small: false, finetune: (value != 0).then_some(-FineTune::new(8 * value as i32)), }), - 0x24 => PatternEffect::Porta(Porta::Bump { + 0x24 => E::Porta(Porta::Bump { up: true, small: true, finetune: (value != 0).then_some(FineTune::new(2 * (value & 0xF) as i32)), }), - 0x25 => PatternEffect::Porta(Porta::Bump { + 0x25 => E::Porta(Porta::Bump { up: false, small: true, finetune: (value != 0).then_some(-FineTune::new(2 * (value & 0xF) as i32)), }), - 0x09 => { - PatternEffect::SampleOffset((value != 0).then_some(value as usize * 256)) - } - 0x0E => PatternEffect::Speed(if value >= 0x20 { + 0x09 => E::SampleOffset((value != 0).then_some(value as usize * 256)), + 0x0E => E::Speed(if value >= 0x20 { Speed::Bpm(value as usize) } else { Speed::TicksPerRow(value as usize) }), - 0x0C => PatternEffect::Volume(Volume::Set(convert_volume(value))), - 0x0A => PatternEffect::Volume(Volume::Slide((value != 0).then_some( - if value >= 16 { - convert_volume(value / 16) - } else { - -convert_volume(value) - }, - ))), - 0x0F => PatternEffect::GlobalVolume(convert_volume(value)), - 0x21 => PatternEffect::NoteDelay(value as usize), + 0x0D => E::PatternBreak, + 0x0B => E::PatternJump(value as usize), + 0x0C => E::Volume(Volume::Set(convert_volume(value))), + 0x0A => E::Volume(Volume::Slide((value != 0).then_some(if value >= 16 { + convert_volume(value / 16) + } else { + -convert_volume(value) + }))), + 0x1E => E::Volume(Volume::Bump { + up: true, + volume: (value != 0).then_some(convert_volume(value)), + }), + 0x1F => E::Volume(Volume::Bump { + up: false, + volume: (value != 0).then_some(-convert_volume(value)), + }), + 0x0F => E::GlobalVolume(convert_volume(value)), + 0x21 => E::NoteDelay(value as usize), // TODO(nenikitov): Remove dummy effect 0x00 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x22 | 0x2E | 0x2F - | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => PatternEffect::Dummy(kind), + | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => E::Dummy(kind), // TODO(nenikitov): Add support for other effects // 0x00 => Self::Arpegio, // 0x01 => Self::PortaUp, @@ -225,10 +234,11 @@ impl AssetParser for Option { // 0x33 => Self::SoundControlQuad, // 0x34 => Self::FilterGlobal, // 0x35 => Self::FilterLocal, - 0x36 => PatternEffect::PlaybackDirection(PlaybackDirection::Forwards), - 0x37 => PatternEffect::PlaybackDirection(PlaybackDirection::Backwards), + 0x36 => E::PlaybackDirection(PlaybackDirection::Forwards), + 0x37 => E::PlaybackDirection(PlaybackDirection::Backwards), // TODO(nenikitov): Should be a `Result` - kind => unreachable!("Effect is outside the range {kind}"), + 0x0..=0x37 => todo!("Ashen effect {kind} should have been implemented"), + _ => unreachable!("Effect is outside the range {kind}"), }), )) } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 963aaf2..38a2ba2 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0x1; + let i = 0xC; let song = sounds .iter() .filter_map(|s| match s { @@ -92,7 +92,7 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; - dbg!(song); + //dbg!(song); } sounds From b88841dcfdd0e75181b139383c06af656122aeda Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 7 Oct 2024 09:05:01 -0400 Subject: [PATCH 67/87] feat(effect): pattern jump --- engine/src/asset/sound/dat/mixer_new.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index a7c8a59..20f5748 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -297,7 +297,7 @@ struct Player<'a> { pos_pattern: usize, pos_row: usize, pos_tick: usize, - jump: Option<(usize, usize)>, + jump: Option, tempo: usize, bpm: usize, @@ -424,9 +424,12 @@ impl<'a> Player<'a> { } fn row(&mut self) { - if let Some((pos_pattern, pos_row)) = self.jump.take() { + if let Some(pos_pattern) = self.jump.take() { + if pos_pattern <= self.pos_pattern { + self.pos_loop += 1; + } self.pos_pattern = pos_pattern; - self.pos_row = pos_row; + self.pos_row = 0; } let Some(row) = self @@ -470,14 +473,10 @@ impl<'a> Player<'a> { self.tempo = ticks_per_row; } E::PatternBreak => { - self.jump = Some((self.pos_pattern + 1, 0)); + self.jump = Some(self.pos_pattern + 1); } - E::PatternJump(position) => { - println!( - "On pat {pat:x} row {row:x} -> {position:x}", - pat = self.pos_pattern, - row = self.pos_row - ) + E::PatternJump(pos_pattern) => { + self.jump = Some(pos_pattern); } E::GlobalVolume(volume) => { self.volume_global = volume; From 56aea46f6690a5a5f805a3147d101b8e25448776 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 7 Oct 2024 09:12:06 -0400 Subject: [PATCH 68/87] feat: ramping for global volume --- engine/src/asset/sound/dat/mixer_new.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 20f5748..0863bf9 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -301,7 +301,8 @@ struct Player<'a> { tempo: usize, bpm: usize, - volume_global: f32, + volume_global_target: f32, + volume_global_actual: f32, volume_amplification: f32, channels: Vec, @@ -320,7 +321,8 @@ impl<'a> Player<'a> { jump: None, tempo: song.speed as usize, bpm: song.bpm as usize, - volume_global: 1., + volume_global_target: 1., + volume_global_actual: 0., volume_amplification: 0.25, channels: (0..song.orders[0][0].len()) .map(|_| PlayerChannel::default()) @@ -343,7 +345,12 @@ impl<'a> Player<'a> { //.enumerate() //.filter_map(|(i, s)| (i == 0).then_some(s)) .sum::(); - S::from_normalized_f32(sample * self.volume_global * self.volume_amplification) + self.volume_global_actual = advance_to( + self.volume_global_actual, + self.volume_global_target, + PlayerChannel::MAX_VOLUME_CHANGE, + ); + S::from_normalized_f32(sample * self.volume_global_actual * self.volume_amplification) } fn tick(&mut self) { @@ -479,7 +486,7 @@ impl<'a> Player<'a> { self.jump = Some(pos_pattern); } E::GlobalVolume(volume) => { - self.volume_global = volume; + self.volume_global_target = volume; } E::Volume(Volume::Set(volume)) => { channel.volume = volume; From 2348563e525ed65077d3596ea6d4c3aaa8711307 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 7 Oct 2024 11:22:27 -0400 Subject: [PATCH 69/87] feat(effect): note retrigger --- engine/src/asset/sound/dat/mixer_new.rs | 34 +++++++++++++++----- engine/src/asset/sound/dat/pattern_effect.rs | 2 ++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs index 0863bf9..307a9bc 100644 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ b/engine/src/asset/sound/dat/mixer_new.rs @@ -62,6 +62,7 @@ struct PlayerChannel { effects: [Option; 2], effects_memory: HashMap, note_delay: usize, + retrigger: bool, previous: Option<(Box, f64)>, @@ -309,7 +310,7 @@ struct Player<'a> { } impl<'a> Player<'a> { - fn new(song: &'a TSong, sample_rate: usize) -> Self { + fn new(song: &'a TSong, sample_rate: usize, amplification: f32) -> Self { Self { song, sample_rate, @@ -323,7 +324,7 @@ impl<'a> Player<'a> { bpm: song.bpm as usize, volume_global_target: 1., volume_global_actual: 0., - volume_amplification: 0.25, + volume_amplification: amplification, channels: (0..song.orders[0][0].len()) .map(|_| PlayerChannel::default()) .collect(), @@ -388,6 +389,14 @@ impl<'a> Player<'a> { E::NoteDelay(_) => { channel.note_delay = channel.note_delay.saturating_sub(1); } + E::RetriggerNote(frequency) => { + if frequency != 0 && self.pos_tick != 0 && (self.pos_tick % frequency) == 0 + { + // HACK(nenikitov): After processing all effects, retrigger will happen by callign `trigger_note` and `advance_envelopes` + // Because of mutability here + channel.retrigger = true; + } + } // Noops - no tick E::Speed(..) | E::PatternBreak @@ -408,6 +417,13 @@ impl<'a> Player<'a> { } } } + + // TODO(nenikitov): Move this to the event `matchc + if channel.retrigger { + channel.trigger_note(); + channel.advance_envelopes(); + channel.retrigger = false; + } } self.pos_tick += 1; @@ -519,12 +535,13 @@ impl<'a> Player<'a> { channel.note_delay = delay; } // Noops - no init - E::Volume(Volume::Slide(..)) => {} - E::Porta(Porta::Tone(..)) => {} - E::Porta(Porta::Slide { .. }) => {} + E::Volume(Volume::Slide(..)) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Slide { .. }) + | E::RetriggerNote(..) => {} // TODO(nenikitov): To implement E::Dummy(code) => { - //println!("{code:x}"); + println!("{code:x}"); } // Unreachable because memory has to be initialized E::Volume(Volume::Bump { volume: None, .. }) @@ -545,9 +562,10 @@ pub trait TSongMixerNew { impl TSongMixerNew for TSong { fn mix_new(&self) -> Sample { - const SAMPLE_RATE: usize = 16000; + const SAMPLE_RATE: usize = 48000; + const AMPLIFICATION: f32 = 0.3125; - let mut player = Player::new(self, SAMPLE_RATE); + let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); let samples: Vec<_> = std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 975ab3e..28a7c3f 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -67,6 +67,7 @@ pub enum PatternEffect { NoteDelay(usize), PatternBreak, PatternJump(usize), + RetriggerNote(usize), } impl PatternEffect { @@ -181,6 +182,7 @@ impl AssetParser for Option { }), 0x0D => E::PatternBreak, 0x0B => E::PatternJump(value as usize), + 0x1D => E::RetriggerNote(value as usize), 0x0C => E::Volume(Volume::Set(convert_volume(value))), 0x0A => E::Volume(Volume::Slide((value != 0).then_some(if value >= 16 { convert_volume(value / 16) From b988e8b4ba3d9ebd9206578bf7a15b605146a4c7 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Mon, 7 Oct 2024 12:05:02 -0400 Subject: [PATCH 70/87] refactor: remove old mixer and dummy effect --- engine/src/asset/sound/dat/mixer.rs | 820 ++++++++++++------- engine/src/asset/sound/dat/mixer_new.rs | 580 ------------- engine/src/asset/sound/dat/mod.rs | 1 - engine/src/asset/sound/dat/pattern_effect.rs | 44 +- engine/src/asset/sound/dat/pattern_event.rs | 10 +- engine/src/asset/sound/mod.rs | 24 +- 6 files changed, 535 insertions(+), 944 deletions(-) delete mode 100644 engine/src/asset/sound/dat/mixer_new.rs diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 00c7364..304e319 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -1,376 +1,574 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{ + collections::HashMap, + ops::{Add, Sub}, + rc::Rc, + time::Duration, +}; + +use itertools::Itertools; -use super::{pattern_effect::*, pattern_event::*, t_instrument::*, t_song::*}; -use crate::asset::sound::{ - dat::finetune::FineTune, - sample::{Interpolation, Sample, SampleDataProcessing, SamplePointProcessing}, +use super::{ + finetune::FineTune, pattern_effect::*, pattern_event::*, t_instrument::*, t_song::TSong, }; +use crate::asset::sound::sample::Sample; -const GLOBAL_VOLUME: f32 = 0.3125; +trait AudioSamplePoint { + type Bytes: IntoIterator; -pub trait TSongMixer { - fn mix(&self, restart: bool) -> Sample; + fn into_normalized_f32(&self) -> f32; + fn from_normalized_f32(value: f32) -> Self; + fn into_bytes(&self) -> Self::Bytes; } -impl TSongMixer for TSong { - fn mix(&self, restart: bool) -> Sample { - TSongMixerUtils::mix( - self, - if restart { - self.restart_order as usize - } else { - 0 - }, - ) +impl AudioSamplePoint for i16 { + type Bytes = [u8; 2]; + + fn into_normalized_f32(&self) -> f32 { + if *self < 0 { + -(*self as f32 / Self::MIN as f32) + } else { + (*self as f32 / Self::MAX as f32) + } + } + + fn from_normalized_f32(value: f32) -> Self { + if value < 0. { + -(value * i16::MIN as f32) as i16 + } else { + (value * i16::MAX as f32) as i16 + } } + + fn into_bytes(&self) -> Self::Bytes { + self.to_le_bytes() + } +} + +#[derive(Default, Clone, Debug)] +struct PlayerChannelNote { + finetune: Option, + finetune_initial: Option, + on: bool, } -trait TSongMixerUtils { - const SAMPLE_RATE: usize = 16_000; +#[derive(Default, Clone)] +struct PlayerChannel { + instrument: Option>, + sample: Option>, + note: PlayerChannelNote, + volume: f32, + volume_target: f32, + volume_actual: f32, + effects: [Option; 2], + effects_memory: HashMap, + note_delay: usize, + retrigger: bool, - fn mix(&self, start: usize) -> Sample; + previous: Option<(Box, f64)>, - fn seconds_per_row(bpm: usize, speed: usize) -> f32; + pos_sample: f64, + pos_volume_envelope: usize, + direction: PlaybackDirection, } -impl TSongMixerUtils for TSong { - fn mix(&self, start: usize) -> Sample { - let mut song = Sample::mono(Self::SAMPLE_RATE); - - let mut channels: Vec<_> = (0..self.patterns[0][0].len()) - .map(|_| Channel::default()) - .collect(); - - let mut offset = 0; - let mut sample_length_fractional = 0.0; - let mut bpm = self.bpm as usize; - let mut speed = self.speed as usize; - let mut volume_global = 1.0; - - // TODO!(nenikitov): Remove all `enumerate` - for (p, pattern) in self.orders[start..].iter().enumerate() { - for (r, row) in pattern.iter().enumerate() { - // Update channels - for (c, (event, channel)) in - Iterator::zip(row.iter(), channels.iter_mut()).enumerate() - { - // Process note - if let Some(note) = event.note { - channel.change_note(note); - } - if let Some(instrument) = &event.instrument { - channel.change_instrument(instrument); - } - if let Some(volume) = event.volume { - channel.change_volume(volume); - } +impl PlayerChannel { + // Length of a transition between the current and the next sample in seconds + // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. + // Too little and transitions between notes will click. + // Chosen value is a bit of an arbitrary value that I found sounds nice. + // It amounts to: + // - 13 samples at 16000 + // - 35 samples at 44100 + // - 38 samples at 48000 + const SAMPLE_BLEND: f64 = Duration::from_micros(800).as_secs_f64(); + // Maximum difference in volume between 2 audio samples + // Volume as in channels volume, does not account for samples + // A bit of an arbitrary amount too + const MAX_VOLUME_CHANGE: f32 = 1. / 128.; + + fn note_cut(&mut self) { + self.volume = 0.; + self.volume_actual = 0.; + } - let effects: Vec<_> = event - .effects - .iter() - .enumerate() - .filter_map(|(i, effect)| { - if let Some(effect) = effect { - channel.change_effect(i, *effect); - Some(i) - } else { - None - } - }) - .collect(); + fn pos_reset(&mut self) { + self.pos_sample = 0.; + self.pos_volume_envelope = 0; + self.direction = PlaybackDirection::Forwards; + } - // Init effects - // Efffects from now on have their memory initialized - for effect in effects.into_iter().map(|e| { - channel.effects[e].expect("effect is initialized after assignment") - }) { - match effect { - PatternEffect::Dummy(_) => {} - PatternEffect::Speed(Speed::Bpm(s)) => { - bpm = s; - } - PatternEffect::Speed(Speed::TicksPerRow(s)) => { - speed = s; - } - PatternEffect::Porta(Porta::Bump { - finetune: Some(finetune), - .. - }) => { - if let Some(note) = &mut channel.note { - note.finetune += finetune; - } - } - PatternEffect::Volume(Volume::Set(volume)) => { - channel.volume = volume; - } - PatternEffect::Volume(Volume::Bump { - volume: Some(volume), - .. - }) => { - channel.volume += volume; - } - PatternEffect::SampleOffset(Some(offset)) => { - channel.sample_position = offset; - } - PatternEffect::PlaybackDirection(PlaybackDirection::Forwards) => { - channel.playback_direction = PlaybackDirection::Forwards - } - PatternEffect::PlaybackDirection(PlaybackDirection::Backwards) => { - channel.playback_direction = PlaybackDirection::Backwards; - if channel.sample_position == 0 - && let Some((_, _, sample)) = - channel.get_note_instrument_sample() - { - channel.sample_position = sample.sample_beginning().len() - + sample.sample_loop().len(); - } - } - PatternEffect::GlobalVolume(volume) => { - volume_global = volume; - } - PatternEffect::Porta(Porta::Tone(None)) - | PatternEffect::Porta(Porta::Slide { finetune: None, .. }) - | PatternEffect::Porta(Porta::Bump { finetune: None, .. }) - | PatternEffect::Volume(Volume::Slide(None)) - | PatternEffect::Volume(Volume::Bump { volume: None, .. }) - | PatternEffect::SampleOffset(None) => { - unreachable!("effect {effect:?} ({:?}) at ({p} {r} {c}) memory should already be initialized", effect.memory_key()) - } - _ => {} - }; - } + fn generate_sample(&mut self, step: f64) -> T { + let current_sample = if let Some(instrument) = &self.instrument + && let Some(sample) = &self.sample + && let Some(note) = self.note.finetune + && self.note_delay == 0 + && let Some(value) = sample.get(self.pos_sample) + { + let pitch_factor = (note + sample.finetune).pitch_factor(); + let step = step / pitch_factor; + self.pos_sample += match self.direction { + PlaybackDirection::Forwards => step, + PlaybackDirection::Backwards => -step, + }; - // Process repeatable effects - for effect in channel.effects.iter().flatten() { - match effect { - PatternEffect::Porta(Porta::Tone(Some(step))) => { - if let Some(note) = &mut channel.note { - note.finetune = match note.finetune.cmp(¬e.finetune_initial) - { - std::cmp::Ordering::Less => FineTune::min( - note.finetune + *step, - note.finetune_initial, - ), - std::cmp::Ordering::Greater => FineTune::max( - note.finetune - *step, - note.finetune_initial, - ), - std::cmp::Ordering::Equal => note.finetune, - } - } - } - PatternEffect::Porta(Porta::Slide { - finetune: Some(finetune), - .. - }) => { - if let Some(note) = &mut channel.note { - note.finetune += *finetune; - } - } - PatternEffect::Volume(Volume::Slide(Some(volume))) => { - channel.volume += volume; - } - _ => {} - } + let volume_envelope = match &instrument.volume { + TInstrumentVolume::Envelope(envelope) => { + if self.note.on { + envelope + .volume_beginning() + .get(self.pos_volume_envelope) + .cloned() + .unwrap_or(envelope.volume_loop()) + } else { + envelope + .volume_end() + .get( + self.pos_volume_envelope + .saturating_sub(envelope.volume_beginning().len()), + ) + .cloned() + .unwrap_or(0.) } } + TInstrumentVolume::Constant(_) => 1., + }; + self.volume_target = volume_envelope * self.volume; + self.volume_actual = advance_to( + self.volume_actual, + self.volume_target, + Self::MAX_VOLUME_CHANGE, + ); - // Mix current row - let sample_length = Self::seconds_per_row(bpm, speed) * Self::SAMPLE_RATE as f32 - + sample_length_fractional; - sample_length_fractional = sample_length - sample_length.floor(); - let sample_length = sample_length as usize; - let tick_length = sample_length / speed; - - for (i, c) in channels.iter_mut().enumerate() { - for j in 0..speed { - let offset = offset + j * tick_length; - let tick_length = if (j + 1) != speed { - tick_length - } else { - sample_length - j * tick_length - }; - - let data = c.tick(tick_length, volume_global); - song.data.add_sample(&data, offset); - } - } + value.into_normalized_f32() * self.volume_actual + } else { + 0. + }; + + let current_sample = if let Some((previous, position)) = &mut self.previous { + let factor = (*position / Self::SAMPLE_BLEND) as f32; + let previous_sample = previous.generate_sample::(step).into_normalized_f32(); - // Advance to next tick - offset += sample_length; + *position += step; + if *position >= Self::SAMPLE_BLEND { + self.previous = None } - } - song - } + previous_sample + factor * (current_sample - previous_sample) + } else { + current_sample + }; - fn seconds_per_row(bpm: usize, speed: usize) -> f32 { - // TODO(nenikitov): The formula from the game is `5 / 2 / BPM * SPEED`, not sure why - 2.5 / bpm as f32 * speed as f32 + T::from_normalized_f32(current_sample) } -} -#[derive(Clone)] -struct ChannelNote { - finetune: FineTune, - finetune_initial: FineTune, - on: bool, -} + fn trigger_note(&mut self) { + // Previous state is kept to subtly blend in notes to remove clicks. -#[derive(Default)] -struct Channel<'a> { - instrument: Option<&'a Option>>, - note: Option, - effects: [Option; 2], - effects_memory: HashMap, + // Disregard previous state before `self.clone` so we don't have a fully recursive structure. + self.previous = None; + self.previous = Some((Box::new(self.clone()), 0.)); - sample_position: usize, + self.pos_reset(); + } - volume: f32, - volume_last: f32, - volume_envelope_position: usize, + fn change_instrument(&mut self, instrument: Option>) { + if let Some(instrument) = instrument { + self.instrument = Some(instrument); + } - playback_direction: PlaybackDirection, -} + if self.instrument.is_some() { + self.trigger_note(); + } else { + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); + self.instrument = None; + self.sample = None; + } + } -impl<'a> Channel<'a> { fn change_note(&mut self, note: PatternEventNote) { - match (&mut self.note, note) { - (None, PatternEventNote::Off) => { - self.note = None; - } - (None, PatternEventNote::On(target)) => { - self.note = Some(ChannelNote { - finetune: target, - finetune_initial: target, - on: true, - }); - } - (Some(current), PatternEventNote::Off) => { - current.on = false; - self.volume_envelope_position = 0; - } - (Some(current), PatternEventNote::On(target)) => { - current.finetune = target; - current.on = true; + if let Some(instrument) = &self.instrument { + match note { + PatternEventNote::Off => { + self.note.on = false; + } + PatternEventNote::On(note) => { + self.note.finetune = Some(note); + self.note.finetune_initial = Some(note); + self.note.on = true; + self.sample = instrument.samples[note.note() as usize].clone(); + } } + } else { + // TODO(nenikitov): Idk honestly, figure this out + self.note_cut(); } } - fn change_instrument(&mut self, instrument: &'a Option>) { - self.instrument = Some(instrument); - self.sample_position = 0; - self.volume_envelope_position = 0; - self.playback_direction = PlaybackDirection::Forwards; - } - fn change_volume(&mut self, volume: PatternEventVolume) { self.volume = match volume { PatternEventVolume::Sample => { - if let Some((_, _, sample)) = self.get_note_instrument_sample() { + if let Some(sample) = &self.sample { sample.volume } else { - 0.0 + self.note_cut(); + return; } } + PatternEventVolume::Value(volume) => volume, + }; + } + + fn change_effect(&mut self, i: usize, effect: PatternEffect) { + // Recall from memory + let effect = if let Some(key) = effect.memory_key() { + if !effect.is_empty() { + self.effects_memory.insert(key, effect); + } - PatternEventVolume::Value(value) => value, + self.effects_memory[&key] + } else { + effect }; + self.effects[i] = Some(effect); } - fn get_note_instrument_sample(&self) -> Option<(&ChannelNote, &Rc, &Rc)> { - if let Some(note) = &self.note - && let Some(Some(instrument)) = self.instrument - && let Some(sample) = &instrument.samples[note.finetune.note().clamp(0, 95) as usize] + fn advance_envelopes(&mut self) { + if let Some(instrument) = &self.instrument + && let TInstrumentVolume::Envelope(envelope) = &instrument.volume { - Some((note, instrument, sample)) - } else { - None + if (self.note.on + && if let Some(sustain) = envelope.sustain { + self.pos_volume_envelope < sustain + } else { + true + }) + || !self.note.on + { + self.pos_volume_envelope += 1; + } } } - fn tick(&mut self, duration: usize, volume_global: f32) -> Vec<[i16; 1]> { - if let Some((note, instrument, sample)) = self.get_note_instrument_sample() { - // Generate data - // TODO(nenikitov): If `volume_envelope` is `0`, this means that the sample already finished playing - // and there is no reason to keep `note.on`. - let (volume_instrument, should_increment_volume_envelope) = match &instrument.volume { - TInstrumentVolume::Envelope(envelope) => { - let (envelope, default) = if note.on { - (envelope.volume_beginning(), envelope.volume_loop()) - } else { - (envelope.volume_end(), 0.0) - }; - let envelope = envelope - .get(self.volume_envelope_position) - .map(ToOwned::to_owned); + fn clear_effects(&mut self) { + self.effects = [None, None]; + } +} - (envelope.unwrap_or(default), envelope.is_some()) +trait MinMax { + fn generic_min(a: Self, b: Self) -> Self; + fn generic_max(a: Self, b: Self) -> Self; +} + +macro_rules! impl_min_max { + ($($ty:tt),*) => { + $(impl MinMax for $ty { + fn generic_min(a: Self, b: Self) -> Self { + $ty::min(a, b) + } + + fn generic_max(a: Self, b: Self) -> Self { + $ty::max(a, b) + } + })* + }; +} + +impl_min_max!(f32, FineTune); + +fn advance_to(from: T, to: T, step: T) -> T +where + T: PartialOrd + Add + Sub + MinMax, +{ + use std::cmp::Ordering; + match from.partial_cmp(&to) { + Some(Ordering::Less) => T::generic_min(from + step, to), + Some(Ordering::Greater) => T::generic_max(from - step, to), + Some(Ordering::Equal) | None => from, + } +} + +struct Player<'a> { + song: &'a TSong, + sample_rate: usize, + + time_in_tick: f64, + + pos_loop: usize, + pos_pattern: usize, + pos_row: usize, + pos_tick: usize, + jump: Option, + + tempo: usize, + bpm: usize, + volume_global_target: f32, + volume_global_actual: f32, + volume_amplification: f32, + + channels: Vec, +} + +impl<'a> Player<'a> { + fn new(song: &'a TSong, sample_rate: usize, amplification: f32) -> Self { + Self { + song, + sample_rate, + time_in_tick: 0., + pos_loop: 0, + pos_pattern: 0, + pos_row: 0, + pos_tick: 0, + jump: None, + tempo: song.speed as usize, + bpm: song.bpm as usize, + volume_global_target: 1., + volume_global_actual: 0., + volume_amplification: amplification, + channels: (0..song.orders[0][0].len()) + .map(|_| PlayerChannel::default()) + .collect(), + } + } + + fn generate_sample(&mut self) -> S { + if self.time_in_tick <= 0. { + self.tick(); + } + let step = 1. / self.sample_rate as f64; + self.time_in_tick -= step; + + let sample = self + .channels + .iter_mut() + .map(|c| c.generate_sample::(step)) + .map(|c| c.into_normalized_f32()) + //.enumerate() + //.filter_map(|(i, s)| (i == 0).then_some(s)) + .sum::(); + self.volume_global_actual = advance_to( + self.volume_global_actual, + self.volume_global_target, + PlayerChannel::MAX_VOLUME_CHANGE, + ); + S::from_normalized_f32(sample * self.volume_global_actual * self.volume_amplification) + } + + fn tick(&mut self) { + if self.pos_tick == 0 { + self.row(); + } + + for channel in self.channels.iter_mut() { + channel.advance_envelopes(); + + for effect in channel.effects.iter().flatten() { + use PatternEffect as E; + match *effect { + // Tick effects + E::Volume(Volume::Slide(Some(volume))) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Tone(Some(step))) => { + if let Some(finetune_initial) = channel.note.finetune_initial { + channel.note.finetune = channel + .note + .finetune + .map(|finetune| advance_to(finetune, finetune_initial, step)); + } + } + E::Porta(Porta::Slide { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel + .note + .finetune + .map(|f| (f + finetune).clamp(FineTune::new(0), FineTune::new(15488))); + } + E::NoteDelay(_) => { + channel.note_delay = channel.note_delay.saturating_sub(1); + } + E::RetriggerNote(frequency) => { + if frequency != 0 && self.pos_tick != 0 && (self.pos_tick % frequency) == 0 + { + // HACK(nenikitov): After processing all effects, retrigger will happen by callign `trigger_note` and `advance_envelopes` + // Because of mutability here + channel.retrigger = true; + } + } + // Noops - no tick + E::Speed(..) + | E::PatternBreak + | E::PatternJump(..) + | E::Volume(Volume::Set(..)) + | E::Volume(Volume::Bump { .. }) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Bump { .. }) + | E::GlobalVolume(..) + | E::SampleOffset(..) + | E::PlaybackDirection(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Slide(None)) + | E::Porta(Porta::Slide { finetune: None, .. }) => { + unreachable!("Effects should have their memory initialized") + } } - TInstrumentVolume::Constant(volume) => (*volume, false), - }; - let volume = - (GLOBAL_VOLUME * volume_global * volume_instrument * self.volume).clamp(0.0, 4.0); - - let pitch_factor = (note.finetune + sample.finetune).pitch_factor(); - let duration_scaled = (duration as f64 / pitch_factor).round() as usize; - let mut sample = match self.playback_direction { - PlaybackDirection::Forwards => sample - .sample_beginning() - .iter() - .chain(sample.sample_loop().iter().cycle()) - .skip(self.sample_position) - .take(duration_scaled + 1) - .copied() - .collect::>(), - PlaybackDirection::Backwards => sample - .sample_beginning() - .iter() - .chain(sample.sample_loop()) - .rev() - .skip(self.sample_position) - .take(duration_scaled + 1) - .copied() - .collect::>(), - }; - let first_sample_after = sample.pop(); - let pitch_factor = duration as f32 / sample.len() as f32; - - let mut data = sample.volume_range(self.volume_last..volume).stretch( - //let mut data = sample.volume(volume).stretch( - pitch_factor, - Interpolation::Linear { - first_sample_after: first_sample_after.map(|s| s.volume(volume)), - }, - ); + } - // Update - self.sample_position += duration_scaled; - if should_increment_volume_envelope { - self.volume_envelope_position += 1; + // TODO(nenikitov): Move this to the event `matchc + if channel.retrigger { + channel.trigger_note(); + channel.advance_envelopes(); + channel.retrigger = false; } - self.volume_last = volume; + } - // Return - data - } else { - vec![] + self.pos_tick += 1; + + if self.pos_tick >= self.tempo { + self.pos_tick = 0; + self.pos_row += 1; } + if let Some(pattern) = self.song.orders.get(self.pos_pattern) + && self.pos_row >= pattern.len() + { + self.pos_row = 0; + self.pos_pattern += 1; + }; + if self.pos_pattern >= self.song.orders.len() { + self.pos_pattern = self.song.restart_order as usize; + self.pos_loop += 1; + } + + self.time_in_tick += 2.5 / (self.bpm as f64); } - pub fn recall_effect_with_memory(&mut self, effect: PatternEffect) -> PatternEffect { - if let Some(key) = effect.memory_key() { - if !effect.is_empty() { - self.effects_memory.insert(key, effect); + fn row(&mut self) { + if let Some(pos_pattern) = self.jump.take() { + if pos_pattern <= self.pos_pattern { + self.pos_loop += 1; } + self.pos_pattern = pos_pattern; + self.pos_row = 0; + } - self.effects_memory[&key] - } else { - effect + let Some(row) = self + .song + .orders + .get(self.pos_pattern) + .and_then(|p| p.get(self.pos_row)) + else { + return; + }; + + for (channel, event) in self.channels.iter_mut().zip_eq(row) { + if let Some(instrument) = &event.instrument { + channel.change_instrument(instrument.clone()); + } + + if let Some(note) = &event.note { + channel.change_note(note.clone()); + } + + if let Some(volume) = &event.volume { + channel.change_volume(volume.clone()); + } + + channel.clear_effects(); + for (i, effect) in event + .effects + .iter() + .enumerate() + .filter_map(|(i, e)| e.map(|e| (i, e))) + { + channel.change_effect(i, effect.clone()); + + use PatternEffect as E; + match channel.effects[i].unwrap() { + // Init effects + E::Speed(Speed::Bpm(bpm)) => { + self.bpm = bpm; + } + E::Speed(Speed::TicksPerRow(ticks_per_row)) => { + self.tempo = ticks_per_row; + } + E::PatternBreak => { + self.jump = Some(self.pos_pattern + 1); + } + E::PatternJump(pos_pattern) => { + self.jump = Some(pos_pattern); + } + E::GlobalVolume(volume) => { + self.volume_global_target = volume; + } + E::Volume(Volume::Set(volume)) => { + channel.volume = volume; + } + E::Volume(Volume::Bump { + volume: Some(volume), + .. + }) => { + channel.volume = (channel.volume + volume).clamp(0., 1.); + } + E::Porta(Porta::Bump { + finetune: Some(finetune), + .. + }) => { + channel.note.finetune = channel.note.finetune.map(|f| f + finetune); + } + E::PlaybackDirection(direction) => { + channel.direction = direction; + if let Some(sample) = &channel.sample + && direction == PlaybackDirection::Backwards + { + channel.pos_sample = sample.data.len_seconds() as f64 + } + } + E::SampleOffset(Some(offset)) => { + // TODO(nenikitov): Remove this hardcoded value + channel.pos_sample = 1. / 16_000. * offset as f64; + } + E::NoteDelay(delay) => { + channel.note_delay = delay; + } + // Noops - no init + E::Volume(Volume::Slide(..)) + | E::Porta(Porta::Tone(..)) + | E::Porta(Porta::Slide { .. }) + | E::RetriggerNote(..) => {} + // Unreachable because memory has to be initialized + E::Volume(Volume::Bump { volume: None, .. }) + | E::Porta(Porta::Tone(None)) + | E::Porta(Porta::Bump { finetune: None, .. }) + | E::SampleOffset(None) => { + unreachable!("Effects should have their memory initialized") + } + } + } } } +} - fn change_effect(&mut self, i: usize, effect: PatternEffect) { - self.effects[i] = Some(self.recall_effect_with_memory(effect)); +pub trait TSongMixer { + fn mix(&self) -> Sample; +} + +impl TSongMixer for TSong { + fn mix(&self) -> Sample { + const SAMPLE_RATE: usize = 48000; + const AMPLIFICATION: f32 = 0.3125; + + let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); + + let samples: Vec<_> = + std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) + .map(|s| [s]) + .collect(); + + Sample { + data: samples, + sample_rate: player.sample_rate, + } } } diff --git a/engine/src/asset/sound/dat/mixer_new.rs b/engine/src/asset/sound/dat/mixer_new.rs deleted file mode 100644 index 307a9bc..0000000 --- a/engine/src/asset/sound/dat/mixer_new.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::{ - collections::HashMap, - ops::{Add, Sub}, - rc::Rc, - time::Duration, -}; - -use itertools::Itertools; - -use super::{ - finetune::FineTune, pattern_effect::*, pattern_event::*, t_instrument::*, t_song::TSong, -}; -use crate::asset::sound::sample::Sample; - -trait AudioSamplePoint { - type Bytes: IntoIterator; - - fn into_normalized_f32(&self) -> f32; - fn from_normalized_f32(value: f32) -> Self; - fn into_bytes(&self) -> Self::Bytes; -} - -impl AudioSamplePoint for i16 { - type Bytes = [u8; 2]; - - fn into_normalized_f32(&self) -> f32 { - if *self < 0 { - -(*self as f32 / Self::MIN as f32) - } else { - (*self as f32 / Self::MAX as f32) - } - } - - fn from_normalized_f32(value: f32) -> Self { - if value < 0. { - -(value * i16::MIN as f32) as i16 - } else { - (value * i16::MAX as f32) as i16 - } - } - - fn into_bytes(&self) -> Self::Bytes { - self.to_le_bytes() - } -} - -#[derive(Default, Clone, Debug)] -struct PlayerChannelNote { - finetune: Option, - finetune_initial: Option, - on: bool, -} - -#[derive(Default, Clone)] -struct PlayerChannel { - instrument: Option>, - sample: Option>, - note: PlayerChannelNote, - volume: f32, - volume_target: f32, - volume_actual: f32, - effects: [Option; 2], - effects_memory: HashMap, - note_delay: usize, - retrigger: bool, - - previous: Option<(Box, f64)>, - - pos_sample: f64, - pos_volume_envelope: usize, - direction: PlaybackDirection, -} - -impl PlayerChannel { - // Length of a transition between the current and the next sample in seconds - // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. - // Too little and transitions between notes will click. - // Chosen value is a bit of an arbitrary value that I found sounds nice. - // It amounts to: - // - 13 samples at 16000 - // - 35 samples at 44100 - // - 38 samples at 48000 - const SAMPLE_BLEND: f64 = Duration::from_micros(800).as_secs_f64(); - // Maximum difference in volume between 2 audio samples - // Volume as in channels volume, does not account for samples - // A bit of an arbitrary amount too - const MAX_VOLUME_CHANGE: f32 = 1. / 128.; - - fn note_cut(&mut self) { - self.volume = 0.; - self.volume_actual = 0.; - } - - fn pos_reset(&mut self) { - self.pos_sample = 0.; - self.pos_volume_envelope = 0; - self.direction = PlaybackDirection::Forwards; - } - - fn generate_sample(&mut self, step: f64) -> T { - let current_sample = if let Some(instrument) = &self.instrument - && let Some(sample) = &self.sample - && let Some(note) = self.note.finetune - && self.note_delay == 0 - && let Some(value) = sample.get(self.pos_sample) - { - let pitch_factor = (note + sample.finetune).pitch_factor(); - let step = step / pitch_factor; - self.pos_sample += match self.direction { - PlaybackDirection::Forwards => step, - PlaybackDirection::Backwards => -step, - }; - - let volume_envelope = match &instrument.volume { - TInstrumentVolume::Envelope(envelope) => { - if self.note.on { - envelope - .volume_beginning() - .get(self.pos_volume_envelope) - .cloned() - .unwrap_or(envelope.volume_loop()) - } else { - envelope - .volume_end() - .get( - self.pos_volume_envelope - .saturating_sub(envelope.volume_beginning().len()), - ) - .cloned() - .unwrap_or(0.) - } - } - TInstrumentVolume::Constant(_) => 1., - }; - self.volume_target = volume_envelope * self.volume; - self.volume_actual = advance_to( - self.volume_actual, - self.volume_target, - Self::MAX_VOLUME_CHANGE, - ); - - value.into_normalized_f32() * self.volume_actual - } else { - 0. - }; - - let current_sample = if let Some((previous, position)) = &mut self.previous { - let factor = (*position / Self::SAMPLE_BLEND) as f32; - let previous_sample = previous.generate_sample::(step).into_normalized_f32(); - - *position += step; - if *position >= Self::SAMPLE_BLEND { - self.previous = None - } - - previous_sample + factor * (current_sample - previous_sample) - } else { - current_sample - }; - - T::from_normalized_f32(current_sample) - } - - fn trigger_note(&mut self) { - // Previous state is kept to subtly blend in notes to remove clicks. - - // Disregard previous state before `self.clone` so we don't have a fully recursive structure. - self.previous = None; - self.previous = Some((Box::new(self.clone()), 0.)); - - self.pos_reset(); - } - - fn change_instrument(&mut self, instrument: Option>) { - if let Some(instrument) = instrument { - self.instrument = Some(instrument); - } - - if self.instrument.is_some() { - self.trigger_note(); - } else { - // TODO(nenikitov): Idk honestly, figure this out - self.note_cut(); - self.instrument = None; - self.sample = None; - } - } - - fn change_note(&mut self, note: PatternEventNote) { - if let Some(instrument) = &self.instrument { - match note { - PatternEventNote::Off => { - self.note.on = false; - } - PatternEventNote::On(note) => { - self.note.finetune = Some(note); - self.note.finetune_initial = Some(note); - self.note.on = true; - self.sample = instrument.samples[note.note() as usize].clone(); - } - } - } else { - // TODO(nenikitov): Idk honestly, figure this out - self.note_cut(); - } - } - - fn change_volume(&mut self, volume: PatternEventVolume) { - self.volume = match volume { - PatternEventVolume::Sample => { - if let Some(sample) = &self.sample { - sample.volume - } else { - self.note_cut(); - return; - } - } - PatternEventVolume::Value(volume) => volume, - }; - } - - fn change_effect(&mut self, i: usize, effect: PatternEffect) { - // Recall from memory - let effect = if let Some(key) = effect.memory_key() { - if !effect.is_empty() { - self.effects_memory.insert(key, effect); - } - - self.effects_memory[&key] - } else { - effect - }; - self.effects[i] = Some(effect); - } - - fn advance_envelopes(&mut self) { - if let Some(instrument) = &self.instrument - && let TInstrumentVolume::Envelope(envelope) = &instrument.volume - { - if (self.note.on - && if let Some(sustain) = envelope.sustain { - self.pos_volume_envelope < sustain - } else { - true - }) - || !self.note.on - { - self.pos_volume_envelope += 1; - } - } - } - - fn clear_effects(&mut self) { - self.effects = [None, None]; - } -} - -trait MinMax { - fn generic_min(a: Self, b: Self) -> Self; - fn generic_max(a: Self, b: Self) -> Self; -} - -macro_rules! impl_min_max { - ($($ty:tt),*) => { - $(impl MinMax for $ty { - fn generic_min(a: Self, b: Self) -> Self { - $ty::min(a, b) - } - - fn generic_max(a: Self, b: Self) -> Self { - $ty::max(a, b) - } - })* - }; -} - -impl_min_max!(f32, FineTune); - -fn advance_to(from: T, to: T, step: T) -> T -where - T: PartialOrd + Add + Sub + MinMax, -{ - use std::cmp::Ordering; - match from.partial_cmp(&to) { - Some(Ordering::Less) => T::generic_min(from + step, to), - Some(Ordering::Greater) => T::generic_max(from - step, to), - Some(Ordering::Equal) | None => from, - } -} - -struct Player<'a> { - song: &'a TSong, - sample_rate: usize, - - time_in_tick: f64, - - pos_loop: usize, - pos_pattern: usize, - pos_row: usize, - pos_tick: usize, - jump: Option, - - tempo: usize, - bpm: usize, - volume_global_target: f32, - volume_global_actual: f32, - volume_amplification: f32, - - channels: Vec, -} - -impl<'a> Player<'a> { - fn new(song: &'a TSong, sample_rate: usize, amplification: f32) -> Self { - Self { - song, - sample_rate, - time_in_tick: 0., - pos_loop: 0, - pos_pattern: 0, - pos_row: 0, - pos_tick: 0, - jump: None, - tempo: song.speed as usize, - bpm: song.bpm as usize, - volume_global_target: 1., - volume_global_actual: 0., - volume_amplification: amplification, - channels: (0..song.orders[0][0].len()) - .map(|_| PlayerChannel::default()) - .collect(), - } - } - - fn generate_sample(&mut self) -> S { - if self.time_in_tick <= 0. { - self.tick(); - } - let step = 1. / self.sample_rate as f64; - self.time_in_tick -= step; - - let sample = self - .channels - .iter_mut() - .map(|c| c.generate_sample::(step)) - .map(|c| c.into_normalized_f32()) - //.enumerate() - //.filter_map(|(i, s)| (i == 0).then_some(s)) - .sum::(); - self.volume_global_actual = advance_to( - self.volume_global_actual, - self.volume_global_target, - PlayerChannel::MAX_VOLUME_CHANGE, - ); - S::from_normalized_f32(sample * self.volume_global_actual * self.volume_amplification) - } - - fn tick(&mut self) { - if self.pos_tick == 0 { - self.row(); - } - - for channel in self.channels.iter_mut() { - channel.advance_envelopes(); - - for effect in channel.effects.iter().flatten() { - use PatternEffect as E; - match *effect { - // Tick effects - E::Volume(Volume::Slide(Some(volume))) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); - } - E::Porta(Porta::Tone(Some(step))) => { - if let Some(finetune_initial) = channel.note.finetune_initial { - channel.note.finetune = channel - .note - .finetune - .map(|finetune| advance_to(finetune, finetune_initial, step)); - } - } - E::Porta(Porta::Slide { - finetune: Some(finetune), - .. - }) => { - channel.note.finetune = channel - .note - .finetune - .map(|f| (f + finetune).clamp(FineTune::new(0), FineTune::new(15488))); - } - E::NoteDelay(_) => { - channel.note_delay = channel.note_delay.saturating_sub(1); - } - E::RetriggerNote(frequency) => { - if frequency != 0 && self.pos_tick != 0 && (self.pos_tick % frequency) == 0 - { - // HACK(nenikitov): After processing all effects, retrigger will happen by callign `trigger_note` and `advance_envelopes` - // Because of mutability here - channel.retrigger = true; - } - } - // Noops - no tick - E::Speed(..) - | E::PatternBreak - | E::PatternJump(..) - | E::Volume(Volume::Set(..)) - | E::Volume(Volume::Bump { .. }) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Bump { .. }) - | E::GlobalVolume(..) - | E::SampleOffset(..) - | E::PlaybackDirection(..) => {} - // TODO(nenikitov): Unemplemented - E::Dummy(..) => {} - // Unreachable because memory has to be initialized - E::Volume(Volume::Slide(None)) - | E::Porta(Porta::Slide { finetune: None, .. }) => { - unreachable!("Effects should have their memory initialized") - } - } - } - - // TODO(nenikitov): Move this to the event `matchc - if channel.retrigger { - channel.trigger_note(); - channel.advance_envelopes(); - channel.retrigger = false; - } - } - - self.pos_tick += 1; - - if self.pos_tick >= self.tempo { - self.pos_tick = 0; - self.pos_row += 1; - } - if let Some(pattern) = self.song.orders.get(self.pos_pattern) - && self.pos_row >= pattern.len() - { - self.pos_row = 0; - self.pos_pattern += 1; - }; - if self.pos_pattern >= self.song.orders.len() { - self.pos_pattern = self.song.restart_order as usize; - self.pos_loop += 1; - } - - self.time_in_tick += 2.5 / (self.bpm as f64); - } - - fn row(&mut self) { - if let Some(pos_pattern) = self.jump.take() { - if pos_pattern <= self.pos_pattern { - self.pos_loop += 1; - } - self.pos_pattern = pos_pattern; - self.pos_row = 0; - } - - let Some(row) = self - .song - .orders - .get(self.pos_pattern) - .and_then(|p| p.get(self.pos_row)) - else { - return; - }; - - for (channel, event) in self.channels.iter_mut().zip_eq(row) { - if let Some(instrument) = &event.instrument { - channel.change_instrument(instrument.clone()); - } - - if let Some(note) = &event.note { - channel.change_note(note.clone()); - } - - if let Some(volume) = &event.volume { - channel.change_volume(volume.clone()); - } - - channel.clear_effects(); - for (i, effect) in event - .effects - .iter() - .enumerate() - .filter_map(|(i, e)| e.map(|e| (i, e))) - { - channel.change_effect(i, effect.clone()); - - use PatternEffect as E; - match channel.effects[i].unwrap() { - // Init effects - E::Speed(Speed::Bpm(bpm)) => { - self.bpm = bpm; - } - E::Speed(Speed::TicksPerRow(ticks_per_row)) => { - self.tempo = ticks_per_row; - } - E::PatternBreak => { - self.jump = Some(self.pos_pattern + 1); - } - E::PatternJump(pos_pattern) => { - self.jump = Some(pos_pattern); - } - E::GlobalVolume(volume) => { - self.volume_global_target = volume; - } - E::Volume(Volume::Set(volume)) => { - channel.volume = volume; - } - E::Volume(Volume::Bump { - volume: Some(volume), - .. - }) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); - } - E::Porta(Porta::Bump { - finetune: Some(finetune), - .. - }) => { - channel.note.finetune = channel.note.finetune.map(|f| f + finetune); - } - E::PlaybackDirection(direction) => { - channel.direction = direction; - if let Some(sample) = &channel.sample - && direction == PlaybackDirection::Backwards - { - channel.pos_sample = sample.data.len_seconds() as f64 - } - } - E::SampleOffset(Some(offset)) => { - // TODO(nenikitov): Remove this hardcoded value - channel.pos_sample = 1. / 16_000. * offset as f64; - } - E::NoteDelay(delay) => { - channel.note_delay = delay; - } - // Noops - no init - E::Volume(Volume::Slide(..)) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Slide { .. }) - | E::RetriggerNote(..) => {} - // TODO(nenikitov): To implement - E::Dummy(code) => { - println!("{code:x}"); - } - // Unreachable because memory has to be initialized - E::Volume(Volume::Bump { volume: None, .. }) - | E::Porta(Porta::Tone(None)) - | E::Porta(Porta::Bump { finetune: None, .. }) - | E::SampleOffset(None) => { - unreachable!("Effects should have their memory initialized") - } - } - } - } - } -} - -pub trait TSongMixerNew { - fn mix_new(&self) -> Sample; -} - -impl TSongMixerNew for TSong { - fn mix_new(&self) -> Sample { - const SAMPLE_RATE: usize = 48000; - const AMPLIFICATION: f32 = 0.3125; - - let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); - - let samples: Vec<_> = - std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) - .map(|s| [s]) - .collect(); - - Sample { - data: samples, - sample_rate: player.sample_rate, - } - } -} diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index ca288e5..cb02826 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -11,7 +11,6 @@ pub mod pattern_event; pub mod t_effect; mod t_instrument; pub mod t_song; -pub mod mixer_new; // TODO(nenikitov): Make this falliable. fn uncompress(bytes: &[u8]) -> Vec { diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 28a7c3f..aa6c882 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -57,7 +57,6 @@ pub enum PatternEffectMemoryKey { #[derive(Debug, Clone, Copy)] pub enum PatternEffect { - Dummy(u8), Speed(Speed), Volume(Volume), Porta(Porta), @@ -142,7 +141,7 @@ impl AssetParser for Option { use PatternEffect as E; Ok(( input, - should_parse.then_some(match kind { + should_parse.then(|| match kind { 0x01 => E::Porta(Porta::Slide { up: true, finetune: (value != 0).then_some(FineTune::new(8 * value as i32)), @@ -199,48 +198,11 @@ impl AssetParser for Option { }), 0x0F => E::GlobalVolume(convert_volume(value)), 0x21 => E::NoteDelay(value as usize), - // TODO(nenikitov): Remove dummy effect - 0x00 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x0A | 0x0B | 0x0C | 0x0D - | 0x14 | 0x15 | 0x16 | 0x1D | 0x1E | 0x1F | 0x20 | 0x22 | 0x2E | 0x2F - | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 => E::Dummy(kind), - // TODO(nenikitov): Add support for other effects - // 0x00 => Self::Arpegio, - // 0x01 => Self::PortaUp, - // 0x02 => Self::PortaDown, - // 0x03 => Self::PortaTone, - // 0x04 => Self::Vibrato, - // 0x05 => Self::PortaVolume, - // 0x06 => Self::VibratoVolume, - // 0x07 => Self::Tremolo, - // 0x08 => Self::Pan, - // 0x0A => Self::VolumeSlide, - // 0x0B => Self::PositionJump, - // 0x0C => Self::Volume, - // 0x0D => Self::Break, - // 0x14 => Self::Sync, - // 0x15 => Self::PortaFineUp, - // 0x16 => Self::PortaFineDown, - // 0x1D => Self::NoteRetrigger, - // 0x1E => Self::VolumeSlideFineUp, - // 0x1F => Self::VolumeSlideFineDown, - // 0x20 => Self::NoteCut, - // 0x21 => ???, - // 0x22 => Self::PatternDelay, - // 0x24 => Self::PortaExtraFineUp, - // 0x25 => Self::PortaExtraFineDown, - // 0x2E => Self::SoundControlSurroundOn, - // 0x2F => Self::SoundControlSurroundOff, - // 0x30 => Self::SoundControlReverbOn, - // 0x31 => Self::SoundControlReverbOff, - // 0x32 => Self::SoundControlCentre, - // 0x33 => Self::SoundControlQuad, - // 0x34 => Self::FilterGlobal, - // 0x35 => Self::FilterLocal, 0x36 => E::PlaybackDirection(PlaybackDirection::Forwards), 0x37 => E::PlaybackDirection(PlaybackDirection::Backwards), // TODO(nenikitov): Should be a `Result` - 0x0..=0x37 => todo!("Ashen effect {kind} should have been implemented"), - _ => unreachable!("Effect is outside the range {kind}"), + 0x0..=0x37 => todo!("Ashen effect 0x{kind:X} should have been implemented"), + _ => unreachable!("Effect is outside the range 0x{kind:X}"), }), )) } diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 02cdc1a..0d93023 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -42,12 +42,12 @@ impl AssetParser for Option { bitflags! { #[derive(Debug, Clone, Copy)] pub struct PatternEventFlags: u8 { - const ChangeNote = 1 << 0; + const ChangeNote = 1 << 0; const ChangeInstrument = 1 << 1; - const ChangeVolume = 1 << 2; - const ChangeEffect1 = 1 << 3; - const ChangeEffect2 = 1 << 4; - const IsEmpty = 1 << 5; + const ChangeVolume = 1 << 2; + const ChangeEffect1 = 1 << 3; + const ChangeEffect2 = 1 << 4; + const IsEmpty = 1 << 5; } } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 38a2ba2..f7bf098 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,7 +1,7 @@ mod dat; pub(crate) mod sample; -use self::{dat::mixer_new::TSongMixerNew, sample::Sample}; +use self::{dat::mixer::TSongMixer, sample::Sample}; use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -19,7 +19,7 @@ pub enum Sound { impl Sound { pub fn mix(&self) -> Sample { match self { - Sound::Song(sound) => sound.mix_new(), + Sound::Song(sound) => sound.mix(), Sound::Effect(effect) => effect.mix(), } } @@ -84,7 +84,7 @@ mod tests { // TODO(nenikitov): Remove this debug code { - let i = 0xC; + let i = 0x3; let song = sounds .iter() .filter_map(|s| match s { @@ -92,17 +92,29 @@ mod tests { Sound::Effect(_) => None, }) .collect::>()[i]; + //dbg!(song); } sounds .iter() - .filter(|s| matches!(s, Sound::Song(_))) + .filter_map(|s| { + if let Sound::Song(song) = s { + Some((s, song)) + } else { + None + } + }) .enumerate() - .try_for_each(|(i, song)| { + .try_for_each(|(i, (song, t))| -> std::io::Result<()> { let file = output_dir.join(format!("{i:0>2X}.wav")); println!("# SONG {i}"); - output_file(file, song.mix().to_wave()) + output_file(file, song.mix().to_wave())?; + + let file = output_dir.join(format!("{i:0>2X}.txt")); + output_file(file, format!("{t:#?}"))?; + + Ok(()) })?; let output_dir = PathBuf::from(parsed_file_path!("sounds/effects/")); From f5d316428e927b1f0b31bf1f92a1333a86ef6328 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 8 Oct 2024 11:29:47 -0400 Subject: [PATCH 71/87] wip: removing old code --- engine/src/asset/sound/dat/mixer.rs | 13 +- engine/src/asset/sound/dat/t_effect.rs | 6 +- engine/src/asset/sound/dat/t_instrument.rs | 32 +- engine/src/asset/sound/mod.rs | 4 +- engine/src/asset/sound/sample.rs | 501 ++------------------- engine/src/utils/format.rs | 4 +- 6 files changed, 56 insertions(+), 504 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 304e319..671f7ea 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -10,7 +10,7 @@ use itertools::Itertools; use super::{ finetune::FineTune, pattern_effect::*, pattern_event::*, t_instrument::*, t_song::TSong, }; -use crate::asset::sound::sample::Sample; +use crate::asset::sound::sample::AudioBuffer; trait AudioSamplePoint { type Bytes: IntoIterator; @@ -522,7 +522,7 @@ impl<'a> Player<'a> { if let Some(sample) = &channel.sample && direction == PlaybackDirection::Backwards { - channel.pos_sample = sample.data.len_seconds() as f64 + channel.pos_sample = sample.buffer.len_seconds() as f64 } } E::SampleOffset(Some(offset)) => { @@ -551,22 +551,21 @@ impl<'a> Player<'a> { } pub trait TSongMixer { - fn mix(&self) -> Sample; + fn mix(&self) -> AudioBuffer; } impl TSongMixer for TSong { - fn mix(&self) -> Sample { + fn mix(&self) -> AudioBuffer { const SAMPLE_RATE: usize = 48000; - const AMPLIFICATION: f32 = 0.3125; + const AMPLIFICATION: f32 = 0.375; let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); let samples: Vec<_> = std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) - .map(|s| [s]) .collect(); - Sample { + AudioBuffer { data: samples, sample_rate: player.sample_rate, } diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index b32114e..08e9716 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -5,7 +5,7 @@ use super::{ uncompress, }; use crate::{ - asset::{extension::*, sound::sample::Sample, AssetParser}, + asset::{extension::*, sound::sample::AudioBuffer, AssetParser}, utils::nom::*, }; @@ -16,8 +16,8 @@ pub struct TEffect { // It should be separated impl TEffect { - pub fn mix(&self) -> Sample { - self.sample.data.clone() + pub fn mix(&self) -> AudioBuffer { + self.sample.buffer.clone() } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index ccb2c71..6d0f949 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -4,7 +4,7 @@ use bitflags::bitflags; use super::{convert_volume, finetune::FineTune}; use crate::{ - asset::{extension::*, sound::sample::Sample, AssetParser}, + asset::{extension::*, sound::sample::AudioBuffer, AssetParser}, utils::{iterator::CollectArray, nom::*}, }; @@ -206,7 +206,7 @@ pub struct TSample { pub align: u8, pub finetune: FineTune, pub loop_length: usize, - pub data: Sample, + pub buffer: AudioBuffer, } impl AssetParser for TSample { @@ -239,11 +239,8 @@ impl AssetParser for TSample { align, finetune: FineTune::new(finetune), loop_length: loop_length as usize, - data: Sample { - data: sample_data[sample_offset as usize..loop_end as usize] - .into_iter() - .map(|&s| [s]) - .collect(), + buffer: AudioBuffer { + data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), sample_rate: Self::SAMPLE_RATE, }, }, @@ -255,19 +252,6 @@ impl AssetParser for TSample { impl TSample { const SAMPLE_RATE: usize = 16_000; - pub fn sample_beginning(&self) -> &[[i16; 1]] { - &self.data[..self.data.len_seconds() - (self.loop_length as f32 / Self::SAMPLE_RATE as f32)] - } - - pub fn sample_loop(&self) -> &[[i16; 1]] { - if self.loop_length != 0 { - &self.data - [self.data.len_seconds() - (self.loop_length as f32 / Self::SAMPLE_RATE as f32)..] - } else { - &[[0; 1]] - } - } - // TODO(nenikitov): I think the whole `Sample` will need to be removed pub fn get(&self, position: f64) -> Option { if position < 0. { @@ -282,18 +266,18 @@ impl TSample { }; let next = self.normalize(position as usize + 1); - let prev = self.data[prev][0] as f32; - let next = next.map(|next| self.data[next][0] as f32).unwrap_or(0.); + let prev = self.buffer[prev] as f32; + let next = next.map(|next| self.buffer[next] as f32).unwrap_or(0.); Some((prev + frac * (next - prev)) as i16) } fn normalize(&self, position: usize) -> Option { - if position >= self.data.data.len() && self.loop_length == 0 { + if position >= self.buffer.data.len() && self.loop_length == 0 { None } else { let mut position = position; - while position >= self.data.data.len() { + while position >= self.buffer.data.len() { position -= self.loop_length; } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index f7bf098..5253dbf 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -1,7 +1,7 @@ mod dat; pub(crate) mod sample; -use self::{dat::mixer::TSongMixer, sample::Sample}; +use self::{dat::mixer::TSongMixer, sample::AudioBuffer}; use super::{extension::*, AssetParser}; use crate::{ asset::sound::dat::{ @@ -17,7 +17,7 @@ pub enum Sound { } impl Sound { - pub fn mix(&self) -> Sample { + pub fn mix(&self) -> AudioBuffer { match self { Sound::Song(sound) => sound.mix(), Sound::Effect(effect) => effect.mix(), diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 09e50ce..ddd4063 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,490 +1,59 @@ // TODO(nenikitov): Remove this test code -use std::{ - fmt::Debug, - ops::{Index, Range, RangeFrom, RangeTo}, -}; +use std::{fmt::Debug, ops::Index}; -use itertools::Itertools; - -use crate::utils::iterator::CollectArray; - -pub trait SamplePointConversions -where - Self: Sized, -{ - const SIZE_BITS: usize; - - fn into_f32(self) -> f32; - fn from_f32(value: f32) -> Self; - - fn to_integer_le_bytes(self) -> Vec; +pub trait SamplePoint: Default + Clone + Copy { + fn into_normalized_f32(&self) -> f32; + fn from_normalized_f32(value: f32) -> Self; } -macro_rules! impl_sample_point_conversions_other { - ($type: ty, $size: literal) => { - impl SamplePointConversions for $type { - const SIZE_BITS: usize = $size as usize; - - fn into_f32(self) -> f32 { - self as f32 - } - - fn from_f32(value: f32) -> Self { - value as Self - } - - fn to_integer_le_bytes(self) -> Vec { - ((self.clamp(-1.0, 1.0) * i32::MAX as f32) as i32) - .to_le_bytes() - .to_vec() - } - } - }; -} -macro_rules! impl_sample_point_conversions_integer { - ($type: tt) => { - impl SamplePointConversions for $type { - const SIZE_BITS: usize = $type::BITS as usize; - - fn into_f32(self) -> f32 { - self as f32 / $type::MAX as f32 - } - - fn from_f32(value: f32) -> Self { - (value * $type::MAX as f32) as Self +macro_rules! impl_sample_point_for { + ($($ty:ty),*) => { + $(impl SamplePoint for $ty { + fn into_normalized_f32(&self) -> f32 { + if *self < 0 { + -(*self as f32 / Self::MIN as f32) + } else { + (*self as f32 / Self::MAX as f32) + } } - fn to_integer_le_bytes(self) -> Vec { - self.to_le_bytes().to_vec() + fn from_normalized_f32(value: f32) -> Self { + if value < 0. { + -(value * Self::MIN as f32) as Self + } else { + (value * Self::MAX as f32) as Self + } } - } - }; + })* + } } -impl_sample_point_conversions_other!(f32, 32); -impl_sample_point_conversions_integer!(i16); -impl_sample_point_conversions_integer!(i32); - -pub trait SamplePoint: Copy + Clone + SamplePointConversions + Debug {} - -impl SamplePoint for i16 {} -impl SamplePoint for i32 {} -impl SamplePoint for f32 {} - -#[derive(Debug, Clone, Copy)] -pub enum Interpolation { - Nearest, - Linear { - first_sample_after: Option<[S; CHANNELS]>, - }, -} +impl_sample_point_for!(i16); #[derive(Debug, Clone)] -pub struct Sample { - pub data: Vec<[S; CHANNELS]>, +pub struct AudioBuffer { + pub data: Vec, pub sample_rate: usize, } -impl Index for Sample { - type Output = [S; CHANNELS]; - - fn index(&self, index: usize) -> &Self::Output { - self.data.index(index) - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: Range) -> &Self::Output { - &self.data[range] - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: RangeFrom) -> &Self::Output { - &self.data[range] - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: RangeTo) -> &Self::Output { - &self.data[range] - } -} - -impl Index for Sample { - type Output = [S; CHANNELS]; - - fn index(&self, index: f32) -> &Self::Output { - self.data.index(self.time_to_index(index as f64)) - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: Range) -> &Self::Output { - &self.data[self.time_to_index(range.start as f64)..self.time_to_index(range.end as f64)] - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: RangeFrom) -> &Self::Output { - &self.data[self.time_to_index(range.start as f64)..] - } -} - -impl Index> for Sample { - type Output = [[S; CHANNELS]]; - - fn index(&self, range: RangeTo) -> &Self::Output { - &self.data[..self.time_to_index(range.end as f64)] - } -} - -impl Index for Sample { - type Output = [S; CHANNELS]; - - fn index(&self, index: f64) -> &Self::Output { - self.data.index(self.time_to_index(index)) - } -} - -impl Sample { - fn time_to_index(&self, time: f64) -> usize { - (time * self.sample_rate as f64) as usize +impl AudioBuffer { + pub fn index_to_seconds(&self, index: usize) -> f64 { + index as f64 / self.sample_rate as f64 } - pub fn len_samples(&self) -> usize { - self.data.len() + pub fn seconds_to_index(&self, seconds: f64) -> usize { + (seconds * self.sample_rate as f64) as usize } - pub fn len_seconds(&self) -> f32 { - self.data.len() as f32 / self.sample_rate as f32 - } - - pub fn resample(&self, sample_rate: usize, interpolation: Interpolation) -> Self { - let data = self - .data - .stretch(sample_rate as f32 / self.sample_rate as f32, interpolation); - - Self { - data, - sample_rate: self.sample_rate, - } - } -} - -pub trait SamplePointProcessing { - fn add_sample(&mut self, other: &[S; CHANNELS]); - fn volume(&self, volume: f32) -> Self; -} - -impl SamplePointProcessing for [S; CHANNELS] { - fn add_sample(&mut self, other: &[S; CHANNELS]) { - for channel_i in 0..CHANNELS { - self[channel_i] = S::from_f32(self[channel_i].into_f32() + other[channel_i].into_f32()); - } - } - - fn volume(&self, volume: f32) -> Self { - self.iter() - .map(|sample| sample.into_f32() * volume) - .map(S::from_f32) - .collect_array::() - } -} - -pub trait SampleDataProcessing { - fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize); - fn volume(&self, volume: f32) -> Self; - fn volume_range(&self, volume: Range) -> Self; - fn stretch(&self, factor: f32, interpolation: Interpolation) -> Self; -} - -impl SampleDataProcessing - for Vec<[S; CHANNELS]> -{ - fn add_sample(&mut self, other: &[[S; CHANNELS]], offset: usize) { - let new_len = offset + other.len(); - - if new_len > self.len() { - self.resize(new_len, [S::from_f32(0.0); CHANNELS]); - } - - for (i, samples_other) in other.iter().enumerate() { - let i = i + offset; - - for channel_i in 0..CHANNELS { - self[i].add_sample(samples_other); - } - } - } - - fn volume(&self, volume: f32) -> Self { - self.iter().map(|samples| samples.volume(volume)).collect() - } - - fn stretch(&self, factor: f32, interpolation: Interpolation) -> Self { - let len = (self.len() as f32 * factor).round() as usize; - - (0..len) - .map(|(i_sample)| { - (0..CHANNELS) - .map(|i_channel| match interpolation { - Interpolation::Nearest => { - self[(i_sample as f32 / factor).floor() as usize][i_channel] - } - Interpolation::Linear { - first_sample_after: last_sample, - } => { - let frac = i_sample as f32 / factor; - let index = frac.floor() as usize; - let frac = frac - index as f32; - - let sample_1 = self[index][i_channel].into_f32(); - let sample_2 = if self.len() > index + 1 { - self[index + 1][i_channel] - } else { - last_sample.map_or(self[index][i_channel], |s| s[i_channel]) - } - .into_f32(); - - S::from_f32((1.0 - frac) * sample_1 + frac * sample_2) - } - }) - .collect_array::() - }) - .collect() - } - - fn volume_range(&self, volume: Range) -> Self { - let len = self.iter().len(); - self.iter() - .enumerate() - .map(|(i, samples)| { - samples.volume(volume.start + (i as f32 / len as f32) * (volume.end - volume.start)) - }) - .collect() + pub fn len_seconds(&self) -> f64 { + self.index_to_seconds(self.data.len()) } } -impl Sample { - pub fn mono(sample_rate: usize) -> Self { - Self { - data: Default::default(), - sample_rate, - } - } - - pub fn to_stereo(&self) -> Sample { - let data = self.data.iter().map(|[s]| [*s, *s]).collect(); +impl Index for AudioBuffer { + type Output = S; - Sample:: { - data, - sample_rate: self.sample_rate, - } - } -} - -impl Sample { - pub fn stereo(sample_rate: usize) -> Self { - Self { - data: Default::default(), - sample_rate, - } - } - - pub fn to_mono(&self) -> Sample { - let data = self - .data - .iter() - .map(|[sample_1, sample_2]| { - let sample_1 = sample_1.into_f32(); - let sample_2 = sample_2.into_f32(); - - [S::from_f32(sample_1 * 0.5 + sample_2 * 0.5)] - }) - .collect_vec(); - - Sample:: { - data, - sample_rate: self.sample_rate, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn volume() { - let samples = vec![ - [11, 20], - [-20, -40], - [30, 60], - [-40, -80], - [-50, -100], - [-80, -160], - ]; - - assert_eq!( - samples.volume(1.47), - vec![ - [16, 29], - [-29, -59], - [44, 88], - [-59, -118], - [-74, -147], - [-118, -235], - ] - ); - - assert_eq!( - samples.volume(0.5), - vec![ - [6, 10], - [-10, -20], - [15, 30], - [-20, -40], - [-25, -50], - [-40, -80], - ] - ); - } - - #[test] - fn stretch_nearest_integer() { - let samples = vec![ - [10, 20], - [-20, -40], - [30, 60], - [-40, -80], - [-50, -100], - [-80, -160], - ]; - - assert_eq!( - samples.stretch(2.0, Interpolation::Nearest), - vec![ - [10, 20], - [10, 20], - [-20, -40], - [-20, -40], - [30, 60], - [30, 60], - [-40, -80], - [-40, -80], - [-50, -100], - [-50, -100], - [-80, -160], - [-80, -160], - ] - ); - } - - #[test] - fn stretch_nearest_frac() { - let samples = vec![ - [10, 20], - [-20, -40], - [30, 60], - [-40, -80], - [-50, -100], - [-80, -160], - ]; - - assert_eq!( - samples.stretch(1.5, Interpolation::Nearest), - vec![ - [10, 20], - [10, 20], - [-20, -40], - [30, 60], - [30, 60], - [-40, -80], - [-50, -100], - [-50, -100], - [-80, -160], - ] - ); - } - - #[test] - fn stretch_linear_integer_without_last() { - let samples = vec![ - [10, 20], - [-20, -40], - [30, 60], - [-40, -80], - [-50, -100], - [-80, -160], - ]; - - assert_eq!( - samples.stretch( - 2.0, - Interpolation::Linear { - first_sample_after: None - } - ), - vec![ - [10, 20], - [-5, -10], - [-20, -40], - [5, 10], - [30, 60], - [-5, -10], - [-40, -80], - [-45, -90], - [-50, -100], - [-65, -130], - [-80, -160], - [-80, -160], - ] - ); - } - - #[test] - fn stretch_linear_integer_with_last() { - let samples = vec![ - [10, 20], - [-20, -40], - [30, 60], - [-40, -80], - [-50, -100], - [-80, -160], - ]; - - assert_eq!( - samples.stretch( - 2.0, - Interpolation::Linear { - first_sample_after: Some([500, 500]) - } - ), - vec![ - [10, 20], - [-5, -10], - [-20, -40], - [5, 10], - [30, 60], - [-5, -10], - [-40, -80], - [-45, -90], - [-50, -100], - [-65, -130], - [-80, -160], - [210, 170], - ] - ); + fn index(&self, index: usize) -> &Self::Output { + self.data.index(index) } } diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index 721f545..6830cf6 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -10,7 +10,7 @@ use image::{ use crate::asset::{ color_map::Color, - sound::sample::{Sample, SamplePoint}, + sound::sample::{AudioBuffer, SamplePoint}, }; pub trait PngFile { @@ -102,7 +102,7 @@ pub trait WaveFile { fn to_wave(&self) -> Vec; } -impl WaveFile for Sample { +impl WaveFile for AudioBuffer { fn to_wave(&self) -> Vec { let bits_per_sample: usize = S::SIZE_BITS; let bytes_per_sample: usize = bits_per_sample / 8; From 869c6efc9edcdc93fb8ec3dc1cbfeb37a3a0f229 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 8 Oct 2024 13:48:54 -0400 Subject: [PATCH 72/87] refactor: more old code removed --- engine/src/asset/sound/dat/mixer.rs | 43 ++-------------- engine/src/asset/sound/sample.rs | 79 +++++++++++++++++++---------- engine/src/lib.rs | 3 ++ engine/src/utils/format.rs | 61 ++++++---------------- 4 files changed, 76 insertions(+), 110 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 671f7ea..8a59c0c 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -10,39 +10,7 @@ use itertools::Itertools; use super::{ finetune::FineTune, pattern_effect::*, pattern_event::*, t_instrument::*, t_song::TSong, }; -use crate::asset::sound::sample::AudioBuffer; - -trait AudioSamplePoint { - type Bytes: IntoIterator; - - fn into_normalized_f32(&self) -> f32; - fn from_normalized_f32(value: f32) -> Self; - fn into_bytes(&self) -> Self::Bytes; -} - -impl AudioSamplePoint for i16 { - type Bytes = [u8; 2]; - - fn into_normalized_f32(&self) -> f32 { - if *self < 0 { - -(*self as f32 / Self::MIN as f32) - } else { - (*self as f32 / Self::MAX as f32) - } - } - - fn from_normalized_f32(value: f32) -> Self { - if value < 0. { - -(value * i16::MIN as f32) as i16 - } else { - (value * i16::MAX as f32) as i16 - } - } - - fn into_bytes(&self) -> Self::Bytes { - self.to_le_bytes() - } -} +use crate::asset::sound::sample::{AudioBuffer, AudioSamplePoint}; #[derive(Default, Clone, Debug)] struct PlayerChannelNote { @@ -97,7 +65,7 @@ impl PlayerChannel { self.direction = PlaybackDirection::Forwards; } - fn generate_sample(&mut self, step: f64) -> T { + fn generate_sample(&mut self, step: f64) -> f32 { let current_sample = if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(note) = self.note.finetune @@ -146,7 +114,7 @@ impl PlayerChannel { let current_sample = if let Some((previous, position)) = &mut self.previous { let factor = (*position / Self::SAMPLE_BLEND) as f32; - let previous_sample = previous.generate_sample::(step).into_normalized_f32(); + let previous_sample = previous.generate_sample(step); *position += step; if *position >= Self::SAMPLE_BLEND { @@ -158,7 +126,7 @@ impl PlayerChannel { current_sample }; - T::from_normalized_f32(current_sample) + current_sample } fn trigger_note(&mut self) { @@ -341,8 +309,7 @@ impl<'a> Player<'a> { let sample = self .channels .iter_mut() - .map(|c| c.generate_sample::(step)) - .map(|c| c.into_normalized_f32()) + .map(|c| c.generate_sample(step)) //.enumerate() //.filter_map(|(i, s)| (i == 0).then_some(s)) .sum::(); diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index ddd4063..b0956eb 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,42 +1,63 @@ // TODO(nenikitov): Remove this test code -use std::{fmt::Debug, ops::Index}; +use std::{fmt::Debug, mem::size_of, ops::Index}; + +pub enum AudioSamplePointFormat { + Int, + Float, +} + +impl AudioSamplePointFormat { + pub const fn signature(&self) -> u16 { + match self { + AudioSamplePointFormat::Int => 1, + AudioSamplePointFormat::Float => 3, + } + } +} + +pub trait AudioSamplePoint: Default + Clone + Copy { + const SIZE_BITS: usize = size_of::(); -pub trait SamplePoint: Default + Clone + Copy { fn into_normalized_f32(&self) -> f32; fn from_normalized_f32(value: f32) -> Self; + + fn wave_format() -> AudioSamplePointFormat; + fn wave_le_bytes(&self) -> [u8; Self::SIZE_BITS]; } -macro_rules! impl_sample_point_for { - ($($ty:ty),*) => { - $(impl SamplePoint for $ty { - fn into_normalized_f32(&self) -> f32 { - if *self < 0 { - -(*self as f32 / Self::MIN as f32) - } else { - (*self as f32 / Self::MAX as f32) - } - } - - fn from_normalized_f32(value: f32) -> Self { - if value < 0. { - -(value * Self::MIN as f32) as Self - } else { - (value * Self::MAX as f32) as Self - } - } - })* +impl AudioSamplePoint for i16 { + fn into_normalized_f32(&self) -> f32 { + if *self < 0 { + -(*self as f32 / Self::MIN as f32) + } else { + (*self as f32 / Self::MAX as f32) + } } -} -impl_sample_point_for!(i16); + fn from_normalized_f32(value: f32) -> Self { + if value < 0. { + -(value * Self::MIN as f32) as Self + } else { + (value * Self::MAX as f32) as Self + } + } + + fn wave_format() -> AudioSamplePointFormat { + AudioSamplePointFormat::Int + } + + fn wave_le_bytes(&self) -> [u8; Self::SIZE_BITS] { + self.to_le_bytes() + } +} #[derive(Debug, Clone)] -pub struct AudioBuffer { +pub struct AudioBuffer { pub data: Vec, pub sample_rate: usize, } -impl AudioBuffer { +impl AudioBuffer { pub fn index_to_seconds(&self, index: usize) -> f64 { index as f64 / self.sample_rate as f64 } @@ -45,12 +66,16 @@ impl AudioBuffer { (seconds * self.sample_rate as f64) as usize } + pub fn len_samples(&self) -> usize { + self.data.len() + } + pub fn len_seconds(&self) -> f64 { - self.index_to_seconds(self.data.len()) + self.index_to_seconds(self.len_samples()) } } -impl Index for AudioBuffer { +impl Index for AudioBuffer { type Output = S; fn index(&self, index: usize) -> &Self::Output { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 1c89dbc..d8d7057 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -10,6 +10,8 @@ clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::unreadable_literal, + + incomplete_features, )] #![warn(unused_imports)] #![feature( @@ -17,6 +19,7 @@ // https://github.com/rust-lang/rust/pull/101179 debug_closure_helpers, duration_consts_float, + generic_const_exprs, io_error_more, let_chains, maybe_uninit_uninit_array_transpose, diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index 6830cf6..f58ec12 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -10,7 +10,7 @@ use image::{ use crate::asset::{ color_map::Color, - sound::sample::{AudioBuffer, SamplePoint}, + sound::sample::{AudioBuffer, AudioSamplePoint}, }; pub trait PngFile { @@ -98,14 +98,21 @@ where } } -pub trait WaveFile { - fn to_wave(&self) -> Vec; +pub trait WaveFile { + fn to_wave(&self) -> Vec + where + [(); S::SIZE_BITS]:; } -impl WaveFile for AudioBuffer { - fn to_wave(&self) -> Vec { - let bits_per_sample: usize = S::SIZE_BITS; - let bytes_per_sample: usize = bits_per_sample / 8; +impl WaveFile for AudioBuffer { + fn to_wave(&self) -> Vec + where + [(); S::SIZE_BITS]:, + { + const CHANNELS: usize = 1; + + let bytes_per_sample: usize = S::SIZE_BITS; + let bits_per_sample: usize = bytes_per_sample / 8; let size = self.len_samples() * CHANNELS * bytes_per_sample; @@ -115,7 +122,7 @@ impl WaveFile for AudioBuffer { .chain("WAVE".bytes()) .chain("fmt ".bytes()) .chain(u32::to_le_bytes(16)) - .chain(u16::to_le_bytes(1)) + .chain(u16::to_le_bytes(S::wave_format().signature())) .chain(u16::to_le_bytes(CHANNELS as u16)) .chain(u32::to_le_bytes(self.sample_rate as u32)) .chain(u32::to_le_bytes( @@ -125,43 +132,7 @@ impl WaveFile for AudioBuffer { .chain(u16::to_le_bytes(bits_per_sample as u16)) .chain("data".bytes()) .chain(u32::to_le_bytes(size as u32)) - .chain( - self.data - .iter() - .flat_map(|s| s.into_iter().flat_map(|s| s.to_integer_le_bytes())), - ) - .collect() - } -} - -pub trait WaveFileOld { - fn to_wave(&self, sample_rate: usize, channel_count: usize) -> Vec; -} - -impl WaveFileOld for Vec { - fn to_wave(&self, sample_rate: usize, channel_count: usize) -> Vec { - const BITS_PER_SAMPLE: usize = 16; - const BYTES_PER_SAMPLE: usize = BITS_PER_SAMPLE / 8; - - let size = self.len() * BYTES_PER_SAMPLE; - - "RIFF" - .bytes() - .chain(u32::to_be_bytes((36 + size) as u32)) - .chain("WAVE".bytes()) - .chain("fmt ".bytes()) - .chain(u32::to_le_bytes(16)) - .chain(u16::to_le_bytes(1)) - .chain(u16::to_le_bytes(channel_count as u16)) - .chain(u32::to_le_bytes(sample_rate as u32)) - .chain(u32::to_le_bytes( - (sample_rate * channel_count * BYTES_PER_SAMPLE) as u32, - )) - .chain(u16::to_le_bytes((channel_count * BYTES_PER_SAMPLE) as u16)) - .chain(u16::to_le_bytes(BITS_PER_SAMPLE as u16)) - .chain("data".bytes()) - .chain(u32::to_le_bytes(size as u32)) - .chain(self.iter().flat_map(|s| s.to_le_bytes())) + .chain(self.data.iter().flat_map(|s| s.wave_le_bytes())) .collect() } } From a41c48c70d1b0ac52e68a6ccb5a1303ec23958d1 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 8 Oct 2024 22:41:37 -0400 Subject: [PATCH 73/87] hotfix: invalid file size --- engine/src/asset/sound/dat/finetune.rs | 7 +++++-- engine/src/asset/sound/sample.rs | 6 +++--- engine/src/utils/format.rs | 8 ++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 7f7cd11..e11c44c 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,4 +1,7 @@ -use std::ops::{Add, AddAssign, Neg, Sub}; +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Neg, Sub}, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord)] pub struct FineTune { @@ -69,7 +72,7 @@ impl Neg for FineTune { } impl PartialOrd for FineTune { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { self.cents.partial_cmp(&other.cents) } } diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index b0956eb..b2b9c79 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -16,13 +16,13 @@ impl AudioSamplePointFormat { } pub trait AudioSamplePoint: Default + Clone + Copy { - const SIZE_BITS: usize = size_of::(); + const SIZE_BYTES: usize = size_of::(); fn into_normalized_f32(&self) -> f32; fn from_normalized_f32(value: f32) -> Self; fn wave_format() -> AudioSamplePointFormat; - fn wave_le_bytes(&self) -> [u8; Self::SIZE_BITS]; + fn wave_le_bytes(&self) -> [u8; Self::SIZE_BYTES]; } impl AudioSamplePoint for i16 { @@ -46,7 +46,7 @@ impl AudioSamplePoint for i16 { AudioSamplePointFormat::Int } - fn wave_le_bytes(&self) -> [u8; Self::SIZE_BITS] { + fn wave_le_bytes(&self) -> [u8; Self::SIZE_BYTES] { self.to_le_bytes() } } diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index f58ec12..136da0e 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -101,18 +101,18 @@ where pub trait WaveFile { fn to_wave(&self) -> Vec where - [(); S::SIZE_BITS]:; + [(); S::SIZE_BYTES]:; } impl WaveFile for AudioBuffer { fn to_wave(&self) -> Vec where - [(); S::SIZE_BITS]:, + [(); S::SIZE_BYTES]:, { const CHANNELS: usize = 1; - let bytes_per_sample: usize = S::SIZE_BITS; - let bits_per_sample: usize = bytes_per_sample / 8; + let bytes_per_sample: usize = S::SIZE_BYTES; + let bits_per_sample: usize = bytes_per_sample * 8; let size = self.len_samples() * CHANNELS * bytes_per_sample; From b75f1bf6210667c9c215412b430c238183509584 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Tue, 8 Oct 2024 23:55:38 -0400 Subject: [PATCH 74/87] refactor --- engine/src/asset/sound/dat/mixer.rs | 39 +++++++++++----------- engine/src/asset/sound/dat/t_instrument.rs | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 8a59c0c..c007e46 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -205,13 +205,11 @@ impl PlayerChannel { if let Some(instrument) = &self.instrument && let TInstrumentVolume::Envelope(envelope) = &instrument.volume { - if (self.note.on - && if let Some(sustain) = envelope.sustain { - self.pos_volume_envelope < sustain - } else { - true - }) - || !self.note.on + if !self.note.on + || (self.note.on + && envelope + .sustain + .map_or(true, |s| self.pos_volume_envelope < s)) { self.pos_volume_envelope += 1; } @@ -310,8 +308,6 @@ impl<'a> Player<'a> { .channels .iter_mut() .map(|c| c.generate_sample(step)) - //.enumerate() - //.filter_map(|(i, s)| (i == 0).then_some(s)) .sum::(); self.volume_global_actual = advance_to( self.volume_global_actual, @@ -402,7 +398,7 @@ impl<'a> Player<'a> { { self.pos_row = 0; self.pos_pattern += 1; - }; + } if self.pos_pattern >= self.song.orders.len() { self.pos_pattern = self.song.restart_order as usize; self.pos_loop += 1; @@ -434,12 +430,12 @@ impl<'a> Player<'a> { channel.change_instrument(instrument.clone()); } - if let Some(note) = &event.note { - channel.change_note(note.clone()); + if let Some(note) = event.note { + channel.change_note(note); } - if let Some(volume) = &event.volume { - channel.change_volume(volume.clone()); + if let Some(volume) = event.volume { + channel.change_volume(volume); } channel.clear_effects(); @@ -452,7 +448,7 @@ impl<'a> Player<'a> { channel.change_effect(i, effect.clone()); use PatternEffect as E; - match channel.effects[i].unwrap() { + match channel.effects[i].expect("`change_effect` sets the effect") { // Init effects E::Speed(Speed::Bpm(bpm)) => { self.bpm = bpm; @@ -494,7 +490,7 @@ impl<'a> Player<'a> { } E::SampleOffset(Some(offset)) => { // TODO(nenikitov): Remove this hardcoded value - channel.pos_sample = 1. / 16_000. * offset as f64; + channel.pos_sample = offset as f64 / TSample::SAMPLE_RATE as f64; } E::NoteDelay(delay) => { channel.note_delay = delay; @@ -528,9 +524,14 @@ impl TSongMixer for TSong { let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); - let samples: Vec<_> = - std::iter::from_fn(|| (player.pos_loop == 0).then(|| player.generate_sample::())) - .collect(); + let samples: Vec<_> = std::iter::from_fn(|| { + if player.pos_loop == 0 { + Some(player.generate_sample::()) + } else { + None + } + }) + .collect(); AudioBuffer { data: samples, diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 6d0f949..f56e48f 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -250,7 +250,7 @@ impl AssetParser for TSample { } impl TSample { - const SAMPLE_RATE: usize = 16_000; + pub const SAMPLE_RATE: usize = 16_000; // TODO(nenikitov): I think the whole `Sample` will need to be removed pub fn get(&self, position: f64) -> Option { From 0ef415c1c238462b1893510024eebd17be05991b Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 00:49:37 -0400 Subject: [PATCH 75/87] refactor: clippy warnings --- engine/src/asset/sound/dat/finetune.rs | 15 ++---- engine/src/asset/sound/dat/mixer.rs | 53 +++++++++----------- engine/src/asset/sound/dat/pattern_effect.rs | 20 +++++--- engine/src/asset/sound/dat/pattern_event.rs | 25 ++++++--- engine/src/asset/sound/dat/t_instrument.rs | 15 +++--- engine/src/asset/sound/dat/t_song.rs | 43 ++++++++-------- engine/src/asset/sound/sample.rs | 10 ++-- engine/src/lib.rs | 2 + 8 files changed, 91 insertions(+), 92 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index e11c44c..1b6a53d 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,9 +1,6 @@ -use std::{ - cmp::Ordering, - ops::{Add, AddAssign, Neg, Sub}, -}; +use std::ops::{Add, AddAssign, Neg, Sub}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct FineTune { cents: i32, } @@ -51,7 +48,7 @@ impl Add for FineTune { impl AddAssign for FineTune { fn add_assign(&mut self, rhs: Self) { - self.cents = self.cents.saturating_add(rhs.cents) + self.cents = self.cents.saturating_add(rhs.cents); } } @@ -71,12 +68,6 @@ impl Neg for FineTune { } } -impl PartialOrd for FineTune { - fn partial_cmp(&self, other: &Self) -> Option { - self.cents.partial_cmp(&other.cents) - } -} - #[cfg(test)] mod tests { use assert_approx_eq::assert_approx_eq; diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index c007e46..1fa194a 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -85,7 +85,7 @@ impl PlayerChannel { envelope .volume_beginning() .get(self.pos_volume_envelope) - .cloned() + .copied() .unwrap_or(envelope.volume_loop()) } else { envelope @@ -94,7 +94,7 @@ impl PlayerChannel { self.pos_volume_envelope .saturating_sub(envelope.volume_beginning().len()), ) - .cloned() + .copied() .unwrap_or(0.) } } @@ -112,21 +112,19 @@ impl PlayerChannel { 0. }; - let current_sample = if let Some((previous, position)) = &mut self.previous { + if let Some((previous, position)) = &mut self.previous { let factor = (*position / Self::SAMPLE_BLEND) as f32; let previous_sample = previous.generate_sample(step); *position += step; if *position >= Self::SAMPLE_BLEND { - self.previous = None + self.previous = None; } previous_sample + factor * (current_sample - previous_sample) } else { current_sample - }; - - current_sample + } } fn trigger_note(&mut self) { @@ -139,8 +137,8 @@ impl PlayerChannel { self.pos_reset(); } - fn change_instrument(&mut self, instrument: Option>) { - if let Some(instrument) = instrument { + fn change_instrument(&mut self, instrument: PatternEventInstrument) { + if let PatternEventInstrument::Instrument(instrument) = instrument { self.instrument = Some(instrument); } @@ -164,7 +162,8 @@ impl PlayerChannel { self.note.finetune = Some(note); self.note.finetune_initial = Some(note); self.note.on = true; - self.sample = instrument.samples[note.note() as usize].clone(); + self.sample + .clone_from(&instrument.samples[note.note() as usize]); } } } else { @@ -204,15 +203,12 @@ impl PlayerChannel { fn advance_envelopes(&mut self) { if let Some(instrument) = &self.instrument && let TInstrumentVolume::Envelope(envelope) = &instrument.volume + && (!self.note.on + || envelope + .sustain + .map_or(true, |s| self.pos_volume_envelope < s)) { - if !self.note.on - || (self.note.on - && envelope - .sustain - .map_or(true, |s| self.pos_volume_envelope < s)) - { - self.pos_volume_envelope += 1; - } + self.pos_volume_envelope += 1; } } @@ -322,11 +318,12 @@ impl<'a> Player<'a> { self.row(); } - for channel in self.channels.iter_mut() { + for channel in &mut self.channels { channel.advance_envelopes(); for effect in channel.effects.iter().flatten() { use PatternEffect as E; + match *effect { // Tick effects E::Volume(Volume::Slide(Some(volume))) => { @@ -364,10 +361,8 @@ impl<'a> Player<'a> { E::Speed(..) | E::PatternBreak | E::PatternJump(..) - | E::Volume(Volume::Set(..)) - | E::Volume(Volume::Bump { .. }) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Bump { .. }) + | E::Volume(Volume::Set(..) | Volume::Bump { .. }) + | E::Porta(Porta::Tone(..) | Porta::Bump { .. }) | E::GlobalVolume(..) | E::SampleOffset(..) | E::PlaybackDirection(..) => {} @@ -445,9 +440,9 @@ impl<'a> Player<'a> { .enumerate() .filter_map(|(i, e)| e.map(|e| (i, e))) { - channel.change_effect(i, effect.clone()); - use PatternEffect as E; + + channel.change_effect(i, effect); match channel.effects[i].expect("`change_effect` sets the effect") { // Init effects E::Speed(Speed::Bpm(bpm)) => { @@ -485,7 +480,7 @@ impl<'a> Player<'a> { if let Some(sample) = &channel.sample && direction == PlaybackDirection::Backwards { - channel.pos_sample = sample.buffer.len_seconds() as f64 + channel.pos_sample = sample.buffer.len_seconds(); } } E::SampleOffset(Some(offset)) => { @@ -497,13 +492,11 @@ impl<'a> Player<'a> { } // Noops - no init E::Volume(Volume::Slide(..)) - | E::Porta(Porta::Tone(..)) - | E::Porta(Porta::Slide { .. }) + | E::Porta(Porta::Tone(..) | Porta::Slide { .. }) | E::RetriggerNote(..) => {} // Unreachable because memory has to be initialized E::Volume(Volume::Bump { volume: None, .. }) - | E::Porta(Porta::Tone(None)) - | E::Porta(Porta::Bump { finetune: None, .. }) + | E::Porta(Porta::Tone(None) | Porta::Bump { finetune: None, .. }) | E::SampleOffset(None) => { unreachable!("Effects should have their memory initialized") } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index aa6c882..0c830de 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -115,15 +115,19 @@ impl PatternEffect { self.memory_key().is_some() } + #[rustfmt::skip] pub fn is_empty(&self) -> bool { + use PatternEffect as E; + matches!( self, - PatternEffect::Porta(Porta::Tone(None)) - | PatternEffect::Porta(Porta::Slide { finetune: None, .. }) - | PatternEffect::Porta(Porta::Bump { finetune: None, .. }) - | PatternEffect::Volume(Volume::Slide(None)) - | PatternEffect::Volume(Volume::Bump { volume: None, .. }) - | PatternEffect::SampleOffset(None) + E::Porta( + Porta::Tone(None) + | Porta::Slide { finetune: None, .. } + | Porta::Bump { finetune: None, .. } + ) + | E::Volume(Volume::Slide(None) | Volume::Bump { volume: None, .. }) + | E::SampleOffset(None) ) } } @@ -135,10 +139,10 @@ impl AssetParser for Option { fn parser(should_parse: Self::Context<'_>) -> impl Fn(Input) -> Result { move |input| { + use PatternEffect as E; + let (input, kind) = number::le_u8(input)?; let (input, value) = number::le_u8(input)?; - - use PatternEffect as E; Ok(( input, should_parse.then(|| match kind { diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 0d93023..f159e3d 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -63,15 +63,15 @@ impl AssetParser for PatternEventFlags { Ok(( input, // TODO(nenikitov): Should be a `Result` - PatternEventFlags::from_bits(flags).expect(&format!( - "PatternEvent flags should be valid: received: {flags:b}" - )), + PatternEventFlags::from_bits(flags).unwrap_or_else(|| { + panic!("PatternEvent flags should be valid: received: {flags:b}") + }), )) } } } -impl AssetParser for Option>> { +impl AssetParser for Option { type Output = Self; type Context<'ctx> = (bool, &'ctx [Rc]); @@ -84,7 +84,10 @@ impl AssetParser for Option>> { Ok(( input, - should_parse.then(|| instruments.get(instrument as usize).map(Rc::clone)), + should_parse.then(|| match instruments.get(instrument as usize) { + Some(instrument) => PatternEventInstrument::Instrument(instrument.clone()), + None => PatternEventInstrument::Ghost, + }), )) } } @@ -125,10 +128,18 @@ impl AssetParser for Option { } } +#[derive(Default, Debug, Clone)] +pub enum PatternEventInstrument { + #[default] + Ghost, + Instrument(Rc), +} + #[derive(Default, Debug)] pub struct PatternEvent { pub note: Option, - pub instrument: Option>>, + // Option> + pub instrument: Option, pub volume: Option, pub effects: [Option; 2], } @@ -158,7 +169,7 @@ impl AssetParser for PatternEvent { flags.contains(PatternEventFlags::ChangeNote), )(input)?; - let (input, instrument) = >>>::parser(( + let (input, instrument) = >::parser(( (flags.contains(PatternEventFlags::ChangeInstrument)), instruments, ))(input)?; diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index f56e48f..eac484f 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -29,9 +29,9 @@ impl AssetParser for TInstrumentFlags { Ok(( input, // TODO(nenikitov): Should be a `Result` - TInstrumentFlags::from_bits(flags).expect(&format!( - "PatternEvent flags should be valid: received: {flags:b}" - )), + TInstrumentFlags::from_bits(flags).unwrap_or_else(|| { + panic!("PatternEvent flags should be valid: received: {flags:b}") + }), )) } } @@ -261,13 +261,12 @@ impl TSample { let position = Self::SAMPLE_RATE as f64 * position; let frac = position.fract() as f32; - let Some(prev) = self.normalize(position as usize) else { - return None; - }; - let next = self.normalize(position as usize + 1); + let prev = self.normalize(position as usize)?; let prev = self.buffer[prev] as f32; - let next = next.map(|next| self.buffer[next] as f32).unwrap_or(0.); + + let next = self.normalize(position as usize + 1); + let next = next.map_or(0., |next| self.buffer[next] as f32); Some((prev + frac * (next - prev)) as i16) } diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index c5f424c..1ab8de1 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -35,7 +35,7 @@ impl std::fmt::Debug for TSong { d.value_with(|f| { let mut d = f.debug_map(); for (r, row) in pattern.iter().enumerate() { - if !row.iter().any(|c| c.has_content()) { + if !row.iter().any(PatternEvent::has_content) { continue; } @@ -50,31 +50,30 @@ impl std::fmt::Debug for TSong { d.key(&format!("C 0x{e:X}")); d.value_with(|f| { let mut d = f.debug_struct("Event"); - event.note.map(|note| { - d.field_with("note", |f| { - f.write_fmt(format_args!("{:?}", note)) - }); - }); - event.volume.map(|volume| { - d.field_with("volume", |f| { - f.write_fmt(format_args!("{:?}", volume)) - }); - }); - event.instrument.as_ref().map(|instrument| { + if let Some(note) = event.note { + d.field_with("note", |f| write!(f, "{note:?}")); + } + if let Some(volume) = event.volume { + d.field_with("volume", |f| write!(f, "{volume:?}")); + } + if let Some(instrument) = &event.instrument { d.field_with("instrument", |f| match instrument { - None => f.write_fmt(format_args!("None")), - Some(instrument) => f.write_fmt(format_args!( - "Some({})", - self.instruments - .iter() - .position(|i| Rc::ptr_eq(i, instrument)) - .unwrap() - )), + PatternEventInstrument::Ghost => write!(f, "Ghost"), + PatternEventInstrument::Instrument(instrument) => { + write!( + f, + "Instrument({})", + self.instruments + .iter() + .position(|i| Rc::ptr_eq(i, instrument)) + .unwrap() + ) + } }); - }); + } if event.effects.iter().any(Option::is_some) { d.field_with("effects", |f| { - f.write_fmt(format_args!("{:?}", event.effects)) + write!(f, "{:?}", event.effects) }); } d.finish() diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index b2b9c79..1874940 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -18,7 +18,7 @@ impl AudioSamplePointFormat { pub trait AudioSamplePoint: Default + Clone + Copy { const SIZE_BYTES: usize = size_of::(); - fn into_normalized_f32(&self) -> f32; + fn into_normalized_f32(self) -> f32; fn from_normalized_f32(value: f32) -> Self; fn wave_format() -> AudioSamplePointFormat; @@ -26,11 +26,11 @@ pub trait AudioSamplePoint: Default + Clone + Copy { } impl AudioSamplePoint for i16 { - fn into_normalized_f32(&self) -> f32 { - if *self < 0 { - -(*self as f32 / Self::MIN as f32) + fn into_normalized_f32(self) -> f32 { + if self < 0 { + -(self as f32 / Self::MIN as f32) } else { - (*self as f32 / Self::MAX as f32) + (self as f32 / Self::MAX as f32) } } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index d8d7057..a6f1160 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -9,6 +9,8 @@ clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::missing_fields_in_debug, clippy::unreadable_literal, incomplete_features, From 6a5e62fed2f984c21c47d66fd19c56c615470342 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 00:54:56 -0400 Subject: [PATCH 76/87] refactor: loom --- Cargo.lock | 346 ++----------------------------------------- engine/Cargo.toml | 1 - engine/tests/loom.rs | 4 +- 3 files changed, 17 insertions(+), 334 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2976078..d21b5aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,41 +159,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - [[package]] name = "either" version = "1.9.0" @@ -211,26 +176,15 @@ dependencies = [ "fixed", "flate2", "image", - "itertools 0.12.0", + "itertools", "lewton", "loom", "nom", - "partial-borrow", "paste", "sealed", "thiserror", ] -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "exr" version = "1.6.4" @@ -257,12 +211,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "fdeflate" version = "0.3.4" @@ -272,26 +220,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fehler" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635" -dependencies = [ - "fehler-macros", -] - -[[package]] -name = "fehler-macros" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "fixed" version = "1.24.0" @@ -327,12 +255,6 @@ dependencies = [ "spin", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "futures-core" version = "0.3.30" @@ -391,24 +313,12 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "image" version = "0.24.8" @@ -433,25 +343,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.0" @@ -508,12 +399,6 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "lock_api" version = "0.4.11" @@ -558,15 +443,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -642,34 +518,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "partial-borrow" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4841b3043764a29a834246d1faf87fac8f70fa37a53c503af5785c6a2e98af8a" -dependencies = [ - "memoffset", - "partial-borrow-macros", -] - -[[package]] -name = "partial-borrow-macros" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c561c7c8fb375e70b41f324c284f2489b69b5a6f669ae9d1ce4267e7da59d581" -dependencies = [ - "darling", - "either", - "fehler", - "indexmap", - "itertools 0.10.5", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", - "tempfile", -] - [[package]] name = "paste" version = "1.0.14" @@ -693,7 +541,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -715,30 +563,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.76" @@ -786,15 +610,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" version = "1.10.2" @@ -839,19 +654,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rustix" -version = "0.38.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" -dependencies = [ - "bitflags 2.4.2", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -879,7 +681,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -912,23 +714,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.48" @@ -940,19 +725,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "thiserror" version = "1.0.56" @@ -970,7 +742,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -1028,7 +800,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -1094,12 +866,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1127,7 +893,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-shared", ] @@ -1149,7 +915,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1194,25 +960,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1221,29 +969,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1252,90 +984,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 0eb4729..f4fd223 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -12,7 +12,6 @@ itertools = "0.12.0" lewton = "0.10.2" loom = { version = "0.7.1", optional = true } nom = "7.1.3" -partial-borrow = "1.0.1" paste = "1.0.14" sealed = "0.5.0" thiserror = "1.0.56" diff --git a/engine/tests/loom.rs b/engine/tests/loom.rs index ead64d8..f3d31ae 100644 --- a/engine/tests/loom.rs +++ b/engine/tests/loom.rs @@ -1,3 +1,3 @@ -#[cfg(loom)] -#[cfg(tests)] +#[cfg(feature = "loom")] +#[cfg(test)] mod loom_tests {} From b1182ddcde62604f537af2bd48835d1cfabff054 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 01:19:33 -0400 Subject: [PATCH 77/87] refactor --- engine/src/asset/sound/dat/mixer.rs | 1 - engine/src/asset/sound/dat/mod.rs | 3 +-- engine/src/asset/sound/dat/pattern_effect.rs | 2 +- engine/src/asset/sound/dat/pattern_event.rs | 5 ++--- engine/src/asset/sound/dat/t_instrument.rs | 4 +--- engine/src/asset/sound/dat/t_song.rs | 4 +++- engine/src/asset/sound/mod.rs | 14 -------------- engine/src/asset/sound/sample.rs | 1 - 8 files changed, 8 insertions(+), 26 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1fa194a..5ab0f1c 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -484,7 +484,6 @@ impl<'a> Player<'a> { } } E::SampleOffset(Some(offset)) => { - // TODO(nenikitov): Remove this hardcoded value channel.pos_sample = offset as f64 / TSample::SAMPLE_RATE as f64; } E::NoteDelay(delay) => { diff --git a/engine/src/asset/sound/dat/mod.rs b/engine/src/asset/sound/dat/mod.rs index cb02826..c6869ce 100644 --- a/engine/src/asset/sound/dat/mod.rs +++ b/engine/src/asset/sound/dat/mod.rs @@ -52,6 +52,5 @@ fn uncompress(bytes: &[u8]) -> Vec { } fn convert_volume(volume: u8) -> f32 { - // TODO(nenikitov): Check if the volume should be limited to `1` max - volume as f32 / 64.0 + volume as f32 / 64. } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index 0c830de..f672c2b 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -103,7 +103,7 @@ impl PatternEffect { PatternEffect::Volume(Volume::Bump { up: true, .. }) => { Some(PatternEffectMemoryKey::VolumeBumpUp) } - PatternEffect::Volume(Volume::Bump { up: down, .. }) => { + PatternEffect::Volume(Volume::Bump { up: false, .. }) => { Some(PatternEffectMemoryKey::VolumeBumpDown) } PatternEffect::SampleOffset(..) => Some(PatternEffectMemoryKey::SampleOffset), diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index f159e3d..73651d8 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -86,7 +86,7 @@ impl AssetParser for Option { input, should_parse.then(|| match instruments.get(instrument as usize) { Some(instrument) => PatternEventInstrument::Instrument(instrument.clone()), - None => PatternEventInstrument::Ghost, + None => PatternEventInstrument::Previous, }), )) } @@ -131,14 +131,13 @@ impl AssetParser for Option { #[derive(Default, Debug, Clone)] pub enum PatternEventInstrument { #[default] - Ghost, + Previous, Instrument(Rc), } #[derive(Default, Debug)] pub struct PatternEvent { pub note: Option, - // Option> pub instrument: Option, pub volume: Option, pub effects: [Option; 2], diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index eac484f..90e9787 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -8,12 +8,11 @@ use crate::{ utils::{iterator::CollectArray, nom::*}, }; -// TODO(nenikitov): Double check these flags bitflags! { #[derive(Debug, Clone, Copy)] pub struct TInstrumentFlags: u8 { const HasVolumeEnvelope = 1 << 0; - const HasPanEnvelope = 1 << 1; + const HasPanEnvelope = 1 << 1; } } @@ -190,7 +189,6 @@ impl AssetParser for TInstrument { } } -// TODO(nenikitov): I'm not sure about this flag bitflags! { #[derive(Debug, Clone, Copy)] pub struct TSampleFlags: u8 { diff --git a/engine/src/asset/sound/dat/t_song.rs b/engine/src/asset/sound/dat/t_song.rs index 1ab8de1..cbef1a4 100644 --- a/engine/src/asset/sound/dat/t_song.rs +++ b/engine/src/asset/sound/dat/t_song.rs @@ -58,7 +58,9 @@ impl std::fmt::Debug for TSong { } if let Some(instrument) = &event.instrument { d.field_with("instrument", |f| match instrument { - PatternEventInstrument::Ghost => write!(f, "Ghost"), + PatternEventInstrument::Previous => { + write!(f, "Previous") + } PatternEventInstrument::Instrument(instrument) => { write!( f, diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 5253dbf..7d4312a 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -82,20 +82,6 @@ mod tests { let output_dir = PathBuf::from(parsed_file_path!("sounds/songs/")); - // TODO(nenikitov): Remove this debug code - { - let i = 0x3; - let song = sounds - .iter() - .filter_map(|s| match s { - Sound::Song(s) => Some(s), - Sound::Effect(_) => None, - }) - .collect::>()[i]; - - //dbg!(song); - } - sounds .iter() .filter_map(|s| { diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 1874940..70e8ecd 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,4 +1,3 @@ -// TODO(nenikitov): Remove this test code use std::{fmt::Debug, mem::size_of, ops::Index}; pub enum AudioSamplePointFormat { From 870f0840b6f8f80e57656e79dd213b6555b3d6e2 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 14:56:59 -0400 Subject: [PATCH 78/87] perf(sample): optimized normalize --- engine/src/asset/sound/dat/finetune.rs | 2 +- engine/src/asset/sound/dat/mixer.rs | 5 +---- engine/src/asset/sound/dat/t_instrument.rs | 14 +++++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 1b6a53d..7716054 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -20,7 +20,7 @@ impl FineTune { // TODO(nenikitov): This formula is from the game // And it's very magic. // Maybe simplify it or at least name constants. - 1.0 / (2f64.powf((self.cents as f64 + 1.0) / (12.0 * FineTune::CENTS_PER_NOTE as f64)) + 1.0 / (2f64.powf((self.cents.clamp(0, 15488) as f64 + 1.0) / (12.0 * FineTune::CENTS_PER_NOTE as f64)) * 8363.0 // TODO(nenikitov): This is `2^20`, which is divided by `2048` and `8192` results in `1/16` * 1048576.0 diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 5ab0f1c..846625a 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -341,10 +341,7 @@ impl<'a> Player<'a> { finetune: Some(finetune), .. }) => { - channel.note.finetune = channel - .note - .finetune - .map(|f| (f + finetune).clamp(FineTune::new(0), FineTune::new(15488))); + channel.note.finetune = channel.note.finetune.map(|f| (f + finetune)); } E::NoteDelay(_) => { channel.note_delay = channel.note_delay.saturating_sub(1); diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index 90e9787..fb3c98f 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -270,15 +270,15 @@ impl TSample { } fn normalize(&self, position: usize) -> Option { - if position >= self.buffer.data.len() && self.loop_length == 0 { + if position < self.buffer.len_samples() { + Some(position) + } else if self.loop_length == 0 { None } else { - let mut position = position; - while position >= self.buffer.data.len() { - position -= self.loop_length; - } - - Some(position) + Some( + self.buffer.len_samples() - self.loop_length + + (position - self.buffer.len_samples()) % self.loop_length, + ) } } } From ff4e408570011c214c16718d38ce08681054a664 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 15:25:50 -0400 Subject: [PATCH 79/87] perf(sample): remove sample computations when volume envelope ended --- engine/src/asset/sound/dat/mixer.rs | 57 +++++++++++++++++------------ 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 846625a..5f99b29 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -65,11 +65,37 @@ impl PlayerChannel { self.direction = PlaybackDirection::Forwards; } - fn generate_sample(&mut self, step: f64) -> f32 { - let current_sample = if let Some(instrument) = &self.instrument + fn compute_volume_envelope(&self) -> Option { + self.instrument.as_ref().and_then(|i| match &i.volume { + TInstrumentVolume::Envelope(envelope) => { + if self.note.on { + Some( + envelope + .volume_beginning() + .get(self.pos_volume_envelope) + .copied() + .unwrap_or(envelope.volume_loop()), + ) + } else { + envelope + .volume_end() + .get( + self.pos_volume_envelope + .saturating_sub(envelope.volume_beginning().len()), + ) + .copied() + } + } + TInstrumentVolume::Constant(_) => Some(1.), + }) + } + + fn generate_current_sample(&mut self, step: f64) -> f32 { + if let Some(instrument) = &self.instrument && let Some(sample) = &self.sample && let Some(note) = self.note.finetune && self.note_delay == 0 + && let Some(volume_envelope) = self.compute_volume_envelope() && let Some(value) = sample.get(self.pos_sample) { let pitch_factor = (note + sample.finetune).pitch_factor(); @@ -79,27 +105,6 @@ impl PlayerChannel { PlaybackDirection::Backwards => -step, }; - let volume_envelope = match &instrument.volume { - TInstrumentVolume::Envelope(envelope) => { - if self.note.on { - envelope - .volume_beginning() - .get(self.pos_volume_envelope) - .copied() - .unwrap_or(envelope.volume_loop()) - } else { - envelope - .volume_end() - .get( - self.pos_volume_envelope - .saturating_sub(envelope.volume_beginning().len()), - ) - .copied() - .unwrap_or(0.) - } - } - TInstrumentVolume::Constant(_) => 1., - }; self.volume_target = volume_envelope * self.volume; self.volume_actual = advance_to( self.volume_actual, @@ -110,7 +115,11 @@ impl PlayerChannel { value.into_normalized_f32() * self.volume_actual } else { 0. - }; + } + } + + fn generate_sample(&mut self, step: f64) -> f32 { + let current_sample = self.generate_current_sample(step); if let Some((previous, position)) = &mut self.previous { let factor = (*position / Self::SAMPLE_BLEND) as f32; From 493ff226c5e4680c30dd2dd12f3e8b7fd3bf0469 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 17:09:04 -0400 Subject: [PATCH 80/87] perf(finetune): precompute all possible finetunes --- engine/src/asset/sound/dat/finetune.rs | 37 ++++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 7716054..0207af9 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -1,12 +1,36 @@ -use std::ops::{Add, AddAssign, Neg, Sub}; +use std::{ + ops::{Add, AddAssign, Neg, Sub}, + sync::LazyLock, +}; + +use crate::utils::iterator::CollectArray; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct FineTune { cents: i32, } +static PITCH_FACTORS: LazyLock<[f64; FineTune::MAX]> = LazyLock::new(|| { + // TODO(nenikitov): This formula is from the game + // And it's very magic. + // Maybe simplify it or at least name constants. + (0..FineTune::MAX) + .map(|i| i as f64) + .map(|cents| { + 1.0 / (2f64.powf(cents / (12.0 * FineTune::CENTS_PER_NOTE as f64)) + * 8363.0 + // TODO(nenikitov): This is `2^20`, which is divided by `2048` and `8192` results in `1/16` + * 1048576.0 + / 16000.0 + / 2048.0 + / 8192.0) + }) + .collect_array() +}); + impl FineTune { const CENTS_PER_NOTE: i32 = 128; + const MAX: usize = 15488; pub const fn new(cents: i32) -> Self { Self { cents } @@ -17,16 +41,7 @@ impl FineTune { } pub fn pitch_factor(self) -> f64 { - // TODO(nenikitov): This formula is from the game - // And it's very magic. - // Maybe simplify it or at least name constants. - 1.0 / (2f64.powf((self.cents.clamp(0, 15488) as f64 + 1.0) / (12.0 * FineTune::CENTS_PER_NOTE as f64)) - * 8363.0 - // TODO(nenikitov): This is `2^20`, which is divided by `2048` and `8192` results in `1/16` - * 1048576.0 - / 16000.0 - / 2048.0 - / 8192.0) + PITCH_FACTORS[(self.cents as usize).clamp(0, Self::MAX)] } pub fn cents(self) -> i32 { From 1554f4af47b0c98723416d2b5ae109ca391b54a0 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Wed, 9 Oct 2024 20:26:00 -0400 Subject: [PATCH 81/87] fix(effects): off by 1 error in backwards playback --- engine/src/asset/sound/dat/mixer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 5f99b29..65ce245 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -486,7 +486,9 @@ impl<'a> Player<'a> { if let Some(sample) = &channel.sample && direction == PlaybackDirection::Backwards { - channel.pos_sample = sample.buffer.len_seconds(); + channel.pos_sample = sample + .buffer + .index_to_seconds(sample.buffer.len_samples().saturating_sub(1)); } } E::SampleOffset(Some(offset)) => { From 96ec9590b4f832d3b5529977a37e055ae23def20 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Thu, 10 Oct 2024 21:56:43 -0400 Subject: [PATCH 82/87] chore: address comments --- engine/src/asset/sound/dat/finetune.rs | 3 +- engine/src/asset/sound/dat/mixer.rs | 85 ++++++++++------------ engine/src/asset/sound/dat/t_instrument.rs | 2 +- engine/src/asset/sound/sample.rs | 2 +- 4 files changed, 41 insertions(+), 51 deletions(-) diff --git a/engine/src/asset/sound/dat/finetune.rs b/engine/src/asset/sound/dat/finetune.rs index 0207af9..357daba 100644 --- a/engine/src/asset/sound/dat/finetune.rs +++ b/engine/src/asset/sound/dat/finetune.rs @@ -15,9 +15,8 @@ static PITCH_FACTORS: LazyLock<[f64; FineTune::MAX]> = LazyLock::new(|| { // And it's very magic. // Maybe simplify it or at least name constants. (0..FineTune::MAX) - .map(|i| i as f64) .map(|cents| { - 1.0 / (2f64.powf(cents / (12.0 * FineTune::CENTS_PER_NOTE as f64)) + 1.0 / (2f64.powf(cents as f64 / (12.0 * FineTune::CENTS_PER_NOTE as f64)) * 8363.0 // TODO(nenikitov): This is `2^20`, which is divided by `2048` and `8192` results in `1/16` * 1048576.0 diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 65ce245..6518b31 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -40,18 +40,18 @@ struct PlayerChannel { } impl PlayerChannel { - // Length of a transition between the current and the next sample in seconds - // Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. - // Too little and transitions between notes will click. - // Chosen value is a bit of an arbitrary value that I found sounds nice. - // It amounts to: - // - 13 samples at 16000 - // - 35 samples at 44100 - // - 38 samples at 48000 + /// Length of a transition between the current and the next sample in seconds + /// Too large of a time and samples will audibly blend and play 2 notes at the same time, which sounds weird. + /// Too little and transitions between notes will click. + /// Chosen value is a bit of an arbitrary value that I found sounds nice. + /// It amounts to: + /// - 13 samples at 16000 + /// - 35 samples at 44100 + /// - 38 samples at 48000 const SAMPLE_BLEND: f64 = Duration::from_micros(800).as_secs_f64(); - // Maximum difference in volume between 2 audio samples - // Volume as in channels volume, does not account for samples - // A bit of an arbitrary amount too + /// Maximum difference in volume between 2 audio samples + /// Volume as in channels volume, does not account for samples + /// A bit of an arbitrary amount too const MAX_VOLUME_CHANGE: f32 = 1. / 128.; fn note_cut(&mut self) { @@ -69,19 +69,17 @@ impl PlayerChannel { self.instrument.as_ref().and_then(|i| match &i.volume { TInstrumentVolume::Envelope(envelope) => { if self.note.on { - Some( - envelope - .volume_beginning() - .get(self.pos_volume_envelope) - .copied() - .unwrap_or(envelope.volume_loop()), - ) + envelope + .volume_start() + .get(self.pos_volume_envelope) + .copied() + .or_else(|| Some(envelope.volume_loop())) } else { envelope .volume_end() .get( self.pos_volume_envelope - .saturating_sub(envelope.volume_beginning().len()), + .saturating_sub(envelope.volume_start().len()), ) .copied() } @@ -130,7 +128,7 @@ impl PlayerChannel { self.previous = None; } - previous_sample + factor * (current_sample - previous_sample) + factor.mul_add(current_sample - previous_sample, previous_sample) } else { current_sample } @@ -164,9 +162,7 @@ impl PlayerChannel { fn change_note(&mut self, note: PatternEventNote) { if let Some(instrument) = &self.instrument { match note { - PatternEventNote::Off => { - self.note.on = false; - } + PatternEventNote::Off => self.note.on = false, PatternEventNote::On(note) => { self.note.finetune = Some(note); self.note.finetune_initial = Some(note); @@ -226,35 +222,30 @@ impl PlayerChannel { } } -trait MinMax { - fn generic_min(a: Self, b: Self) -> Self; - fn generic_max(a: Self, b: Self) -> Self; -} - -macro_rules! impl_min_max { - ($($ty:tt),*) => { - $(impl MinMax for $ty { - fn generic_min(a: Self, b: Self) -> Self { - $ty::min(a, b) - } - - fn generic_max(a: Self, b: Self) -> Self { - $ty::max(a, b) - } - })* - }; -} - -impl_min_max!(f32, FineTune); - fn advance_to(from: T, to: T, step: T) -> T where - T: PartialOrd + Add + Sub + MinMax, + T: PartialOrd + Add + Sub, { use std::cmp::Ordering; + fn partial_min(input: T, min: T) -> T { + if input < min { + min + } else { + input + } + } + fn partial_max(input: T, max: T) -> T { + if input > max { + max + } else { + input + } + } match from.partial_cmp(&to) { - Some(Ordering::Less) => T::generic_min(from + step, to), - Some(Ordering::Greater) => T::generic_max(from - step, to), + Some(Ordering::Less) => partial_max(from + step, to), + Some(Ordering::Greater) => partial_min(from - step, to), + // Calling `clamp_max` and `clamp_min` with `f32` or `f64` is "safe", because + // `from.partial_cmp(&to)` would return `None` if either of the values are `NAN`. Some(Ordering::Equal) | None => from, } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index fb3c98f..e5ed8e5 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -43,7 +43,7 @@ pub struct TInstrumentVolumeEnvelope { } impl TInstrumentVolumeEnvelope { - pub fn volume_beginning(&self) -> &[f32] { + pub fn volume_start(&self) -> &[f32] { if let Some(sustain) = self.sustain { &self.data[0..sustain] } else { diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 70e8ecd..77bfd67 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, mem::size_of, ops::Index}; +use std::{fmt::Debug, ops::Index}; pub enum AudioSamplePointFormat { Int, From dca4c2cb7c09403e5cb856a0fb854ff42ab69bb9 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Thu, 10 Oct 2024 22:28:08 -0400 Subject: [PATCH 83/87] refactor: volume envelope --- engine/src/asset/sound/dat/mixer.rs | 33 ++++++++++--------- engine/src/asset/sound/dat/pattern_event.rs | 2 +- engine/src/asset/sound/dat/t_instrument.rs | 36 ++++++++++----------- engine/src/asset/sound/sample.rs | 2 +- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 6518b31..1dfa41d 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -52,22 +52,22 @@ impl PlayerChannel { /// Maximum difference in volume between 2 audio samples /// Volume as in channels volume, does not account for samples /// A bit of an arbitrary amount too - const MAX_VOLUME_CHANGE: f32 = 1. / 128.; + const MAX_VOLUME_CHANGE: f32 = 1.0 / 128.0; fn note_cut(&mut self) { - self.volume = 0.; - self.volume_actual = 0.; + self.volume = 0.0; + self.volume_actual = 0.0; } fn pos_reset(&mut self) { - self.pos_sample = 0.; + self.pos_sample = 0.0; self.pos_volume_envelope = 0; self.direction = PlaybackDirection::Forwards; } fn compute_volume_envelope(&self) -> Option { - self.instrument.as_ref().and_then(|i| match &i.volume { - TInstrumentVolume::Envelope(envelope) => { + self.instrument.as_ref().and_then(|i| { + if let Some(envelope) = &i.volume { if self.note.on { envelope .volume_start() @@ -83,8 +83,9 @@ impl PlayerChannel { ) .copied() } + } else { + Some(1.0) } - TInstrumentVolume::Constant(_) => Some(1.), }) } @@ -139,7 +140,7 @@ impl PlayerChannel { // Disregard previous state before `self.clone` so we don't have a fully recursive structure. self.previous = None; - self.previous = Some((Box::new(self.clone()), 0.)); + self.previous = Some((Box::new(self.clone()), 0.0)); self.pos_reset(); } @@ -207,7 +208,7 @@ impl PlayerChannel { fn advance_envelopes(&mut self) { if let Some(instrument) = &self.instrument - && let TInstrumentVolume::Envelope(envelope) = &instrument.volume + && let Some(envelope) = &instrument.volume && (!self.note.on || envelope .sustain @@ -276,7 +277,7 @@ impl<'a> Player<'a> { Self { song, sample_rate, - time_in_tick: 0., + time_in_tick: 0.0, pos_loop: 0, pos_pattern: 0, pos_row: 0, @@ -284,8 +285,8 @@ impl<'a> Player<'a> { jump: None, tempo: song.speed as usize, bpm: song.bpm as usize, - volume_global_target: 1., - volume_global_actual: 0., + volume_global_target: 1.0, + volume_global_actual: 0.0, volume_amplification: amplification, channels: (0..song.orders[0][0].len()) .map(|_| PlayerChannel::default()) @@ -294,10 +295,10 @@ impl<'a> Player<'a> { } fn generate_sample(&mut self) -> S { - if self.time_in_tick <= 0. { + if self.time_in_tick <= 0.0 { self.tick(); } - let step = 1. / self.sample_rate as f64; + let step = 1.0 / self.sample_rate as f64; self.time_in_tick -= step; let sample = self @@ -327,7 +328,7 @@ impl<'a> Player<'a> { match *effect { // Tick effects E::Volume(Volume::Slide(Some(volume))) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); + channel.volume = (channel.volume + volume).clamp(0.0, 1.0); } E::Porta(Porta::Tone(Some(step))) => { if let Some(finetune_initial) = channel.note.finetune_initial { @@ -464,7 +465,7 @@ impl<'a> Player<'a> { volume: Some(volume), .. }) => { - channel.volume = (channel.volume + volume).clamp(0., 1.); + channel.volume = (channel.volume + volume).clamp(0.0, 1.0); } E::Porta(Porta::Bump { finetune: Some(finetune), diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index 73651d8..cd65856 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -101,7 +101,7 @@ pub enum PatternEventVolume { impl Default for PatternEventVolume { fn default() -> Self { - PatternEventVolume::Value(0.0) + PatternEventVolume::Value(0.) } } diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index e5ed8e5..d5b94ed 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -68,13 +68,7 @@ impl TInstrumentVolumeEnvelope { } } -#[derive(Debug)] -pub enum TInstrumentVolume { - Envelope(TInstrumentVolumeEnvelope), - Constant(f32), -} - -impl AssetParser for TInstrumentVolume { +impl AssetParser for Option { type Output = Self; type Context<'ctx> = bool; @@ -89,20 +83,18 @@ impl AssetParser for TInstrumentVolume { Ok(( input, - if has_envelope { + has_envelope.then(|| { let data = data .into_iter() .skip(begin as usize) .take(cmp::min(cmp::min(end, end_total), 325) as usize) .map(convert_volume) .collect::>(); - TInstrumentVolume::Envelope(TInstrumentVolumeEnvelope { + TInstrumentVolumeEnvelope { data, sustain: (sustain != u16::MAX).then_some((sustain - begin) as usize), - }) - } else { - TInstrumentVolume::Constant(1.0) - }, + } + }), )) } } @@ -112,7 +104,7 @@ impl AssetParser for TInstrumentVolume { pub struct TInstrument { pub flags: TInstrumentFlags, - pub volume: TInstrumentVolume, + pub volume: Option, pub pan_begin: u16, pub pan_end: u16, @@ -141,10 +133,12 @@ impl AssetParser for TInstrument { let (input, _) = bytes::take(1usize)(input)?; - let (input, volume_envelope) = TInstrumentVolume::parser( + let (input, volume_envelope) = Option::::parser( flags.contains(TInstrumentFlags::HasVolumeEnvelope), )(input)?; + // TODO(nenikitov): None of the instruments (except some weird one in the very first song I believe) use pan + // See if this is needed let (input, pan_begin) = number::le_u16(input)?; let (input, pan_end) = number::le_u16(input)?; let (input, pan_sustain) = number::le_u16(input)?; @@ -153,11 +147,17 @@ impl AssetParser for TInstrument { let (input, _) = bytes::take(1usize)(input)?; + // TODO(nenikitov): None of the instruments use vibrato + // See if this is needed let (input, vibrato_depth) = number::le_u8(input)?; let (input, vibrato_speed) = number::le_u8(input)?; let (input, vibrato_sweep) = number::le_u8(input)?; + // TODO(nenikitov): There is some variation (0, 256, 1024) + // But it's only used by effects? let (input, fadeout) = number::le_u32(input)?; + // TODO(nenikitov): There is some variation (0, 256) + // But it's only used by effects? let (input, vibrato_table) = number::le_u32(input)?; let (input, sample_indexes): (_, [_; 96]) = multi::count!(number::le_u8)(input)?; @@ -252,7 +252,7 @@ impl TSample { // TODO(nenikitov): I think the whole `Sample` will need to be removed pub fn get(&self, position: f64) -> Option { - if position < 0. { + if position < 0.0 { return None; } @@ -264,9 +264,9 @@ impl TSample { let prev = self.buffer[prev] as f32; let next = self.normalize(position as usize + 1); - let next = next.map_or(0., |next| self.buffer[next] as f32); + let next = next.map_or(0.0, |next| self.buffer[next] as f32); - Some((prev + frac * (next - prev)) as i16) + Some(frac.mul_add(next - prev, prev) as i16) } fn normalize(&self, position: usize) -> Option { diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 77bfd67..474a9b7 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -34,7 +34,7 @@ impl AudioSamplePoint for i16 { } fn from_normalized_f32(value: f32) -> Self { - if value < 0. { + if value < 0.0 { -(value * Self::MIN as f32) as Self } else { (value * Self::MAX as f32) as Self From abaa85edb39615029c8759d5dea349b67dcc00f8 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 11 Oct 2024 13:53:59 -0400 Subject: [PATCH 84/87] chore: update readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 205f8d3..cc635bf 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,6 @@ File parsing is in test suite only, for now. - **Purpose** - Sound effects - Music - - **TO DO** - - Improve mixer with pitch, pan, and other sound effects - - Support sustained instruments - - Set correct tempo - **Output format** - WAV audio file - [x] String table From c044cab3ee41864946efdfeab41e842ad7338192 Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 11 Oct 2024 17:58:57 -0400 Subject: [PATCH 85/87] chore: more comments --- engine/src/asset/color_map.rs | 6 ++--- engine/src/asset/sound/dat/mixer.rs | 2 +- engine/src/asset/sound/dat/pattern_effect.rs | 6 ++--- engine/src/asset/sound/dat/t_effect.rs | 1 - engine/src/asset/sound/dat/t_instrument.rs | 25 +++++++++++++------- engine/src/asset/sound/mod.rs | 6 ++--- engine/src/asset/sound/sample.rs | 1 + engine/src/utils/format.rs | 4 ++-- engine/src/utils/iterator.rs | 6 ++--- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/engine/src/asset/color_map.rs b/engine/src/asset/color_map.rs index 79053ff..4b19571 100644 --- a/engine/src/asset/color_map.rs +++ b/engine/src/asset/color_map.rs @@ -1,4 +1,4 @@ -use std::{mem, ops::Deref}; +use std::ops::Deref; use super::{extension::*, AssetParser}; use crate::{error, utils::nom::*}; @@ -64,7 +64,7 @@ impl AssetParser for ColorMap { move |input| { error::ensure_bytes_length( input, - mem::size_of::() * COLORS_COUNT * SHADES_COUNT, + size_of::() * COLORS_COUNT * SHADES_COUNT, "Incorrect `ColorMap` format (256x32 array of 12-bit [padded to 32-bit] colors)", )?; @@ -78,7 +78,7 @@ impl AssetParser for ColorMap { let colors = { let colors = colors.into_boxed_slice(); // Ensure the original box is not dropped. - let mut colors = mem::ManuallyDrop::new(colors); + let mut colors = std::mem::ManuallyDrop::new(colors); // SAFETY: [_] and [_; N] has the same memory layout as long // as the slice contains exactly N elements. unsafe { Box::from_raw(colors.as_mut_ptr().cast()) } diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index 1dfa41d..cc1244b 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -195,7 +195,7 @@ impl PlayerChannel { fn change_effect(&mut self, i: usize, effect: PatternEffect) { // Recall from memory let effect = if let Some(key) = effect.memory_key() { - if !effect.is_empty() { + if effect.has_content() { self.effects_memory.insert(key, effect); } diff --git a/engine/src/asset/sound/dat/pattern_effect.rs b/engine/src/asset/sound/dat/pattern_effect.rs index f672c2b..b7b91a6 100644 --- a/engine/src/asset/sound/dat/pattern_effect.rs +++ b/engine/src/asset/sound/dat/pattern_effect.rs @@ -1,5 +1,3 @@ -use std::hash::Hash; - use super::{convert_volume, finetune::FineTune}; use crate::{ asset::{extension::*, AssetParser}, @@ -116,10 +114,10 @@ impl PatternEffect { } #[rustfmt::skip] - pub fn is_empty(&self) -> bool { + pub fn has_content(&self) -> bool { use PatternEffect as E; - matches!( + !matches!( self, E::Porta( Porta::Tone(None) diff --git a/engine/src/asset/sound/dat/t_effect.rs b/engine/src/asset/sound/dat/t_effect.rs index 08e9716..1966b12 100644 --- a/engine/src/asset/sound/dat/t_effect.rs +++ b/engine/src/asset/sound/dat/t_effect.rs @@ -14,7 +14,6 @@ pub struct TEffect { sample: Rc, } -// It should be separated impl TEffect { pub fn mix(&self) -> AudioBuffer { self.sample.buffer.clone() diff --git a/engine/src/asset/sound/dat/t_instrument.rs b/engine/src/asset/sound/dat/t_instrument.rs index d5b94ed..53972f0 100644 --- a/engine/src/asset/sound/dat/t_instrument.rs +++ b/engine/src/asset/sound/dat/t_instrument.rs @@ -79,7 +79,7 @@ impl AssetParser for Option { let (input, end) = number::le_u16(input)?; let (input, sustain) = number::le_u16(input)?; let (input, end_total) = number::le_u16(input)?; - let (input, data) = multi::count!(number::le_u8, 325)(input)?; + let (input, data) = multi::count!(number::le_u8, TInstrument::ENVELOPE_SIZE)(input)?; Ok(( input, @@ -87,9 +87,12 @@ impl AssetParser for Option { let data = data .into_iter() .skip(begin as usize) - .take(cmp::min(cmp::min(end, end_total), 325) as usize) + .take( + cmp::min(cmp::min(end, end_total), TInstrument::ENVELOPE_SIZE as u16) + as usize, + ) .map(convert_volume) - .collect::>(); + .collect(); TInstrumentVolumeEnvelope { data, sustain: (sustain != u16::MAX).then_some((sustain - begin) as usize), @@ -110,7 +113,7 @@ pub struct TInstrument { pub pan_end: u16, pub pan_sustain: u16, pub pan_envelope_border: u16, - pub pan_envelope: Box<[u8; 325]>, + pub pan_envelope: Box<[u8; Self::ENVELOPE_SIZE]>, pub vibrato_depth: u8, pub vibrato_speed: u8, @@ -122,6 +125,10 @@ pub struct TInstrument { pub samples: Box<[Option>; 96]>, } +impl TInstrument { + const ENVELOPE_SIZE: usize = 325; +} + impl AssetParser for TInstrument { type Output = Self; @@ -203,7 +210,7 @@ pub struct TSample { pub panning: u8, pub align: u8, pub finetune: FineTune, - pub loop_length: usize, + pub loop_len: usize, pub buffer: AudioBuffer, } @@ -236,7 +243,7 @@ impl AssetParser for TSample { panning, align, finetune: FineTune::new(finetune), - loop_length: loop_length as usize, + loop_len: loop_length as usize, buffer: AudioBuffer { data: sample_data[sample_offset as usize..loop_end as usize].to_vec(), sample_rate: Self::SAMPLE_RATE, @@ -272,12 +279,12 @@ impl TSample { fn normalize(&self, position: usize) -> Option { if position < self.buffer.len_samples() { Some(position) - } else if self.loop_length == 0 { + } else if self.loop_len == 0 { None } else { Some( - self.buffer.len_samples() - self.loop_length - + (position - self.buffer.len_samples()) % self.loop_length, + self.buffer.len_samples() - self.loop_len + + (position - self.buffer.len_samples()) % self.loop_len, ) } } diff --git a/engine/src/asset/sound/mod.rs b/engine/src/asset/sound/mod.rs index 7d4312a..946ccdb 100644 --- a/engine/src/asset/sound/mod.rs +++ b/engine/src/asset/sound/mod.rs @@ -92,13 +92,13 @@ mod tests { } }) .enumerate() - .try_for_each(|(i, (song, t))| -> std::io::Result<()> { + .try_for_each(|(i, (sound, song))| -> std::io::Result<()> { let file = output_dir.join(format!("{i:0>2X}.wav")); println!("# SONG {i}"); - output_file(file, song.mix().to_wave())?; + output_file(file, sound.mix().to_wave())?; let file = output_dir.join(format!("{i:0>2X}.txt")); - output_file(file, format!("{t:#?}"))?; + output_file(file, format!("{song:#?}"))?; Ok(()) })?; diff --git a/engine/src/asset/sound/sample.rs b/engine/src/asset/sound/sample.rs index 474a9b7..e757e7f 100644 --- a/engine/src/asset/sound/sample.rs +++ b/engine/src/asset/sound/sample.rs @@ -52,6 +52,7 @@ impl AudioSamplePoint for i16 { #[derive(Debug, Clone)] pub struct AudioBuffer { + // TODO(nenikitov): Make non-`pub` pub data: Vec, pub sample_rate: usize, } diff --git a/engine/src/utils/format.rs b/engine/src/utils/format.rs index 136da0e..88a2544 100644 --- a/engine/src/utils/format.rs +++ b/engine/src/utils/format.rs @@ -111,8 +111,8 @@ impl WaveFile for AudioBuffer { { const CHANNELS: usize = 1; - let bytes_per_sample: usize = S::SIZE_BYTES; - let bits_per_sample: usize = bytes_per_sample * 8; + let bytes_per_sample = S::SIZE_BYTES; + let bits_per_sample = bytes_per_sample * 8; let size = self.len_samples() * CHANNELS * bytes_per_sample; diff --git a/engine/src/utils/iterator.rs b/engine/src/utils/iterator.rs index 2f620cf..f767d9b 100644 --- a/engine/src/utils/iterator.rs +++ b/engine/src/utils/iterator.rs @@ -1,5 +1,3 @@ -use std::mem::{self, MaybeUninit}; - use itertools::Itertools; // Code from [this post](https://www.reddit.com/r/learnrust/comments/lfw6uy/comment/gn16m4o) @@ -7,9 +5,9 @@ pub trait CollectArray: Sized + Iterator { fn collect_array(self) -> [Self::Item; N] { // TODO(nenikitov): Replace with compile-time assertions or const generic expressions // When it will be supported. - assert!(N > 0 && mem::size_of::() > 0); + assert!(N > 0 && size_of::() > 0); - let mut array = MaybeUninit::<[Self::Item; N]>::uninit().transpose(); + let mut array = std::mem::MaybeUninit::<[Self::Item; N]>::uninit().transpose(); Itertools::zip_eq(array.iter_mut(), self).for_each(|(dest, item)| _ = dest.write(item)); From 9aec4b087cd0e1e38c5aa44b4734d8808d0b41cb Mon Sep 17 00:00:00 2001 From: nenikitov Date: Fri, 11 Oct 2024 18:01:12 -0400 Subject: [PATCH 86/87] chore: more comments --- engine/src/asset/sound/dat/mixer.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/engine/src/asset/sound/dat/mixer.rs b/engine/src/asset/sound/dat/mixer.rs index cc1244b..cecc64c 100644 --- a/engine/src/asset/sound/dat/mixer.rs +++ b/engine/src/asset/sound/dat/mixer.rs @@ -155,7 +155,6 @@ impl PlayerChannel { } else { // TODO(nenikitov): Idk honestly, figure this out self.note_cut(); - self.instrument = None; self.sample = None; } } @@ -516,9 +515,9 @@ impl TSongMixer for TSong { let mut player = Player::new(self, SAMPLE_RATE, AMPLIFICATION); - let samples: Vec<_> = std::iter::from_fn(|| { + let samples = std::iter::from_fn(|| { if player.pos_loop == 0 { - Some(player.generate_sample::()) + Some(player.generate_sample()) } else { None } From edd08f3bdebc830cb9b1b2883d459ffb167bba20 Mon Sep 17 00:00:00 2001 From: UserIsntAvailable Date: Fri, 11 Oct 2024 15:35:12 -0400 Subject: [PATCH 87/87] chore: change `0.` to `0.0` --- engine/src/asset/sound/dat/pattern_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/asset/sound/dat/pattern_event.rs b/engine/src/asset/sound/dat/pattern_event.rs index cd65856..73651d8 100644 --- a/engine/src/asset/sound/dat/pattern_event.rs +++ b/engine/src/asset/sound/dat/pattern_event.rs @@ -101,7 +101,7 @@ pub enum PatternEventVolume { impl Default for PatternEventVolume { fn default() -> Self { - PatternEventVolume::Value(0.) + PatternEventVolume::Value(0.0) } }