aboutsummaryrefslogblamecommitdiff
path: root/src/itfile.rs
blob: 6d85a878417bd76384179803e0a90276294480c6 (plain) (tree)
1
2
3
4
5
6
7
8
9




                                
                              
 

                                    













                                   





















                                                








                                                                             



                                                             
                                                                                        

                     





















































                                               

                          











                                                          






                                                                                         




                                                                                       

                                                   











































                                                                                       


                                                                                                 
                                                                        














                               
                  

















































                                                                                                      
                                                  
     
                                                
     
                                                                      



































                                                                                
                                                   
                     











                                                                             

















                                                                                      








                         






                         























                                                            
                                                                   







































                                                   






                                                   




















                                                                                                   















































































                                                                                 






















                                                                                 

                                                


















                                                                                            






                                                                                       


                         

                                   
                                          



                                                                                                            







                                                     








                                                                                                               
                                                                 






                                                     

                                                                  



                            


























                                                                                                                     


           
use std::io;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::fs::File;
use std::mem::{size_of, zeroed};
use std::collections::HashSet;

use crate::portmod::{Effect, Slide};

macro_rules! zero_default {
    ($t:ty) =>
    {
        impl Default for $t
        {
            fn default() -> Self
            { unsafe { zeroed() } }
        }
    }
}

#[repr(C, packed)]
pub struct Header
{
    pub magic: [u8; 4],
    pub song_name: [u8; 26],
    pub pat_highlight: u16,     //PHiligt
    pub nord: u16,              //OrdNum
    pub ninst: u16,             //InsNum
    pub nsamp: u16,             //SmpNum
    pub npatt: u16,             //PatNum
    pub tracker_version: u16,   //Cwt/v
    pub format_version: u16,    //Cmwt
    pub flags: u16,             //Flags
    pub special: u16,           //Special
    pub global_volume: u8,      //GV
    pub mix_volume: u8,         //MV
    pub speed: u8,              //IS
    pub tempo: u8,              //IT
    pub stereo_seperation: u8,  //Sep
    pub wheel_depth: u8,        //PWD
    pub msg_length: u16,        //MsgLgth
    pub msg_offset: u32,        //Message Offset
    pub reserved: u32,
    pub channel_pan: [u8; 64],
    pub 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 & 0x1 != 0 }
    fn time_signature(self: &Self) -> Option<(u32, u32)>
    {
        if self.special & 0x4 != 0 && self.pat_highlight != 0
        { Some(((self.pat_highlight & 0xff) as u32, (self.pat_highlight >> 8) as u32)) }
        else { None }
    }
}

#[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,
    key_samples: [u8; 120]
}

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)?;
        let mut ks = [0u8; 120];
        for i in 0..120
        {
            if header.sample_table[i * 2] < 120
            { ks[header.sample_table[i * 2] as usize] = header.sample_table[i * 2 + 1]; }
        }
        Ok(Inst{header, env_vol, env_pan, env_pit, key_samples: ks})
    }
    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() }
    pub fn samp_for_key(self: &Self, key: u8) -> u8
    { self.key_samples[key as usize] }
}

#[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() }
    pub fn is_compressed(self: &Self) -> bool { self.header.flag & 0x8 != 0 }
    pub fn num_channels(self: &Self) -> u8 { if self.header.flag & 0x4 != 0 { 2 } else { 1 }}
    pub fn bits_per_sample(self: &Self) -> u8 { if self.header.flag & 0x2 != 0 { 16 } else { 8 }}
    pub fn default_vol(self: &Self) -> u8 { self.header.default_volume }
}

#[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: u8,
    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: nrows as u8, nch:64, data})
    }
    pub fn cell_at(&self, r: u8, c: u8) -> &Cell
    {
        &self.data[((r as u16 * 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 Effect::from_it_vol(*vol)
                    {
                        Effect::SetVolume(vol) => print!("v{:02} ", vol),
                        Effect::VolumeSlide(s) => match s {
                              Slide::FineUp(x) => print!("a{:02} ", x),
                              Slide::FineDown(x) => print!("b{:02} ", x),
                              Slide::Up(x) => print!("c{:02} ", x),
                              Slide::Down(x) => print!("d{:02} ", x),
                          },
                        Effect::PortaDown(x) => print!("e{:02} ", x / 4),
                        Effect::PortaUp(x) => print!("f{:02} ", x / 4),
                        Effect::SetPan(p) => print!("p{:02} ", p),
                        Effect::TonePorta(_) => print!("g{:02} ", vol - 193),
                        Effect::Vibrato(_, x) => print!("h{:02} ", x),
                        _ => 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!("==================================================");
    }
}

#[derive(Debug)]
#[derive(Default)]
pub struct MPTTrackExt
{
    pub rpb: Option<u32>,
    pub rpm: Option<u32>,
    pub nch: Option<u16>
}

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>,
    pub trkext: Option<MPTTrackExt>
}

impl ITFile
{
    pub fn time_signature(self: &Self) -> Option<(u32, u32)>
    {
        if let Some((b, m)) = self.header.time_signature()
        {
            Some((b as u32, m as u32))
        }
        else
        {
            if let Some(trex) = self.trkext.as_ref()
            {
                match (trex.rpb, trex.rpm)
                {
                    (Some(b), Some(m)) => Some((b, m)),
                    (_, _) => None
                }
            } else { None }
        }
    }
    pub fn inst_mode(self: &Self) -> bool { self.header.ninst > 0 }
}

#[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_u32(f: &mut File) -> Result<u32, io::Error>
{
    let mut buf = [0u8; 4];
    f.read_exact(&mut buf)?;
    Ok(u32::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)
}

fn check_magic(f: &mut File, m: &str) -> Result<bool, io::Error>
{
    let mb = m.as_bytes();
    assert_eq!(mb.len(), 4);
    let mut buf = [0u8; 4];
    f.read_exact(&mut buf)?;
    if mb != buf { f.seek(SeekFrom::Current(-4))?; }
    Ok(mb == buf)
}

fn read_magic(f: &mut File) -> Result<String, io::Error>
{
    let mut buf = [0u8; 4];
    f.read_exact(&mut buf)?;
    Ok(String::from_utf8_lossy(&buf).into_owned())
}

pub fn read_inst_ext(f: &mut File, ninst: u16) -> Result<(), io::Error>
//instr extension currently completely unused
{
    println!("trying to read MPT Instr Extension at {:x}", f.stream_position()?);
    if check_magic(f, "XTPM")?
    {
        println!("MPT Instr Extension found at {:x}", f.stream_position()?);
        loop
        {
            if check_magic(f, "STPM")?
            {
                f.seek(SeekFrom::Current(-4))?;
                break;
            }
            read_u32(f)?;
            let chunk_len_inst = read_u16(f)? as i64;
            f.seek(SeekFrom::Current(chunk_len_inst * ninst as i64))?;
        }
    }
    Ok(())
}

pub fn read_track_ext(f: &mut File) -> Result<Option<MPTTrackExt>, io::Error>
{
    if check_magic(f, "STPM")?
    {
        println!("MPT Track Extension found at {:x}", f.stream_position()?);
        let mut ret: MPTTrackExt = Default::default();
        loop
        {
            let mr = read_magic(f);
            let mut dlen = 0;
            if mr.is_ok() { dlen = read_u16(f)?; }
            match mr.as_deref()
            {
                Err(_) => { break; },
                Ok("...C") => { ret.nch = Some(read_u16(f)?); },
                Ok(".BPR") => { ret.rpb = Some(read_u32(f)?); },
                Ok(".MPR") => { ret.rpm = Some(read_u32(f)?); },
                Ok(&_) => {f.seek(SeekFrom::Current(dlen.into()))?; }
            }
        }
        return Ok(Some(ret));
    }
    Ok(None)
}

pub fn read_mpt_ext(f: &mut File, ninst: u16) -> Option<MPTTrackExt>
{
    match read_inst_ext(f, ninst)
    {
        Err(_) => None,
        Ok(_) =>
        {
            match read_track_ext(f)
            {
                Err(_) => None,
                Ok(r) => r
            }
        }
    }
}

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 {:?}", orders);
    println!("inst offsets {: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);
        }
        let mut sampsused = HashSet::<u8>::new();
        for i in 0u8..120
        {
            if inst.samp_for_key(i) > 0
            { sampsused.insert(inst.samp_for_key(i)); }
        }
        println!("{} [{}] samples {:?}", inst.inst_name(), inst.filename(), sampsused);
        insts.push(inst);
    }
    
    let mut last_samp_off = 0u64;
    let mut last_samp_comp = false;
    let mut samps: Vec<Samp> = Vec::new();
    if header.nsamp > 0
    {
        last_samp_off = (samp_offsets[header.nsamp as usize - 1] as usize + size_of::<SampHeader>()) as u64;
    }
    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);
        }
        f.seek(SeekFrom::Start(samp.header.samp_ptr.into()))?;
        last_samp_comp = samp.is_compressed();
        if !last_samp_comp
        {
            let data_len = samp.header.length * samp.num_channels() as u32 * samp.bits_per_sample() as u32 / 8;
            f.seek(SeekFrom::Current(data_len.into()))?;
        }
        let dataend_off = f.stream_position()?;
        last_samp_off = std::cmp::max(last_samp_off, dataend_off);
        println!("{} [{}]", samp.sample_name(), samp.filename());
        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)?;
        let dataend_off = f.stream_position()?;
        last_samp_off = std::cmp::max(last_samp_off, dataend_off);
        patt.dump();
        patterns.push(patt);
    }

    if last_samp_off > 0
    {
        f.seek(SeekFrom::Start(last_samp_off))?;
        if last_samp_comp
        {
            loop
            {
                if read_u32(&mut f).is_err() { break; }
                else { f.seek(SeekFrom::Current(-4))?; }
                if check_magic(&mut f, "STPM").unwrap_or_default() || check_magic(&mut f, "XTPM").unwrap_or_default()
                {
                    let id = read_u32(&mut f)?;
                    f.seek(SeekFrom::Current(-8))?;
                    if (id & 0x80808080 == 0) && (id & 0x60606060 != 0)
                    {
                        break;
                    }
                }
                let chnklen = read_u16(&mut f)?;
                f.seek(SeekFrom::Current(chnklen as i64))?;
            }
        }
    }

    let trkext = read_mpt_ext(&mut f, header.ninst);

    let ret = ITFile{header, orders, message, insts, samps, patterns, trkext};

    Ok(ret)
}