diff --git a/share/scripts/_scl_to_mts.lua b/share/scripts/_scl_to_mts.lua new file mode 100644 index 0000000000..f4371fcdc3 --- /dev/null +++ b/share/scripts/_scl_to_mts.lua @@ -0,0 +1,162 @@ +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