aboutsummaryrefslogtreecommitdiff
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
downloadit2midi-7f49730fd8a6dc52fc45937e868749e5612c5234.tar.xz
(internal) initial commit
-rw-r--r--.gitignore6
-rw-r--r--Cargo.toml8
-rw-r--r--README.md18
-rw-r--r--src/convert.rs193
-rw-r--r--src/itfile.rs448
-rw-r--r--src/main.rs17
-rw-r--r--src/midifile.rs144
-rw-r--r--src/utils.rs35
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}
+}