Different RandSeed for the same SynthDef

Hello, if I have a SynthDef with a 30 randomized params, how can I decide to control the first 15 params by a specific RandSeed and the 15 other by another one ? So far, I am just able to use one RandSeed for all the synth.
Thanks a lot

Anyone please ? Impossible de find any examples on the documentation or on the forums. So like:

(
SynthDef(\Rand, {

/// one RandSeed for thoses :
var aaa =  rrand(1, 10);    
var aaa2 = rrand(10, 20);

/// another RandSeed for thoses :
var bbb  = rrand(100, 1000); 
var bbb2 = rrand(1000, 2000);

/// but both staying in the same SynthDef

}).add;

)

Why would you want to do this? Randomness is supposed to be uniform, so it makes no difference to the distribution if you change the seed, or get the next value.

The reason why you would want to set a random seed in the first place is so that many synths can have the same random value for each subsequent call to the random number generator. If many synths are sharing the same parameters, then it would be better to generate all the values at once, store them, then initialise the synths with the same values.

You’re slightly mistaken about how this is working: rrand is called when you BUILD the SynthDef - this is controlled by thisThread.randSeed - setting this seed will control the seed value for future calls to rrand, so you can do something like this:

thisThread.randSeed = 10;
var aaa =  rrand(1, 10);    
var aaa2 = rrand(10, 20);

thisThread.randSeed = 11;
var bbb  = rrand(100, 1000); 
var bbb2 = rrand(1000, 2000);

These random values are now hard-coded into your SynthDef structure - not sure if this is desired or not.

RandSeed resets the random seed used for UGen’s for a Synth while it’s running. This would apply to things like TRand and WhiteNoise. You can’t straightforwardly use two RandSeed UGens in your SynthDef, since UGens aren’t necessarily run in the order that you specify them in code, so placing a RandSeed in between your two sets of parameters may not mean much - and in any case, random values are random values, changing the seed in between will just result in more random values :). If you want reproducible “random” values for a group of parameters, I’ve used a pattern like this:

SynthDef(\random, {
   var baseSeed, makeRand, seed1, seed1, a, b, c,d;
   baseSeed = rrand(0, 1000); // this can be deterministic also....
   makeRand  = {
        |min, max, seed|
        baseSeed = baseSeed  + 1;
        Hasher.ar(baseSeed + (seed * 1000)).linlin(-1, 1, min, max)
   };
   seed1 = \seed1.kr(10);
   seed2 = \seed2.kr(11);

   a = makeRand.(0, 100, seed1);
   b = makeRand.(0, 400, seed1);
   c = makeRand.(0, 10, seed2);
   d = makeRand.(-0.3, 0.3, seed2);
});

Hasher produces a (deterministic) random number based on an input value. Your input value is a combination of (1) a synth-global value you increment each time you run the function (so that each makeRand call produces a different value), and (2) a seed value you pass in. With this setup, you can bump a seed to re-randomize ALL the values that use that seed. Your values will be random, but depend on baseSeed - this can be hard-coded (so the “random” values are the same ever time), or also randomized in whatever controlled way you want.

4 Likes

That’s clear, thank you taking your time to clarify this.

Sorry to reopen this, thanks a lot for the explanation.

How would it be possible to run inside a SynthDef two LFNoise0.ar(1) and LFNoise0.ar(2) which have different frequencies but outputs the same list of values?

With two SynthDefs I can make this, but I have no idea how to make it within a single SynthDef:

(
SynthDef(\test, {arg seed, freq;
	RandSeed.ir(1, seed);
	Out.ar(0, SinOsc.ar(LFNoise0.kr(freq).range(440, 880), 0, 0.4));
}).add;
)
a = Synth(\test, [\freq, 3, \seed, 1000]);
b = Synth(\test, [\freq, 2, \seed, 1000]);

But setting the RandSeed only makes each instance static, but not all of them equal:

{RandSeed.ir(1, 500);[LFNoise0.ar(500),LFNoise0.ar(1000), LFNoise0.ar(2000), LFNoise0.ar(4000)]}.plot

Moreover, how would it be possible to run inside a SynthDef two LFNoise0.ar(1) and LFNoise0.ar(2) which have different frequencies but outputs the same list of values alongside with other two LFNoise0.ar(3) and LFNoise0.ar(4) which have different frequencies and also different list of values (different RandSeed for each group of oscillators)?

1 Like

as far as I can see, this is currently not possible:


(
{ 
	var a, b;
	RandID.ir(0);
	RandSeed.ir(0,1789);
	a = LFNoise1.ar(2000);
	RandID.ir(1);
	RandSeed.ir(0,1789);
	b = LFNoise1.ar(2000);
	[a, b]
}.plot
)

This doesn’t produce the same form twice, I think because the rand id is set only once and not at each cycle. This was one of my first projects in supercollider, and I didn’t think of that (if I remember correctly).

1 Like

Would it be interesting to have a variant of LFNoise0 that has a seed (maybe as a parameter) that cold be reused/shared?

I also came across other pseudo-random algorithms, that claim to be more efficient and more ‘random’, focused on the opposite, splitting generators. (Not sure any of this would be useful for sc)

1 Like

@hobbes Lazy as i am. I would use something like CuspN or for this. :grinning:

edit: or pick one of these ChaosGen.subclasses and try to find some initial values that sounds interresting.

2 Likes

Perlin noise can be a good candidate for very controllable and deterministic noise.
https://doc.sccode.org/Classes/Perlin3.html

Though iirc there are some settings when generating perlin noise that are not exposed here, so it may have limited flexibility?

1 Like

I can never get RandSeed to do my bidding personally, so I jerry-rig everything together with Hasher, which I use as a deterministic white noise source, a way to randomize parameters seeded by MIDI note number, etc. Can elaborate on request.

(Nerdy inside baseball stuff: Ultimately I believe the RGen architecture in scsynth wasn’t the right choice. Every UGen with randomness should have just accepted an explicit seed, which would not only offer more control for the user but would reduce global state, precluding thread safety issues with the shared prng. This take is unequivocally correct to the point that I am not taking questions or comments on this :wink: )

3 Likes

I second that.

But it also reminded me of colleagues who recorded pink noise from a beautiful analog generator, so they could work with “truly random noise.” I’m not sure it did make any difference, but, as soon as you recorded it, is it still random or just a sequence of numbers in your computer?

1 Like

It would be cool to have this as a new feature, if possible!

Cunning solution, I will definitively try this out!

This implementation of Perlin noise have some bugs, right? I’ve tried to explore it but it shows some really strange behavior like triangular waveforms and impulse trains…

Please elaborate further! If you can post some code example of this it would be nice as well!

I’ve checked the sources, and in principle, it would work if we call the RandID for every block:


(
{ 
	var a, b;
	RandID.kr(0);
	RandSeed.ir(0,1789);
	a = LFNoise1.kr(200);
	RandID.kr(1);
	RandSeed.ir(0,1789);
	b = LFNoise1.kr(200);
	[a, b] // should be the same
}.plot(0.5)
)

The problem seems to be in the setting of the RandID:

But this would have to be checked. I can’t do this myself at the moment.

1 Like

Thanks for the help!! Unfortunately, even when the RandSeed is the same, the results are not equal…

(
{ 
	var a, b;
	RandID.kr(0);
	RandSeed.ir(0,1789);
	a = LFNoise1.kr(200);
	RandID.kr(1);
	RandSeed.ir(0,1789);
	b = LFNoise1.kr(200);
	[a, b] // a!=b
}.plot(0.5)
)

(
{ 
	var a, b;
	RandID.kr(0);
	RandSeed.ir(0,1789);
	a = LFNoise1.kr(200);
	RandID.kr(0);
	RandSeed.ir(0,1789);
	b = LFNoise1.kr(200);
	[a, b] // a!=b
}.plot(0.5)
)

Here’s how I get seeded randomness in scsynth in a way that completely avoids the above-discussed fiddliness of RandSeed. (This method brings its own limitations and downsides, and again it would have been better if all nondeterminstic ugen had a deterministic seed.) Hasher takes any floating point value and produces another floating point value that’s ideally uncorrelated and ranges from -1 to 1. It’s a little bit like sampling a huge infinite brick of white noise, with its input being the simple index. This configuration

Hasher.ar(Sweep.ar)

is just white noise, and will always be the exact same white noise regardless of any surrounding context, unless the sample rate is changed. (This highlights another flaw of the RandSeed/RGen system – the random behavior is sensitive to the UGen graph changing IIRC, whereas Hasher has no context-dependence whatsoever.) I type this almost automatically when I’m working on drum SynthDefs and want my noise bursts to be deterministic for a colder, digital sample sound that is exactly the same every trigger.

If Sweep.ar is retriggered periodically, the white noise becomes a periodic white noise oscillator:

Hasher.ar(Sweep.ar(Impulse.ar(100)))

I love the sound of this oscillator with a fundamental in the low-mid range, great for making fake xrun-type glitches.

If you need multichannel versions of the above, just enter random additive offsets to Sweep.ar. In theory this makes one channel a delayed version of the other, in practice if that delay is large it will be indistinguishable from uncorrelated noise:

Hasher.ar(Sweep.ar + [0, 43758, 1234])

Or you can multiply the Sweeps by different rates, doesn’t matter much.

Triggered randomness (TRand, etc) equivalent is Hasher.ar(Latch.ar(Sweep.ar, trigger)). Smooth value noise like LFNoise1, LFNoise2 can be emulated roughly with a Ramp or .lag filter on triggered randomness. You can get some (low-quality) pink noise by adding fractal LFNoise2s. Random trigger via a Bernoulli process are ((Hasher.ar(Sweep.ar) + 1) / 2) < (1 / 1e6). If you need distributions other than uniform, you can use inverse transform sampling, etc.

For an array of constant random values: Hasher.kr(seed + (0..15)). “Seed” will give you a different set of 16 values when changed, make sure no two seeds are related by integer offsets less than 15. Something I do often is us a SynthDef’s “freq” control as the offset/seed for numerous Hasher UGens to randomize parameters. This allows me to simulate imperfections in mallet and keyboard instruments where certain properties are consistent across time but inconsistent across keys, for example a harmonium with one reed out of tune.

More complex random UGens like Gendy pretty much have to be re-emulated from scratch, which is annoying and not always possible. In general this approach is a royal hack and the code can get cryptic — I get a lot of questions about copious Hasher use in my code — but it does always work, unlike RandSeed.

3 Likes

All makes sense to me, I agree. In the case of Gendys, a good splittable generator, providing several parallel deterministic high-quality generators would be be optimal option. We can’t do without it, nathan, someone has to fix it.

I don’t see what you mean by splitting a generator. There’s no statistical difference between taking even/odd numbered outputs from an ideal RNG and having two separately seeded ideal RNGs generated independently.

1 Like

Well, you avoid threatening seeds. That’s exactly the point, statistically it’s as good as independent generators (which is not the case with other algorithms)

Yes, exactly this was my point – it doesn’t work, but should.

I think I’ve found the problem.

void RandID_next(RandID* unit, int inNumSamples) {
    float id = ZIN0(0);

    if (id != unit->m_id) {
        unit->m_id = id;
        uint32 iid = (uint32)id;
        if (iid < unit->mWorld->mNumRGens) {
            unit->mParent->mRGen = unit->mWorld->mRGen + iid;
        }
    }
    ZOUT0(0) = 0.f;
}

should be

void RandID_next(RandID* unit, int inNumSamples) {
    float id = ZIN0(0);

    if (id != unit->m_id) {
        unit->m_id = id;
   }
   if (iid < unit->mWorld->mNumRGens) {
        uint32 iid = (uint32)id;
        unit->mParent->mRGen = unit->mWorld->mRGen + iid;
   }
    ZOUT0(0) = 0.f;
}

I just can’t test it right now.