2020-06-15 21:25:20 -04:00
|
|
|
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 ()
|
|
|
|
|
2020-06-15 22:22:57 -04:00
|
|
|
function midi_targets ()
|
2020-06-15 21:25:20 -04:00
|
|
|
local rv = {}
|
2020-06-15 22:22:57 -04:00
|
|
|
for r in Session:get_tracks():iter() do
|
|
|
|
|
|
|
|
if not r:to_track():isnil() then
|
|
|
|
local mtr = r:to_track():to_midi_track()
|
|
|
|
if not mtr:isnil() then
|
|
|
|
rv["Track: '" .. r:name() .. "'"] = mtr
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local i = 0;
|
|
|
|
while true do
|
|
|
|
local proc = r:nth_plugin (i)
|
|
|
|
if proc:isnil () then break end
|
|
|
|
local pi = proc:to_plugininsert ()
|
|
|
|
if pi:is_instrument () then
|
|
|
|
rv["Track: '" .. r:name() .. "' | Plugin: '" .. pi:name() .. "'"] = pi
|
|
|
|
end
|
|
|
|
i = i + 1
|
|
|
|
end
|
|
|
|
|
2020-06-15 21:25:20 -04:00
|
|
|
::continue::
|
|
|
|
end
|
2020-06-15 22:22:57 -04:00
|
|
|
|
2020-06-15 21:25:20 -04:00
|
|
|
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" },
|
2020-06-15 22:22:57 -04:00
|
|
|
{ type = "dropdown", key = "tx", title = "Target", values = midi_targets () }
|
2020-06-15 21:25:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2020-06-15 22:22:57 -04:00
|
|
|
local tx = rv["tx"]
|
2020-06-15 21:25:20 -04:00
|
|
|
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
|
2020-06-15 22:22:57 -04:00
|
|
|
tx:write_immediate_event (parser:buffer_size (), parser:midi_buffer ())
|
2020-06-15 21:25:20 -04:00
|
|
|
-- 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
|