diff options
author | Chris Xiong <chirs241097@gmail.com> | 2022-11-07 17:15:15 -0500 |
---|---|---|
committer | Chris Xiong <chirs241097@gmail.com> | 2022-11-07 17:15:15 -0500 |
commit | 7f49730fd8a6dc52fc45937e868749e5612c5234 (patch) | |
tree | 9729ec896e42279f9cc49e862c9405ac60cc6b42 /src/itfile.rs | |
download | it2midi-7f49730fd8a6dc52fc45937e868749e5612c5234.tar.xz |
(internal) initial commit
Diffstat (limited to 'src/itfile.rs')
-rw-r--r-- | src/itfile.rs | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/src/itfile.rs b/src/itfile.rs new file mode 100644 index 0000000..03c79ee --- /dev/null +++ b/src/itfile.rs @@ -0,0 +1,448 @@ +use std::io; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::fs::File; +use std::mem::{size_of, zeroed}; + +macro_rules! zero_default { + ($t:ty) => + { + impl Default for $t + { + fn default() -> Self + { unsafe { zeroed() } } + } + } +} + +#[repr(C, packed)] +pub struct Header +{ + magic: [u8; 4], + song_name: [u8; 26], + pat_highlight: u16, //PHiligt + nord: u16, //OrdNum + ninst: u16, //InsNum + nsamp: u16, //SmpNum + npatt: u16, //PatNum + tracker_version: u16, //Cwt/v + format_version: u16, //Cmwt + flags: u16, //Flags + special: u16, //Special + global_volume: u8, //GV + mix_volume: u8, //MV + speed: u8, //IS + tempo: u8, //IT + stereo_seperation: u8, //Sep + wheel_depth: u8, //PWD + msg_length: u16, //MsgLgth + msg_offset: u32, //Message Offset + reserved: u32, + channel_pan: [u8; 64], + channel_vol: [u8; 64] +} + +zero_default!{Header} + +impl Header +{ + fn song_name(self: &Self) -> String + { String::from_utf8_lossy(terminate_cstr(&self.song_name)).into_owned() } + fn has_message(self: &Self) -> bool + { self.special & 1 != 0 } +} + +#[repr(C, packed)] +pub struct InstHeader +{ + magic: [u8; 4], + dos_filename: [u8; 12], + zero: u8, //00h + override_action: u8, //NNA + duplicate_check: u8, //DCT + duplicate_action: u8, //DCA + fade_out: u16, //FadeOut + pitch_pan_separa: u8, //PPS + pitch_pan_center: u8, //PPC + global_volume: u8, //GbV + default_pan: u8, //DfP + rand_vol: u8, //RV + rand_pan: u8, //RP + tracker_version: u16, //TrkVers + nsamples: u8, //NoS + reserved: u8, //x + inst_name: [u8; 26], + filter_cutoff: u8, //IFC + filter_reso: u8, //IFR + midi_ch: u8, //MCh + midi_pc: u8, //MPr + midi_bank: u16, //MIDIBnk + sample_table: [u8; 240] +} + +zero_default!{InstHeader} + +#[repr(C, packed)] +#[derive(Default)] +pub struct EnvelopeNode {pub y: u8, pub t: u16} + +#[repr(C, packed)] +#[derive(Default)] +pub struct Envelope +{ + pub flag: u8, //Flg + pub nnodes: u8, //Num + pub loop_begin: u8, //LpB + pub loop_end: u8, //LpE + pub sust_begin: u8, //SLB + pub sust_end: u8, //SLE + pub nodes: [EnvelopeNode; 25] +} + +pub struct Inst +{ + pub header: InstHeader, + pub env_vol: Envelope, + pub env_pan: Envelope, + pub env_pit: Envelope +} + +impl Inst +{ + pub fn load(f: &mut File) -> Result<Inst, self::Error> + { + let header: InstHeader = read_struct(f)?; + let env_vol: Envelope = read_struct(f)?; + f.seek(SeekFrom::Start(1))?; + let env_pan: Envelope = read_struct(f)?; + f.seek(SeekFrom::Start(1))?; + let env_pit: Envelope = read_struct(f)?; + Ok(Inst{header, env_vol, env_pan, env_pit}) + } + pub fn inst_name(self: &Self) -> String + { String::from_utf8_lossy(terminate_cstr(&self.header.inst_name)).into_owned() } + pub fn filename(self: &Self) -> String + { String::from_utf8_lossy(terminate_cstr(&self.header.dos_filename)).into_owned() } +} + +#[repr(C, packed)] +#[derive(Default)] +pub struct SampHeader +{ + magic: [u8; 4], + dos_filename: [u8; 12], + zero: u8, + global_volume: u8, + flag: u8, + default_volume: u8, + sample_name: [u8; 26], + convert: u8, + default_pan: u8, + length: u32, + loop_begin: u32, + loop_end: u32, + c5_speed: u32, + sust_begin: u32, + sust_end: u32, + samp_ptr: u32, + vib_speed: u8, + vib_depth: u8, + vib_type: u8, + vib_rate: u8 +} + +pub struct Samp +{ + pub header: SampHeader +} + +impl Samp +{ + pub fn load(f: &mut File) -> Result<Samp, self::Error> + { + let header: SampHeader = read_struct(f)?; + Ok(Samp{header}) + } + pub fn sample_name(self: &Self) -> String + { String::from_utf8_lossy(terminate_cstr(&self.header.sample_name)).into_owned() } + pub fn filename(self: &Self) -> String + { String::from_utf8_lossy(terminate_cstr(&self.header.dos_filename)).into_owned() } +} + +#[derive(Copy, Clone, Default)] +pub struct Cell +{ + pub mask: u8, + pub note: u8, + pub inst: u8, + pub vol : u8, + pub efx : u8, + pub fxp : u8 +} + +pub struct Patt +{ + pub nrows: u16, + pub nch: u8, + pub data: Vec<Cell> +} + +impl Patt +{ + pub fn load(f: &mut File) -> Result<Patt, self::Error> + { + let mut chmem: [Cell; 64] = [Default::default(); 64]; + let datalen = read_u16(f)?; + let nrows = read_u16(f)?; + let nch: u8 = 64; + f.seek(SeekFrom::Current(4))?; + let mut data: Vec<Cell> = Vec::new(); + data.resize_with((nrows * (nch as u16)).into(), Default::default); + let ofst = f.stream_position()?; + for currow in 0..nrows + { + loop + { + let chvar = read_u8(f)?; + if chvar == 0 { break; } + let ch = ((chvar - 1) & 0x3f) as usize; + let Cell{mut mask, ..} = chmem[ch]; + if (chvar >> 7) != 0 + { + mask = read_u8(f)?; + chmem[ch].mask = mask; + } + let mut note = 0u8; + let mut inst = 0u8; + let mut vol = 0u8; + let mut efx = 0u8; + let mut fxp = 0u8; + if (mask & 0x01) != 0 { note = read_u8(f)?; chmem[ch] = Cell{note, ..chmem[ch]}; } + if (mask & 0x02) != 0 { inst = read_u8(f)?; chmem[ch] = Cell{inst, ..chmem[ch]}; } + if (mask & 0x04) != 0 { vol = read_u8(f)?; chmem[ch] = Cell{vol , ..chmem[ch]}; } + if (mask & 0x08) != 0 { efx = read_u8(f)?; + fxp = read_u8(f)?; chmem[ch] = Cell{efx, fxp, ..chmem[ch]}; } + if (mask & 0x10) != 0 { Cell{note, ..} = chmem[ch]; } + if (mask & 0x20) != 0 { Cell{inst, ..} = chmem[ch]; } + if (mask & 0x40) != 0 { Cell{vol, ..} = chmem[ch]; } + if (mask & 0x80) != 0 { Cell{efx, fxp, ..} = chmem[ch]; } + data[currow as usize * nch as usize + ch] = Cell{mask, note, inst, vol, efx, fxp}; + } + } + if f.stream_position()? - ofst != datalen as u64 + { + return Err(self::Error::CorruptPatternData); + } + Ok(Patt{nrows, nch:64, data}) + } + pub fn cell_at(&self, r: u16, c: u8) -> &Cell + { + &self.data[((r * self.nch as u16) + c as u16) as usize] + } + pub fn dump(&self) + { + const NOTES: &str = "C-C#D-D#E-F-F#G-G#A-A#B-"; + println!("=================================================="); + for r in 0..self.nrows + { + print!("|"); + for ch in 0..self.nch + { + let Cell{mask, note, inst, vol, efx, fxp} = self.cell_at(r, ch); + if mask & 0x11 != 0 + { + match note + { + 0xff => print!("== "), + 0xfe => print!("^^ "), + 0x78..=0xfd => print!("~~ "), + 0 => print!("... "), + _ => + { + let p = (note % 12 * 2) as usize; + print!("{}{:1} ", &NOTES[p..p + 2], note / 12); + } + } + } else { print!("... "); } + if mask & 0x22 != 0 + { + match inst + { + 0 => print!(".. "), + _ => print!("{:02X} ", inst) + } + } else { print!(".. "); } + if mask & 0x44 != 0 + { + match vol + { + 0..=64 => print!("v{:02} ", vol), + 65..=74 => print!("a{:02} ", vol - 65), + 75..=84 => print!("b{:02} ", vol - 75), + 85..=94 => print!("c{:02} ", vol - 85), + 95..=104 => print!("d{:02} ", vol - 95), + 105..=114 => print!("e{:02} ", vol - 105), + 115..=124 => print!("f{:02} ", vol - 115), + 128..=192 => print!("p{:02} ", vol - 128), + 193..=202 => print!("g{:02} ", vol - 193), + 203..=212 => print!("h{:02} ", vol - 203), + _ => print!("??? ") + } + } else { print!("... "); } + if mask & 0x88 != 0 + { + match efx + { + 0 => print!("...|"), + _ => print!("{}{:02X}|", (('A' as u8) - 1 + efx) as char, fxp) + } + } else { print!("...|"); } + } + println!(""); + } + println!("=================================================="); + } +} + +pub struct ITFile +{ + pub header: Header, + pub orders: Vec<u8>, + pub message: String, + pub insts: Vec<Inst>, + pub samps: Vec<Samp>, + pub patterns: Vec<Patt> +} + +#[derive(Debug)] +pub enum Error +{ + IOError(io::Error), + InvalidHeader, + UnsupportedVersion, + CorruptPatternData +} + +impl From<io::Error> for self::Error +{ + fn from(e: io::Error) -> Self + { self::Error::IOError(e) } +} + +fn terminate_cstr(s: &[u8]) -> &[u8] +{ + match s.iter().position(|&x| x==0) + { + Some(p) => &s[0..p], + None => s + } +} + +fn read_u8(f: &mut File) -> Result<u8, io::Error> +{ + let mut buf = [0u8; 1]; + f.read_exact(&mut buf)?; + Ok(buf[0]) +} + +fn read_u16(f: &mut File) -> Result<u16, io::Error> +{ + let mut buf = [0u8; 2]; + f.read_exact(&mut buf)?; + Ok(u16::from_le_bytes(buf)) +} + +fn read_vec<T>(f: &mut File, len: usize) -> Result<Vec<T>, io::Error> +where T: Clone, +{ + let mut ret: Vec<T> = Vec::with_capacity(len); + let buf: &mut [u8] = + unsafe { std::slice::from_raw_parts_mut(ret.as_mut_ptr() as *mut u8, len * size_of::<T>()) }; + f.read_exact(buf)?; + unsafe { ret.set_len(len) }; + Ok(ret) +} + +fn read_struct<T>(f: &mut File) -> Result<T, io::Error> +where T: Default, +{ + let mut ret = T::default(); + let buf: &mut [u8] = + unsafe { std::slice::from_raw_parts_mut(&mut ret as *mut T as *mut u8, size_of::<T>()) }; + f.read_exact(buf)?; + Ok(ret) +} + +pub fn load(filename: &str) -> Result<ITFile, self::Error> +{ + let mut f = File::open(filename)?; + let header: Header = read_struct(&mut f)?; + if header.magic != "IMPM".as_bytes() + { + return Err(self::Error::InvalidHeader); + } + if cfg!(debug_assertions) + { println!("song name: {}", header.song_name()); } + if header.format_version < 0x200 + { return Err(self::Error::UnsupportedVersion); } + + { + let (ninst, nsamp, npatt) = (header.ninst, header.nsamp, header.npatt); + println!("{} instruments, {} samples, {} patterns", ninst, nsamp, npatt); + } + + let orders: Vec<u8> = read_vec(&mut f, header.nord as usize)?; + let inst_offsets: Vec<u32> = read_vec(&mut f, header.ninst as usize)?; + let samp_offsets: Vec<u32> = read_vec(&mut f, header.nsamp as usize)?; + let patt_offsets: Vec<u32> = read_vec(&mut f, header.npatt as usize)?; + + println!("{:?}", orders); + println!("{:x?}",inst_offsets); + + let mut message = String::new(); + if header.has_message() + { + f.seek(SeekFrom::Start(header.msg_offset.into()))?; + let msg_raw: Vec<u8> = read_vec(&mut f, header.msg_length.into())?; + message = String::from_utf8_lossy(terminate_cstr(&msg_raw[..])).replace("\r", "\n"); + } + println!("{}", message); + + let mut insts: Vec<Inst> = Vec::new(); + for inst_offset in inst_offsets + { + f.seek(SeekFrom::Start(inst_offset.into()))?; + let inst = Inst::load(&mut f)?; + if inst.header.magic != "IMPI".as_bytes() + { + return Err(self::Error::InvalidHeader); + } + println!("{}", inst.inst_name()); + insts.push(inst); + } + + let mut samps: Vec<Samp> = Vec::new(); + for samp_offset in samp_offsets + { + f.seek(SeekFrom::Start(samp_offset.into()))?; + let samp = Samp::load(&mut f)?; + if samp.header.magic != "IMPS".as_bytes() + { + return Err(self::Error::InvalidHeader); + } + println!("{}", samp.sample_name()); + samps.push(samp); + } + let mut patterns: Vec<Patt> = Vec::new(); + for patt_offset in patt_offsets + { + f.seek(SeekFrom::Start(patt_offset.into()))?; + let patt = Patt::load(&mut f)?; + patt.dump(); + patterns.push(patt); + } + + let ret = ITFile{header, orders, message, insts, samps, patterns}; + + Ok(ret) +} |