Convert ZIP version to git repo

master
OldCoder 2021-07-27 22:01:20 -07:00
commit 12ca1a43de
652 changed files with 914 additions and 0 deletions

275
midi/init.lua Normal file
View File

@ -0,0 +1,275 @@
midi = {}
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
----------------------------------------
-- Registration function
----------------------------------------
midi.registered_instruments = {}
function midi.register_instrument(program_number, def)
midi.registered_instruments[program_number] = {
description = def.description,
get_sounds = def.get_sounds,
}
end
----------------------------------------
-- Helper
----------------------------------------
function midi.get_scale(note_number)
local scales = {
"C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1",
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9"
}
local scale = scales[note_number + 1] -- Note number starts at 0, but Lua table index starts at 1.
return scale or "None"
end
----------------------------------------
-- Loading function
----------------------------------------
-- Merge tracks to single track
local function merge_tracks(tracks)
local track_merged, timings = {}, {}
for _, track in ipairs(tracks) do
local time = 0 -- Current time
local program = 1 -- Instrument number
for _, message in ipairs(track.messages) do
time = time + message.time
-- Initialize notes
if not track_merged[time] then
track_merged[time] = {
notes = {},
tempo_change = nil -- For 'time_to_seconds' function
}
table.insert(timings, time)
end
if (message.type == "on") or (message.type == "off") then -- Note on/off
local note = {
type = message.type,
number = message.number,
velocity = message.velocity or 80,
program = program
}
table.insert(track_merged[time].notes, note)
elseif (message.type == "meta") and message.tempo then -- Change tempo
track_merged[time].tempo_change = message.tempo
elseif (message.type == "program_change") then -- Change program(instrument)
program = message.program
end
end
end
table.sort(timings)
return track_merged, timings
end
-- Convert track to table that has seconds as key
local function time_to_seconds(track, timings, timebase)
local track_converted = {}
local tempo = 0.5 -- Dummy
local seconds = 0
for i, time in ipairs(timings) do
local data = track[time]
-- Change tempo
if data.tempo_change then
tempo = (data.tempo_change / 1000000) -- Micro seconds to seconds
end
-- Convert time to seconds
local difftime = timings[i] - (timings[i - 1] or 0)
seconds = seconds + (difftime / timebase * tempo)
for _, note in ipairs(data.notes) do
if (note.type == "on") or (note.type == "off") then
if not track_converted[seconds] then
track_converted[seconds] = {}
end
table.insert(track_converted[seconds], note)
end
end
end
return track_converted
end
local parser = dofile(modpath .. "/lib/parser.lua")
function midi.load_midi(midi_path)
local midi_parsed = parser(midi_path)
local tracks = midi_parsed.tracks
local track_merged, timings = merge_tracks(tracks)
local track = time_to_seconds(track_merged, timings, midi_parsed.timebase)
return track
end
----------------------------------------
-- Playing function
----------------------------------------
function midi.play_midi(name, track, delay)
local function note_off(note)
local fade = [[
if handles[%d] then
for i = 1, #handles[%d] do
fade(handles[%d][i], %f, 0)
end
handles[%d] = nil
end
]]
local step = ((note.velocity ~= 0 and note.velocity or 50) / -10)
return fade:format(note.number, note.number, note.number, step, note.number)
end
local function note_on(note)
local create_handle_list = [[
if not handles[%d] then
handles[%d] = {}
end
]]
local play = [[
handles[%d][#handles[%d] + 1] = play("%s", {
gain = %f,
pitch = %f,
to_player = "%s"
})
]]
-- Get sounds
local instrument = midi.registered_instruments[note.program]
if not instrument then
return ""
end
local sounds = instrument.get_sounds(table.copy(note))
if (#sounds == 0) then
return ""
end
local func = ""
for _, sound in ipairs(sounds) do
if (sound.gain > 0) and (sound.pitch > 0) then
func = func .. " " .. play:format(note.number, note.number, sound.name, sound.gain, sound.pitch, name)
end
end
local is_func_empty = (func:gsub(" ", "") == "")
if is_func_empty then
return ""
end
return create_handle_list:format(note.number, note.number) .. func
end
local function note_release(note)
local play = [[
play("%s", {
gain = %f,
pitch = %f,
to_player = "%s"
})
]]
-- Get sounds
local instrument = midi.registered_instruments[note.program]
if not instrument then
return ""
end
local sounds = instrument.get_sounds(table.copy(note))
if (#sounds == 0) then
return ""
end
local func = ""
for _, sound in ipairs(sounds) do
if (sound.gain > 0) and (sound.pitch > 0) then
func = func .. " " .. play:format(sound.name, sound.gain, sound.pitch, name)
end
end
return func
end
local handles = {}
for seconds, notes in pairs(track) do
local function_string = ""
for _, note in ipairs(notes) do
if (note.velocity == 0) then
note.type = "off"
end
if (note.type == "on") then
function_string = function_string .. " " .. note_on(note)
elseif (note.type == "off") then
function_string = note_off(note) .. " " .. function_string
function_string = function_string .. " " .. note_release(note)
end
end
local is_func_empty = (function_string:gsub(" ", "") == "")
if not is_func_empty then
local func = [[
return function(handles, play, fade)
%s
end
]]
minetest.after((seconds + delay), loadstring(func:format(function_string))(), handles, minetest.sound_play, minetest.sound_fade)
end
end
end
----------------------------------------
-- Chatcommand
----------------------------------------
minetest.register_chatcommand("midi", {
description = "Play midi",
params = "<midiname> [delay]",
func = function(name, param)
local params = param:split(" ")
local midi_name = params[1]
if not midi_name then
return false, "midiname required"
end
local flag, ret = pcall(function()
local midi_path = (modpath .. "/midi/" .. midi_name)
return midi.load_midi(midi_path)
end)
if not flag then
return false, ret
end
local delay = tonumber(params[2]) or 1
midi.play_midi(name, ret, delay)
end
})

434
midi/lib/parser.lua Normal file
View File

@ -0,0 +1,434 @@
--[[
midiParser for Lua
MIT License
Copyright (c) 2016 Yutaka Obuchi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The original version is located at:
https://github.com/FMS-Cat/Lua_midiParser
--]]
local MThd = {77, 84, 104, 100}
local MTrk = {77, 84, 114, 107}
local function Parser(filepath)
local result = {}
------------------
-- Prepare file --
------------------
if not filepath then
error("Path is nil")
end
local midi do
local file = io.open(filepath, "rb")
if not file then
error("Not found: " .. filepath)
end
midi = file:read("*all")
midi:gsub("\r\n", "\n")
file:close()
end
--------------------
-- Some functions --
--------------------
local function byteArray(start, length)
local tbl = {}
for i = 1, length do
tbl[i] = midi:byte(i + start - 1)
end
return tbl
end
local function bytesToNumber(start, length)
local n = 0
for i = 1, length do
n = n + midi:byte(i + start - 1) * math.pow(256, length - i)
end
return n
end
-- Variable-length quantity
local function vlq(start)
local n = 0
local head = 0
local byte = 0
repeat
byte = midi:byte(start + head)
n = n * 128 + (byte - math.floor(byte / 128) * 128)
head = head + 1
until math.floor(byte / 128) ~= 1
return n, head
end
local function isSameTable(a, b)
for i, v in ipairs(a) do
if v ~= b[i] then
return false
end
end
for i, v in ipairs(b) do
if v ~= a[i] then
return false
end
end
return true
end
------------------
-- Check format --
------------------
local head = 1
do -- Check "MThd"
local MThd_LENGTH = 4
assert(isSameTable(byteArray(head, MThd_LENGTH), MThd),
"Input file is not midi")
head = head + MThd_LENGTH
end
do -- Header chunk length
local HEADER_LEN_LENGTH = 4
local header_length = bytesToNumber(head, HEADER_LEN_LENGTH)
result.header_length = header_length
head = head + HEADER_LEN_LENGTH
end
do -- Check midi format
local FORMAT_LENGTH = 2
local format = bytesToNumber(head, FORMAT_LENGTH)
result.format = format
assert((format == 0 or format == 1),
"Not supported format " .. format .. " of midi")
head = head + FORMAT_LENGTH
end
do -- Track count
local TRACK_COUNT_LENGTH = 2
local track_count = bytesToNumber(head, TRACK_COUNT_LENGTH)
result.track_count = track_count
head = head + TRACK_COUNT_LENGTH
end
do -- Timebase
local TIMEBASE_LENGTH = 2
local timebase = bytesToNumber(head, TIMEBASE_LENGTH)
result.timebase = timebase
head = head + TIMEBASE_LENGTH
end
------------------------
-- Fight against midi --
------------------------
result.tracks = {}
while (#midi > head) do
local is_MTrk = (function() -- Check MTrk
local MTrk_LENGTH = 4
local is_MTrk = isSameTable(byteArray(head, MTrk_LENGTH), MTrk)
head = head + MTrk_LENGTH
return is_MTrk
end)()
local chunk_length = (function() -- Chunk length
local CHUNK_LEN_LENGTH = 4
local chunk_length = bytesToNumber(head, CHUNK_LEN_LENGTH)
head = head + CHUNK_LEN_LENGTH
return chunk_length
end)()
if not is_MTrk then
-- Skip unknown chunk
head = head + chunk_length
else
local track = {messages = {}}
table.insert(result.tracks, track)
local status = 0
local chunk_start = head
while (chunk_start + chunk_length) > head do
local deltaTime, deltaHead = vlq(head)
head = head + deltaHead
local tempStatus = bytesToNumber(head, 1)
if math.floor(tempStatus / 128) == 1 then -- event, running status
head = head + 1
status = tempStatus
end
local event = math.floor(status / 16)
local channel = status - event * 16
if event == 8 then -- Note off
local data = byteArray(head, 2)
head = head + 2
table.insert(track.messages, {
type = "off",
time = deltaTime,
channel = channel,
number = data[1],
velocity = data[2]
})
elseif event == 9 then -- Note on
local data = byteArray(head, 2)
head = head + 2
table.insert(track.messages, {
type = "on",
time = deltaTime,
channel = channel,
number = data[1],
velocity = data[2]
})
elseif event == 10 then -- Polyphonic keypressure
local data = byteArray(head, 2)
head = head + 2
table.insert(track.messages, {
time = deltaTime
})
elseif event == 11 then -- Control change
local data = byteArray(head, 2)
head = head + 2
table.insert(track.messages, {
time = deltaTime
})
elseif event == 12 then -- Program change
local data = byteArray(head, 1)
head = head + 1
table.insert(track.messages, {
type = "program_change",
time = deltaTime,
channel = channel,
program = tonumber(data[1])
})
elseif event == 13 then -- Channel pressure
local data = byteArray(head, 1)
head = head + 1
table.insert(track.messages, {
time = deltaTime
})
elseif event == 14 then -- Pitch bend
local data = byteArray(head, 2)
head = head + 2
table.insert(track.messages, {
time = deltaTime
})
elseif status == 255 then -- Meta event
local metaType = bytesToNumber(head, 1)
head = head + 1
local metaLength, metaHead = vlq(head)
--[[if metaType == 0 then -- sequence number
elseif metaType == 1 then -- text
elseif metaType == 2 then -- licence
else]]if metaType == 3 then -- track name
head = head + metaHead
track.name = midi:sub(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 = midi:sub(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 == 6 then -- marker
--elseif metaType == 7 then -- queue point
elseif metaType == 8 then -- program name or sound name
head = head + metaHead
local v = string.sub( midi, head, head + metaLength - 1 )
head = head + metaLength
table.insert( track.messages, {
time = deltaTime,
type = "meta",
meta = "Meta8",
text = v
} )
elseif metaType == 9 then -- device name or spundfont name
head = head + metaHead
local v = string.sub( midi, head, head + metaLength - 1 )
head = head + metaLength
table.insert( track.messages, {
time = deltaTime,
type = "meta",
meta = "Meta9",
text = v
} )
--elseif metaType == 32 then -- midi channel prefix
--elseif metaType == 33 then -- select port
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 == 84 then -- SMPTE offset
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
} )
--elseif metaType == 127 then -- sequencer specific event
else -- comment
head = head + metaHead
local text = midi:sub(head, head + metaLength - 1)
head = head + metaLength
table.insert(track.messages, {
time = deltaTime,
type = "meta",
meta = "Unknown Text: ",
text = text
})
end
end
end
end
end
return result
end
return Parser

23
midi/license.txt Normal file
View File

@ -0,0 +1,23 @@
License of source code
----------------------
MIT License
Copyright (c) 2017 Rui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
midi/midi/midis_here.txt Normal file
View File

@ -0,0 +1 @@
Put your midi files in this folder.

1
midi/mod.conf Normal file
View File

@ -0,0 +1 @@
name = midi

View File

@ -0,0 +1 @@
midi

View File

@ -0,0 +1,140 @@
----------------------------------------
-- Pitches
----------------------------------------
local pitch_down, pitch_up = (0.5 ^ (1 / 12)), (2 ^ (1 / 12))
local pitches = {
Gs = {scale = "A", pitch = pitch_down},
As = {scale = "A", pitch = pitch_up},
D = {scale = "Ds", pitch = pitch_down},
E = {scale = "Ds", pitch = pitch_up},
F = {scale = "Fs", pitch = pitch_down},
G = {scale = "Fs", pitch = pitch_up},
B = {scale = "C", pitch = pitch_down},
Cs = {scale = "C", pitch = pitch_up}
}
----------------------------------------
-- Getting sound functions
----------------------------------------
local function select_velocity_number(velocity)
local function range(v, lovel, hivel)
if hivel then
return (v >= lovel) and (v <= hivel)
else
return (v >= lovel)
end
end
return (range(velocity, 1, 26) and 1)
or (range(velocity, 27, 34) and 2)
or (range(velocity, 35, 36) and 3)
or (range(velocity, 37, 43) and 4)
or (range(velocity, 44, 46) and 5)
or (range(velocity, 47, 50) and 6)
or (range(velocity, 51, 56) and 7)
or (range(velocity, 57, 64) and 8)
or (range(velocity, 65, 72) and 9)
or (range(velocity, 73, 80) and 10)
or (range(velocity, 81, 88) and 11)
or (range(velocity, 89, 96) and 12)
or (range(velocity, 97, 104) and 13)
or (range(velocity, 105, 112) and 14)
or (range(velocity, 113, 120) and 15)
or (range(velocity, 121) and 16)
or 1 -- Else
end
local function calc_gain(velocity, amp_veltrack)
local gain = 20 * math.log(velocity, 10)
return gain + (gain * (amp_veltrack / 100))
end
local function get_noteon_sounds(sounds, note)
-- Note
do
local scale_with_pitch = midi.get_scale(note.number):gsub("#", "s")
local scale, scalenumber = scale_with_pitch:match("(.+)(%d)")
local pitch = 1
local scaledef = pitches[scale]
if scaledef then
if (scale == "B") then
scalenumber = scalenumber + 1
end
scale_with_pitch = scaledef.scale .. scalenumber
pitch = scaledef.pitch
end
local amp_veltrack = 73
local sound = scale_with_pitch .. "v" .. select_velocity_number(note.velocity)
local gain = calc_gain(note.velocity, amp_veltrack)
table.insert(sounds, {name = ("midi_instrument_salamander_" .. sound), gain = gain, pitch = pitch})
end
-- HammerNoise
if minetest.settings:get_bool("midi.salamander.hammernoise") then
if (note.number >= 21) and (note.number <= 108) then
local volume = -37
local amp_veltrack = 82 * (100 / (volume + 150))
local sound = "rel" .. (note.number - 20)
local gain = calc_gain(note.velocity, amp_veltrack) / 500
table.insert(sounds, {name = ("midi_instrument_salamander_" .. sound), gain = gain, pitch = 1})
end
end
end
local function get_noteoff_sounds(sounds, note)
-- Pedal
if minetest.settings:get_bool("midi.salamander.pedal") then
-- Pedal 1
do
local volume = -20
local amp_veltrack = 100 / (volume + 150)
local sound = "pedalD" .. math.random(1, 2)
local gain = calc_gain(note.velocity, amp_veltrack)
table.insert(sounds, {name = ("midi_instrument_salamander_" .. sound), gain = gain, pitch = 1})
end
-- Pedal 2
do
local volume = -19
local amp_veltrack = 100 / (volume + 150)
local sound = "pedalU" .. math.random(1, 2)
local gain = calc_gain(note.velocity, amp_veltrack)
table.insert(sounds, {name = ("midi_instrument_salamander_" .. sound), gain = gain, pitch = 1})
end
end
end
local function get_sounds(note)
local sounds = {}
if (note.type == "on") then
get_noteon_sounds(sounds, note)
elseif (note.type == "off") then
get_noteoff_sounds(sounds, note)
end
return sounds
end
----------------------------------------
-- Register
----------------------------------------
for i = 0, 7 do -- 0 ~ 7: Piano
midi.register_instrument(i, {
description = "Salamander",
get_sounds = get_sounds
})
end

View File

@ -0,0 +1,33 @@
License of source code
----------------------
MIT License
Copyright (c) 2017 Rui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
License of sounds
-----------------
Attribution 4.0 International (CC BY 4.0)
Copyright (C) 2015 Alexander Holm
For more details:
http://creativecommons.org/licenses/by/4.0/

View File

@ -0,0 +1 @@
name = midi_instrument_salamander

View File

@ -0,0 +1,5 @@
# Enable HammerNoise Sound
midi.salamander.hammernoise (HammerNoise) bool false
# Enable Pedal Sound
midi.salamander.pedal (Pedal) bool false

Some files were not shown because too many files have changed in this diff Show More