Thats really cool ![]()
Have you also implemented the roateUgens function? Was curious about that one ![]()
Thank you.
Yep I implemented the rotateUgen function cause I don’t find anywhere in SC (vanilla) an equivalent, maybe I missed something ?
hey, i have separated the velvet logic into trigger generation and sign operation. The dwhiteUni function creates uniformly distributed random periods i guess similiar to ARS velvet noise (additive random sampling) and the bias parameter between -1 and 1 creates a probability for values of -1 or 1. You can also add amplitude variation with the flux parameter.
(
var dwhiteUni = { |rate, randomness|
var subDiv = Ddup(2, Dwhite(1.0 - randomness, 1.0 + randomness)) / rate;
Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};
var dwhiteExp = { |rate, randomness|
var subDiv = Ddup(2, (2 ** (Dwhite(-1.0, 1.0) * randomness))) / rate;
Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};
var multiChannelDwhite = { |triggers|
var demand = Dwhite(-1.0, 1.0);
triggers.collect{ |localTrig|
Demand.ar(localTrig, DC.ar(0), demand)
};
};
var multiChannelSign = { |triggers, bias, flux|
var sign = (Dwhite(0.0, 1.0) < ((bias + 1) * 0.5)) * 2 - 1;
var amp = Dwhite(0.0, 1.0) * flux;
triggers.collect{ |localTrig|
Demand.ar(localTrig, DC.ar(0), sign * (1 - amp));
};
};
{
var numChannels = 8;
var numSpeakers = 2;
var reset, tFreq;
var events, voices, windowPhases;
var grainFreqMod, grainFreqs, grainPhases, grainWindows;
var grainOscs, grains, sig, pan;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreq = dwhiteUni.(\tFreq.kr(30), \randomness.kr(0.5));
events = SchedulerCycle.ar(tFreq, reset);
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[\trigger],
rate: events[\rate] / \overlap.kr(5),
subSampleOffset: events[\subSampleOffset],
);
grainWindows = HanningWindow.ar(
phase: voices[\phases],
skew: \skew.kr(0.03)
);
grainFreqMod = multiChannelDwhite.(voices[\triggers]);
grainFreqs = \freq.kr(800) * (2 ** (grainFreqMod * \freqMD.kr(2)));
grainPhases = RampIntegrator.ar(
trig: voices[\triggers],
rate: grainFreqs,
subSampleOffset: events[\subSampleOffset]
);
grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);
grainOscs = grainOscs * multiChannelSign.(voices[\triggers], \bias.kr(0), \flux.kr(0.75)).poll(voices[\triggers]);
grains = grainOscs * grainWindows;
pan = multiChannelDwhite.(voices[\triggers]);
grains = PanAz.ar(
numChans: numSpeakers,
in: grains,
pos: pan.linlin(-1, 1, -1 / numSpeakers, (2 * numSpeakers - 3) / numSpeakers);
);
sig = grains.sum;
sig = LeakDC.ar(sig);
sig * 0.1;
}.play;
)
Nice! there is one more cool thing that I have previously discovered with velvet noise (I haven’t tried this in SC yet):
If you randomly flip the polarity of the L and R channels independently of the trigger, you then get some really cool stereo phasing effects
My gen code for this, where n is L, and n2 is R. If stereo param is 0, R = L
noise_norm(){
return abs(noise());
}
velvet_noise_stereo(density, bias, stereo){
n, n2 = 0;
//compute the threshold values
high = 1 - density;
//compute the output value
nx1 = noise_norm();
if(nx1 > high){
low = density * 2;
if(nx1 <= (high + low)){
nx2 = noise_norm() + (bias * 0.5);
n = nx2 <= 0.5 ? -1 : 1;
nx3 = noise_norm();
if(nx3 < stereo){
nx4 = noise_norm() + (bias * 0.5);
n2 = nx4 <= 0.5 ? -1 : 1;
} else {
n2 = n;
}
}
}
return n, n2;
}
I guess we could bind the pan position to the phase inversion or ?
Yes, but parameterising the probability of stereo polarity flip as well. Something like this?
~multiVelvet = { |triggers, density = 0.05, bias = 0, stereo = 0|
var densityGate = Dwhite(0, 1, inf) > (1 - density);
var nx1 = (Dwhite(0, 1, inf) < (0.5 + (bias * 0.5))) * 2 - 1;
var n = Dswitch1([0, nx1], densityGate);
var nx2 = (Dwhite(0, 1, inf) < (0.5 + (bias * 0.5))) * 2 - 1;
var n2 = Dswitch1([0, nx2], densityGate);
n2 = Dswitch1([n2, n], (Dwhite(0, 1, inf) < stereo));
triggers.collect{ |localTrig|
Demand.ar(localTrig, DC.ar(0), [n, n2]);
};
};
Not exactly sure how to map the signal to the input - im not great at multichannel sc stuff
Im not really sure what we want to achieve here. Do we want to place the velvet noise after we have been summing the channels or before or between panning and summing?
Lets say we have numChannels = 5 and numSpeakers = 2.
// here ?
grains = grainOscs * grainWindows; // 5 channels
grains = PanAz.ar(numSpeakers, grains, pan);
// or here?
sig = grains.sum; // 2 channels
// or here ?
As far as i understand the paper its using a sparse noise signal mostly 0s and occasional -1s or 1s for convolution. But instead of using FFT processing they use a multi-tap delay line with a fixed sequence of -1s and 1s. They are not creating the values dynamically and also not multiplying by 0s. Thats really different from what we are doing here. The approach might be related but the result couldnt be more different, so what are we trying to achieve here? ![]()
Right – I have read that paper, but not using the velvet noise for convolution – I’m using it to simply flip the polarity of each grain voice at random: This adds phase cancellation between the voices, which removes the sound of the window at fast trigger rates. The stereo param is supposed to randomly flip the polarity of each grain voice’s L/R pair independently, but use the same density trig – this adds stereo depth through phase cancellation.
Here is an example from my m4l device, from polarity randomization at 0, to polarity param at 1, to stereo param at 1 also. I’m essentially trying to recreate the stereo param part in sc now.
It’s not essential, but is a cool technique I thought I’d share!
hey, thanks alot for your explanation. The sound example is cool ![]()
I think what i dont understand is why we would like to multiply by zeros?
Thats also not the case when following the convolution example from the paper and even if we would convolve with a zero then that would mean the delay tap is just 0 but not the amplitude of the signal.
I would assume that its desirable to flip the phase by a ternary operator either -1 or 1.
In my opinion gating the signal is completely independent from this sign flip and more an operation which should be done on the trigger generation trig * PulseCount.ar(trig, \prob.kr(1));
Additionally i also dont understand why we would like to flip the phase at two stages a.) on grain voices b.) on spatialized and summed signals, isnt this redundant?
Im currently working on another unit shaper called UnitRandom, a phasor driven Ugen to create random interpolated numbers between 0 and 1, so these could have a trajectory per grain and can also be synced to your phasor driven clock. I guess there will also be a mix param where you could interpolate between stepped and interpolated values. Lets see ![]()
finally got around to playing around with this. amazing!
so uh, if someone were to compile this with the maximum number of channels set at 4096 or something, what would be the likely outcome? Asking for a friend
Hey thanks. Im not sure why you would need 4096 channels of polyphony, but feel free to try that out ![]()
Would you say we would need lerp between stepped and interpolated values, or just a binary switch or no lerp at all and just the interpolated values?
(
{
var phase = Phasor.ar(DC.ar(0), 1000 * SampleDur.ir);
var randStepped = UnitRandom.ar(phase, 0);
var randSmooth = UnitRandom.ar(phase, 1);
[phase, randStepped, randSmooth];
}.plot(0.041);
)
I was investigating different Splines for interpolation. Splines are so cool ![]()
The Inline cubicinterp function uses a cubic hermite spline with catmull-rom tangents, which is C1 continuous but can cause overshooting for steep tangents caused by big jumps via random values.
So i have used non-polynomial interpolation (cosine) for now, which is also C1 continuous for segments but C0 continious at joints but doesnt have the overshooting problem (Abletons smoothed LFO also uses cosine interpolation).
That video is really awesome ![]()
I have thought about this once more. We are currently deriving a trigger from the phase to be used for the interpolated random values internally for UnitRandom. What we want to do is to use this with our multichannel output from VoiceAllocator, either phases or triggers to be sure its in sync with our clock.
I was thinking if we could have a new prefix alongside the “Unit” prefix for the UnitShapers which shape the phase of a linear ramp signal (UnitKink, UnitTriangle, UnitCubic etc. and the window functions) called “Step” for trigger based stuff. Then i would not have to derive the trigger from the ramps internally but we could pass it directly from VoiceAllocator. I could rename the ShiftRegister Ugen into StepRegister and add StepRandom andStepWalk for a random walk etc. and some others.
EDIT: nvm, figured it out of course, 5 minutes after asking the question.
Answers:
- Yes, the VoiceAllocator samples and holds the rate value it gets at trigger time;
- Yes, to set params for individual grains, use a Demand UGen with voices[\triggers] as the trigger.
nvm
Hi, I’m trying to port over a (probably vastly inefficient) Pattern-based approach to this, and I’m struggling with the following. Maybe I just completely misunderstood something there:
Assume I have some function f(n) that determines overlap for each grain individually, where n is the order in time of the grain as it goes in to the VoiceAllocator (i.e., first grain is 0, second grain is 1, etc.), how/where do I plug f(n) in?
Pattern example (simplified):
Pbind(
delta: 0.001,
overlap: Pseries(1, 10/700, 700).loop, // what I mean by f(n);
)
// to clarify: I'm porting this to demand, so the question isn't about how to use patterns with GrainUtils!
Ideally I want that particular overlap f(n) only to affect the grain n (and not other grains that are already running), so I figure this needs to be multichannel… but the input to VoiceAllocator is not yet multichannel, right? Or does it simply sample and hold the rate value for that grain that it got at trigger time, even it changes later?
On the other hand, if I index into the multichannel output of the VoiceAllocator (voices[\phases]) and change overlap per grain, 1. I’m messing with the results of the allocation algorithm, so that would be bad; and 2. I’m not even sure how the indexing works, i.e., if I want to modulate grain number n, what is its index in the voices[\phases] array? Your examples mostly have probabilistic UGens (Dwhite) for FM, unless I’ve missed something, so it’s hard to tell…
However, still not sure how to approach the other part of the question:
If I want to set some parameter per grain , is there a predictable way of getting the channel index of a particular grain in the voices array? Since the voice allocator isn’t strictly round robin, I’m not sure how to approach this.
recycling the question from before:
Assume I have some function f(n) that determines freq (or some other param) for each grain individually, where n is the order in time of the trigger as it goes into the VoiceAllocator (i.e., first grain is 0, second grain is 1, etc.), how/where in voices do I plug f(n) in? Is this also solved via a demand?
(Fwiw, this is the background of the joke about 4096 channels: I have a lot of overlap there…)
If you use the Multichannel trigger of the Voice allocator for your demand Sequence each new Value Polled from the demand Sequence or the Multichannel phases for an osc will effect the current grain. It doesnt matter on which Channel that might be.
I havent created a new release yet (would like to improve some other aspects first), but i have added UnitRand and UnitWalk for phasor driven random values between 0 and 1. These could for example be used for multichannel amplitude flux in the context of granulation by using the multichannel phase from VoiceAllocator.
I would still like to investigate other interpolation methods, currently we are using cosine interpolation (which is really cheap and doesnt overshoot but could be smoother) and i have deleted the lerp between stepped and interpolated signals. I think it would make more sense to make a binary switch or just leave the interpolation on default, dont know. For the UnitWalk we could also add other noise distributions, currently we are using rgen.fsum3rand() for quasi gaussian noise. I have looked at the gendyn implementation, but maybe thats overkill and im not sure about some of the implementations there. The cauchy distribution seems to have some weird param limiting. I guess you can also create a 2nd order random walk by cascading the UnitWalk, have read about that in some paper about stochastic synthesis. As you can see in the examples below seeding is also working great ![]()
(
{
var phase, random;
RandSeed.kr(\seedtrg.kr(1), \seed.kr(1000));
phase = Phasor.ar(DC.ar(0), 1000 * SampleDur.ir);
random = UnitRand.ar(phase);
[phase, random];
}.plot(0.041);
)
(
{
var phase, walk;
RandSeed.kr(\seedtrg.kr(1), \seed.kr(1000));
phase = Phasor.ar(DC.ar(0), 1000 * SampleDur.ir);
walk = UnitWalk.ar(phase, \step.kr(0.2));
[phase, walk];
}.plot(0.041);
)
I have created a new release Release GrainUtils v1.1.0 · dietcv/GrainUtils · GitHub
The main new features together with some code refactoring are:
-added SC conventions for real time safety and the canonical trigger definition (replaced std::vector with std:array and added isTrigger)
-added UnitRand and UnitWalk
-added DualOscOS and SingleOscOS (this is just my personal implementation, you can just use OscOS from Oversampling Oscillators)
I will delete the DualOscOS repository, so from now on the phasor driven oscs can be found here and i will probably add more in the future.


