This weekend's silly question: Pattern, type: \phrase and synths not freeing

Hi all, I have this pattern here that I like quite a bit (some absolutely demented Ferneyhough musicality), but it tends to build up a lot of synths very quickly (usually in the hundreds after 15 seconds or so, which is absolutely unnecessary). Since I’d ultimately like to run this kind of stuff with more complicated SynthDefs and maybe for longer, I’m wondering what I can do to keep that from happening. I’m using the default synth for now, which has a doneAction: FreeSelf.
Tried:
\sendGate, true, (but this is the default already, right?)
\callback, {|event| event[\addToCleanup] = {event.free} }
and rewriting the outer, phrase-type pattern as a Routine.
No real difference for all I can tell. (I might try to do a less random MWE to be able to check this more systematically…).

Anyway, the inner pattern is this:

Pdef(\inner, { | amplo = 0.7, amphi = 1.1, durlo = 0.7, durhi = 1.2, midilo = -1, midihi = 2 |
	Pbind(
		\instrument, \default,
		\midinote, Pseries(start: 60 + 12.rand, step: Pbrown(midilo, midihi, 1), length: 1 + 22.rand).loop,
		\amp, Pgeom(start: 0.1, grow: Pbrown(amplo, amphi, 0.1), length: 1 + 22.rand).loop,
		\dur, Pgeom(start: 0.1, grow: Pbrown(durlo, durhi, 0.1), length: 1 + 22.rand).loop, //max possible dur of individual note here is 4.6 seconds (highly unlikely)
	)
});     

//This is relatively well behaved on its own if I call stop periodically on the Pdef:
fork { loop {s.numSynths.postln; Pdef(\inner).play; {exprand(1,3)}.wait; Pdef(\inner).stop}};

//However, I'd like to schedule this through an outer, phrase-type pattern: 
(
Pbind(     
	\type, \phrase,
	\instrument, \inner,
	\dur, Pexprand(1, 3, inf),
	\amplo, Pseq([0.7, 1.05, 0.9, 1.1], inf),
	\amphi, Pfunc { |ev| ev.amplo + 0.2.rand}, // already edited as suggested by PitchTrebler
	\durlo, Pseq([0.7, 0.6, 0.9, 0.8], inf),
	\durhi, Pfunc { |ev| ev.durlo + 0.3.rand}, 
	\midilo, Pseq([-1, 0, -2, -3], inf),
	\midihi, Pseq([2, 3, 0, 1, 1], inf),
).play;
fork { loop {s.numSynths.postln; 1.wait}};
)

I’m not quite sure what’s going on here, and whether I’m using this more or less as intended.
I did a bit of poking around and the phrase type events have an internal Pfindur, which calls an EventStreamCleanup, maybe there’s something not being cleaned up there that should be?

Hi,

When an Event plays a gated synth, it schedules the release for the future, but it doesn’t monitor the behavior of other events, so if synths accumulate too rapidly, it will never know and it won’t do anything about it.

An addToCleanup in an event callback also won’t help, because the adding-to-cleanup happens in the pattern, before the event is yielded. Event play time is too late.

I think what you’re looking for is a voice-stealing synth manager, such as the ddwVoicer quark.

v = Voicer(20, \default);

p = Pbind(
    \type, \voicer,
    \voicer, v,
    ... everything else as normal...
).play;

That puts a cap of 20 synths onto it; when the pattern tries to exceed that, it will force-close envelopes (quick release by gate <= -1) on the oldest nodes. Set the max number of nodes as you like, and then the pattern shouldn’t have to worry about it.

hjh

1 Like

There are a couple of problems with these lines:

\amphi, Pkey(\amplo) + {0.3.rand};
\durhi, Pkey(\durlo) + {0.5.rand};
  1. They should end with a comma, not a semicolon, since they are part of an argument list.
  2. They will produce BinaryOpFunctions at the \amphi and \durhi keys. These then get passed into PbrownPgeom, which will output a BinaryOpFunction, but what you want in the end is numeric values.

You can fix this in the phrase function by calling amphi.value and durhi.value before passing them to the Pbrown. But I think the better solution is to correct them in the outer Pbind. Here I put all the logic inside of a Pfunc because (for me) that makes it easier to read:

(
Pbind(     
	\type, \phrase,
	\instrument, \inner,
	\dur, Pexprand(1, 3, inf),
	\amplo, Pseq([0.7, 1.05, 0.9, 1.1], inf),
    // \amphi, Pkey(\amplo) + {0.3.rand};
    \amphi, Pfunc{|ev| ev.amplo + 0.3.rand},
	\durlo, Pseq([0.7, 1.1, 0.9, 1.2], inf),
    // \durhi, Pkey(\durlo) + {0.5.rand};
    \durhi, Pfunc{|ev| ev.durlo + 0.5.rand},
	\midilo, Pseq([-1, 0, -2, -3], inf),
	\midihi, Pseq([2, 3, 0, 1, 1], inf),
).play;
fork { loop {s.numSynths.postln; 1.wait}};
)
1 Like

Thank you both!
yeah, I just noticed the semicolons (had something else there originally, replaced it yesterday as I was posting here and accidentally introduced the semicolons). But of course today I just copypasted the stuff I posted yesterday and wondered why it didn’t work… And I was also why I got away without using .value before, so that explains that.

I assume regular Patterns or Pfuncs don’t require the .value call?

In the meantime, I still get synth buildup with the Voicer (maybe slightly less), and even if I have a Routine running that periodically calls .deepFreeMsg (or .freeAllMsg) during Rests on the group that the synths are running in. Not sure why, I guess it’s some scheduling issue.

Is there a way that I can make a Pattern tell me the NodeIDs of the synths it has instantiated or is about to schedule (I guess no, or only through the callback, since Nodes live server-side)? Or force the server to constrain the range of NodeIDs from that pattern and have it round-robin between those, such that I can then free all the Nodes in that range?

Using this ‘simple’ synthdef which forces a gate open, I don’t get buildup of synths:

(
SynthDef(\simple, {
	var sig = LFSaw.ar(\freq.kr(220));
	var env = Env.asr(0.01, 1, 0.1).kr(2, \gate.kr(0) + Impulse.kr(0));
	Out.ar(\bus.kr(0), sig * env * \amp.kr(0.1) ! 2)
}).add;

Pdef(\inner, { | amplo = 0.7, amphi = 1.1, durlo = 0.7, durhi = 1.2, midilo = -1, midihi = 2 |
	Pbind(
		\instrument, \simple,
		\midinote, Pseries(start: 60 + 12.rand, step: Pbrown(midilo, midihi, 1), length: 1 + 22.rand).loop,
		\amp, Pgeom(start: 0.1, grow: Pbrown(amplo, amphi, 0.1), length: 1 + 22.rand).loop,
		\dur, Pgeom(start: 0.1, grow: Pbrown(durlo, durhi, 0.1), length: 1 + 22.rand).loop
	)
});    
)

(
Pbind(     
	\type, \phrase,
	\instrument, \inner,
	\dur, Pexprand(1, 3, inf),
	\amplo, Pseq([0.7, 1.05, 0.9, 1.1], inf),
	\amphi, Pfunc { |ev| ev.amplo + 0.2.rand}, // already edited as suggested by PitchTrebler
	\durlo, Pseq([0.7, 0.6, 0.9, 0.8], inf),
	\durhi, Pfunc { |ev| ev.durlo + 0.3.rand}, 
	\midilo, Pseq([-1, 0, -2, -3], inf),
	\midihi, Pseq([2, 3, 0, 1, 1], inf),
).play;
fork { loop {s.numSynths.postln; 1.wait}};
)
1 Like

nice… that would suggest that this is an issue with the default synthdef then! I hadn’t even considered that. Thanks!

Not specifically that, but, sometimes a piece of code accidentally requests for a synth to be created and then immediately closed, duration less than 1.5 ms or so (depending on sample rate and block size). In that case, there is no way to tell the difference between “I’m creating this synth with a zero gate, with the intention of triggering it later” and “I’m asking to play a note with an imperceptible duration.” There are a lot of threads here about this.

If you know in advance that the use case is “always trigger and immediately release,” then this can be expressed in the SynthDef as Thor described. The default SynthDef doesn’t assume this – hence it’s not exactly an issue, just a matter of being based on an assumption that’s different from yours.

If you’re creating a very high density of notes, and using Voicer for note control, and the voicer is set to too small a limit, then it increases the likelihood of immediately cutting off notes and leaving the synths hanging. Voicer assumes that the limit is reasonable for the use case. Generating a hundred notes at once with a limit of 20 notes in the Voicer is likely to cause trouble, whereas, if you’re generating, say, 4-note chords in 16th-notes at 120 bpm, and the limit is even as low as 4, that shouldn’t be a problem because the forced cutoffs will not be immediate.

EDIT: Except, this case works just fine –

v = Voicer(20, \default);

v.gate(Array.fill(100, { exprand(200, 800) }), 2, 0.2, [amp: 0.01]);

// see "20s" in the server status bar

And I see my mistake – the event type is \voicerNote :man_facepalming:

This does (mostly) cap at 20 synths:

p = Pbind(
	\type, \voicerNote, \voicer, v,
	\freq, Pexprand(200, 800, inf).clump(50),
	\dur, 0.1,
	\amp, 0.01
).play;

p.stop;

hjh

1 Like

Thanks for the explanations! To be clear, I did not mean to disrespect the default synth :wink: … so not an “issue” in the sense of bug, but more that I hadn’t considered that I would have to change anything about the synthdef (because I thought it was something about the pattern/phrase business that I had misunderstood).

Ok, I think I get it now: Without the extra Impulse (and without the Voicer) Synths are created but never start playing, so they cannot ever free themselves. And presumably they do not start playing because the close-gate message arrives in the same control block as the create-synth message. Is that it?

Slightly OT, but I’d like to know what the default synthdef looks like, but haven’t been able to find where that is defined in the class library… or does it exist only on the server?