diff --git a/share/scripts/_scl_to_mts.lua b/share/scripts/_scl_to_mts.lua index beeb002541..892374bbf6 100644 --- a/share/scripts/_scl_to_mts.lua +++ b/share/scripts/_scl_to_mts.lua @@ -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