Refine scala -> MTS script
* clean up, add comments * skip out-of-bounds notes * replace assert() with dialogs * add simple icon
This commit is contained in:
parent
dc23b9e118
commit
c93ce8d6a4
@ -8,6 +8,10 @@ ardour {
|
||||
|
||||
function factory () return function ()
|
||||
|
||||
-- return table of all MIDI tracks, and all instrument plugins.
|
||||
--
|
||||
-- MidiTrack::write_immediate_event() injects MIDI events to the track's input
|
||||
-- PluginInsert::write_immediate_event() sends events directly to a plugin
|
||||
function midi_targets ()
|
||||
local rv = {}
|
||||
for r in Session:get_tracks():iter() do
|
||||
@ -29,10 +33,7 @@ function factory () return function ()
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
return rv
|
||||
end
|
||||
|
||||
@ -40,6 +41,7 @@ function factory () return function ()
|
||||
return math.log (v) / math.log (2)
|
||||
end
|
||||
|
||||
-- calculate MIDI note-number and cent-offset for a given frequency
|
||||
function freq_to_mts (hz)
|
||||
local note = math.floor (12. * log2 (hz / 440) + 69.0)
|
||||
local freq = 440.0 * 2.0 ^ ((note - 69) / 12);
|
||||
@ -48,17 +50,18 @@ function factory () return function ()
|
||||
return note, cent
|
||||
end
|
||||
|
||||
-- apply octave and cent offset to the given frequency
|
||||
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 = "tx", title = "Target", values = midi_targets () }
|
||||
{ type = "file", key = "file", title = "Select .scl file" },
|
||||
{ type = "dropdown", key = "tx", title = "MIDI SysEx Target", values = midi_targets () }
|
||||
}
|
||||
|
||||
local rv = LuaDialog.Dialog ("Select Taget", dialog_options):run ()
|
||||
dialog_options = nil -- drop references (ports, shared ptr)
|
||||
local rv = LuaDialog.Dialog ("Select Scala File and MIDI Taget", dialog_options):run ()
|
||||
dialog_options = nil -- drop references (track, plugins, shared ptr)
|
||||
collectgarbage () -- and release the references immediately
|
||||
|
||||
if not rv then return end -- user cancelled
|
||||
@ -71,20 +74,23 @@ function factory () return function ()
|
||||
|
||||
if not f then
|
||||
LuaDialog.Message ("Scala to MTS", "File Not Found", LuaDialog.MessageType.Error, LuaDialog.ButtonType.Close):run ()
|
||||
goto out
|
||||
return
|
||||
end
|
||||
|
||||
-- parse scala file and convert all intervals to cents
|
||||
-- http://www.huygens-fokker.org/scala/scl_format.html
|
||||
freqtbl[1] = 0.0
|
||||
freqtbl[1] = 0.0 -- implicit
|
||||
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)
|
||||
expected_len = tonumber (line) -- number of notes on scale
|
||||
if expected_len < 1 or expected_len > 256 then break end -- invalid file
|
||||
goto nextline
|
||||
end
|
||||
|
||||
local cents
|
||||
if string.find (line, ".", 1, true) then
|
||||
cents = tonumber (line)
|
||||
@ -99,49 +105,73 @@ function factory () return function ()
|
||||
end
|
||||
--print ("SCL", ln - 2, cents)
|
||||
freqtbl[ln - 1] = cents
|
||||
|
||||
::nextline::
|
||||
end
|
||||
f:close ()
|
||||
|
||||
-- We need at least one interval.
|
||||
-- While legal in scl, single note scales are not useful here.
|
||||
if expected_len < 1 or expected_len + 2 ~= ln then
|
||||
LuaDialog.Message ("Scala to MTS", "Invalid or unusable scale file.", LuaDialog.MessageType.Error, LuaDialog.ButtonType.Close):run ()
|
||||
return
|
||||
end
|
||||
|
||||
-- The last entry must be an octave to map it
|
||||
if freqtbl[expected_len + 1] ~= 1200 then
|
||||
LuaDialog.Message ("Scala to MTS", "The scale does not repeat after an octave.", LuaDialog.MessageType.Error, LuaDialog.ButtonType.Close):run ()
|
||||
return
|
||||
end
|
||||
|
||||
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
|
||||
-- TODO consider reading a .kbm file or make these configurable in the dialog
|
||||
-- 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
|
||||
-- calculate frequency at ref_root
|
||||
local ref_base = ref_freq * 2.0 ^ ((ref_root - ref_note) / 12);
|
||||
|
||||
local tx = rv["tx"]
|
||||
-- prepare sending data
|
||||
local tx = rv["tx"] -- output 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");
|
||||
|
||||
-- iterate over all MIDI notes
|
||||
for nn = 0, 127 do
|
||||
if pdialog:canceled () then break end
|
||||
|
||||
-- calculate the note relative to kbm's ref_root
|
||||
local delta = nn - ref_root
|
||||
local delta_octv = math.floor (delta / expected_len)
|
||||
local delta_note = delta % expected_len
|
||||
|
||||
-- calculate the frequency of the note according to the scl
|
||||
local fq = calc_freq (ref_base, freqtbl [ delta_note + 1 ], delta_octv)
|
||||
-- and then convert this frequency to the MIDI note number (and cent offset)
|
||||
local base, cent = freq_to_mts (fq)
|
||||
|
||||
-- MTS uses two MIDI bytes (2^14) for cents
|
||||
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
|
||||
|
||||
--print ("MIDI Note:", nn, "scale-note:", delta_note, "Octave:", delta_octv, "-> Freq:", fq, "= note:", base, "+", cent, "cents (", cc, ")")
|
||||
|
||||
if (base < 0 or base > 127) then
|
||||
-- skip out of bounds MIDI notes
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- MIDI Tuning message
|
||||
-- http://www.microtonal-synthesis.com/MIDItuning.html
|
||||
local syx = string.char (
|
||||
0xf0, 0x7f, -- realtime sysex
|
||||
0x7f, -- target-id
|
||||
@ -155,9 +185,12 @@ function factory () return function ()
|
||||
0xf7
|
||||
)
|
||||
|
||||
-- parse message to C/C++ uint8_t* array (Validate message correctness. This
|
||||
-- also returns C/C++ uint8_t* array for direct use with write_immediate_event.)
|
||||
for b = 1, 12 do
|
||||
if parser:process_byte (syx:byte (b)) then
|
||||
tx:write_immediate_event (parser:buffer_size (), parser:midi_buffer ())
|
||||
-- Slow things down a bit to ensure that no messages as lost.
|
||||
-- Physical MIDI is sent at 31.25kBaud.
|
||||
-- Every message is sent as 10bit message on the wire,
|
||||
-- so every MIDI byte needs 320usec.
|
||||
@ -165,12 +198,24 @@ function factory () return function ()
|
||||
end
|
||||
end
|
||||
|
||||
-- show progress
|
||||
pdialog:progress (nn / 127, string.format ("Note %d freq: %.2f (%d + %d)", nn, fq, base, cc))
|
||||
if pdialog:canceled () then break end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- hide modal progress dialog and destroy it
|
||||
pdialog:done ();
|
||||
|
||||
::out::
|
||||
end end
|
||||
|
||||
-- simple icon
|
||||
function icon (params) return function (ctx, width, height, fg)
|
||||
ctx:set_source_rgba (ARDOUR.LuaAPI.color_to_rgba (fg))
|
||||
local txt = Cairo.PangoLayout (ctx, "ArdourMono ".. math.ceil(math.min (width, height) * .45) .. "px")
|
||||
txt:set_text ("SCL\nMTS")
|
||||
local tw, th = txt:get_pixel_size ()
|
||||
ctx:move_to (.5 * (width - tw), .5 * (height - th))
|
||||
txt:show_in_cairo_context (ctx)
|
||||
end end
|
||||
|
Loading…
Reference in New Issue
Block a user