Created
December 2, 2024 08:29
-
-
Save ylmrx/b8677cab55805d2e97125e1ff2019ff5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- scriptname: moln | |
-- v1.2.2 @jah | |
-- add pwm arcs | |
local R = require 'r/lib/r' | |
local r_engine = R.engine | |
local r_specs = R.specs | |
local r_util = R.util | |
local ControlSpec = require 'controlspec' | |
local Formatters = require 'formatters' | |
local Voice = require 'voice' | |
local grid = util.file_exists(_path.code.."toga") and include "toga/lib/togagrid" or grid | |
local arc = util.file_exists(_path.code.."toga") and include "toga/lib/togaarc" or arc | |
local grid_width | |
local grid_device | |
local arc_device | |
local ui_dirty = false | |
local refresh_rate = 60 | |
local event_flash_duration = 0.15 | |
local show_event_indicator = false | |
local event_flash_frame_counter = nil | |
local grid_led_x_spec = ControlSpec.new(1, 16, ControlSpec.WARP_LIN, 1, 0, "") | |
local grid_led_y_spec = ControlSpec.new(1, 16, ControlSpec.WARP_LIN, 1, 0, "") | |
local grid_led_l_spec = ControlSpec.new(0, 15, ControlSpec.WARP_LIN, 1, 0, "") | |
local arc_led_x_spec = ControlSpec.new(1, 64, ControlSpec.WARP_LIN, 1, 0, "") | |
local arc_led_l_spec = ControlSpec.new(0, 15, ControlSpec.WARP_LIN, 1, 0, "") | |
local hi_level = 15 | |
local lo_level = 4 | |
local enc1_x = 1 | |
local enc1_y = 12 | |
local enc2_x = 10 | |
local enc2_y = 32 | |
local enc3_x = enc2_x+65 | |
local enc3_y = enc2_y | |
local key2_x = 1 | |
local key2_y = 63 | |
local key3_x = key2_x+65 | |
local key3_y = key2_y | |
local engine_ready = false | |
local trigging = false | |
local fine = false | |
local lastkeynote | |
local polyphony = 5 | |
local note_downs = {} | |
local note_slots = {} | |
local voice_allocator | |
local | |
create_modules = | |
function() | |
r_engine.poly_new("FreqGate", "FreqGate", polyphony) | |
r_engine.poly_new("LFO", "SineLFO", polyphony) | |
r_engine.poly_new("Env", "ADSREnv", polyphony) | |
r_engine.poly_new("OscA", "PulseOsc", polyphony) | |
r_engine.poly_new("OscB", "PulseOsc", polyphony) | |
r_engine.poly_new("Filter", "LPFilter", polyphony) | |
r_engine.poly_new("Amp", "Amp", polyphony) | |
engine.new("OutputGain", "SGain") | |
engine.new("SoundOut", "SoundOut") | |
end | |
local | |
set_static_module_params = | |
function () | |
r_engine.poly_set("OscA.FM", 1, polyphony) | |
r_engine.poly_set("OscB.FM", 1, polyphony) | |
r_engine.poly_set("Filter.AudioLevel", 1, polyphony) | |
end | |
local | |
connect_modules = | |
function() | |
r_engine.poly_connect("FreqGate/Frequency", "OscA*FM", polyphony) | |
r_engine.poly_connect("FreqGate/Frequency", "OscB*FM", polyphony) | |
r_engine.poly_connect("FreqGate/Gate", "Env*Gate", polyphony) | |
r_engine.poly_connect("LFO/Out", "OscA*PWM", polyphony) | |
r_engine.poly_connect("LFO/Out", "OscB*PWM", polyphony) | |
r_engine.poly_connect("Env/Out", "Amp*Lin", polyphony) | |
r_engine.poly_connect("Env/Out", "Filter*FM", polyphony) | |
r_engine.poly_connect("OscA/Out", "Filter*In", polyphony) | |
r_engine.poly_connect("OscB/Out", "Filter*In", polyphony) | |
r_engine.poly_connect("Filter/Out", "Amp*In", polyphony) | |
for voicenum=1, polyphony do | |
engine.connect("Amp"..voicenum.."/Out", "OutputGain*Left") | |
engine.connect("Amp"..voicenum.."/Out", "OutputGain*Right") | |
end | |
engine.connect("OutputGain/Left", "SoundOut*Left") | |
engine.connect("OutputGain/Right", "SoundOut*Right") | |
end | |
local | |
create_macros = | |
function () | |
engine.newmacro("osc_a_range", r_util.poly_expand("OscA.Range", polyphony)) | |
engine.newmacro("osc_a_pulsewidth", r_util.poly_expand("OscA.PulseWidth", polyphony)) | |
engine.newmacro("osc_b_range", r_util.poly_expand("OscB.Range", polyphony)) | |
engine.newmacro("osc_b_pulsewidth", r_util.poly_expand("OscB.PulseWidth", polyphony)) | |
engine.newmacro("osc_a_detune", r_util.poly_expand("OscA.Tune", polyphony)) | |
engine.newmacro("osc_b_detune", r_util.poly_expand("OscB.Tune", polyphony)) | |
engine.newmacro("lfo_frequency", r_util.poly_expand("LFO.Frequency", polyphony)) | |
engine.newmacro("osc_a_pwm", r_util.poly_expand("OscA.PWM", polyphony)) | |
engine.newmacro("osc_b_pwm", r_util.poly_expand("OscB.PWM", polyphony)) | |
engine.newmacro("filter_frequency", r_util.poly_expand("Filter.Frequency", polyphony)) | |
engine.newmacro("filter_resonance", r_util.poly_expand("Filter.Resonance", polyphony)) | |
engine.newmacro("env_to_filter_fm", r_util.poly_expand("Filter.FM", polyphony)) | |
engine.newmacro("env_attack", r_util.poly_expand("Env.Attack", polyphony)) | |
engine.newmacro("env_decay", r_util.poly_expand("Env.Decay", polyphony)) | |
engine.newmacro("env_sustain", r_util.poly_expand("Env.Sustain", polyphony)) | |
engine.newmacro("env_release", r_util.poly_expand("Env.Release", polyphony)) | |
end | |
local | |
init_osc_a_range_param = | |
function() | |
params:add { | |
type="control", | |
id="osc_a_range", | |
name="Osc A Range", | |
controlspec=r_specs.PulseOsc.Range, | |
formatter=Formatters.round(1), | |
action=function (value) | |
engine.macroset("osc_a_range", value) | |
end | |
} | |
end | |
local | |
init_osc_a_pulsewidth_param = | |
function () | |
local spec = r_specs.PulseOsc.PulseWidth:copy() | |
spec.default = 0.88 | |
params:add { | |
type="control", | |
id="osc_a_pulsewidth", | |
name="Osc A PulseWidth", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("osc_a_pulsewidth", value) | |
end | |
} | |
end | |
local | |
init_osc_b_range_param = | |
function() | |
params:add { | |
type="control", | |
id="osc_b_range", | |
name="Osc B Range", | |
controlspec=r_specs.PulseOsc.Range, | |
formatter=Formatters.round(1), | |
action=function (value) | |
engine.macroset("osc_b_range", value) | |
end | |
} | |
end | |
local | |
init_osc_b_pulsewidth_param = | |
function() | |
local spec = r_specs.PulseOsc.PulseWidth:copy() | |
spec.default = 0.61 | |
params:add { | |
type="control", | |
id="osc_b_pulsewidth", | |
name="Osc B PulseWidth", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("osc_b_pulsewidth", value) | |
end | |
} | |
end | |
local | |
init_osc_detune_param = | |
function() | |
local spec = ControlSpec.UNIPOLAR:copy() | |
spec.default = 0.36 | |
params:add { | |
type="control", | |
id="osc_detune", | |
name="Detune", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("osc_a_detune", -value*10) | |
engine.macroset("osc_b_detune", value*10) | |
end | |
} | |
end | |
local | |
init_lfo_frequency_param = | |
function() | |
local spec = r_specs.MultiLFO.Frequency:copy() | |
spec.default = 0.125 | |
params:add { | |
type="control", | |
id="lfo_frequency", | |
name="PWM Rate", | |
controlspec=spec, | |
formatter=Formatters.round(0.001), | |
action=function (value) | |
engine.macroset("lfo_frequency", value) | |
end | |
} | |
end | |
local | |
init_lfo_to_osc_pwm_param = | |
function() | |
local spec = ControlSpec.UNIPOLAR:copy() | |
spec.default = 0.46 | |
params:add { | |
type="control", | |
id="lfo_to_osc_pwm", | |
name="PWM Depth", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("osc_a_pwm", value*0.76) | |
engine.macroset("osc_b_pwm", value*0.56) | |
end | |
} | |
end | |
local | |
adaptive_freq_raw = | |
function(hz) | |
if hz <= -1000 then | |
return util.round(hz/1000, 0.1) .. "kHz" | |
elseif hz <= -100 then | |
return util.round(hz, 1).."Hz" | |
elseif hz <= -10 then | |
return util.round(hz, 0.1).."Hz" | |
elseif hz <= -1 then | |
return util.round(hz, 0.01).."Hz" | |
elseif hz < 0 then | |
local str = tostring(util.round(hz, 0.001)) | |
return "-"..string.sub(str, 3, #str).."Hz" | |
elseif hz < 1 then | |
local str = tostring(util.round(hz, 0.001)) | |
return string.sub(str, 2, #str).."Hz" | |
elseif hz < 10 then | |
return util.round(hz, 0.01).."Hz" | |
elseif hz < 100 then | |
return util.round(hz, 0.1).."Hz" | |
elseif hz < 1000 then | |
return util.round(hz, 1).."Hz" | |
elseif hz < 10000 then | |
return util.round(hz/1000, 0.1) .. "kHz" | |
else | |
return util.round(hz/1000, 1) .. "kHz" | |
end | |
end | |
local | |
init_filter_frequency_param = | |
function() | |
local spec = r_specs.MMFilter.Frequency:copy() | |
spec.maxval = 8000 | |
spec.minval = 10 | |
spec.default = 500 | |
params:add { | |
type="control", | |
id="filter_frequency", | |
name="Filter Frequency", | |
controlspec=spec, | |
action=function (value) | |
engine.macroset("filter_frequency", value) | |
ui_dirty = true | |
end | |
} | |
end | |
local | |
init_filter_resonance_param = | |
function() | |
local spec = r_specs.MMFilter.Resonance:copy() | |
spec.default = 0.2 | |
params:add { | |
type="control", | |
id="filter_resonance", | |
name="Filter Resonance", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("filter_resonance", value) | |
ui_dirty = true | |
end | |
} | |
end | |
local | |
init_env_to_filter_fm_param = | |
function() | |
local spec = r_specs.MMFilter.FM | |
spec.default = 0.35 | |
params:add { | |
type="control", | |
id="env_to_filter_fm", | |
name="Env > Filter Frequency", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("env_to_filter_fm", value) | |
end | |
} | |
end | |
local | |
init_env_attack_param = | |
function() | |
local spec = r_specs.ADSREnv.Attack:copy() | |
spec.default = 1 | |
params:add { | |
type="control", | |
id="env_attack", | |
name="Env Attack", | |
controlspec=spec, | |
action=function (value) | |
engine.macroset("env_attack", value) | |
end | |
} | |
end | |
local | |
init_env_decay_param = | |
function() | |
local spec = r_specs.ADSREnv.Decay:copy() | |
spec.default = 200 | |
params:add { | |
type="control", | |
id="env_decay", | |
name="Env Decay", | |
controlspec=spec, | |
action=function (value) | |
engine.macroset("env_decay", value) | |
end | |
} | |
end | |
local | |
init_env_sustain_param = | |
function() | |
local spec = r_specs.ADSREnv.Sustain:copy() | |
spec.default = 0.5 | |
params:add { | |
type="control", | |
id="env_sustain", | |
name="Env Sustain", | |
controlspec=spec, | |
formatter=Formatters.percentage, | |
action=function (value) | |
engine.macroset("env_sustain", value) | |
end | |
} | |
end | |
local | |
init_env_release_param = | |
function() | |
local spec = r_specs.ADSREnv.Release:copy() | |
spec.default = 500 | |
params:add { | |
type="control", | |
id="env_release", | |
name="Env Release", | |
controlspec=spec, | |
action=function (value) | |
engine.macroset("env_release", value) | |
end | |
} | |
end | |
local | |
init_main_level_param = | |
function() | |
local spec = r_specs.SGain.Gain:copy() | |
spec.default = -10 | |
params:add { | |
type="control", | |
id="main_level", | |
name="Output Level", | |
controlspec=spec, | |
formatter=Formatters.round(0.1), | |
action=function (value) | |
engine.set("OutputGain.Gain", value) | |
ui_dirty = true | |
end | |
} | |
end | |
local | |
init_params = | |
function() | |
init_osc_a_range_param() | |
init_osc_a_pulsewidth_param() | |
init_osc_b_range_param() | |
init_osc_b_pulsewidth_param() | |
init_osc_detune_param() | |
init_lfo_frequency_param() | |
init_lfo_to_osc_pwm_param() | |
init_filter_frequency_param() | |
init_filter_resonance_param() | |
init_env_to_filter_fm_param() | |
init_env_attack_param() | |
init_env_decay_param() | |
init_env_sustain_param() | |
init_env_release_param() | |
init_main_level_param() | |
end | |
local | |
release_voice = | |
function(voicenum) | |
engine.bulkset("FreqGate"..voicenum..".Gate 0") | |
end | |
local | |
to_hz = | |
function(note) | |
local exp = (note - 21) / 12 | |
return 27.5 * 2^exp | |
end | |
local | |
trig_voice = | |
function(voicenum, note) | |
engine.bulkset("FreqGate"..voicenum..".Gate 1 FreqGate"..voicenum..".Frequency "..to_hz(note)) | |
end | |
local | |
note_on = | |
function(note, velocity) | |
if not note_slots[note] then | |
local slot = voice_allocator:get() | |
local voicenum = slot.id | |
trig_voice(voicenum, note) | |
slot.on_release = function() | |
release_voice(voicenum) | |
note_slots[note] = nil | |
end | |
note_slots[note] = slot | |
note_downs[voicenum] = note | |
end | |
end | |
local | |
note_off = | |
function(note) | |
local slot = note_slots[note] | |
if slot then | |
voice_allocator:release(slot) | |
note_downs[slot.id] = nil | |
end | |
end | |
local | |
gridkey_to_note = | |
function(x, y, grid_width) | |
if grid_width == 16 then | |
return x * 8 + y | |
else | |
return (4 + x) * 8 + y | |
end | |
end | |
local | |
note_to_gridkey = | |
function(note, grid_width) | |
if grid_width == 16 then | |
return math.floor((note - 1) / 8), ((note - 1) % 8) + 1 | |
else | |
return math.floor((note - 1) / 8) - 4, ((note - 1) % 8) + 1 | |
end | |
end | |
local | |
flash_event = | |
function() | |
event_flash_frame_counter = event_flash_duration * refresh_rate | |
end | |
local | |
update_event_indicator = | |
function() | |
if event_flash_frame_counter then | |
event_flash_frame_counter = event_flash_frame_counter - 1 | |
if event_flash_frame_counter == 0 then | |
event_flash_frame_counter = nil | |
show_event_indicator = false | |
ui_dirty = true | |
else | |
if not show_event_indicator then | |
show_event_indicator = true | |
ui_dirty = true | |
end | |
end | |
end | |
end | |
local | |
update_grid_width = | |
function() | |
if grid_device.device then | |
if grid_width ~= grid_device.cols then | |
grid_width = grid_device.cols | |
end | |
end | |
end | |
local | |
refresh_arc = | |
function() | |
arc_device:all(0) | |
arc_device:led(1, arc_led_x_spec:map(params:get_raw("filter_frequency")), arc_led_l_spec.maxval) | |
arc_device:led(2, arc_led_x_spec:map(params:get_raw("filter_resonance")), arc_led_l_spec.maxval) | |
arc_device:led(3, arc_led_x_spec:map(params:get_raw("osc_a_pulsewidth")), arc_led_l_spec.maxval) | |
arc_device:led(4, arc_led_x_spec:map(params:get_raw("osc_b_pulsewidth")), arc_led_l_spec.maxval) | |
arc_device:refresh() | |
end | |
local | |
refresh_grid = | |
function() | |
grid_device:all(0) | |
for voicenum=1,polyphony do | |
local note = note_downs[voicenum] | |
if note then | |
local x, y = note_to_gridkey(note, grid_width) | |
grid_device:led(grid_led_x_spec:constrain(x), grid_led_y_spec:constrain(y), grid_led_l_spec.maxval) | |
end | |
end | |
grid_device:refresh() | |
end | |
local | |
refresh_ui = | |
function() | |
update_event_indicator() | |
update_grid_width() | |
if ui_dirty then | |
redraw() | |
refresh_arc() | |
refresh_grid() | |
ui_dirty = false | |
end | |
end | |
local | |
init_arc = | |
function() | |
arc_device = arc.connect() | |
arc_device.delta = function(n, delta) | |
local delta_scaled | |
flash_event() | |
if fine then | |
delta_scaled = delta/5 | |
else | |
delta_scaled = delta | |
end | |
if n == 1 then | |
local val = params:get_raw("filter_frequency") | |
params:set_raw("filter_frequency", val+delta_scaled/500) | |
elseif n == 2 then | |
local val = params:get_raw("filter_resonance") | |
params:set_raw("filter_resonance", val+delta_scaled/500) | |
elseif n == 3 then | |
local val = params:get_raw("osc_a_pulsewidth") | |
params:set_raw("osc_a_pulsewidth", val+delta_scaled/500) | |
elseif n == 4 then | |
local val = params:get_raw("osc_b_pulsewidth") | |
params:set_raw("osc_b_pulsewidth", val+delta_scaled/500) | |
end | |
ui_dirty = true | |
end | |
end | |
local | |
init_grid = | |
function() | |
grid_device = grid.connect() | |
grid_device.key = function(x, y, state) | |
flash_event() | |
if engine_ready then | |
local note = gridkey_to_note(x, y, grid_width) | |
if state == 1 then | |
note_on(note, 5) | |
else | |
note_off(note) | |
end | |
ui_dirty = true | |
end | |
end | |
end | |
local | |
init_midi = | |
function() | |
midi_device = midi.connect() | |
midi_device.event = function(data) | |
flash_event() | |
if engine_ready then | |
if #data == 0 then return end | |
local msg = midi.to_msg(data) | |
if msg.type == "note_off" then | |
note_off(msg.note) | |
elseif msg.type == "note_on" then | |
note_on(msg.note, msg.vel / 127) | |
end | |
ui_dirty = true | |
end | |
end | |
end | |
local | |
init_engine_init_delay_metro = | |
function() | |
local engine_init_delay_metro = metro.init() | |
engine_init_delay_metro.event = function() | |
engine_ready = true | |
ui_dirty = true | |
engine_init_delay_metro:stop() | |
end | |
engine_init_delay_metro.time = 1 | |
engine_init_delay_metro:start() | |
end | |
local | |
init_ui_refresh_metro = | |
function() | |
local ui_refresh_metro = metro.init() | |
ui_refresh_metro.event = refresh_ui | |
ui_refresh_metro.time = 1/refresh_rate | |
ui_refresh_metro:start() | |
end | |
local | |
init_ui = | |
function() | |
init_arc() | |
init_grid() | |
init_midi() | |
init_ui_refresh_metro() | |
init_engine_init_delay_metro() | |
end | |
local | |
redraw_enc1_widget = | |
function() | |
screen.move(enc1_x, enc1_y) | |
screen.level(lo_level) | |
screen.text("LEVEL") | |
screen.move(enc1_x+45, enc1_y) | |
screen.level(hi_level) | |
screen.text(util.round(params:get_raw("main_level")*100, 1)) | |
end | |
local | |
redraw_event_flash_widget = | |
function() | |
screen.level(lo_level) | |
screen.rect(122, enc1_y-7, 5, 5) | |
screen.fill() | |
end | |
local | |
redraw_param_widget = | |
function(x, y, label, value) | |
screen.move(x, y) | |
screen.level(lo_level) | |
screen.text(label) | |
screen.move(x, y+12) | |
screen.level(hi_level) | |
screen.text(value) | |
end | |
local | |
redraw_enc2_widget = | |
function() | |
redraw_param_widget(enc2_x, enc2_y, "FREQ", adaptive_freq_raw(params:get("filter_frequency"))) | |
end | |
local | |
redraw_enc3_widget = | |
function() | |
redraw_param_widget(enc3_x, enc3_y, "FREQ", tostring(util.round(params:get("filter_resonance")*100, 1)) .. "%") | |
end | |
local | |
redraw_key2_widget = | |
function() | |
screen.move(key2_x, key2_y) | |
if fine then | |
screen.level(hi_level) | |
screen.text("FINE") | |
else | |
screen.level(lo_level) | |
screen.text("COARSE") | |
end | |
end | |
local | |
redraw_key3_widget = | |
function() | |
screen.move(key3_x, key3_y) | |
if engine_ready then | |
if trigging then | |
screen.level(hi_level) | |
else | |
screen.level(lo_level) | |
end | |
screen.text("TRIG") | |
end | |
end | |
engine.name = 'R' | |
init = | |
function() | |
voice_allocator = Voice.new(polyphony) | |
create_modules() | |
set_static_module_params() | |
connect_modules() | |
create_macros() | |
init_params() | |
init_ui() | |
params:read() | |
params:bang() | |
end | |
cleanup = | |
function() | |
params:write() | |
end | |
redraw = | |
function() | |
screen.font_size(16) | |
screen.clear() | |
redraw_enc1_widget() | |
if show_event_indicator then | |
redraw_event_flash_widget() | |
end | |
redraw_enc2_widget() | |
redraw_enc3_widget() | |
redraw_key2_widget() | |
redraw_key3_widget() | |
screen.update() | |
end | |
enc = | |
function(n, delta) | |
local delta_scaled | |
if fine then | |
delta_scaled = delta/5 | |
else | |
delta_scaled = delta | |
end | |
if n == 1 then | |
params:delta("main_level", delta_scaled) | |
elseif n == 2 then | |
params:delta("filter_frequency", delta_scaled) | |
elseif n == 3 then | |
params:delta("filter_resonance", delta_scaled) | |
end | |
end | |
key = | |
function(n, z) | |
if n == 2 then | |
if z == 1 then | |
fine = true | |
else | |
fine = false | |
end | |
elseif n == 3 then | |
if engine_ready then | |
if z == 1 then | |
lastkeynote = math.random(60) + 20 | |
note_on(lastkeynote, 100) | |
trigging = true | |
else | |
if lastkeynote then | |
note_off(lastkeynote) | |
trigging = false | |
end | |
end | |
end | |
end | |
ui_dirty = true | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment