-- Usage: -- midi = midiParser( filePath ) local midiParser = function( _path ) local ret = {} ------------------ -- prepare file -- ------------------ if _path == nil then error( 'path is not defined' ) end local file = io.open( _path, 'rb' ) if file == nil then error( 'not found: ' .. _path ) end local midi = file:read( '*all' ) midi = string.gsub( midi, '\r\n', '\n' ) -------------------- -- some functions -- -------------------- local byteArray = function( _start, _length ) local retArray = {} for i = 1, _length do retArray[ i ] = string.byte( midi, i + _start - 1 ) end return retArray end local bytesToNumber = function( _start, _length ) local retNumber = 0 for i = 1, _length do retNumber = retNumber + string.byte( midi, i + _start - 1 ) * math.pow( 256, _length - i ) end return retNumber end local vlq = function( _start ) -- Variable-length quantity local retNumber = 0 local head = 0 local byte = 0 repeat byte = string.byte( midi, _start + head ) retNumber = retNumber * 128 + ( byte - math.floor( byte / 128 ) * 128 ) head = head + 1 until math.floor( byte / 128 ) ~= 1 return retNumber, head end local isSameTable = function( _a, _b ) for i, v in ipairs( _a ) do if _a[ i ] ~= _b[ i ] then return false end end return true end ------------------ -- check format -- ------------------ local head = 1 if not isSameTable( byteArray( head, 4 ), { 77, 84, 104, 100 } ) then error( 'input file seems not to be a .mid file' ) end head = head + 4 -- header chunk magic number head = head + 4 -- header chunk length ret.format = bytesToNumber( head, 2 ) if not ( ret.format == 0 or ret.format == 1 ) then error( 'not supported such format of .mid' ) end head = head + 2 -- format head = head + 2 -- trackCount ret.timebase = bytesToNumber( head, 2 ) head = head + 2 -- timeBase ------------------------ -- fight against .mid -- ------------------------ ret.tracks = {} while head < string.len( midi ) do if not isSameTable( byteArray( head, 4 ), { 77, 84, 114, 107 } ) then -- if chunk is not track chunk head = head + 4 -- unknown chunk magic number head = head + 4 + bytesToNumber( head, 4 ) -- chunk length + chunk data else head = head + 4 -- track chunk magic number local chunkLength = bytesToNumber( head, 4 ) head = head + 4 -- chunk length local chunkStart = head local track = {} track.messages = {} table.insert( ret.tracks, track ) local status = 0 while head < chunkStart + chunkLength do local deltaTime, deltaHead = vlq( head ) -- timing head = head + deltaHead local tempStatus = byteArray( head, 1 )[ 1 ] if math.floor( tempStatus / 128 ) == 1 then -- event, running status head = head + 1 status = tempStatus end local type = math.floor( status / 16 ) local channel = status - type * 16 if type == 8 then -- note off local data = byteArray( head, 2 ) head = head + 2 table.insert( track.messages, { time = deltaTime, type = 'off', channel = channel, number = data[ 1 ], velocity = data[ 2 ] } ) elseif type == 9 then -- note on local data = byteArray( head, 2 ) head = head + 2 table.insert( track.messages, { time = deltaTime, type = 'on', channel = channel, number = data[ 1 ], velocity = data[ 2 ] } ) elseif type == 10 then -- polyphonic keypressure head = head + 2 elseif type == 11 then -- control change head = head + 2 elseif type == 12 then -- program change head = head + 1 elseif type == 13 then -- channel pressure head = head + 1 elseif type == 14 then -- pitch bend head = head + 2 elseif status == 255 then -- meta event local metaType = byteArray( head, 1 )[ 1 ] head = head + 1 local metaLength, metaHead = vlq( head ) if metaType == 3 then -- track name head = head + metaHead track.name = string.sub( midi, head, head + metaLength - 1 ) head = head + metaLength table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Track Name', text = track.name } ) elseif metaType == 4 then -- instrument name head = head + metaHead track.instrument = string.sub( midi, head, head + metaLength - 1 ) head = head + metaLength table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Instrument Name', text = track.instrument } ) elseif metaType == 5 then -- lyric head = head + metaHead track.lyric = string.sub( midi, head, head + metaLength - 1 ) head = head + metaLength table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Lyric', text = track.lyric } ) elseif metaType == 47 then -- end of track head = head + 1 table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'End of Track' } ) break elseif metaType == 81 then -- tempo head = head + 1 local micros = bytesToNumber( head, 3 ) head = head + 3 table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Set Tempo', tempo = micros } ) elseif metaType == 88 then -- time signature head = head + 1 local sig = byteArray( head, 4 ) head = head + 4 table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Time Signature', signature = sig } ) elseif metaType == 89 then -- key signature head = head + 1 local sig = byteArray( head, 2 ) head = head + 2 table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Key Signature', signature = sig } ) else -- comment head = head + metaHead local text = string.sub( midi, head, head + metaLength - 1 ) head = head + metaLength table.insert( track.messages, { time = deltaTime, type = 'meta', meta = 'Unknown Text: ' .. event[ 2 ], text = text } ) end end end end end return ret end return midiParser