Git Product home page Git Product logo

midly's Introduction

Midly

Midly is a feature-complete MIDI decoder and encoder designed for efficiency and ease of use.

See the crate-level documentation for examples, detailed documentation and available cargo features.

Features

  • Supports both .mid files and real-time MIDI packets.
  • Very complete, supports reading and writing while handling all edge cases in the MIDI spec.
  • Simple API, just load your data and call Smf::parse or LiveEvent::parse.
  • Optional no_std and no alloc support.
  • Zero-copy, all MIDI types simply reference the original buffer.
  • Fast! See the speed section below.

Getting started

First add the following line to your Cargo.toml file, under the [dependencies] section:

midly = "0.5"

Then use the Smf type in the crate root:

// Load bytes first
let data = std::fs::read("Pi.mid").unwrap();

// Parse the raw bytes
let mut smf = midly::Smf::parse(&data).unwrap();

// Use the information
println!("midi file has {} tracks!", smf.tracks.len());

// Modify the file
smf.header.format = midly::Format::Sequential;

// Save it back
smf.save("PiRewritten.mid").unwrap();

Or use the LiveEvent type to parse real-time MIDI events:

use midly::{live::LiveEvent, MidiMessage};

fn on_midi(event: &[u8]) {
    let event = LiveEvent::parse(event).unwrap();
    match event {
        LiveEvent::Midi { channel, message } => match message {
            MidiMessage::NoteOn { key, vel } => {
                println!("hit note {} on channel {}", key, channel);
            }
            _ => {}
        },
        _ => {}
    }
}

Most types to be imported are on the crate root and are documented in-place. Check the crate documentation for more information.

Speed

Although performance is not critical in a MIDI library, it still is an important objective of the midly library, providing automatic multithreading for large files and minimal allocations. The following chart presents benchmark results against other MIDI libraries in the ecosystem capable of reading .mid files.

File name File size rimd 0.0.1 nom-midi 0.5.1 augmented-midi 1.3.0 midly 0.5.3
Clementi.mid 4 KB 4 ms Error 0.06 ms 0.07 ms
CrabRave.mid 53 KB 4 ms 0.48 ms 0.53 ms 0.15 ms
Beethoven.rmi 90 KB Error Error Error 0.48 ms
Pi.mid 24 MB 20575 ms 253 ms 214 ms 60 ms
PiDamaged.mid 64 KB Freeze Error Error 0.41 ms

The above results are only referential, actual performance depends wildly on the hardware and operating system. The benchmarks were done on a Linux x64 machine with a warm file cache. The benchmark code is available in the /benchmark directory in the source.

midly's People

Contributors

kovaxis avatar negamartin avatar a2aaron avatar akiyukiokayasu avatar corvusprudens avatar

Stargazers

 avatar  avatar Eclypse Prime avatar George Frolov avatar David Sanders avatar 张浩扬穿JK avatar charlotte avatar  avatar ph3nac avatar  avatar Tatsuya Shiozawa avatar Michael Hinton avatar Stéphane Busso avatar @MevakeshEmetGPT avatar Robert Masen avatar Jeron Aldaron Lau avatar Octahedron-Alum avatar Matt Fellenz avatar Luis Miguel Báez avatar  avatar JQ avatar Felipe S. S. Schneider avatar  avatar  avatar 李佩道 avatar Will Bodron avatar Song Li avatar Ivan L avatar Éttore Leandro Tognoli avatar yuma14 avatar 伊欧 avatar Onur C. Cakmak avatar  avatar Mihir Chakma avatar Philippe Duval avatar Roma avatar 0xabad1dea (Melissa Elliott) avatar Lan Qingyong avatar Toby Murray avatar Stepan Samutichev avatar Felix Roos avatar Doug Stoeckmann avatar gerald avatar Jacob Goodale avatar Tyler Paul Thompson avatar 29 avatar Zhiqiang Zhang avatar  avatar PinkLea avatar Nmlgc avatar Yorling avatar Robert Bendun avatar Violeta Hernández avatar  avatar Nick avatar Mohsin Zaidi avatar 液氦 avatar hiromasa avatar Morgan Creekmore avatar Arseny Savchenko avatar HIRAKI Satoru avatar Denis Redozubov avatar  avatar Ian avatar Kyle Decot avatar Thandi R. Menelas avatar Dot32 avatar Marcus Adair avatar Efflam POTIN avatar kara avatar Iohann Rabeson avatar Anton Domnikov avatar Brian Redfern avatar Ilias Woithe avatar Yuan-Man avatar Nikita avatar Billy Messenger avatar Shujaat Ali Khan avatar Eliot Bolduc avatar Jaehee Lee avatar Jakub Arnold avatar Willem Dinkelspiel avatar  avatar 莯凛 avatar Kevin Tan avatar  avatar Adam M avatar now_its_dark avatar Atsuya Kobayashi avatar fwcd avatar Evelyn Heller avatar Steven Engler avatar Adam Vogel avatar Andriy Semenets avatar Anthony LoMagno avatar Chase Kanipe avatar Mikhail Akopov avatar  avatar L. T. S. avatar benjamin avatar

Watchers

Avindra Goolcharan avatar Jacob Rosenthal avatar  avatar 29 avatar

midly's Issues

Decouple write feature from std

Writing is possible without std, but it's currently unavailable without it.
One could imagine embedded devices that need to generate MIDI, so it'd be great to support that use-case.

Static SMF

Rather than clearing bytes, why is MetaMessage not composed of Cow so make_static can just turned them into the owned variants?

Playing back a midi file

I'm porting over a simple midi player I made in python to rust with rodio and midly. I'm now stuck at how I can properly play back a midi file, by looping over all note-ons in order, and waiting the correct amount of time before playing the next note

Difficulty with timing

Maybe not an issue with this library, but I cannot seem to get timing right.

const TICKS_PER_FRAME: u8 = 80;
const TICKS_PER_SECOND: u64 = 24 * (TICKS_PER_FRAME as u64);

fn midi_header() -> Header {
    Header::new(
        Format::SingleTrack,
        Timing::Timecode(Fps::Fps24, TICKS_PER_FRAME),
    )
}

Then when I receive a midi event, I calculate the tick based on the microseconds (us):

let tick = (us * TICKS_PER_SECOND) / 1_000_000;

In a small test run that lasted less than 10 seconds, I log:

Nov 07 09:35:21.921 DEBG MIDI event, us: 3398330, tick: 6524, event: Midi { channel: u4(0), message: NoteOn { key: u7(60), vel: u7(1) } }
Nov 07 09:35:22.078 DEBG MIDI event, us: 3555581, tick: 6826, event: Midi { channel: u4(0), message: NoteOff { key: u7(60), vel: u7(120) } }
Nov 07 09:35:22.209 DEBG MIDI event, us: 3686743, tick: 7078, event: Midi { channel: u4(0), message: NoteOn { key: u7(60), vel: u7(34) } }
Nov 07 09:35:22.390 DEBG MIDI event, us: 3867814, tick: 7426, event: Midi { channel: u4(0), message: NoteOff { key: u7(60), vel: u7(104) } }
Nov 07 09:35:22.856 DEBG MIDI event, us: 4334220, tick: 8321, event: Midi { channel: u4(0), message: NoteOn { key: u7(60), vel: u7(25) } }
Nov 07 09:35:23.097 DEBG MIDI event, us: 4575175, tick: 8784, event: Midi { channel: u4(0), message: NoteOff { key: u7(60), vel: u7(101) } }
Nov 07 09:35:23.112 DEBG MIDI event, us: 4590073, tick: 8812, event: Midi { channel: u4(0), message: NoteOn { key: u7(64), vel: u7(36) } }
Nov 07 09:35:24.298 DEBG MIDI event, us: 5775449, tick: 11088, event: Midi { channel: u4(0), message: NoteOff { key: u7(64), vel: u7(56) } }
Nov 07 09:35:24.302 DEBG MIDI event, us: 5780360, tick: 11098, event: Midi { channel: u4(0), message: NoteOn { key: u7(60), vel: u7(16) } }
Nov 07 09:35:27.139 DEBG MIDI event, us: 8617264, tick: 16545, event: Midi { channel: u4(0), message: NoteOff { key: u7(60), vel: u7(110) } }

But when I import the resulting MIDI file into Bitwig, it lasts for 35 seconds...
Attaching zip file with midi file inside.
out.mid.zip

Writing SMF file with running status to reduce file size?

It seems that currently running status is only used for encoding of live events but not when writing SMF files, right?
For many use cases where large midi files are generated (e.g. many 14-bit CC messages generated via algorithmic composition, or use cases like black mid where midi files are often huge), it would be very useful to make use of running status to reduce the file size.

And (if I understand it correctly), running status in a SMF file is per track (so if an event contains no status, the decoder will use the status of the last event from the same track, even if another track contained an event in the meantime). This allows optimizing file size even more by splitting events by kind onto separate tracks, e.g. having one channel only for all NoteOn events of a certain channel and another track for all NoteOff events of this channel, and another track for all CC msgs of that channel. This can save a lot of kb, especially when generating a lot of 14-bit CC curves :)

Support for CoreMIDI midi message format

Hi. Great work.

It would be nice if midly could parse MIDI messages received live within CoreMIDI framework, which uses uses 32-bit integers instead of 8-bit integers for its MIDI message packets.

Support for midi files with RIFF header?

It would be great if midi files that have a RIFF header could be loaded, too :)

E.g.:
http://www.midiarchive.co.uk/downloadfile/Classics/Beethoven/Beethoven.rmi

image

It just requires skipping those header bytes:

https://www.loc.gov/preservation/digital/formats/fdd/fdd000120.shtml

Hex: 52 49 46 46 xx xx xx xx 52 4D 49 44 64 61 74 61
ASCII: RIFF....RMIDdata

https://www.garykessler.net/library/file_sigs.html

52 49 46 46 xx xx xx xx52 4D 49 44 64 61 74 61 RIFF....RMIDdata
RMI   Resource Interchange File Format -- Windows MusicalInstrument Digital Interface file, where xx xx xx xx is the filesize (little endian)

Not sure what those 4 bytes before MThd are but they can also be skipped.


Btw, here is how I implemented this for rimd a while ago:
https://github.com/RustAudio/rimd/blob/54fd9bd2bd3caaa6fe1c31fbf71c0f3c6597fd1a/src/reader.rs#L14-L25

At first it reads 14 bytes because the normal midi file header is 14 bytes (MThd.........ð in the case above). But if the first 4 bytes are RIFF, it skips 6 bytes so that it has skipped the first 20 bytes in total, and then it reads the real midi header into the header buffer.

Incremental writing?

I don't know if this is something typical to do, but I think it could kind of make sense? It seems at least like the MIDI file format should lend itself perfectly to writing incrementally to a MIDI file, if I am not mistaken. I could help develop it, I just don't know the best API.

`EndOfTrack` is never present, even after adding it

I've noticed something weird:
For some reason, even if I add EndOfTrack to the end of each track before writing a SMF, the resulting SMF data never has any EndOfTrack messages. Any idea why?
Also whenever I parse a SMF, and print it out, there are also no EndOfTrack events.
Are they "transparently removed" during writing/parsing?
Do I even need to add EndOfTrack before writing a SMF?

Consider using the bounded_integer crate

Hi @kovaxis! This is more of a suggestion than anything else: maybe there is interest in reducing the technical debt of maintaining u7, u4, etc. and use some purpose-built solution? I've checked the available crates and bounded_integer seemed interesting and used enough to be considered?

cc. @gbrochar you've currently involved in something related in #26 and #27, aren't you?

Remove generic cruft

Smf being generic over TrackRepr is a feature for advanced users that gets in the way of the documentation for basic users (which should be the large majority if the 80/20 rule is worth anything).
Begginers shouldn't have to pay for features they don't use.

Ideally there should be separate types like Smf like SmfBytemap.
Since deferred parsing already avoids most allocations, why not do it right and avoid all allocations?
Perhaps a standalone function that returns a (Header, TrackIter) tuple, with TrackIter being an iterator yielding actual tracks (not like the current TrackIter that yields events).

Add examples on how to write LiveEvents to mid files

I'm trying to write events, coming from jack midi input port, to a mid file. I guess I could say I'm trying to record a midi sequence. The documentation doesn't really have much info nor any examples on how to write live event sequences to mid files.

I still tried and couldn't figure out how to convert LiveEvent to TrackEvent, so I could write it.

I've set up an SMF:

let header = midly::Header {
  format: midly::Format::SingleTrack, // Replace with the desired format
  timing: midly::Timing::Metrical(480.into()), // Replace with the desired timing information
};
let smf = midly::Smf::new(header);

and then tried to push an event to the first track:

let arena = midly::Arena::new();    
...
let live_event = LiveEvent::parse(data).unwrap();
smf.tracks[0].push(live_event.as_track_event(&arena));

But as_track_event returns TrackEventKind and I can't find no way to convert that.

Cannot use Arena together with the midir crate

I'm using midir to connect to a midi device and receive and store midi events.
Here's the problem: https://docs.rs/midir/0.7.0/midir/struct.MidiInput.html#method.connect requires a closure that is Send. If a closure owns an Arena, it is not Send.

Here is my project, so you may judge whether it is a use case you want to support:

Dependencies:

[dependencies]
midir = "0.7.0"
midly = "0.5.0"

main.rs:

#![feature(try_blocks)]
#![feature(with_options)]
use midir::{Ignore, MidiInput};
use midly::{live::LiveEvent, num::u28, Arena, Format, Fps, Header, Timing, TrackEvent};
use std::{
    error::Error,
    fs::File,
    sync::{Arc, Mutex},
    thread,
    time::{Duration, Instant},
};

const TICKS_PER_FRAME: u8 = 80;
const TICKS_PER_SECOND: u64 = 24 * (TICKS_PER_FRAME as u64);

fn midi_header() -> Header {
    Header::new(
        Format::SingleTrack,
        Timing::Timecode(Fps::Fps24, TICKS_PER_FRAME),
    )
}
fn main() -> Result<(), Box<dyn Error>> {
    let mut midi_in = MidiInput::new("midir reading input")?;
    midi_in.ignore(Ignore::None);

    let in_ports = midi_in.ports();
    let in_port = in_ports[6].clone();
    let in_port_name = midi_in.port_name(&in_port)?;

    let file = File::with_options()
        .append(true)
        .create(true)
        .open("out.midi");
    let file = Arc::new(Mutex::new(file));
    let last_change = Arc::new(Mutex::new(Instant::now()));

    let track = Arc::new(Mutex::new(Vec::<TrackEvent>::new()));
    let arena = Arena::new();

    let _conn_in = midi_in.connect(
        &in_port,
        "midir-read-input",
        move |us, mut message, _| {
            let result: Result<(), midly::Error> = try {
                println!("{}: {:?} (len = {})", us, message, message.len());

                let tick = (us * TICKS_PER_SECOND) / 1_000_000;

                let event = LiveEvent::parse(&message)?;

                let mut track = track.lock().unwrap();
                track.push(TrackEvent {
                    delta: u28::try_from(tick as u32).unwrap(),
                    kind: event.as_track_event(&arena),
                });

                *last_change.lock().unwrap() = Instant::now();
            };
            if let Err(e) = result {
                println!("ERROR HANDLING MIDI EVENT: {:?}", e);
            }
        },
        (),
    )?;

    println!(
        "Connection open, reading input from '{}' (press enter to exit) ...",
        in_port_name
    );

    std::thread::sleep(Duration::from_secs(9999999999999999));
    println!("Closing connection");
    Ok(())
}

Error:

error[E0277]: `*mut [u8]` cannot be sent between threads safely
  --> src/main.rs:40:28
   |
40 |     let _conn_in = midi_in.connect(
   |                            ^^^^^^^ `*mut [u8]` cannot be sent between threads safely
   |
   = help: the trait `std::marker::Send` is not implemented for `*mut [u8]`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::ptr::Unique<*mut [u8]>`
   = note: required because it appears within the type `alloc::raw_vec::RawVec<*mut [u8]>`
   = note: required because it appears within the type `std::vec::Vec<*mut [u8]>`
   = note: required because it appears within the type `std::cell::UnsafeCell<std::vec::Vec<*mut [u8]>>`
   = note: required because it appears within the type `midly::Arena`
   = note: required because it appears within the type `[closure@src/main.rs:43:9: 63:10 track:std::sync::Arc<std::sync::Mutex<std::vec::Vec<midly::TrackEvent<'_>>>>, arena:midly::Arena, last_change:std::sync::Arc<std::sync::Mutex<std::time::Instant>>]`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `transcribe`.

Add full support for realtime MIDI streams

Raw MIDI streams work differently, with different kinds of events and whatnot.

This is an opportunity to simplify the API to just 2 entry points: SMF parsing and raw MIDI event parsing. Something similar should be done with the write API.

Better support for primitive ops

Hello, thank you for your work.

I have run into a little problem, I wrote a function to transpose a midi file by n semi tones :

use midly::MidiMessage;
use midly::Smf;
use midly::TrackEventKind;
use std::cmp::min;

pub fn transpose_smf(smf: &mut Smf, interval: i8) {
    for track in smf.tracks.iter_mut() {
        for event in track.iter_mut() {
            if let TrackEventKind::Midi {
                channel,
                mut message,
            } = event.kind
            {
                message = match (message, interval.is_negative()) {
                    (MidiMessage::NoteOn { key, vel }, false) => MidiMessage::NoteOn {
                        key: key + (interval as u8).into(),
                        vel,
                    },
                    (MidiMessage::NoteOff { key, vel }, false) => MidiMessage::NoteOff {
                        key: key + (interval as u8).into(),
                        vel,
                    },
                    (MidiMessage::NoteOn { key, vel }, true) => {
                        if (interval.abs() as u8) > key {
                            eprintln!("warning: clamping key to 0");
                        }
                        MidiMessage::NoteOn {
                            key: key - min((interval.abs() as u8).into(), key),
                            vel,
                        }
                    },
                    (MidiMessage::NoteOff { key, vel }, true) => {
                        if (interval.abs() as u8) > key {
                            eprintln!("warning: clamping key to 0");
                        }
                        MidiMessage::NoteOff {
                            key: key - min((interval.abs() as u8).into(), key),
                            vel,
                        }
                    }
                    _ => message,
                };
                event.kind = TrackEventKind::Midi { channel, message };
            }
        }
    }
}

But as you can see I cannot simply do "u7 + i8" or something like that. Would implementing such operations with panic in debug mode for overflows (just like rust does) and overflow acting "like C" in release mode ?
I would be willing to work on the project.

My desired source code would be something like :

use midly::MidiMessage;
use midly::Smf;
use midly::TrackEventKind;
use std::cmp::min;

pub fn transpose_smf(smf: &mut Smf, interval: i8) {
    for track in smf.tracks.iter_mut() {
        for event in track.iter_mut() {
            if let TrackEventKind::Midi {
                channel,
                mut message,
            } = event.kind
            {
                message = match message {
                    MidiMessage::NoteOn { key, vel } => MidiMessage::NoteOn {
                        key: key + interval,
                        vel,
                    },
                    MidiMessage::NoteOff { key, vel } => MidiMessage::NoteOff {
                        key: key + interval,
                        vel,
                    },
                    _ => message,
                };
                event.kind = TrackEventKind::Midi { channel, message };
            }
        }
    }
}

I can understand the issue with underflowing and thus having to manage the case somehow for either the library or the end user. What are your views on this ? Again I am willing to work on it, even if you give me completely different instructions on how to solve the problem.

thank you :)

Where did the benchmark MIDI files come from?

Hey mate great job with this crate!

I have been working on augmented-midi for fun and hadn't really looked into this library before.

My parser/serializer uses nom, but since performance is fun, I ran the benchmarks in this repository and got the following results:

parsing file "../test-asset/Levels.mid" (2 KB)
  midly: 5 tracks in 50 iters / min 0.01 / avg 0.02 / max 0.44
  augmented-midi: 5 tracks in 50 iters / min 0.01 / avg 0.01 / max 0.01

parsing file "../test-asset/ClementiRewritten.mid" (4 KB)
  midly: 3 tracks in 50 iters / min 0.06 / avg 0.77 / max 8.14
  augmented-midi: 3 tracks in 50 iters / min 0.02 / avg 0.24 / max 3.08

parsing file "../test-asset/SysExTest.mid" (0 KB)
  midly: 3 tracks in 50 iters / min 0.01 / avg 0.01 / max 2.17
  augmented-midi: parse error

parsing file "../test-asset/Sandstorm.mid" (78 KB)
  midly: 19 tracks in 50 iters / min 0.1 / avg 1.38 / max 9.6
  augmented-midi: 19 tracks in 50 iters / min 0.21 / avg 0.32 / max 2.78

parsing file "../test-asset/Pi.mid" (24650 KB)
  midly: 30 tracks in 50 iters / min 23.32 / avg 33.24 / max 156.62
  augmented-midi: 30 tracks in 50 iters / min 79.74 / avg 96.66 / max 141.69

parsing file "../test-asset/CrabRave.mid" (53 KB)
  midly: 14 tracks in 50 iters / min 0.17 / avg 1.92 / max 10.47
  augmented-midi: 14 tracks in 50 iters / min 0.16 / avg 0.27 / max 2.4

parsing file "../test-asset/PiDamaged.mid" (64 KB)
  midly: 3 tracks in 50 iters / min 0.18 / avg 0.62 / max 4.8
  augmented-midi: parse error

parsing file "../test-asset/Beethoven.rmi" (90 KB)
  midly: 15 tracks in 50 iters / min 0.2 / avg 1.01 / max 8.57
  augmented-midi: parse error

parsing file "../test-asset/Clementi.mid" (4 KB)
  midly: 3 tracks in 50 iters / min 0.04 / avg 0.1 / max 0.55
  augmented-midi: 3 tracks in 50 iters / min 0.02 / avg 0.03 / max 0.1

parsing file "../test-asset/RiverFlowsInYou.mid" (6 KB)
  midly: 2 tracks in 50 iters / min 0.03 / avg 0.16 / max 1.12
  augmented-midi: 2 tracks in 50 iters / min 0.02 / avg 0.03 / max 0.08

I fixed the nº of iterations at 50.

There are some parse errors I can look into, but the most interesting for me is:

parsing file "../test-asset/Pi.mid" (24650 KB)
  midly: 30 tracks in 50 iters / min 23.32 / avg 33.24 / max 156.62
  augmented-midi: 30 tracks in 50 iters / min 79.74 / avg 96.66 / max 141.69

Initially my parser was around 5 times worse for this large file (min ~100), which was improved by trying to pre-allocate the Vecs.

I suppose it will be slower for varied reasons due to this being handwritten, but it's a fun exercise to understand why exactly.

Is the 25MB MIDI file just lots of pitch-wheel events? There might be a difference in behaviour (when the 14bit nº is converted into 16bit or otherwise).

Cheers

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.