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 | |
download | it2midi-7f49730fd8a6dc52fc45937e868749e5612c5234.tar.xz |
(internal) initial commit
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | src/convert.rs | 193 | ||||
-rw-r--r-- | src/itfile.rs | 448 | ||||
-rw-r--r-- | src/main.rs | 17 | ||||
-rw-r--r-- | src/midifile.rs | 144 | ||||
-rw-r--r-- | src/utils.rs | 35 |
8 files changed, 869 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6173d56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +/debug +Cargo.lock + +*.swp +Session.vim diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..66715b6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "it2midi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..363cc39 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# IT2MIDI + +_#Rewrite it in Rust_ + +Rust rewrite the original it2midi.cpp I wrote in 2017, with properly configuratble +conversion options (rather than editing the source code everytime). + +Unrelated to Un4seen's 2MIDI. IT2MIDI is built from the ground up with a very different +goal in mind. IT2MIDI does not intend to become a 2MIDI replacement. + +Architecturally, this rewrite still closely resemble the original C++ version (hence +a lot of passing references around). This is not satisfactory and will be redesigned +in the future. However currently I'm focusing on getting a working converter. + +Since this is my very first Rust project, I try to implement as much by myself as possible. +As usual this went too far. For this reason IT2MIDI currently has no external dependencies. + +Original C++ version here: https://cgit.chrisoft.org/oddities.git/tree/music/it2midi.cpp diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..9ed703f --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,193 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::any::Any; +use std::cell::RefCell; +use std::rc::Rc; +use crate::itfile; +use crate::utils; +use crate::midifile; + +struct PlayerState +{ + skip_row: u16, + skip_ord: u8, + current_ord: u8, + current_row: u16, + in_loop: bool +} + +struct Player<'a> +{ + h: Rc<RefCell<dyn CellHandler>>, + it: &'a itfile::ITFile +} + +pub struct Converter<'a> +{ + instch: Option<BTreeMap<(u8, u8), usize>>, + it: &'a itfile::ITFile +} + +trait CellHandler +{ + fn process(&mut self, ch: u8, cell: itfile::Cell, player: &mut Player); + fn result_data(&mut self) -> Box<dyn Any>; +} + +trait EffectHandler +{ + fn process(&mut self, ch: u8, cell: itfile::Cell, chmem: &mut ChannelMemory) -> Vec<midifile::TimedMidiEvent>; + fn handled_effects(&self) -> Vec<u8>; +} + +struct PrePassCellHandler +{ + instchmap: Option<BTreeSet<(u8, u8)>>, + chinst: [u8; 64] +} + +#[derive(Default)] +struct ChannelMemory +{ + note: u8, + vol: u8, + efxmem: [u8; 32], + pitch: utils::Rational +} + +struct ConvertCellHandler<'a> +{ + chmem: [ChannelMemory; 64], + fx_handlers: [Rc<RefCell<dyn EffectHandler + 'a>>; 32] +} + +impl<'a> Player<'a> +{ + fn new(handler: Rc<RefCell<dyn CellHandler>>, it: &'a itfile::ITFile) -> Player<'a> + { + Player + { + h: handler, + it + } + } + fn process_pattern(&mut self, pat: usize, st: PlayerState) -> PlayerState + { + let skip_row = if !st.skip_row == 0 { 0 } else { st.skip_row }; + let ret = PlayerState{ + skip_row: !0, + skip_ord: !0, + current_ord: 0, + current_row: 0, + in_loop: false + }; + self.skip_row = !0; + for r in skip_row..self.it.patterns[pat].nrows + { + self.current_row = r; + for c in 0..64 + { + let cell = *self.it.patterns[pat].cell_at(r, c); + Rc::clone(&self.h).borrow_mut().process(c as u8, cell, self); + } + Rc::clone(&self.h).borrow_mut().process(!0, itfile::Cell::default(), self); + if (!self.skip_row) != 0 || (!self.skip_ord) != 0 { return; } + } + ret + } + /// Used for effects Bxx, Cxx and SBx + /// + /// passing !0 to row or ord if it's unused + /// + /// in_loop == true inhibits loop detection and should only be used for SBx + fn skip_to(&mut self, row: u16, ord: u8, in_loop: bool) + { + self.skip_row = if !row != 0 { row } else { self.skip_row }; + self.skip_ord = if !ord != 0 { ord } else { self.skip_ord }; + self.in_loop = in_loop; + } + fn process_orders(&mut self) + { + self.skip_to(!0, !0, false); + let mut oid = 0; + loop + { + if oid >= self.it.orders.len() { break; } + if self.it.orders[oid] == 0xff { break; } + if self.it.orders[oid] == 0xfe { continue; } + self.process_pattern(self.it.orders[oid].into()); + if !self.skip_ord != 0 + { + if self.skip_ord as usize <= oid && !self.in_loop + { println!("loop?"); } + else { oid = self.skip_ord as usize; } + self.skip_ord = !0; + } + else { oid += 1; } + } + } +} + +impl CellHandler for PrePassCellHandler +{ + fn process(&mut self, ch: u8, cell: itfile::Cell, _p: &mut Player) + { + if ch == 0xff { return; } + let itfile::Cell{mask, mut inst, ..} = cell; + if mask & 0x22 != 0 + { self.chinst[ch as usize] = inst; } + else + { inst = self.chinst[ch as usize]; } + if mask & 0x11 != 0 + { self.instchmap.as_mut().unwrap().insert((ch, inst)); } + } + fn result_data(&mut self) -> Box<dyn Any> + { Box::new(self.instchmap.take().unwrap()) } +} + +impl<'a> ConvertCellHandler<'a> +{ + fn register_fx_handler(&mut self, hndlr: Rc<RefCell<dyn EffectHandler + 'a>>) + { + let efxs = hndlr.borrow().handled_effects(); + efxs.iter() + .filter(|e| **e < 32) + .for_each(|e| self.fx_handlers[*e as usize] = Rc::clone(&hndlr)); + } +} + +impl<'a> CellHandler for ConvertCellHandler<'a> +{ + fn process(&mut self, ch: u8, cell: itfile::Cell, player: &mut Player) + { + let itfile::Cell{mask, note, inst, vol, efx, fxp} = cell; + } + fn result_data(&mut self) -> Box<dyn Any> + { Box::new(()) } +} + +impl<'a> Converter<'a> +{ + pub fn new(it: &itfile::ITFile) -> Converter + { + Converter{instch: None, it} + } + fn pre_pass(&'a mut self) + { + let h = PrePassCellHandler{instchmap: Some(BTreeSet::new()), chinst: [0; 64]}; + let mut p = Player::new(Rc::new(RefCell::new(h)), self.it); + p.process_orders(); + let Player{mut h, ..} = p; + if let Ok(m) = h.borrow_mut().result_data().downcast::<BTreeSet<(u8, u8)>>() + { + let mut instch = BTreeMap::new(); + m.iter().enumerate().for_each( + |(i, p)| { instch.insert(*p, i + 1); } ); + self.instch = Some(instch); + } + println!("{:?}", self.instch); + } + pub fn convert(&'a mut self) + { + self.pre_pass(); + } +} 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) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cdc2322 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,17 @@ +mod itfile; +mod midifile; +mod convert; +mod utils; + +fn main() -> Result<(), itfile::Error> { + match itfile::load("/home/chrisoft/Music/mods/rr_exp.it") + { + Ok(f) => + { + let mut conv = convert::Converter::new(&f); + conv.convert(); + Ok(()) + } + Err(e) => Err(e) + } +} diff --git a/src/midifile.rs b/src/midifile.rs new file mode 100644 index 0000000..71a861c --- /dev/null +++ b/src/midifile.rs @@ -0,0 +1,144 @@ +use std::io; +use std::io::prelude::*; +use std::fs::File; + +pub struct RawMidiEvent +{ + ty: u8, + p1: u8, + p2: u8, //any value > 0x7f won't be written to file + data: Vec<u8> +} + +pub enum MidiEvent +{ + NoteOn{ch: u8, key: u8, vel: u8}, + NoteOff{ch: u8, key: u8, vel: u8}, + KeyAfterTouch{ch: u8, key: u8, val: u8}, + CtrlChange{ch: u8, no: u8, val: u8}, + ProgChange{ch: u8, val: u8}, + ChannelAfterTouch{ch: u8, val: u8}, + PitchBend{ch: u8, val: u16}, + //SysExc(Vec<u8>), + MetaTempo(f64), + MetaTimeSig{n: u8, d_pot: u8}, + MetaTrackName(String), + MetaEndOfTrack +} + +pub struct TimedMidiEvent +{ + pub t: u32, + pub e: MidiEvent +} + +impl From<&MidiEvent> for RawMidiEvent +{ + fn from(e: &MidiEvent) -> Self + { + match e + { + MidiEvent::NoteOff{ch, key, vel} + if *vel == 0x40 => RawMidiEvent{ty: 0x90 | ch, p1: *key, p2: 0, data:vec![]}, + MidiEvent::NoteOff{ch, key, vel} => RawMidiEvent{ty: 0x80 | ch, p1: *key, p2: *vel, data: vec![]}, + MidiEvent::NoteOn {ch, key, vel} => RawMidiEvent{ty: 0x90 | ch, p1: *key, p2: *vel, data: vec![]}, + MidiEvent::KeyAfterTouch{ch, key, val} => RawMidiEvent{ty: 0xa0 | ch, p1: *key, p2: *val, data: vec![]}, + MidiEvent::CtrlChange{ch, no, val} => RawMidiEvent{ty: 0xb0 | ch, p1: *no, p2: *val, data: vec![]}, + MidiEvent::ProgChange{ch, val} => RawMidiEvent{ty: 0xc0 | ch, p1: *val, p2: 0xff, data: vec![]}, + MidiEvent::ChannelAfterTouch{ch, val} => RawMidiEvent{ty: 0xd0 | ch, p1: *val, p2: 0xff, data: vec![]}, + MidiEvent::PitchBend{ch, val} => RawMidiEvent{ty: 0xe0 | ch, p1: (val & 0x7f) as u8, p2: (val >> 7) as u8, data: vec![]}, + MidiEvent::MetaTempo(tempo) => + { + let us = (60000000. / tempo) as u32; + let usbuf = us.to_be_bytes(); + RawMidiEvent{ty: 0xff, p1: 0x51, p2: 3, data: Vec::from(&usbuf[1..])} + } + MidiEvent::MetaTimeSig{n, d_pot} => + { + let x: [u8; 4] = [*n, *d_pot, 24, 8]; + RawMidiEvent{ty: 0xff, p1: 0x58, p2: 4, data: Vec::from(x)} + } + MidiEvent::MetaTrackName(s) => + { + let sb = s.as_bytes(); + RawMidiEvent{ty: 0xff, p1: 0x03, p2: s.len() as u8, data: Vec::from(sb)} + } + MidiEvent::MetaEndOfTrack => + RawMidiEvent{ty: 0xff, p1: 0x2f, p2: 0xff, data: vec![]} + } + } +} + +type MidiTrack = Vec<TimedMidiEvent>; + +pub struct MidiFile +{ + pub div: u16, + pub tracks: Vec<MidiTrack> +} + +fn write_u16be(f: &mut File, v: u16) -> io::Result<()> +{ + let bytes = v.to_be_bytes(); + f.write_all(&bytes) +} + +fn write_u32be(f: &mut File, v: u32) -> io::Result<()> +{ + let bytes = v.to_be_bytes(); + f.write_all(&bytes) +} + +fn write_varlen(f: &mut File, v: u32) -> io::Result<()> +{ + if v > 0x0fffffff + { return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid variable length value")); } + let mut sh: u32 = 4 * 7; + while sh > 0 && (v >> sh) == 0 + { sh -= 7; } + let mut buf: Vec<u8> = Vec::new(); + while sh > 0 + { buf.push((((v >> sh) & 0x7f) | 0x80) as u8); sh -= 7; } + buf.push((v & 0x7f) as u8); + f.write_all(&buf[..]) +} + +fn write_raw_event(f: &mut File, re: &RawMidiEvent) -> io::Result<()> +{ + let mut buf: Vec<u8> = Vec::new(); + buf.push(re.ty); + buf.push(re.p1); + if re.p2 < 0x80 { buf.push(re.p2); } + for d in &re.data { buf.push(*d); } + f.write_all(&buf[..]) +} + +fn write_track(f: &mut File, trk: &MidiTrack) -> io::Result<()> +{ + let header = "MTrk".as_bytes(); + f.write_all(header)?; + let mut curt = 0u32; + for te in trk + { + let TimedMidiEvent{t, e} = te; + write_varlen(f, t - curt)?; + curt = *t; + let re = RawMidiEvent::from(e); + write_raw_event(f, &re)?; + } + Ok(()) +} + +fn write_file(filename: &str, mf: &MidiFile) -> io::Result<()> +{ + let mut f = File::create(filename)?; + let header = "MThd".as_bytes(); + f.write_all(header)?; + write_u32be(&mut f, 6)?; + write_u16be(&mut f, 1)?; + write_u16be(&mut f, mf.tracks.len() as u16)?; + write_u16be(&mut f, mf.div)?; + for trk in &mf.tracks + { write_track(&mut f, &trk)?; } + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c53b959 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,35 @@ +pub struct Rational +{ + n: i64, + d: i64 +} + +fn gcd(a: i64, b: i64) -> i64 +{if b != 0 { gcd(b, a % b) } else { a }} + +impl Default for Rational +{ + fn default() -> Rational { Rational::from_int(0) } +} + +impl Rational +{ + fn from_int(v: i64) -> Rational {Rational{n: v, d: 1}} + fn reduced(self) -> Rational + { + let c = gcd(self.n, self.d); + Rational{n: self.n / c, d: self.d / c} + } + fn add(self, other: Rational) -> Rational + { + let c = gcd(self.d, other.d); + Rational{n: self.n * (other.d / c) + other.n * (self.d / c), + d: self.d / c * other.d}.reduced() + } + fn multiply(self, other: Rational) -> Rational + { + Rational{n: self.n * other.n, d: self.d * other.d}.reduced() + } + fn as_int_trunc(self) -> i64 {self.n / self.d} + fn as_int_round(self) -> i64 {(self.n as f64 / self.d as f64).round() as i64} +} |