Created
October 14, 2023 18:29
-
-
Save sonocircuit/ed4d5156cf091c843e8e81ec58a98440 to your computer and use it in GitHub Desktop.
norns reflection lib edits
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
| --- clocked pattern recorder library | |
| -- @module lib.reflection | |
| local reflection = {} | |
| reflection.__index = reflection | |
| --- constructor | |
| function reflection.new() | |
| local p = {} | |
| setmetatable(p, reflection) | |
| p.rec = 0 | |
| p.rec_enabled = 0 -- NEW: I still think this is good to have for allowing visual feedback. | |
| p.play = 0 | |
| p.event = {} | |
| p.event_prev = {} -- NEW: store event table for "undo" function | |
| p.step = 0 | |
| p.count = 0 | |
| p.loop = 0 | |
| p.clock = nil | |
| p.queued_rec = nil | |
| p.rec_dur = nil | |
| p.quantize = 1/48 | |
| p.endpoint = 0 | |
| p.start_callback = function() end | |
| p.end_of_loop_callback = function() end -- NEW: this callback can be really useful. | |
| p.end_of_rec_callback = function() end -- NEW: this callback can be really useful too | |
| p.end_callback = function() end | |
| p.process = function(_) end | |
| return p | |
| end | |
| -- NEW: moved these functions from the end to the top of the file. | |
| -- deep_copy is required for the "undo" function and it seemed good to keep them together | |
| local function deep_copy(tbl) | |
| local ret = {} | |
| if type(tbl) ~= 'table' then return tbl end | |
| for key, value in pairs(tbl) do | |
| ret[key] = deep_copy(value) | |
| end | |
| return ret | |
| end | |
| --- copy data from one reflection to another | |
| function reflection.copy(to, from) | |
| to.event = deep_copy(from.event) | |
| to.endpoint = from.endpoint | |
| end | |
| --- doubles the current loop | |
| function reflection:double() | |
| local copy = deep_copy(self.event) | |
| for i = 1, self.endpoint do | |
| self.event[self.endpoint + i] = copy[i] | |
| end | |
| self.endpoint = self.endpoint * 2 | |
| self.step_max = self.endpoint | |
| end | |
| --- start transport | |
| function reflection:start(beat_sync) -- NEW: changed arg to beat_sync for clarity | |
| beat_sync = beat_sync or self.quantize | |
| if self.clock then | |
| clock.cancel(self.clock) | |
| end | |
| self.clock = clock.run(function() | |
| clock.sync(beat_sync) | |
| self:begin_playback() | |
| end) | |
| end | |
| --- stop transport | |
| function reflection:stop() | |
| if self.clock then | |
| clock.cancel(self.clock) | |
| end | |
| self.clock = clock.run(function() | |
| clock.sync(self.quantize) | |
| self:end_playback() | |
| end) | |
| end | |
| --- enable / disable record head | |
| -- @tparam number rec 1 for recording, 2 for queued recording or 0 for not recording | |
| -- @tparam number dur (optional) duration in beats for recording | |
| -- NEW: beat_sync (optional) sync recording start to beat value | |
| function reflection:set_rec(rec, dur, beat_sync) | |
| self.rec = rec == 1 and 1 or 0 | |
| self.rec_enabled = rec > 0 and 1 or 0 -- NEW: if rec is enabled, even if queued, we'd like to know, right? Useful for visual feedback. | |
| self.queued_rec = nil | |
| -- if standard rec flag is enabled but play isn't, | |
| -- then we should start playing, yeah? | |
| if rec == 1 and self.play == 0 then | |
| self:start(beat_sync) -- NEW: if beat_sync arg provided the recording starts accordingly. | |
| end | |
| -- NEW: if pattern contains data then copy event data to temp table (required for the "undo" function) | |
| if rec == 1 and self.count > 0 then | |
| self.event_prev = {} | |
| self.event_prev = deep_copy(self.event) | |
| end | |
| if rec == 1 and dur then | |
| -- NEW: store dur in two separate variables (used to set the pattern duration). | |
| -- with the prev. implem it sometimes missed a step resulting in patterns being too short and not syncing properly. | |
| -- see lines 205-207 | |
| self.rec_dur = {count = dur, length = dur} | |
| end | |
| if rec == 2 then | |
| if self.count > 0 then | |
| local fn = self.start_callback | |
| self.start_callback = function() | |
| -- on next data pass, enable recording, | |
| self:set_rec(1, dur) -- NEW: pass arg dur if present (overdub for a specified duration). The countdown in the beginn_playback() function won't work otherwise. | |
| -- call our callback | |
| fn() | |
| -- and restore the state of the callback | |
| self.start_callback = fn | |
| end | |
| else | |
| self.queued_rec = {state = true, duration = dur} | |
| end | |
| end | |
| if rec == 0 then | |
| self:_clear_flags() | |
| self.end_of_rec_callback() -- NEW: end of rec callback. Useful thing to have. | |
| end | |
| end | |
| --- enable / disable looping | |
| -- (has no effect on first run) -- NEW: now it does. I think that it's improtant to support seamless looping. | |
| -- @tparam number loop 1 for looping or 0 for not looping | |
| function reflection:set_loop(loop) | |
| self.loop = loop == 0 and 0 or 1 | |
| end | |
| --- quantize playback | |
| -- @tparam float q defaults to 1/48 | |
| -- (should be at least 1/96) | |
| function reflection:set_quantization(q) | |
| self.quantize = q == nil and 1/48 or q | |
| end | |
| -- NEW: set length in beats | |
| -- used to set the pattern to a specifed length after recording. | |
| function reflection:set_length(beats) | |
| if self.count > 0 then | |
| self.endpoint = beats * 96 | |
| end | |
| end | |
| -- NEW: if temp event table contains data then replace event table (undo function). | |
| function reflection:undo() | |
| if next(self.event_prev) then | |
| self.event = deep_copy(self.event_prev) | |
| end | |
| end | |
| --- reset | |
| function reflection:clear() | |
| if self.clock then | |
| clock.cancel(self.clock) | |
| end | |
| self.rec = 0 | |
| self.play = 0 | |
| self.event = {} | |
| self.event_prev = {} | |
| self.step = 0 | |
| self.count = 0 | |
| -- self.quantize = 1/48 NEW: remove -> if the user changes the quantization value it should not be reset after clear. | |
| self.endpoint = 0 | |
| self.queued_rec = nil | |
| end | |
| --- watch | |
| function reflection:watch(event) | |
| local step_one = false | |
| if self.queued_rec ~= nil then | |
| self:set_rec(1,self.queued_rec.duration) | |
| self.queued_rec = nil | |
| step_one = true | |
| end | |
| if (self.rec == 1 and self.play == 1) or step_one then | |
| event._flag = true | |
| local s = math.floor(step_one == true and 1 or self.step) | |
| if not self.event[s] then | |
| self.event[s] = {} | |
| end | |
| table.insert(self.event[s], event) | |
| self.count = self.count + 1 | |
| end | |
| end | |
| function reflection:begin_playback() | |
| -- Q: could you please explain to me why the clock cancel was removed? | |
| -- I'd like to understand why it prevented playback, besides occasional clock errors I haven't run into any issues myself. | |
| self.step = 0 | |
| self.play = 1 | |
| self.clock = clock.run(function() | |
| self.start_callback() | |
| while self.play == 1 do | |
| clock.sync(1/96) | |
| self.step = self.step + 1 | |
| local q = math.floor(96 * self.quantize) | |
| if self.endpoint == 0 then | |
| -- don't process on first pass | |
| if self.rec_dur then | |
| self.rec_dur.count = self.rec_dur.count - 1/96 | |
| if self.rec_dur.count <= 0 then | |
| self.endpoint = self.rec_dur.length * 96 -- NEW: calc the endpoint according to the specified arg | |
| self:set_rec(0) | |
| -- NEW: don't end playback for seemless looping. | |
| -- rec is already set to 0 and play should remain at 1. | |
| -- also, the endpoint has already been set. | |
| -- self:end_playback(true) -- <- remove | |
| self.rec_dur = nil | |
| if self.loop == 1 then | |
| self.start_callback() | |
| self.step = 0 | |
| self.play = 1 | |
| end | |
| end | |
| -- NEW: if rec.dur not specifed then set the endpoint when rec == 0 and playback still running. | |
| -- this allows seemless looping without a predefined length. | |
| else | |
| if self.rec == 0 and self.count > 0 then | |
| self.endpoint = self.step | |
| if self.loop == 1 then | |
| self.step = 0 | |
| self:_clear_flags() | |
| self:start_callback() | |
| end | |
| end | |
| end | |
| else | |
| if self.step % q ~= 1 then goto continue end | |
| for i = q - 1, 0, - 1 do | |
| if self.event[self.step - i] and next(self.event[self.step - i]) then | |
| for j = 1, #self.event[self.step - i] do | |
| local event = self.event[self.step - i][j] | |
| if not event._flag then self.process(event) end | |
| end | |
| end | |
| end | |
| ::continue:: | |
| -- NEW: if overdubbing with dur as arg, then cound down and end rec | |
| -- this needs to be after ::continue:: otherwise the counting is off. | |
| -- previously it was before continue and when self.step % q ~= 1 it was skipped, | |
| -- which resulted in a much longer rec time than specified by dur. | |
| if self.rec_dur then | |
| self.rec_dur.count = self.rec_dur.count - 1/96 | |
| if self.rec_dur.count <= 0 then | |
| self:set_rec(0) | |
| self.rec_dur = nil | |
| -- as a convenience, if this was our first pass, stop playback | |
| --if self.endpoint == 0 then self:end_playback() return end -- NEW: I'd remove this becuase it prevents looping when overdubbing with dur. | |
| end | |
| end | |
| -- if the endpoint is reached reset counter or stop playback | |
| if self.count > 0 and self.step >= self.endpoint then | |
| self.end_of_loop_callback() -- NEW | |
| if self.loop == 0 then | |
| self:end_playback() | |
| elseif self.loop == 1 then | |
| self.step = self.step - self.endpoint -- Q: this should always be equivalent to self.step = 0 right? | |
| self:_clear_flags() | |
| self:start_callback() | |
| end | |
| end | |
| end | |
| end | |
| end) | |
| end | |
| function reflection:end_playback(silent) | |
| if self.clock and not silent then | |
| clock.cancel(self.clock) | |
| self.clock = nil | |
| end | |
| self.play = 0 | |
| self.rec = 0 | |
| if self.endpoint == 0 and next(self.event) then | |
| self.endpoint = self.step | |
| end | |
| self:_clear_flags() | |
| self.end_callback() | |
| end | |
| function reflection:_clear_flags() | |
| if self.endpoint == 0 then return end | |
| for i = 1, self.endpoint do | |
| local list = self.event[i] | |
| if list then | |
| for _, event in ipairs(list) do | |
| event._flag = nil | |
| end | |
| end | |
| end | |
| end | |
| return reflection |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment