Coder Social home page Coder Social logo

midilib's Introduction

midilib

midilib is a pure Ruby MIDI library useful for reading and writing standard MIDI files and manipulating MIDI event data.

The GitHub project page and Web site of midilib is github.com/jimm/midilib and the RubyGems.org page is rubygems.org/gems/midilib, where you can also find all the RDoc documentation.

midilib is compatible with Ruby 2.6 and higher.

Dependencies

midilib does not require any other packages. The test suite in the tests directory requires the testing framework TestUnit, which comes with Ruby 1.8 and later and can also be found in the Ruby Application Archive (raa.ruby-lang.org).

To rebuild the gem or RDocs or run the tests easily, you can use the Rakefile which requires Rake (rake.rubyforge.org).

Installation

RubyGems Installation

To install midilib as a gem, type

% gem install midilib

or, if you already have a previous version, use

% gem update midilib

You may need root privileges to install or update the gem.

Manual Installation

After downloading and expanding the archive, you can install midilib with the command

% ruby install.rb

(or)

% ruby install.rb --install-dir=my_directory

You may need root privileges to install.

Testing

% rake test

runs all of the tests in the test directory.

Overview

midilib can read and write MIDI file formats 0 (one single track) or 1 multiple tracks. By default, it writes format 1 which is the most common format. MIDI file format 2 is not yet supported.

MIDI::Sequence

A sequence contains a collection of tracks and global information like the sequence’s pulses per quarter note (ppqn) and time signature.

The first track in a sequence is special; it holds meta-events like tempo and sequence name. Don’t put any notes in this track.

MIDI::Sequence also contains some convenience methods that let you set and retrieve the sequence’s name, the time signature, and to retrieve the first tempo event’s beats-per-minute value.

Normally instances of MIDI::IO::SeqReader and MIDI::IO::SeqWriter are used when a sequence reads itself from or writes itself to a MIDI file. You can change that by setting a sequence’s reader_class or writer_class attributes. Instances of the classes contained in those attributes are created and used whenever the sequence reads or writes itself.

MIDI::Track

A track contains an array of events.

When you modify the events array, make sure to call recalc_times so each event gets its time_from_start recalculated. You don’t have to do that after every event you add; just remember to do so before using the track in a way that expects the list of events to be ordered correctly.

A Track also holds a bit mask that specifies the channels used by the track. This bit mask is set when the track is read from the MIDI file by a SeqReader but is not kept up to date by any other methods. Specifically, if you add events to a track at any other time, the bit mask will not be updated.

When a Track is read from a MIDI file, a MIDI::META_TRACK_END event is added to the end if there isn’t one in the file already. When a Track is written to a MIDI file, a MIDI::META_TRACK_END event is always output even if the Track does not have one.

The Track#merge method ensures that there is only one MIDI::META_TRACK_END event after the merge and that it’s at its proper place at the end of the list of events. It does so by calling Track#ensure_track_end_meta_event.

MIDI::Measure

This class contains information about a measure from the sequence. Measure data is based on the time signature information from the sequence and is not stored in the sequence itself.

MIDI::Measures

The class MIDI::Sequence method get_measures returns a MIDI::Measures object. MIDI::Measures is a subclass of Array. It is a specialized container for MIDI::Measure objects, which can be use to map event times to measure numbers. Please note that this object has to be remade when events are deleted/added in the sequence.

MIDI::Measure and MIDI::Measures are brought to us by Jari Williamsson <[email protected]>, who also contributed some improvements to the MIDI::Event and MIDI::Track classes.

MIDI::Event

Each event holds not only its delta time but also its time from the start of the track. The track is responsible for recalculating its events’ start times. You can call MIDI::Track#recalc_times to do so.

Subclasses of MIDI::Event implement the various MIDI messages such as note on and off, controller values, system exclusive data, and realtime bytes.

MIDI::Realtime events have delta values and start times, just like all the other midilib event types do. (MIDI real time status bytes don’t have delta times, but this way we can record when in a track the realtime byte was received and should be sent. This is useful for start/continue/stop events that control other devices, for example.) Note that when a MIDI::Realtime event is written out to a MIDI file, the delta time is not written.

MIDI::MetaEvent events hold an array of bytes named ‘data’. Many meta events are string holders (text, lyric, marker, etc.) Though the ‘data’ value is always an array of bytes, MIDI::MetaEvent helps with saving and accessing string. The MIDI::MetaEvent#data_as_str method returns the data bytes as a string. When assigning to a meta event’s data, if you pass in a string it will get converted to an array of bytes.

How To Use midilib

The following examples show you how to use midilib to read, write, and manipulate MIDI files and modify track events. See also the files in the examples directory, which are described below.

Reading a MIDI File

To read a MIDI file, create a MIDI::Sequence object and call its #read method, passing in an IO object.

The #read method takes an optional block. If present, the block is called once after each track has finished being read. Each time, it is passed the track object, the total number of tracks and the number of the current track that has just been read. This is useful for notifying the user of progress, for example by updating a GUI progress bar.

require 'midilib/io/seqreader'

# Create a new, empty sequence.
seq = MIDI::Sequence.new()

# Read the contents of a MIDI file into the sequence.
File.open('my_midi_file.mid', 'rb') { | file |
    seq.read(file) { | track, num_tracks, i |
        # Print something when each track is read.
        puts "read track #{i} of #{num_tracks}"
    }
}

Writing a MIDI File

To write a MIDI file, call the write method, passing in an IO object.

require 'midilib/io/seqwriter'

# Start with a sequence that has something worth saving.
seq = read_or_create_seq_we_care_not_how()

# Write the sequence to a MIDI file.
File.open('my_output_file.mid', 'wb') { | file | seq.write(file) }

Editing a MIDI File

Combining the last two examples, here is a script that reads a MIDI file, transposes some events, and writes the sequence out to a different file. This is a useful template for programatically manipulating MIDI data.

This code transposes all of the note events (note on, note off, and poly pressure) on channel 5 down one octave.

Transposing One Channel

require 'midilib/io/seqreader'
require 'midilib/io/seqwriter'

# Create a new, empty sequence.
seq = MIDI::Sequence.new()

# Read the contents of a MIDI file into the sequence.
File.open('my_input_file.mid', 'rb') { | file |
    seq.read(file) { | track, num_tracks, i |
        # Print something when each track is read.
        puts "read track #{i} of #{num_tracks}"
    }
}

# Iterate over every event in every track.
seq.each { | track |
    track.each { | event |
        # If the event is a note event (note on, note off, or poly
        # pressure) and it is on MIDI channel 5 (channels start at
        # 0, so we use 4), then transpose the event down one octave.
        if MIDI::NoteEvent === event && event.channel == 4
            event.note -= 12
        end
    }
}

# Write the sequence to a MIDI file.
File.open('my_output_file.mid', 'wb') { | file | seq.write(file) }

Manipulating tracks

If you modify a track’s list of events directly, don’t forget to call MIDI::Track#recalc_times when you are done.

track.events[42, 1] = array_of_events
track.events << an_event
track.merge(array_of_events)
track.recalc_times

Calculating delta times

A few methods in MIDI::Sequence make it easier to calculate the delta times that represent note lengths. MIDI::Sequence#length_to_delta takes a note length (a multiple of a quarter note) and returns the delta time given the sequence’s current ppqn (pulses per quarter note) setting. 1 is a quarter note, 1.0/32.0 is a 32nd note (use floating-point numbers to avoid integer rounding), 1.5 is a dotted quarter, etc. See the documentation for that method for more information.

MIDI::Sequence#note_to_length takes a note name and returns a length value (again, as a multiple of a quarter note). Legal note names are those found in MIDI::Sequence::NOTE_TO_LENGTH, and may begin with “dotted” and/or end with “triplet”. For example, “whole”, “sixteenth”, “32nd”, “quarter triplet”, “dotted 16th”, and “dotted 8th triplet” are all legal note names.

Finally, MIDI::Sequence#note_to_delta takes a note name and returns a delta time. It does this by calling note_to_length, then passing the result to length_to_delta.

Example Scripts

Here are short descriptions of each of the examples found in the examples directory.

  • examples/from_scratch.rb shows you how to create a new sequence from scratch and save it to a MIDI file. It creates a file called ‘from_scratch.mid’.

  • examples/seq2text.rb dumps a MIDI file as text. It reads in a sequence and uses the to_s method of each event.

  • examples/reader2text.rb dumps a MIDI file as text. It subclasses MIDI::SeqReader instead of creating a sequence containing tracks and events.

  • examples/transpose.rb transposes all note events (note on, note off, poly pressure) on a specified channel by a specified amount.

  • There is also one MIDI file, examples/NoFences.mid. It is a little pop ditty I wrote. The instruments in this file use General MIDI patch numbers and drum note assignments. Since I don’t normally use GM patches, the sounds used here are at best approximations of the sounds I use.

Resources

The Ruby Web site (www.ruby-lang.org/en/index.html) contains an introduction to Ruby, the Ruby Application Archive (RAA) at raa.ruby-lang.org, and pointers to more information.

<cite>Programming Ruby, The Pragmatic Programmer’s Guide</cite>, by David Thomas and Andrew Hunt, is a well-written and practical introduction to Ruby. Its Web page at www.rubycentral.com/book also contains a wealth of Ruby information. Though the first edition book is available online, I encourage you to purchase a copy of the latest edition.

A description of the MIDI file format can be found in a few places such as www.csie.ntu.edu.tw/~r92092/ref/midi/.

The MIDI message reference at www.jimmenard.com/midi_ref.html describes the format of MIDI commands.

To Do

:include: TODO.rdoc

Support

Administrivia

Author

Jim Menard ([email protected])

Copyright

Copyright © 2003-2023 Jim Menard

License

Distributed under the same license as Ruby.

Copying

midilib is copyrighted free software by Jim Menard and is released under the same license as Ruby. See the Ruby license at www.ruby-lang.org/en/LICENSE.txt.

midilib may be freely copied in its entirety providing this notice, all source code, all documentation, and all other files are included.

midilib is Copyright © 2003-2023 by Jim Menard.

The song “No Fences” contained in the MIDI file examples/NoFences.mid is Copyright © 1992 by Jim Menard ([email protected]). It may be freely used for non-commercial purposes as long as the author is given credit.

Warranty

This software is provided “as is” and without any express or implied warranties, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose.

midilib's People

Contributors

adamjmurray avatar jimm avatar kaorukobo avatar mike-bourgeous avatar rhelsing avatar shaiguitar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

midilib's Issues

@curr_ticks looks haywire

I just ran a short MIDI file through examples/reader2text.rb, and the @curr_ticks output appeared erratic. (See the whole output on this pastie.) The only two values that showed up for @curr_ticks were 0 and 240, but in fact the MIDI file has 14 moments of attack for a total of 17 "note on" events.

I'm hoping to make use of a tool which will reliably indicate the order and simultaneity (where extant) of MIDI events. Can you shed some light on this for me?

Avoid monkey patching core classes

The core Array class is being monkey patched in lib/midilib/track.rb. Its definition of Array#split clashes with the implementation found in ActiveSupport which makes using midilib with Rails applications difficult.

I've created a patch at 326391d that avoids monkey patching by creating the MIDI::Array class that extends the core Array class.

Incorrect handling of pitchbend lsb and msb

It seems that the pitch bend messages are having their bytes read in the wrong order in the MIDI::PitchBend class.

This particulary affects some files like this one, created with tuxguitar: http://dl.dropbox.com/u/2701879/music/racemeeee.mid

Just opening and saving the same file (works with some of the code in the examples directory) will yield strange results.

I don't know if that's the right solution, but I'm using a monkey patched Midi::PitchBend in a project of mine that processes midi files and it seems to work the expected way:

class MIDI::PitchBend
    def data_as_bytes
        data = []
        data << (@status + @channel)
        # the values where switched
        data << ((@value >> 7) & 0x7f) # msb
        data << (@value & 0x7f) # lsb
    end
end 

Thank you

should length to delta round?

The length_to_delta method on sequence currently strips any decimal place to return only the integer part. When dealing with data that has been input from live playing (not quantized) this could be problematic.

eg:

seq.ppqn = 96
seq.length_to_data(0.237) #22.752 is interpreted as 22.

instead of to_i, would it not perhaps be better to use .round?

Docu example: how to play a midi file from within ruby?

Hi,

Could a simple example be added to the intro-documentation so that newcomers like me can figure out how to play midi files from within ruby?

Whether it is possible to do so without having to rely on external players, or whether it is only possible with i.e. mplayer.

Whatever the solution, I think a short section to tell people how to play those created midi files would be AWESOME!

Song tempo incorrectly calculated

MIDIs (even in format 1) can have multiple tempos.

method beats_per_minute in sequence.rb assumes that there is one single tempo on the first track for the entire song. Tempo events can occur on any track, but more importantly, multiple tempo events often occur throughout a song to account for accelerandos, etc.

# Returns the song tempo in beats per minute.
def beats_per_minute
  return DEFAULT_TEMPO if @tracks.nil? || @tracks.empty?
  event = @tracks.first.events.detect { |e| e.kind_of?(MIDI::Tempo) }
  return event ? (Tempo.mpq_to_bpm(event.tempo)) : DEFAULT_TEMPO
end

As a consequence, pulses per second is also incorrect. You can't make the assumption that there is a single, static tempo. A correct implementation of this requires summation across multiple tempos.

def pulses_to_seconds(pulses)
  (pulses.to_f / @ppqn.to_f / beats_per_minute()) * 60.0
end

Any plan to release 3.x to rubygems?

Do you have any plan to release midilib 3.x to rubygems?

Midilib in rubygems is stopped at 2.0.5, which turned out to not compatible to ruby 3. I believed that is the latest development for a long time.
However today I checked out the latest source and the test successfully ran in ruby 3.1.2.
I expect the valuable library to be updated and live with ruby 3 in rubygems. Is there any issue which prevents the release, or any breaking changes?

General Purpose CC codes are off

It looks like the CC codes in consts.rb are off:

  CC_HOLD_2 = 69
  CC_GEN_PURPOSE_5 = 50
  CC_GEN_PURPOSE_6 = 51
  CC_GEN_PURPOSE_7 = 52
  CC_GEN_PURPOSE_8 = 53
  CC_TREMELO_DEPTH = 92

The general purpose CCs 5 through 10 should start at 70, not 50.

Feature suggestion: determine track duration in SeqReader#eot for META_TRACK_END events

Hi there! I use midilib in a couple of my own projects, so thanks for writing it!

One of my projects is a toy FM synthesizer that can play a MIDI file. To detect the end of a song, I use the latest of the final note-off event, the final event in any track, or the final oscillator envelope decay. It'd be nice to be able to use the delta time of the FF 2F 00 track end meta-event to determine track length instead, at least for looping.

If this sounds interesting, I'd be happy to open a merge request that adds a track_length or duration attribute to MIDIFile, and modify SeqReader#eot to save track length (but while still not adding the META_TRACK_END event to the event list, in keeping with documented behavior).

Force into Format Type 0 possible?

Hey Jimm!

Is there a straight forward way to force a file/sequence into Format Type 0 using midilib? See attached an example binary structure of a Type 1 file with multiple tracks. Is there a way to force this into Type 0 with only 1 track without losing data? Sorry can't really find this in the docs.

Love from Germany
Mohan

Bildschirmfoto 2024-02-19 um 16 03 56

undefined method `note' for nil:NilClass

I'm getting this (test_tottoro.bat, test_tottoro.sh, ruby 2.7, both linux and windows):

chan_settings: ["d", "me", "p", "m", "m", "m", "me", "p", "mew"]
track , num_tracks 7, index 0
track #<MIDI::Track:0x000000000682f978>, num_tracks 7, index 1
...
mixing lchan:8 into 2
--- render into text ---
Traceback (most recent call last):                                                 
        7: from autosiril.rb:905:in `<main>'                                       
        6: from autosiril.rb:905:in `times'                                        
        5: from autosiril.rb:913:in `block in <main>'                              
        4: from autosiril.rb:913:in `map'                                          
        3: from autosiril.rb:914:in `block (2 levels) in <main>'                   
        2: from autosiril.rb:914:in `!='                                           
        1: from autosiril.rb:914:in `=='                                           
autosiril.rb:416:in `<=>': undefined method `note' for nil:NilClass (NoMethodError)

How to fix?

Notes get moved forward in time

I am not that much into the whole midi spec sorry about that, but maybe you can help me on this one maybe this is a problem in the midi file and not a problem with midilib:
When I just run the attached original midi file (I don't know how this was exported) through midilib via
seq.each { | track | track.each { | event | ... } }
without even manipulating it at all I get the result shown in the attached screenshot. It works until the marked point, then notes seem to get shifted "forward in time".

Maybe this has something to do with midi export resolution, delta times, I would be happy if anyone could help me understand this.

Thank you so much already!

Bildschirm­foto 2023-01-19 um 18 56 55

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.