Norns 2.0: softcut

What would be the best way to go about creating a ping-pong delay with softcut? Should I pan 2 voices left and right, and somehow offset the playback of the second voice? I’ve tried doing this but couldn’t find a softcut param that does what I want exactly. Very likely I’m misunderstanding how the buffers work.

Would it make more sense to use one voice, and create a lua LFO to dynamically pan it left and right?

maybe something like

input > voice1/voice2 stereo pair
voice1 > voice2
voice2 > voice1
voices are synced, pre_level=0

not sure if I explained that super well, but essentially for ping pong the stereo image needs to be reversed in the feedback stage. you can do that with voice routings (as opposed to pre_level)

@crim you’ve got an autopanning delay there–for a pingpong my understanding was that the dry input is fed into only one delay, is that right?

input -> delayLeft  ---------------> outputLeft
         delayLeft  -> delayRight -> outputRight
         delayRight -> delayLeft
1 Like

I think this would work if ya only wanted it in mono but to my memory it’s all good to feed a stereo input into both delays as long as you cris-cross the feedback lines

1 Like

thanks @andrew and @murray, this all makes sense conceptually. I’ll play around with it a bit more and see if I can get something working.

1 Like

bonus points: I’d recommend adding in those output filters for decaying tails.

if you’re max literate you can check out alliterate for fun decaying pitch shifting ping pong stuff. It’s a pretty simple patch that I’ve wound up using a lot

& now I’m thinking how fun this routing would be with softcut pitch shifting ooo yes

2 Likes

Got it! Much simpler than I thought of course. What tripped me up initially was I had a mono voice from my modular split into both inputs on norns, so when I swapped the left and right channels I was getting the same thing in both sides (duh). Routing would have to be a little different for that setup. Also pre_level does definitely need to be 0 if you don’t want eventual screeching feedback :slight_smile:

In case anyone's interested
softcut.level_input_cut(1, 1, 1)
softcut.level_input_cut(1, 2, 0)
  
softcut.level_input_cut(2, 1, 0)
softcut.level_input_cut(2, 2, 1) -- value needs to be 0 if both channels same mono source
  
softcut.level_cut_cut(1, 2, 0.9) -- 0.9 is feedback
softcut.level_cut_cut(2, 1, 0.9)
  
softcut.pan(1, 1)
softcut.pan(2, -1)

for i=1,2 do
  softcut.pre_level(i, 0) -- no screeching feedback pls thank u
end
4 Likes

Just wondering… I don’t suppose there’s also a “filter cutoff slew” control that would be possible to expose to lua?

no, there isn’t. filter coefficient is relatively expensive to recalculate and so it is not slewed. considering an approximation that would make per-sample modulation more reasonable. downsides are reduced frequency range and accuracy.

2 Likes

Hello All,

I have an observation I could use help sorting. I have a simple looper script that:

  1. E3 to start recording, E3 to stop recording and set the end of loop.
  2. E3 to start/stop overdub by simply toggling soft cut.rec.

If while overdubbing the running loop crosses back over the punch in time (i.e. at least one full loop while overdubbing), the new sounds are recorded in time with the loop as I created them. However, if I punch in and out shorter than one full loop, the new material is placed in the loop at t = start_of_loop + (new_sounds_time - punch_in_time).

I could constrain punch out to > one loop length, but wondering if folks have ideas for keeping time for quick punch ins? Could I be missing a soft cut setting somewhere?

Thanks!

Edit: here’s the script.

Summary
-- Simple Looper
--
local amirecording = 0
local base_loop = 0
local end_of_loop = 61
local time_ref = 0

function init()
  softcut.buffer_clear()
  audio.level_adc_cut(0.75)
  for i = 1,2 do
    if (i % 2) == 0 then
      softcut.level_input_cut(2,i,0.75)
      softcut.buffer(i,2)
      softcut.pan(i,0.9)
    else
      softcut.level_input_cut(1,i,0.75)
      softcut.buffer(i,1)
      softcut.pan(i,-0.9)
    end
    softcut.play(i,1)
    softcut.enable(i,1)
    softcut.rec_offset(i,-0.06)
    softcut.level(i,0.75)
    softcut.loop(i,1)
    softcut.loop_start(i,1)
    softcut.loop_end(i,end_of_loop + 1)
    softcut.position(i,1)
    softcut.rate(i,1.0)
    softcut.fade_time(i,0)
    softcut.rec(i,amirecording)
    softcut.rec_level(i,0.7)
    softcut.pre_level(i,0.9)
    softcut.level_slew_time(i,0.5)
    softcut.rate_slew_time(i,0.05)
  end
  redraw()
end

function key(n,z)
    if n==3 and z==1 then
      if amirecording == 1 then
        softcut.rec(1,0)
        softcut.rec(2,0)
        amirecording = 0
        if base_loop == 0 then
          end_of_loop = util.time() - time_ref + 1
          trimmed_loop_end = end_of_loop
          softcut.loop_end(1,end_of_loop)
          softcut.loop_end(2,end_of_loop)
          base_loop = 1
        end
      elseif amirecording == 0 then
        if base_loop == 0 then
          time_ref = util.time()
          softcut.position(1,1)
          softcut.position(2,1)
        end
        softcut.rec(1,1)
        softcut.rec(2,1)
        amirecording = 1
      end
    end
  redraw()
end

function redraw()
  screen.clear()
  screen.aa(0)
  screen.level(10)
  screen.font_size(8)
  screen.move(64,25)
  if amirecording == 0 and base_loop == 0 then
    screen.text_center("K3 to Record")
  elseif amirecording == 1 and base_loop == 0 then
    screen.text_center("Recording ... K3 to Stop")
  elseif amirecording == 0 and base_loop == 1 then
    screen.text_center("Got It ... K3 to Overdub")
  elseif amirecording == 1 and base_loop == 1 then
    screen.text_center("Overdubbing ... K3 to Stop")
  end
  screen.update()
end

Share script please?

Doh! post updated with the script. Thanks!

1 Like

cool, thanks.

well i have good news and bad news.

bad: that does indeed seem like a regression, which i can’t quite explain yet. thanks for spotting it and i’ll try and fix it quickly.

under the hood, there are two R/W head pairs in each voice, for crossfading, and it sounds like the wrong one is getting activated in this condition: enabling record while already playing, and loop/crossfade hasn’t occurred since the last explicit position change / voice-state change / something.

this isn’t too surprising since there are actually different sample callback functions depending on which state the voice is in, this was done relatively recently in a refactor, so i will double check on the logic around that.

good: there is a better way to implement this, which is to use rec_level instead of the rec flag. the former is intended for realtime modulation. the latter should be thought of as a configuration / optimization setting. so here’s a version that seems to work as expected and doesn’t have clicks on punch-in/out:

-- Simple Looper (updated)
--
amirecording = 0
base_loop = 0
end_of_loop = 61
time_ref = 0

pan = {-0.75, 0.75}

function init()
  print("simple hello")
  softcut.buffer_clear()
  audio.level_adc_cut(0.75)
  
  for i = 1,2 do
    softcut.level_input_cut(i,i,1)
    softcut.buffer(i,i)
    softcut.pan(i, pan[i])
    
    softcut.play(i,1)
    softcut.rec(i,1)
    softcut.enable(i,1)
    softcut.rec_offset(i,-0.06)
    
    softcut.pre_level(i,1)
    softcut.rec_level(i, 1)
    
    softcut.level(i,0.75)
    softcut.loop(i,1)
    softcut.loop_start(i,1)
    softcut.loop_end(i,end_of_loop + 1)
    
    softcut.position(i,1)
    
    softcut.rate(i,1.0)
    softcut.fade_time(i,0)
    
    softcut.level_slew_time(i,0.5)
    softcut.rate_slew_time(i,0.05)
  end
  redraw()
end

function key(n,z)
    
    if n==3 and z==1 then
      if amirecording == 1 then
        softcut.rec_level(1,0)
        softcut.rec_level(2,0)
        amirecording = 0
        if base_loop == 0 then
          end_of_loop = util.time() - time_ref + 1
          print("end_of_loop: "..end_of_loop)
          softcut.loop_end(1,end_of_loop)
          softcut.loop_end(2,end_of_loop)
          base_loop = 1
        end
      elseif amirecording == 0 then
        if base_loop == 0 then
          time_ref = util.time()
          softcut.position(1,1)
          softcut.position(2,1)
        end
        softcut.rec_level(1,1)
        softcut.rec_level(2,1)
        amirecording = 1
      end
    end
  redraw()
end

function redraw()
  screen.clear()
  screen.aa(0)
  screen.level(10)
  screen.font_size(8)
  screen.move(64,25)
  if amirecording == 0 and base_loop == 0 then
    screen.text_center("K3 to Record")
  elseif amirecording == 1 and base_loop == 0 then
    screen.text_center("Recording ... K3 to Stop")
  elseif amirecording == 0 and base_loop == 1 then
    screen.text_center("Got It ... K3 to Overdub")
  elseif amirecording == 1 and base_loop == 1 then
    screen.text_center("Overdubbing ... K3 to Stop")
  end
  screen.update()
end

setting pre_level=1 and rec_level=0 makes a lossless loop.

8 Likes

Excellent! Super helpful. Thanks for this @zebra.

Hi guys! I think I have a problem with Softcut. After the last update, apps that use it like MLR, Cranes, Cheat Codes … sometimes stop recording, the app works but doesn’t record. To solve I have to restart Norns. How could I solve it?
Thank’s :slight_smile:

I haven’t experienced this.

Which update exactly?
Maybe re-run the update?

Any other notes to reproduce? (After long uptime? After some interaction, immediately after loading a script, or “out of the blue”?)

If possible, shell into norns when issue occurs and do
sudo journalctl -u norns-crone
and same with norns-jack, norns-matron

Thanks for your interest. The update is 191201. How can i re-run the update? if I do it directly from norns it tells me “up to date”.
I have not yet been able to understand if there is something that creates the problem. When it happens I try to follow your advice!

when i do norns-jack it tell me that

can you run the 191230 update and continue testing from there?

1 Like

I just did the update, for the moment everything seems to be working.
Thanks Tehn! :slight_smile: