diff --git a/README.md b/README.md index 4abf16b..8d4f8ad 100644 --- a/README.md +++ b/README.md @@ -23,27 +23,102 @@ Note that after installing you must `SYSTEM => RESET` your Norns before running Launching randomizes all voices, and drops you into a euclidean sequencer. Controls are as follows: ``` +Trigger View E1 select E2 density E3 length -K2 reset phase -K3 start/stop +Pitch View +E1 select +E2 note +E3 (turn right) increment all pitches in sequence +E3 (turn left) randomly increment each pitch in sequence + +Both Views K1 = ALT +K2 reset phase +K3 start/stop ALT-E1 = bpm +ALT+K2 = switch between trigger and pitch views ALT+K3 = randomize all voices ``` -Use the `PARAMS` menu to configure voices manually, set up clock/sync behavior, MIDI map, and manage voice presets. +Use the `PARAMS` menu to configure voices manually, set up clock/sync behavior, MIDI map, and manage voice presets. To switch between "Trigger View" and "Pitch View" press and hold down K1 and press K2 afterwards. Trigger View displays the trigger patterns for each track. Pitch View displays the note and pitch sequence for each track. + +Each track has a fixed length pitch sequence of 4. Each step will offset the current note by either 0, +5, +7, or +12 semitones. Each time a trigger occurs, it will step forward 1 step in the pitch sequence. For example, the default note is 60 which is middle C. If the offset sequence is 0, +5, 0, +12, then you will hear midi note sequence 60, 65, 60, 72. Turn E3 to the right to increment all offsets, which would result in a sequence +5, +7, +5, 0 instead. If you turn E3 to the left, then for each step there is a 50% chance the offset will increment. Press a pad on the grid (columns 12 - 15) to increment the offset of a step. Pressing K2 only resets the trigger sequencer. ## Grid Control -Tap in & clear rhythms with 1-4, nudge synth parameters up & down with 5-10. Randomize the synth voice with 15 ![Grid Control](grid-rudiments.PNG) +Tap in & clear rhythms with 1-4, nudge synth parameters up & down with 5-11, set pitch offset sequence with 12-15, randomize the synth voice with 16. + +Column Pads (left to right) +1. Clear all triggers +2. Increase density by 1 +3. Set length to 1 +4. Increase length by 1 +5. Decrease pitch by 1 note +6. Increase pitch by 1 note +7. Decrease envelope time +8. Increase envelope time +9. Decrease LFO time, behavior varies per synth +10. Increase LFO time, behavior varies per synth +11. Switch (ON/OFF), behavior varies per synth +12. Pitch sequence step 1 +13. Pitch sequence step 2 +14. Pitch sequence step 3 +15. Pitch sequence step 4 +16. Randomizes synth voice, behavior varies per synth + +## nb synth controls + +To turn off the internal supercollider engine Rudiments, find the parameter internal_ON_OFF and set it to 0 (default 1, ON). To use nb synths, the library nb is required as well as the nb mods for each synth you wish to use. + +In the parameter menu, there should be "nb_1" through "nb_8". There is an nb voice selector for each track in Rudiments. Multiple tracks can select the same nb voice. For example, multiple tracks can select doubledecker to play polyphonically. Or multiple tracks can select emplaitress 1 to combine multiple tracks into a more complicated monophonic sequence. + +Here are specific grid column behaviors (columns 9, 10, 11, 16) for each supported nb voice. + +### [nb_drumcrow](https://github.com/entzmingerc/nb_drumcrow) +9. Decrease pulse width (-0.05) +10. Increase pulse width (+0.05) (pw behavior changes for each drumcrow synth model) +11. ON/OFF: bit, bit lfo, and amplitude envelope bit +16. Randomizes env decay, pulse width, pulse width 2, and midi note of track + +### [emplaitress](https://github.com/sixolet/emplaitress) +9. Decrease harmonics (-0.05) +10. Increase harmonics (+0.05) +11. Set FM mod to 0.9 (ON) or 0 (OFF) +16. Randomizes decay, harmonics, timbre, morph, and midi note of track + +### [oilcan](https://github.com/zjb-s/oilcan) +9. Decrease modulation release for all 7 timbres (x * 0.9) +10. Increase modulation release for all 7 timbres (x * 1.1) +11. +1 midi note (ON) +0 midi note (OFF) +16. Randomizes frequency, sweep time, sweep index, env release, modulation release, modulation level, modulation ratio, feedback, and fold for all 7 timbres + +Note: Midi note selects which of the 7 "timbres" is triggered! This value wraps around every 7 notes. Try using multiple rudiments rows to trigger the same Oilcan voice with each track at different pitches. The default midi note 60 selects timbre 4. Use column 11 switch for variation of selected timbres (+1 or +0). Use the pitch sequencer to vary the timbre sequencing. + +### [nb_rudiments](https://github.com/entzmingerc/nb_rudiments) +9. Decrease lfoFreq and lfoSweep parameters (x * 0.9) +10. Increase lfoFreq and lfoSweep parameters (x * 1.1) +11. Sets osc shape to square (ON) or sine (OFF) +16. Randomizes all parameters except osc shape and gain + +### [doubledecker](https://github.com/sixolet/doubledecker) +9. Decrease brilliance (-0.05) +10. Increase brilliance (+0.05) +11. EACH PRESS will increment the layer 2 pitch ratio value and wrap through 9 values +16. Randomizes brilliance, amp release, portomento, and low and high pass filter parameters + ## SuperCollider Engine -This script makes a new SuperCollider engine available, `Rudiments`. Please see `lib/engine_rudiments.sc` for the latest parameter definitions. +This script makes a new SuperCollider engine available, `Rudiments`. Please see `lib/engine_rudiments.sc` for the latest parameter definitions. The SuperCollider engine Rudiments was ported as an nb voice called [nb_rudiments](https://github.com/entzmingerc/nb_rudiments). Please refer to [nb](https://llllllll.co/t/n-b-et-al-v0-1/60374) if you'd like to add specific support for an nb voice. ## Thanks -Thanks to [@rbxbx](http://github.com/rbxbx) for porting the playfair sequencer to this engine. +Thanks to [@cfdrake](https://github.com/cfdrake) for creating rudiments +Thanks to [@rbxbx](http://github.com/rbxbx) for porting the playfair sequencer to this engine +Thanks to [@tehn](https://llllllll.co/u/tehn/summary) for writing playfair +Thanks to [@yaw](https://llllllll.co/u/yaw/summary) for adding grid controls +Thanks to [@sixolet](https://llllllll.co/u/sixolet/summary) for writing nb +Thanks to [@postsolarpunk](https://llllllll.co/u/postsolarpunk/summary) for adding pitch view, grid controls, nb support diff --git a/grid-rudiments.PNG b/grid-rudiments.PNG index 27d7c69..38d4879 100644 Binary files a/grid-rudiments.PNG and b/grid-rudiments.PNG differ diff --git a/lib/nb/README.md b/lib/nb/README.md new file mode 100644 index 0000000..a9d0b13 --- /dev/null +++ b/lib/nb/README.md @@ -0,0 +1,42 @@ +# Nota Bene + +A norns library that plays notes well. + +The goal of this is to reduce the work of supporting a lot of different kinds of voices, and to allow people to install the voices they want to play with individually, as mods. You can also package voices with your script. + + +To use the `nb` library in your script, do: +``` +git submodule add https://github.com/sixolet/nb.git lib/nb +``` + +This will add `nb` as a *submodule*. You can then update it to the latest version by using + +``` +git submodule update --remote +git commit -m 'update submodules' +``` + +Then you can edit your script to use the `nb` library. Some highlights: + +``` +nb:init() -- run this first, from your init method. +``` + +``` +nb:add_param("voice_id", "voice") -- adds a voice selector param to your script. +nb:add_player_params() -- Adds the parameters for the selected voices to your script. +``` + +``` +-- Grab the chosen voice's player off your param +local player = params:lookup_param("voice_id"):get_player() +-- Play a note at velocity 0.5 for 0.2 beats (according to the norns clock) +player:play_note(48, 0.5, 0.2) +-- You can also use note_on and note_off methods +player:note_on(48, 0.5) +--- time elapses... +player:note_off(48) +``` + +MIDI devices that are currently connected while the script is started will be available for selection as vocies. Other vocies depend on any voices included with the script or installed as mods. diff --git a/lib/nb/lib/nb.lua b/lib/nb/lib/nb.lua new file mode 100644 index 0000000..a07b6c3 --- /dev/null +++ b/lib/nb/lib/nb.lua @@ -0,0 +1,196 @@ +local mydir = debug.getinfo(1).source:match("@?" .. _path.code .. "(.*/)") +local player_lib = include(mydir .. "player") +local nb = {} + +if note_players == nil then + note_players = {} +end + +-- note_players is a global that you can add note-players to from anywhere before +-- you call nb:add_param. +nb.players = note_players -- alias the global here. Helps with standalone use. + +nb.none = player_lib:new() + +-- Set this before init() to affect the number of voices added for some mods. +nb.voice_count = 1 + +local abbreviate = function(s) + if string.len(s) < 8 then return s end + local acronym = util.acronym(s) + if string.len(acronym) > 3 then return acronym end + return string.sub(s, 1, 8) +end + +local function add_midi_players() + for i, v in ipairs(midi.vports) do + for j = 1, nb.voice_count do + (function(i, j) + if v.connected then + local conn = midi.connect(i) + local player = { + conn = conn + } + function player:add_params() + params:add_group("midi_voice_" .. i .. '_' .. j, "midi "..j..": " .. abbreviate(v.name), 2) + params:add_number("midi_chan_" .. i .. '_' .. j, "channel", 1, 16, 1) + params:add_number("midi_modulation_cc_" .. i .. '_' .. j, "modulation cc", 1, 127, 72) + params:hide("midi_voice_" .. i .. '_' .. j) + end + + function player:ch() + return params:get("midi_chan_" .. i .. '_' .. j) + end + + function player:note_on(note, vel) + self.conn:note_on(note, util.clamp(math.floor(127 * vel), 0, 127), self:ch()) + end + + function player:note_off(note) + self.conn:note_off(note, 0, self:ch()) + end + + function player:active() + params:show("midi_voice_" .. i .. '_' .. j) + _menu.rebuild_params() + end + + function player:inactive() + params:hide("midi_voice_" .. i .. '_' .. j) + _menu.rebuild_params() + end + + function player:modulate(val) + self.conn:cc(params:get("midi_modulation_cc_" .. i.. '_' .. j), util.clamp(math.floor(127 * val), 0, 127), + self:ch()) + end + + function player:describe() + local mod_d = "cc" + if params.lookup["midi_modulation_cc_" .. i .. '_' .. j] ~= nil then + mod_d = "cc " .. params:get("midi_modulation_cc_" .. i .. '_' .. j) + end + return { + name = "v.name", + supports_bend = false, + supports_slew = false, + modulate_description = mod_d + } + end + + nb.players["midi: " .. abbreviate(v.name) .. " " .. j] = player + end + end)(i, j) + end + end +end + +-- Call from your init method. +function nb:init() + nb_player_refcounts = {} + add_midi_players() + self:stop_all() +end + +-- Add a voice select parameter. Returns the parameter. You can then call +-- `get_player()` on the parameter object, which will return a player you can +-- use to play notes and stuff. +function nb:add_param(param_id, param_name) + local names = {} + for name, _ in pairs(note_players) do + table.insert(names, name) + end + table.sort(names) + table.insert(names, 1, "none") + local names_inverted = tab.invert(names) + params:add_option(param_id, param_name, names, 1) + local string_param_id = param_id .. "_hidden_string" + params:add_text(string_param_id, "_hidden string", "") + params:hide(string_param_id) + local p = params:lookup_param(param_id) + local initialized = false + function p:get_player() + local name = params:get(string_param_id) + if name == "none" then + if p.player ~= nil then + p.player:count_down() + end + p.player = nil + return nb.none + elseif p.player ~= nil and p.player.name == name then + return p.player + else + if p.player ~= nil then + p.player:count_down() + end + local ret = player_lib:new(nb.players[name]) + ret.name = name + p.player = ret + ret:count_up() + return ret + end + end + + clock.run(function() + clock.sleep(1) + p:get_player() + initialized = true + end, p) + params:set_action(string_param_id, function(name_param) + local i = names_inverted[params:get(string_param_id)] + if i ~= nil then + -- silently set the interface param. + params:set(param_id, i, true) + end + p:get_player() + end) + params:set_action(param_id, function() + if not initialized then return end + local i = p:get() + params:set(string_param_id, names[i]) + end) +end + +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function() -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + +function nb:add_player_params() + if params.lookup['nb_sentinel_param'] then + return + end + for name, player in pairsByKeys(self:get_players()) do + player:add_params() + end + params:add_binary('nb_sentinel_param', 'nb_sentinel_param') + params:hide('nb_sentinel_param') +end + +-- Return all the players in an object by name. +function nb:get_players() + local ret = {} + for k, v in pairs(self.players) do + ret[k] = player_lib:new(v) + end + table.sort(ret) + return ret +end + +-- Stop all voices. Call when you load a pset to avoid stuck notes. +function nb:stop_all() + for _, player in pairs(self:get_players()) do + player:stop_all() + end +end + +return nb diff --git a/lib/nb/lib/player.lua b/lib/nb/lib/player.lua new file mode 100644 index 0000000..1ff3b1d --- /dev/null +++ b/lib/nb/lib/player.lua @@ -0,0 +1,137 @@ +local player = { + length = 1.0 +} + +if nb_player_refcounts == nil then + nb_player_refcounts = {} +end + +-- Wrap an object to be a full "player", with default implementations of all +-- the player methods +function player:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +-- Implement this to add params from your player to the script +function player:add_params() +end + +-- Implement to do midi-style note-on. If you only implement play_note, +-- you should implement a trivial note_on to call it. +-- "properties" is an optional table of note mod properties. It should +-- contain only keys that the vocie says it can modulate in note_mod_targets +-- in the description. +function player:note_on(note, vel, properties) +end + +-- Implement to do midi-style note-off. This is optional if you implement +-- play_note instead. +function player:note_off(note) +end + +-- Optional. Send pitch bend to the voice. If the voice doesn't support +-- per-note pitch bend, may bend all notes. Amount is given in semitones. +function player:pitch_bend(note, amount) +end + +-- Optional. Modulate the voice, in whatever way seems best. Range 0-1. +function player:modulate(val) +end + +-- Optional. Set the slew time for the voice. +function player:set_slew(slew) +end + +-- Optional. Modulate the note. `key` should be present in note_mod_targets +-- in the description. `value` should be between -1 and 1. +-- Depending on the parameter, the voice may interperet modulation as either +-- additive to, multiplicative of, or replacing any timbral properties. +-- For example, modulating `amp` should multiply with the `amp` in properties, +-- but modulating a filter cutoff should be additive. +function player:modulate_note(note, key, value) +end + +-- Recommended. Describe the voice's capabilities. +-- Supported keys: +-- supports_bend (does this voice support pitch bend?) +-- supports_slew (does this voice support set_slew?) +-- note_mod_targets (optional list of targets for "note mod") +function player:describe() + return { + name = "none", + supports_bend = false, + supports_slew = false, + modulate_description = "unsupported", + } +end + +-- Optional. Callback for when a voice is used by at least one selector. +-- Suggest using it to show parameters to control the voice. +function player:active() + self.is_active = true + self.active_routine = clock.run(function() + clock.sleep(1) + if self.is_active then + self:delayed_active() + end + self.active_routine = nil + end) +end + +-- Optional. Callback for when a voice is slected for more than one second. +-- This is where you want to change modes on external devices or whatever. +function player:delayed_active() +end + +-- Optional. Callback for when a voice is no longer used. Useful for hiding +-- parameters or whatnot. +function player:inactive() + self.is_active = false + if self.active_routine ~= nil then + clock.cancel(self.active_routine) + end +end + +-- Stop all voices. Use on pset load. +function player:stop_all() +end + +-- Play a note for a given length. `properties` is optional, see note_on +-- for further description. +function player:play_note(note, vel, length, properties) + self:note_on(note, vel, properties) + clock.run(function() + clock.sleep(length*clock.get_beat_sec()) + self:note_off(note) + end) +end + +-- Private. +function player:count_up() + if self.name ~= nil then + if nb_player_refcounts[self.name] == nil then + nb_player_refcounts[self.name] = 1 + self:active() + else + nb_player_refcounts[self.name] = nb_player_refcounts[self.name] + 1 + end + end +end + +-- Private +function player:count_down() + if self.name ~= nil then + if nb_player_refcounts[self.name] ~= nil then + nb_player_refcounts[self.name] = nb_player_refcounts[self.name] - 1 + if nb_player_refcounts[self.name] == 0 then + nb_player_refcounts[self.name] = nil + self:inactive() + end + end + end +end + +return player \ No newline at end of file diff --git a/lib/nb/nb.lua b/lib/nb/nb.lua new file mode 100644 index 0000000..473e7b1 --- /dev/null +++ b/lib/nb/nb.lua @@ -0,0 +1,49 @@ +local nb = require "nb/lib/nb" + +g = grid.connect() + +grid_dirty = true + +function init() + nb:init() + nb:add_param("voice", "voice") + nb:add_player_params() + clock.run(function() + while true do + if grid_dirty then + grid_redraw() + end + clock.sync(1/32) + end + end) +end + +function g.key(x, y, z) + local note = 24 + note = note + x + note = note + 5 * (8 - y) + local player = params:lookup_param("voice"):get_player() + if z == 1 then + player:note_on(note, 1) + else + player:note_off(note) + end +end + +function grid_redraw() + g:all(0) + for x=1,16,1 do + for y=1,8,1 do + local note = 24 + note = note + x + note = note + 5 * (8 - y) + local hs = note % 12 + if hs == 0 then + g:led(x, y, 6) + elseif hs == 2 or hs == 4 or hs == 5 or hs == 7 or hs == 9 or hs == 11 then + g:led(x, y, 3) + end + end + end + g:refresh() +end \ No newline at end of file diff --git a/rudiments grid update.pdf b/rudiments grid update.pdf new file mode 100644 index 0000000..4563b96 Binary files /dev/null and b/rudiments grid update.pdf differ diff --git a/rudiments.lua b/rudiments.lua index 19519f8..ff69b7e 100644 --- a/rudiments.lua +++ b/rudiments.lua @@ -14,45 +14,115 @@ -- ALT+K3 = randomize all voices engine.name = "Rudiments" - +nb = include("lib/nb/lib/nb") +mutil = require 'musicutil' +seq = require 'sequins' local last = 0 +rudiments_track_count = 8 +local pitch_step_count = 4 +local BeatClock = require 'beatclock' +local clk = BeatClock.new() +local all_midi = midi.connect() +local pitch_view_enable = false +last_note = {0, 0, 0, 0, 0, 0, 0, 0} +local note_trig_flag = {false, false, false, false, false, false, false, false} +key1_hold = false -local voice_count = 8 +-- grid initialization +col_11_flag = 0 +g = grid.connect() + +g:refresh() +for i=1,8 do + g:led(1,i,1) + g:led(2,i,1) + g:led(3,i,6) + g:led(4,i,6) + g:led(5,i,1) + g:led(6,i,1) + g:led(7,i,6) + g:led(8,i,6) + g:led(9,i,1) + g:led(10,i,1) + g:led(11,i,1) + g:led(12,i,0) + g:led(13,i,0) + g:led(14,i,0) + g:led(15,i,0) + g:led(16,i,2) +end -local accents = 1 +grid_LED_seq = {} +track_pitch_seq = {} +track_pitch_pos = {} -local BeatClock = require 'beatclock' +for i = 1, rudiments_track_count do + track_pitch_pos[i] = seq{12, 13, 14, 15} -- grid position in pitch sequence +end -local clk = BeatClock.new() -local all_midi = midi.connect() +-- sequencer initialization +er = require 'er' -- euclidean rhythm! +local reset = false +local running = true +local track_edit = 1 +local current_pattern = 0 +local current_pset = 0 +-- let's prebake some strings so we don't have to spend time concatenating them +t_density = {"density1", "density2", "density3", "density4", "density5", "density6", "density7", "density8"} +t_length = {"length1", "length2", "length3", "length4", "length5", "length6", "length7", "length8"} +t_pitch = {"pitch1", "pitch2", "pitch3", "pitch4", "pitch5", "pitch6", "pitch7", "pitch8"} +nb_voices = {"nb_1", "nb_2", "nb_3", "nb_4","nb_5", "nb_6", "nb_7", "nb_8"} +track = {} +pitch_offset_number = 0 + +for i=1,rudiments_track_count do + -- TODO: I've replaced track[i].k and track[i].n with density and length params accessible via norns menu + -- I need to replace the rest of this, it used to have k = 0 and n = 9 - i in this table + track[i] = { + pos = 1, -- position in trigger sequence + s = {} -- trigger TRUE or FALSE + } +end +-- functions +function density_check(i) + params:set(t_density[i], util.clamp(params:get(t_density[i]), 0, params:get(t_length[i])) ) + reer(i) + redraw() +end function setup_params() - for i = 1,voice_count do + for i = 1,rudiments_track_count do -- OSC - params:add_separator() - params:add_control("shape" .. i, "osc " .. i .. " shape", controlspec.new(0, 1, 'lin', 1, 0, '')) - params:set_action("shape" .. i, function(x) engine.shape(x, i) end) - - params:add_control("freq" .. i, "osc " .. i .. " freq", controlspec.new(20, 10000, 'lin', 1, 120, 'hz')) - params:set_action("freq" .. i, function(x) engine.freq(x, i) end) + params:add_separator("Engine " .. i, i) + + -- Length, Density, Pitch Params + params:add_number(t_length[i], "length " .. i, 1, 32, rudiments_track_count - i + 1) + params:add_number(t_density[i], "density " .. i, 0, 32, 0) + params:add_number(t_pitch[i], "pitch ".. i, 0, 127, 60) + params:set_action(t_length[i], function(x) + density_check(i) + end) + params:set_action(t_density[i], function(x) + density_check(i) + end) + + params:add_option("shape" .. i, "osc " .. i .. " shape", {"square", "triangle"}) + params:set_action("shape" .. i, function(x) engine.shape(x-1, i) end) + -- params:add_control("freq" .. i, "osc " .. i .. " freq", controlspec.new(20, 10000, 'lin', 1, 120, 'hz')) + -- params:set_action("freq" .. i, function(x) engine.freq(x, i) end) -- old freq param, using midi notes instead now -- ENV params:add_control("decay" .. i, "env " .. i .. " decay", controlspec.new(0.05, 1, 'lin', 0.01, 0.2, 'sec')) params:set_action("decay" .. i, function(x) engine.decay(x, i) end) - params:add_control("sweep" .. i, "env " .. i .. " sweep", controlspec.new(0, 2000, 'lin', 1, 100, '')) params:set_action("sweep" .. i, function(x) engine.sweep(x, i) end) - -- TODO: Sweep direction sounds a little wonky right now... - -- LFO params:add_control("lfoFreq" .. i, "lfo " .. i .. " freq", controlspec.new(1, 1000, 'lin', 1, 1, 'hz')) params:set_action("lfoFreq" .. i, function(x) engine.lfoFreq(x, i) end) - - params:add_control("lfoShape" .. i, "lfo " .. i .. " shape", controlspec.new(0, 1, 'lin', 1, 0, '')) - params:set_action("lfoShape" .. i, function(x) engine.lfoShape(x, i) end) - + params:add_option("lfoShape" .. i, "lfo " .. i .. " shape", {"square", "triangle"}) + params:set_action("lfoShape" .. i, function(x) engine.lfoShape(x-1, i) end) params:add_control("lfoSweep" .. i, "lfo " .. i .. " sweep", controlspec.new(0, 2000, 'lin', 1, 0, '')) params:set_action("lfoSweep" .. i, function(x) engine.lfoSweep(x, i) end) end @@ -64,121 +134,390 @@ function setup_midi() end end -function trigger(i) - last = i - engine.trigger(i) -end + function randomize() - for i = 1,voice_count do - params:set("shape" .. i, math.random(0, 1)) - params:set("freq" .. i, math.random(20, 10000)) + for i = 1,rudiments_track_count do + params:set(t_pitch[i], math.floor(math.random()*80+20)) + params:set("shape" .. i, math.random(1, 2)) params:set("decay" .. i, math.random()) params:set("sweep" .. i, math.random(0, 2000)) params:set("lfoFreq" .. i, math.random(1, 1000)) - params:set("lfoShape" .. i, math.random(0, 1)) + params:set("lfoShape" .. i, math.random(1, 2)) params:set("lfoSweep" .. i, math.random(0, 2000)) end end --- grid section - -g = grid.connect() - --- show mapped buttons --- TODO: some sort of visual feedback -for i=1,8 do - - g:led(1,i,1) - g:led(2,i,1) - g:led(3,i,3) - g:led(4,i,3) - g:led(5,i,1) - g:led(6,i,1) - g:led(7,i,3) - g:led(8,i,3) - g:led(9,i,5) - g:led(10,i,5) - - g:led(15,i,2) - -end -g:refresh() - - g.key = function(x,y,z) if z == 1 then -- track operations if x == 1 then - -- CLEAR TRACK - track[y].k = 0 - reer(y) - redraw() + -- ZERO DENSITY + params:set(t_density[y], 0) + density_check(y) + elseif x == 2 then -- MORE DENSITY - track[y].k = util.clamp(track[y].k+1,0,track[y].n) - reer(y) - redraw() + params:set(t_density[y], params:get(t_density[y]) + 1) -- add 1 + density_check(y) elseif x == 3 then - -- LESS TRACK LENGTH - track[y].n = 1 - -- track[y].n = util.clamp(track[y].n-1,1,32) - track[y].k = util.clamp(track[y].k,0,track[y].n) - reer(y) - redraw() + -- TRACK LENGTH SET TO 0 + params:set(t_length[y], 1) + params:set(t_density[y], util.clamp(params:get(t_density[y]), 0, params:get(t_length[y]))) + density_check(y) + elseif x == 4 then -- MORE TRACK LENGTH - track[y].n = util.clamp(track[y].n+1,1,32) - track[y].k = util.clamp(track[y].k,0,track[y].n) - reer(y) - redraw() + params:set(t_length[y], util.clamp(params:get(t_length[y])+1, 1, 32)) + density_check(y) -- synth ops elseif x == 5 then -- OSC LOWER - params:set("freq" .. y, util.clamp(params:get("freq" .. y)*0.9,20,10000)) + params:set(t_pitch[y], util.clamp(params:get(t_pitch[y]) - 1, 0, 127)) + elseif x == 6 then -- OSC HIGHER - params:set("freq" .. y, util.clamp(params:get("freq" .. y)*1.1,20,10000)) + params:set(t_pitch[y], util.clamp(params:get(t_pitch[y]) + 1, 0, 127)) elseif x == 7 then - -- ENV DECAY + -- ENV DECAY LOWER params:set("decay" .. y, util.clamp(params:get("decay" .. y)*0.9,0.01,1)) params:set("sweep" .. y, math.random(0,2000)) + + -- drumcrow = "amp_cycle" + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + p_name = "drumcrow_amp_cycle_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*1.15, 0.1, 500)) + + -- emplaitress = "decay" + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + p_name = "plaits_decay_"..p_num + params:set(p_name, util.clamp(params:get(p_name)-0.05, 0, 1)) + + -- nb_rudiments, "rudiments_decay" + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) + p_name = "rudiments_decay_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*0.9,0.05,1)) + + -- oilcan, "oilcan carrier release" + elseif string.find(desc.name, "Oilcan") then + p_num = string.sub(params:string(nb_voices[y]), 8, 8) -- uhhh oilcan name isn't numbered for some reason, get the string directly from nb param + for timbre = 1,7 do + p_name = "oilcan_car_rel_"..timbre.."_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*0.9,0.01,3)) + end + + -- doubledecker, "doubledecker_amp_release_1" "doubledecker_amp_release_2" + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_amp_release_1" + params:set(p_name, util.clamp(params:get(p_name)*0.9, 0, 8)) + p_name = "doubledecker_amp_release_2" + params:set(p_name, util.clamp(params:get(p_name)*0.9, 0, 8)) + + end + elseif x == 8 then - -- ENV DECAY + -- ENV DECAY HIGHER params:set("decay" .. y, util.clamp(params:get("decay" .. y)*1.1,0.01,1)) params:set("sweep" .. y, math.random(0,2000)) + -- drumcrow = "amp_cycle" + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + p_name = "drumcrow_amp_cycle_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*0.85, 0, 500)) + + -- emplaitress = "decay" + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + p_name = "plaits_decay_"..p_num + params:set(p_name, util.clamp(params:get(p_name)+0.05, 0, 1)) + + -- nb_rudiments, "rudiments_decay" + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) + p_name = "rudiments_decay_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*1.1,0.05,1)) + + -- oilcan, "oilcan carrier release" + elseif string.find(desc.name, "Oilcan") then + p_num = string.sub(params:string(nb_voices[y]), 8, 8) -- uhhh oilcan name isn't numbered for some reason, get the string directly from nb param + for timbre = 1,7 do + p_name = "oilcan_car_rel_"..timbre.."_" .. p_num + params:set(p_name, util.clamp(params:get(p_name)*1.1,0.01,3)) + end + + -- doubledecker, "doubledecker_amp_release_1" "doubledecker_amp_release_2" + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_amp_release_1" + params:set(p_name, util.clamp(params:get(p_name)*1.1, 0, 8)) + p_name = "doubledecker_amp_release_2" + params:set(p_name, util.clamp(params:get(p_name)*1.1, 0, 8)) + + end elseif x == 9 then - -- LFO FREQ + -- LFO FREQ LOWER params:set("lfoFreq" .. y, util.clamp(params:get("lfoFreq" .. y)*0.95,1,1000)) - -- params:set("lfoShape" .. y, math.random(0, 1)) params:set("lfoSweep" .. y, math.random(0,2000)) + + -- drumcrow = "pw" + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + p_name = "drumcrow_pw_"..p_num + params:set(p_name, util.clamp(params:get(p_name)-0.05, -1, 1)) + + -- emplaitress = "harmonics" + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + p_name = "plaits_harmonics_"..p_num + params:set(p_name, util.clamp(params:get(p_name)-0.05, 0, 1)) + + -- nb_rudiments + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) + p_name = "rudiments_lfoFreq_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*0.9,1,1000)) + p_name = "rudiments_lfoSweep_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*0.9,1,2000)) + + -- oilcan, modulation release decrease + elseif string.find(desc.name, "Oilcan") then + p_num = string.sub(params:string(nb_voices[y]), 8, 8) + for timbre = 1,7 do + p_name = "oilcan_mod_rel_"..timbre.."_" .. p_num + params:set(p_name, util.clamp(params:get(p_name)*0.9,0.1,200)) + end + + -- doubledecker, "doubledecker_brilliance" + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_brilliance" + params:set(p_name, util.clamp(params:get(p_name)-0.05, -1, 1)) + + + end elseif x == 10 then - -- LFO FREQ + -- LFO FREQ HIGHER params:set("lfoFreq" .. y, util.clamp(params:get("lfoFreq" .. y)*1.05,1,1000)) - -- params:set("lfoShape" .. y, math.random(0, 1)) params:set("lfoSweep" .. y, math.random(0,2000)) + -- drumcrow = "pw" + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + p_name = "drumcrow_pw_"..p_num + params:set(p_name, util.clamp(params:get(p_name)+0.05, -1, 1)) + + -- emplaitress = "harmonics" + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + p_name = "plaits_harmonics_"..p_num + params:set(p_name, util.clamp(params:get(p_name)+0.05, 0, 1)) + + -- nb_rudiments, increase both lfoFreq and lfoShape + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) + p_name = "rudiments_lfoFreq_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*1.1,1,1000)) + p_name = "rudiments_lfoSweep_"..p_num + params:set(p_name, util.clamp(params:get(p_name)*1.1,1,2000)) + + -- oilcan, modulation release increase + elseif string.find(desc.name, "Oilcan") then + p_num = string.sub(params:string(nb_voices[y]), 8, 8) + for timbre = 1,7 do -- there are 7 timbres for oilcan + p_name = "oilcan_mod_rel_"..timbre.."_" .. p_num + params:set(p_name, util.clamp(params:get(p_name)*1.1,0.1,200)) + end + + -- doubledecker, "doubledecker_brilliance" + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_brilliance" + params:set(p_name, util.clamp(params:get(p_name)+0.05, -1, 1)) - elseif x == 15 then - -- RANDOMIZE ALL - params:set("shape" .. y, math.random(0, 1)) - params:set("freq" .. y, math.random(20, 10000)) - params:set("decay" .. y, math.random()) - params:set("sweep" .. y, math.random(0, 2000)) - params:set("lfoFreq" .. y, math.random(1, 1000)) - params:set("lfoShape" .. y, math.random(0, 1)) - params:set("lfoSweep" .. y, math.random(0, 2000)) - + end + elseif x == 11 then + -- ON/OFF Switch, unique behavior for each nb voice + + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + if col_11_flag == 0 then + -- drumcrow = "pw2" = random + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + params:set("drumcrow_bit_"..p_num, 1) + params:set("drumcrow_lfo_bit_"..p_num, 1.7) + params:set("drumcrow_amp_bit_"..p_num, 1) + + -- emplaitress = "fm_mod" = 1 + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + params:set("plaits_fm_mod_"..p_num, 0.9) + + -- nb_rudiments, set osc shape to 1 + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) -- length of name + 2 + params:set("rudiments_shape_" .. p_num, 1) + + -- oilcan, add 1 to note, midi note selects timbre on oilcan + elseif string.find(desc.name, "Oilcan") then + if params:get(t_pitch[y]) <= 126 and params:get(t_pitch[y]) >= 0 then -- max midi note 126 to add 1 to, min midi note is 0 + params:set(t_pitch[y], params:get(t_pitch[y]) + 1) + end + + -- doubledecker, "pitch ratio 2" increment with wrapping + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_pitch_ratio_2" + params:set(p_name, util.wrap(params:get(p_name)+1, 1, 9)) + + end + + col_11_flag = 1 + g:led(x,y,15) + g:refresh() + else + -- drumcrow = "pw2" = 0 + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) + params:set("drumcrow_bit_"..p_num, 0) + params:set("drumcrow_lfo_bit_"..p_num, 0) + params:set("drumcrow_amp_bit_"..p_num, 0) + + -- emplaitress = "fm_mod" = 0 + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) + params:set("plaits_fm_mod_"..p_num, 0) + + -- nb_rudiments, set osc shape to 0 + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) -- length of name + 2 + params:set("rudiments_shape_" .. p_num, 0) + + -- oilcan, subtract 1 from note, midi note selects timbre on oilcan + elseif string.find(desc.name, "Oilcan") then + if params:get(t_pitch[y]) <= 127 and params:get(t_pitch[y]) >= 1 then -- max midi note 127, min midi note to sub from is 1 + params:set(t_pitch[y], params:get(t_pitch[y]) - 1) + end + + -- doubledecker, "pitch ratio 2" increment with wrapping + elseif string.find(desc.name, "doubledecker") then + p_name = "doubledecker_pitch_ratio_2" + params:set(p_name, util.wrap(params:get(p_name)+1, 1, 9)) + + end + + col_11_flag = 0 + g:led(x,y,1) + g:refresh() + end + elseif x == 12 or x == 13 or x == 14 or x == 15 then + g:led(x, y, grid_LED_seq[x][y]()) -- inc LED brightness + track_pitch_seq[x][y]() -- inc pitch offset for square + g:refresh() + elseif x == 16 then + -- RANDOMIZE ALL + if params:get("internal_ON_OFF") == 2 then + params:set(t_pitch[y], math.floor(math.random()*80+20)) + params:set("shape" .. y, math.random(1, 2)) + params:set("decay" .. y, math.random()) + params:set("sweep" .. y, math.random(0, 2000)) + params:set("lfoFreq" .. y, math.random(1, 1000)) + params:set("lfoShape" .. y, math.random(1, 2)) + params:set("lfoSweep" .. y, math.random(0, 2000)) + end + + local player = params:lookup_param(nb_voices[y]):get_player() + desc = player:describe() + + -- drumcrow + if string.find(desc.name, "drumcrow") then + p_num = string.sub(desc.name, 10, 10) -- length of name + 2 + params:set("drumcrow_amp_cycle_"..p_num, math.random() * 35 + 0.1) + params:set("drumcrow_pw_"..p_num, math.random() * 2 - 1) + params:set("drumcrow_pw2_"..p_num, math.random() * 20 - 10) + -- randomize nb midi note + params:set(t_pitch[y], math.floor(math.random()*80+20)) + + -- emplaitress + elseif string.find(desc.name, "emplait") then + p_num = string.sub(desc.name, 9, 9) -- length of name + 2 + params:set("plaits_decay_"..p_num, math.random()*0.8) + params:set("plaits_harmonics_"..p_num, math.random()*0.8) + params:set("plaits_timbre_"..p_num, math.random()*0.8) + params:set("plaits_morph_"..p_num, math.random()*0.8) + -- randomize nb midi note + params:set(t_pitch[y], math.floor(math.random()*80+20)) + + -- nb_rudiments + elseif string.find(desc.name, "rudiments") then + p_num = string.sub(desc.name, 11, 11) -- length of name + 2 + -- everything except osc shape, set that with col 11 + params:set("rudiments_freq_" .. p_num, math.random(20, 10000)) + params:set("rudiments_decay_" .. p_num, math.random()) + params:set("rudiments_sweep_" .. p_num, math.random(0, 2000)) + params:set("rudiments_lfoFreq_" .. p_num, math.random(1, 1000)) + params:set("rudiments_lfoShape_" .. p_num, math.random(0, 1)) + params:set("rudiments_lfoSweep_" .. p_num, math.random(0, 2000)) + -- randomize nb midi note + params:set(t_pitch[y], math.floor(math.random()*80+20)) + + -- oilcan + elseif string.find(desc.name, "Oilcan") then + p_num = string.sub(params:string(nb_voices[y]), 8, 8) + -- each player of oilcan has 7 timbres + -- "oilcan_mod_ratio_1_3" example of parameter name where 1 is timbre (1-7) 3 is player (1-4?) + -- I think randomize all timbres sounds like a plan + for timbre = 1,7 do + params:set("oilcan_freq_"..timbre.."_" .. p_num, 600*math.random()^2 + 5) -- freq + params:set("oilcan_sweep_time_"..timbre.."_" .. p_num, math.random()*2.5) -- sweep time + params:set("oilcan_sweep_ix_"..timbre.."_" .. p_num, math.random()*0.6 - 0.3) -- sweep index + params:set("oilcan_car_rel_"..timbre.."_" .. p_num, math.random()) -- carrier release + params:set("oilcan_mod_rel_"..timbre.."_" .. p_num, math.random()*100) -- modulator release + params:set("oilcan_mod_ix_"..timbre.."_" .. p_num, math.random()*0.25) -- modulator level + params:set("oilcan_mod_ratio_"..timbre.."_" .. p_num, 10*math.random()^2) -- modulator ratio + params:set("oilcan_fb_"..timbre.."_" .. p_num, math.random()*1.5) -- feedback + params:set("oilcan_fold_"..timbre.."_" .. p_num, math.random()^2*15) -- fold + end + + -- doubledecker + elseif string.find(desc.name, "doubledecker") then + params:set("doubledecker_brilliance", math.random()*2 - 1) + params:set("doubledecker_amp_release_1", math.random()*3) + params:set("doubledecker_amp_release_2", math.random()*3) + params:set("doubledecker_portomento", math.random()*0.25) + params:set("doubledecker_lp_freq_1", math.random()*1500 + 100) + params:set("doubledecker_lp_freq_2", math.random()*1500 + 100) + params:set("doubledecker_lp_res_1", math.random()*0.9) + params:set("doubledecker_lp_res_2", math.random()*0.9) + params:set("doubledecker_hp_freq_1", math.random()*1500 + 20) + params:set("doubledecker_hp_freq_2", math.random()*1500 + 20) + params:set("doubledecker_hp_res_1", math.random()*0.9) + params:set("doubledecker_hp_res_2", math.random()*0.9) + params:set("doubledecker_filter_init_1", math.random()*2-1) + params:set("doubledecker_filter_init_2", math.random()*2-1) + params:set("doubledecker_filter_attack_level_1", math.random()*2-1) + params:set("doubledecker_filter_attack_level_2", math.random()*2-1) + + end + g:led(x,y,math.random(1,15)) + g:refresh() end end end @@ -186,78 +525,115 @@ end -- sequencer section -er = require 'er' - -local reset = false -local running = true -local track_edit = 1 -local current_pattern = 0 -local current_pset = 0 - -track = {} -for i=1,voice_count do - track[i] = { - k = 0, - n = 9 - i, - pos = 1, - s = {} - } -end - -local pattern = {} -for i=1,112 do - pattern[i] = { - data = 0, - k = {}, - n = {} - } - for x=1,voice_count do - pattern[i].k[x] = 0 - pattern[i].n[x] = 0 - end -end - function reer(i) - if track[i].k == 0 then - for n=1,32 do track[i].s[n] = false end + if params:get(t_density[i]) == 0 then + for n=1,32 do + track[i].s[n] = false + end else - track[i].s = er.gen(track[i].k,track[i].n) + track[i].s = er.gen(params:get(t_density[i]), params:get(t_length[i])) end end local function trig() - for i=1,voice_count do + for i=1,rudiments_track_count do if track[i].s[track[i].pos] then - if accents==1 then - params:set("lfoShape" .. i, math.random(0, 1)) + if params:get("internal_accents") == 2 then + params:set("lfoShape" .. i, math.random(1, 2)) + end + if note_trig_flag[i] then + trigger_note_off(i) -- turn off previous note end trigger(i) - g:led(1,i,15) + g:led(1,i,15) -- high brightness trigger on square 1 + + for j = 1,4 do + g:led(j + 11, i, grid_LED_seq[j+11][i]:peek()) -- set grid LED values + end + g:led(track_pitch_pos[i]:peek(), i, 15) -- highlight the pitch sequence square that we're at + else - g:led(1,i,1) + if note_trig_flag[i] then + trigger_note_off(i) -- turn off previous note + end + g:led(1,i,1) -- turn off square 1 + end + g:led(16,i,math.random(1,15)) -- always randomize the random button g:refresh() + + end +end + +function trigger(i) + last = i + col = track_pitch_pos[i]() -- increment pitch sequence position on trigger + last_note[i] = params:get(t_pitch[i]) + track_pitch_seq[col][i]:peek() -- get pitch offset from grid square + if params:get("internal_ON_OFF") == 2 then + engine.freq(mutil.note_num_to_freq(last_note[i]), i) -- current freq to midi, add offset, midi to freq, set engine freq + engine.trigger(i) -- triggers internal rudiments SC engine end + -- trigger nb voice + local player = params:lookup_param(nb_voices[i]):get_player() + player:play_note(util.clamp(last_note[i], 0, 127), 5, params:get("nb_note_length")) + note_trig_flag[i] = true +end + +function trigger_note_off(i) + -- call this first to release previous note, then call trigger to increment to new note + note_trig_flag[i] = false + local player = params:lookup_param(nb_voices[i]):get_player() + player:note_off(util.clamp(last_note[i], 0, 127)) end function init() - params:add_separator() + nb.voice_count = 1 + nb:init() + nb:add_param(nb_voices[1], nb_voices[1]) + nb:add_param(nb_voices[2], nb_voices[2]) + nb:add_param(nb_voices[3], nb_voices[3]) + nb:add_param(nb_voices[4], nb_voices[4]) + nb:add_param(nb_voices[5], nb_voices[5]) + nb:add_param(nb_voices[6], nb_voices[6]) + nb:add_param(nb_voices[7], nb_voices[7]) + nb:add_param(nb_voices[8], nb_voices[8]) + nb:add_player_params() + + params:add_control("nb_note_length", "NB Note Length", controlspec.new(0, 10, 'lin', 0.1, 1, '')) + params:add_option("internal_ON_OFF", "Internal: Engine", {"OFF", "ON"}, 2) + params:add_option("internal_accents", "Internal: Accents", {"OFF", "ON"}, 2) + params:add_separator("Internal Engine", "Internal Engine") clk:add_clock_params() setup_params() setup_midi() randomize() + track_notes = {60, 60, 60, 60, 60, 60, 60, 60} -- set default note to 60 after randomize() - for i=1,voice_count do reer(i) end + for i=1,rudiments_track_count do reer(i) end screen.line_width(1) clk.on_step = step clk.on_select_internal = function() clk:start() end clk.on_select_external = reset_pattern - params:default() - clk:start() + + -- ...was having trouble initializing these arrays??? idk??? + for x = 12,15 do -- grid columns + grid_LED_seq[x] = {} + track_pitch_seq[x] = {} + end + for x = 12,15 do -- grid columns + for y = 1,8 do -- grid rows + grid_LED_seq[x][y] = seq{0, 1, 5, 9} -- grid square LED brightness + track_pitch_seq[x][y] = seq{0, 5, 7, 12} -- grid square pitch offsets + g:led(x, y, grid_LED_seq[x][y]()) + track_pitch_seq[x][y]() + g:refresh() + end + end + end function reset_pattern() @@ -267,24 +643,32 @@ end function step() if reset then - for i=1,voice_count do track[i].pos = 1 end + for i=1,rudiments_track_count do track[i].pos = 1 end reset = false else - for i=1,voice_count do track[i].pos = (track[i].pos % track[i].n) + 1 end + for i=1,rudiments_track_count do track[i].pos = (track[i].pos % params:get(t_length[i])) + 1 end end trig() redraw() end -key1_hold = false -function key(n,z) - if n==1 and z==1 then +function key(n,z) -- n is number 1-3, z is pressed: 1, released: 0 + if n==1 and z==1 then key1_hold = true elseif n==1 and z==0 then key1_hold = false - elseif n==2 and z==1 then reset_pattern() + elseif n==2 and z==1 then + if key1_hold then + if pitch_view_enable == true then + pitch_view_enable = false + else + pitch_view_enable = true + end + else + reset_pattern() + end elseif n==3 and z==1 then if key1_hold then randomize() @@ -300,48 +684,112 @@ function key(n,z) redraw() end -function enc(n,d) - if n==1 then - if key1_hold then - params:delta("bpm", d) - else - track_edit = util.clamp(track_edit+d,1,voice_count) +function enc(n,d) -- number, direction/step? + redraw_flag = true + if pitch_view_enable == false then + if n==1 then -- trigger sequencer view + if key1_hold then + params:delta("bpm", d) + else + track_edit = util.clamp(track_edit+d,1,rudiments_track_count) + end + elseif n == 2 then + params:set(t_density[track_edit], params:get(t_density[track_edit]) + d) + redraw_flag = false + elseif n==3 then + params:set(t_length[track_edit], params:get(t_length[track_edit]) + d) + params:set(t_density[track_edit], util.clamp(params:get(t_density[track_edit]), 0, params:get(t_length[track_edit]))) + redraw_flag = false + end + else -- pitch sequencer view + if n==1 then + track_edit = util.clamp(track_edit+d,1,rudiments_track_count) -- select track + elseif n == 2 then + params:set(t_pitch[track_edit], util.clamp(params:get(t_pitch[track_edit]) + d, 0, 127)) -- select pitch + elseif n == 3 then + if d < 0 then + for x = 12, 15 do + if math.random() > 0.5 then + g:led(x, track_edit, grid_LED_seq[x][track_edit]()) + track_pitch_seq[x][track_edit]() + end + end + else + for x = 12, 15 do + g:led(x, track_edit, grid_LED_seq[x][track_edit]()) + track_pitch_seq[x][track_edit]() + end + end end - elseif n == 2 then - track[track_edit].k = util.clamp(track[track_edit].k+d,0,track[track_edit].n) - elseif n==3 then - track[track_edit].n = util.clamp(track[track_edit].n+d,1,32) - track[track_edit].k = util.clamp(track[track_edit].k,0,track[track_edit].n) end - - reer(track_edit) - redraw() + + if redraw_flag == true then + reer(track_edit) + redraw() + end end function redraw() screen.aa(0) screen.clear() - for i=1,voice_count do - screen.level((i == track_edit) and 15 or 4) - screen.move(5, i*8) - screen.text_center(track[i].k) - screen.move(20,i*8) - screen.text_center(track[i].n) - - for x=1,track[i].n do - screen.level((track[i].pos==x and not reset) and 15 or 2) - screen.move(x*3 + 30, i*8) - - if track[i].s[x] then - screen.line_rel(0,-8) - else - screen.line_rel(0,-2) + if pitch_view_enable == false then -- trigger sequencer + for i=1,rudiments_track_count do + -- first two numbers and brightness + screen.level((i == track_edit) and 15 or 4) -- 0 to 15 brightness, 15 if selected with track_edit 1-8 + screen.move(5, i*8) -- move draw cursor, 8, ... , 64 + screen.text_center(params:get(t_density[i])) + screen.move(20,i*8) -- move draw cursor + screen.text_center(params:get(t_length[i])) + + -- create sequence and brightness + -- for x=1,track[i].n do + for x=1,params:get(t_length[i]) do + screen.level((track[i].pos==x and not reset) and 15 or 2) + screen.move(x*3 + 30, i*8) -- move draw cursor + + if track[i].s[x] then + screen.line_rel(0,-8) -- draw line relative to current position + else + screen.line_rel(0,-2) -- draw line relative to current position + end + + screen.stroke() -- renders all the stuff drawn end - - screen.stroke() + end + else -- pitch sequencer + for i = 1, rudiments_track_count do + -- first two numbers and brightness + screen.level((i == track_edit) and 15 or 4) -- 0 to 15 brightness, 15 if selected with track_edit 1-8 + screen.move(5, i*8) -- move draw cursor, 8, ... , 64 + screen.text_center(#track_pitch_pos[i]) -- draw text, length of pitch offset sequence + screen.move(20,i*8) -- move draw cursor + screen.text_center(params:get(t_pitch[i])) -- draw text, base note of sequence + + -- create line sequence and brightness + pitch_pos = track_pitch_pos[i]:peek() + for x = 12, 15 do + screen.level((pitch_pos == x and not reset) and 15 or 2) + screen.move((x-11)*3 + 30, i*8) -- move draw cursor + + pitch_offset = track_pitch_seq[x][i]:peek() + if pitch_offset == 0 then + screen.line_rel(0,-2) -- draw line relative to current position + elseif pitch_offset == 5 then + screen.line_rel(0,-4) + elseif pitch_offset == 7 then + screen.line_rel(0,-6) + elseif pitch_offset == 12 then + screen.line_rel(0,-8) + else + screen.line_rel(0,-2) -- shouldn't ever get here + end + + screen.stroke() -- renders all the stuff drawn + end + end end - screen.update() + screen.update() -- uhhh.... updates the screen with the new render? end