From 5f3f12b472d8b2be387278b5dc096cbe019641bb Mon Sep 17 00:00:00 2001 From: Trent Gill Date: Mon, 15 Nov 2021 13:52:22 -0800 Subject: [PATCH 1/3] add qscale library --- lib/lualink.c | 2 ++ lua/crowlib.lua | 1 + lua/scale.lua | 33 +++++++++++++++++++ tests/scale.lua | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 lua/scale.lua create mode 100644 tests/scale.lua diff --git a/lib/lualink.c b/lib/lualink.c index 5032b943..192593f3 100644 --- a/lib/lualink.c +++ b/lib/lualink.c @@ -43,6 +43,7 @@ #include "lua/calibrate.lua.h" #include "lua/sequins.lua.h" #include "lua/quote.lua.h" +#include "lua/scale.lua.h" #include "build/ii_lualink.h" // generated C header for linking to lua @@ -64,6 +65,7 @@ const struct lua_lib_locator Lua_libs[] = , { "lua_calibrate" , lua_calibrate , true} , { "lua_sequins" , lua_sequins , true} , { "lua_quote" , lua_quote , true} + , { "lua_scale" , lua_scale , true} , { NULL , NULL , true} }; diff --git a/lua/crowlib.lua b/lua/crowlib.lua index 388d15c8..60ed8c86 100644 --- a/lua/crowlib.lua +++ b/lua/crowlib.lua @@ -14,6 +14,7 @@ public = dofile('lua/public.lua') clock = dofile('lua/clock.lua') sequins= dofile('lua/sequins.lua') quote = dofile('lua/quote.lua') +qscale = dofile('lua/scale.lua') function C.reset() diff --git a/lua/scale.lua b/lua/scale.lua new file mode 100644 index 00000000..214e2650 --- /dev/null +++ b/lua/scale.lua @@ -0,0 +1,33 @@ +--- scale quantizer generator library +-- call the library similarly to input.scale or output.scale +-- returns a function that will perform the quantization on an input + +local Q = {} + +Q.ji = function(rs, ins, outs) + -- create a 12TET-ified version of rs + return Q.__call(S, just12(rs), ins, outs) +end + +local chrom = {0,1,2,3,4,5,6,7,8,9,10,11} +Q.__call = function(self, ns, ins, outs) + -- defaults + ins, outs = ins or 12, outs or 1 + -- chromatic shortcut with empty table or empty call + ns = (not ns or #ns == 0) and chrom or ns + + -- optimize by precalculating constants + local _INS = 1/ins -- inverse of in-scaling (mul cheaper than div!) + local OFF = 0.5 * _INS -- half an input-window of offset + local LEN = #ns -- memoize length + + + return function(n) + local norm = (n+OFF) * _INS -- normalize to input scaling + local octs = math.floor(norm) -- extract octaves + local ix = math.floor((norm - octs) * LEN) + return (ns[ix+1]*_INS + octs) * outs -- scale lookup & reconstruct + end +end + +return setmetatable(Q,Q) diff --git a/tests/scale.lua b/tests/scale.lua new file mode 100644 index 00000000..e2593f79 --- /dev/null +++ b/tests/scale.lua @@ -0,0 +1,84 @@ +--- scale library test + +sk = dofile("../lua/scale.lua") + + +--- major scale quantizer +-- input[1].mode('scale',{0,2,4,7,9},12,1.0) +-- output[1].scale({0,2,4,7,9},12,1.0) + +-- myquantizer = scale({0,2,4,7,9}, 12, 1.0) -- convert to volts +-- myquantizerN = scale({0,2,4,7,9}, 12, 12) -- input/output mapping the same + +-- think of the 'divs'/'scaling' as input/output ranges +-- so if divs == 12, then our table should be in 12TET +-- if divs == 'ji', then our table should be in just fractions +-- if scaling == divs, then output matches input +-- if divs==12 & scaling==1 then convert note table to voltage + +-- quantize to octaves, staying in 12TET +local s1 = sk({0},12,12) +assert(type(s1) == 'function') +assert(s1(0) == 0) +assert(s1(7) == 0) +assert(s1(11) == 0) -- always round down +assert(s1(11.99) == 12) -- capture marginally beneath bounds +assert(s1(12) == 12) +assert(s1(-12) == -12) -- test negative numbers +assert(s1(-11) == -12) +assert(s1(-13) == -24) + +-- quantize to octaves, convert to volts +local s1 = sk({0},12,1) +assert(s1(0) == 0) +assert(s1(7) == 0) +assert(s1(11) == 0) -- always round down +assert(s1(11.99) == 1) -- capture marginally beneath bounds +assert(s1(12) == 1) +assert(s1(-12) == -1) -- test negative numbers +assert(s1(-11) == -1) +assert(s1(-13) == -2) + +-- quantize to 2 values, splitting the octave evenly +local s1 = sk({0,1},12,12) +assert(s1(0) == 0) +assert(s1(5) == 0) -- round down +assert(s1(6) == 1) -- round up +assert(s1(12) == 12) -- round up + +-- as above but confirm default in/out is 12,1 +local s1 = sk({0,36}) +assert(s1(0) == 0) +assert(s1(5) == 0) -- round down +assert(s1(6) == 3) -- round up (note out-of-octave vals allowed) +assert(s1(12) == 1) -- round up + +-- chromatic n->v scaler +local s1 = sk() +assert(s1(0) == 0) +assert(s1(1) == 1/12) +assert(s1(11) == 11/12) +assert(s1(12) == 1) + +-- chromatic n->v scaler: table-call syntax +local s1 = sk{} +assert(s1(0) == 0) +assert(s1(1) == 1/12) +assert(s1(11) == 11/12) +assert(s1(12) == 1) + +-- chromatic n->n scaler +local s1 = sk({}, 12, 12) +assert(s1(0) == 0) +assert(s1(1) == 1) +assert(s1(11) == 11) +assert(s1(12) == 12) + +-- just intonation support +-- use a separate function +local sj1 = sk.ji({1/1, 2/1}, 12, 1) +assert(sj1(0) == 0) +assert(sj1(5) == 0) -- round down +-- note we need to account for floating point inaccuracies here +assert(sj1(6) >= 0.9999 and sj1(6) <= 1.0001) -- round up +assert(sj1(12) == 1.0) -- round up From 046ab076719ec9587a1a1248aa8d12c21e7eb867 Mon Sep 17 00:00:00 2001 From: Trent Gill Date: Mon, 15 Nov 2021 14:30:21 -0800 Subject: [PATCH 2/3] update tests to handle alternate input scaling --- tests/scale.lua | 56 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/tests/scale.lua b/tests/scale.lua index e2593f79..67dc8068 100644 --- a/tests/scale.lua +++ b/tests/scale.lua @@ -1,6 +1,36 @@ --- scale library test -sk = dofile("../lua/scale.lua") +--- Just Intonation helpers +-- convert a single fraction, or table of fractions to just intonation +-- optional 'offset' is itself a just ratio +-- justvolts converts to volts-per-octave +-- just12 converts to 12TET representation (for *.scale libs) +-- just12 will convert a fraction or table of fractions into 12tet 'semitones' +function _justint(fn, f, off) + off = off and fn(off) or 0 -- optional offset is a just ratio + if type(f) == 'table' then + local t = {} + for k,v in ipairs(f) do + t[k] = fn(v) + off + end + return t + else -- assume number + return fn(f) + off + end +end +JIVOLT = 1 / math.log(2) +JI12TET = 12 * JIVOLT +function _jiv(f) return math.log(f) * JIVOLT end +function _ji12(f) return math.log(f) * JI12TET end +-- public functions +function justvolts(f, off) return _justint(_jiv, f, off) end +function just12(f, off) return _justint(_ji12, f, off) end +function hztovolts(hz, ref) + ref = ref or 261.63 -- optional. defaults to middle-C + return justvolts(hz/ref) +end + +qscale = dofile("../lua/scale.lua") --- major scale quantizer @@ -15,9 +45,9 @@ sk = dofile("../lua/scale.lua") -- if divs == 'ji', then our table should be in just fractions -- if scaling == divs, then output matches input -- if divs==12 & scaling==1 then convert note table to voltage - +--[[ -- quantize to octaves, staying in 12TET -local s1 = sk({0},12,12) +local s1 = qscale({0},12,12) assert(type(s1) == 'function') assert(s1(0) == 0) assert(s1(7) == 0) @@ -29,7 +59,7 @@ assert(s1(-11) == -12) assert(s1(-13) == -24) -- quantize to octaves, convert to volts -local s1 = sk({0},12,1) +local s1 = qscale({0},12,1) assert(s1(0) == 0) assert(s1(7) == 0) assert(s1(11) == 0) -- always round down @@ -40,35 +70,35 @@ assert(s1(-11) == -1) assert(s1(-13) == -2) -- quantize to 2 values, splitting the octave evenly -local s1 = sk({0,1},12,12) +local s1 = qscale({0,1},12,12) assert(s1(0) == 0) assert(s1(5) == 0) -- round down assert(s1(6) == 1) -- round up assert(s1(12) == 12) -- round up -- as above but confirm default in/out is 12,1 -local s1 = sk({0,36}) +local s1 = qscale({0,36}) assert(s1(0) == 0) assert(s1(5) == 0) -- round down assert(s1(6) == 3) -- round up (note out-of-octave vals allowed) assert(s1(12) == 1) -- round up -- chromatic n->v scaler -local s1 = sk() +local s1 = qscale() assert(s1(0) == 0) assert(s1(1) == 1/12) assert(s1(11) == 11/12) assert(s1(12) == 1) -- chromatic n->v scaler: table-call syntax -local s1 = sk{} +local s1 = qscale{} assert(s1(0) == 0) assert(s1(1) == 1/12) assert(s1(11) == 11/12) assert(s1(12) == 1) -- chromatic n->n scaler -local s1 = sk({}, 12, 12) +local s1 = qscale({}, 12, 12) assert(s1(0) == 0) assert(s1(1) == 1) assert(s1(11) == 11) @@ -76,9 +106,15 @@ assert(s1(12) == 12) -- just intonation support -- use a separate function -local sj1 = sk.ji({1/1, 2/1}, 12, 1) +local sj1 = qscale.ji({1/1, 2/1}, 12, 1) assert(sj1(0) == 0) assert(sj1(5) == 0) -- round down -- note we need to account for floating point inaccuracies here assert(sj1(6) >= 0.9999 and sj1(6) <= 1.0001) -- round up assert(sj1(12) == 1.0) -- round up +]] +-- input as voltage +local sv1 = qscale({0,7},1.0,12) +print(sv1(0)) +assert(sv1(0) == 0) +assert(sv1(0.5) == 7) From 1642a270487f77893295200cb56af37fb3959493 Mon Sep 17 00:00:00 2001 From: Trent Gill Date: Mon, 15 Nov 2021 14:42:30 -0800 Subject: [PATCH 3/3] fix voltage-input scaling. note-table is assumed 12TET --- lua/scale.lua | 7 ++++--- tests/scale.lua | 16 +++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lua/scale.lua b/lua/scale.lua index 214e2650..ba3f44d2 100644 --- a/lua/scale.lua +++ b/lua/scale.lua @@ -18,15 +18,16 @@ Q.__call = function(self, ns, ins, outs) -- optimize by precalculating constants local _INS = 1/ins -- inverse of in-scaling (mul cheaper than div!) - local OFF = 0.5 * _INS -- half an input-window of offset + local TET = 12 -- TODO configurable + local _TET = 1/TET -- TODO configurable + local OFF = 0.5 * ins / TET -- half an input-window of offset local LEN = #ns -- memoize length - return function(n) local norm = (n+OFF) * _INS -- normalize to input scaling local octs = math.floor(norm) -- extract octaves local ix = math.floor((norm - octs) * LEN) - return (ns[ix+1]*_INS + octs) * outs -- scale lookup & reconstruct + return (ns[ix+1]*_TET + octs) * outs -- scale lookup & reconstruct end end diff --git a/tests/scale.lua b/tests/scale.lua index 67dc8068..7ee6e4ad 100644 --- a/tests/scale.lua +++ b/tests/scale.lua @@ -45,7 +45,7 @@ qscale = dofile("../lua/scale.lua") -- if divs == 'ji', then our table should be in just fractions -- if scaling == divs, then output matches input -- if divs==12 & scaling==1 then convert note table to voltage ---[[ +-- -- quantize to octaves, staying in 12TET local s1 = qscale({0},12,12) assert(type(s1) == 'function') @@ -112,9 +112,15 @@ assert(sj1(5) == 0) -- round down -- note we need to account for floating point inaccuracies here assert(sj1(6) >= 0.9999 and sj1(6) <= 1.0001) -- round up assert(sj1(12) == 1.0) -- round up -]] --- input as voltage + +-- input as voltage, convert to 12TET local sv1 = qscale({0,7},1.0,12) -print(sv1(0)) assert(sv1(0) == 0) -assert(sv1(0.5) == 7) +assert(sv1(0.5) >= 6.9999 and sv1(0.5) <= 7.0001) -- round up +assert(sv1(1) == 12) + +-- input as voltage, output to voltage +local sv1 = qscale({0,7},1.0,1.0) +assert(sv1(0) == 0) +assert(sv1(0.5) >= 0.583 and sv1(0.5) <= 0.584) -- round up +assert(sv1(1) == 1)