monome / softcut-lib

sample cutting library
GNU General Public License v3.0
35 stars 6 forks source link

add interpolation mode & resampling bypass commands #61

Closed andr-ew closed 2 years ago

andr-ew commented 2 years ago

not something off the issues list, so apologies if these changes are unwanted, but I’ve been enjoying this feature & I wanted to see if it made sense for upstream norns softcut : )

it’s a command for setting recording+playback interpolation. it also disables resampling entirely on the lowest setting (like max/msp poke~ & friends). right now it works like this:

softcut.interpolation(0) -- no interpolation, no resampling
softcut.interpolation(1) -- no interpolation
softcut.interpolation(2) -- linear interpolation
softcut.interpolation(4) -- cubic interpolation

I tried keeping the interface as simple as possible to start off, but I could see a couple of changes to make it more intuitive / flexible

I’ve more or less just translated some of the preprocessor directives for this into runtime flags, so apologies if there’s some obvious or needed optimizations I’m missing. the main changes are in the Resampler & Subhead classes, where I added a few alternate pathways based on the 4 states of the command.

testing

use this norns PR for this softcut-lib PR: https://github.com/monome/norns/pull/1503

build procedure (copied this from zack's last PR, hopefully I got it right):

# get everything
cd ~/norns
git remote set-url origin https://github.com/andr-ew/norns.git
git pull
git checkout sc-interpolation-modes
cd ~/norns/crone/softcut/softcut-lib
git remote set-url origin https://github.com/andr-ew/softcut-lib.git
git pull 
git checkout sc-interpolation-modes
# compile everything
cd ~/norns/crone/softcut/softcut-lib && ./waf && cd ~/norns && ./waf
# restart norns
sudo systemctl restart norns-jack.service; sudo systemctl restart norns-matron.service; sudo systemctl restart norns-crone.service

test script (requires some audio input):

-- softcut interpolation options test
--
-- E1: pre_level
-- E2: rate (course)
-- E3: rate (fine)
-- K1: clear
-- K2: record into half second loop
-- K3: interpolation mode

tab = require 'tabutil'

interps = {
    0,
    1,
    2,
    4,
}
interp_names = {
    'none, noresamp',
    'none',
    'linear',
    'cubic',
}

interp = 4
rate = 1
pre = 0
rec = false
dur = 0.5

function update_interp()
    softcut.interpolation(1, interps[interp])
end

function set_rec(v)
    rec = v
    softcut.rec_level(1, v and 1 or 0)
    softcut.pre_level(1, v and pre or 1)
end

function set_rate(v)
    rate = v
    softcut.rate(1, v)
    -- softcut.loop_end(1, util.clamp(0, dur, rate * dur))
end

function init()
    audio.level_cut(1.0)
    audio.level_adc_cut(1)

    softcut.level_input_cut(1, 1, 1)
    softcut.level_input_cut(2, 1, 1)

    softcut.buffer_clear()
    softcut.enable(1, 1)
    softcut.rec(1, 1)
    softcut.play(1, 1)
    softcut.recpre_slew_time(1, 0.1)
    softcut.fade_time(1, 0.1)
    softcut.buffer(1, 1)
    softcut.level(1, 1)
    softcut.rate(1, rate)
    softcut.loop(1, 1)
    softcut.loop_start(1, 0)
    softcut.loop_end(1, dur)
    softcut.position(1, 0)
    softcut.rate_slew_time(1, 0.1)

    update_interp()

    set_rec(false)
    set_rate(rate)

    redraw()
end

function key(n, z)
    if z==1 then
        if n==1 then
            softcut.buffer_clear()
        elseif n==2 then
            set_rec(not rec)
        elseif n==3 then
            interp = interp % #interp_names + 1
            update_interp()
        end

        redraw()
    end
end

function enc(n, d)
    if n==1 then
        pre = util.clamp(pre + (d*0.01), 0, 1)
        set_rec(rec)
    elseif n==2 then
        if d > 0 then set_rate(rate * 2)
        elseif d < 0 then set_rate(rate / 2)
        end
    elseif n==3 then
        set_rate(rate + d*0.01)
    end

    redraw()
end

function redraw()
    screen.clear()

    screen.move(2,64 * 1/4)
    screen.text("pre: " ..pre)

    screen.move(2,64 * 1/2)
    screen.text("rate: " .. rate)

    screen.move(2, 64 * 3/4)
    screen.text("rec: " .. (rec and "on" or "off"))

    screen.move(128 * 1/3, 64 * 3/4)
    screen.text("interp: " .. interp_names[interp])

    screen.update()
end

(also, I can’t get the command to show up in the api docs when I build it, not sure what I did wrong)

tehn commented 2 years ago

Have you done any benchmarking? I'm concerned about CPU capabilities.

On Sat, Jan 15, 2022, 1:43 PM andr-ew @.***> wrote:

not something off the issues list, so apologies if these changes are unwanted, but I’ve been enjoying this feature & I wanted to see if it made sense for upstream norns softcut : )

it’s a command for setting recording/playback interpolation. it also disables resampling entirely on the lowest setting (like max/msp poke~ & friends). right now it works like this:

softcut.interpolation(0) -- no interpolation, no resampling

softcut.interpolation(1) -- no interpolation

softcut.interpolation(2) -- linear interpolation

softcut.interpolation(4) -- cubic interpolation

I tried keeping the interface as simple as possible to start off, but I could see a couple of changes to make it more intuitive / flexible

  • text flags instead of integers (I copied the GrainBuf interpolation arg for the current format)
  • since it’s one command controlling write interpolation, read interpolation, and resampling on/off I could see splitting it up into 2 or 3 separate commands.

I’ve more or less just translated some of the preprocessor directives for this into runtime flags, so apologies if there’s some obvious optimizations I’m missing. the main changes are in the Resampler & Subhead classes, where I added a few alternate pathways based on the 4 states of the command. testing

use this norns PR for this softcut-lib PR: [TODO]

build procedure (copied this from zack's last PR, hopefully I got it right):

get everything

cd ~/norns

git remote set-url origin @.***:andr-ew/norns.git

git pull

git checkout sc-interpolation-modes

cd ~/norns/crone/softcut/softcut-lib

git remote set-url origin @.***:andr-ew/softcut-lib.git

git pull

git checkout sc-interpolation-modes

compile everything

cd ~/norns/crone/softcut/softcut-lib && ./waf && cd ~/norns && ./waf

restart norns

sudo systemctl restart norns-jack.service; sudo systemctl restart norns-matron.service; sudo systemctl restart norns-crone.service

test script (requires some audio input):

-- softcut interpolation options test

--

-- E1: pre_level

-- E2: rate (course)

-- E3: rate (fine)

-- K1: clear

-- K2: record into half second loop

-- K3: interpolation mode

tab = require 'tabutil'

interps = {

0,

1,

2,

4,

}

interp_names = {

'none, noresamp',

'none',

'linear',

'cubic',

}

interp = 4

rate = 1

pre = 0

rec = false

dur = 0.5

function update_interp()

softcut.interpolation(1, interps[interp])

end

function set_rec(v)

rec = v

softcut.rec_level(1, v and 1 or 0)

softcut.pre_level(1, v and pre or 1)

end

function set_rate(v)

rate = v

softcut.rate(1, v)

-- softcut.loop_end(1, util.clamp(0, dur, rate * dur))

end

function init()

audio.level_cut(1.0)

audio.level_adc_cut(1)

softcut.level_input_cut(1, 1, 1)

softcut.level_input_cut(2, 1, 1)

softcut.buffer_clear()

softcut.enable(1, 1)

softcut.rec(1, 1)

softcut.play(1, 1)

softcut.recpre_slew_time(1, 0.1)

softcut.fade_time(1, 0.1)

softcut.buffer(1, 1)

softcut.level(1, 1)

softcut.rate(1, rate)

softcut.loop(1, 1)

softcut.loop_start(1, 0)

softcut.loop_end(1, dur)

softcut.position(1, 0)

softcut.rate_slew_time(1, 0.1)

update_interp()

set_rec(false)

set_rate(rate)

redraw()

end

function key(n, z)

if z==1 then

    if n==1 then

        softcut.buffer_clear()

    elseif n==2 then

        set_rec(not rec)

    elseif n==3 then

        interp = interp % #interp_names + 1

        update_interp()

    end

    redraw()

end

end

function enc(n, d)

if n==1 then

    pre = util.clamp(pre + (d*0.01), 0, 1)

    set_rec(rec)

elseif n==2 then

    if d > 0 then set_rate(rate * 2)

    elseif d < 0 then set_rate(rate / 2)

    end

elseif n==3 then

    set_rate(rate + d*0.01)

end

redraw()

end

function redraw()

screen.clear()

screen.move(2,64 * 1/4)

screen.text("pre: " ..pre)

screen.move(2,64 * 1/2)

screen.text("rate: " .. rate)

screen.move(2, 64 * 3/4)

screen.text("rec: " .. (rec and "on" or "off"))

screen.move(128 * 1/3, 64 * 3/4)

screen.text("interp: " .. interp_names[interp])

screen.update()

end

(also, I can’t get the command to show up in the api docs when I build it, not sure what I did wrong)

You can view, comment on, or merge this pull request online at:

https://github.com/monome/softcut-lib/pull/61 Commit Summary

File Changes

(9 files https://github.com/monome/softcut-lib/pull/61/files)

Patch Links:

— Reply to this email directly, view it on GitHub https://github.com/monome/softcut-lib/pull/61, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB4I4CNW2ZMTB2BF2ENEA3UWG53VANCNFSM5MBJCJCA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you are subscribed to this thread.Message ID: @.***>

andr-ew commented 2 years ago

Have you done any benchmarking? I'm concerned about CPU capabilities.

no, I'd love some pointers on how do do that if you have any. I can work on an A/B comparison with the main branch.

schollz commented 2 years ago

no, I'd love some pointers on how do do that if you have any. I can work on an A/B comparison with the main branch.

I did some A/B testing by maxing out all six voices to be recording+playing at a fast rate (8x) and then monitoring crone CPU usage using top. here's a script that I use: https://github.com/schollz/gatherum/blob/master/softcut-recmax.lua

andr-ew commented 2 years ago

thanks @schollz !

after adding an optimization to writes (details), I ran this test on main & sc-interpolation-modes. stock norns, all 6 voices recording + playing back from tape at rates 1 & 8, cubic interpolation on both branches.

here are my results after the latest commits: branch rate %CPU %MEM
main 1 51.7 23.7
main 8 71.3 23.7
sc-interpolation-modes 1 52.3 23.7
sc-interpolation-modes 8 70.5 23.7

(not sure why my branch is getting a slight edge on the high rates only, but I did recompile & run the test a couple times just to be sure, results were very consistent. the gist comments have a few more details if you want em)

andr-ew commented 2 years ago

@catfact - to clarify (when I should have earlier) - a command to bypass resampling & setting read/write interpolation are both features I'd like to add to softcut for a norns script I'm working on. combining them into one command was an arbitrary choice on my part and I think it makes more sense to split them up. I get that disabling resampling is going to be a niche/weird feature so if you don't want the overhead of maintaining that feature upstream I can use my own fork of softcut instead. but I'm happy to spend more time on this PR to minimize the impact of additional features.

I'll research alternatives to per-sample switches and see if I can find the technique you're referring to, thanks for bearing with me & for taking the time to review.

catfact commented 2 years ago

I'm saying you can move the switch out of the per sample update func (and : please do if you want this to be merged)

catfact commented 2 years ago

And c++ lang provides ways to do this with less code than you have

catfact commented 2 years ago

Basically I think you can implement this change without nontrivial changes to SubHead, and without adding instructions to the per sample update functions. I'm not trying to cold golf or prematurely optimize, just giving same advice I would have received 15 years ago.

andr-ew commented 2 years ago

...ok I think I just misunderstood your first comment.

main culprit == over-reliance on switch/case in the sample loop ?

catfact commented 2 years ago

yes, if you look carefully you will notice that some pains have been taken to avoid switch/branch in sample loop (by factoring into different sample update funcs.)

in your change, the resampling / interpolation mode never changes from one sample to another, only from one block to another. so structure the change in such a way that it is applied at the top of the block and never tested again during the block. i don't much care how you do this, there are several ways, but the way i would do it is to add a template parameter to Resampler which takes a mode value, specialize the interpolate function based on mode value, and switch between three different specialized Resampler objects.

the "no resampling" mode adds an extra wrinkle. to be honest, i am not stoked on this mode. it looks like you just write every Nth sample and leave intermediate samples untouched. that is of course a very glitchy effect. i am not convinced it needs to be in the main branch of this library.

andr-ew commented 2 years ago

ok cool, thank you for sharing more details. that sounds like enough of a direction for me to start finding a solution.

re: the "no resampling" thing it's of course up to you. for me it brings back some of what I like about trying to build buffer-based stuff in max/puredata/SC/etc (where there were less options to do things the "right" way so you end up doing glitchy stuff with a phasor heading into a write head) but mixing in everything that softcut can do. if you don't think it fits in the main branch I can just drop it from this PR and work toward including my own fork of softcut with my script instead.

catfact commented 2 years ago

@andr-ew i'm sorry i overreacted there. rough day, rough week, rough year i guess, but no excuse.

i don't want to merge this yet. i appreciate the effort and i think we should use parts of this and most/all of your other PR on norns. but particularly i question whether the "no resampling" mode is a good thing to add. (really let's call it what it is, which is "no interpolation" as opposed to zero-order, 1st order, or 3rd order interpolation.) i don't mind adding the effect, but the architecture doesn't efficiently support swapping peek/poke functions so i would want a substantial refactor around that.

and truly, the idea that you will attempt to bundle a forked softcut if i don't merge that part, is off-putting; if you are going to make that attempt on this basis, then i won't spend time correcting and merging your other changes here.

catfact commented 2 years ago

re: profiling, here is a comparison collecting many batched samples of top, and also collecting samples of jack_cpu_load. you can see that the former is basically noise within 5%.

this is N=100, which is honestly still not really enough (see those outliers,) but it is enough to illustrate the small (1%) but definite (see first quartile range) trend towards higher JACK CPU loads with the proposed change. this is the effect of adding more per-sample logic (even a small amount.)

catfact commented 2 years ago

profiles