glitchdetector icon

MIDI

glitchdetector | PRO | 10/10/18 08:09:44 PM UTC | 0 ⭐ | 565 👁️ | Never ⏰ | []
Lua |

67.24 KB

|

None

|

0 👍

/

0 👎

#!/usr/bin/lua
--require 'DataDumper'   -- http://lua-users.org/wiki/DataDumper
local M = {} -- public interface
M.Version = 'VERSION'
M.VersionDate = 'DATESTAMP'
-- 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
 
---------------------------- private -----------------------------
local sysex2midimode = {
    ["\126\127\09\01\247"] = 1,
    ["\126\127\09\02\247"] = 0,
    ["\126\127\09\03\247"] = 2,
}
 
local previous_warning = '' -- 5.4
local previous_times = 0    -- 5.4
local function clean_up_warnings() -- 5.4
    -- Call this before returning from any publicly callable function
    -- whenever there's a possibility that a warning might have been printed
    -- by the function, or by any private functions it might have called.
    if previous_times > 1 then
        io.stderr:write('  previous message repeated '
         ..previous_times..' times\n')
    elseif previous_times > 0 then
        io.stderr:write('  previous message repeated\n')
    end
    previous_times = 0
    previous_warning = ''
end
local function warn(str)
    if str == previous_warning then -- 5.4
        previous_times = previous_times + 1
    else
        clean_up_warnings()
        io.stderr:write(str,'\n')
        previous_warning = str
    end
end
local function die(str)
    clean_up_warnings()
    io.stderr:write(str,'\n')
    os.exit(1)
end
local function round(x) return math.floor(x+0.5) end
 
local function readOnly(t)  -- Programming in Lua, page 127
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function (t, k, v)
            die("attempt to update a read-only table")
        end
    }
    setmetatable(proxy, mt)
    return proxy
end
 
local function dict(a)
    local d = {}
    if a == nil then return d end
    for k,v in ipairs(a) do d[v] = true end
    return d
end
 
local function sorted_keys(t)
    local a = {}
    for k,v in pairs(t) do a[#a+1] = k end
    table.sort(a)
    return  a
end
 
local function int2byte(i)
    return string.char(math.floor((i+0.5) % 256))
end
 
local function int2sevenbits(i)
    return string.char(math.floor((i+0.5) % 128))
end
 
local function int2twobytes(i)
    local b1 = math.floor(i/256) % 256
    local b2 = i % 256
    return string.char(b1,b2)
end
 
local function twobytes2int(s)
    return 256*string.byte(string.sub(s,1)) + string.byte(string.sub(s,2))
end
 
local function int2fourbytes(i)
    local b1 = math.floor(i/16777216) % 256
    local b2 = math.floor(i/65536) % 256
    local b3 = math.floor(i/256) % 256
    local b4 = i % 256
    return string.char(b1,b2,b3,b4)
end
 
local function fourbytes2int(s)
    return 16777216*string.byte(string.sub(s,1)) +
     65536 * string.byte(string.sub(s,2)) +
     256*string.byte(string.sub(s,3)) + string.byte(string.sub(s,4))
end
 
local function read_14_bit(byte_a)
    -- decode a 14 bit quantity from two bytes,
    return string.byte(byte_a,1) + 128 * string.byte(byte_a,2)
end
 
local function write_14_bit(integer)
    -- encode a 14 bit quantity into two bytes,
    return string.char(integer % 128, math.floor(integer/128) % 128)
    -- return string.char((integer/128) % 128, integer % 128) 
end
 
local function ber_compressed_int(integer)
--[[BER compressed integer (not an ASN.1 BER, see perlpacktut for
details).  Its bytes represent an unsigned integer in base 128,
most significant digit first, with as few digits as possible.
Bit eight (the high bit) is set on each byte except the last.
]]
-- stderr.write('integer = ..',integer)
    -- warn('integer = '..tostring(integer)..' type '..type(integer))
    if integer == 0 then return '\000' end
    local ber = { string.char(integer % 128) }
    while integer > 127 do
        integer = math.floor(integer/128)
        local seven_bits = integer % 128
        table.insert(ber, 1, string.char(128+seven_bits))
    end
    return table.concat(ber)
end
 
local function str2ber_int(s, start)
--[[Given (a string, and a position within it), returns
(the ber_integer at that position, and the position after the ber_integer).
]]
    local i = start
    local integer = 0
    while true do
        local byte = string.byte(s, i)
        integer = integer + (byte%128)
        if byte < 127.5 then
            return integer, i+1
        end
        if i >= #s then
            warn('str2ber_int: no end-of-integer found')
            return 0, start
        end
        i = i + 1
        integer = integer * 128
    end
end
 
local function some_text_event(which_kind, text)
    if not which_kind then which_kind = 1 end
    if not text then text = 'some_text' end
    return '\255'..int2sevenbits(which_kind)..ber_compressed_int(text:len())..text
end
 
local function copy(t)
    local new_table = {}
    for k, v in pairs(t) do new_table[k] = v end
    return new_table
end
 
local function deepcopy(object)  -- http://lua-users.org/wiki/CopyTable
    local lookup_table = {}
    local function _copy(object)
        if type(object) ~= "table" then
            return object
        elseif lookup_table[object] then
            return lookup_table[object]
        end
        local new_table = {}
        lookup_table[object] = new_table
        for index, value in pairs(object) do
            new_table[_copy(index)] = _copy(value)
        end
        return setmetatable(new_table, getmetatable(object))
    end
    return _copy(object)
end
 
local function _decode(trackdata, exclude, include, event_callback, exclusive_event_callback, no_eot_magic)
--[[Decodes MIDI track data into an opus-style list of events.
The options:
  'exclude' is a dictionary-table of event types which will be ignored
  'include' (and no exclude), makes exclude an array of all
      possible events, /minus/ what include specifies
  'event_callback' is a function
  'exclusive_event_callback' is a function
]]
 
    if not trackdata then trackdata= '' end
    if not exclude then exclude = {} end
    if not include then include = {} end
    if include and not exclude then exclude = M.All_events end  -- 4.6
 
    local event_code = -1 -- used for running status
    local event_count = 0
    local events = {}
 
    local i = 1     -- in Lua, i is the pointer to within the trackdata
    while i < #trackdata do   -- loop while there's anything to analyze
        local eot = false -- when True event registrar aborts this loop 4.6,4.7
        event_count = event_count + 1
 
        local E = {} -- event; feed it to the event registrar at the end. 4.7
 
        -- Slice off the delta time code, and analyze it
        local time
        time, i = str2ber_int(trackdata, i)
 
        -- Now let's see what we can make of the command
        local first_byte = string.byte(trackdata,i); i = i+1
 
        if first_byte < 240 then  -- It's a MIDI event
            if first_byte % 256 > 127 then
                event_code = first_byte
            else
                -- It wants running status; use last event_code value
                i = i-1
                if event_code == -1 then
                    warn("Running status not set; Aborting track.")
                    return {}
                end
            end
 
            local command = math.floor(event_code / 16) * 16
            local channel = event_code % 16
            local parameter
            local param1
            local param2
 
            if command == 246 then  --  0-byte argument
                --pass
            elseif command == 192 or command == 208 then  --  1-byte arg
                parameter = string.byte(trackdata, i); i = i+1
            else -- 2-byte argument could be BB or 14-bit
                param1 = string.byte(trackdata, i); i = i+1
                param2 = string.byte(trackdata, i); i = i+1
            end
 
            ----------------- MIDI events -----------------------
 
            local continue = false
            if command      == 128 then
                if exclude['note_off'] then
                    continue = true
                else
                    E = {'note_off', time, channel, param1, param2}
                end
            elseif command == 144 then
                if exclude['note_on'] then
                    continue = true
                else
                    E = {'note_on', time, channel, param1, param2}
                end
            elseif command == 160 then
                if exclude['key_after_touch'] then
                    continue = true
                else
                    E = {'key_after_touch',time,channel,param1,param2}
                end
            elseif command == 176 then
                if exclude['control_change'] then
                    continue = true
                else
                    E = {'control_change',time,channel,param1,param2}
                end
            elseif command == 192 then
                if exclude['patch_change'] then
                    continue = true
                else
                    E = {'patch_change', time, channel, parameter}
                end
            elseif command == 208 then
                if exclude['channel_after_touch'] then
                    continue = true
                else
                    E = {'channel_after_touch', time, channel, parameter}
                end
            elseif command == 224 then
                if exclude['pitch_wheel_change'] then
                    continue = true
                else -- the 2 param bytes are a 14-bit int
                    E = {'pitch_wheel_change', time, channel,
                     128*param2+param1-8192}
                end
            else
                warn("Shouldn't get here; command="..tostring(command))
            end
 
        elseif first_byte == 255 then  -- It's a Meta-Event!
            local command = string.byte(trackdata, i); i = i+1
            local length
            length, i = str2ber_int(trackdata, i)
            if (command      == 0) then
                if length == 2 then  -- 3.9
                    E = {'set_sequence_number', time,
                     twobytes2int(string.sub(trackdata,i,i+1)) }
                else
                    warn('set_sequence_number: length must be 2, not '
                     .. tostring(length))
                    E = {'set_sequence_number', time, 0}
                end
 
            -- Defined text events ------
            elseif command == 1 then
                E = {'text_event', time, string.sub(trackdata,i,i+length-1)}
            elseif command == 2 then  -- 4.9
                E = {'copyright_text_event', time, string.sub(trackdata,i,i+length-1)}
            elseif command == 3 then
                E = {'track_name',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 4 then
                E = {'instrument_name',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 5 then
                E = {'lyric',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 6 then
                E = {'marker',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 7 then
                E = {'cue_point',time, string.sub(trackdata,i,i+length-1)}
 
            -- Reserved but apparently unassigned text events -------------
            elseif command == 8 then
                E = {'text_event_08',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 9 then
                E = {'text_event_09',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 10 then
                E = {'text_event_0a',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 11 then
                E = {'text_event_0b',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 12 then
                E = {'text_event_0c',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 13 then
                E = {'text_event_0d',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 14 then
                E = {'text_event_0e',time, string.sub(trackdata,i,i+length-1)}
            elseif command == 15 then
                E = {'text_event_0f',time, string.sub(trackdata,i,i+length-1)}
            
            -- Now the sticky events -------------------------------------
            elseif command == 47 then
                E = {'end_track', time}
                -- The code for handling this, oddly, comes LATER,
                -- in the event registrar.
            elseif command == 81 then -- DTime, Microseconds/Crochet
                if length ~= 3 then
                    warn('set_tempo event, but length='..length)
                end
                E = {'set_tempo', time,
                    string.byte(trackdata,i) * 65536
                    + string.byte(trackdata,i+1) * 256
                    + string.byte(trackdata,i+2)
                }
            elseif command == 84 then
                if length ~= 5 then   -- DTime, HR, MN, SE, FR, FF
                    warn('smpte_offset event, but length='..length)
                end
                E = {'smpte_offset', time,
                    string.byte(trackdata,i),
                    string.byte(trackdata,i+1),
                    string.byte(trackdata,i+2),
                    string.byte(trackdata,i+3),
                    string.byte(trackdata,i+4)
                }
            elseif command == 88 then
                if length ~= 4 then   -- DTime, NN, DD, CC, BB
                    warn('time_signature event, but length='..length)
                end
                E = {'time_signature', time,
                    string.byte(trackdata,i),
                    string.byte(trackdata,i+1),
                    string.byte(trackdata,i+2),
                    string.byte(trackdata,i+3)
                }
            elseif command == 89 then
                if length ~= 2 then   -- DTime, SF(signed), MI
                    warn('key_signature event, but length='..length)
                end
                local b1 = string.byte(trackdata,i)
                if b1 > 127 then b1 = b1 - 256 end   -- signed byte :-(
                local b2 = string.byte(trackdata,i+1)
                -- list(struct.unpack(">bB",trackdata[0:2]))}
                E = {'key_signature', time, b1, b2 }
            elseif (command == 127) then
                E = {'sequencer_specific',time,
                    string.sub(trackdata,i,i+length-1)}
            else
                E = {'raw_meta_event', time, command,
                    string.sub(trackdata,i,i+length-1)}
                --"[uninterpretable meta-event command of length length]"
                -- DTime, Command, Binary Data
                -- It's uninterpretable; record it as raw_data.
            end
 
            -- Pointer += length; --  Now move Pointer
            i = i + length
            -- Hmm... in lua, we should be using Pointer again....
            -- trackdata =  string.sub(trackdata, length+1)
 
        --#####################################################################
        elseif first_byte == 240 or first_byte == 247 then
            -- Note that sysexes in MIDI /files/ are different than sysexes
            -- in MIDI transmissions!! The vast majority of system exclusive
            -- messages will just use the F0 format. For instance, the
            -- transmitted message F0 43 12 00 07 F7 would be stored in a
            -- MIDI file as F0 05 43 12 00 07 F7. As mentioned above, it is
            -- required to include the F7 at the end so that the reader of the
            -- MIDI file knows that it has read the entire message. (But the F7
            -- is omitted if this is a non-final block in a multiblock sysex;
            -- but the F7 (if there) is counted in the message's declared
            -- length, so we don't have to think about it anyway.)
            --command = trackdata.pop(0)
            local length
            length, i = str2ber_int(trackdata, i)
            if first_byte == 240 then
                -- 20091008 added ISO-8859-1 to get an 8-bit str
                E = {'sysex_f0', time, string.sub(trackdata,i,i+length-1)}
            else
                E = {'sysex_f7', time, string.sub(trackdata,i,i+length-1)}
            end
            i = i + length
            -- trackdata =  string.sub(trackdata, length+1)
 
        --#####################################################################
        -- Now, the MIDI file spec says:
        --  <track data> = <MTrk event>+
        --  <MTrk event> = <delta-time> <event>
        --  <event> = <MIDI event> | <sysex event> | <meta-event>
        -- I know that, on the wire, <MIDI event> can include note_on,
        -- note_off, and all the other 8x to Ex events, AND Fx events
        -- other than F0, F7, and FF -- namely, <song position msg>,
        -- <song select msg>, and <tune request>.
        --
        -- Whether these can occur in MIDI files is not clear specified
        -- from the MIDI file spec.  So, I'm going to assume that
        -- they CAN, in practice, occur.  I don't know whether it's
        -- proper for you to actually emit these into a MIDI file.
        
        elseif first_byte == 242 then   -- DTime, Beats
            --  <song position msg> ::=     F2 <data pair>
            E = {'song_position', time, read_14_bit(string.sub(trackdata,i))}
            trackdata = string.sub(trackdata,3)
 
        elseif first_byte == 243 then -- <song select> ::= F3 <data singlet>
            -- E=['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]
            E = {'song_select', time, string.byte(trackdata,i)}
            -- trackdata = trackdata[1:]
            trackdata = string.sub(trackdata,2)
            -- DTime, Thing (what?! song number?  whatever ...)
 
        elseif first_byte == 246 then   -- DTime
            E = {'tune_request', time}
            -- What would a tune request be doing in a MIDI /file/?
 
        --########################################################
        -- ADD MORE META-EVENTS HERE.  TODO:
        -- f1 -- MTC Quarter Frame Message. One data byte follows
        --     the Status; it's the time code value, from 0 to 127.
        -- f8 -- MIDI clock.    no data.
        -- fa -- MIDI start.    no data.
        -- fb -- MIDI continue. no data.
        -- fc -- MIDI stop.     no data.
        -- fe -- Active sense.  no data.
        -- f4 f5 f9 fd -- unallocated
 
--[[
        elseif (first_byte > 240) { -- Some unknown kinda F-series event ####
            -- Here we only produce a one-byte piece of raw data.
            -- But the encoder for 'raw_data' accepts any length of it.
            E = [ 'raw_data', time, substr(trackdata,Pointer,1) ]
            -- DTime and the Data (in this case, the one Event-byte)
            ++Pointer;  -- itself
 
]]
        elseif first_byte > 240 then  -- Some unknown F-series event
            -- Here we only produce a one-byte piece of raw data.
            E = {'raw_data', time, string.byte(trackdata,i)}  -- 4.6
            trackdata = string.sub(trackdata,2)  -- 4.6
        else  -- Fallthru.
            warn(string.format("Aborting track.  Command-byte first_byte=0x%x",first_byte)) --4.6
            break
        end
        -- End of the big if-group
 
 
        --#####################################################################
        --  THE EVENT REGISTRAR...
        -- warn('3: E='+str(E))
        if E and  E[1] == 'end_track' then
            -- This is the code for exceptional handling of the EOT event.
            eot = true
            if not no_eot_magic then
                if E[2] > 0 then  -- a null text-event to carry the delta-time
                    E = {'text_event', E[2], ''}  -- 4.4
                else
                    E = nil   -- EOT with a delta-time of 0; ignore it.
                end
            end
        end
 
        if E and not exclude[E[1]] then
            --if ( $exclusive_event_callback ):
            --    &{ $exclusive_event_callback }( @E );
            --else
            --    &{ $event_callback }( @E ) if $event_callback;
            events[#events+1] = E
        end
        if eot then break end
    end
    -- End of the big "Event" while-block
 
    return events
end
 
local function _encode(events_lol)
    local no_running_status = false
    local no_eot_magic      = false   -- 4.6
    local never_add_eot     = false   -- 4.6
    local unknown_callback  = false   -- 4.6
    local data = {} -- what I'll store the chunks of byte-data in
 
    -- This is so my end_track magic won't corrupt the original
    local events = deepcopy(events_lol)
 
    if not never_add_eot then -- One way or another, tack on an 'end_track'
        if #events > 0 then   -- 5.7
            local last = events[#events] -- 4.5, 4.7
            if not (last[1] == 'end_track') then  -- no end_track already
                if (last[1] == 'text_event' and last[3] == '') then -- 4.5,4.6
                    -- 0-length text event at track-end. 
                    if no_eot_magic then
                        -- Exceptional case: don't mess with track-final
                        -- 0-length text_events; just peg on an end_track
                        events.append({'end_track', 0})
                    else
                        -- NORMAL CASE: replace with an end_track, leaving DTime
                        last[1] = 'end_track'
                    end
                else
                    -- last event was neither 0-length text_event nor end_track
                    events[#events+1] = {'end_track', 0}
                end
            end
        else  -- an eventless track!
            events = { {'end_track', 0},}
        end
    end
 
    -- maybe_running_status = not no_running_status  -- unused? 4.7
    local last_status = -1 -- 4.7
 
    for k,E in ipairs(events) do
        -- get rid of the two pop's and increase the other E[] indices by two
        if not E then break end
 
        local event = E[1] -- 4.7
        if #event < 1 then break end
 
        local dtime = round(E[2]) -- 4.7 6.8
        -- print('event='..event..' dtime='..dtime)
 
        local event_data = '' -- 4.7
 
        if    -- MIDI events -- eligible for running status
             event  == 'note_on'
             or event == 'note_off'
             or event == 'control_change'
             or event == 'key_after_touch'
             or event == 'patch_change'
             or event == 'channel_after_touch'
             or event == 'pitch_wheel_change'   then
 
            -- This block is where we spend most of the time.  Gotta be tight.
            local status = nil     -- 4.7
            local parameters = nil -- 4.7
            if (event == 'note_off') then
                status = 128 + (E[3] % 16)
                parameters = int2sevenbits(E[4]%128)..int2sevenbits(E[5]%128)
            elseif event == 'note_on' then
                status = 144 + (E[3] % 16)
                parameters = int2sevenbits(E[4]) .. int2sevenbits(E[5])
            elseif event == 'key_after_touch' then
                status = 160 + (E[3] % 16)
                parameters = int2sevenbits(E[4]) .. int2sevenbits(E[5])
            elseif event == 'control_change' then
                status = 176 + (E[3] % 16)
                parameters = int2sevenbits(E[4]) .. int2sevenbits(E[5])
            elseif event == 'patch_change' then
                status = 192 + (E[3] % 16)
                parameters = int2sevenbits(E[4])
            elseif event == 'channel_after_touch' then
                status = 208 + (E[3] % 16)
                parameters = int2sevenbits(E[4])
            elseif event == 'pitch_wheel_change' then
                status = 224 + (E[3] % 16)
                parameters =  write_14_bit(E[4] + 8192)
            else
                warn("BADASS FREAKOUT ERROR 31415!")
            end
 
            -- And now the encoding
 
            data[#data+1] = ber_compressed_int(dtime)
            if (status ~= last_status) or no_running_status then
                data[#data+1] = int2byte(status)
            end
            data[#data+1] = parameters
            last_status = status
            -- break
        else
            -- Not a MIDI event.
            last_status = -1
 
            if event == 'raw_meta_event' then
                event_data = some_text_event(E[3], E[4])
            elseif (event == 'set_sequence_number') then  -- 3.9
                event_data = some_text_event(0, int2twobytes(E[3]))
 
            -- Text Meta-events...
            -- a case for a dict, I think (pjb) ...
            elseif (event == 'text_event') then
                event_data = some_text_event(1, E[3])
            elseif (event == 'copyright_text_event') then
                event_data = some_text_event(2, E[3])
            elseif (event == 'track_name') then
                event_data = some_text_event(3, E[3])
            elseif (event == 'instrument_name') then
                event_data = some_text_event(4, E[3])
            elseif (event == 'lyric') then
                event_data = some_text_event(5, E[3])
            elseif (event == 'marker') then
                event_data = some_text_event(6, E[3])
            elseif (event == 'cue_point') then
                event_data = some_text_event(7, E[3])
            elseif (event == 'text_event_08') then
                event_data = some_text_event(8, E[3])
            elseif (event == 'text_event_09') then
                event_data = some_text_event(9, E[3])
            elseif (event == 'text_event_0a') then
                event_data = some_text_event(10, E[3])
            elseif (event == 'text_event_0b') then
                event_data = some_text_event(11, E[3])
            elseif (event == 'text_event_0c') then
                event_data = some_text_event(12, E[3])
            elseif (event == 'text_event_0d') then
                event_data = some_text_event(13, E[3])
            elseif (event == 'text_event_0e') then
                event_data = some_text_event(14, E[3])
            elseif (event == 'text_event_0f') then
                event_data = some_text_event(15, E[3])
            -- End of text meta-events
 
            elseif (event == 'end_track') then
                event_data = '\255\47\0'
            elseif (event == 'set_tempo') then
                --event_data = struct.pack(">BBwa*", 0xFF, 0x51, 3,
                --            substr( struct.pack('>I', E[0]), 1, 3))
                event_data = '\255\81\03' .. string.sub(int2fourbytes(E[3]),2)
                -- XXX don't understand that ?!
            elseif (event == 'smpte_offset') then
                event_data = '\255\84\05' ..
                    string.char(E[3],E[4],E[5],E[6],E[7])
            elseif (event == 'time_signature') then
                event_data = '\255\88\04' .. string.char(E[3],E[4],E[5],E[6])
            elseif (event == 'key_signature') then
                local e3 = E[3]; if e3<0 then e3 = 256+e3 end  -- signed byte
                event_data = '\255\89\02' .. string.char(e3,E[4])
            elseif (event == 'sequencer_specific') then
                event_data = some_text_event(127, E[3])
            -- End of Meta-events
 
            -- Other Things...
            elseif (event == 'sysex_f0') then
                event_data =
                 "\240"..ber_compressed_int(string.len(E[3]))..E[3]
            elseif (event == 'sysex_f7') then
                event_data =
                 "\247"..ber_compressed_int(string.len(E[3]))..E[3]
            elseif (event == 'song_position') then
                 event_data = "\242"..write_14_bit( E[3] )
            elseif (event == 'song_select') then
                 event_data = "\243"..string.char(E[3])
            elseif (event == 'tune_request') then
                 event_data = "\246"
            elseif (event == 'raw_data') then
                warn("_encode: raw_data event not supported")
                break
            -- End of Other Stuff
 
            -- The Big Fallthru
            else
                if not unknown_callback then
                    warn("Unknown event: "..tostring(event))
                end
                break
            end
 
            --print "Event $event encoded part 2\n"
            --if str(type(event_data)).find('str') >= 0 then
            --  event_data = bytearray(event_data.encode('Latin1', 'ignore'))
            --end
            if event_data and (#event_data > 0) then -- how could it be empty?
                -- data.append(struct.pack('>wa*', dtime, event_data))
                -- print(' event_data='+str(event_data))
                data[#data+1] = ber_compressed_int(dtime)
                data[#data+1] = event_data
            end
        end
    end
    return table.concat(data)
end
 
local function consistentise_ticks(scores) -- 3.6
    -- used by mix_scores, merge_scores, concatenate_scores
    if #scores == 1 then return deepcopy(scores) end
    local are_consistent = true
    local ticks = scores[1][1]
    for iscore = 2,#scores do
        if scores[iscore][1] ~= ticks then
            are_consistent = false
            break
        end
    end
    if are_consistent then return deepcopy(scores) end
    local new_scores = {}
    for ks,score in ipairs(scores) do
        new_scores[ks] = M.opus2score(M.to_millisecs(M.score2opus(score)))
    end
    return new_scores
end
 
-------------------------- public ------------------------------
M.All_events = readOnly{
    note_off=true, note_on=true, key_after_touch=true, control_change=true,
    patch_change=true, channel_after_touch=true, pitch_wheel_change=true,
    text_event=true, copyright_text_event=true, track_name=true,
    instrument_name=true, lyric=true, marker=true, cue_point=true,
    text_event_08=true, text_event_09=true, text_event_0a=true,
    text_event_0b=true, text_event_0c=true, text_event_0d=true,
    text_event_0e=true, text_event_0f=true,
    end_track=true, set_tempo=true, smpte_offset=true,
    time_signature=true, key_signature=true,
    sequencer_specific=true, raw_meta_event=true,
    sysex_f0=true, sysex_f7=true,
    song_position=true, song_select=true, tune_request=true,
}
-- And three dictionaries:
M.Number2patch = readOnly{   -- General MIDI patch numbers:
[0]='Acoustic Grand',
[1]='Bright Acoustic',
[2]='Electric Grand',
[3]='Honky-Tonk',
[4]='Electric Piano 1',
[5]='Electric Piano 2',
[6]='Harpsichord',
[7]='Clav',
[8]='Celesta',
[9]='Glockenspiel',
[10]='Music Box',
[11]='Vibraphone',
[12]='Marimba',
[13]='Xylophone',
[14]='Tubular Bells',
[15]='Dulcimer',
[16]='Drawbar Organ',
[17]='Percussive Organ',
[18]='Rock Organ',
[19]='Church Organ',
[20]='Reed Organ',
[21]='Accordion',
[22]='Harmonica',
[23]='Tango Accordion',
[24]='Acoustic Guitar(nylon)',
[25]='Acoustic Guitar(steel)',
[26]='Electric Guitar(jazz)',
[27]='Electric Guitar(clean)',
[28]='Electric Guitar(muted)',
[29]='Overdriven Guitar',
[30]='Distortion Guitar',
[31]='Guitar Harmonics',
[32]='Acoustic Bass',
[33]='Electric Bass(finger)',
[34]='Electric Bass(pick)',
[35]='Fretless Bass',
[36]='Slap Bass 1',
[37]='Slap Bass 2',
[38]='Synth Bass 1',
[39]='Synth Bass 2',
[40]='Violin',
[41]='Viola',
[42]='Cello',
[43]='Contrabass',
[44]='Tremolo Strings',
[45]='Pizzicato Strings',
[46]='Orchestral Harp',
[47]='Timpani',
[48]='String Ensemble 1',
[49]='String Ensemble 2',
[50]='SynthStrings 1',
[51]='SynthStrings 2',
[52]='Choir Aahs',
[53]='Voice Oohs',
[54]='Synth Voice',
[55]='Orchestra Hit',
[56]='Trumpet',
[57]='Trombone',
[58]='Tuba',
[59]='Muted Trumpet',
[60]='French Horn',
[61]='Brass Section',
[62]='SynthBrass 1',
[63]='SynthBrass 2',
[64]='Soprano Sax',
[65]='Alto Sax',
[66]='Tenor Sax',
[67]='Baritone Sax',
[68]='Oboe',
[69]='English Horn',
[70]='Bassoon',
[71]='Clarinet',
[72]='Piccolo',
[73]='Flute',
[74]='Recorder',
[75]='Pan Flute',
[76]='Blown Bottle',
[77]='Skakuhachi',
[78]='Whistle',
[79]='Ocarina',
[80]='Lead 1 (square)',
[81]='Lead 2 (sawtooth)',
[82]='Lead 3 (calliope)',
[83]='Lead 4 (chiff)',
[84]='Lead 5 (charang)',
[85]='Lead 6 (voice)',
[86]='Lead 7 (fifths)',
[87]='Lead 8 (bass+lead)',
[88]='Pad 1 (new age)',
[89]='Pad 2 (warm)',
[90]='Pad 3 (polysynth)',
[91]='Pad 4 (choir)',
[92]='Pad 5 (bowed)',
[93]='Pad 6 (metallic)',
[94]='Pad 7 (halo)',
[95]='Pad 8 (sweep)',
[96]='FX 1 (rain)',
[97]='FX 2 (soundtrack)',
[98]='FX 3 (crystal)',
[99]='FX 4 (atmosphere)',
[100]='FX 5 (brightness)',
[101]='FX 6 (goblins)',
[102]='FX 7 (echoes)',
[103]='FX 8 (sci-fi)',
[104]='Sitar',
[105]='Banjo',
[106]='Shamisen',
[107]='Koto',
[108]='Kalimba',
[109]='Bagpipe',
[110]='Fiddle',
[111]='Shanai',
[112]='Tinkle Bell',
[113]='Agogo',
[114]='Steel Drums',
[115]='Woodblock',
[116]='Taiko Drum',
[117]='Melodic Tom',
[118]='Synth Drum',
[119]='Reverse Cymbal',
[120]='Guitar Fret Noise',
[121]='Breath Noise',
[122]='Seashore',
[123]='Bird Tweet',
[124]='Telephone Ring',
[125]='Helicopter',
[126]='Applause',
[127]='Gunshot',
}
 
M.Notenum2percussion = readOnly{   -- General MIDI Percussion (on Channel 9):
[33]='Metronome Click',
[34]='Metronome Bell',
[35]='Acoustic Bass Drum',
[36]='Bass Drum 1',
[37]='Side Stick',
[38]='Acoustic Snare',
[39]='Hand Clap',
[40]='Electric Snare',
[41]='Low Floor Tom',
[42]='Closed Hi-Hat',
[43]='High Floor Tom',
[44]='Pedal Hi-Hat',
[45]='Low Tom',
[46]='Open Hi-Hat',
[47]='Low-Mid Tom',
[48]='Hi-Mid Tom',
[49]='Crash Cymbal 1',
[50]='High Tom',
[51]='Ride Cymbal 1',
[52]='Chinese Cymbal',
[53]='Ride Bell',
[54]='Tambourine',
[55]='Splash Cymbal',
[56]='Cowbell',
[57]='Crash Cymbal 2',
[58]='Vibraslap',
[59]='Ride Cymbal 2',
[60]='Hi Bongo',
[61]='Low Bongo',
[62]='Mute Hi Conga',
[63]='Open Hi Conga',
[64]='Low Conga',
[65]='High Timbale',
[66]='Low Timbale',
[67]='High Agogo',
[68]='Low Agogo',
[69]='Cabasa',
[70]='Maracas',
[71]='Short Whistle',
[72]='Long Whistle',
[73]='Short Guiro',
[74]='Long Guiro',
[75]='Claves',
[76]='Hi Wood Block',
[77]='Low Wood Block',
[78]='Mute Cuica',
[79]='Open Cuica',
[80]='Mute Triangle',
[81]='Open Triangle',
}
 
M.Event2channelindex = readOnly{ ['note']=4, ['note_off']=3, ['note_on']=3,
 ['key_after_touch']=3, ['control_change']=3, ['patch_change']=3,
 ['channel_after_touch']=3, ['pitch_wheel_change']=3,
}
 
function M.concatenate_scores(scores)
    -- the deepcopys are needed if input_scores are refs to the same table
    -- e.g. if invoked by midisox's repeat()
    local input_scores = consistentise_ticks(scores) -- 3.6
    local output_score = deepcopy(input_scores[1])   -- 4.2
    for i = 2,#input_scores do
        local input_score = input_scores[i]
        local output_stats = M.score2stats(output_score)
        local delta_ticks = output_stats['nticks']
        for itrack = 2,#input_score do
            if itrack > #output_score then -- new output track if doesn't exist
                output_score[#output_score+1] = {}
            end
            for k,event in ipairs(input_score[itrack]) do
                local new_event = copy(event)
                new_event[2] = new_event[2] + delta_ticks
                table.insert(output_score[itrack], new_event)
                -- output_score[itrack][-1][1] += delta_ticks  -- hmm...
            end
        end
    end
    return output_score
end
 
function M.grep(score, t)
    if score == nil then return {1000,{},} end
    local ticks = score[1]
    local new_score = {ticks,{},}
    if not t or type(t) ~= 'table' then return new_score end
    local channels = dict(t)
    local itrack = 2 while itrack <= #score do
        new_score[itrack] = {}
        for k,event in ipairs(score[itrack]) do
            local channel_index = M.Event2channelindex[event[1]]
            if channel_index then
                if channels[event[channel_index]] then
                    table.insert(new_score[itrack], event)
                end
            else
                table.insert(new_score[itrack], event)
            end
        end
        itrack = itrack + 1
    end
    return new_score
end
 
function M.merge_scores(scores)
    local output_score = {1000,}
    local channels_so_far = {}
    local all_channels = dict{0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}
    for ks,input_score in ipairs(consistentise_ticks(scores)) do -- 3.6
        local new_stats = M.score2stats(input_score)
        local new_channels = dict(new_stats['channels_total']) -- 4.2 dict
        new_channels[9] = nil  -- 2.8 cha9 must remain cha9 (in GM)
        for j,channel in ipairs(sorted_keys(new_channels)) do  -- 4.2 to catch 0
            if channels_so_far[channel] then
                local free_channels = copy(all_channels)
                for k,v in pairs(channels_so_far) do
                    if v then free_channels[k] = nil end
                end
                for k,v in pairs(new_channels) do
                    if v then free_channels[k] = nil end
                end
                -- consistently choose lowest avaiable, to ease testing
                local free_channel = nil
                local fcs = sorted_keys(free_channels)
                if #fcs > 0 then
                    free_channel = fcs[1]
                else
                    break
                end
                for itrack = 2,#input_score do
                    for k3,input_event in ipairs(input_score[itrack]) do
                        local ci = M.Event2channelindex[input_event[1]]
                        if ci and input_event[ci]==channel then
                            input_event[ci] = free_channel
                        end
                    end
                end
                channels_so_far[free_channel] = true
            end
            channels_so_far[channel] = true
        end
        for itrack = 2,#input_score do
            output_score[#output_score+1] = input_score[itrack]
        end
    end
    return output_score
end
 
function M.mix_opus_tracks(input_tracks) -- 5.5
    -- must convert each track to absolute times !
    local output_score = {1000, {}}
    for ks,input_track in ipairs(input_tracks) do -- 5.8
        local input_score = M.opus2score({1000, input_track})
        for k,event in ipairs(input_score[2]) do
            table.insert(output_score[2], event)
        end
    end
    table.sort(output_score[2], function (e1,e2) return e1[2]<e2[2] end) 
    local output_opus = M.score2opus(output_score)
    return output_opus[2]
end
 
function M.mix_scores(input_scores)
    local output_score = {1000, {}}
    for ks,input_score in ipairs(consistentise_ticks(input_scores)) do -- 3.6
        for itrack = 2,#input_score do
            for k,event in ipairs(input_score[itrack]) do
                table.insert(output_score[2], event)
            end
        end
    end
    return output_score
end
 
function M.midi2ms_score(midi)
    return M.opus2score(M.to_millisecs(M.midi2opus(midi)))
end
 
function M.midi2opus(s)
    if not s then s = '' end
    --my_midi=bytearray(midi)
    if #s < 4 then return {1000,{},} end
    local i = 1
    local id = string.sub(s, i, i+3); i = i+4
    if id ~= 'MThd' then
        warn("midi2opus: midi starts with "..id.." instead of 'MThd'")
        clean_up_warnings()
        return {1000,{},}
    end
    -- h:short; H:unsigned short; i:int; I:unsigned int;
    -- l:long; L:unsigned long; f:float; d:double.
    -- [length, format, tracks_expected, ticks] = struct.unpack(
    --  '>IHHH', bytes(my_midi[4:14]))  is this 10 bytes or 14 ?
    -- NOT 2+4+4+4 grrr...   'MHhd'+4+2+2+2 !
    local length          = fourbytes2int(string.sub(s,i,i+3)); i = i+4
    local format          = twobytes2int(string.sub(s,i,i+1)); i = i+2
    local tracks_expected = twobytes2int(string.sub(s,i,i+1)); i = i+2
    local ticks           = twobytes2int(string.sub(s,i,i+1)); i = i+2
    if length ~= 6 then
        warn("midi2opus: midi header length was "..tostring(length).." instead of 6")
        clean_up_warnings()
        return {1000,{},}
    end
    local my_opus = {ticks,}
    local track_num = 1   -- 5.1
    while i < #s-8 do
        local track_type   = string.sub(s, i, i+3); i = i+4
        if track_type ~= 'MTrk' then
            warn('midi2opus: Warning: track #'..track_num..' type is '..track_type.." instead of 'MTrk'")
        end
        local track_length = fourbytes2int(string.sub(s,i,i+3)); i = i+4
        if track_length > #s then
            warn('midi2opus: track #'..track_num..' length '..track_length..' is too large')
            clean_up_warnings()
            return my_opus  -- 4.9
        end
        local my_midi_track = string.sub(s, i, i+track_length-1) -- 4.7
        i = i+track_length
        local my_track = _decode(my_midi_track) -- 4.7
        my_opus[#my_opus+1] = my_track
        track_num = track_num + 1   -- 5.1
    end
    clean_up_warnings()
    return my_opus
end
 
function M.midi2score(midi)
    return M.opus2score(M.midi2opus(midi))
end
 
function M.play_score(score)
    if not score then return end
    local midi
    if M.score_type(score) == 'opus' then
        midi = M.opus2midi(score)
    else
        midi = M.score2midi(score)
    end
    local posix  -- 6.0 in lua5.2 require posix returns the posix table
    pcall(function() posix = require 'posix' end)
    if posix and posix.fork then   -- 4.2
        local pid = posix.fork()
        if pid == 0 then
            local p = assert(io.popen("aplaymidi -", 'w'))  -- background
            p:write(midi) ; p:close() ; os.exit(0)
        end
    else
        local fn = os.tmpname()
        local fh = assert(io.open(fn, 'w'));  fh:write(midi);  fh:close()
        os.execute("aplaymidi "..fn..' ; rm '..fn..' &')
    end
end
 
function M.opus2midi(opus)
    if #opus < 2 then opus = {1000, {},} end
    -- tracks = copy.deepcopy(opus)
    local ntracks = #opus - 1
    local ticks = opus[1]
    local format
    if ntracks == 1 then format = 0 else format = 1 end
    local my_midi = "MThd\00\00\00\06" ..
     int2twobytes(format) .. int2twobytes(ntracks) .. int2twobytes(ticks)
    -- struct.pack('>HHH',format,ntracks,ticks)
    --for track in tracks:
    for i = 2, #opus do
        local events = _encode(opus[i])
        -- should really do an array and then concat...
        my_midi = my_midi .. 'MTrk' .. int2fourbytes(#events) .. events
    end
    clean_up_warnings()
    return my_midi
end
 
function M.opus2score(opus)
    if opus == nil or #opus < 2 then return {1000,{},} end
    local ticks = opus[1]
    local score = {ticks,}
    local itrack = 2; while itrack <= #opus do
        local opus_track = opus[itrack]
        local ticks_so_far = 0
        local score_track = {}
        local chapitch2note_on_events = {}   -- 4.0
        local k; for k,opus_event in ipairs(opus_track) do
            ticks_so_far = ticks_so_far + opus_event[2]
            if opus_event[1] == 'note_off' or
             (opus_event[1] == 'note_on' and opus_event[5] == 0) then -- 4.8
                local cha = opus_event[3]  -- 4.0
                local pitch = opus_event[4]
                local key = cha*128 + pitch  -- 4.0
                local pending_notes = chapitch2note_on_events[key] -- 5.3
                if pending_notes and #pending_notes > 0 then
                    local new_e = table.remove(pending_notes, 1)
                    new_e[3] = ticks_so_far - new_e[2]
                    score_track[#score_track+1] = new_e
                elseif pitch > 127 then
                    warn('opus2score: note_off with no note_on, bad pitch='
                     ..tostring(pitch))
                else
                    warn('opus2score: note_off with no note_on cha='
                     ..tostring(cha)..' pitch='..tostring(pitch))
                end
            elseif opus_event[1] == 'note_on' then
                local cha = opus_event[3]  -- 4.0
                local pitch = opus_event[4]
                local new_e = {'note',ticks_so_far,0,cha,pitch,opus_event[5]}
                local key = cha*128 + pitch  -- 4.0
                if chapitch2note_on_events[key] then
                    table.insert(chapitch2note_on_events[key], new_e)
                else
                    chapitch2note_on_events[key] = {new_e,}
                end
            else
                local new_e = copy(opus_event)
                new_e[2] = ticks_so_far
                score_track[#score_track+1] = new_e
            end
        end
        -- check for unterminated notes (Oisín) -- 5.2
        for chapitch,note_on_events in pairs(chapitch2note_on_events) do
            for k,new_e in ipairs(note_on_events) do
                new_e[3] = ticks_so_far - new_e[2]
                score_track[#score_track+1] = new_e
                --warn("adding unterminated note: {'"..new_e[1].."', "..new_e[2]
                -- ..', '..new_e[3]..', '..new_e[4]..', '..new_e[5]..'}')
                warn("opus2score: note_on with no note_off cha="..new_e[4]
                 ..' pitch='..new_e[5]..'; adding note_off at end')
            end
        end
        score[#score+1] = score_track
        itrack = itrack + 1
    end
    clean_up_warnings()
    return score
end
 
function M.score2opus(score)
    if score == nil or #score < 2 then return {1000,{},} end
    local ticks = score[1]
    local opus = {ticks,}
    local itrack = 2; while itrack <= #score do
        local score_track = score[itrack]
        local time2events = {}
        local k,scoreevent; for k,scoreevent in ipairs(score_track) do
            local continue = false
            if scoreevent[1] == 'note' then
                local note_on_event = {'note_on',scoreevent[2],
                 scoreevent[4],scoreevent[5],scoreevent[6]}
                local note_off_event = {'note_off',scoreevent[2]+scoreevent[3],
                 scoreevent[4],scoreevent[5],scoreevent[6]}
                if time2events[note_on_event[2]] then
                   table.insert(time2events[note_on_event[2]], note_on_event)
                else
                   time2events[note_on_event[2]] = {note_on_event,}
                end
                if time2events[note_off_event[2]] then
                   table.insert(time2events[note_off_event[2]], note_off_event)
                else
                   time2events[note_off_event[2]] = {note_off_event,}
                end
                continue = true
            end
            if not continue then
                if time2events[scoreevent[2]] then
                    table.insert(time2events[scoreevent[2]], scoreevent)
                else
                    time2events[scoreevent[2]] = {scoreevent, }
                end
            end
        end
        local sorted_times = {}  -- list of keys
        for k,v in pairs(time2events) do
            sorted_times[#sorted_times+1] = k
        end
        table.sort(sorted_times)
        local sorted_events = {} -- once-flattened list of values sorted by key
        for k,time in ipairs(sorted_times) do
            for k2,v in ipairs(time2events[time]) do
                --sorted_events[#sorted_events+1] = v NOPE, must copy!
                sorted_events[#sorted_events+1] = {}
                for k3,v3 in ipairs(v) do
                    table.insert(sorted_events[#sorted_events],v3)
                end
            end
        end
        local abs_time = 0
        for k,event in ipairs(sorted_events) do  -- abs times => delta times
            local delta_time = event[2] - abs_time
            abs_time = event[2]
            event[2] = delta_time
        end
        opus[#opus+1] = sorted_events
        itrack = itrack + 1
    end
    clean_up_warnings()
    return opus
end
 
function M.score_type(t)
    if t == nil or type(t) ~= 'table' or #t < 2 then return '' end
    local i = 2   -- ignore first element  -- 4.7
    while i <= #t do
        local k,event; for k,event in ipairs(t[i]) do
            if event[1] == 'note' then
                return 'score'
            elseif event[1] == 'note_on' then
                return 'opus'
            end
        end
        i = i + 1
    end
    return ''
end
 
function M.score2midi(score)
    return M.opus2midi(M.score2opus(score))
end
 
function M.score2stats(opus_or_score)
--[[ returns a table:
 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)
]]
    local bank_select_msb = -1
    local bank_select_lsb = -1
    local bank_select = {}
    local channels_by_track = {}
    local channels_total    = {}
    local general_midi_mode = {}
    local num_notes_by_channel = {} -- 5.7
    local patches_used_by_track  = {}
    local patches_used_total     = {}
    local patch_changes_by_track = {}
    local patch_changes_total    = {}
    local percussion = {} -- histogram of channel 9 "pitches"
    local pitches    = {} -- histogram of pitch-occurrences channels 0-8,10-15
    local pitch_range_sum = 0   -- u pitch-ranges of each track
    local pitch_range_by_track = {}
    local is_a_score = true
    if opus_or_score == nil then
        return {bank_select={}, channels_by_track={}, channels_total={},
         general_midi_mode={}, ntracks=0, nticks=0,
         num_notes_by_channel={},
         patch_changes_by_track={}, patch_changes_total={},
         percussion={}, pitches={}, pitch_range_by_track={},
         ticks_per_quarter=0, pitch_range_sum=0
        }
    end
    local ticks_per_quarter = opus_or_score[1]
    local nticks = 0 -- 4.7
    for i = 2,#opus_or_score do  -- ignore first element, which is ticks
        local highest_pitch = 0  -- 4.7
        local lowest_pitch = 128 -- 4.7
        local channels_this_track = {}      -- 4.7
        local patch_changes_this_track = {} -- 4.7
        for k,event in ipairs(opus_or_score[i]) do
            if event[1] == 'note' then
                num_notes_by_channel[event[4]] = (num_notes_by_channel[event[4]] or 0) + 1
                if event[4] == 9 then
                    percussion[event[5]] = (percussion[event[5]] or 0) + 1
                else
                    pitches[event[5]]    = (pitches[event[5]] or 0) + 1
                    if event[5] > highest_pitch then
                        highest_pitch = event[5]
                    end
                    if event[5] < lowest_pitch then
                        lowest_pitch = event[5]
                    end
                end
                channels_this_track[event[4]] = true
                channels_total[event[4]] = true
                local finish_time = event[2] + event[3] -- 4.7
                if finish_time > nticks then
                    nticks = finish_time
                end
            elseif event[1] == 'note_on' then
                is_a_score = false   -- 4.6
                num_notes_by_channel[event[3]] = (num_notes_by_channel[event[3]] or 0) + 1
                if event[3] == 9 then
                    percussion[event[4]] = (percussion[event[4]] or 0) + 1
                else
                    pitches[event[4]]    = (pitches[event[4]] or 0) + 1
                    if event[4] > highest_pitch then
                        highest_pitch = event[4]
                    end
                    if event[4] < lowest_pitch then
                        lowest_pitch = event[4]
                    end
                end
                channels_this_track[event[3]] = true
                channels_total[event[3]] = true
            elseif event[1] == 'note_off' then
                local finish_time = event[2] -- 4.7
                if finish_time > nticks then
                    nticks = finish_time
                end
            elseif event[1] == 'patch_change' then
                patch_changes_this_track[event[3]] = event[4]
                patch_changes_total[event[4]] = true
            elseif event[1] == 'control_change' then
                if event[4] == 0 then  -- bank select MSB
                    bank_select_msb = event[5]
                elseif event[4] == 32 then  -- bank select LSB
                    bank_select_lsb = event[5]
                end
                if bank_select_msb >= 0 and bank_select_lsb >= 0 then
                    table.insert(bank_select,{bank_select_msb,bank_select_lsb})
                    bank_select_msb = -1
                    bank_select_lsb = -1
                end
            elseif event[1] == 'sysex_f0' then
                if sysex2midimode[event[3]] then
                table.insert(general_midi_mode,sysex2midimode[event[3]]) -- 5.0
                end
            end
            if is_a_score then
                if event[2] > nticks then
                    nticks = event[2]
                end
            else
                nticks = nticks + event[2]
            end
        end
        if lowest_pitch == 128 then
            lowest_pitch = 0
        end
        table.insert(channels_by_track, sorted_keys(channels_this_track))
        table.insert(patch_changes_by_track, patch_changes_this_track) -- 4.2
        table.insert(pitch_range_by_track, {lowest_pitch,highest_pitch})
        pitch_range_sum = pitch_range_sum + highest_pitch - lowest_pitch
        i = i + 1
    end
 
    return {
        bank_select=bank_select,
        channels_by_track=channels_by_track,
        channels_total=sorted_keys(channels_total),
        general_midi_mode=general_midi_mode,
        ntracks=#opus_or_score-1,
        nticks=nticks,
        num_notes_by_channel=num_notes_by_channel,
        patch_changes_by_track=patch_changes_by_track,
        patch_changes_total=sorted_keys(patch_changes_total),
        percussion=percussion,
        pitches=pitches,
        pitch_range_by_track=pitch_range_by_track,
        pitch_range_sum=pitch_range_sum,
        ticks_per_quarter=ticks_per_quarter
    }
end
 
function M.segment(...)
    local args = {...}  -- 3.8
    local score, start, endt, tracks = ...
    if #args == 1 and type(args[1][1]) == 'table' then
        score = args[1][1]
        start = args[1]['start_time'] -- 4.1
        endt = args[1]['end_time']    -- 4.1
        tracks = args[1]['tracks']
    end
    if not score == nil or type(score) ~= 'table' or #score < 2 then
        return {1000, {},}
    end
    if not start then start = 0 end -- 4.1
    if not endt  then endt  = 1000000000 end
    if not tracks then tracks = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15} end
    local new_score = {score[1],}
    local my_type = M.score_type(score)
    if my_type == '' then
        return new_score
    end
    if my_type == 'opus' then
        -- more difficult (disconnecting note_on's from their note_off's)...
        warn("segment: opus format is not supported\n")
        clean_up_warnings()
        return new_score
    end
    tracks = dict(tracks)  -- convert list to lookup
    for i = 2,#score do   -- ignore ticks; we count in ticks anyway
        if tracks[i-1] then
            local new_track = {}
            local channel2cc_num  = {} -- recentest controlchange before start
            local channel2cc_val  = {}
            local channel2cc_time = {}
            local channel2patch_num = {} -- recentest patchchange before start
            local channel2patch_time = {}
            local set_tempo_num = 500000 -- recentest tempochange 6.3
            local set_tempo_time = 0
            local earliest_note_time = endt
            for k,event in ipairs(score[i]) do
                if event[1] == 'control_change' then  -- 6.5
                    local cc_time = channel2cc_time[event[3]] or 0
                    if event[2]<=start and event[2]>=cc_time then
                        channel2cc_num[event[3]]  = event[4]
                        channel2cc_val[event[3]]  = event[5]
                        channel2cc_time[event[3]] = event[2]
                    end
                elseif event[1] == 'patch_change' then
                    local patch_time = channel2patch_time[event[3]] or 0 -- 4.7
                    if event[2]<=start and event[2]>=patch_time then  -- 2.0
                        channel2patch_num[event[3]]  = event[4]
                        channel2patch_time[event[3]] = event[2]
                    end
                elseif event[1] == 'set_tempo' then   -- 6.4 <=start not <start
                    if (event[2]<=start) and (event[2]>=set_tempo_time) then
                        set_tempo_num  = event[3]
                        set_tempo_time = event[2]
                    end
                end
                if event[2] >= start and event[2] <= endt then
                    new_track[#new_track+1]= event
                    if event[1] == 'note' and event[2]<earliest_note_time then
                        earliest_note_time = event[2]
                    end
                end
            end
            if #new_track > 0 then
                new_track[#new_track+1] = ({'set_tempo', start, set_tempo_num})
                for k,c in ipairs(sorted_keys(channel2patch_num)) do -- 4.3
                    new_track[#new_track+1] =
                     ({'patch_change', start, c, channel2patch_num[c]})
                end
                for k,c in ipairs(sorted_keys(channel2cc_num)) do -- 6.5
                    new_track[#new_track+1] = ({'control_change', start, c,
                      channel2cc_num[c],  channel2cc_val[c]})
                end
                new_score[#new_score+1] = (new_track)
            end
        end
    end
    clean_up_warnings()
    return new_score
end
 
function M.timeshift(...)
    local args = {...}  -- 3.8
    local score, shift, start_time, from_time, tracks_array = ...
    if #args == 1 and type(args[1][1]) == 'table' then
        score = args[1][1]
        shift = args[1]['shift']
        start_time = args[1]['start_time']
        from_time = args[1]['from_time']
        tracks_array = args[1]['tracks']
    end
    if score == nil or #score < 2 then return {1000, {},} end
    if from_time == nil then from_time = 0 end
    if not tracks_array then
        tracks_array = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
    end
    local new_score = {score[1],}
    local my_type = M.score_type(score)
    if my_type == '' then return new_score end
    if my_type == 'opus' then
        warn("timeshift: opus format is not supported\n")
        clean_up_warnings()
        return new_score
    end
    if shift ~= nil and start_time ~= nil then
        warn("timeshift: shift and start_time specified: ignoring shift\n")
        shift = nil
    end
    if shift == nil then
        if start_time == nil or start_time < 0 then
            start_time = 0
        end
        -- shift = start_time - from_time
    end
 
    local tracks = dict(tracks_array)  -- convert list to lookup
    local earliest = 1000000000
    if start_time ~= nil or shift < 0 then -- find the earliest event
        for i = 2,#score do   -- ignore first element (ticks)
            if tracks[i-1] then
                for k,event in ipairs(score[i]) do
                    local continue2 = false
                    if event[2] < from_time then
                        -- just inspect the to_be_shifted events
                        continue2 = true
                    end
                    if not continue2 then
                        if event[2] < earliest then
                            earliest = event[2]
                        end
                    end
                end
            end
        end
    end
    if earliest > 999999999 then
        earliest = 0
    end
    if shift == nil then
        shift = start_time - earliest
    elseif (earliest + shift) < 0 then
        start_time = 0
        shift = 0 - earliest
    end
 
    local i = 2   -- ignore first element (ticks) -- 4.7
    while i <= #score do
        if not tracks[i-1] then
            new_score[#new_score+1] = deepcopy(score[i])
            i = i + 1
        else
            local new_track = {} -- 4.7
            for k,event in ipairs(score[i]) do
                local continue = false
                local new_event = copy(event) -- 4.7
                if new_event[2] >= from_time then
                    -- 4.1 must not rightshift set_tempo
                    if new_event[1] ~= 'set_tempo' or shift<0 then
                        new_event[2] = new_event[2] + shift
                    end
                elseif (shift < 0) and (new_event[2] >= (from_time+shift)) then
                    continue = true
                end
                if not continue then
                    new_track[#new_track+1] = new_event
                end
            end
            if #new_track > 0 then
                new_score[#new_score+1] = new_track
            end
            i = i + 1
        end
    end
    clean_up_warnings()
    return new_score
end
 
function M.to_millisecs(old_opus)   -- 6.7
    if old_opus == nil then return {1000,{},}; end
    local old_tpq  = old_opus[1]
    local new_opus = {1000,}
    -- 6.7 first go through building a dict of set_tempos by absolute-tick
    local ticks2tempo = {}
    local itrack = 2
    while itrack <= #old_opus do
        local ticks_so_far = 0
        local k; for k,old_event in ipairs(old_opus[itrack]) do
            if old_event[1] == 'note' then
                warn('to_millisecs needs an opus, not a score')
                clean_up_warnings()
                return {1000,{},}
            end
            ticks_so_far = ticks_so_far + old_event[2]
            if old_event[1] == 'set_tempo' then
                ticks2tempo[ticks_so_far] = old_event[3]
            end
        end
        itrack = itrack + 1
    end
    --  then get the sorted-array of their keys
    local tempo_ticks = sorted_keys(ticks2tempo)
    --  then go through converting to millisec, testing if the next
    --  set_tempo lies before the next track-event, and using it if so.
    local itrack = 2
    while itrack <= #old_opus do
        local ms_per_old_tick = 500.0 / old_tpq -- will be rounded later 6.3
        local i_tempo_ticks = 1
        local ticks_so_far = 0
        local ms_so_far = 0.0
        local previous_ms_so_far = 0.0
        local new_track = {{'set_tempo',0,1000000},}  -- new "crochet" is 1 sec
        local k; for k,old_event in ipairs(old_opus[itrack]) do
            -- detect if ticks2tempo has something before this event
            -- If ticks2tempo is at the same time, don't handle it yet.
            local event_delta_ticks = old_event[2]
            if i_tempo_ticks <= #tempo_ticks and
              tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[2]) then
                local delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far
                ms_so_far = ms_so_far + (ms_per_old_tick * delta_ticks)
                ticks_so_far = tempo_ticks[i_tempo_ticks]
                ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq)
                i_tempo_ticks = i_tempo_ticks + 1
                event_delta_ticks = event_delta_ticks - delta_ticks
            end  -- now handle the new event
            local new_event = copy(old_event) -- 4.7
            ms_so_far = ms_so_far + (ms_per_old_tick * old_event[2])  -- NO!
            new_event[2] = math.floor(0.5 + ms_so_far - previous_ms_so_far)
            if old_event[1] ~= 'set_tempo' then -- set_tempos are already known
                previous_ms_so_far = ms_so_far
                new_track[#new_track+1] = new_event
            end
            ticks_so_far = ticks_so_far + event_delta_ticks
        end
        new_opus[#new_opus+1] = new_track
        itrack = itrack + 1
    end
    clean_up_warnings()
    return new_opus
end
 
return M
 
-- http://lua-users.org/wiki/ModuleDefinition
-- http://lua-users.org/wiki/LuaModuleFunctionCritiqued
--[=[
 
=pod
 
=head1 NAME
 
MIDI.lua - Reading, writing and manipulating MIDI data
 
=head1 SYNOPSIS
 
 local MIDI = require 'MIDI'
 
 local my_score = {
    96,  -- ticks per beat
    {    -- first track
        {'patch_change', 0, 1, 8},
        {'note', 5, 96, 1, 25, 96},
        {'note', 101, 96, 1, 29, 96},
    },  -- 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()
 
=head1 DESCRIPTION
 
This module offers functions:  concatenate_scores(), grep(),
merge_scores(), 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, 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"
 
MIDI.lua is a call-compatible translation into Lua of the Python module
http://www.pjb.com.au/midi/free/MIDI.py ;
see http://www.pjb.com.au/midi/MIDI.html
 
=head1 FUNCTIONS
 
=over 3
 
=item I<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.
 
=item I<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,}
 
=item I<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...
 
=item I<mix_opus_tracks> (tracks)
 
Mixes an array of opus tracks into one track.
A mixed track cannot be un-mixed.
It is assumed that the tracks share the same I<ticks> parameter
and the same tempo.
Mixing score-tracks is trivial (just insert all the events into one array).
Mixing opus-tracks is only slightly harder,
but it's common enough that a dedicated function is useful.
 
=item I<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.
 
 
=item I<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()
 
=item I<midi2opus> (midi_in_string_form)
 
Translates MIDI into an "opus".  For a description of the
"opus" format, see opus2midi()
 
=item I<midi2score> (midi_in_string_form)
 
Translates MIDI into a "score", using midi2opus() then opus2score()
 
 
=item I<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 -"
 
=item I<opus2score> (an_opus)
 
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)
 
=item I<play_score> (opus_or_score)
 
Converts the "score" to midi, and feeds it into 'aplaymidi -'.
If Lua's I<posix> module is installed, the aplaymidi process will
be run in the background.
 
=item I<score_type> (opus_or_score)
 
Returns a string, either 'opus' or 'score' or ''
 
=item I<score2midi> (a_score)
 
Translates a "score" into MIDI, using score2opus() then opus2midi()
 
=item I<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 in a "score"
 
 {'note', start_time, duration, channel, pitch, velocity}
 
 my_score = {
    96,
    {   -- first track
        {'patch_change', 0, 1, 8},
        {'note', 5, 96, 1, 25, 96},
        {'note', 101, 96, 1, 29, 96},
    },  -- end of first track
 }
 my_opus = score2opus(my_score)
 
=item I<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 arrays),
 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)
 
=item I<segment> (score, start_time, end_time, tracks)
 
=item I<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.
 
=item I<timeshift> (score, shift, start_time, from_time, tracks)
 
=item I<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.
 
=item I<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.
 
=back
 
=head1 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}
 
=head1 DATA TYPES
 
 channel = a value 0 to 15
 controller = 0 to 127 (see http://www.pjb.com.au/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 http://www.pjb.com.au/muscript/gm.html )
 pitch_wheel = a value -8192 to 8191 (0x1FFF)
 raw = 0 or more bytes of binary data
 sequence_number = a value 0 to 65,535 (0xFFFF)
 song_pos = a value 0 to 16,383 (0x3FFF)
 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 of ASCII text
 ticks = the number of ticks per crochet (quarter-note)
 
In I<sysex_f0> events, the I<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 I<sysex> data
into one I<sysex_f0> followed by one or more I<sysex_f7>s, then
only the last of those I<sysex_f7> events must end with the explicit \xF7
byte (again, the I<raw> data of individual I<sysex_f7> events
must not start with any \xF7 byte, since this gets added automatically).
 
=head1 PUBLIC-ACCESS TABLES
 
=over 3
 
=item I<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: http://www.pjb.com.au/muscript/gm.html#patch
 
=item I<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: http://www.pjb.com.au/muscript/gm.html#perc
 
=item I<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 I<Channel-number> occurs.
It is very useful for manipulating particular channels
within a score (see SYNOPSIS)
 
=back
 
=head1 DOWNLOAD
 
This module is available as a LuaRock in
http://luarocks.org/modules/peterbillam
so you should be able to install it with the command:
B<sudo luarocks install midi>
 
The source is in
http://www.pjb.com.au/comp/lua/MIDI.lua
for you to install by hand in your LUA_PATH
 
The test script used during development is
http://www.pjb.com.au/comp/lua/test_mi.lua
which requires the DataDumper module.
 
You should be able to install the luaposix module with:
B<sudo luarocks install luaposix>
 
=head1 AUTHOR
 
Peter J Billam, http://www.pjb.com.au/comp/contact.html
 
=head1 SEE ALSO
 
 http://www.pjb.com.au/
 http://www.pjb.com.au/comp/index.html#lua
 http://www.pjb.com.au/comp/lua/MIDI.html
 http://www.pjb.com.au/midi/MIDI.html
 http://www.pjb.com.au/muscript/gm.html
 
=cut
 
]=]

Comments