MIDI.lua


NAME

MIDI.lua - Reading, writing and manipulating MIDI data


SYNOPSIS

 local MIDI = require 'MIDI'
 local my_score = {
    96,  -- ticks per beat
    {    -- first track
        {'patch_change', 0, 1, 8},
        {'note', 5, 96, 1, 25, 98},
        {'note', 101, 96, 1, 29, 98},
    },  -- end of first track
 }
 -- Going through a score within a Lua program...
 channels = {[2]=true, [3]=true, [5]=true, [8]=true, [13]=true}
 for itrack = 2,#my_score do  -- skip 1st element, which is ticks
    for k,event in ipairs(my_score[itrack]) do
       if event[1] == 'note' then
          -- for example, do something to all notes
       end
       -- to work on events in only particular channels...
       channelindex = MIDI.Event2channelindex[event[1]]
       if channelindex and channels[event[channelindex]] then
          -- do something to channels 2,3,5,8 and 13
       end
    end
 end
 local midifile = assert(io.open('f.mid','w'))
 midifile:write(MIDI.score2midi(my_score))
 midifile:close()


DESCRIPTION

This module offers functions:   concatenate_scores(),   grep(),   merge_scores(),   mix_opus_tracks(),   mix_scores(),   midi2opus(),   midi2score(),   opus2midi(),   opus2score(),   play_score(),   score2midi(),   score2opus(),   score2stats(),   score_type(),   segment(),   timeshift() and   to_millisecs(),   where "midi" means the MIDI-file bytes (as can be put in a .mid file, or piped into aplaymidi), and opus and score are list-structures as inspired by Sean Burke's MIDI-Perl CPAN module.

The opus is a direct translation of the midi-file-events (see opus2midi()), where the times are delta-times, in ticks, since the previous event:

 {'note_on',  dtime, channel, note, velocity}       -- in an opus
 {'note_off', dtime, channel, note, velocity}       -- in an opus

The score is more human-centric (see score2opus()); it uses absolute times, and combines the separate note_on and note_off events into one "note" event, with a duration:

 {'note', start_time, duration, channel, note, velocity} -- in a score

MIDI.lua is a call-compatible translation into Lua of the Python module MIDI.py;
see peterbillam.fastmail.user.fm/midi/MIDI.html

As an example, the script in_c.lua when run as lua in_c.lua -s 999 -q -m 20
generated in_c.mp3 ( see en.wikipedia.org/wiki/In_C )

As another example, the script   txt2morse   plays the morse code corresponding to its input text, eg:
  echo Hello, world! | txt2morse


FUNCTIONS

concatenate_scores(),   grep(),   merge_scores(),   mix_opus_tracks(),   mix_scores(),   midi2opus(),   midi2score(),   opus2midi(),   opus2score(),   play_score(),   score2midi(),   score2opus(),   score2stats(),   score_type(),   segment(),   timeshift() and   to_millisecs()

concatenate_scores (array_of_scores)

Concatenates an array of scores into one score. If the scores differ in their ticks parameter, they will all get converted to millisecond-tick format.

grep (score, channels)

Returns a score containing only the channels specified. (It also works on an opus, but because of the incremental times the result will usually be useless.) The second parameter is an array of the wanted channel numbers, for example:

 channels = {0, 4,}
merge_scores (array_of_scores)

Merges an array of scores into one score. A merged score comprises all of the tracks from all of the input scores; un-merging is possible by selecting just some of the tracks. If the scores differ in their ticks parameter, they will all get converted to millisecond-tick format. merge_scores attempts to resolve channel-conflicts, but there are of course only 15 available channels...

mix_opus_tracks (array_of_tracks)

Mixes an array of tracks into one track. A mixed track cannot be un-mixed. It is assumed that the tracks share the same ticks parameter and the same tempo. Mixing score-tracks is trivial (just insert all events into one array). Mixing opus-tracks is only slightly harder, but it's common enough that a dedicated function is useful.

mix_scores (array_of_scores)

Mixes an array of scores into one one-track score. A mixed score cannot be un-mixed. Hopefully the scores have no undesirable channel conflicts between them... If the scores differ in their ticks parameter, they will all get converted to millisecond-tick format.

midi2ms_score (midi_in_string_form)

Translates MIDI into a score with one beat per second and one tick per millisecond, using midi2opus() then to_millisecs() then opus2score()

midi2opus (midi_in_string_form)

Translates MIDI into an opus. For a description of the opus format, see opus2midi().

midi2score (midi_in_string_form)

Translates MIDI into a score, using midi2opus() then opus2score()

opus2midi (an_opus)

The argument is an array: the first item in the list is the ticks parameter, the others are the tracks. Each track is an array of midi-events, and each event is itself an array; see EVENTS below. opus2midi() returns a string of the MIDI, which can then be written to a .mid file, or to stdout.

 local MIDI = require 'MIDI'
 my_opus = {
    96, -- MIDI-ticks per beat
    {   -- first track:
        {'patch_change', 0, 1, 8},   -- and these are the events...
        {'set_tempo', 0, 750000},    -- microseconds per beat
        {'note_on',   5, 1, 25, 96},
        {'note_off', 96, 1, 25, 0},
        {'note_on',   0, 1, 29, 96},
        {'note_off', 96, 1, 29, 0},
    },  -- end of first track
 }
 local my_midi = MIDI.opus2midi(my_opus)
 io.write(my_midi)  -- can be saved in o.mid or piped into "aplaymidi -"
opus2score (an_opus)

Converts the "opus" to a "score". For a description of the opus and score formats, see opus2midi() and score2opus().
The score track is returned sorted by the end-times of the notes, so if you need it sorted by their start-times you have to do that yourself:
  table.sort(score[itrack], function (e1,e2) return e1[2]<e2[2] end)

play_score (opus_or_score)

Converts the score to midi, and feeds it into 'aplaymidi -'
If Lua's posix module is installed, the aplaymidi process will run in the background.

score_type (opus_or_score)

Returns a string, either 'opus' or 'score' or ''

score2midi (a_score)

Translates a score into MIDI, using score2opus() then opus2midi()

score2opus (a_score)

The argument is an array: the first item in the list is the ticks parameter, the others are the tracks. Each track is an array of score-events, and each event is itself an array. score2opus() returns an array specifying the equivalent opus. A score-event is similar to an opus-event (see above), except that in a score:
1) all times are expressed as an absolute number of ticks from the track's start time
2) the pairs of 'note_on' and 'note_off' events in an opus are abstracted into a single 'note' event

 {'note', start_time, duration, channel, pitch, velocity}

 my_score = {
    96,
    {   -- first track
        {'patch_change', 0, 1, 8},
        {'note',   5, 96, 1, 25, 98},
        {'note', 101, 96, 1, 29, 98},
    },  -- end of first track
 }
 my_opus = score2opus(my_score)
score2stats (opus_or_score)

Returns a table of some basic stats about the score, like:

 bank_select (array of 2-element arrays {msb,lsb}),
 channels_by_track (table, by track, of arrays),
 channels_total (array),
 general_midi_mode (array),
 ntracks,
 nticks,
 num_notes_by_channel (table of numbers),
 patch_changes_by_track (table of tables),
 patch_changes_total (array),
 percussion (a dictionary histogram of channel-9 events),
 pitches (dict histogram of pitches on channels other than 9),
 pitch_range_by_track (table, by track, of two-member-arrays),
 pitch_range_sum (sum over tracks of the pitch_ranges)
segment (score, start_time, end_time, tracks)
segment {score, start_time=100, end_time=2000, tracks={3,4,5}}

Returns a score which is a segment of the one supplied as the argument, beginning at "start_time" ticks and ending at "end_time" ticks (or at the end if "end_time" is not supplied). If the array "tracks" is specified, only those tracks will be returned.

The current state at the start of the segment, of the tempo, the patches and the controllers is noted, and these settings are added in at the beginning of the returned segment, so that it sounds the same.

timeshift (score, shift, start_time, from_time, tracks)
timeshift {score, shift=50, start_time=nil, from_time=2000, tracks={2,3}}

Returns a score shifted in time by "shift" ticks, or shifted so that the first event starts at "start_time" ticks.

If "from_time" is specified, only those events in the score that begin after it are shifted. If "start_time" is less than "from_time" (or "shift" is negative), then the intermediate notes are deleted, though patch-change events are preserved.

If "tracks" are specified, then only those tracks (0 to 15) get shifted. "tracks" should be an array.

It is deprecated to specify both "shift" and "start_time". If this does happen, timeshift() will print a warning to stderr and ignore the "shift" argument.

If "shift" is negative and sufficiently large that it would leave some event with a negative tick-value, then the score is shifted so that the first event occurs at time 0. This also occurs if "start_time" is negative, and is also the default if neither "shift" nor "start_time" are specified.

to_millisecs (an_opus)

Recallibrates all the times in an opus to use one beat per second and one tick per millisecond. This makes it hard to retrieve any information about beats or barlines, but it does make it easy to mix different scores together.


EVENTS

The opus is a direct translation of the midi-file-events, where the times are delta-times, in ticks, since the previous event.

 {'note_on',  dtime, channel, note, velocity}       -- in an opus
 {'note_off', dtime, channel, note, velocity}       -- in an opus

The score is more human-centric; it uses absolute times, and combines the separate note_on and note_off events into one "note" event, with a duration:

 {'note', start_time, duration, channel, note, velocity} -- in a score

Events (in an opus structure):

 {'note_off', dtime, channel, note, velocity}       -- in an opus
 {'note_on',  dtime, channel, note, velocity}       -- in an opus
 {'key_after_touch', dtime, channel, note, velocity}
 {'control_change', dtime, channel, controller(0-127), value(0-127)}
 {'patch_change', dtime, channel, patch}
 {'channel_after_touch', dtime, channel, velocity}
 {'pitch_wheel_change', dtime, channel, pitch_wheel}
 {'text_event', dtime, text}
 {'copyright_text_event', dtime, text}
 {'track_name', dtime, text}
 {'instrument_name', dtime, text}
 {'lyric', dtime, text}
 {'marker', dtime, text}
 {'cue_point', dtime, text}
 {'text_event_08', dtime, text}
 {'text_event_09', dtime, text}
 {'text_event_0a', dtime, text}
 {'text_event_0b', dtime, text}
 {'text_event_0c', dtime, text}
 {'text_event_0d', dtime, text}
 {'text_event_0e', dtime, text}
 {'text_event_0f', dtime, text}
 {'end_track', dtime}
 {'set_tempo', dtime, tempo}
 {'smpte_offset', dtime, hr, mn, se, fr, ff}
 {'time_signature', dtime, nn, dd, cc, bb}
 {'key_signature', dtime, sf, mi}
 {'sequencer_specific', dtime, raw}
 {'raw_meta_event', dtime, command(0-255), raw}
 {'sysex_f0', dtime, raw}
 {'sysex_f7', dtime, raw}
 {'song_position', dtime, song_pos}
 {'song_select', dtime, song_number}
 {'tune_request', dtime}


DATA TYPES

 channel = a value 0 to 15
 controller = 0 to 127 (see peterbillam.fastmail.user.fm/muscript/gm.html#cc)
 dtime = time measured in ticks, 0 to 268435455
 velocity = a value 0 (soft) to 127 (loud)
 note = a value 0 to 127  (middle-C is 60)
 patch = 0 to 127 (see peterbillam.fastmail.user.fm/muscript/gm.html )
 pitch_wheel = a value -8192 to 8191 (\x1FFF)
 raw = 0 or more bytes of binary data (for sysex events see below)
 sequence_number = a value 0 to 65,535 (\xFFFF)
 song_pos = a value 0 to 16,383 (\x3FFF)
 song_number = a value 0 to 127
 tempo = microseconds per crochet (quarter-note), 0 to 16777215
 text = a string of 0 or more bytes of ASCII text
 ticks = the number of ticks per crochet (quarter-note)

In sysex_f0 events, the raw data must not start with a \xF0 byte, since this gets added automatically;
  but it must end with an explicit \xF7 byte !
In the very unlikely case that you ever need to split sysex data into one sysex_f0 followed by one or more sysex_f7s, then only the last of those sysex_f7 events must end with the explicit \xF7 byte   (again, the raw data of individual sysex_f7 events must not start with any \xF7 byte, since this gets added automatically).


PUBLIC-ACCESS TABLES

Number2patch

In this table the index is the patch-number (0 to 127), and the value is its corresponding General-MIDI Patch (on Channels other than 9). See: peterbillam.fastmail.user.fm/muscript/gm.html#patch

Notenum2percussion

In this table the index is the note-number (35 to 81), and the value is its corresponding General-MIDI Percussion instrument (on Channel 9). See: peterbillam.fastmail.user.fm/muscript/gm.html#perc

Event2channelindex

In this table the index is the event-name (see EVENTS), and the value is the position within the event-array at which the Channel-number occurs. It is very useful for manipulating particular channels within a score (see SYNOPSIS)


DOWNLOAD

This module is available as a LuaRock in luarocks.org/modules/peterbillam so you should be able to install it with the command:

 $ su
 Password:
 # luarocks install midi

The test script used during development is peterbillam.fastmail.user.fm/comp/lua/test_mi.lua
which requires the DataDumper.lua module.

You should be able to install the luaposix module with:
    # luarocks install luaposix

and datadumper with either:
    # luarocks install datadumper
or, if you're using Lua 5.3:
    # luarocks install http://peterbillam.fastmail.user.fm/comp/lua/datadumper-1.1-0.rockspec


CHANGES

 20170917 6.8 fix 153: bad argument #1 to 'char', and round dtime
 20160702 6.7 to_millisecs() now handles set_tempo across multiple Tracks
 20150921 6.5 segment restores controllers as well as patch and tempo
 20150920 6.4 segment respects a set_tempo exactly on the start time
 20150628 6.3 absent any set_tempo, default is 120bpm (see MIDI filespec 1.1)
 20150422 6.2 works with lua5.3
 20140609 6.1 switch pod and doc over to using moonrocks 
 20140108 6.0 in lua5.2 require('posix') returns the posix table
 20120504 5.9 add the contents of mid_opus_tracks()
 20111129 5.7 _encode handles empty tracks; score2stats num_notes_by_channel
 20111111 5.6 fix patch 45 and 46 in Number2patch, should be Pizz and Harp
 20110115 5.5 add mix_opus_tracks()
 20110126 5.4 "previous message repeated N times" to save space on stderr
 20110126 5.3 robustness fix if one note_on and multiple note_offs
 20110125 5.2 opus2score terminates unended notes at the end of the track
 20110124 5.1 the warnings in midi2opus display track_num
 20110122 5.0 sysex2midimode.get pythonism eliminated
 20110119 4.9 copyright_text_event "time" item was missing
 20110110 4.8 note_on with velocity=0 treated as a note-off
 20110109 4.7 many global vars localised, passes lualint :-)
 20110108 4.6 duplicate int2sevenbits removed, passes lualint -r
 20110108 4.5 related end_track bugs fixed around line 516
 20110108 4.4 null text_event bug fixed
 20101026 4.3 segment() remembers all patch_changes, not just the list values
 20101010 4.2 play_score() uses posix.fork if available
 20101009 4.2 merge_scores() moves aside conflicting channels correctly
 20101006 4.1 concatenate_scores() deepcopys also its 1st score
 20101006 4.1 segment() uses start_time and end_time named arguments
 20101005 4.1 timeshift() must not pad the set_tempo command
 20101003 4.0 pitch2note_event must be chapitch2note_event
 20100918 3.9 set_sequence_number supported, FWIW
 20100918 3.8 timeshift and segment accept named args
 20100913 3.7 first released version

AUTHOR

Peter J Billam,   peterbillam.fastmail.user.fm/comp/contact.html


SEE ALSO

 peterbillam.fastmail.user.fm/
 peterbillam.fastmail.user.fm/comp/
 peterbillam.fastmail.user.fm/comp/lua/test_mi.lua
 peterbillam.fastmail.user.fm/midi/free/txt2morse
 peterbillam.fastmail.user.fm/muscript/gm.html
 www.michael-gogins.com
 www.cs.cmu.edu/~music/cmsip/readings/Standard-MIDI-file-format-updated.pdf
 peterbillam.fastmail.user.fm/midi/free/MIDI.py
 peterbillam.fastmail.user.fm/midi/MIDI.html
 peterbillam.fastmail.user.fm/midi/free/in_c.lua
 peterbillam.fastmail.user.fm/midi/free/in_c.mp3
 http://luarocks.org/modules/peterbillam/MIDI
 peterbillam.fastmail.user.fm/comp/lua/midialsa.html
 http://luarocks.org/modules/peterbillam/midialsa
 http://luarocks.org/modules/gvvaughan/luaposix
 http://luarocks.org/modules/luarocks/datadumper