Norns 1.0: softcut

A thread to discuss the SoftCut engine on norns.

any shot you could give more info about this? I have a softcut script that I’m trying to debug (Norns: code review) and I don’t know what’s wonky softcut behavior vs poor code. if we could compare experiences, then it’d be easier to file a report :slight_smile:

Code review! That’s the thread I was looking for. Should have posted there not here.

  • Sometimes it doesn’t work straight after a reboot
  • I haven’t worked out how to clear the buffer; and reset the loop end - so my workaround is to set a very long loop end when clearing the buffer
  • I get random crashes when using this script. CPU usage is over 200% from the moment it starts recording, which seems excessive.

I’ll have a look at your script too, @Dan_Derks - much more elegant than mine.

oh, I hard disagree – cranes is more gilded, but it’s lacking a fundamental elegance like your use of if playing == 0 and recording == 0 then, etc.

thank you for sharing your script! it’s clarifying to read.

do you mean the sound engine is sort of bitcrushed upon reboot? or something else? I’ve run into the former a bit.

No, haven’t had that - have just had it refuse to start until I reload the script. But I’ve not done a rigorous test of when it happens or not, I need to work on that.

Now I’ve looked at your code in more detail I’m going to build something from your foundation rather than mine, I hope that’s OK!

What’s up with reversing the softcut engine? If I run engine.rate(1,-1) while a loop is playing I get all kinds of colourful noise. Do you know anything about how to fix that?

go for it! you might want to build from the cranes in the norns update that’s dropping today, rather than the gist — cleaned some stuff up over the last few days.

I logged a bug for this: https://github.com/monome/dust/issues/193. It might just be unknowns rather than a direct issue with the engine, since mlr is able to reverse the buffer playback, but I’m definitely excited to add it to cranes!

1 Like

i’m pretty sure this artifact is from writing while reversing. in MLR the write head is never also the play head. in halfsecond the play head is also always writing, and just varying pre/rec gain levels.

i’ll fix this, sorry i’ve been so slow, busy couple of months.

3 Likes

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.

3 Likes

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.

2 Likes

@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

UI:

  • 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 {
		2.0.wait;
		Crone.setEngine("Engine_SoftCut");
		2.0.wait;

		// 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);
	}.play;
};


// 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 = Window.new(bounds:Rect(0, 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);
	~pos.postln;
};

~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);
		});
	});
};

w.front;

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.

4 Likes

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: https://gist.github.com/dndrks/c41e3d6a980265817671d17397343919

5 Likes

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

2 Likes