monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

Input.scale can create many rapid callbacks at note boundaries #381

Closed trentgill closed 3 years ago

trentgill commented 3 years ago

This relates to the input quantizer function, as discussed inline here https://github.com/monome/crow/blob/5abb605ccec9ce3eea912915f53c01f335d5bd88/lib/detect.c#L256

It's more than a simple 'add some hysteresis' problem. I think there is something wrong with the logic of the quantization, so it may need to be approached from first-principles again.

trentgill commented 3 years ago

i rebuilt it in lua so rather than comparing input against the previous output voltage, we compare against calculated window-bounds.

the issue was that we used the quantized output which was not necessarily linear (unless using the chromatic mode).

now, whenever a new note is detected we calculate the limits of that window, extended by 5% hysteresis on either side. thus when checking if the new input has left the current note window, we just do a simple bounds check.

here's the code. just need to convert back to C:

-- 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