diff --git a/README.md b/README.md index 38b311b..37164d1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ passthrough is available from the maiden catalogue or by running the following c ## getting started passthrough assigns some midi routing settings for each connected midi device in the norns system menu found at `SYSTEM > DEVICES > MIDI` : +- `Active` turns on or off passthrough for this port - `Target` may be all connected devices, or individual ones. this is the destination of incoming midi data - `Input channel` selects which midi channel is listened to for incoming midi data - `Output channel` changes outgoing midi data to a specific midi channel, or leaves unchanged @@ -76,12 +77,11 @@ scripts can listen for midi events handled in passthrough and define their callb ``` -- script-level callbacks for midi event - -- data is your midi, origin lets you know where it comes from - function user_midi_event(data, origin) + -- id is the midi device id, data is your midi data + function user_midi_event(id, data) local msg = midi.to_msg(data) - if msg.type ~= 'clock' then - print(origin.port .. ' ' .. origin.name .. ' ' .. msg.type) - end + -- to find the port number, there is a helper function provided + -- port = passthrough.get_port_from_id(id) end passthrough.user_event = user_midi_event diff --git a/example_lib.lua b/example_lib.lua index 7fdd78c..22fe829 100644 --- a/example_lib.lua +++ b/example_lib.lua @@ -8,11 +8,11 @@ local utils = require 'passthrough/lib/utils' -- script-level callbacks for midi event -- data is your midi, origin lets you know where it comes from -function user_midi_event(data, origin) +function user_midi_event(id, data) local msg = midi.to_msg(data) -- do something with your data if msg.type ~= 'clock' then - utils.examples_start_screen_datum({type = msg.type, port = origin.port}) + utils.examples_start_screen_datum({type = msg.type, port = passthrough.get_port_from_id(id)}) end end diff --git a/example_mod.lua b/example_mod.lua index 313e085..6ffdc71 100644 --- a/example_mod.lua +++ b/example_mod.lua @@ -14,11 +14,11 @@ local passthrough = mod_running and require 'passthrough/lib/mod' or nil -- script-level callbacks for midi event -- data is your midi, origin lets you know where it comes from -function user_midi_event(data, origin) +function user_midi_event(id, data) local msg = midi.to_msg(data) -- do something with your data if msg.type ~= 'clock' then - utils.examples_start_screen_datum({type = msg.type, port = origin.port}) + utils.examples_start_screen_datum({type = msg.type, port = passthrough.get_port_from_id(id)}) end end @@ -38,6 +38,7 @@ function init() redraw() end screen_refresh_metro:start(1 / screen_framerate) + end end diff --git a/img/mod_menu.gif b/img/mod_menu.gif index 935d6a9..99fa45d 100644 Binary files a/img/mod_menu.gif and b/img/mod_menu.gif differ diff --git a/lib/core.lua b/lib/core.lua index d42aee6..6eef9de 100644 --- a/lib/core.lua +++ b/lib/core.lua @@ -1,18 +1,29 @@ local MusicUtil = require "musicutil" -local pt_core = {} +local pt = {} local utils = require("passthrough/lib/utils") -pt_core.midi_panic_active = false -pt_core.input_channels = {"No change"} -pt_core.output_channels = {"Device src."} -pt_core.toggles = {"no", "yes"} -pt_core.midi_ports = {} -pt_core.midi_connections = {} -pt_core.available_targets = {} -pt_core.scales = {} +pt.midi_panic_active = false +pt.input_channels = {"No change"} +pt.output_channels = {"Device src."} +pt.toggles = {"no", "yes"} +pt.scales = {} local active_notes = {} -pt_core.port_connections = {} +-- TODO: this is a mess and needs refactoring +id_port_lookup = {} -- used to lookup midi port by id +pt.port_connections = {} -- used to quickly grab table of targets for each port +pt.ports = {} -- port settings, id, name, port, connect +pt.targets = {} -- assign available targets (filters out itself) for each port + +pt.origin_event = function (id, data) end + +-- CORE NORNS OVERRIDES -- +local midi_event = _norns.midi.event + +_norns.midi.event = function(id, data) + midi_event(id, data) + pt.origin_event(id, data) -- passthrough +end -- UTIL -- local function get_midi_channel_value(channel_param_value, msg_channel) @@ -21,27 +32,29 @@ local function get_midi_channel_value(channel_param_value, msg_channel) end -- SCALE SETUP -- -pt_core.scale_names = {} +pt.scale_names = {} for i = 1, #MusicUtil.SCALES do - table.insert(pt_core.scale_names, string.lower(MusicUtil.SCALES[i].name)) + table.insert(pt.scale_names, string.lower(MusicUtil.SCALES[i].name)) end -pt_core.build_scale = function(root, scale, index) - pt_core.scales[index] = MusicUtil.generate_scale_of_length(root, scale, 128) +pt.build_scale = function(root, scale, index) + pt.scales[index] = MusicUtil.generate_scale_of_length(root, scale, 128) end -- MIDI DEVICE DETECTION -- for i = 1, 16 do - table.insert(pt_core.input_channels, i) - table.insert(pt_core.output_channels, i) + table.insert(pt.input_channels, i) + table.insert(pt.output_channels, i) end -pt_core.get_target_connections = function(origin, selection) +pt.get_port_from_id = function(id) return id_port_lookup[id] end + +pt.set_target_connections = function(origin, selection) local t = {} -- SELECT ALL PORTS if selection == 1 then - for k, v in pairs(pt_core.midi_connections) do + for k, v in pairs(pt.ports) do if v.port ~= origin then table.insert(t, v.connect) end @@ -50,9 +63,9 @@ pt_core.get_target_connections = function(origin, selection) return t else -- SINGLE PORT - still create iterable for ease - local port_target = pt_core.midi_ports[selection - 1].port + local port_target = pt.targets[origin][selection] if origin ~= port_target then - local mc = utils.table_find_value(pt_core.midi_connections, function(k, v) return v.port == port_target end) + local mc = utils.table_find_value(pt.ports, function(k, v) return v.port == port_target end) if mc then table.insert(t, mc.connect) end end end @@ -60,33 +73,47 @@ pt_core.get_target_connections = function(origin, selection) return t end -pt_core.setup_midi = function() +local create_port_targets_table = function(port) + local t = {"all"} + + for k, v in pairs(pt.ports) do + if port ~= v.port then + table.insert(t, v.port) + end + end + + return t +end + +pt.setup_midi = function() + local id_port_map = {} local midi_ports={} - local midi_connections = {} - local available_targets = {"all"} + local ports = {} + local targets = {} for _,dev in pairs(midi.devices) do if dev.port~=nil then - table.insert(midi_ports, {name=dev.name, port=dev.port}) - table.insert(midi_connections, {connect= midi.connect(dev.port), port=dev.port}) + id_port_map[dev.id] = dev.port + ports[dev.port] = {id=dev.id, name=dev.name, port=dev.port, connect=midi.connect(dev.port)} end end - table.sort(midi_connections, function(a, b) return a.port < b.port end) - table.sort(midi_ports, function(a, b) return a.port < b.port end) - - pt_core.midi_ports = midi_ports - pt_core.midi_connections = midi_connections - for i = 1, tab.count(midi_ports) do - table.insert(available_targets, i) + pt.ports = ports + + for k, v in pairs(pt.ports) do + local port_targets = create_port_targets_table(v.port) + targets[v.port] = port_targets end - pt_core.available_targets = available_targets + + id_port_lookup = id_port_map + + pt.targets = targets end -pt_core.root_note_formatter = MusicUtil.note_num_to_name +pt.root_note_formatter = MusicUtil.note_num_to_name -- EVENTS ON MENU CHANGE -- -pt_core.remove_active_note = function(target, note, ch) +pt.remove_active_note = function(target, note, ch) local i = 1 while i <= #active_notes do if active_notes[i][1] == target and active_notes[i][2] == note and active_notes[i][3] == ch then @@ -96,15 +123,15 @@ pt_core.remove_active_note = function(target, note, ch) end end -pt_core.stop_clocks = function(origin) +pt.stop_clocks = function(origin) local msg = {type="stop"} - local connections = pt_core.port_connections[origin] + local connections = pt.port_connections[origin] for k, v in pairs(connections) do - pt_core.handle_clock_data(msg, v) + pt.handle_clock_data(msg, v) end end -pt_core.stop_all_notes = function() +pt.stop_all_notes = function() if #active_notes then for i=1, #active_notes do active_notes[i][1]:note_off(active_notes[i][2], 0, active_notes[i][3]) @@ -114,7 +141,7 @@ pt_core.stop_all_notes = function() end -- DATA HANDLERS -- -pt_core.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_scale) +pt.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_scale) local note = msg.note if note ~= nil then @@ -125,7 +152,7 @@ pt_core.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_ if msg.type == "note_off" then target:note_off(note, 0, out_ch) - pt_core.remove_active_note(target, note, out_ch) + pt.remove_active_note(target, note, out_ch) elseif msg.type == "note_on" then target:note_on(note, msg.vel, out_ch) table.insert(active_notes,{target,note,out_ch}) @@ -142,7 +169,7 @@ pt_core.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_ end end -pt_core.handle_clock_data = function(msg, target) +pt.handle_clock_data = function(msg, target) if msg.type == "clock" then target:clock() elseif msg.type == "start" then @@ -154,7 +181,7 @@ pt_core.handle_clock_data = function(msg, target) end end -pt_core.device_event = function(origin, device_target, input_channel, output_channel, send_clock, quantize_midi, current_scale, data) +pt.device_event = function(origin, device_target, input_channel, output_channel, send_clock, quantize_midi, current_scale, data) if #data == 0 then print("no data") return @@ -162,28 +189,29 @@ pt_core.device_event = function(origin, device_target, input_channel, output_cha local msg = midi.to_msg(data) - local connections = pt_core.port_connections[origin] + local connections = pt.port_connections[origin] -- check this out to debug local in_chan = get_midi_channel_value(input_channel, msg.ch) local out_ch = get_midi_channel_value(output_channel, msg.ch) - if msg and msg.ch == in_chan then + --OPTIMISE THIS + if msg and msg.ch == in_chan and msg.type ~= "clock" then -- get scale stored in scales object - local scale = pt_core.scales[origin] + local scale = pt.scales[origin] for k, v in pairs(connections) do - pt_core.handle_midi_data(msg, v, out_ch, quantize_midi, scale) + pt.handle_midi_data(msg, v, out_ch, quantize_midi, scale) end end if send_clock then for k, v in pairs(connections) do - pt_core.handle_clock_data(msg, v) + pt.handle_clock_data(msg, v) end end + -- UNTIL HERE end -pt_core.user_event = function(data, origin) -end +pt.user_event = function(id, data) end -return pt_core \ No newline at end of file +return pt \ No newline at end of file diff --git a/lib/mod.lua b/lib/mod.lua index 204c8ff..12d1a52 100644 --- a/lib/mod.lua +++ b/lib/mod.lua @@ -1,5 +1,4 @@ local mod = require "core/mods" - local core = require("passthrough/lib/core") local utils = require("passthrough/lib/utils") local tab = require "tabutil" @@ -8,11 +7,10 @@ local api = {} local config = {} local state = {} --- NORNS OVERRIDES -- +-- MOD NORNS OVERRIDES -- local midi_add = _norns.midi.add local midi_remove = _norns.midi.remove -local midi_connect = _norns.midi.connect local script_clear = norns.script.clear _norns.midi.add = function(id, name, dev) @@ -22,11 +20,7 @@ end _norns.midi.remove = function(id) midi_remove(id) - launch_passthrough() -end - -_norns.midi.connect = function(id) - midi_connect(id) + update_devices() end norns.script.clear = function() @@ -42,19 +36,20 @@ function write_state() local counter = 0 for k, v in pairs(state) do counter = counter + 1 - local port_config = state[k] + if counter~=1 then io.write(",") end io.write("["..k.."] =") - io.write("{ dev_port="..port_config.dev_port..",") - io.write("target="..port_config.target..",") - io.write("input_channel="..port_config.input_channel..",") - io.write("output_channel="..port_config.output_channel..",") - io.write("send_clock="..port_config.send_clock..",") - io.write("quantize_midi="..port_config.quantize_midi..",") - io.write("current_scale="..port_config.current_scale..",") - io.write("root_note="..port_config.root_note.."}") + io.write("{ active="..v.active..",") + io.write("dev_port="..v.dev_port..",") + io.write("target="..v.target..",") + io.write("input_channel="..v.input_channel..",") + io.write("output_channel="..v.output_channel..",") + io.write("send_clock="..v.send_clock..",") + io.write("quantize_midi="..v.quantize_midi..",") + io.write("current_scale="..v.current_scale..",") + io.write("root_note="..v.root_note.."}") end io.write("}\n") io.close(f) @@ -106,15 +101,14 @@ mod.hook.register("script_pre_init", "passthrough", function() end end) - -- ACTIONS + EVENTS -- function create_config() local config={} - for k, v in pairs(core.midi_ports) do - -- if no state exists for this port, create a new one - if state[k] == nil then - print("No state saved for port, adding defaults") - state[k] = { + + for k, v in pairs(core.ports) do + if state[v.port] == nil then + state[v.port] = { + active = 1, dev_port = v.port, target = 1, input_channel = 1, @@ -125,28 +119,31 @@ function create_config() root_note = 0 } else - state[k].dev_port = v.port + state[v.port].dev_port = v.port end -- config creates an object for each passthru parameter config[k] = { + active = { + param_type = "option", + id = "active", + name = "Active", + options = core.toggles + }, target = { param_type = "option", id = "target", name = "Target", - options = core.available_targets, + options = core.targets[v.port], action = function(value) - core.midi_connections[k].connect.event = function(data) - device_event(data, v.port) - end - - core.port_connections[v.port] = core.get_target_connections(v.port, value) + core.port_connections[v.port] = core.set_target_connections(v.port, value) end, formatter = function(value) - if value == 1 then return core.available_targets[value] end - found_port = utils.table_find_value(core.midi_ports, function(key, val) return val.port == value - 1 end) - + if value == 1 then return core.targets[v.port][value] end + local target = core.targets[v.port][value] + local found_port = utils.table_find_value(core.ports, function(_,v) return target == v.port end) if found_port then return found_port.name end + return "Saved port unconnected" end }, @@ -209,32 +206,34 @@ function create_config() return config end -function device_event(data, origin) - core.device_event( - origin, - state[origin].target, - state[origin].input_channel, - state[origin].output_channel, - state[origin].send_clock, - state[origin].quantize_midi, - state[origin].current_scale, - data) - - device = core.midi_ports[origin] +function device_event(id, data) + local port = core.get_port_from_id(id) + port_config = state[port] - api.user_event(data, {name=device.name,port=device.port}) + + if port_config ~= nil and port_config.active == 2 then + core.device_event( + port, + port_config.target, + port_config.input_channel, + port_config.output_channel, + port_config.send_clock, + port_config.quantize_midi, + port_config.current_scale, + data) + + api.user_event(id, data) + end end +core.origin_event = device_event -- assign device_event to core origin + function update_devices() core.setup_midi() config = create_config() assign_state() end -function launch_passthrough() - update_devices() -end - function update_parameter(p, index, dir) -- update options if p.param_type == "option" then @@ -265,16 +264,28 @@ function format_parameter(p, index) return state[index][p.id] end +local get_menu_pagination_table = function() + local t = {} + + local counter = 1 + for k, v in pairs(config) do + t[counter] = k + counter = counter + 1 + end + + return t +end -- MOD MENU -- -local screen_order = {"target", "input_channel", "output_channel", "send_clock", "quantize_midi", "root_note", "current_scale", "midi_panic"} +local screen_order = {"active", "target", "input_channel", "output_channel", "send_clock", "quantize_midi", "root_note", "current_scale", "midi_panic"} local m = { list=screen_order, pos=0, page=1, len=tab.count(screen_order), show_hint = true, - display_panic = false + display_panic = false, + display_devices = {} } local toggle_display_panic = function() @@ -291,7 +302,7 @@ m.key = function(n, z) mod.menu.exit() end if n == 3 and z == 1 then - m.page = util.wrap(m.page + z, 1, tab.count(config)) + m.page = util.wrap(m.page + z, 1, tab.count(m.display_devices)) m.pos = 0 m.show_hint = false mod.menu.redraw() @@ -309,11 +320,12 @@ m.enc = function(n, d) end if n == 3 then + local page_port = m.display_devices[m.page] if m.list[m.pos+1] == "midi_panic" then core.stop_all_notes() toggle_display_panic() else - update_parameter(config[m.page][m.list[m.pos + 1]], m.page, d) + update_parameter(config[page_port][m.list[m.pos + 1]], page_port, d) end end mod.menu.redraw() @@ -321,6 +333,7 @@ end m.redraw = function() screen.clear() + local page_port = m.display_devices[m.page] for i=1,6 do if (i > 2 - m.pos) and (i < m.len - m.pos + 3) then screen.move(0,10*i) @@ -337,8 +350,8 @@ m.redraw = function() screen.level(m.display_panic and 15 or 4) screen.fill() else - local param = config[m.page][line] - screen.text(param.name .. " : " .. format_parameter(param, m.page)) + local param = config[page_port][line] + screen.text(param.name .. " : " .. format_parameter(param, page_port)) end end end @@ -346,25 +359,28 @@ m.redraw = function() screen.level(0) screen.fill() screen.level(15) + screen.move(0, 10) + screen.text(page_port) screen.move(120, 10) - screen.text_right(string.upper(core.midi_ports[m.page].name)) + screen.text_right(string.upper(core.ports[page_port].name)) if m.show_hint then screen.level(2) screen.move(0, 20) screen.text("E2 scroll") + screen.move(42, 20) + screen.text("E3 select") screen.move(120, 20) - screen.text_right("E3 select") - screen.move(0, 10) - screen.text("K3 port") + screen.text_right("K3 port") end screen.update() end -m.init = function() +m.init = function() m.page = 1 m.pos = 0 m.show_hint=true update_devices() + m.display_devices = get_menu_pagination_table() end m.deinit = function() @@ -378,6 +394,14 @@ api.get_state = function() return state end +api.get_connections = function() + return core.port_connections +end + +api.get_port_from_id = function(id) + return core.get_port_from_id(id) +end + api.user_event = core.user_event return api diff --git a/lib/passthrough.lua b/lib/passthrough.lua index af02b6d..2647b01 100644 --- a/lib/passthrough.lua +++ b/lib/passthrough.lua @@ -15,21 +15,27 @@ local tab = require "tabutil" local mod = require "core/mods" Passthrough.user_event = core.user_event +Passthrough.get_port_from_id = core.get_port_from_id -local function device_event(data, device) +local function device_event(id, data) + local port = core.get_port_from_id(id) + + if port ~= nil and params:get("active_"..port) == 2 then core.device_event( - device.port, - params:get("target_"..device.port), - params:get("input_channel_"..device.port), - params:get("output_channel_"..device.port), - params:get("send_clock_"..device.port)==2, - params:get("quantize_midi_"..device.port), - params:get("current_scale_"..device.port), + port, + params:get("target_"..port), + params:get("input_channel_"..port), + params:get("output_channel_"..port), + params:get("send_clock_"..port)==2, + params:get("quantize_midi_"..port), + params:get("current_scale_"..port), data) - - Passthrough.user_event(data, {name=device.name,port=device.port}) + + Passthrough.user_event(id, data) + end end + function Passthrough.init() if tab.contains(mod.loaded_mod_names(), "passthrough") then print("Passthrough already running as mod") @@ -37,42 +43,40 @@ function Passthrough.init() end core.setup_midi() + core.origin_event = device_event -- assign to core event + + port_amount = tab.count(core.ports) + params:add_group("PASSTHROUGH", 9*port_amount + 2) - port_amount = tab.count(core.midi_connections) - params:add_group("PASSTHROUGH", 8*port_amount + 2) - - for k, v in pairs(core.midi_connections) do - local name = utils.table_find_value(core.midi_ports, function(key, value) return value.port == v.port end).name - params:add_separator(name .. ' ' .. v.port) + for k, v in pairs(core.ports) do + params:add_separator(v.port .. ': ' .. v.name) + + params:add { + type="option", + id="active_" .. v.port, + name = "Active", + options = core.toggles + } params:add { type="number", id="target_" .. v.port, name = "Target", - min=1, - max = #core.available_targets, - default = 1, + min = 1, + max = tab.count(core.targets[v.port]), + default=1, action = function(value) - connector = utils.table_find_value(core.midi_connections, function(key, val) return val.port == v.port end) - connector.connect.event = nil - connector.connect.event = function(data) - device = utils.table_find_value(core.midi_ports, function(key, val) return val.port == v.port end) - if device_event then device_event(data, device) end - if not device_event then - print("no event found") - end - end - core.port_connections[v.port] = core.get_target_connections(v.port, value) + core.port_connections[v.port] = core.set_target_connections(v.port, value) end, formatter = function(param) value = param:get() - if value == 1 then - return core.available_targets[value] - else - found_port = utils.table_find_value(core.midi_ports, function(key, val) return val.port == value - 1 end) - if found_port then return found_port.name end - return "Saved port unconnected" - end + + if value == 1 then return core.targets[v.port][value] end + local target = core.targets[v.port][value] + local found_port = utils.table_find_value(core.ports, function(_,v) return target == v.port end) + if found_port then return found_port.name end + + return "Saved port unconnected" end, }