Skip to content

Instantly share code, notes, and snippets.

@ylmrx
Created January 12, 2022 17:30
Show Gist options
  • Save ylmrx/4b3228c9e9483581e301360b846f31eb to your computer and use it in GitHub Desktop.
Save ylmrx/4b3228c9e9483581e301360b846f31eb to your computer and use it in GitHub Desktop.
adding prog_changes
--====================================================--
-- Class: Midi
--====================================================--
--Todo: midi in
class 'Midi'
function Midi:__init(midi_in_device_name,midi_out_device_name,omit_nrpn_lsb)
self.midi_channel = 1
self.midi_in_device = nil
self.midi_in_device_name = midi_in_device_name
self.midi_out_device = nil
self.midi_out_device_name = midi_out_device_name
self.device_id = 0
self.omit_nrpn_lsb = omit_nrpn_lsb
self:initialize()
end
function Midi:send_cc(number,value)
local cc_start = 0xB0 + (self.midi_channel-1)
--print(("¤ Sending CC: %s %s"):format(number, value))
self:send({cc_start,number,value})
end
function Midi:send_pc(value)
local pc_start = 0xC0 + (self.midi_channel-1)
--print(("¤ Sending PC: %s %s"):format(value))
self:send({pc_start,value})
end
function Midi:send_pitchbend(value)
local msb_value = math.floor(value/128)
local lsb_value = value-(msb_value*128)
local pitchbend_start = 0xE0 + (self.midi_channel-1)
--print(("¤ Sending pitchbend: %s %s %s"):format(value,lsb_value,msb_value))
self:send({pitchbend_start,lsb_value,msb_value})
end
function Midi:send_sysex(message_template, number, value)
if value == nil then return end
local message = {}
for k,v in pairs(message_template) do
if v == "dd" then
table.insert(message, self.device_id)
elseif v == "cc" then
table.insert(message, self.midi_channel-1)
elseif v == "nn" then
table.insert(message, number)
elseif v == "vv" then
if (value > 127) then
local ms_nibble = math.floor(value/128)
value = value - (ms_nibble*128)
table.insert(message, ms_nibble)
table.insert(message, value)
else
table.insert(message, value)
end
else
table.insert(message, v)
end
end
--print(("¤ Sending sysex: %s %s"):format(number,value))
self:send(message)
end
function Midi:send_nrpn(number,value)
local cc_start = 0xB0 + (self.midi_channel-1)
local msb_number = math.floor(number/128)
local lsb_number = number-(msb_number*128)
--print(("¤ Sending NRPN: %s %s"):format(number,value))
self:send({cc_start,99,msb_number})
self:send({cc_start,98,lsb_number})
if (self.omit_nrpn_lsb) then
self:send({cc_start,6,value})
else
local msb_value = math.floor(value/128)
local lsb_value = value-(msb_value*128)
self:send({cc_start,6,msb_value})
self:send({cc_start,38,lsb_value})
end
end
function Midi:send(message_table)
local message = ""
for k,v in ipairs(message_table) do message = message .. " " .. v end
--print("¤ Sending midi: " .. message)
if (self.midi_out_device ~= nil) then
self.midi_out_device:send(message_table)
else
print("¤ Could not send midi, no device")
end
end
function Midi:set_midi_channel(midi_channel)
self.midi_channel = midi_channel
end
function Midi:set_device_id(device_id)
self.device_id = device_id
end
--== Init ==--
function Midi:initialize()
if not self:midi_out_device_exists(self.midi_out_device_name) then
self.midi_out_device_name = self:get_default_midi_out_device_name()
end
self:open_midi_out_device(self.midi_out_device_name)
end
--== Check if midi out device exists ==--
function Midi:midi_out_device_exists(device_name)
if (device_name == nil or device_name == "") then return false end
local ret = table.find(renoise.Midi.available_output_devices(),device_name) ~= nil
if not ret then
print(("¤ MIDI out did not exist: '%s'"):format(device_name))
end
return ret
end
--== Get first occurring midi out device ==--
function Midi:get_default_midi_out_device_name()
if (#renoise.Midi.available_output_devices() > 0) then
print(("¤ MIDI out device defaulted to '%s'"):format(renoise.Midi.available_output_devices()[1]))
return renoise.Midi.available_output_devices()[1]
end
return nil
end
--== Open/create up midi out device ==--
function Midi:open_midi_out_device(device_name)
if self:midi_out_device_exists(device_name) then
if (self.midi_out_device ~= nil and self.midi_out_device.is_open) then
print("¤ Closing open MIDI device")
self.midi_out_device:close()
end
self.midi_out_device = renoise.Midi.create_output_device(device_name)
print(("¤ Opened MIDI out device: '%s'"):format(device_name))
end
return nil
end
--==================================================--
-- Class: Parameter
--==================================================--
class 'Parameter'
function Parameter:__init(tbl)
self.id = assert(tbl.id, "Parameter.id is missing")
self.name = assert(tbl.name, "Parameter.name is missing")
self.type = tbl.type -- "sysex", "cc", "pc", "nrpn", "pitchbend"
if (self.type ~= "pitchbend" and self.type ~= "pc") then
self.number = assert(tbl.number, "Parameter.number is missing (" .. self.id .. ")")
end
self.lsb_number = tbl.lsb_number -- Todo...
self.default_value = tbl.default_value
self.min_value = tbl.min_value
self.max_value = tbl.max_value
self.display_min_value = tbl.display_min_value
self.display_max_value = tbl.display_max_value
self.display_value_factor = 1
self.randomize_min = tbl.randomize_min
self.randomize_max = tbl.randomize_max
self.items = tbl.items
self.item_values = tbl.item_values
self.gui_type = tbl.gui_type
self.sysex_message_template = tbl.sysex_message_template
self.value_callback = tbl.value_callback
if self.items then
assert(self.min_value == nil, "Don't set min_value for switches this will be set automatically (" .. self.id .. ")")
assert(self.max_value == nil, "Don't set max_value for switches this will be set automatically (" .. self.id .. ")")
end
if (self.type == nil) then self.type = "sysex" end
if (self.min_value == nil) then
if (self.items ~= nil) then
self.min_value = 1
else
self.min_value = 0
end
end
if (self.max_value == nil) then
if (self.type == "pitchbend") then
self.max_value = 16383
self.display_max_value = 100
self.display_min_value = -100
elseif self.items ~= nil then
self.max_value = #self.items
else
self.max_value = 127
end
end
if (self.default_value == nil) then
if (self.type == "pitchbend") then
self.default_value = 8192
elseif (self.items ~= nil) then
self.default_value = 1
else
self.default_value = self.min_value
end
end
if (self.items ~= nil) then
assert(self.default_value > 0, "Error in synth definition: default value for a switch/dropdown cannot be less than 1")
end
if self.display_min_value == nil then
self.display_min_value = self.min_value
end
if self.display_max_value == nil then
self.display_max_value = self.max_value
end
-- Display value factor = factor for recalculating the sliders display value
-- which may be other than the actual/midi/patch value
local display_range = self.display_max_value - self.display_min_value
local actual_range = self.max_value - self.min_value
self.display_value_factor = display_range / actual_range
self.patch_document_variable = nil
self.midi_mapping_id = nil
self.has_changed = false
self.synth_definition = nil
self.is_updating_value = false
end
--== Set up reference to synth definition, create patch document, add midi mappings, set sysex message start ==--
function Parameter:initialize(synth_definition, sysex_message_template)
if (self.sysex_message_template == nil) then
self.sysex_message_template = sysex_message_template
end
self.synth_definition = synth_definition
self.synth_definition.parameters[self.id] = self
self.midi_mapping_id = self.synth_definition.midi_mapping_prefix .. self.id
local patch_document = self.synth_definition.patch_document
self.patch_document_variable = patch_document:add_property(self.id, self.default_value)
if self.synth_definition.omit_nrpn_lsb and self.type == "nrpn" then
assert(self.max_value <= 127, "Error in synth definition: max value for nrpn parameter cannot be more than 127 if omit_nrpn_lsb is set")
end
if self.randomize_min ~= nil or self.randomize_max ~= nil then
self.synth_definition.has_randomize_values = true
end
end
function Parameter:add_midi_mappings(replace_existing)
if renoise.tool():has_midi_mapping(self.midi_mapping_id) and replace_existing then --"Take over" the midi mapping
print("Removing midi mapping:" .. self.midi_mapping_id)
renoise.tool():remove_midi_mapping(self.midi_mapping_id)
end
if not renoise.tool():has_midi_mapping(self.midi_mapping_id) then --Only add midi mapping for the first instance of a synth
--print("Adding midi mapping:" .. self.midi_mapping_id)
renoise.tool():add_midi_mapping{
name = self.midi_mapping_id,
invoke = function(message)
self:handle_midi_mapping_message(message)
end
}
end
end
function Parameter:set_changed()
self.synth_definition:set_changed()
end
--== Handle incoming renoise midi mapping message ==--
function Parameter:handle_midi_mapping_message(message)
local value = message.int_value
--print(("# Handle midi mapping %s "):format(value))
self:value(value)
self:set_changed()
end
--GET/SET document value for the parameter (and send midi)
--Document value is the same as midi value, except for
--switches/dropdowns where it's the item index starting at 1
function Parameter:document_value(value)
--GET
if value == nil then
value = self.patch_document_variable.value
value = math.floor(value + 0.5)
--SET
else
value = math.floor(value + 0.5)
--print(("# Parameter value changed: %s %s"):format(self.id, value))
self.patch_document_variable.value = value
if not self.is_updating_value then
self:update_ui()
end
if not self.synth_definition.is_loading then
self:send_midi()
end
end
return value
end
--GET/SET the (midi) value for the parameter
--GET converts document value to midi value
--SET converts midi value to document value and sets it
function Parameter:value(value)
--GET
if value == nil then
value = self:document_value()
if (self.items ~= nil) then
if (self.item_values ~= nil) then
value = self.item_values[value]
else
value = value - 1
end
end
--SET
else
value = math.floor(value+0.5)
if (self.items ~= nil) then
if (self.item_values ~= nil) then
--Find as a close/high match as possible, 1 as default
local out_value = 1
for k,v in ipairs(self.item_values) do
if (value >= v) then
out_value = k
else
break
end
end
value = out_value
else
value = value + 1
end
end
print(("# value() %s "):format(value))
self:document_value(value)
end
return value
end
--GET/SET the display value for the parameter
--GET converts document value to display value
--SET converts display value to document value and sets it
function Parameter:display_value(value)
-- GET
if value == nil then
value = self:document_value()
value = value - self.min_value
value = value * self.display_value_factor + self.display_min_value
value = math.floor(value + 0.5)
-- SET
else
value = value - self.display_min_value
value = value * (1/self.display_value_factor) + self.min_value
value = math.floor(value + 0.5)
self:document_value(value)
end
return value
end
--Update the parameters UI element when value changes
function Parameter:update_ui()
local display_value = self:display_value()
self.is_updating_value = true
local vb = self.synth_definition.view_builder
local ui_element = vb.views[self.id]
if ui_element ~= nil and type(ui_element) ~= "Button" then
ui_element.value = display_value
end
local ui_valuefield = vb.views["valuefield_" .. self.id]
if ui_valuefield ~= nil then
ui_valuefield.value = display_value
end
self.is_updating_value = false
end
-- Set value from Ui element
function Parameter:ui_set_value(display_value)
if self.is_updating_value then return end
self.is_updating_value = true
self:set_changed()
self:display_value(display_value)
local vb = self.synth_definition.view_builder
local ui_valuefield = vb.views["valuefield_" .. self.id]
if ui_valuefield ~= nil then
ui_valuefield.value = display_value
end
self.is_updating_value = false
end
--Set value from UI value field
function Parameter:vf_set_value(display_value)
if self.is_updating_value then return end
self.is_updating_value = true
self:set_changed()
self:display_value(display_value)
local vb = self.synth_definition.view_builder
local ui_element = vb.views[self.id]
if ui_element ~= nil then
ui_element.value = display_value
end
self.is_updating_value = false
end
function Parameter:send_midi()
local midi_value = self:value()
if self.value_callback ~= nil then
local callback_value = self:value_callback(self)
if callback_value ~= nil then
midi_value = callback_value
print(("# After value callback %s"):format(midi_value))
end
end
if (self.type == "sysex") then
assert(self.sysex_message_template, "sysex_message_template not specified")
self.synth_definition.midi:send_sysex(self.sysex_message_template, self.number, midi_value)
elseif self.type == "cc" then
if self.synth_definition.id == "korg_poly800_hawk800" then --Sigh...
self.synth_definition.midi:send_cc(84, self.number)
self.synth_definition.midi:send_cc(85, midi_value)
else
self.synth_definition.midi:send_cc(self.number, midi_value)
end
elseif self.type == "nrpn" then
self.synth_definition.midi:send_nrpn(self.number,midi_value)
elseif self.type == "pitchbend" then
self.synth_definition.midi:send_pitchbend(midi_value)
elseif self.type == "pc" then
self.synth_definition.midi:send_pc(midi_value)
end
end
--== Create a user interface for Parameter ==--
function Parameter:create_ui()
local vb = self.synth_definition.view_builder
local ui = vb:row {
vb:text{
id = "label_" .. self.id,
text = self.name,
width = 105
}
}
if (self.items ~= nil) then
if (#self.items > 10 or self.gui_type == "dropdown") then
ui:add_child(
vb:popup {
id = self.id,
width = 200,
midi_mapping = self.midi_mapping_id,
notifier = function(value)
self:ui_set_value(value)
end,
items = self.items
}
)
elseif (#self.items == 1 and self.item_values ~= nil) then
--Super secret functionality for buttons
ui:add_child(
vb:button {
id = self.id,
width = 200,
text = self.items[1],
pressed = function()
self.synth_definition.midi:send_cc(self.number, self.item_values[1])
end,
midi_mapping = self.midi_mapping_id,
}
)
else
ui:add_child(
vb:switch {
id = self.id,
width = 200,
midi_mapping = self.midi_mapping_id,
notifier = function(value)
self:ui_set_value(value)
end,
items = self.items
}
)
end
ui:add_child(
vb:space{
width = 40
}
)
else
local min = self.min_value
local max = self.max_value
if self.display_min_value ~= nil and self.display_max_value ~= nil then
min = self.display_min_value
max = self.display_max_value
end
ui:add_child(
vb:slider {
id = self.id,
width = 200,
midi_mapping = self.midi_mapping_id,
notifier = function(value)
self:ui_set_value(value)
end,
min = min,
max = max
}
)
ui:add_child(
vb:valuefield{
id = "valuefield_" .. self.id,
width = 40,
min = min,
max = max,
notifier = function(value)
self:vf_set_value(value)
end,
tostring = function(value)
return ("%.0f"):format(tostring(value))
end,
tonumber = function(value)
local success, new_val = try_round(value)
if (success) then
value = new_val
if (value < self.min_value) then value = self.min_value end
if (value > self.max_value) then value = self.max_value end
return value
end
end
}
)
end
return ui
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment