use alloc::{boxed::Box, vec::Vec}; use crate::{ file::{SansIO, SansIOWritten, err}, project::{ pan::PackedPan, pattern_order::PackedPatternOrder, song::{ChannelVolume, Volume}, }, }; use core::{ ffi::CStr, num::{NonZero, NonZeroU32}, ops::Deref, }; use core::mem; use crate::file::InFilePtr; // 26 bytes, so copy should be fine #[derive(Eq, Clone, Copy)] pub struct SongName([u8; 26]); #[cfg(feature = "arbitrary")] impl<'a> arbitrary::Arbitrary<'a> for SongName { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let mut data = [0; 26]; let mut idx = 0; u.arbitrary_loop(None, Some(25), |u| { use core::ops::ControlFlow; data[idx] = u.arbitrary()?; idx += 1; Ok(ControlFlow::Continue(())) })?; Ok(Self(data)) } fn size_hint(_depth: usize) -> (usize, Option) { (0, Some(26)) } } impl Deref for SongName { type Target = CStr; fn deref(&self) -> &Self::Target { // could be unchecked CStr::from_bytes_until_nul(&self.0).unwrap() } } impl PartialEq for SongName { fn eq(&self, other: &Self) -> bool { // C-String comparison, so that the end is ignored, although it should always be null CStr::eq(&self, other.deref()) } } impl core::fmt::Debug for SongName { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { // debug this as a C-String f.debug_tuple("SongName").field(&self.deref()).finish() } } impl SongName { /// returns None if value is longer than 26 bytes (including null) pub fn new(value: &CStr) -> Option { let value = value.to_bytes(); if value.len() > 25 { None } else { let mut data = [0; 26]; data.iter_mut() .zip(value) .for_each(|(base, name)| *base = *name); Some(Self(data)) } } } #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct ImpulseHeader { // C-String pub song_name: SongName, pub philight: u16, pub created_with: u16, pub compatible_with: u16, pub flags: u16, pub special: u16, pub global_volume: Volume, pub mix_volume: Volume, pub initial_speed: NonZero, pub initial_tempo: NonZero, pub pan_separation: u8, pub pitch_wheel_depth: u8, pub message_length: u16, pub message_offset: u32, pub channel_pan: [PackedPan; 64], pub channel_volume: [ChannelVolume; 64], // file_size: 1 pub orders: Box<[PackedPatternOrder]>, // length is oder_num // file_size: 4 pub instr_offsets: Box<[InFilePtr]>, // file_size: 4 pub sample_offsets: Box<[InFilePtr]>, /// None means empty pattern // file_size: 4 pub pattern_offsets: Box<[Option]>, } #[derive(Debug, PartialEq, Eq)] pub struct ParsedImpulseHeader { // C-String pub song_name: SongName, pub philight: u16, pub created_with: u16, pub compatible_with: u16, pub flags: u16, pub special: u16, pub global_volume: Volume, pub mix_volume: Volume, pub initial_speed: Option>, pub initial_tempo: Option>, pub pan_separation: u8, pub pitch_wheel_depth: u8, pub message_length: u16, pub message_offset: u32, pub channel_pan: [PackedPan; 64], pub channel_volume: [ChannelVolume; 64], // file_size: 1 pub orders: Box<[PackedPatternOrder]>, // 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 // file_size: 4 pub instr_offsets: Box<[Option]>, // file_size: 4 pub sample_offsets: Box<[Option]>, /// other Option: if it was valid in the file /// inner Option: if it is there // file_size: 4 pub pattern_offsets: Box<[Option>]>, } impl TryFrom for ImpulseHeader { type Error = (); /// this should succeed if during parsing no defect was reported. TODO: defect reporting fn try_from(value: ParsedImpulseHeader) -> Result { if value.instr_offsets.iter().any(Option::is_none) || value.sample_offsets.iter().any(Option::is_none) || value.pattern_offsets.iter().any(Option::is_none) { return Err(()); } Ok(Self { song_name: value.song_name, philight: value.philight, created_with: value.created_with, compatible_with: value.compatible_with, flags: value.flags, special: value.special, global_volume: value.global_volume, mix_volume: value.mix_volume, initial_speed: value.initial_speed.ok_or(())?, initial_tempo: value.initial_tempo.ok_or(())?, pan_separation: value.pan_separation, pitch_wheel_depth: value.pitch_wheel_depth, message_length: value.message_length, message_offset: value.message_offset, channel_pan: value.channel_pan, channel_volume: value.channel_volume, orders: value.orders, instr_offsets: value .instr_offsets .iter() .copied() .map(Option::unwrap) .collect(), sample_offsets: value .sample_offsets .iter() .copied() .map(Option::unwrap) .collect(), pattern_offsets: value .pattern_offsets .iter() .copied() .map(Option::unwrap) .collect(), }) } } // https://github.com/schismtracker/schismtracker/wiki/ITTECH.TXT impl ImpulseHeader { pub const BASE_SIZE: usize = 0xC0; // = 192 /// compute the lenght of the header if written into a file. /// Should be used to place everything else first, as the header needs the position of /// the other stuff. pub fn file_len(&self) -> usize { Self::BASE_SIZE + self.orders.len() + (self.instr_offsets.len() + self.sample_offsets.len() + self.pattern_offsets.len()) * 4 } pub fn copy_from_song(&mut self, song: &crate::project::song::Song) { // keep in sync with `copy_values_into_song` self.global_volume = song.global_volume; self.initial_speed = song.initial_speed; self.initial_tempo = song.initial_tempo; self.mix_volume = song.mix_volume; self.pan_separation = song.pan_separation; self.pitch_wheel_depth = song.pitch_wheel_depth; self.channel_pan = song.pan; self.channel_volume = song.volume; self.orders = song .pattern_order .iter() .copied() .collect::>() .into_boxed_slice(); } pub fn write(&self, base: &mut [u8; Self::BASE_SIZE]) -> HeaderWriter { *base.first_chunk_mut().unwrap() = *b"IMPM"; // max 26 bytes written two times, maybe llvm fixes this maybe it doesn't base[0x4..=0x1D].fill(0); base[0x4..=0x1D] .iter_mut() .zip(&self.song_name.0) .for_each(|(base, name)| *base = *name); let (u16_chunks, []) = base[0x1E..=0x2F].as_chunks_mut() else { panic!() }; u16_chunks[0] = self.philight.to_le_bytes(); u16_chunks[1] = u16::try_from(self.orders.len()).unwrap().to_le_bytes(); u16_chunks[2] = u16::try_from(self.instr_offsets.len()) .unwrap() .to_le_bytes(); u16_chunks[3] = u16::try_from(self.sample_offsets.len()) .unwrap() .to_le_bytes(); u16_chunks[4] = u16::try_from(self.pattern_offsets.len()) .unwrap() .to_le_bytes(); u16_chunks[5] = self.created_with.to_le_bytes(); u16_chunks[6] = self.compatible_with.to_le_bytes(); u16_chunks[7] = self.flags.to_le_bytes(); u16_chunks[8] = self.special.to_le_bytes(); base[0x30] = self.global_volume.get(); base[0x31] = self.mix_volume.get(); base[0x32] = self.initial_speed.get(); base[0x33] = self.initial_tempo.get(); base[0x34] = self.pan_separation; base[0x35] = self.pitch_wheel_depth; *base[0x36..=0x37].first_chunk_mut().unwrap() = self.message_length.to_le_bytes(); *base[0x38..=0x3B].first_chunk_mut().unwrap() = self.message_offset.to_le_bytes(); // reserved base[0x3C] = 0; base[0x3D] = 0; base[0x3E] = 0; base[0x3F] = 0; base[0x40..0x80] .iter_mut() .zip(self.channel_pan) .for_each(|(base, pan)| *base = pan.get_inner()); *base[0x80..0xC0].first_chunk_mut().unwrap() = self.channel_volume.map(ChannelVolume::get); HeaderWriter { order_num: 0, orders: self.orders.clone(), instr_num: 0, instr_offsets: self.instr_offsets.clone(), sample_num: 0, sample_offsets: self.sample_offsets.clone(), pattern_num: 0, pattern_offsets: self.pattern_offsets.clone(), } } /// takes the values that are included in Song from the header and write them to the song. /// /// Mostly applies to metadata about the song. /// Samples, patterns, instruments are not filled as they are not included in the header pub fn copy_values_into_song(&self, song: &mut crate::project::song::Song) { // keep in sync with `copy_values_from_song` song.global_volume = self.global_volume; // TODO: figure out if i want to error here or when parsing the Header song.initial_speed = self.initial_speed; song.initial_tempo = self.initial_tempo; song.mix_volume = self.mix_volume; song.pan_separation = self.pan_separation; song.pitch_wheel_depth = self.pitch_wheel_depth; song.pan = self.channel_pan; song.volume = self.channel_volume; self.orders .iter() .copied() .zip(song.pattern_order.iter_mut()) .for_each(|(header, song)| *song = header); } pub fn parse(base: &[u8; Self::BASE_SIZE]) -> Result { // verify that the start matches if !base.starts_with(b"IMPM") { return Err(err::LoadErr::Invalid); } let song_name = { 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 // can never be none, as it can't be too long SongName::new(c_str.unwrap_or_default()).unwrap() }; 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]]); // TODO: report defect let global_volume = Volume::new(base[0x30]).unwrap_or(const { Volume::new(64).unwrap() }); // TODO: report defect let mix_volume = Volume::new(base[0x31]).unwrap_or(const { Volume::new(64).unwrap() }); let initial_speed = NonZero::new(base[0x32]); if initial_speed == None { // report defect } let initial_tempo = NonZero::new(base[0x33]); if initial_tempo == None { // report defect } 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: [PackedPan; 64] = pan_vals.map(|pan| PackedPan::new(pan).unwrap_or_default()); let channel_volume = <[u8; 64]>::try_from(&base[0x80..0xC0]) // .try_into() .unwrap() .map(|v| ChannelVolume::new(v).unwrap_or(const { ChannelVolume::new(64).unwrap() })); // 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(HeaderParser { 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, }) } } pub struct HeaderParser { // C-String song_name: SongName, philight: u16, created_with: u16, compatible_with: u16, flags: u16, special: u16, global_volume: Volume, mix_volume: Volume, initial_speed: Option>, initial_tempo: Option>, pan_separation: u8, pitch_wheel_depth: u8, message_length: u16, message_offset: u32, channel_pan: [PackedPan; 64], channel_volume: [ChannelVolume; 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, /// inner option comes from the file means empty pattern pattern_offsets: Vec>>, } impl HeaderParser { /// needs at most 4 bytes at once pub fn parse(&mut self, mut buf: &[u8]) -> SansIO { assert!(self.orders.len() <= self.order_num || self.order_num == 0); assert!(self.instr_offsets.len() <= self.instr_num || self.instr_num == 0); assert!(self.sample_offsets.len() <= self.sample_num || self.sample_num == 0); // lesser than here, if it is equal the parser is already done // if you hit this you drive the parser wrong. You shouldn't call parse if it returned Finished once. assert!(self.pattern_offsets.len() < self.pattern_num || self.pattern_num == 0); let mut read = 0; while self.orders.len() < self.order_num { if let Some(&val) = buf.split_off_first() { read += 1; let order = if let Some(order) = PackedPatternOrder::new(val) { order } else { // report err PackedPatternOrder::default() }; 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 <= ImpulseHeader::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 <= ImpulseHeader::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 Some(None) } else if val <= ImpulseHeader::BASE_SIZE as u32 { // handle defect None } else { Some(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( ParsedImpulseHeader { 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, ) } } pub struct HeaderWriter { order_num: usize, orders: Box<[PackedPatternOrder]>, instr_num: usize, instr_offsets: Box<[InFilePtr]>, sample_num: usize, sample_offsets: Box<[InFilePtr]>, /// here None should go into the pattern, which means an empty pattern // file_size: 4 pattern_num: usize, pattern_offsets: Box<[Option]>, } impl HeaderWriter { pub fn write(&mut self, mut buf: &mut [u8]) -> SansIOWritten { assert!(self.order_num <= self.orders.len()); assert!(self.instr_num <= self.instr_offsets.len()); assert!(self.sample_num <= self.sample_offsets.len()); // lesser than here, if it is equal the parser is already done assert!(self.pattern_num <= self.pattern_offsets.len()); let mut read = 0; while self.order_num < self.orders.len() { if let Some(w) = buf.split_off_first_mut() { read += 1; *w = self.orders[self.order_num].get_inner(); self.order_num += 1; } else { return SansIO::Pending(read); } } while self.instr_num < self.instr_offsets.len() { if let Some((w, new_buf)) = buf.split_first_chunk_mut() { buf = new_buf; read += 4; *w = self.instr_offsets[self.instr_num].get().get().to_le_bytes(); self.instr_num += 1; } else { return SansIO::Pending(read); } } while self.sample_num < self.sample_offsets.len() { if let Some((w, new_buf)) = buf.split_first_chunk_mut() { buf = new_buf; read += 4; *w = self.sample_offsets[self.sample_num] .get() .get() .to_le_bytes(); self.sample_num += 1; } else { return SansIO::Pending(read); } } while self.pattern_num < self.pattern_offsets.len() { if let Some((w, new_buf)) = buf.split_first_chunk_mut() { buf = new_buf; read += 4; *w = if let Some(ptr) = self.pattern_offsets[self.pattern_num] { ptr.get().get() } else { 0 } .to_le_bytes(); self.pattern_num += 1; } 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((), read) } }