Suggestions for simplifying nested Demand sequence

hey,

im currently trying to come up with a solution to subdivide an array into groups of unequal sizes, which i then can repeat individually. I have found a solution which works for all the test cases i have tried out so far, beside numGroups == numBuffers (but thats okay).
For subdividing the groups into unequal lengths im using Dbjorklund2. I have the feeling that the attempt can be simplified. The main problem is to derive a trigger from the withinGroupIndex, which should only advance on group boundaries, but not on wrapping boundaries based on the repeating cycles with Ddup. Then one could probably get rid of calculating the groupIndex and deriving yet another trigger i guess. Im happy about any suggestions. I know thats a bit tricky :slight_smile:

(
{
	|numGroups = 2, repeatGroup = 2, numOfBuffers = 5|
	//|numGroups = 3, repeatGroup = 3, numOfBuffers = 11|
	
    var trig, wrapTrigger, changeGroupTrig, resetTrig;
    var withinGroupIndex, groupIndex, baseOffset, bufferIndex;
    var step;
    
    trig = Impulse.ar(2);
    
    // Create a counter for position within the current group
    withinGroupIndex = Demand.ar(trig, DC.ar(0),
        Dseq([Dseries(0, 1, Ddup(repeatGroup, Dbjorklund2(numGroups, numOfBuffers)))], inf)
    );//.poll(trig, "withinGroupIdx");
    
    // Detect when withinGroupIndex wraps back to 0
    wrapTrigger = HPZ1.ar(withinGroupIndex) < 0 + Impulse.ar(0);
    
    // Create a counter for which group we're in
    groupIndex = Demand.ar(wrapTrigger, DC.ar(0),
        Ddup(repeatGroup, Dseq([Dseries(0, 1, numGroups)], inf))
    );//.poll(trig, "groupIdx");
    
    // Detect when the group index changes
    changeGroupTrig = Changed.ar(groupIndex) + Impulse.ar(0);
    
    // Detect reset (back to group 0)
    resetTrig = (groupIndex <= 0) * changeGroupTrig;
    
    // Get the step size
    step = Demand.ar(changeGroupTrig, resetTrig,
        Ddup(repeatGroup, Dbjorklund2(numGroups, numOfBuffers))
    );//.poll(changeGroupTrig, "step");
    
    // Create an accumulator that only advances when we change to a different group
    baseOffset = Demand.ar(changeGroupTrig + resetTrig, resetTrig,
        Dseries(0, step, inf)
    );//.poll(wrapTrigger, "baseOffset");
    
    // Final buffer index
    bufferIndex = (baseOffset + withinGroupIndex).poll(trig, "bufIdx");
    
    Silent.ar;
}.play;
)

With numGroups = 2, repeatGroup = 2, numBuffers = 5 i get a sequence of [0,1, 0, 1, 2, 3, 4, 2, 3, 4, 0, 1, …]
which when using a multiplexer for buffer selection works perfectly at audio rate (used some different shapes here to visualize the result):

When dealing with these kind of Demand structures, i always think something is missing. I cant really tell what it is. I often need a trigger which advances only on a subdivision of the main trigger like phrase or group boundaries. Maybe an “end of ramp trigger” which is fired when the accumulator wraps around (here with HPZ1) or an Integrator which i have tried to implement here with an accumulator with variable increments.
I think basically these are single-sample feedback problems, where im trying to find workarounds for (where im often times overusing Ddup to sample and hold a specific value, gets even more exciting when polling from the same demand stream), like increment a counter for each trigger and look up the next item from a list which is then determining when its time to increment again to look up the next item. Maybe setup a resusable function which incapsulates Dbufrd / Dbufwr.

Could you post code without the custom Dbjorklund2?

hey,

the specific testcase for Dbjorklund2(numGroups, numOfBuffers), where numGroups = 2 and numOfBuffers = 5 would lead to Dbjorklund2(2, 5)[2, 3]. This could therefore be swapped for Dseq([2, 3], inf), however this would miss the whole point of dynamically subdividing the array to groups of unequal size, with just one numGroups param, which is the whole point of using Dbjorklund2 here. Or more precisely it will take care of the case where the division of numBuffers / numGroups doesnt lead to an integer value.

But you could test that with Dseq([2, 3], inf), and would manually have to create other arrays for other testing scenarios, for example numGroups = 3, numOfBuffers = 11 would lead to Dbjorkdlund2(3, 11) → [4, 4, 3]:

(
{
	|numGroups = 2, repeatGroup = 2, numOfBuffers = 5|
	//|numGroups = 3, repeatGroup = 3, numOfBuffers = 11|
	
    var trig, wrapTrigger, changeGroupTrig, resetTrig;
    var withinGroupIndex, groupIndex, baseOffset, bufferIndex;
    var step;
    
    trig = Impulse.ar(2);
    
    // Create a counter for position within the current group
    withinGroupIndex = Demand.ar(trig, DC.ar(0),
        Dseq([Dseries(0, 1, Ddup(repeatGroup, Dseq([2, 3], inf)))], inf)
    );//.poll(trig, "withinGroupIdx");
    
    // Detect when withinGroupIndex wraps back to 0
    wrapTrigger = HPZ1.ar(withinGroupIndex) < 0 + Impulse.ar(0);
    
    // Create a counter for which group we're in
    groupIndex = Demand.ar(wrapTrigger, DC.ar(0),
        Ddup(repeatGroup, Dseq([Dseries(0, 1, numGroups)], inf))
    );//.poll(trig, "groupIdx");
    
    // Detect when the group index changes
    changeGroupTrig = Changed.ar(groupIndex) + Impulse.ar(0);
    
    // Detect reset (back to group 0)
    resetTrig = (groupIndex <= 0) * changeGroupTrig;
    
    // Get the step size
    step = Demand.ar(changeGroupTrig, resetTrig,
        Ddup(repeatGroup, Dseq([2, 3], inf))
    );//.poll(changeGroupTrig, "step");
    
    // Create an accumulator that only advances when we change to a different group
    baseOffset = Demand.ar(changeGroupTrig + resetTrig, resetTrig,
        Dseries(0, step, inf)
    );//.poll(wrapTrigger, "baseOffset");
    
    // Final buffer index
    bufferIndex = (baseOffset + withinGroupIndex).poll(trig, "bufIdx");
    
    Silent.ar;
}.play;
)

I can’t say I’ve digested every detail here, but I’m wondering… take that [2, 3] example. I think currently you’re counting (“within” index) 0, 1, 0, 1, 0, 1, 2, 0, 1, 2 and looking for a higher level trigger after 4 indices.

What if instead it counts 0, 1, 2, 3, 0, 1, 2, 3, 4, 5? Then the higher level trigger is just the HPZ < 0, and the “within” index is this count modulo the size of the group.

Well, maybe you considered they already and rejected it for some other reason… in any case, it may be easier than trying to suppress some but not all of the HPZ triggers in your original version.

hjh

hey,

thanks this was a great idea :slight_smile: I have made another version which doesnt need the additional Changed, which is quite nice (still work in progress). Currently im wondering if the baseOffset calculation could additionally be simplified (we want to reset the baseOffset, whenenver the groupSequence starts from the beginning). I think withinGroupIndex, groupTrigger and groupSize are solid now :slight_smile:

(
{ |numGroups = 2, repeatGroup = 2, numOfBuffers = 5|

    var trig;
    var withinGroupIndex, groupTrigger, groupSize;
	var groupIndex, resetTrigger, baseOffset;
	var bufferIndex;

    trig = Impulse.ar(2);

    // Create a counter that wraps at group boundaries
    withinGroupIndex = Demand.ar(trig, DC.ar(0),
        Dseq([Dseries(0, 1, repeatGroup * Dseq([2, 3], inf))], inf)
    );

    // Detect when counter wraps (group change)
    groupTrigger = HPZ1.ar(withinGroupIndex) < 0 + Impulse.ar(0);

    // Get the current group size, used for the base offset and the within-group index modulo
    groupSize = Demand.ar(groupTrigger, DC.ar(0), Dseq([2, 3], inf));

	///////////////////////////////////////////////////////////////////////////

	// Create a counter for which group we're in
    groupIndex = Demand.ar(groupTrigger, DC.ar(0), Dseq([Dseries(0, 1, numGroups)], inf));

    // Detect reset (back to group 0)
    resetTrigger = HPZ1.ar(groupIndex) < 0 + Impulse.ar(0);

    // Create an accumulator with variable increment
    baseOffset = Demand.ar(groupTrigger, resetTrigger, Dseries(0, groupSize, inf));

	/////////////////////////////////////////////////////////////////////////

    // Final buffer index
	bufferIndex = (baseOffset + (withinGroupIndex % groupSize)).poll(trig, "bufIdx");

    Silent.ar;
}.play;
)

EDIT: one additional thought i had was to approach everything from “outside → in” (e.g subdividing a main ramp into groups and events), but dont know if thats more straight forward without single-sample feedback:

sequenceIndex = Demand.ar(trig, DC.ar(0),
	Dseq([Dseries(0, 1, numGroups * numOfBuffers)], inf)
);

groupSize = Demand.ar(trig, DC.ar(0),
	Ddup(repeatGroup, Ddup(Dbjorklund2(numGroups, numOfBuffers), Dbjorklund2(numGroups, numOfBuffers)))
).poll(trig);

I think it would be very cool, if this Ddup hack could be a Demand ugen:

(
{
    var trig, stream, demand;
    trig = Impulse.ar(2);
	stream = Ddup(2, Dseq([2, 3], inf));
	demand = Demand.ar(trig, DC.ar(0), Ddup(stream, stream)).poll(trig);
    Silent.ar;
}.play;
)