#!/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