monome / softcut-lib

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

command to "stop recording at end of loop+fade" #59

Closed schollz closed 2 years ago

schollz commented 2 years ago

here I'm introducing a new command called (softcut.rec_once) (addressing https://github.com/monome/softcut-lib/issues/39) that when enabled will tell the softcut voice to continue recording until a position change, at which point the new subhead stops writing and the old subhead continues to write (while fading out), after which no writing takes place. this allows you to make single-loop recordings with crossfades.

to make this work, I had to modify ReadWriteHead in a way that allows it to have only one active writing subhead (normally it always has two, or none), so I added some flags to control the behavior. the implementation makes the API a little wonky:

I'm open to any improvements and changes I can make to help make this better.

testing

this PR goes along with this branch of norns: https://github.com/monome/norns/pull/1494

testing procedure (if there is a better way please let me know!):

# get everything
cd ~/norns
git remote set-url origin https://github.com:schollz/norns.git
git pull
git checkout sc-rec-once
cd ~/norns/crone/softcut/softcut-lib
git remote set-url origin https://github.com:schollz/softcut-lib.git
git pull 
git checkout rec-once
# 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

for testing, I used this script (modified study 4). it results in audio files (/home/we/dust/audio/rec_once_[1|2|3|4].wav) which should show a single loop with crossfades extending past the loop point based on fade_time.

-- softcut.rec_once test

rate = 1.0
rec = 1.0
pre = 0.0

function init()
    audio.level_adc_cut(1)
  softcut.buffer_clear()
  softcut.enable(1,1)
  softcut.recpre_slew_time(1,0)
  softcut.fade_time(1,0.5)
  softcut.buffer(1,1)
  softcut.level(1,1.0)
  softcut.rate(1,1.0)
  softcut.loop(1,1)
  softcut.loop_start(1,1)
  softcut.loop_end(1,2)
  softcut.position(1,1)
  softcut.play(1,1)
  softcut.level_input_cut(1,1,1.0)
  softcut.level_input_cut(2,1,1.0)
  softcut.rec_level(1,rec)
  softcut.pre_level(1,pre)

  -- async test
  clock.run(function()
    -- give it some time to initiate seems nessecary to avoid a dropout in the beginning
    print("initiating tests")
    clock.sleep(1)
    -- initiates voice 1 to recording a loop starting at position 1
    softcut.rec_once(1,1)
    clock.sleep(2.5)
    softcut.buffer_write_mono("/home/we/dust/audio/rec_once_1.wav",0,4)
    print("wrote /home/we/dust/audio/rec_once_1.wav")
    clock.sleep(1)

     -- initiates voice 1 to recording a loop starting at position 1.5
    softcut.rec_once(1,1.5)
    clock.sleep(2)
    softcut.buffer_write_mono("/home/we/dust/audio/rec_once_2.wav",0,4)
    print("wrote /home/we/dust/audio/rec_once_2.wav")
    clock.sleep(1)

    -- this would normally erase but the recording is stopped
    softcut.rec_level(1,0) 
    softcut.pre_level(1,0)
    clock.sleep(1.5)
    softcut.buffer_write_mono("/home/we/dust/audio/rec_once_3.wav",0,4)
    print("wrote /home/we/dust/audio/rec_once_3.wav (should be same as 2)")
    clock.sleep(1)

    print("overdubbing when it crosses again - this should be synced with 2 and 3")
    softcut.rec_level(1,0.5) 
    softcut.pre_level(1,1)
     -- initiates voice 1 to recording a loop starting at next cross
    softcut.rec_once(1)
    clock.sleep(2.5)
    softcut.buffer_write_mono("/home/we/dust/audio/rec_once_4.wav",0,4)
    print("wrote /home/we/dust/audio/rec_once_4.wav")

  end)

end

function enc(n,d)
  if n==1 then
    rate = util.clamp(rate+d/100,-4,4)
    softcut.rate(1,rate)
  elseif n==2 then
    rec = util.clamp(rec+d/100,0,1)
    softcut.rec_level(1,rec)
  elseif n==3 then
    pre = util.clamp(pre+d/100,0,1)
    softcut.pre_level(1,pre)
  end
  redraw()
end

function key(n,z)
  if z==1 then
    clock.run(function()
      print("overdubbing")
      softcut.pre_level(1,0)
      softcut.rec_once(1,1)
      clock.sleep(4)
      softcut.buffer_write_mono("/home/we/dust/audio/rec_once_4.wav",0,4)
      print("wrote /home/we/dust/audio/rec_once_4.wav")
    end)
  end
  redraw()
end

function redraw()
  screen.clear()
  screen.move(10,30)
  screen.text("rate: ")
  screen.move(118,30)
  screen.text_right(string.format("%.2f",rate))
  screen.move(10,40)
  screen.text("rec: ")
  screen.move(118,40)
  screen.text_right(string.format("%.2f",rec))
  screen.move(10,50)
  screen.text("pre: ")
  screen.move(118,50)
  screen.text_right(string.format("%.2f",pre))
  screen.update()
end
catfact commented 2 years ago

thanks for taking that on!

i haven't had chance to test but things look fine and make sense.

as you mention in script comments, issuing many commands at once does stall the softcut logic on the audio thread until the command Q is emptied... for now that's just the way it works... so it's another reason to make single commands do more work.

schollz commented 2 years ago

thanks! I fixed the "2nd problem", i.e. needing to manually reset the voice after using the rec_once. now after rec_once finishes, it will turn off all recording similar to doing a rec(<voice>,0). I added in a getter for the recOnceDone flag which the Voice checks for and if it is done, it resets it and changes over to a non-writing process function (basically turning recording off).

for your first point -> I agree it would be nicer to extend the api. I am trying to do it but for some reason it just isn't working basically I am changing the [if] function to a [iif] and using set_cut_param_iif but for whatever reason that is not working. I will make a new fork with the changes to norns/softcut with this api extension.

however, this current PR is fully working still. I updated the example above.

schollz commented 2 years ago

okay I found my confusion. I was previously using the _set_cut_param which automatically puts voices to 0-index, but the _set_cut-param_iif does not assume voices so I was indexing the wrong voice.

schollz commented 2 years ago

okay I've modified it now to rec_once(<voice>,<on/off>,<position>). if <position> is < 0 then it will initiate at the next crossing, otherwise it jumps to that position. the above examples are updated to reflect this behavior.

tehn commented 2 years ago

@schollz this is a rad feature, thank you so much for diving in

catfact commented 2 years ago

ok cool. i'm going to go ahead and merge this. but may do a slight refactor and add pingpong mode before recommending inclusion in norns release.

catfact commented 1 year ago

i measured the additional per-sample conditional logic as adding 1-2% baseline CPU usage for all softcut applications, which doesnt' sound like a lot but is enough to make a difference.

additionally, it makes the optimization of swapping out the per-sample function here useless, and transforms it into technical debt: https://github.com/monome/softcut-lib/blob/main/softcut-lib/src/Voice.cpp#L63

the idea there is that the ultimately most expensive operations of reading and (especially) writing ot the buffer can be skipped for a whole block depending on the state of the voice at the top of the block. that's not true any more when the R/W state can change per sample. so at minimum for a change like this i'd like to see that loose end tied up. preferably i'd also llike to see a properly calibrated assessment of the performance impact of the change.

(that said, i find i'm no longer very invested either way, so i'll leave it to other admins to re-approve. but i would do that refactor to clean things up and avoid future confusion.)

schollz commented 1 year ago

I did some measurements with baseline and found no significant difference, though I trust your measurements more. that level of cpu usage does not seem worth having activated for all applications all the time when most (pretty much all applications now) don't use this feature. if it ever comes up again, I'd be happy to help integrate it as I have been keeping a fork alive and up-to-date for my own purposes.

I always appreciate your help and input @catfact, thanks.

catfact commented 1 year ago

ok, i'll leave it up to you / @tehn / @dndrks . if CPU impact is not measurable, that is fine. (but please do sharae the actual baseline results if you get a chance - even better might be setting up some other variations of baseline that aren't just fully R/W all the time.)

schollz commented 1 year ago

yes absolutely. here are my actual results on a metal norns with cm3+ using baseline.lua with wifi on in both cases.

norns latest q1-q3 is 66.4-66.8% cpu usage.

norns w/ rec_once q1-q3 is 65.9-66.3% cpu usage.

norns latest ``` [meta] norns_version = "e8ae3606 (HEAD -> main, origin/main, origin/HEAD) lfo fixes + improvements (#1630)" softcut_version = "a92fcd6 (HEAD, origin/norns-latest) fix clobbered args from svf refactor" N = 500 period = 0.25 [speed] "cpu_hz_start" = 1200000 "cpu_hz_mid" = 1200000 "cpu_hz_end" = 1200000 [load] min = 66.181716918945 max = 71.736839294434 mean = 66.727247238159 median = 66.5791015625 q1 = 66.43470954895 q3 = 66.827291488647 [xruns] min = 0.0 max = 0.0 mean = 0.0 median = 0.0 q1 = 0.0 q3 = 0.0 ```
norns with rec_once ([softcut repo](https://github.com/schollz/softcut-lib/tree/rec-once5) and [norns](https://github.com/schollz/norns/tree/rec-once5)) ``` [meta] norns_version = "2204a946 (HEAD -> rec-once5, origin/rec-once5) update rec-once submodule" softcut_version = "7c9cc07 (HEAD -> rec-once5, origin/rec-once5) optimize this check" N = 500 period = 0.25 [speed] "cpu_hz_start" = 1200000 "cpu_hz_mid" = 1200000 "cpu_hz_end" = 1200000 [load] min = 65.682647705078 max = 69.326599121094 mean = 66.181434310913 median = 66.069259643555 q1 = 65.942672729492 q3 = 66.310354232788 [xruns] min = 0.0 max = 0.0 mean = 0.0 median = 0.0 q1 = 0.0 q3 = 0.0 ```
here is my whole test procedure ``` # create rec_once version into ~/norns.rec_once git clone git@github.com:schollz/norns norns.rec_once cd ~/norns.rec_once git checkout rec-once5 git submodule update --init --recursive rm -rf ~/norns.rec_once/crone/softcut cd ~/norns.rec_once/crone git clone git@github.com:schollz/softcut-lib softcut cd ~/norns.rec_once/crone/softcut git checkout rec-once5 cd ~/norns.rec_once ./waf configure --release ./waf build --release # create main version into ~/norns.main cd ~ git clone git@github.com:monome/norns norns.main cd ~/norns.main git submodule update --init --recursive ./waf configure --release ./waf build --release # test rec_once version rm -rf ~/norns cp -r ~/norns.rec_once ~/norns ~/norns/stop.sh && sleep 1 && ~/norns/start.sh # RUN baseline.lua # test main version rm -rf ~/norns cp -r ~/norns.main ~/norns ~/norns/stop.sh && sleep 1 && ~/norns/start.sh # RUN baseline.lua ```
catfact commented 1 year ago

interesting, not sure how things were different several months ago but must have been some confounding variable.

not sure what is going on, something pretty abstruse with flow prediction i suppose. but it definitely seems worth checking what happens if the separate per-sample functions are elided as i mentioned above.

anyways yeah if there are no ill effects for any users for sure it is fine to merge/realease the feature.