Skip to content

Instantly share code, notes, and snippets.

@trentgill
Last active November 15, 2021 19:56
Show Gist options
  • Save trentgill/f5e45a06ee14520b02f6d5e3bba5f178 to your computer and use it in GitHub Desktop.
Save trentgill/f5e45a06ee14520b02f6d5e3bba5f178 to your computer and use it in GitHub Desktop.
a lua-quantizer in the .scale style
function quantize( volt, scale, divs, range )
-- defaults
chrom = {0,1,2,3,4,5,6,7,8,9,10,11}
if scale == nil then scale = chrom end
local notes = #scale
if notes == 0 then
scale = chrom
notes = #scale
end
divs,range = divs or 12
, range or 1.0
-- separate note & octave
local wrap = 0
if type(divs) == 'string' then -- assume just intonation
else
while volt > range do
end
end
-- calc index into table
local ix = 1+math.floor(volt/range * #scale)
-- scale to voltage
local v = 0
if type(divs) == 'string' then -- assume just intonation
v = math.log(scale[ix]) / math.log(2)
else
v = scale[ix] / divs
end
return v * range
-- reapply octave
end
---- from input.scale fix
-- nb: remove all tracking of previous outputs
function find_bounds(s, ix, oct)
-- find ideal voltage for this window
local sum = (oct*s.scaling) + ix*s.win -- sum in terms of voltage
-- calculate bounds
sum = sum - s.offset
s.lower = sum - s.hyst
s.upper = sum + s.win + s.hyst
print(s.lower, s.upper)
end
function make_scale(scale, scaleLen, divs, scaling)
local t = { scale = scale
, sLen = scaleLen
, divs = divs or 12
, scaling = scaling or 1.0
}
if t.sLen == 0 then -- auto-chromatic
t.sLen = divs
for i=1,divs do t.scale[i] = (i-1)/divs end
end
-- pre-calc for bounds & hysteresis (optimzation)
t.offset = 0.5 * t.scaling / t.divs -- half of a div
t.win = (1.0 / t.sLen) * t.scaling -- a note window in terms of voltage
t.hyst = t.win/20 -- calculate the hysteresis per window (5%)
t.hyst = (t.hyst < 0.006) and 0.006 or t.hyst -- clamp to 1 LSB at 12bit
find_bounds(t, 0, -10) -- set to invalid note
return t
end
function do_scale(s, v)
if v > s.upper
or v < s.lower then
-- offset input to ensure we capture noisy notes at the divs
v = v + s.offset
-- calculate index of input
local n_level = v / s.scaling
local octaves = math.floor(n_level)
local phase = n_level - octaves
local fix = phase * s.sLen
local ix = math.floor(fix)
-- perform scale lookup & prepare outs
local note = s.scale[ix+1]
local noteOct = note + octaves*s.divs
local volts = (note/s.divs + octaves) * s.scaling
-- TODO call action
-- calculate new bounds
find_bounds(s, ix, octaves)
end
end
S = make_scale({5,-12,7,11}, 4)
for i=1,2,0.01 do
do_scale(S, i)
do_scale(S, i)
end
--- another version?
--- issues:
--- lastVolts is not initialized
--- lastIndex is not initialized
--- lastOct is not initialized
--- hwin clamp is too big. i think it's missing a zero -> 0.00666
-- 0.066 is only 15 divisions per octave
-- it should be *half* a division, so 1/24 for chromatic semitones
-- the only reason to clamp it is to avoid microdivs, but that's prob unrealistic?
function make_scale(scale, scaleLen, divs, scaling)
local t = { action = event
, scale = scale
, sLen = scaleLen
, divs = divs
, scaling = scaling
, offset = 0.5 * scaling / divs
}
print('offset',t.offset)
t.hwin = t.offset * 1.1
print('hwin',t.hwin)
t.hwin = (t.hwin < 0.02) and 0.02 or t.hwin
print('hwin\'',t.hwin)
print()
if t.sLen == 0 then -- auto-chromatic
t.sLen = 1
t.scale[1] = 0.0
t.scaling = scaling / divs
t.divs = 1.0
end
t.lastNote = -100.0
t.lastVolts = -100
return t
end
function do_scale(s, v)
print(v, (s.lastVolts - s.hwin), (s.lastVolts + s.hwin))
-- print((s.lastVolts - s.hwin))
---- the issue is that we compare against s.lastVolts
-- however, this is not necessarily a linear (or even ordered) parameter
-- instead we *must* determine the window of the input on every sample
-- or alternatively, store the previous input voltage & whether a change occured.
-- then use hwin to see if it's moved sufficiently far from the previous *input* (not output)
if (v > (s.lastVolts + s.hwin))
or (v < (s.lastVolts - s.hwin)) then -- in a new division zone, might be a new note
-- print' close'
v = v + s.offset
local n_level = v / s.scaling
local octaves = math.floor(n_level)
local phase = n_level - octaves
local fix = phase * s.sLen
local ix = math.floor(fix)
if (ix ~= s.lastIndex)
or (octaves ~= s.lastOct) then
print('',ix,s.lastIndex, ix ~= s.lastIndex)
print('',octaves,s.lastOct, octaves ~= s.lastOct)
local note = s.scale[ix+1]
local noteOct = note + octaves*s.divs
local volts = (note/s.divs + octaves) * s.scaling
s.lastIndex = ix
s.lastOct = octaves
s.lastNote = noteOct
s.lastVolts = volts
print(' new', ix, octaves, noteOct, volts)
end
else print' _'
end
end
-- function do_scale(s, v)
-- print((s.lastVolts + s.hwin))
-- print((s.lastVolts - s.hwin))
-- if (v > (s.lastVolts + s.hwin))
-- or (v < (s.lastVolts - s.hwin)) then
-- v = v + s.offset
-- local n_level = v / s.scaling
-- local octaves = math.floor(n_level)
-- local phase = n_level - octaves
-- local fix = phase * s.sLen
-- local ix = math.floor(fix)
-- if (ix ~= s.lastIndex)
-- or (octaves ~= lastOct) then
-- local note = s.scale[ix+1]
-- local noteOct = note + octaves*s.divs
-- local volts = (note/s.divs + octaves) * s.scaling
-- s.lastIndex = ix
-- s.lastOct = octaves
-- s.lastNote = noteOct
-- s.lastVolts = volts
-- print(ix, octaves, noteOct, volts)
-- end
-- end
-- end
S = make_scale({0,4,7,11}, 4, 12, 1.0)
do_scale(S, 1)
do_scale(S, 1)
do_scale(S, 1.1)
do_scale(S, 1.1)
do_scale(S, 1.2)
do_scale(S, 1.22)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment