aboutsummaryrefslogtreecommitdiff
path: root/src/itfile.rs
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2022-11-07 17:15:15 -0500
committerGravatar Chris Xiong <chirs241097@gmail.com> 2022-11-07 17:15:15 -0500
commit7f49730fd8a6dc52fc45937e868749e5612c5234 (patch)
tree9729ec896e42279f9cc49e862c9405ac60cc6b42 /src/itfile.rs
downloadit2midi-7f49730fd8a6dc52fc45937e868749e5612c5234.tar.xz
(internal) initial commit
Diffstat (limited to 'src/itfile.rs')
-rw-r--r--src/itfile.rs448
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)
+}