Thank you for the information, and sharing the project, its very nice!

Question 1:
How would one go about adding a “Feedback” control for a Delay?

EDIT: Found Rymd and I’m trying to use that as an example of how to do feedback amount, but it seems a bit touchy so far and some gain values make it get louder each repeat (?)

Question 2:
What do you think about a LongDelay module with something like:

'DelayTime' -> (
    Spec: ControlSpec(1, 120, 'lin', 0, 4, "sec"),

I’m trying to add a LongDelay module by copying Delay in R.sc, but I can’t seem to get it to work. (even just copying Delay and changing just it’s name)

I get an error unable to create "Delay". invalid module type "LongDelay" in maiden when starting my test script. I’ve ;restarted SC after making changes to R.sc and setup an appropriate listing in r.lua. What else am I missing?

Is your code posted anywhere? Here’s a commit where I added a pink noise module. It’s a super simple example, but maybe it will be useful to see what was involved.

1 Like

@okyeron @Justmat

Hi! Very sorry, I’ve refactored R engine considerably but old left-overs are not removed yet and docs are not updated. Modules are now defined in the main r.scd file (yeah, it’s in one big file :slight_smile: ) :

See the stdModules array here:

3 Likes

Specifically, re your questions.

Question 1:
In Rymd I just loop back the audio with a gain stage in between. It is, of course, possible to add internal feedback in the Delay module (I guess using a local buffer would do). With an external path it’s possible to pass the signal that is fed back through additional modules - in Rymd there’s a filter in the feedback path, and IIRC the signals are being cross-fed left / right.

Question 2:
A module with longer delay makes sense. See above link for how modules now are created. The code alone likely will make sense (I’ll try to update the docs soon). Just make sure 120 secs do not allocate too much memory for the PI to handle.


By the way - to get a faster feedback loop I spent some time to create a “norns engine tester” kind of capability for prototyping scripts in SuperCollider on a regular computer. This only covers the subset of norns features I actually use in my scripts and is currently undocumented + unsupported.

Anyhow - with this you should be able to run a bunch of my scripts (ie. bleeding edge versions moln, rymd, bob and skev) on the computer. Just open the files and evaluate the content.

3 Likes

Thnx!

I’ll look at r.scd tonight.

What would be a quick/easy way to determine this on the pi?

I know top will show me cpu use…

(120 was just a arbitrary number - 60 may be plenty?)

Welp… looks like anything over 20 seconds barfs with this:
JackDriver: exception in real time: alloc failed, increase server's memory allocation (e.g. via ServerOptions) (on Pi4 2GB). Have not attempted to change via ServerOptions.

increasing server memory can be done by uncommenting here:

[ https://github.com/monome/norns/blob/master/sc/core/Crone.sc#L70 ]

and in fact should probably be done in main branch since we aren’t using supernova.


but also another way is to replace DelayC with BufDelayC, and supply a buffer for max delay size. buffers are allocated from the heap and limited only by physical memory. (should have some hundred MB, so minutes not seconds.)

this option could be noticeably less performant if you are making a large number of delays. but on balance i think most SC folks would agree it is better to use the heap than to make server mem allocation very large.

4 Likes

OK so… trying to do this and failing so far. The following doesn’t get me any sound. (numFrames below for 60 seconds)

snipped from r.scd

		ugenGraphFunc: {
			|
				in_In,
				in_DelayTimeModulation,
				out_Out,
				param_DelayTime,
				param_DelayTimeModulation,
				visual_DelayTime
			|
			var buffer = Buffer.alloc(numFrames: 48000*60, numChannels: 1);
			
			var sig_In = In.ar(in_In);
			var sig_DelayTimeModulation = In.ar(in_DelayTimeModulation);

			var delayTimeSpec = ControlSpec(0.5, 120, 'lin', 0, 4, "sec");

			var delayTimeSec = delayTimeSpec.map(
				delayTimeSpec.unmap(param_DelayTime) + (sig_DelayTimeModulation * param_DelayTimeModulation)
			);

			var delayed = BufDelayC.ar(buf: buffer.bufnum, in: sig_In, delaytime: delayTimeSec); 

			Out.ar(out_Out, delayed);
			Out.kr(visual_DelayTime, delayTimeSec);
			buffer.free;
		}

examples show a server as a variable for Buffer.alloc, do I need to do that here or is the default server assumed? If I need it what do I use? (context.server? Server.default? )

b = Buffer.alloc(s,44100,1);

In SuperCollider buffers need to be allocated and freed outside of the ugenGraphFunc. Here you’ve stumlbed on a limitation with the current r framework - which abstracts away a lot of things and does not deal with buffers custom to modules yet. So the BufDelayC route is not really feasible right now. I can look into whether it can be supported in some way (by providing a way to create buffers for modules), but right now it’s not as easy as with writing an engine from scratch in SuperCollider.

1 Like

I’m new to SuperCollider so I may be missing something, but: I’ve definitely made buffers inside my SynthDefs before and had them work just fine… Is there something I’m doing wrong, or misunderstanding how my plain ol SynthDefs may be different from how R is constructed?

(For example, here’s the Delay pedal for Pedalboard and an Amp simulator that uses a buffer for an efficient transfer function/wavetable)

I think it’s due to how R is constructed?
see here for where the modules get setup: https://github.com/antonhornquist/r/blob/5395bab6158f94107a1d3e1a898f85e689da320f/r.scd#L1822

As far as I can tell that func is just sent to synthdef.new https://github.com/antonhornquist/r/blob/5395bab6158f94107a1d3e1a898f85e689da320f/r.scd#L1333

Back to your original question: have you tried sending context.server to Buffer.alloc's server argument?

var buffer = Buffer.alloc(server: context.server, numFrames: 48000*60, numChannels: 1);

like @jah, i am surprised that this is possible, but it seems like limitations are always changing in SC-land.

it doesn’t seem like SynthDef.wrap is supplying any special magic.

however, @okyeron you definitely don’t want to call buffer.free until you’re done with the synth.

and @21echoes i can’t immediately see where those buffers are being freed. (are buffers allocated in a ugen graph func, freed automatically when the synth is freed? seems worth checking for leaks.)

1 Like

That’s probably true – I free any synths I make with that synthdef, but I never explicitly free those buffers or anything. If I understand the synthdef/synth boundary properly, it’s probably also the case that if I made multiple synths from that same synthdef they’d be sharing the same buffer? Because the synthdef happens once to create the ugen graph, not once per synth creation?

that’s what i’d guess too - the ugen raph function is executed on the client, a buffer is allocated but never freed… should be easy to verify one way or another.

to my eyes it’s a very odd usage though and i can’t really reason about it. it “used to be” that you needed to use LocalBuf to declare a buffer within a synthdef function. (“local buffers” are allocated from the server’s memory pool and not from the heap, IIRC, and each is specific to a Synth instance.)

i would have probably made it so that a Pedal has to explicitly do the bookkeeping for its buffer resources, just like you have done for its bus resources. but it’s nice to learn new tricks.

1 Like

I’ve never done this level of SC debugging before – how would I check if the buffer gets freed?

Also, apologies for derailing this thread a bit! Hopefully it’s been somewhat helpful for y’all :zipper_mouth_face:

hm, not super elegant but this seems to work:

~countAllocatedBuffers = { arg server;
	var bufs = List.new;
	server.cachedBuffersDo({ arg buf;
		bufs.add(buf.bufnum);
	});
	postln("there are " ++ bufs.size ++ " buffers allocated: " ++ bufs);
};
1 Like

catching up…

I believe I tried this and it errored (?). I’d need to go again to get the error it generated.

I am defintely “stumbling” around here. :grin:

@21echoes FWIW - errors:

R is initializing... 
ERROR: Message 'server' not understood.

... etc ...

^^ The preceding error dump is for ERROR: Message 'server' not understood.
RECEIVER: Frame (0x1bbcfb0) of Interpreter:functionCompileContext

While it is technically possible to allocate buffers in the SynthDef ugenGraphFunc - and each R “module type” (ie. MultiOsc) just end up as a SynthDef as @21echoes correctly noted - this isn’t the right approach here. Buffers allocated in a SynthDef ugenGraphFunc would be shared between Synths based on that SynthDef.

SC Example
s.boot;

(
SynthDef('test', {
	var buf1, buf2;
	buf1 = Buffer.alloc(s, 1234);
	buf1.bufnum.poll(trig: 1, label: 'buf1_bufnum');
	buf2 = Buffer.alloc(s, 1234);
	buf2.bufnum.poll(trig: 1, label: 'buf2_bufnum');
}).add;
)

a=Synth('test');
a.free;
b=Synth('test'); // compare polled buf1_bufnum/buf2_bufnum printouts - these synths use the same pair of buffers ...
b.free;

c=Synth('test'); d=Synth('test'); // ... even if they're running at the same time

The handling of the ugenGraphFunc passed to a SynthDef is one of SuperCollider’s many quirks. Given how dynamic the language is, as a newcomer one believes the ugenGraphFunc is evaluated for every Synth that is created from a SynthDef. But, it is in fact evaluated at SynthDef creation time and language semantics are very different in it.

The “Static versus Dynamic Elements” section in the docs for SynthDef elaborates on this:

… A def’s UGen graph function (and the SC code within it) is evaluated only when the def is created. Thus ‘if’ statements, etc. will have no further effect at the time the def is used to create a Synth, and it is important to understand that a UGen graph function should not be designed in the same way as functions in the language, where multiple evaluations can yield different results. It will be evaluated once and only once .

This is weird and you learn it by experience. :slight_smile:

Anyway, @okyeron if you get it to work in R your delay line would be shared between multiple “module instances” (= what you get when you invoke engine.new("MyOsc", "MultiOsc") = a Synth based on the MultiOsc’s SynthDef), which I do not think you want (two delay modules would write into and read from one and the same buffer). Also, as noted above the life cycle of the buffer would be suboptimal - a really long buffer would always be present and take up memory, regardless if whether you use the module or not, and it wouldn’t be deallocated in a proper way.

To solve this we’d have to have a way in R - which essentially is a very regulated/constrained way of creating SynthDefs in order to standardize modules and make them interconnected - to specify custom buffers. Ie:

Idea
(
	name: 'LongDelay',
	description: " ... description ... ",
	inputs: [
		// .. input metadata ...
	],
	outputs: [
		// .. output metadata ...
	],
	params: [
		// .. params metadata ...
	],
	custom_buffers: [
		'DelayLine' -> (
			NumFrames: 120 * 44100,
			NumChannels: 1
		),
	],
	ugenGraphFunc: {
		|
			in_In,
			in_DelayTimeModulation,
			out_Out,
			param_DelayTime,
			param_DelayTimeModulation,
			visual_DelayTime,
			bufnum_DelayLine
		|

		var sig_In = In.ar(in_In);
		var sig_DelayTimeModulation = In.ar(in_DelayTimeModulation);
		var buffer = Buffer.alloc(numFrames: 48000*60, numChannels: 1);

		var delayTimeSpec = ControlSpec(0.1, 5000, 'exp', 0, 300, "ms");

		var delayTimeMs = delayTimeSpec.map(
			delayTimeSpec.unmap(param_DelayTime) + (sig_DelayTimeModulation * param_DelayTimeModulation)
		);

		var delayed = BufDelayC.ar(buf: bufnum_DelayLine, in: sig_In, delaytime: delayTimeSec); 

		Out.ar(out_Out, delayed);
		Out.kr(visual_DelayTime, delayTimeMs);
	}
)

This needs a bit of non-trivial frameworky stuff, but I’ve done something similar for upcoming sample support so I might be able to reuse it in some way.

2 Likes