Norns: softcut studies

I’ve been trying a bunch of different things out, so I may have misremembered the test results, but I believe that if rec_level is 0, then even if pre_level is 0 for the duration of a whole loop, when you turn pre_level back to 1, the content is still in the buffer. Let me test again real quick :zipper_mouth_face:

EDIT: i was wrong, pre_level still applies even when rec_level is 0

you may be right. if that’s the case it’s an oversight (an attempted “optimization” maybe.)

but i will say:
if rec is zero (the flag, not the level) then the entire write routine is skipped, and there will be no erasure. so occams razor thinks: maybe misremembered.

1 Like

actually i don’t think there is a good reason for this. post-roll material is needed for crossfading. (pre-roll if rate is negative.) but sudden silence in the pre/post period doesn’t help and will cause clicks. it is more important to set loop points such that there is some material after the loop end and before the loop start.

I think what happened was I am still on the fence about whether or not I wanted pre_level to deteriorate the loop when recording is “disarmed” lol. So I just remembered “huh, changing rec_level didn’t do what I wanted” (i.e., didn’t allow the loop to stop deteriorating), but then when I changed what I wanted, I assumed that changing rec_level did the other thing.

Anyway, I think I have the tools now to go either direction depending on which of those I choose – I’ll change rec_level no matter what, and I’ll either also change pre_level to 1.0 when “disarming”, or I’ll leave pre_level alone :+1: Thanks for your help! And happy to hear that the bug is fixed in some upcoming version :smile:

your last suggestion is exactly what i do in mlr, works wonderfully once you wrap your brain around it

1 Like

If I have a running loop with content, and I want to layer on a one-shot recording, how would I do that? I know I can set softcut.loop(recording_voice, 0), but that will just record the portion of the loop from when it’s called until the end of the loop. E.g., if it’s called a second into the loop, then that first second will not be recorded. I’m thinking I want to somehow know exactly when the loop is starting, and call it right then. As far as I can tell, the closest you can get to doing that is using the phase polling system, something like:

softcut.phase_quant(recording_voice, loop_dur)
softcut.event_phase(function(voice, position)
  softcut.loop(recording_voice, 0)
  softcut.rec_level(recording_voice, 1.0)
end)
softcut.poll_start_phase()

Does this guarantee (via a phase of loop_dur, assuming that’s the length of the loop in seconds) that it’ll be called basically right at the start of the loop?

Then the next step: what if I don’t want to wait until the start of the loop? I want to record from right now back around to this same position in the loop again. I could use softcut.phase_quant(recording_voice, current_position) (and make some modifications to the event_phase callback) , but as far as I can tell there’s no way to know the current_position

I guess I could just choose a pretty short phase_quant, on first callback record the position, and treat that as “current position” (so: start recording, then inside the callback see if we’ve made our way back around yet, and if we have stop recording). That may be sufficient (with a very short phase_quant, the “lag” between button press and recording starting would be very short as well), but I was wondering if I was missing something

Does this guarantee (via a phase of loop_dur , assuming that’s the length of the loop in seconds) that it’ll be called basically right at the start of the loop?

if the loop start point is divisible by the phase quantum, then it should work fine to use the phase poll.

e.g. if loop start point is 6.25s and the phase is reported every 1.25 seconds then you can track the value of the poll and know when a loop has just begun.

in the new version (sorry) there is an additional poll that fires on loop points.

using polls, there will be some latency/jitter (audio block size -> OSC -> lua event loop,) but it may be OK for the purpose.

we could also consider adding “meta commands” like “record next loop from the beginning” (which would just modulate rec_level appropriately.) on the backend, this kind of thing can be sample-accurate.


finally, you could just not use softcut’s loop points at all, and instead use a metro on the lua side to trigger 1-shot record and playback. (you would want to do this anyway in a case where you wanted loop timing to be independent of playback rate; if you want it to act more like tape then the phase poll is probably the way to go for now.)

4 Likes

Thanks for the reply, I’d forgotten I’d asked the question . . . !

Thanks for this! This is what I ended up going with and it works just fine – I hadn’t thought to use “non-softcut” tools here, but metro is basically perfect.

I’m now trying to figure out how to “duplicate” a portion of the buffer most effectively back-to-back. I’ve thought about a level_cut_cut approach that uses spare record heads to pipe the currently playing loop into another head that’s writing to a different portion of the buffer, but that seemed more complicated (and also doesn’t happen offline). I’ve now started using buffer_write / buffer_read to basically bounce the buffers to disk then read them back out:

local id = string.match(os.clock(), "....$")
local full_path = "/home/we/dust/audio/boros/tmp-"..id..".wav"
softcut.buffer_write_stereo(full_path, 0, loop_dur)
softcut.buffer_read_stereo(full_path, 0, loop_dur, loop_dur)

this works, but it seems a little silly, so I was wondering if there’s some feature I’m missing? There’s also a small click at the discontinuity that gets introduced where the two buffer portions meet. The reason for doing this vs just looping on the shorter buffer is so that you can add a longer recording on top of a shorter loop.

1 Like

hm, we don’t have any commands like “copy/mix,” but they would be straightforward to add. the work is a little clunky because of (too many) glue layers:

on the crone/c++ side:

(note that for this we don’t need to add to Commands enum because there is no work to do on the audio thread.)

and then on the lua/matron/C side:


added GH issue in case anyone is interested.


i’m guessing that’s because you are reading and writing exactly the loop points, and not accounting for (pre/)post-roll.

with positive playback rates, the buffer is actually read from / written to up to (loop end + fade time).

with negative rates, up to (loop start - fade time).


you may event want to go for a clock if you’re interested in making the looper syncable with midi/link?

4 Likes

Even if I write to disk a bit past loop end, won’t there still be a discontinuity mid-loop because softcut.buffer_read_stereo fully overwrites at start_dst, vs the playheads which would have a bit of cross-fade?

Ooh that does sound interesting, esp. as it’s definitely meant to be used with MIDI/Link

if the playback rate is always positive: no.

crossfades in softcut begin fading in from the loop start point, and begin fading out from the loop endpoint.

this means that the very beginning of the material at (loop_start) will actually be silent.

but on the other hand, timing within the loop is respected: if you start playback at (wall clock = 0), then at (wall clock = 1s) you will be hearing material from exactly 1s from the beginning of the source media (if you read it into the buffer at loop_start.)

in other words: this is the only way to do it given that a command to initiate a crossfade can occur at any time and we want the xfade to begin “as soon as possible.” otherwise, (if we wanted the material at loop_start to be fully audible, after a pre-roll period,) accurate timing would require control latency of (fade_time / 2), or something.

ok, sorry… i guess i’m not taking the context into consideration.

your problem is: duplicating (in time; viz., “loop duplication”) a buffer region that has been written by a softcut process, with post-roll during the fade period. so let’s say

a = loop_start
b = loop_end
f = fade_time
X(t) = buffer contents at time

and you perform the assignment

X(b:(b+(b-a)) = X(a:b)

then you will indeed get a discrepancy, because the actual interval over which softcut records is [a, b + f], so there is a discontinuity at X(b).

that’s one reason it kinda needs a dedicated operation. a “copy” command could easily apply a fade-out (or just a straight mix) of existing material over the interval [b, b+f] when copying.

5 Likes

Can someone help, please? The application is generating audio, sending it to the outputs, and now I’m trying to record the returned wet audio from the norns inputs. My files always come up as silent.

first, just very quickly:

there are no obvious major problems leaping out at me, it should basically work? maybe it will occur to me (or @dan_derks or someone.)

some minor things though:

  • i would not put all that stuff in record.start(). most of it is “setup” and routing and only needs to happen once.
  • rec is a flag and need only be set once; use rec_level at runtime.
  • play is also a flag and can be false since you’re not listening to anything.
  • might want to set pre_filter_dry to full for the voice, and all the other pre-filter modes to zero.
  • in record.stop(), you are writing the entirety of both buffers to a stereo file. since your capture is mono, you probably want buffer_write_mono with a duration corresponding to the actual capture duration. (if you want to capture in stereo you need to use two softcut voices, each pointing at a different buffer.)

more importantly

i would not actually use softcut for this. it’s a specialized tool for real-time varispeed sampling/resampling/filtering/etc, and it’s expensive.

since all you’re doing is capturing the input and saving to disk, and you’re already using a custom SC engine, just add something to your engine using SoundIn.ar([0, 1]) and DiskOut.ar. (look up the relevant helpfiles in SCIDE.) this will be a lot more efficient and probably less verbose, and free up softcut if you want to mix in live-processing functionalities. (e.g., sampling/layering directly in the box from supercollider, i dunno.)

if your ultimate plan is to allow mlr/cheatcodes-style manipulation of the captured buffer, then carry on.

3 Likes

Thank you. Going the SC route is what Dan recommended to me as well. I thought this would be a quick path to an MVP.

Once more unto the breach, dear friends, once more… how did you learn SC? It’s still beating the crap out of me.

Learning all the things: https://www.youtube.com/watch?v=3vu4UbS2NMw

indeed, lots of love expressed on the forum for those vids.

personally i find it very hard to learn from videos and have never managed to sit through one of these. (but, i have been using supercollider since v1 in 1998 - a very differnet beast! - and am therefore not the target audience.)

but at the risk of stating the obvious, start with the extensive builtin help files in the supercolider IDE:

  • open a new file
  • type DiskOut and select it
  • hit ctl-D

you get an HTML help file, usually with runnable examples. the help files vary in depth and quality but most are quite thorough and clear. there are also help files for system-level overviews, such as "Client vs Server" which IMHO is mandatory if you are going to use this software.

you can do this with any Class or .method name. ctl+I will take you directly to class implementations. (classes are sometimes are thin wrappers around compiled implementations, but often are just implemented in SC themselves.)


anyways, here’s a little class that performs a simple capture of the stereo inputs:

SimpleStereoCapture {

	var ioBuf;
	var captureSynth;
	var server;

	*new { arg server;
		^super.new.alloc(server);
	}

	alloc { arg s;
		server = s;
		ioBuf = Buffer.alloc(server, 65536, 2);

		// register a new synthdef on the server
		SynthDef.new(\simpleStereoCapture, {
			arg buf;
			DiskOut.ar(buf, SoundIn.ar([0, 1]));
		}).send(server);
	}


	start { arg path;
		// write the IO buffer and leave it open
		ioBuf.write(path, "wav", "int24" , 0, 0, true);
		captureSynth = Synth.new(\simpleStereoCapture, [\buf, ioBuf], server);
	}

	stop {
		// free the capture synth
		captureSynth.free;
		// close the sound file
		ioBuf.close;
	}
}

here’s a test to be evaluated line-by-line with appropriate paths:

~cap = SimpleStereoCapture.new(Server.default);

~cap.start("/home/emb/Desktop/capture-test-1.wav");
~cap.stop;


~cap.start("/home/emb/Desktop/capture-test-2.wav");
~cap.stop;

you could stick a SimpleStereoCapture in your engine and register commands to control it from lua.


eh… guess i should say, this is not ready to use as written. it needs safety checks: when starting a capture, check if captureSynth is nil; otherwise free the old one and close the IO buffer before opening the buffer and creating a new synth. exercise for reader

2 Likes

Just read that. Very helpful.

Thank you for the class. I’ll reverse engineer and learn!


edit: Okkkaaayyy things are making sense now. Language > Recompile Class Library. :bulb:

1 Like

hey hey, fwiw i ended up digging into your softcut recorder example and got things moving!

code
record = {}

function record.init()
  softcut.buffer_clear()
  softcut.enable(1,1)
  softcut.buffer(1,1)
  softcut.level_input_cut(1,1,1.0)
  softcut.level_input_cut(2,1,1.0)
  softcut.rec_level(1,0)
  softcut.pre_level(1,0)
  softcut.loop_start(1,0)
  softcut.loop_end(1,300)
  softcut.loop(1,0)
  softcut.rate(1,1)
  softcut.rec(1,1)
end

function record.start()
  softcut.position(1,0)
  softcut.rec_level(1,1)
end

function record.stop(filename)
  softcut.rec_level(1,0)
  softcut.buffer_write_mono(filename,0,300,1)
end

the big thing was establishing the loop points – even if we don’t want the head to loop, we need to tell it what its bounds are so it knows how far to go after we set its position to 0. also folded in ezra’s excellent points about rec + play and about doing params setup in the init.

2 Likes