diff --git a/examples/file_load.rs b/examples/file_load.rs index cb54158..520c3f3 100644 --- a/examples/file_load.rs +++ b/examples/file_load.rs @@ -1,16 +1,69 @@ -use torque_tracker_engine::file; +use std::{ + fs, + io::{BufRead, BufReader}, +}; + +use torque_tracker_engine::{ + file::{self, SansIO, header::HeaderParser, pattern::PatternParser}, + project::song::Song, +}; + +fn load_file(path: &str) -> Song { + let file = fs::File::open(path).unwrap(); + + let mut buf = BufReader::new(file); + let mut header_parser = + HeaderParser::new(buf.fill_buf().unwrap().first_chunk().unwrap()).unwrap(); + buf.consume(HeaderParser::BASE_SIZE); + + let header = loop { + match header_parser.parse(buf.fill_buf().unwrap()) { + SansIO::Finished(res, amount) => { + buf.consume(amount); + break res.unwrap(); + } + SansIO::Pending(amount) => buf.consume(amount), + } + }; + + let mut song = Song::default(); + header.copy_values_into_song(&mut song); + + for (idx, ptr) in header + .pattern_offsets + .iter() + // number the slots + .enumerate() + // filter out empty slots, while keeping the numbering the same + .flat_map(|(idx, ptr)| ptr.map(|ptr| (idx, ptr))) + { + ptr.move_to_self(&mut buf).unwrap(); + let mut parser = + PatternParser::new(buf.fill_buf().unwrap().first_chunk().unwrap()).unwrap(); + buf.consume(PatternParser::HEADER_SIZE); + let pattern = loop { + match parser.parse(buf.fill_buf().unwrap()) { + SansIO::Finished(res, amount) => { + buf.consume(amount); + break res; + } + SansIO::Pending(amount) => buf.consume(amount), + } + }; + song.patterns[idx] = pattern; + } + + song +} fn main() { - // let file = include_bytes!("../test-files/test-1.it"); - // let mut reader = std::io::Cursor::new(&file); - // let song = file::parse_song(&mut reader); let path = std::env::args().nth(1).unwrap(); println!("path: {path}"); - // let file_content = std::fs::read(&path).unwrap(); - // assert!(file_content == file); let file = std::fs::File::open(&path).unwrap(); println!("file len: {}", file.metadata().unwrap().len()); - let mut reader = std::io::BufReader::new(file); - let song = file::parse_song(&mut reader).unwrap(); - println!("{song:?}"); + let mut buf = std::io::BufReader::new(file); + let song_read = file::parse_song(&mut buf).unwrap(); + + let song_sans = load_file(&path); + assert_eq!(song_read, song_sans); } diff --git a/src/file/err.rs b/src/file/err.rs index a601e14..1f4b1a6 100644 --- a/src/file/err.rs +++ b/src/file/err.rs @@ -44,3 +44,17 @@ impl Error for LoadErr {} // /// skips loading of the pointed to value // OutOfBoundsPtr, // } + +#[derive(Debug)] +pub enum LoadErr2 { + CantReadFile, + Invalid, + BufferTooShort, +} +impl Display for LoadErr2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl Error for LoadErr2 {} diff --git a/src/file/header.rs b/src/file/header.rs index 6166919..7dd6d77 100644 --- a/src/file/header.rs +++ b/src/file/header.rs @@ -1,5 +1,5 @@ use crate::{ - file::err, + file::{SansIO, err}, project::song::{Pan, PatternOrder}, }; use std::{ @@ -8,6 +8,8 @@ use std::{ num::{NonZero, NonZeroU32}, }; +use core::mem; + use crate::file::InFilePtr; fn order_try_from(value: u8) -> Option { @@ -28,7 +30,7 @@ fn pan_try_from(value: u8) -> Option { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct ImpulseHeader { // C-String pub song_name: [u8; 26], @@ -151,14 +153,13 @@ impl ImpulseHeader { // can unwrap here, because the length is already checked at the beginning let pan_vals: [u8; 64] = base[0x40..0x80].try_into().unwrap(); - // let channel_pan: [Pan; 64] = pan_vals.map(|pan| match Pan::try_from(pan) { - // Ok(pan) => pan, - // Err(_) => { - // // defect_handler(LoadDefect::OutOfBoundsValue); - // Pan::default() - // } - // }); - let channel_pan: [Pan; 64] = pan_vals.map(|pan| pan_try_from(pan).unwrap_or_default()); + let channel_pan: [Pan; 64] = pan_vals.map(|pan| match pan_try_from(pan) { + Some(pan) => pan, + None => { + // defect_handler(LoadDefect::OutOfBoundsValue); + Pan::default() + } + }); let channel_volume: [u8; 64] = { // can unwrap here, because the length is already checked at the beginning @@ -188,7 +189,7 @@ impl ImpulseHeader { }; let instr_offsets = { - let mut data = vec![0; usize::from(instr_num)].into_boxed_slice(); + let mut data = vec![0; usize::from(instr_num) * 4].into_boxed_slice(); reader.read_exact(&mut data)?; data.chunks_exact(std::mem::size_of::()) .map(|chunk| { @@ -205,7 +206,7 @@ impl ImpulseHeader { }; let sample_offsets = { - let mut data = vec![0; usize::from(sample_num)].into_boxed_slice(); + let mut data = vec![0; usize::from(sample_num * 4)].into_boxed_slice(); reader.read_exact(&mut data)?; data.chunks_exact(std::mem::size_of::()) .map(|chunk| { @@ -223,7 +224,7 @@ impl ImpulseHeader { }; let pattern_offsets = { - let mut data = vec![0; usize::from(pattern_num)].into_boxed_slice(); + let mut data = vec![0; usize::from(pattern_num * 4)].into_boxed_slice(); reader.read_exact(&mut data)?; data.chunks_exact(std::mem::size_of::()) .map(|chunk| { @@ -266,3 +267,255 @@ impl ImpulseHeader { }) } } + +pub struct HeaderParser { + // C-String + song_name: [u8; 26], + philight: u16, + + created_with: u16, + compatible_with: u16, + flags: u16, + special: u16, + + global_volume: u8, + mix_volume: u8, + initial_speed: u8, + initial_tempo: u8, + pan_separation: u8, + pitch_wheel_depth: u8, + message_length: u16, + message_offset: u32, + + channel_pan: [Pan; 64], + channel_volume: [u8; 64], + + order_num: usize, + orders: Vec, // length is oder_num + + /// all Offsets are verified to be point outside the header. + /// + /// Invalid offsets are replaced with None, so patterns or orders don't break, because the indexes change + instr_num: usize, + instr_offsets: Vec>, + sample_num: usize, + sample_offsets: Vec>, + pattern_num: usize, + /// here None could come from the file, which means an empty pattern + pattern_offsets: Vec>, +} + +impl HeaderParser { + pub const BASE_SIZE: usize = 0xC0; // = 192 + pub fn new(base: &[u8; Self::BASE_SIZE]) -> Result { + // verify that the start matches + if !base.starts_with(b"IMPM") { + return Err(err::LoadErr2::Invalid); + } + + let song_name = { + let mut name = [0; _]; + let c_str = CStr::from_bytes_until_nul(&base[0x4..=0x1D]); + if c_str.is_err() { + // defect_handler(LoadDefect::InvalidText) + } + // if there is an error use an empty string + let c_str = c_str.unwrap_or_default().to_bytes(); + // copy the name into the output buffer + name.iter_mut().zip(c_str).for_each(|(t, s)| *t = *s); + name + }; + + let philight = u16::from_le_bytes([base[0x1E], base[0x1F]]); + + let order_num = usize::from(u16::from_le_bytes([base[0x20], base[0x21]])); + let instr_num = usize::from(u16::from_le_bytes([base[0x22], base[0x23]])); + let sample_num = usize::from(u16::from_le_bytes([base[0x24], base[0x25]])); + let pattern_num = usize::from(u16::from_le_bytes([base[0x26], base[0x27]])); + let created_with = u16::from_le_bytes([base[0x28], base[0x29]]); + let compatible_with = u16::from_le_bytes([base[0x2A], base[0x2B]]); + let flags = u16::from_le_bytes([base[0x2C], base[0x2D]]); + let special = u16::from_le_bytes([base[0x2E], base[0x2F]]); + + let global_volume = if base[0x30] <= 128 { + base[0x30] + } else { + // defect_handler(LoadDefect::OutOfBoundsValue); + 64 + }; + + let mix_volume = if base[0x31] <= 128 { + base[0x31] + } else { + // defect_handler(LoadDefect::OutOfBoundsValue); + 64 + }; + + let initial_speed = base[0x32]; + let initial_tempo = base[0x33]; + let pan_separation = base[0x34]; + let pitch_wheel_depth = base[0x35]; + let message_length = u16::from_le_bytes([base[0x36], base[0x37]]); + let message_offset = u32::from_le_bytes([base[0x38], base[0x39], base[0x3A], base[0x3B]]); + let _reserved = u32::from_le_bytes([base[0x3C], base[0x3D], base[0x3E], base[0x3F]]); + + // can unwrap here, because the length is const + let pan_vals: [u8; 64] = base[0x40..0x80].try_into().unwrap(); + let channel_pan: [Pan; 64] = pan_vals.map(|pan| match pan_try_from(pan) { + Some(pan) => pan, + None => { + // defect_handler(LoadDefect::OutOfBoundsValue); + Pan::default() + } + }); + + let channel_volume: [u8; 64] = { + // can unwrap here, because the length is already checked at the beginning + let mut vols: [u8; 64] = base[0x80..0xC0].try_into().unwrap(); + + vols.iter_mut().for_each(|vol| { + if *vol > 64 { + // defect_handler(LoadDefect::OutOfBoundsValue); + *vol = 64 + } + }); + vols + }; + + Ok(Self { + song_name, + philight, + created_with, + compatible_with, + flags, + special, + global_volume, + mix_volume, + initial_speed, + initial_tempo, + pan_separation, + pitch_wheel_depth, + message_length, + message_offset, + channel_pan, + channel_volume, + orders: Vec::with_capacity(order_num), + instr_offsets: Vec::with_capacity(instr_num), + sample_offsets: Vec::with_capacity(sample_num), + pattern_offsets: Vec::with_capacity(pattern_num), + order_num, + instr_num, + sample_num, + pattern_num, + }) + } + + /// needs at most 4 bytes at once + pub fn parse(&mut self, mut buf: &[u8]) -> SansIO> { + assert!(self.orders.len() <= self.order_num); + assert!(self.instr_offsets.len() <= self.instr_num); + assert!(self.sample_offsets.len() <= self.sample_num); + // lesser than here, if it is equal the parser is already done + assert!(self.pattern_offsets.len() < self.pattern_num); + + let mut read = 0; + while self.orders.len() < self.order_num { + if let Some((val, new_buf)) = buf.split_first() { + buf = new_buf; + read += 1; + let order = match order_try_from(*val) { + Some(o) => o, + // report err + None => PatternOrder::EndOfSong, + }; + self.orders.push(order); + } else { + return SansIO::Pending(read); + } + } + + while self.instr_offsets.len() < self.instr_num { + if let Some((val, new_buf)) = buf.split_first_chunk() { + buf = new_buf; + read += val.len(); + let val = u32::from_le_bytes(*val); + let opt = if val <= Self::BASE_SIZE as u32 { + // handle defect + None + } else { + Some(InFilePtr(NonZeroU32::new(val).unwrap())) + }; + self.instr_offsets.push(opt); + } else { + return SansIO::Pending(read); + } + } + + while self.sample_offsets.len() < self.sample_num { + if let Some((val, new_buf)) = buf.split_first_chunk() { + buf = new_buf; + read += val.len(); + let val = u32::from_le_bytes(*val); + let opt = if val <= Self::BASE_SIZE as u32 { + // handle defect + None + } else { + Some(InFilePtr(NonZeroU32::new(val).unwrap())) + }; + self.sample_offsets.push(opt); + } else { + return SansIO::Pending(read); + } + } + + while self.pattern_offsets.len() < self.pattern_num { + if let Some((val, new_buf)) = buf.split_first_chunk() { + buf = new_buf; + read += val.len(); + let val = u32::from_le_bytes(*val); + let opt = if val == 0 { + // not a defect 0 is valid here + None + } else if val <= Self::BASE_SIZE as u32 { + // handle defect + None + } else { + Some(InFilePtr(NonZeroU32::new(val).unwrap())) + }; + self.pattern_offsets.push(opt); + } else { + return SansIO::Pending(read); + } + } + + assert!(self.orders.len() == self.order_num); + assert!(self.instr_offsets.len() == self.instr_num); + assert!(self.sample_offsets.len() == self.sample_num); + assert!(self.pattern_offsets.len() == self.pattern_num); + SansIO::Finished( + Ok(ImpulseHeader { + song_name: self.song_name, + philight: self.philight, + created_with: self.created_with, + compatible_with: self.compatible_with, + flags: self.flags, + special: self.special, + global_volume: self.global_volume, + mix_volume: self.mix_volume, + initial_speed: self.initial_speed, + initial_tempo: self.initial_tempo, + pan_separation: self.pan_separation, + pitch_wheel_depth: self.pitch_wheel_depth, + message_length: self.message_length, + message_offset: self.message_offset, + channel_pan: self.channel_pan, + channel_volume: self.channel_volume, + orders: mem::take(&mut self.orders).into_boxed_slice(), + instr_offsets: mem::take(&mut self.instr_offsets).into_boxed_slice(), + sample_offsets: mem::take(&mut self.sample_offsets).into_boxed_slice(), + pattern_offsets: mem::take(&mut self.pattern_offsets).into_boxed_slice(), + }), + read, + ) + } +} diff --git a/src/file/mod.rs b/src/file/mod.rs index 4cab4ba..798abf6 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -15,7 +15,7 @@ pub mod instrument; pub mod pattern; pub mod sample; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InFilePtr(pub(crate) std::num::NonZeroU32); impl InFilePtr { @@ -50,21 +50,30 @@ pub fn parse_song(reader: &mut R) -> Result { + Finished(F, usize), + Pending(usize), +} diff --git a/src/file/pattern.rs b/src/file/pattern.rs index 17bcb8b..5d8d41b 100644 --- a/src/file/pattern.rs +++ b/src/file/pattern.rs @@ -1,4 +1,4 @@ -use crate::file::err; +use crate::file::{SansIO, err}; use crate::project::event_command::NoteCommand; use crate::project::note_event::{Note, NoteEvent, VolumeEffect}; use crate::project::pattern::{InPatternPosition, Pattern}; @@ -6,6 +6,8 @@ use core::error::Error; use core::fmt::Display; use std::io::Read; +use core::mem; + /// reader should be buffered in some way and not do a syscall on every read call. /// /// This function does a lot of read calls @@ -40,6 +42,7 @@ pub fn parse_pattern(reader: &mut R) -> Result { let mut scratch = [0; 1]; while row_num < num_rows { + reader.read_exact(&mut scratch)?; let channel_variable = scratch[0]; if channel_variable == 0 { @@ -228,3 +231,170 @@ fn note_command_try_from( _ => Err(UnknownNoteCommand), } } + +pub struct PatternParser { + // TODO: maybe validate that the lenght of the pattern matches the value from the pattern header + // length: u64, + num_rows: u16, + // changed while parsing + pattern: Pattern, + current_row: u16, + last_mask: [u8; 64], + last_event: [NoteEvent; 64], +} + +impl PatternParser { + pub const HEADER_SIZE: usize = 8; + + pub fn new(header: &[u8; Self::HEADER_SIZE]) -> Result { + let (length, num_rows) = ( + u64::from(u16::from_le_bytes([header[0], header[1]])) + Self::HEADER_SIZE as u64, + u16::from_le_bytes([header[2], header[3]]), + ); + + // a guarantee given by the impulse tracker "specs" + if length >= 64_000 { + return Err(err::LoadErr2::Invalid); + } + + if !(32..=200).contains(&num_rows) { + return Err(err::LoadErr2::Invalid); + } + + Ok(Self { + num_rows, + pattern: Pattern::new(num_rows), + current_row: 0, + last_mask: [0; _], + last_event: [NoteEvent::default(); _], + }) + } + + /// needs at most 7 bytes at once + pub fn parse(&mut self, mut buf: &[u8]) -> SansIO { + let mut read = 0; + while self.current_row < self.num_rows { + let Some(&channel_variable) = buf.split_off_first() else { + return SansIO::Pending(read); + }; + if channel_variable == 0 { + self.current_row += 1; + // row done, commit the read byte + // otherwise need to finish event, before i can commit the read + read += 1; + continue; + } + let mut uncommited_read = 1; + + let channel = (channel_variable - 1) & 63; // 64 channels, 0 based + let channel_id = usize::from(channel); + + let maskvar = if (channel_variable & 0b10000000) != 0 { + let Some(&val) = buf.split_off_first() else { + return SansIO::Pending(read); + }; + uncommited_read += 1; + self.last_mask[channel_id] = val; + val + } else { + self.last_mask[channel_id] + }; + + // compute how many bytes i need based on the maskvar + let mut needed = 0; + // Note + if (maskvar & 0b0000_0001) != 0 { + if (maskvar & 0b0001_0000) != 0 { + // maybe report an error, as this doesn't make sense + // behaviour is the same as if this bit wasn't set, so maybe it doesn't matter + // same for the other three + } + needed += 1; + } + // Instrument / Sample + if (maskvar & 0b0000_0010) != 0 { + needed += 1; + } + // Volume + if (maskvar & 0b0000_0100) != 0 { + needed += 1; + } + // Effect + if (maskvar & 0b0000_1000) != 0 { + needed += 2; + } + + if buf.len() < needed { + return SansIO::Pending(read); + } + // now i am sure that i have enough for the whole event + read += needed + uncommited_read; + + // i can just unwrap from here on, the buffer is big enough + + let mut event = NoteEvent::default(); + + // Note + if (maskvar & 0b0000_0001) != 0 { + let note = *buf.split_off_first().unwrap(); + // TODO: handle invalid values + let note = Note::new(note).unwrap_or_default(); + event.note = note; + self.last_event[channel_id].note = note; + } + // Instrument / Sample + if (maskvar & 0b0000_0010) != 0 { + let sample_instr = *buf.split_off_first().unwrap(); + event.sample_instr = sample_instr; + self.last_event[channel_id].sample_instr = sample_instr; + } + // Volume + if (maskvar & 0b0000_0100) != 0 { + // TODO: handle error + let vol_pan = + volumeeffect_try_from(*buf.split_off_first().unwrap()).unwrap_or_default(); + event.vol = vol_pan; + self.last_event[channel_id].vol = vol_pan; + } + // Effect + if (maskvar & 0b0000_1000) != 0 { + let command = *buf.split_off_first().unwrap(); + let cmd_val = *buf.split_off_first().unwrap(); + // TODO: handle error + let cmd = note_command_try_from((command, cmd_val)).unwrap_or_default(); + event.command = cmd; + self.last_event[channel_id].command = cmd; + } + + // Same note + if (maskvar & 0b00010000) != 0 { + event.note = self.last_event[channel_id].note; + } + + // Same Instr / Sample + if (maskvar & 0b00100000) != 0 { + event.sample_instr = self.last_event[channel_id].sample_instr; + } + + // Same volume + if (maskvar & 0b01000000) != 0 { + event.vol = self.last_event[channel_id].vol; + } + + // Same Command + if (maskvar & 0b10000000) != 0 { + event.command = self.last_event[channel_id].command; + } + + self.pattern.set_event( + InPatternPosition { + row: self.current_row, + channel, + }, + event, + ); + } + let finished = mem::replace(&mut self.pattern, Pattern::new(self.num_rows)); + SansIO::Finished(finished, read) + } +} diff --git a/src/project/event_command.rs b/src/project/event_command.rs index 6d975eb..eca2225 100644 --- a/src/project/event_command.rs +++ b/src/project/event_command.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum NoteCommand { // TODO: figure out if i want to keep the None or start using it with Option around it #[default] diff --git a/src/project/note_event.rs b/src/project/note_event.rs index 7b4438a..5fcb03c 100644 --- a/src/project/note_event.rs +++ b/src/project/note_event.rs @@ -61,7 +61,7 @@ impl Default for Note { } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct NoteEvent { pub note: Note, pub sample_instr: u8, @@ -69,7 +69,7 @@ pub struct NoteEvent { pub command: NoteCommand, } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum VolumeEffect { FineVolSlideUp(u8), FineVolSlideDown(u8), diff --git a/src/project/pattern.rs b/src/project/pattern.rs index 0ec7fca..f438096 100644 --- a/src/project/pattern.rs +++ b/src/project/pattern.rs @@ -25,7 +25,7 @@ mod test { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Pattern { rows: u16, // Events are sorted with InPatternPosition as the key. diff --git a/src/project/song.rs b/src/project/song.rs index 81552dc..c2b1e33 100644 --- a/src/project/song.rs +++ b/src/project/song.rs @@ -6,7 +6,7 @@ use super::pattern::{Pattern, PatternOperation}; use crate::Collector; use crate::sample::{Sample, SampleMetaData}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Song { pub global_volume: u8, pub mix_volume: u8, @@ -186,7 +186,7 @@ impl simple_left_right::Absorb for Song { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Pan { /// Value ranges from 0 to 64, with 32 being center Value(u8), diff --git a/src/sample.rs b/src/sample.rs index c6d793b..7b1d9ca 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -34,7 +34,7 @@ pub(crate) trait ProcessingFunction { fn process(position: f32, data: &[Fr; N]) -> Fr; } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct Sample { // also stores the mono state on the heap to make the packing in arrays more efficient data: ThinArc, @@ -155,7 +155,7 @@ impl Debug for Sample { } } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum VibratoWave { #[default] Sine = 0, @@ -164,7 +164,7 @@ pub enum VibratoWave { Random = 3, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SampleMetaData { pub default_volume: u8, pub global_volume: u8,