Skip to content

Commit

Permalink
Send MIDI data to crow as port option (#20)
Browse files Browse the repository at this point in the history
* Send MIDI data to crow as port option

* Fixes and tidies

* Define crow output table once

* Clarity in readme

* share logic
  • Loading branch information
nattog committed Jun 12, 2022
1 parent b6402f4 commit 1a6a41b
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 38 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -20,6 +20,8 @@ passthrough is built as a mod and also as a library that can be added to individ

- by leveraging callbacks at a script level, incoming midi events can be shared between norns scripts and external hardware

- converting MIDI note data to cv/gate, and CC data to cv by sending to crow

## requirements

norns + midi devices
Expand All @@ -43,6 +45,10 @@ passthrough assigns some midi routing settings for each connected midi device in
- `Root` sets the root note of the current scale
- `Scale` sets the scale type (Major, Minor.. )
- `CC limit` sets the limit of midi CC messages to be sent for every channel per `25ms` timeframe. if more messages than this limit are received, then the last messages (per channel) will be sent automatically on next timeframe. this is useful when a midi controller is generating too many messages too fast (eg. moving all the faders at once on a novation launchcontrol xl). the `Pass all` option allows all CC messages to passthrough, without any kind of limiting. the `Pass none` option doesn't allow any midi CC messages to passthrough, effectively removing all of them
- `Crow note output` allows note and gate data to be sent to Monome Crow output pairs `1+2` or `3+4`.
- `Crow cc output` allows two streams of control change data to be sent to Monome Crow output pairs 1+2 or 3+4
- `Crow cc out a` sets the MIDI control change number to assign to the first of the assigned pair of `Crow cc output`
- `Crow cc out b` sets the MIDI control change number to assign to the second of the assigned pair of `Crow cc output`

additionally, `Midi panic` is a toggle to stop all active notes if some notes are hanging.

Expand Down
82 changes: 73 additions & 9 deletions lib/core.lua
Expand Up @@ -8,6 +8,12 @@ pt.output_channels = {"Device src."}
pt.toggles = {"no", "yes"}
pt.scales = {}
pt.cc_limits = {"Pass all", "Pass none"}
local crow_output_options = {"Off", "1+2", "3+4"}
pt.crow_notes = crow_output_options
pt.crow_cc_outputs = crow_output_options

local current_crow_note = nil

for i = 1, 10 do table.insert(pt.cc_limits, i) end
local active_notes = {}
local cc_limit_count = {}
Expand Down Expand Up @@ -179,16 +185,38 @@ pt.stop_all_notes = function()
active_notes= {}
end

-- DATA HANDLERS --
pt.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_scale, cc_limit)
local note = msg.note
-- CROW DATA
pt.crow_note_on_data = function(note_num, note_channel, gate_channel)
current_crow_note = note_num
crow.output[note_channel].volts = utils.n2v(note_num)
crow.output[gate_channel].action = "{to(5,0)}"
crow.output[gate_channel].execute()
end

pt.crow_note_off_data = function(note_num, gate_channel)
if current_crow_note == nil or current_crow_note == note_num then
crow.output[gate_channel].action = "{to(0,0)}"
crow.output[gate_channel].execute()
current_crow_note = nil
end
end

pt.crow_cc_data = function(msg, channel)
crow.output[channel].volts = utils.cc_cv(msg.val)
end

pt.quantize_note_data = function(note, current_scale)
if note ~= nil then
if quantize_midi == 2 then
note = MusicUtil.snap_note_to_array(note, current_scale)
end
note = MusicUtil.snap_note_to_array(note, current_scale)
end

return note
end

-- DATA HANDLERS --
pt.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_scale, cc_limit)
local note = (quantize_midi == 2 and msg.note ~= nil) and pt.quantize_note_data(msg.note, current_scale) or msg.note

if msg.type == "note_off" then
target:note_off(note, 0, out_ch)
pt.remove_active_note(target, note, out_ch)
Expand All @@ -208,6 +236,36 @@ pt.handle_midi_data = function(msg, target, out_ch, quantize_midi, current_scale
end
end

pt.process_data_for_crow = function(msg, crow_notes, crow_cc_outputs, crow_cc_selection_a, crow_cc_selection_b, quantize_midi, current_scale)
local note = (quantize_midi == 2 and msg.note ~= nil) and pt.quantize_note_data(msg.note, current_scale) or msg.note

if (crow_notes > 1) then
local is_first_output_pair = crow_notes == 2
if msg.type == "note_on" then
local note_channel = is_first_output_pair and 1 or 3
pt.crow_note_on_data(note, note_channel, note_channel+1)
elseif msg.type == "note_off" then
pt.crow_note_off_data(note, is_first_output_pair and 2 or 4)
end
end
if msg.type == "cc" then
if (crow_cc_outputs > 1) then
local is_selection_a = msg.cc == crow_cc_selection_a
local is_selection_b = msg.cc == crow_cc_selection_b

if is_selection_a or is_selection_b then
local crow_output = crow_cc_outputs == 2 and 1 or 3
if is_selection_a then
pt.crow_cc_data(msg, crow_output)
end
if is_selection_b then
pt.crow_cc_data(msg, crow_output+1)
end
end
end
end
end

pt.handle_clock_data = function(msg, target)
if msg.type == "clock" then
target:clock()
Expand All @@ -233,7 +291,7 @@ pt.handle_cc_limit = function()
cc_limit_init = {}
end

pt.device_event = function(origin, device_target, input_channel, output_channel, send_clock, quantize_midi, current_scale, cc_limit, data)
pt.device_event = function(origin, device_target, input_channel, output_channel, send_clock, quantize_midi, current_scale, cc_limit, crow_notes, crow_cc_outputs, crow_cc_selection_a, crow_cc_selection_b, data)
if #data == 0 then
print("no data")
return
Expand All @@ -245,11 +303,12 @@ pt.device_event = function(origin, device_target, input_channel, output_channel,

local in_chan = get_midi_channel_value(input_channel, msg.ch)
local out_ch = get_midi_channel_value(output_channel, msg.ch)
-- get scale stored in scales object
local scale = pt.scales[origin]

--OPTIMISE THIS
if msg and msg.ch == in_chan and msg.type ~= "clock" then
-- get scale stored in scales object
local scale = pt.scales[origin]


for k, v in pairs(connections) do
pt.handle_midi_data(msg, v, out_ch, quantize_midi, scale, cc_limit)
Expand All @@ -262,6 +321,11 @@ pt.device_event = function(origin, device_target, input_channel, output_channel,
end
end
-- UNTIL HERE

if crow_notes > 1 or crow_cc_outputs > 1 then
pt.process_data_for_crow(msg, crow_notes, crow_cc_outputs, crow_cc_selection_a, crow_cc_selection_b, quantize_midi, scale)
end

end

pt.user_event = function(id, data) end
Expand Down
79 changes: 62 additions & 17 deletions lib/mod.lua
Expand Up @@ -6,6 +6,21 @@ local tab = require "tabutil"
local api = {}
local config = {}
local state = {}
local default_port_state = {
active = 1,
target = 1,
input_channel = 1,
output_channel = 1,
send_clock = 1,
quantize_midi = 1,
current_scale = 1,
root_note = 0,
cc_limit = 1,
crow_notes = 1,
crow_cc_outputs = 1,
crow_cc_selection_a = 1,
crow_cc_selection_b = 1,
}

-- MOD NORNS OVERRIDES --

Expand Down Expand Up @@ -50,7 +65,11 @@ function write_state()
io.write("quantize_midi="..v.quantize_midi..",")
io.write("current_scale="..v.current_scale..",")
io.write("root_note="..v.root_note..",")
io.write("cc_limit="..v.cc_limit.."}")
io.write("cc_limit="..v.cc_limit..",")
io.write("crow_notes="..v.crow_notes..",")
io.write("crow_cc_outputs="..v.crow_cc_outputs..",")
io.write("crow_cc_selection_a="..v.crow_cc_selection_a..",")
io.write("crow_cc_selection_b="..v.crow_cc_selection_b.."}")
end
io.write("}\n")
io.close(f)
Expand Down Expand Up @@ -112,22 +131,18 @@ function create_config()

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,
output_channel = 1,
send_clock = 1,
quantize_midi = 1,
current_scale = 1,
root_note = 0,
cc_limit = 1
}
state[v.port] = default_port_state
else
state[v.port].dev_port = v.port
-- ensure that the state is up to date with changing api keys
for key, value in pairs(default_port_state) do
if state[v.port][key] == nil then
state[v.port][key] = value
end
end
end

state[v.port].dev_port = v.port

-- config creates an object for each passthru parameter
config[k] = {
active = {
Expand Down Expand Up @@ -201,13 +216,39 @@ function create_config()
action = function()
core.build_scale(state[k].root_note, state[k].current_scale, k)
end
},
},
cc_limit = {
param_type = "option",
id = "cc_limit",
name = "CC limit",
options = core.cc_limits
}
},
crow_notes = {
param_type = "option",
id = "crow_notes",
name = "Crow note output",
options = core.crow_notes
},
crow_cc_outputs = {
param_type = "option",
id = "crow_cc_outputs",
name = "Crow cc output",
options = core.crow_cc_outputs
},
crow_cc_selection_a = {
param_type = "number",
id = "crow_cc_selection_a",
name = "Crow cc out a",
minimum = 1,
maximum = 128,
},
crow_cc_selection_b = {
param_type = "number",
id = "crow_cc_selection_b",
name = "Crow cc out b",
minimum = 1,
maximum = 128,
},
}

config[k].target.action(state[k].target)
Expand All @@ -233,6 +274,10 @@ function device_event(id, data)
port_config.quantize_midi,
port_config.current_scale,
port_config.cc_limit,
port_config.crow_notes,
port_config.crow_cc_outputs,
port_config.crow_cc_selection_a,
port_config.crow_cc_selection_b,
data)

api.user_event(id, data)
Expand Down Expand Up @@ -290,7 +335,7 @@ local get_menu_pagination_table = function()
end

-- MOD MENU --
local screen_order = {"active", "target", "input_channel", "output_channel", "send_clock", "quantize_midi", "root_note", "current_scale", "cc_limit", "midi_panic"}
local screen_order = {"active", "target", "input_channel", "output_channel", "send_clock", "quantize_midi", "root_note", "current_scale", "cc_limit", "crow_notes", "crow_cc_outputs", "crow_cc_selection_a", "crow_cc_selection_b", "midi_panic"}
local m = {
list=screen_order,
pos=0,
Expand Down
72 changes: 61 additions & 11 deletions lib/passthrough.lua
Expand Up @@ -14,6 +14,23 @@ local utils = require("passthrough/lib/utils")
local tab = require "tabutil"
local mod = require "core/mods"

local port_param_items = {
"separator",
"active",
"target",
"input_channel",
"output_channel",
"send_clock",
"quantize_midi",
"root_note",
"current_scale",
"cc_limit",
"crow_notes",
"crow_cc_outputs",
"crow_cc_selection_a",
"crow_cc_selection_b",
}

Passthrough.user_event = core.user_event
Passthrough.get_port_from_id = core.get_port_from_id

Expand All @@ -30,6 +47,10 @@ local function device_event(id, data)
params:get("quantize_midi_"..port),
params:get("current_scale_"..port),
params:get("cc_limit_"..port),
params:get("crow_notes_"..port),
params:get("crow_cc_outputs_"..port),
params:get("crow_cc_selection_a_"..port),
params:get("crow_cc_selection_b_"..port),
data)

Passthrough.user_event(id, data)
Expand All @@ -49,7 +70,7 @@ function Passthrough.init()
if core.has_devices == true then

port_amount = tab.count(core.ports)
params:add_group("PASSTHROUGH", 10*port_amount + 2)
params:add_group("PASSTHROUGH", #port_param_items*port_amount + 2)

for k, v in pairs(core.ports) do
params:add_separator(v.port .. ': ' .. v.name)
Expand Down Expand Up @@ -117,8 +138,8 @@ function Passthrough.init()
type = "number",
id = "root_note_"..v.port,
name = "Root",
minimum = 0,
maximum = 11,
min = 0,
max = 11,
formatter = function(param)
return core.root_note_formatter(param:get())
end,
Expand All @@ -141,16 +162,45 @@ function Passthrough.init()
name = "CC limit",
options = core.cc_limits
}
end
params:add_separator("All devices")
params:add {
type = "trigger",
id = "midi_panic",
name = "Midi panic",
action = function()
core.stop_all_notes()
end
type = "option",
id = "crow_notes_"..v.port,
name = "Crow note output",
options = core.crow_notes
}
params:add {
type = "option",
id = "crow_cc_outputs_"..v.port,
name = "Crow cc output",
options = core.crow_cc_outputs
}
params:add {
type = "number",
id = "crow_cc_selection_a_"..v.port,
name = "Crow cc out a",
min = 1,
max = 128,
default = 1
}
params:add {
type = "number",
id = "crow_cc_selection_b_"..v.port,
name = "Crow cc out b",
min = 1,
max = 128,
default = 1
}
end
params:add_separator("All devices")
params:add {
type = "trigger",
id = "midi_panic",
name = "Midi panic",
action = function()
core.stop_all_notes()
end
}

end
params:bang()
end
Expand Down

0 comments on commit 1a6a41b

Please sign in to comment.