Norns 1.0: softcut

completely understandable – very grateful for the work you’ve done. also, that makes total sense and there’s no reasonable expectation for that elasticity to be baked in. I’ll explore MLR’s code more and see what I can learn about smarter buffer referencing.

1 Like

no no, it absolutely should function properly with any parameter settings. the write functionality when rate != 1 is very broken in several ways and needs to be fixed. for now i recommend dedicating a single head for writing, and always leaving rate=1 on that head, but this is a stupid workaround for the lack of more naturalistic “tape-like” behavior.

not sure how this applies, but in my non-norns explorations, I’ve found my favorite behavior is for the play head to follow the write head while overdubbing, and while not overdubbing for the play head and write head to be separate. this basically lets you use something both like a real time granulator and a normal delay (and all the in-betweens of those).

Yes, of course this is a typical application. (See also: aleph-lines.) Softcut ugen includes a “record offset” parameter. It works. The issue is interpolating writes with fractional rates. More complex than interpolated reads. With rate lag, you sweep through fractional rates on the way to negative rates and there is a glitch. I will fix this given some spare time - there is a working build for x86 on softcut-resample branch but it has some issue on ARM, so I need a proper debug session on the metal.

Finally I should point out that using softcut as a standard delay line, as you describe, it doesn’t really matter what rate it’s running at (unless you are going for decimation artifacts on purpose, or something.) In exactly the same way that tape speed is not a critical parameter for tape delay if you can have arbitrary distance between heads. So just leave rate=1 in such applications and manipulate the offset parameter.


I think my problem with the delay line application was more with the play head and write head being out of phase with each other (which screwed up where the first beat of the delay fell (like, delaying the delays if that makes sense)), but I could just just be confusing myself. might also clarify that I’m saying this in an mlr/granular thing context where the heads are jumping around. but yea also different problem than what you’re describing.

took a deeper look at the norns MLR, but I’m running into a bit of a brick wall with understanding the buffer reference routing. for my final version of cranes, I thought I was setting up voice 2 to read the buffer created from voice 1, but negative values still destroy playback on voice 2.

I’ve kinda hacked together my understanding of softcut's commands from the engine’s sc code. I’m definitely not asking you to dive into my script, but if you’re able: what would a basic command workflow be to tell softcut to record into a buffer with a single write head and reference that buffer with a separate read head that can be manipulated into reverse playback?

1 Like

+1 to this question; and related - is engine(2,x) a separate play/record head or a separate ‘piece of tape’?

@dan_derks i’m travelling right now, without norns or linux system, but will try to put something together in a couple days. meantime i’ll see i can spot anything untoward using SoftCut in SC on mac, right now.

the former. there is actually just one large buffer in the engine. by default the different voices are set to access different, non-contiguous and non-overlapping regions of the buffer. each “voice” consists of a read and a write head, which move in lockstep with an arbitrary offset between them. by default the offset is a small (few samples) negative value, putting the write head just behind the read head. so the default behavior is that you hear the old material while writing new material. if you want it to act as a delay, the offset should be positive with rate > 0 and negative with rate < 0.

in fact now that i think about it, this could be related to the issue? if you have negative rate, and you want to hear old material, and you have record enabled on that voice, you should set the offset to be small and positive, rather than small and negative.

[edited the above bit: i confused myself and swapped signs the first time. offset is defined as (read position) - (write position)]

@andrew i’m sorry but i’m not really following your point (/question?). read/write offset with a buffer is just how you make a digital delay/looper, whether the timing is dynamic or whatever, it’s not very mysterious and everything else is implementation detail. the norns SoftCut engine allows you to arbitrarily run some combination of rd/wr heads (4 of each) synchronously, or asynchronously. and i absolutely agree that having both options is necessary to make a wide variety of applications and effects.

as for your problem (?) i’m going to make a wild assumption and guess that you are using max/msp, and a 2nd assumption that you are setting head positions with max messages and (one way or another) running up against the inherent coarseness of timing in the messaging queue. (generally i’d say, “this is a job for gen!” ()) but of course i don’t really know - maybe there is some higher-level design difficulty. we could take a look at your app specifically, on another thread, if you like.


@dan_derks @declutter

just built softcut for mac and loaded it up in supercollider. AFAICT it is working as expected. no lua layer here, but the SC code sends the same engine commands.

in a nutshell:

  • voice 1 and 2 have the same loop endpoints.
  • both are looping
  • voice 1 records, voice 2 doesn’t (rec_on = 1, 0 respectively)
  • voice 2 playback routed to DAC 1+2
  • ADC 1+2 routed to voice 1 record (*)
  • voice 1 rate is always ==1
  • voice 2 rate is changeable


  • toggles rec and pre levels for voice 1
  • scrubs voice 2 rate between -2 and +2
  • arbitrarily resets voice 2 position to an arbitrary location

couple other things to note:

  • rec_on is a hard on/off flag for the record head activation. it will click if you toggle it. rec and pre are smoothed record/erase levels. use the former to put a voice in “play only” mode, use the latter for modulations.
  • i set the loop start point to something > 0, by habit. otherwise, with negative rates, you get potential weirdness at the wrap point.
  • if voice 1 and 2 have the same rate, this is a delay line. phase update is quite accurate and and their relative location will not drift significantly.
  • oh yeah. when voice 1+2 have different rates, there will be clicks when they cross. there is a mechanism in the engine whereby you can set a “ducking” behavior to avoid this (playback level scales to zero when phases are close), but this probably needs a little more tweaking to be really useful.

so here’s the SC script. i should clarify: this needs the norns and dust classes installed, and it needs the norns ugens compiled and installed. it also uses mouse and Qt windows for the test UI. which is an odd setup - i can walk you through it if you’re interested.

s = Server.default;
s.waitForBoot {
	Routine {

		// our own address, where we will send engine commands
		n = NetAddr.localAddr;

		// voices 1 and 2 will access the same 4-second buffer region
		~start = 1.0;
		~end = 5.0;
		n.sendMsg("/command/loop_start", 1, ~start);
		n.sendMsg("/command/loop_end", 1, ~end);
		n.sendMsg("/command/loop_on", 1, 1);
		n.sendMsg("/command/loop_start", 2, ~start);
		n.sendMsg("/command/loop_end", 2, ~end);
		n.sendMsg("/command/loop_on", 2, 1);

		// set up the patch matrix:
		// - from both ADC channels to voice 1 record
		// - from voice 2 playback to both DAC channels
		n.sendMsg("/command/adc_rec", 1, 1, 1.0);
		n.sendMsg("/command/adc_rec", 2, 1, 1.0);
		n.sendMsg("/command/play_dac", 2, 1, 1.0);
		n.sendMsg("/command/play_dac", 2, 2, 1.0);

		// voice 1 records at constant speed, no overdub (default)
		//--- this is a hard on/off flag for rec head
		n.sendMsg("/command/rec_on", 1, 1);
		//--- this is an overdub level, with smoothing
		n.sendMsg("/command/rec", 1, 1.0);

		// voice 2 does _not_ record!
		n.sendMsg("/command/rec_on", 2, 0);
		n.sendMsg("/command/amp", 2, 0.5);
		// give voice 2 some exponential rate lag for stupid blzoops
		n.sendMsg("/command/rate_lag", 2, 0.5);

		// start the things
		n.sendMsg("/command/pos", 1, ~start);
		n.sendMsg("/command/pos", 2, ~start);
		n.sendMsg("/command/start", 1);
		n.sendMsg("/command/reset", 1);
		n.sendMsg("/command/start", 2);
		n.sendMsg("/command/reset", 2);

// stupid UI:
// - mouse Y changes voice 2 rate when dragging
// - 'z' key jumps voice 2 to position indicated by mouse X.
// - 'x' key toggles recording on/off

w =, 0, 400, 400));
l = FlowLayout( w.view.bounds, 10@10, 20@5 );
w.view.decorator = l;
w.view.acceptsMouseOver = true;

w.view.mouseMoveAction = { arg view, x, y, mod, but, click;
	n.sendMsg("/command/rate", 2, y.linlin(0, 400, 2, -2));

~pos = 1.0;
w.view.mouseOverAction = { arg view, x, y, mod, but, click;
	~pos = x.linlin(0, view.bounds.height, ~start, ~end);
	n.sendMsg("/command/pos", 2, ~pos);

~rec = true;
w.view.keyDownAction = { arg view, char, mod, uni, key;
	if(char == $z, {
		n.sendMsg("/command/reset", 2);
	if(char == $x, {
		if(~rec, {
			~rec = false;
			n.sendMsg("/command/rec", 1, 0.0);
			n.sendMsg("/command/pre", 1, 1.0);
		}, {
			~rec = true;
			n.sendMsg("/command/rec", 1, 1.0);
			n.sendMsg("/command/pre", 1, 0.0);


hope it helps… the norns lua version would be quite a bit tidier, and i can post it later - can’t test it now.

(*) BTW @tehn: i noticed that in halfsecond.lua, the arguments for ADC routing are backwards. for both ADC and DAC routing commands, first argument is source, second is sink. so adc_rec(2, 1, [level]) routes the second ADC channel to voice 1 record input. i wonder if this is an issue elsewhere as well.


a great deal. thank you, e. i think my misuse of the commands are the culprit for most of my speedbumps. i’ll try this flow tomorrow morning! :slight_smile:

1 Like

THIS WAS IT. i’ve been keeping the start point 0 on my 2nd varispeed voice. was getting this insane BOOM noise every time it crossed the wrap point at negative rates.

holy moly, ezra, thank you for this direction and spending the time to answer these questions. very excited to pull reverse playback into cranes.

WIP version of cranes with reverse playback:


(mod edit: our discussion of cranes and @declutter’s looper opened up a great deal of SoftCut-specific dialogue, to which @zebra + @andrew were kind enough to contribute thoughts + direction. to preserve the purpose of the code review, I’ve migrated that discussion to this thread)


ok, good to know. i’ll check the wrap logic for negative rates. [ed: yeah it’s a little stupid.] it should not really require a workaround - though i think it is still good common sense to allow some pre- and post-roll, at least equal to the crossfade time. that way you dont have to worry about the ramifications of accessing a completely different part of the buffer during the crossfade.

@tehn looks like this is a potential issue in mlr.lua too.

in case it’s not clear, here’s what happens:

  • softcut performs sample-accurate looping with a crossfade. when a voice hits a loop endpoint, it keeps going for the duration of the xfade time, and another copy of the voice starts at the loop startpoint.

  • with negative rates, reverse “start” and “end” in the above and you see the issue. the phase should correctly wrap to the end of the buffer when you scrub backwards past zero.
    (gritty details:)

the description of the BOOM sound, and the occasional supernova crash reports, led me to fear that i had fumbled this in the worst way and was writing/reading to unitialized memory. but i don’t think this is the case:

  • buffer size is always a power of two. ugen stores a bitmask: mask = (buffer size) - 1.
  • any time an integer phase (sample index) is used (read or write) it is cast to unsigned int and has the mask applied, immediately before use.
    i therefore don’t think there’s any chance that the sample index could ever fall outside the buffer (though indeed, with negative input it could fall on some random place within the buffer - this will be cleaned up!), but please correct me if i’m missing something.
  • but, given that all voices share a buffer, if you expect to be using the start of the buffer, but you wrap to the end during xfade, you could be hearing stuff you didn’t expect to hear, esp. if another voice is using the end of the buf. i’d guess this is the cause of some MLR bug reports.

Been playing with SoftCut, and it’s been really great and straightforward so far. A couple of questions:

  • Is there a way to pan voices in SoftCut? I was attempting to use play_dac, but it wasn’t working quite as I expected.
  • I haven’t gotten clear_range to actually clear a buffer yet, but feel like I must be doing something wrong. It takes frames, and not time right?

works for me, can you be more specific?

[ed] i’ll be more specific.
to pan voice 1 hard left, this works for me:

engine.play_dac(1, 1, 1.0)
engine.play_dac(1, 2, 0.0)

hard right:

engine.play_dac(1, 2, 1.0)
engine.play_dac(1, 1, 0.0)

75/25 left/right:

engine.play_dac(1, 1, 0.75)
engine.play_dac(1, 2, 0.25)

of course you could make a lua func for equal-power panning. the patch levels have some smoothing built in. so here’s equal power:


-- position in [-1, 1]
function xpan_voice(num, pos) 
  local theta = (pos + 1) * math.pi / 4
  local l = math.cos(theta)
  local r = math.sin(theta)
  engine.play_dac(num, 1, l)
  engine.play_dac(num, 2, r)

there’s other pan laws; for -4.5db law you would take the square root of the product of the equal-power and the linear.


ah yeah sorry this is broken in supernova. it is bug. i made an upstream PR
[ ]

but we may ditch supernova anyway:
(issues with supernova load distribution)

(consideration of multple scsynth processes instead:)
[ ]


Ah, this is awesome-- thanks @zebra for that panning code. I was trying something a bit more naive, but that is what I was looking for.

I’m still seeing odd behavior when controlling these values using the encoders. If panned hard left or right: no issue. But when adjusting values using the encoders there is some jerkiness, and occasionally when dialing the level of a channel to 0 it doesn’t come back to full volume (even though I see logging from sc in maiden that seems accurate):

[ PatchMatrix: level_ , 0, 1, 1.0 ]

Now, it could very well be a fundamental misunderstanding some other setup in SoftCut/norns on my part, or I’m passing the wrong value ranges. I’m a new to SuperCollider, so apologies if I’m missing something obvious. Here’s a quick test as a sanity check. It’s a short looping buffer, recording from the inputs. The buttons set hard pan left and right, and then the encoders set the individual channel values directly (I used the code you posted above as well, but this script allowed me to dial in specific levels). Adjusting the encoders doesn’t work as I expect, until hitting zero. And then I notice a drop in volume as I bring them up again. This is usually fixed by using the buttons to hard pan left and right a couple of times.

cool, thanks for the feedback; i’ll take a look


ahh you’re absolutely right; that is a bug; here is a fix

(patch matrix was applying level as levelScale envelope generator parameter. this is, non-obviously, only applied on a new envelope segment. so you could only get the level change by first setting it to 0. which i did, when testing, stupidly.)

thanks again for catching it, and sorry about that.

ehh, in fact duh, there’s no slew now. there used to be. it is steppy. we can ditch the envelope and add a slew. i think the motivation was just to shave a few CPU cycles when patch levels aren’t changing (most of the time) and pause the patch synth entirely when level is zeroed.

19 posts were merged into an existing topic: Norns 2.0: softcut

this thread is purely historical and was created to discuss the softcut engine on norns 1.0 – since then, many new improvements have been made. as syntax has been overhauled, here is a new thread for softcut discussion on norns 2.0: Norns 2.0: softcut

1 Like