13
0
livetrax/share/scripts/_scl_to_mts.lua

163 lines
4.7 KiB
Lua
Raw Normal View History

ardour {
["type"] = "EditorAction",
name = "Scala to MIDI Tuning",
license = "MIT",
author = "Ardour Team",
description = [[Read scala (.scl) tuning from a file, generate MIDI tuning standard (MTS) messages and send them to a MIDI port]]
}
function factory () return function ()
function portlist ()
local rv = {}
local a = Session:engine()
local _, t = a:get_ports (ARDOUR.DataType("midi"), ARDOUR.PortList())
for p in t[2]:iter() do
local amp = p:to_asyncmidiport ()
if amp:isnil() or not amp:sends_output() then goto continue end
rv[amp:name()] = amp
--print (amp:name(), amp:sends_output())
::continue::
end
return rv
end
function log2 (v)
return math.log (v) / math.log (2)
end
function freq_to_mts (hz)
local note = math.floor (12. * log2 (hz / 440) + 69.0)
local freq = 440.0 * 2.0 ^ ((note - 69) / 12);
assert (freq > note)
local cent = 1200.0 * log2 (hz / freq)
return note, cent
end
function calc_freq (hz, cent, octave)
return hz * 2 ^ ((cent + 1200 * octave) / 1200)
end
local dialog_options = {
{ type = "file", key = "file", title = "Select .scl MIDI file" },
{ type = "dropdown", key = "port", title = "Target Port", values = portlist () }
}
local rv = LuaDialog.Dialog ("Select Taget", dialog_options):run ()
dialog_options = nil -- drop references (ports, shared ptr)
collectgarbage () -- and release the references immediately
if not rv then return end -- user cancelled
-- read the scl file
local freqtbl = {}
local ln = 0
local expected_len = 0
local f = io.open (rv["file"], "r")
if not f then
LuaDialog.Message ("Scala to MTS", "File Not Found", LuaDialog.MessageType.Error, LuaDialog.ButtonType.Close):run ()
goto out
end
-- http://www.huygens-fokker.org/scala/scl_format.html
freqtbl[1] = 0.0
for line in f:lines () do
line = string.gsub (line, "%s", "") -- remove all whitespace
if line:sub(0,1) == '!' then goto nextline end -- comment
ln = ln + 1
if ln < 2 then goto nextline end -- name
if ln < 3 then
expected_len = tonumber (line)
goto nextline
end
local cents
if string.find (line, ".", 1, true) then
cents = tonumber (line)
else
local n, d = string.match(line, "(%d+)/(%d+)")
if n then
cents = 1200 * log2 (n / d)
else
local n = tonumber (line)
cents = 1200 * log2 (n)
end
end
--print ("SCL", ln - 2, cents)
freqtbl[ln - 1] = cents
::nextline::
end
f:close ()
assert (expected_len + 2 == ln)
assert (expected_len > 0)
-- last entry should be an octave
assert (freqtbl[expected_len + 1] == 1200)
-- TODO consider kbm or make these configurable
-- http://www.huygens-fokker.org/scala/help.htm#mappings
local ref_root = 60 -- middle C
local ref_note = 69 -- A4
local ref_freq = 440.0
-- calc frequency at ref_root
local ref_base = ref_freq * 2.0 ^ ((ref_root - ref_note) / 12);
local async_midi_port = rv["port"] -- reference to port
local parser = ARDOUR.RawMidiParser () -- construct a MIDI parser
-- show progress dialog
local pdialog = LuaDialog.ProgressWindow ("Scala to MIDI Tuning", true)
pdialog:progress (0, "Tuning");
for nn = 0, 127 do
if pdialog:canceled () then break end
local delta = nn - ref_root
local delta_octv = math.floor (delta / expected_len)
local delta_note = delta % expected_len
local fq = calc_freq (ref_base, freqtbl [ delta_note + 1 ], delta_octv)
local base, cent = freq_to_mts (fq)
local cc = math.floor (163.83 * cent + 0.5) | 0
--print ("MIDI Note:", nn, "scale-note:", delta_note, "Octave:", delta_octv, "-> Freq:", fq, "= note:", base, "+", cent, "cent (", cc, ")")
local cent_lsb = (cc >> 7) & 127
local cent_msb = cc & 127
local syx = string.char (
0xf0, 0x7f, -- realtime sysex
0x7f, -- target-id
0x08, 0x02, -- tuning, note change request
0x00, -- tuning program number 0 to 127 in hexadecimal
0x01, -- number of notes to be changed
nn, -- note number to be changed
base, -- semitone (MIDI note number to retune to, unit is 100 cents)
cent_msb, -- MSB of fractional part (1/128 semitone = 100/128 cents = .78125 cent units)
cent_lsb, -- LSB of fractional part (1/16384 semitone = 100/16384 cents = .0061 cent units)
0xf7
)
for b = 1, 12 do
if parser:process_byte (syx:byte (b)) then
async_midi_port:write (parser:midi_buffer (), parser:buffer_size (), 0)
-- Physical MIDI is sent at 31.25kBaud.
-- Every message is sent as 10bit message on the wire,
-- so every MIDI byte needs 320usec.
ARDOUR.LuaAPI.usleep (400 * parser:buffer_size ())
end
end
pdialog:progress (nn / 127, string.format ("Note %d freq: %.2f (%d + %d)", nn, fq, base, cc))
if pdialog:canceled () then break end
end
-- hide modal progress dialog and destroy it
pdialog:done ();
::out::
end end