Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugins/classlib: Support multiple RandIDs in one SynthDef #6159

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 91 additions & 2 deletions HelpSource/Classes/RandID.schelp
Expand Up @@ -18,6 +18,14 @@ method::kr, ir
argument::id
The random number generator ID.

argument::force
A Boolean flag. A positive value is 'true', and indicates that RandID should set the synth's random number generator ID on every evaluation, even if the input ID has not changed. Negative or zero is 'false', meaning that RandID should set the synth's random number generator ID only when the input ID changes. Default is 0 (false), for compatibility with versions prior to the addition of this feature.

"Forcing" is useful in the special case where different parts of the same SynthDef graph need to use different random number generators. See examples below.

note:: code::RandID.ir:: will always evaluate only once, at the start of the synth, so the code::force:: argument has no effect in that case. ::


Examples::

code::
Expand All @@ -35,8 +43,8 @@ SynthDef("help-RandID", { arg out=0, id=1;
//reset the seed of my rgen at a variable rate
(
SynthDef("help-RandSeed", { arg seed=1910, id=1;
RandID.kr(id);
RandSeed.kr(Impulse.kr(FSinOsc.kr(0.2, 0, 10, 11)), seed);
RandID.kr(id);
RandSeed.kr(Impulse.kr(FSinOsc.kr(0.2, 0, 10, 11)), seed);
}).add;

)
Expand All @@ -50,6 +58,87 @@ x = Synth("help-RandSeed", [\id, 1]);

//change the target randgen to 2 (affects right channel)
x.set(\id, 2);
::

subsection:: Multiple random number generators and 'force'

In most cases, randomized UGens in a SynthDef may all pull from the same random number generator. In this typical case, it's likely unnecessary to set the ID or re-seed at all.

If, however, one is merging randomized signals from multiple synths into a single synth, and one wants to retain individual control over these separate streams, then the following sequence of evaluation is needed:

numberedlist::
## Set RNG ID (and re-seed).
## Calculate some randomized UGens.
## Set a different RNG ID (and re-seed).
## Calculate some other randomized UGens.
::

This entire sequence needs to be performed, in this order, on every control block.

The original RandID implementation would set the ID only when the input changed. Here, even though the code looks like it should produce two identical pseudo-random sequences, they are different because both LFNoise0 units are using the same RNG (ID = 1).

code::
(
{
var a, b;
RandID.kr(0);
RandSeed.ir(1, 88372);
a = LFDNoise0.ar(440);
RandID.kr(1);
RandSeed.ir(1, 88372);
b = LFDNoise0.ar(440);
[a, b, a - b]
}.plot;
)
::

By "forcing" the RNG ID to be updated on every control block, then the random sequences do match.

code::
(
{
var a, b;
RandID.kr(0, 1);
RandSeed.ir(1, 88372);
a = LFDNoise0.ar(440);
RandID.kr(1, 1);
RandSeed.ir(1, 88372);
b = LFDNoise0.ar(440);
[a, b, a - b]
}.plot;
)
::

note:: In such cases, use a SynthDef and check the order of UGens carefully.

code::
(
SynthDef(\test, {
var a, b;
RandID.kr(0, 1);
RandSeed.ir(1, 88372);
a = LFDNoise0.ar(440);
RandID.kr(1, 1);
RandSeed.ir(1, 88372);
b = LFDNoise0.ar(440);
Out.ar(1000, [a, b, a - b])
}).dumpUGens
)

// correct order! annotated:

// one RNG chain, id = 0
[0_RandID, control, [0, 1]]
[1_RandSeed, scalar, [1, 88372]]
[2_LFDNoise0, audio, [440]]

// another RNG chain, id = 1
[3_RandID, control, [1, 1]]
[4_RandSeed, scalar, [1, 88372]]
[5_LFDNoise0, audio, [440]]

[6_-, audio, [2_LFDNoise0, 5_LFDNoise0]]
[7_Out, audio, [1000, 2_LFDNoise0, 5_LFDNoise0, 6_-]]
::

::
8 changes: 4 additions & 4 deletions SCClassLibrary/Common/Audio/Noise.sc
Expand Up @@ -33,12 +33,12 @@ RandSeed : WidthFirstUGen {

RandID : WidthFirstUGen {
// choose which random number generator to use for this synth .
*kr { arg id=0;
this.multiNew('control', id)
*kr { arg id = 0, force = 0;
this.multiNew('control', id, force)
^0.0 // RandID has no output
}
*ir { arg id=0;
this.multiNew('scalar', id)
*ir { arg id = 0, force = 0;
this.multiNew('scalar', id, force)
^0.0 // RandID has no output
}
}
Expand Down
4 changes: 4 additions & 0 deletions server/plugins/NoiseUGens.cpp
Expand Up @@ -790,9 +790,13 @@ void RandID_Ctor(RandID* unit) {

void RandID_next(RandID* unit, int inNumSamples) {
float id = ZIN0(0);
bool fire = unit->mNumInputs >= 2 && (ZIN0(1) > 0.f);

if (id != unit->m_id) {
unit->m_id = id;
fire = true;
}
if (fire) {
uint32 iid = (uint32)id;
if (iid < unit->mWorld->mNumRGens) {
unit->mParent->mRGen = unit->mWorld->mRGen + iid;
Expand Down
109 changes: 109 additions & 0 deletions testsuite/classlibrary/TestNoiseUGens.sc
@@ -0,0 +1,109 @@
TestNoiseUGens : UnitTest {
var server;

setUp {
server = Server(this.class.name);
}

tearDown {
server.quit;
server.remove;
}

test_RandSeed_sets_seed {
var synth;
var cond = CondVar.new;
var buffer;
var ok = false;
var err;

var seed = 7777;
var size = 128;
// 'drop' b/c empirically, the server side drops one value
var testValues = Pseed(seed, Pwhite(-1.0, 1.0, inf).drop(1))
.asStream.nextN(size);
var serverValues;

server.bootSync;

buffer = Buffer.alloc(server, size, 1);
server.sync;

try {
synth = {
RandSeed.ir(1, seed);
RecordBuf.ar(WhiteNoise.ar, buffer, loop: 0, doneAction: 2);
Silent.ar(1)
}.play(server).onFree {
ok = true;
cond.signalAll
};
cond.waitFor(0.1, { ok });
if(ok.not) { Exception("synth timed out").throw };

ok = false;
buffer.getn(0, size, { |data|
serverValues = data;
ok = true;
cond.signalAll;
});
cond.waitFor(0.1, { ok });
if(ok.not) { Exception("b_getn failed").throw };
} { |error|
if(error.isMemberOf(Exception)) {
err = error // my error
} {
buffer.free;
error.throw // unknown error
}
};

buffer.free;
this.assert(
ok and: { testValues == serverValues.as(Array) },
err.tryPerform(\errorString) ?? { "random values on server were seeded identically to the language" }
);
}

test_RandID_forceArgSetsIDForEveryBlock {
var synth;
var cond = CondVar.new;
var ok = false;
var osc;
var countNonMatching;
var err;
var seed = 18624;

server.bootSync;

synth = {
var x, y;
var timer = Line.kr(0, 1, ControlDur.ir * 2, doneAction: 2);
var seed = 36163;

#x, y = Array.fill(2, { |i|
RandID.kr(i, 1);
RandSeed.kr(1, seed);
WhiteNoise.ar
});

SendReply.kr(Done.kr(timer), '/count', A2K.kr(Integrator.ar((x absdif: y) > 0)));
Silent.ar(1)
}.play(server);

osc = OSCFunc({ |msg|
countNonMatching = msg[3];
ok = true;
cond.signalAll;
}, '/count', server.addr);
cond.waitFor(0.1, { ok });

osc.free;

if(ok) {
this.assertEquals(countNonMatching, 0, "Two RNG IDs, seeded equally, should produce 0 non-matching values");
} {
this.assert(false, "timed out")
};
}
}