├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── LICENSE ├── README.md ├── examples ├── 1-read-full-midi.lua ├── 2-read-single-track.lua ├── 3-dispatch-table.lua ├── 4-event-signatures.lua ├── 5-single-events.lua └── 6-timing.lua ├── lib └── midi.lua └── resources └── short-tune.mid /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.lua] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "sumneko.lua", 5 | "actboy168.lua-debug", 6 | "davidanson.vscode-markdownlint", 7 | "fcrespo82.markdown-table-formatter" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Current File", 9 | "type": "lua", 10 | "stopOnEntry": false, 11 | "request": "launch", 12 | "program": "${file}", 13 | "path": "${workspaceFolder}/examples/?.lua;${workspaceFolder}/lib/?.lua" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Possseidon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-midi 2 | 3 | A pure Lua implementation to read midi files using a callback function. 4 | 5 | ## Usage 6 | 7 | The library allows not only reading all tracks in a midi file at once, but also reading only the header (e.g. to a find out the track count) and then reading a single, specific midi track. 8 | 9 | Reading all tracks in a midi file: 10 | 11 | ```lua 12 | local midi = require "midi" 13 | 14 | local file = assert(io.open("short-tune.mid")) 15 | midi.process(file, print) 16 | 17 | file:close() 18 | ``` 19 | 20 | Reading only the last track in a midi file: 21 | 22 | ```lua 23 | local midi = require "midi" 24 | 25 | local file = assert(io.open("short-tune.mid")) 26 | local tracks = midi.processHeader(file) -- find number of tracks 27 | 28 | file:seek("set") -- seek back to the beginning of the file 29 | midi.processTrack(file, print, tracks) 30 | 31 | file:close() 32 | ``` 33 | 34 | --- 35 | 36 | ## Library 37 | 38 | The library consists of a single Lua file, namely [midi.lua](lib/midi.lua). 39 | 40 | ### Reading full midi files 41 | 42 | The following functions require a stream of a real midi file and use a callback to report individual midi events: 43 | 44 | ```lua 45 | function midi.process(stream, callback, onlyHeader, onlyTrack) 46 | function midi.processHeader(stream, callback) 47 | function midi.processTrack(stream, callback, track) 48 | ``` 49 | 50 | All functions return the total number of tracks in the midi file. 51 | 52 | | Parameter | Description | | 53 | | ------------ | ----------------------------------------------------------------------------------------------------- | ------------ | 54 | | `stream` | A stream (e.g. `file*`) that points to the start of a midi file. | **required** | 55 | | `callback` | A callback function which is invoked for all midi events. | *optional* | 56 | | `onlyHeader` | When set to `true`, only the header chunk will be processed. | *optional* | 57 | | `onlyTrack` | When set to any integer, only the header chunk and track with this one-based index will be processed. | *optional* | 58 | | `track` | Same as `onlyTrack` but required. | **required** | 59 | 60 | ### Reading single midi events 61 | 62 | The following function simply reads a single midi event (excluding the usually preceeding delta-time) from the given stream: 63 | 64 | ```lua 65 | function midi.processEvent(stream, callback, runningStatus) 66 | ``` 67 | 68 | It returns how many bytes it had to read from the stream, followed by the updated runningStatus. 69 | 70 | | Parameter | Description | | 71 | | --------------- | -------------------------------------------------------- | ------------ | 72 | | `stream` | A stream (e.g. `file*`) that points to a midi event. | **required** | 73 | | `callback` | A callback function which is invoked for the midi event. | **required** | 74 | | `runningStatus` | The running status of a previous midi event. | *optional* | 75 | 76 | --- 77 | 78 | ## Examples 79 | 80 | ### [1-read-full-midi.lua](examples/1-read-full-midi.lua) 81 | 82 | Prints all midi events in the given midi file. 83 | 84 | ### [2-read-single-track.lua](examples/2-read-single-track.lua) 85 | 86 | Prints only the midi events of a single track in the midi file. 87 | 88 | ### [3-dispatch-table.lua](examples/3-dispatch-table.lua) 89 | 90 | Handles only specific midi events using a dispatch table. 91 | 92 | ### [4-event-signatures.lua](examples/4-event-signatures.lua) 93 | 94 | Lists the signatures, on how the callback is invoked, for each midi event. 95 | 96 | ### [5-single-events.lua](examples/5-single-events.lua) 97 | 98 | Shows how to read single events from a stream. 99 | 100 | ### [6-timing.lua](examples/6-timing.lua) 101 | 102 | Calculates the total length of a midi file in seconds. 103 | 104 | Also outlines how to convert midi ticks to seconds in general. 105 | -------------------------------------------------------------------------------- /examples/1-read-full-midi.lua: -------------------------------------------------------------------------------- 1 | -- Load the library. 2 | local midi = require "midi" 3 | 4 | -- Get a stream e.g. by opening a file with io.open(). 5 | -- Make sure to open in binary mode "rb". 6 | local file = assert(io.open("resources/short-tune.mid", "rb")) 7 | 8 | -- Call midi.process() with a callback function. 9 | -- The callback is invoked for each midi event (see output below). 10 | local tracks = midi.process(file, print) 11 | 12 | print("Loaded " .. tracks .. " midi tracks!") 13 | 14 | -- Be nice and close the file (although this script ends here anyway). 15 | file:close() 16 | 17 | --[[ Output: 18 | 19 | header 1 2 480 20 | track 1 21 | sequencerOrTrackName Piano 22 | timeSignature 4 4 24 8 23 | keySignature 0 C major 24 | setTempo 150.0 25 | modeMessage 1 121 0 26 | program 1 0 27 | controller 1 7 100 28 | controller 1 10 64 29 | controller 1 91 0 30 | controller 1 93 0 31 | noteOn 1 64 0.62992125984252 32 | noteOn 1 71 0.62992125984252 33 | deltatime 227 34 | noteOn 1 64 0.0 35 | noteOn 1 71 0.0 36 | deltatime 13 37 | noteOn 1 64 0.62992125984252 38 | deltatime 227 39 | noteOn 1 64 0.0 40 | deltatime 13 41 | noteOn 1 67 0.62992125984252 42 | deltatime 227 43 | noteOn 1 67 0.0 44 | deltatime 13 45 | noteOn 1 71 0.62992125984252 46 | deltatime 227 47 | noteOn 1 71 0.0 48 | deltatime 13 49 | noteOn 1 69 0.62992125984252 50 | noteOn 1 74 0.62992125984252 51 | deltatime 227 52 | noteOn 1 69 0.0 53 | noteOn 1 74 0.0 54 | deltatime 13 55 | noteOn 1 71 0.62992125984252 56 | deltatime 227 57 | noteOn 1 71 0.0 58 | deltatime 13 59 | noteOn 1 74 0.62992125984252 60 | deltatime 227 61 | noteOn 1 74 0.0 62 | deltatime 13 63 | noteOn 1 67 0.62992125984252 64 | noteOn 1 76 0.62992125984252 65 | deltatime 227 66 | noteOn 1 67 0.0 67 | noteOn 1 76 0.0 68 | deltatime 1 69 | endOfTrack 70 | track 2 71 | sequencerOrTrackName Piano 72 | keySignature 0 C major 73 | noteOn 1 48 0.62992125984252 74 | noteOn 1 52 0.62992125984252 75 | deltatime 911 76 | noteOn 1 48 0.0 77 | noteOn 1 52 0.0 78 | deltatime 49 79 | noteOn 1 50 0.62992125984252 80 | noteOn 1 57 0.62992125984252 81 | deltatime 683 82 | noteOn 1 50 0.0 83 | noteOn 1 57 0.0 84 | deltatime 37 85 | noteOn 1 52 0.62992125984252 86 | noteOn 1 55 0.62992125984252 87 | noteOn 1 59 0.62992125984252 88 | deltatime 227 89 | noteOn 1 52 0.0 90 | noteOn 1 55 0.0 91 | noteOn 1 59 0.0 92 | deltatime 1 93 | endOfTrack 94 | Loaded 2 midi tracks! 95 | 96 | ]] 97 | -------------------------------------------------------------------------------- /examples/2-read-single-track.lua: -------------------------------------------------------------------------------- 1 | local midi = require "midi" 2 | 3 | -- It is possible to read only a single track. 4 | -- This will efficiently skip tracks using stream:seek(). 5 | 6 | local file = assert(io.open("resources/short-tune.mid", "rb")) 7 | 8 | -- First we need to find out how many tracks there are in total. 9 | -- The callback is always optional and defaults to an empty function. 10 | local tracks = midi.processHeader(file) 11 | print("Found " .. tracks .. " midi tracks!") 12 | 13 | -- Now that we know the track count, let's load just the last one. 14 | -- The file already has been read partially, so we need to seek back or reopen the file altogether. 15 | assert(file:seek("set")) 16 | midi.processTrack(file, print, tracks) 17 | 18 | file:close() 19 | 20 | --[[ Output: 21 | 22 | Found 2 midi tracks! 23 | header 1 2 480 24 | track 2 25 | sequencerOrTrackName Piano 26 | keySignature 0 C major 27 | noteOn 1 48 0.62992125984252 28 | noteOn 1 52 0.62992125984252 29 | deltatime 911 30 | noteOn 1 48 0.0 31 | noteOn 1 52 0.0 32 | deltatime 49 33 | noteOn 1 50 0.62992125984252 34 | noteOn 1 57 0.62992125984252 35 | deltatime 683 36 | noteOn 1 50 0.0 37 | noteOn 1 57 0.0 38 | deltatime 37 39 | noteOn 1 52 0.62992125984252 40 | noteOn 1 55 0.62992125984252 41 | noteOn 1 59 0.62992125984252 42 | deltatime 227 43 | noteOn 1 52 0.0 44 | noteOn 1 55 0.0 45 | noteOn 1 59 0.0 46 | deltatime 1 47 | endOfTrack 48 | 49 | ]] 50 | -------------------------------------------------------------------------------- /examples/3-dispatch-table.lua: -------------------------------------------------------------------------------- 1 | local midi = require "midi" 2 | 3 | -- Processing the different midi events can be achieved in various ways. 4 | -- However building a dispatch table is likely not a bad choice. 5 | local handlers = {} 6 | 7 | -- Listen for the start of new tracks: 8 | function handlers.track(track) 9 | print("Track " .. track) 10 | end 11 | 12 | -- Listen for played notes: 13 | function handlers.noteOn(channel, key, velocity) 14 | -- Fairly often "noteOn" with a velocity of zero is used instead of "noteOff". 15 | if velocity > 0 then 16 | print(("Note with key %i and velocity %.2f on channel %i"):format(key, velocity, channel)) 17 | end 18 | -- Sometimes even "noteOff" have a non-zero velocity. 19 | -- If you need to be super safe, use the same handler for both events. 20 | end 21 | 22 | -- Listen for timing events: 23 | function handlers.deltatime(ticks) 24 | print("Pause " .. ticks) 25 | end 26 | 27 | -- Now we can build a callback that dispatches its arguments to the correct handler: 28 | local function callback(name, ...) 29 | -- Get the correct handler... 30 | local handler = handlers[name] 31 | -- and forward the arguments to it, if it exists. 32 | if handler then 33 | handler(...) 34 | end 35 | end 36 | 37 | local file = assert(io.open("resources/short-tune.mid", "rb")) 38 | midi.process(file, callback) 39 | 40 | file:close() 41 | 42 | -- The double pauses in the output below are a result of ignoring "noteOff" events. 43 | 44 | --[[ Output: 45 | 46 | Track 1 47 | Note with key 64 and velocity 0.63 on channel 1 48 | Note with key 71 and velocity 0.63 on channel 1 49 | Pause 227 50 | Pause 13 51 | Note with key 64 and velocity 0.63 on channel 1 52 | Pause 227 53 | Pause 13 54 | Note with key 67 and velocity 0.63 on channel 1 55 | Pause 227 56 | Pause 13 57 | Note with key 71 and velocity 0.63 on channel 1 58 | Pause 227 59 | Pause 13 60 | Note with key 69 and velocity 0.63 on channel 1 61 | Note with key 74 and velocity 0.63 on channel 1 62 | Pause 227 63 | Pause 13 64 | Note with key 71 and velocity 0.63 on channel 1 65 | Pause 227 66 | Pause 13 67 | Note with key 74 and velocity 0.63 on channel 1 68 | Pause 227 69 | Pause 13 70 | Note with key 67 and velocity 0.63 on channel 1 71 | Note with key 76 and velocity 0.63 on channel 1 72 | Pause 227 73 | Pause 1 74 | Track 2 75 | Note with key 48 and velocity 0.63 on channel 1 76 | Note with key 52 and velocity 0.63 on channel 1 77 | Pause 911 78 | Pause 49 79 | Note with key 50 and velocity 0.63 on channel 1 80 | Note with key 57 and velocity 0.63 on channel 1 81 | Pause 683 82 | Pause 37 83 | Note with key 52 and velocity 0.63 on channel 1 84 | Note with key 55 and velocity 0.63 on channel 1 85 | Note with key 59 and velocity 0.63 on channel 1 86 | Pause 227 87 | Pause 1 88 | 89 | ]] 90 | -------------------------------------------------------------------------------- /examples/4-event-signatures.lua: -------------------------------------------------------------------------------- 1 | -- There are a lot of different midi events with varying parameters. 2 | -- All events with their respective function signatures can be found here. 3 | -- Further information about the Midi Specification can be found here: 4 | -- https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html 5 | 6 | local handlers = {} 7 | 8 | ---- Utility Events 9 | -- These events aren't quote on quote "Midi Events", but still useful/necessary. 10 | 11 | ---Advances the song forward in time by a certain number of ticks. 12 | ---@param ticks integer Always positive, never zero. 13 | function handlers.deltatime(ticks) end 14 | 15 | ---Provides information found in the midi header. 16 | ---@param format integer The midi format used: 0 - single track; 1 - simultaneous tracks; 2 - independent tracks 17 | ---@param tracks integer Number of tracks. Also returned from any of the midi.process() functions for convenience. 18 | ---@param division integer The number of ticks per quater note (unless the most significant, 16th bit is set, see below). 19 | ---See [2.1 - Header Chunks](https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html#BM2_1) for more information. 20 | ---The actual time per tick also depends on the current tempo. 21 | function handlers.header(format, tracks, division) end 22 | 23 | ---Signifies the start of a new midi track. 24 | ---@param track integer The index of the track, starting at one. 25 | function handlers.track(track) end 26 | 27 | ---Called when a chunk with a type that is neither `MThd` nor `MTrk` was found. 28 | ---@param chunkType string The 4-character magic bytes of the unknown chunk. 29 | ---@param data string All of the data contained in the chunk as a binary string. 30 | function handlers.unknownChunk(chunkType, data) end 31 | 32 | ---An unknown status byte has been ignored. 33 | ---@param status integer The status byte that was ignored. 34 | function handlers.ignore(status) end 35 | 36 | ---- Midi Events 37 | -- Events that operate on a single channel. 38 | 39 | -- TODO: Midi Event Signatures 40 | 41 | function handlers.noteOff(channel, key, velocity) end 42 | function handlers.noteOn(channel, key, velocity) end 43 | function handlers.keyPressure(channel, key, pressure) end 44 | function handlers.controller(channel, number, value) end 45 | function handlers.modeMessage(channel, number, value) end 46 | function handlers.program(channel, program) end 47 | function handlers.channelPressure(channel, pressure) end 48 | function handlers.pitch(channel, pitch) end 49 | 50 | ---- Meta Events 51 | -- Events containing meta info that is not specific to any channel. 52 | 53 | -- TODO: Meta Event Signatures 54 | 55 | function handlers.sequenceNumber() end 56 | function handlers.text() end 57 | function handlers.copyright() end 58 | function handlers.sequencerOrTrackName() end 59 | function handlers.instrumentName() end 60 | function handlers.lyric() end 61 | function handlers.marker() end 62 | function handlers.cuePoint() end 63 | function handlers.channelPrefix() end 64 | function handlers.endOfTrack() end 65 | function handlers.setTempo() end 66 | function handlers.smpteOffset() end 67 | function handlers.timeSignature() end 68 | function handlers.keySignature() end 69 | function handlers.sequenceEvent() end 70 | 71 | ---- SysEx Event 72 | -- Manufacturer specific events. 73 | 74 | -- TODO: SysEx Event Signature 75 | 76 | function handlers.sysexEvent() end 77 | -------------------------------------------------------------------------------- /examples/5-single-events.lua: -------------------------------------------------------------------------------- 1 | local midi = require "midi" 2 | 3 | -- Instead of reading a whole track, it is also possible to read single midi events. 4 | -- A stream is still required however, which makes this example a bit more complicated. 5 | 6 | -- We build up a dummy stream with a position and some midi data. 7 | local stream = { 8 | pos = 1, 9 | data = "\x90\x50\x7F\x80\x50\x7F" 10 | } 11 | 12 | -- And we give it a read method, to read some bytes and advance the position. 13 | function stream:read(count) 14 | local result = self.data:sub(self.pos, self.pos + count - 1) 15 | self.pos = self.pos + count 16 | return result 17 | end 18 | 19 | -- Now this dummy stream can be processed twice 20 | midi.processEvent(stream, print) --> noteOn 1 80 1.0 21 | midi.processEvent(stream, print) --> noteOff 1 80 1.0 22 | 23 | -- An optional third parameter for the running status can be provided. 24 | -- The function returns: 25 | -- 1. The total number of bytes it had to read for the event. 26 | -- 2. The updated running status. 27 | -------------------------------------------------------------------------------- /examples/6-timing.lua: -------------------------------------------------------------------------------- 1 | local midi = require "midi" 2 | 3 | local file = assert(io.open("resources/short-tune.mid", "rb")) 4 | 5 | local totalTicks = 0 6 | local totalSeconds = 0 7 | 8 | local division -- set in the midi header 9 | local tempo = 120 -- 120 BPM is the default tempo for midi files 10 | print("Initial Tempo:", tempo) 11 | 12 | local function process(event, ...) 13 | if event == "header" then 14 | local format, tracks, div = ... 15 | division = div 16 | print("Division:", division) 17 | elseif event == "deltatime" then 18 | local ticks = ... 19 | totalTicks = totalTicks + ticks 20 | totalSeconds = totalSeconds + ticks / division / tempo * 60 21 | elseif event == "setTempo" then 22 | tempo = ... 23 | print("Tempo Change:", tempo, "at " .. totalSeconds .. " seconds") 24 | end 25 | end 26 | 27 | midi.processTrack(file, process, 1) -- only process the first track 28 | file:close() 29 | 30 | -- It might make sense to process all tracks and use the maximum time 31 | -- Although I think most programs pad all tracks to that maximum anyway 32 | 33 | print("Total Ticks:", totalTicks) --> Total Ticks: 1908 34 | print("Total Seconds:", totalSeconds) --> Total Seconds: 1.59 35 | -------------------------------------------------------------------------------- /lib/midi.lua: -------------------------------------------------------------------------------- 1 | ---Reads exactly count bytes from the given stream, raising an error if it can't. 2 | ---@param stream file* The stream to read from. 3 | ---@param count integer The count of bytes to read. 4 | ---@return string data The read bytes. 5 | local function read(stream, count) 6 | local result = "" 7 | while #result ~= count do 8 | result = result .. assert(stream:read(count), "missing value") 9 | end 10 | return result 11 | end 12 | 13 | ---Reads a variable length quantity from the given stream, raising an error if it can't. 14 | ---@param stream file* The stream to read from. 15 | ---@return integer value The read value. 16 | ---@return integer length How many bytes were read in total. 17 | local function readVLQ(stream) 18 | local value = 0 19 | local length = 0 20 | repeat 21 | local byte = assert(stream:read(1), "incomplete or missing variable length quantity"):byte() 22 | value = value << 7 23 | value = value | byte & 0x7F 24 | length = length + 1 25 | until byte < 0x80 26 | return value, length 27 | end 28 | 29 | local midiEvent = { 30 | [0x80] = function(stream, callback, channel, fb) 31 | local key, velocity = ("I1I1"):unpack(fb .. stream:read(1)) 32 | callback("noteOff", channel, key, velocity / 0x7F) 33 | return 2 34 | end, 35 | [0x90] = function(stream, callback, channel, fb) 36 | local key, velocity = ("I1I1"):unpack(fb .. stream:read(1)) 37 | callback("noteOn", channel, key, velocity / 0x7F) 38 | return 2 39 | end, 40 | [0xA0] = function(stream, callback, channel, fb) 41 | local key, pressure = ("I1I1"):unpack(fb .. stream:read(1)) 42 | callback("keyPressure", channel, key, pressure / 0x7F) 43 | return 2 44 | end, 45 | [0xB0] = function(stream, callback, channel, fb) 46 | local number, value = ("I1I1"):unpack(fb .. stream:read(1)) 47 | if number < 120 then 48 | callback("controller", channel, number, value) 49 | else 50 | callback("modeMessage", channel, number, value) 51 | end 52 | return 2 53 | end, 54 | [0xC0] = function(stream, callback, channel, fb) 55 | local program = fb:byte() 56 | callback("program", channel, program) 57 | return 1 58 | end, 59 | [0xD0] = function(stream, callback, channel, fb) 60 | local pressure = fb:byte() 61 | callback("channelPressure", channel, pressure / 0x7F) 62 | return 1 63 | end, 64 | [0xE0] = function(stream, callback, channel, fb) 65 | local lsb, msb = ("I1I1"):unpack(fb .. stream:read(1)) 66 | callback("pitch", channel, (lsb | msb << 7) / 0x2000 - 1) 67 | return 2 68 | end 69 | } 70 | 71 | ---Processes a manufacturer specific SysEx event. 72 | ---@param stream file* The stream, pointing to one byte after the start of the SysEx event. 73 | ---@param callback function The feedback providing callback function. 74 | ---@param fb string The first already read byte, representing the manufacturer id. 75 | ---@return integer length The total length of the read SysEx event in bytes (including fb). 76 | local function sysexEvent(stream, callback, fb) 77 | local manufacturer = fb:byte() 78 | local data = {} 79 | repeat 80 | local char = stream:read(1) 81 | table.insert(data, char) 82 | until char:byte() == 0xF7 83 | callback("sysexEvent", data, manufacturer, table.concat(data)) 84 | return 1 + #data 85 | end 86 | 87 | ---Creates a simple function, forwarding the provided name and read data to a callback function. 88 | ---@param name string The name of the event, which is passed to the callback function. 89 | ---@return function function The function, calling the provided callback function with name and read data. 90 | local function makeForwarder(name) 91 | return function(data, callback) 92 | callback(name, data) 93 | end 94 | end 95 | 96 | local metaEvents = { 97 | [0x00] = makeForwarder("sequenceNumber"), 98 | [0x01] = makeForwarder("text"), 99 | [0x02] = makeForwarder("copyright"), 100 | [0x03] = makeForwarder("sequencerOrTrackName"), 101 | [0x04] = makeForwarder("instrumentName"), 102 | [0x05] = makeForwarder("lyric"), 103 | [0x06] = makeForwarder("marker"), 104 | [0x07] = makeForwarder("cuePoint"), 105 | [0x20] = makeForwarder("channelPrefix"), 106 | [0x2F] = makeForwarder("endOfTrack"), 107 | [0x51] = function(data, callback) 108 | local rawTempo = (">I3"):unpack(data) 109 | callback("setTempo", 6e7 / rawTempo) 110 | end, 111 | [0x54] = makeForwarder("smpteOffset"), 112 | [0x58] = function(data, callback) 113 | local numerator, denominator, metronome, dotted = (">I1I1I1I1"):unpack(data) 114 | callback("timeSignature", numerator, 1 << denominator, metronome, dotted) 115 | end, 116 | [0x59] = function(data, callback) 117 | local count, minor = (">I1I1"):unpack(data) 118 | callback("keySignature", math.abs(count), count < 0 and "flat" or count > 0 and "sharp" or "C", minor == 0 and "major" or "minor") 119 | end, 120 | [0x7F] = makeForwarder("sequenceEvent") 121 | } 122 | 123 | ---Processes a midi meta event. 124 | ---@param stream file* A stream pointing one byte after the meta event. 125 | ---@param callback function The feedback providing callback function. 126 | ---@param fb string The first already read byte, representing the meta event type. 127 | ---@return integer length The total length of the read meta event in bytes (including fb). 128 | local function metaEvent(stream, callback, fb) 129 | local event = fb:byte() 130 | local length, vlqLength = readVLQ(stream) 131 | local data = read(stream, length) 132 | local handler = metaEvents[event] 133 | if handler then 134 | handler(data, callback) 135 | end 136 | return 1 + vlqLength + length 137 | end 138 | 139 | ---Reads the four magic bytes and length of a midi chunk. 140 | ---@param stream file* A stream, pointing to the start of a midi chunk. 141 | ---@return string type The four magic bytes the chunk type (usually `MThd` or `MTrk`). 142 | ---@return integer length The length of the chunk in bytes. 143 | local function readChunkInfo(stream) 144 | local chunkInfo = stream:read(8) 145 | if not chunkInfo then 146 | return false 147 | end 148 | assert(#chunkInfo == 8, "incomplete chunk info") 149 | return (">c4I4"):unpack(chunkInfo) 150 | end 151 | 152 | ---Reads the content in a header chunk of a midi file. 153 | ---@param stream file* A stream, pointing to the data part of a header chunk. 154 | ---@param callback function The feedback providing callback function. 155 | ---@param chunkLength integer The length of the chunk in bytes. 156 | ---@return integer format The format of the midi file (0, 1 or 2). 157 | ---@return integer tracks The total number of tracks in the midi file. 158 | local function readHeader(stream, callback, chunkLength) 159 | local header = read(stream, chunkLength) 160 | assert(header and #header == 6, "incomplete or missing header") 161 | local format, tracks, division = (">I2I2I2"):unpack(header) 162 | callback("header", format, tracks, division) 163 | return format, tracks 164 | end 165 | 166 | ---Reads only a single event from the midi stream. 167 | ---@param stream file* A stream, pointing to a midi event. 168 | ---@param callback function The callback function, reporting the midi event. 169 | ---@param runningStatus? integer A running status of a previous midi event. 170 | ---@return integer length, integer runningStatus Returns both read length and the updated running status. 171 | local function processEvent(stream, callback, runningStatus) 172 | local firstByte = assert(stream:read(1), "missing event") 173 | local status = firstByte:byte() 174 | 175 | local length = 0 176 | 177 | if status < 0x80 then 178 | status = assert(runningStatus, "no running status") 179 | else 180 | firstByte = stream:read(1) 181 | length = 1 182 | runningStatus = status 183 | end 184 | 185 | 186 | if status >= 0x80 and status < 0xF0 then 187 | length = length + midiEvent[status & 0xF0](stream, callback, (status & 0x0F) + 1, firstByte) 188 | elseif status == 0xF0 then 189 | length = length + sysexEvent(stream, callback, firstByte) 190 | elseif status == 0xF2 then 191 | length = length + 2 192 | elseif status == 0xF3 then 193 | length = length + 1 194 | elseif status == 0xFF then 195 | length = length + metaEvent(stream, callback, firstByte) 196 | else 197 | callback("ignore", status) 198 | end 199 | 200 | return length, runningStatus 201 | end 202 | 203 | ---Reads the content of a track chunk of a midi file. 204 | ---@param stream file* A stream, pointing to the data part of a track chunk. 205 | ---@param callback function The feedback providing callback function. 206 | ---@param chunkLength number The length of the chunk in bytes. 207 | ---@param track integer The one-based index of the track, used in the "track" callback. 208 | local function readTrack(stream, callback, chunkLength, track) 209 | callback("track", track) 210 | 211 | local runningStatus 212 | 213 | while chunkLength > 0 do 214 | local ticks, vlqLength = readVLQ(stream) 215 | if ticks > 0 then 216 | callback("deltatime", ticks) 217 | end 218 | 219 | local readChunkLength 220 | readChunkLength, runningStatus = processEvent(stream, callback, runningStatus) 221 | chunkLength = chunkLength - readChunkLength - vlqLength 222 | end 223 | end 224 | 225 | ---Processes a midi file by calling the provided callback for midi events. 226 | ---@param stream file* A stream, pointing to the start of a midi file. 227 | ---@param callback? function The callback function, reporting the midi events. 228 | ---@param onlyHeader? boolean Wether processing should stop after the header chunk. 229 | ---@param onlyTrack? integer If specified, only this single track (one-based) will be processed. 230 | ---@return integer tracks Returns the total number of tracks in the midi file. 231 | local function process(stream, callback, onlyHeader, onlyTrack) 232 | callback = callback or function() end 233 | 234 | local format, tracks 235 | local track = 0 236 | while true do 237 | local chunkType, chunkLength = readChunkInfo(stream) 238 | 239 | if not chunkType then 240 | break 241 | end 242 | 243 | if chunkType == "MThd" then 244 | assert(not format, "only a single header chunk is allowed") 245 | format, tracks = readHeader(stream, callback, chunkLength) 246 | assert(tracks == 1 or format ~= 0, "midi format 0 can only contain a single track") 247 | assert(not onlyTrack or onlyTrack >= 1 and onlyTrack <= tracks, "track out of range") 248 | if onlyHeader then 249 | break 250 | end 251 | elseif chunkType == "MTrk" then 252 | track = track + 1 253 | 254 | assert(format, "no header chunk before the first track chunk") 255 | assert(track <= tracks, "found more tracks than specified in the header") 256 | assert(track == 1 or format ~= 0, "midi format 0 can only contain a single track") 257 | 258 | if not onlyTrack or track == onlyTrack then 259 | readTrack(stream, callback, chunkLength, track) 260 | if onlyTrack then 261 | break 262 | end 263 | else 264 | stream:seek("cur", chunkLength) 265 | end 266 | else 267 | local data = read(chunkLength) 268 | callback("unknownChunk", chunkType, data) 269 | end 270 | end 271 | 272 | if not onlyHeader and not onlyTrack then 273 | assert(track == tracks, "found less tracks than specified in the header") 274 | end 275 | 276 | return tracks 277 | end 278 | 279 | ---Processes only the header chunk. 280 | ---@param stream file* A stream, pointing to the start of a midi file. 281 | ---@param callback function The callback function, reporting the midi events. 282 | ---@return integer tracks Returns the total number of tracks in the midi file. 283 | local function processHeader(stream, callback) 284 | return process(stream, callback, true) 285 | end 286 | 287 | ---Processes only the header chunk and a single, specified track. 288 | ---@param stream file* A stream, pointing to the start of a midi file. 289 | ---@param callback function The callback function, reporting the midi events. 290 | ---@param track integer The one-based track index to read. 291 | ---@return integer tracks Returns the total number of tracks in the midi file. 292 | local function processTrack(stream, callback, track) 293 | return process(stream, callback, false, track) 294 | end 295 | 296 | return { 297 | process = process, 298 | processHeader = processHeader, 299 | processTrack = processTrack, 300 | processEvent = processEvent 301 | } 302 | -------------------------------------------------------------------------------- /resources/short-tune.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Possseidon/lua-midi/e0090fe5d78e4755ebb49875d53975bc26ddcc85/resources/short-tune.mid --------------------------------------------------------------------------------