Freeing Part of a List of Synths

Hi there -

I’ve been revisiting some things from a project a few years ago, when I asked about making an array of synths and freeing them. The thread is here.

Building off that idea, I’d like to start with a big list of synths, remove some, and then add more, in a task. Something seems to be off about my approach here, though - I was hoping someone could help point me in the right direction.

Thanks.


Task({inf.do{
10.do{~list = ~list.add({SinOsc.ar(800.rand, 0, 0.1)}.play)};

5.wait;

~list.size.do{|i|
	var rand = 2.rand;
	if (rand==0){~list.pop.free;
			~list.size.postln;
			~list.postln;
	}};
5.wait;
}
}).play;
1 Like

When synths are removed, you’re using pop, which is only removed from the end of the list.
This means you’re not genuinely randomizing which synths get removed.
You might want to use removeAt with a random index instead.

EDIT: There might be something more subtle going on as well.

See:

You’re using ~list.size.do which captures the size at the start of the iteration. Then you’re modifying the list (with pop) while iterating through it/ This means you’re trying to access indices that may no longer exist because the list is shrinking as you iterate. (Maybe that’s why you use pop?) I would avoid loops like those.

Suggestion: Try to rewrite with removeAt, and avoid this kind of loop.

I would try two options:

a) interact with the list in reverse.
b) Use copy for iteration

1 Like

Thanks, @smoge.
I definitely understand the removeAt - but I’m not sure I understand what you mean by interacting with the list in reverse or using copy for iteration. How would that work exactly?

1 Like

Avoid trying to access indices that may no longer exist since you are modifying the list you are looping. This is not good code.

In reverse, this problem is irrelevant since the number goes down, and the list change can be balanced with that (since both things remove one element only).

Copying the list is another way to do it. It would be more robust and can be adapted to other situations.

1 Like

You should rewrite this as a while loop. Something like

while { ~list.size != 0 } {
   ... Remove elements and wait...
}

It is the do loop that is the issue for the reasons @smoge suggested - in that you check the size once but it changes, the while loop checks the size every loop. No need for reverse or copys if you do it this way.

Edit: just noticed you don’t want to free everything. In that case, you should randomly choose how many you want to remove first and check when the size is greater than that in the while loop condition.

var size = ~list.size.rand;
while { ~list.size > size } {
   ...
}

Edit2: why was this flagged?

Your post was flagged as inappropriate: the community feels it is offensive, abusive, to be hateful conduct or a violation of our community guidelines.
2 Likes

No “shoulds” here.

The potential downside of using while in SuperCollider’s context is that if the condition never evaluates to false, it could create an infinite loop that will cause other issues. So check that in your code.

One could do it another way, for example, using select .

And so on… and so forth…

EDIT:

With select (or similar method, not tested)

~list = ~list.select { |synth|
            var keep = 2.rand == 1;
            if(keep.not) { synth.free };  
            keep  
};

Differences:

  • No risk of infinite loops
  • No index management issues
  • safer approach (functional and declarative)
  • True Randomness: Each synth has an independent 50% chance of being kept or removed, regardless of its position in the list.
  • Consistent state: we create a new list while processing the old one rather than modifying it while iterating through it.
  • Clarity: The code clearly shows its intent - we select which synths to keep on a condition.

Think of it like a deck of cards:

  • The other method was: “Look at 10 cards; maybe remove the top card of the deck.”
  • The select method is like this: “Look at each card once, and for each one, decide if you’re keeping it or not.”

Example

SynthDef(\simpleSine, { |freq=400, amp=0.1, pan=0|
    var sig = SinOsc.ar(freq, 0, amp);
    Out.ar(0, Pan2.ar(sig, pan));
}).add;


~list = Array.fill(10, {
    Synth(\simpleSine, [
        \freq, rrand(200, 800),
        \amp, 0.1
    ]);
});


~list = ~list.select { |synth|
    var keep = 2.rand == 1;
    if(keep) {
        synth.set(\freq, rrand(300, 600));
        synth.set(\amp, rrand(0.05, 0.15));
    } {
        synth.free;
    };
    keep
}.collect { |synth|
    synth.set(\pan, rrand(-1.0, 1.0));
    synth
};

Adding another difference now:

  • Composition
    a) loop methods: It is not easy to add new operations and requires nesting logic
    b) this method: relatively easy to chain other operations (like the collect here)

Key points:

(synths: List[], params: Dictionary.new)  // Encapsulation of synths and parameters

Env variables, with synth IDs as keys to parameters in a Dictionary.

~valid = { |param| ... }     // Parameter validation function
~transform = { |param| ... } // Parameter transformation function

Implements higher-order functions that operate on Events (parameters), returning Boolean and new Event, respectively.

Collection Methods: select and difference for declarative/functional synth filtering.

synths.select { |synth| ~valid.(params[synth.nodeID]) }  // Filter valid synths
synths.difference(valid)  // Set operation for removal

Each synth’s state is maintained in parallel data structures (List and Dictionary).

~lims = (
    freq: (min: 80, max: 8000),
    amp: (min: 0.002, max: 0.08),
    pan: (min: -0.8, max: 0.8),
    prob: 0.8
);

~valid = { |param|
    param.notNil and: {
        param[\freq].notNil and: 
        param[\amp].notNil and: 
        {param[\freq] < 750} and: 
        {param[\amp] < 0.13} and:
        ~lims.prob.coin
    }
};

~transform = { |param|
    (
        freq: (param[\freq] * rrand(0.5, 2.0)).clip(~lims.freq.min, ~lims.freq.max),
        amp: (param[\amp] * rrand(0.9, 1.1)).clip(~lims.amp.min, ~lims.amp.max),
        pan: if(param[\pan].notNil) { param[\pan] * ~lims.pan.min } { 0 }
    )
};

SynthDef(\sine, { |freq=400, amp=0.1, pan=0, gate=1|
    var sig, env, wet;
    sig = Mix.ar([
        SinOsc.ar(freq, 0, 0.3),
        SinOsc.ar(freq * 2.02, 0, 0.2),
        SinOsc.ar(freq * 0.501, 0, 0.15)
    ]);
    env = EnvGen.kr(Env.asr(2.9, 1, 1.3), Lag.kr(gate, 0.4), doneAction: 2);
    sig = sig * env * Lag.kr(amp, 0.1);
    sig = LPF.ar(sig, freq * 4);
    sig = sig * (1 + LFNoise2.kr(0.5, 0.1));
    wet = CombC.ar(sig, 0.3, LFNoise1.kr(0.2).range(0.1, 0.3), 2) * LFNoise2.kr(0.1).range(0.2, 0.5);
    sig = (sig + (wet * 0.3)) * 0.7;
    Out.ar(0, Pan2.ar(sig, pan));
}).add;

~makeParam = {
    (
        freq: exprand(100, 400),
        amp: exprand(~lims.amp.min, ~lims.amp.max),
        pan: rrand(~lims.pan.min, ~lims.pan.max)
    )
};

~init = {
    var synths = List[];
    var params = Dictionary.new;
    
    30.do {
        var param = ~makeParam.value;
        var synth = Synth(\sine, param.asPairs);
        synths.add(synth);
        params[synth.nodeID] = param;
    };
    (synths: synths, params: params)
};

~manage = { |synths, params|
    var valid = synths.select { |synth| 
        ~valid.(params[synth.nodeID]) 
    };
    
    synths.difference(valid).do { |synth|
        ["Removing", synth.nodeID].postln;
        synth.set(\gate, 0);
    };
    
    valid.do { |synth|
        var param = ~transform.(params[synth.nodeID]);
        ["New  ", synth.nodeID, param].postln;
        synth.set(*param.asPairs);
        params[synth.nodeID] = param;
    };
    (synths: valid, params: params)
};

s.waitForBoot {
    var state = ~init.value;
    Routine({
        inf.do {
            state = ~manage.(state.synths, state.params);
            10.0.rand.wait;
        }
    }).play;
};

For me it looks a bit like

“Take this list.
Keep whichever items meet my condition.
Free (or discard) the ones that don’t.
Transform any I keep (if I want).
Done.”

…rather than

“I have a list. I should walk its indices from 0 to the end—except the end keeps shifting each time I remove an item! Actually, I need to walk it backwards, otherwise I’ll skip over elements or get an out-of-bounds error. But wait, if I remove an item in the middle, the indices after that shift left by one! I’d better keep track of which elements I’ve already visited. Or maybe I can pop from the end, but then I can’t randomly remove items in the middle… so I need random indices. But that might break if the list shrinks too quickly. Where’s my coffee?!”

1 Like
1 Like

Wow, this is amazing. This is actually a far-more nuanced pursuit than I realized and I’m really grateful to understand it better.

One small question - (I’m not sure if I should post this in the main topic thread you’ve created)…but if I attempt to use a second synth, this seems to create a problem:


SynthDef(\crystalBell, { |freq=400, amp=0.1, pan=0|
    var sig = SinOsc.ar(freq, 0, amp);
    Out.ar(0, Pan2.ar(sig, pan));
}).add;

SynthDef(\crystalBell2, { |freq=400, amp=0.1, pan=0|
    var sig = SawDPW.ar(freq, 0, amp);
    Out.ar(0, Pan2.ar(sig, pan));
}).add;

~ensemble = Array.fill(10, {

[ Synth(\crystalBell, [
        \freq, exprand(200, 800),
        \amp, 0.1
    ]), 
	 Synth(\crystalBell2, [
        \freq, exprand(200, 800),
        \amp, 0.1
		])].choose;
});

~ensemble = ~ensemble.select { |synth|
    var survive = 0.7.coin;  // 70% survival rate
    if(survive) {
        // Evolve surviving synths
        synth.set(\freq, exprand(300, 600));
        synth.set(\amp, exprand(0.05, 0.15));
    } {
        synth.free;  //  farewell to departing synths
    };
    survive
}.collect { |synth|
    // Spatial enhancement
    synth.set(\pan, rrand(-0.8, 0.8));
    synth
};
1 Like

This makes two synths and only returns a reference to the other. This means one is left dangling and you can’t free it.

Try

(0.5.coin).if { Synth(...) } { Synth(...) }
1 Like

I am delighted that you liked it so much. Thanks for letting me know

If you put more SynthDefs in this implementation, other parts of the code would need to change. To make this easy, also the SynthDef would have the same argument names,

But also, if you expand this snippet, you can create metadata for each Synth and use that in your conditions.

1 Like