aboutsummaryrefslogblamecommitdiff
path: root/src/midifile.rs
blob: aaa2dfd52b7cddfa6d3394a7eba8d5fc69cada26 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                                                               
                















                                            
                












                                            
                                                                                                            























                                                                                                                                     
                                                                     



         
                                         






                              
                                                                     




                                
                                                                     




                                
                                                                      












                                                                                                
                                                                                    








                                        
                                                                              


                                   
                                      



                                      
                                          

                                       
                                        
     

                                      


          






























                                                                                                                                         
                                                                  











                                                 
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>
}

#[derive(Clone)]
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
}

#[derive(Clone)]
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: 0, data: vec![]}
        }
    }
}

pub type MidiTrack = Vec<TimedMidiEvent>;

pub struct MidiFile
{
    pub div: u16,
    pub tracks: Vec<MidiTrack>
}

fn write_u16be<W>(f: &mut W, v: u16) -> io::Result<()> where W: Write
{
    let bytes = v.to_be_bytes();
    f.write_all(&bytes)
}

fn write_u32be<W>(f: &mut W, v: u32) -> io::Result<()> where W: Write
{
    let bytes = v.to_be_bytes();
    f.write_all(&bytes)
}

fn write_varlen<W>(f: &mut W, v: u32) -> io::Result<()> where W: Write
{
    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<W>(f: &mut W, re: &RawMidiEvent) -> io::Result<()> where W: Write
{
    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<W>(f: &mut W, trk: &MidiTrack) -> io::Result<()> where W: Write
{
    let header = "MTrk".as_bytes();
    f.write_all(header)?;
    let mut buf: Vec<u8> = Vec::new();
    let mut curt = 0u32;
    for te in trk
    {
        let TimedMidiEvent{t, e} = te;
        write_varlen(&mut buf, t - curt)?;
        curt = *t;
        let re = RawMidiEvent::from(e);
        write_raw_event(&mut buf, &re)?;
    }
    write_u32be(f, buf.len() as u32)?;
    f.write_all(&buf[..])?;
    Ok(())
}

pub fn lint_track(trk: &MidiTrack) -> bool
{
    let mut ret = true;
    let mut notest = [false; 128];
    for te in trk
    {
        let TimedMidiEvent{t, e} = te;
        match e
        {
            MidiEvent::NoteOff{ch: _, key: n, vel: _} | MidiEvent::NoteOn{ch: _, key: n, vel: 0} => {
                if !notest[*n as usize] { ret = false; println!("Invalid note off {} @ {}", n, t); }
                notest[*n as usize] = false;
            },
            MidiEvent::NoteOn{ch: _, key: n, vel: _} => {
                if notest[*n as usize] { ret = false; println!("Unterminated note {} @ {}", n, t); }
                notest[*n as usize] = true;
            },
            MidiEvent::MetaTrackName(s) => {
                println!("Linting track {}", s);
            },
            _ => {}
        }
    }
    if ! matches!(trk.last().unwrap_or(&TimedMidiEvent{t: 0, e: MidiEvent::NoteOn{ch: 0, key: 0, vel: 0}}).e , MidiEvent::MetaEndOfTrack)
    {
        println!("Missing end of track event");
        ret = false;
    }
    ret
}

pub 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(())
}