diff --git a/README.md b/README.md index 2c6599b..965f6e3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ -# Geppetto -A lil midi control modulator. +# Geppetto v0.1.1 -Run `;install https://github.com/cachilders/geppetto.git` from maiden to install. Add your own device configs to `lib/devices` following the structure of the sample files. Be sure to back up your config files, and feel free to submit a PR with them to help out the next person. +A lil midi control modulator. -Note that in Geppetto's current form only a single CC can be modulated on a single device at a time. Multiple devices and params are planned but time and tide and all that. +- v0.1.1 + - Initial support for incoming and outgoing start and stop midi events. + - Code refacotrs for future support of multiple devices + - Reduced lfo resolution to ease broadcast of midi messages + - Default selection of device input and disabling of navigation prior to initial selection +- v0.1.0 + - Basic functionality for a single device diff --git a/geppetto.lua b/geppetto.lua index 0a86542..ad31e13 100644 --- a/geppetto.lua +++ b/geppetto.lua @@ -12,37 +12,35 @@ Graph = require('graph') LFO = require('lfo') include('lib/inputs') +include('lib/device') CC_LABEL = 'CC -> ' PROGRAM_LABEL = 'Program -> ' function init() init_midi() - init_devices() - init_lfo() + init_device_configurations() + init_lfo_controls() + init_inputs() + init_state() screen.font_face(1) - - play_status = UI.PlaybackIcon.new(122, 0, 5, 4) - midi_list = Inputs.Select:new({ x = 0, y = 14, selected = 1, options = {1,2,3,4}}) - channel_list = Inputs.Select:new({ x = 20, y = 14, selected = 1, options = m.channels}) - device_list = Inputs.Select:new({ x = 40, y = 14, options = device_names, action = update_device_control_options}) - min_list = Inputs.Select:new({ x = 0, y = 37, selected = 1, options = midi_range, action = function(v) mod_lfo:set('min', v) end}) - max_list = Inputs.Select:new({ x = 20, y = 37, selected = 128, options = midi_range, action = function(v) mod_lfo:set('max', v) end}) - depth_list = Inputs.Select:new({ x = 40, y = 37, selected = 100, options = depth_range, action = function(v) mod_lfo:set('depth', calculate_depth(v)) end}) - shape_list = Inputs.Select:new({ x = 60, y = 37, selected = 1, options = shape_options, action = function(v) mod_lfo:set('shape', shape_options[v]) end}) - baseline_list = Inputs.Select:new({ x = 95, y = 37, selected = 1, options = baseline_options, action = function(v) mod_lfo:set('baseline', baseline_options[v]) end}) - control_list = Inputs.Select:new({ x = screen.text_extents(CC_LABEL) + 6, y = 47}) - program_list = Inputs.Select:new({ x = screen.text_extents(PROGRAM_LABEL) + 6, y = 61}) + redraw() +end + +function init_state() + -- Streamline and make persistent (optionally) + active_device = nil + play_status = UI.PlaybackIcon.new(122, 0, 5, 4) app = { ui = { playing = false, view = 1, views = { [1] = { - active_field = 1, + active_field = 3, fields = { -- midi [1] = midi_list, @@ -62,8 +60,20 @@ function init() } } } +end - redraw() +function init_inputs() + -- These inputs were originally the scrolling select and will need more positioning flexibility on subsequent refactor + midi_list = inputs.Select:new({ x = 0, y = 14, selected = 1, options = {1,2,3,4}, action = function(v) safe_set_device_prop('midi_port', v) end}) + channel_list = inputs.Select:new({ x = 20, y = 14, selected = 1, options = m.channels, action = function(v) safe_set_device_prop('midi_channel', v) end}) + device_list = inputs.Select:new({ x = 40, y = 14, options = device_names, action = update_device}) + min_list = inputs.Select:new({ x = 0, y = 37, selected = 1, options = midi_range, action = function(v) safe_set_device_lfo_prop('min', v) end}) + max_list = inputs.Select:new({ x = 20, y = 37, selected = 128, options = midi_range, action = function(v) safe_set_device_lfo_prop('max', v) end}) + depth_list = inputs.Select:new({ x = 40, y = 37, selected = 100, options = depth_range, action = function(v) safe_set_device_lfo_prop('depth', calculate_depth(v)) end}) + shape_list = inputs.Select:new({ x = 60, y = 37, selected = 1, options = shape_options, action = function(v) safe_set_device_lfo_prop('shape', shape_options[v]) end}) + baseline_list = inputs.Select:new({ x = 95, y = 37, selected = 1, options = baseline_options, action = function(v) safe_set_device_lfo_prop('baseline', baseline_options[v]) end}) + control_list = inputs.Select:new({ x = screen.text_extents(CC_LABEL) + 6, y = 47, action = function(v) safe_set_device_prop('modulated_control', v) end}) + program_list = inputs.Select:new({ x = screen.text_extents(PROGRAM_LABEL) + 6, y = 61, action = function(v) safe_set_device_prop('selected_program', v) end}) end function init_midi() @@ -72,11 +82,15 @@ function init_midi() m = { channels = {}, - devices = {} + ports = {} } for i=1, IO do - m.devices[i] = midi.connect(i) + m.ports[i] = midi.connect(i) + m.ports[i].event = function(d) + local msg = midi.to_msg(d) + handle_midi_event(msg.type, i) + end end for i=1, CH do @@ -84,29 +98,19 @@ function init_midi() end end -function init_devices() - devices = {} +function init_device_configurations() + device_configurations = {} device_names = {} local default_device_list = util.scandir('/home/we/dust/code/geppetto/lib/devices/') for i, v in ipairs(default_device_list) do - devices[i] = include('lib/devices/'..v:gsub('.lua', '')) - device_names[i] = devices[i].model + device_configurations[i] = include('lib/devices/'..v:gsub('.lua', '')) + device_names[i] = device_configurations[i].model end end -function lfo_test(scaled, raw) - mod_value = math.ceil(scaled - .5) - 1 - transmit_control_change(mod_value) - redraw() -end - -function calculate_depth(n) - return 1 * n / 100 -end - -function init_lfo() +function init_lfo_controls() depth_range = {} midi_range = {} period_options = {1, 2, 4, 8, 16, 32, 64} @@ -123,19 +127,26 @@ function init_lfo() for i=1, MAX do midi_range[i] = i end +end - mod_lfo = LFO:add{ - shape = 'sine', - min = 1, - max = 128, - depth = 1, - mode = 'clocked', - period = 4, - action = lfo_test, - baseline = 'center', - reset_target = 'floor', - ppqn = 96 - } + +function calculate_depth(n) + return n / 100 +end + +function safe_set_device_prop(k, v) + -- this whole control scheme will have to change as app grows, + -- but doing this for initial class migration + if active_device then + active_device[k] = v + end +end + +function safe_set_device_lfo_prop(k, v) + -- see note in safe set device prop + if active_device then + active_device.lfo:set(k, v) + end end function enc(e, d) @@ -145,87 +156,76 @@ function enc(e, d) local fields = views[view].fields if e == 1 then - -- time makes fools of us all + -- future home of view nav app.ui.view = util.clamp(view + d, 1, #views) - -- dream big but cut scope elseif e == 2 then - views[view].active_field = util.clamp(active_field + d, 1, #fields) + -- Suspending navigation around inputs until device has been chosen + -- rather than resolve some bugs that will be irrelevant when app + -- allows multiple devices + if active_device then + views[view].active_field = util.clamp(active_field + d, 1, #fields) + end elseif e == 3 then - fields[active_field]:set_index_delta(d, false) + fields[active_field]:set_index_delta(d, false) end redraw() end -function update_device_control_options(v) +function update_device(v) local fields = app.ui.views[1].fields + local last_device = active_device and active_device.make..active_device.model or '' + local next_device = device_configurations[v].make..device_configurations[v].model - handle_cancel() - create_device_cc_tables(devices[v].control) - - fields[9]:set('selected', 0) - fields[9]:set('options', control_options) - fields[10]:set('selected', 0) - fields[10]:set('options', devices[v].program) -end - -function create_device_cc_tables(controls) - control_options = {} - control_channels = {} - - local i = 1 + if next_device ~= last_device then + if active_device then + active_device:rebase(device_configurations[v]) + else + active_device = Device:new(device_configurations[v]) + end - for ch, name in pairs(controls) do - control_options[i] = ch..': '..name - control_channels[i] = ch - i = i + 1 + fields[9]:set('selected', 0) + fields[9]:set('options', active_device.control_options) + fields[10]:set('selected', 0) + fields[10]:set('options', active_device.program) end end -function transmit_program_change() +function handle_confirm() + local view = app.ui.view local views = app.ui.views - local fields = views[1].fields - - local device = devices[fields[3].selected] - local channel = fields[2].selected - local connection = fields[1].selected - local program = fields[10].selected + local active_field = views[view].active_field - if device.program_zero_indexed then - program = program - 1 + if view == 1 and active_field == 10 and active_device then + active_device:transmit_program_change() + elseif active_device then + play() end - - m.devices[connection]:program_change(program, channel) end -function transmit_control_change(v) - local views = app.ui.views - local fields = views[1].fields - - if control_channels then - m.devices[fields[1].selected]:cc(control_channels[fields[9].selected], v, fields[2].selected) +function handle_cancel() + if active_device then + stop() end end -function transmit_event() - -- tk clock, start, stop, continue +function handle_midi_event(event, port) + if active_device and active_device.midi_port == port then + if event == 'start' then + play() + elseif event == 'stop' then + stop() + end + end end -function handle_confirm() - local view = app.ui.view - local views = app.ui.views - local active_field = views[view].active_field - - if view == 1 and active_field == 10 then - transmit_program_change() - else - mod_lfo:start() - app.ui.playing = true - end +function play() + active_device:start() + app.ui.playing = true end -function handle_cancel() - mod_lfo:stop() +function stop() + active_device:stop() app.ui.playing = false end @@ -261,7 +261,7 @@ function paint_mod_value() if app.ui.playing then screen.level(15) screen.move(120, 5) - screen.text_right(mod_value or '') + screen.text_right(active_device.lfo_modulation_value or '') end end diff --git a/lib/device.lua b/lib/device.lua new file mode 100644 index 0000000..025b4a9 --- /dev/null +++ b/lib/device.lua @@ -0,0 +1,126 @@ +Device = { + control_channels = {}, + control_options = {}, + lfo_modulation_value = 0, + make = '', + midi_port = 1, + midi_channel = 1, + model = '', + modulated_control = 0, + modulated_controls = {}, + programs = {}, + selected_program = 0 +} + +function _configure(configuration) + configuration.control_options = {} + configuration.control_channels = {} + configuration.programs = {} + + local i = 1 + + for ch, name in pairs(configuration.control) do + configuration.control_options[i] = ch..': '..name + configuration.control_channels[i] = ch + i = i + 1 + end + + return { + control_channels = configuration.control_channels, + control_options = configuration.control_options, + make = configuration.make, + model = configuration.model, + modulated_control = 0, + modulated_controls = {}, + program = configuration.program, + program_zero_indexed = configuration.program_zero_indexed, + selected_program = 0 + } +end + +function _initiate_lfo(instance) + instance.lfo = LFO:add{ + action = function(v) instance:_lfo_action(v) end, + baseline = 'center', + depth = 1, + max = 128, + min = 1, + mode = 'clocked', + offset = 0, + period = 4, + ppqn = 48, + reset_target = 'floor', + shape = 'sine' + } +end + +function Device:_lfo_action(scaled) + self.lfo_modulation_value = math.ceil(scaled - .5) - 1 + + for k, v in pairs(self.modulated_controls) do + -- TODO: calculate per cc value with spatial offsets for plotted position on LFO + m.ports[self.midi_port]:cc(self.control_channels[v], mod_value, self.midi_channel) + end + + -- temp till multiple controls is implemented + local cc = self.control_channels[self.modulated_control] + m.ports[self.midi_port]:cc(cc, self.lfo_modulation_value, self.midi_channel) + + redraw() +end + +function Device:new(configuration) + local instance = _configure(configuration) or {} + setmetatable(instance, self) + self.__index = self + _initiate_lfo(instance) + + return instance +end + +function Device:update(t) + for k, v in pairs(t) do + self[k] = v + end +end + +function Device:rebase(configuration) + self:stop() + self:update(_configure(configuration)) +end + +function Device:set(k, v) + self[k] = v +end + +function Device:get(k) + -- something is off with this getter + return self[k] +end + +function Device:retire() + self:stop() + self = nil +end + +function Device:transmit_program_change() + local program = self.selected_program + + if self.program_zero_indexed then + program = program - 1 + end + + m.ports[self.midi_port]:program_change(program, self.midi_channel) +end + +function Device:start() + self.lfo:start() + m.ports[self.midi_port]:start() +end + +function Device:stop() + self.lfo:stop() + m.ports[self.midi_port]:stop() +end + +return Device \ No newline at end of file diff --git a/lib/inputs.lua b/lib/inputs.lua index a7a4447..ac3f3e0 100644 --- a/lib/inputs.lua +++ b/lib/inputs.lua @@ -1,4 +1,4 @@ -Inputs = { +inputs = { Select = { x = 0, y = 0, @@ -12,27 +12,27 @@ Inputs = { } } -function Inputs.Select:new(instance) - local instance = instance or {}, +function inputs.Select:new(options) + local instance = options or {} setmetatable(instance, self) self.__index = self return instance end -function Inputs.Select:set(field, v) - self[field] = v +function inputs.Select:set(k, v) + self[k] = v end -function Inputs.Select:get(field) - return self[field] +function inputs.Select:get(k) + return self[k] end -function Inputs.Select:set_index_delta(d) +function inputs.Select:set_index_delta(d) self.selected = util.clamp(self.selected + d, 1, #self.options) self.action(self.selected) end -function Inputs.Select:redraw() +function inputs.Select:redraw() local value = self.options[self.selected] or '