#!/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: -- = + -- = -- = | | -- I know that, on the wire, can include note_on, -- note_off, and all the other 8x to Ex events, AND Fx events -- other than F0, F7, and FF -- namely, , -- , and . -- -- 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 -- ::= F2 E = {'song_position', time, read_14_bit(string.sub(trackdata,i))} trackdata = string.sub(trackdata,3) elseif first_byte == 243 then -- ::= F3 -- 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]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 =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] 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 (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 (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 (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 (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 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 (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 (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 (midi_in_string_form) Translates MIDI into an "opus". For a description of the "opus" format, see opus2midi() =item I (midi_in_string_form) Translates MIDI into a "score", using midi2opus() then opus2score() =item I (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 (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] (opus_or_score) Converts the "score" to midi, and feeds it into 'aplaymidi -'. If Lua's I module is installed, the aplaymidi process will be run in the background. =item I (opus_or_score) Returns a string, either 'opus' or 'score' or '' =item I (a_score) Translates a "score" into MIDI, using score2opus() then opus2midi() =item I (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 (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 (score, start_time, end_time, tracks) =item I {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 (score, shift, start_time, from_time, tracks) =item I {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 (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 events, the I 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 data into one I followed by one or more Is, then only the last of those I events must end with the explicit \xF7 byte (again, the I data of individual I events must not start with any \xF7 byte, since this gets added automatically). =head1 PUBLIC-ACCESS TABLES =over 3 =item I 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 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 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 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 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 =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 ]=]