GrainBuf, TGrains timing accuracy and sampling rate

I’m trying to playback the signal from the buffer using TGrains or GrainBuf. Or in other words make a granulator behave like a simple sampler. And in this context I see the behaviour that’s I can’t quite explain. Maybe somebody can help me to get what is the key factor here.

Here is the code. It plays back a sample using a BufRd and GrainBuf (or TGrains) and subtracts one from another:

b = Buffer.readChannel(s, "/path/to/soundfile_with_sampling_rate_that_match_scsynth_SR", channels:[0]); 
b.play;
b.sampleRate;


Ndef(\granular_reconstruct).clear;
Ndef(\granular_reconstruct).ar(2);
Ndef(\granular_reconstruct).set(\bufnum, b.bufnum);

(
Ndef(\granular_reconstruct, { |bufnum| 
        var overlap=2, tFreq=20;
	var phasor, bufrd, gran, env; 
	var bufFrames = BufFrames.ir(bufnum);
	var t = Impulse.ar(tFreq);

	phasor = Phasor.ar(
		rate: BufRateScale.kr(bufnum), 
		start: 0.0, 
		end: bufFrames, 
	);

	bufrd = BufRd.ar(
		numChannels: 1, 
		bufnum: bufnum, 
		phase: phasor, 
		interpolation: 1
	);

	// bufrd = DelayN.ar(bufrd, delaytime: overlap / tFreq * MouseX.kr(0.499,0.51).poll);
	// // on UGen(MouseX): 0.500292 gives good null test
	// gran = TGrains.ar(
	// 	numChannels: 1, 
	// 	trigger: t, 
	// 	bufnum: bufnum, 
	// 	rate: 1, 
	// 	centerPos: phasor / SampleRate.ir, 
	// 	dur: overlap / tFreq, 
	// 	pan: 0, 
	// 	amp: 1, 
	// 	interp: 1
	// );


	gran = GrainBuf.ar(
		numChannels: 1, 
		trigger: t, 
		dur: overlap / tFreq, 
		sndbuf: bufnum, 
		rate: 1, 
		pos: phasor / bufFrames, 
		interp: 1, 
		envbufnum: -1, 
	);

	bufrd
	- 
	gran
	!2
}).play;
)

And here starts the interesting part that I don’t quite get.
With the sampling rate 44100 GrainBuf gives me perfect null test with BufRd. TGrains gives non-regular amplitude mudulation, or in other words some grains match the content of BufRd ( and the output is 0) but some grains don’t. If I turn TGrains interpoaltion to 2 or 4 things become a bit better (the output becomes a bit quiter) but still no perfect null test.

Now if I start SuperCollider with 48000 Hz Sampling Rate both GrainBuf and TGrains can’t give succesfull null test and result in non-periodic amplitude modulation.

Now on another machine thigs are a bit different. With 48000 Hz Sampling Rate TGrains is able to produce succesfull null test and GrainBuf results in amplitude modulation. And with 44100 Hz both TGrains and GrainBuf fail to pass null test.

On these 2 machines I tested things with same and different SC versions, same and different audio cards (built-in ones and USB Focusrite Solo). OS and kernels are different (different versions of Debian) but jackd versions are the same and clocksource/timer related configurations are the same. When changing sampling rate I also change the soundfile to match scsynth’s (or jackd’s) sampling rate. In all cases of amplitude modulation output turning on interpolation in granulator make things better but still modulation is there.

Does anybody have a clue what might be the key factor there? Why GrainBuf fails where TGrains doesn’t and vice versa. And how server sampling rate is connected to this behaviour. And may be something else…

For the rate in GrainBuf, try using BufRateScale to ensure the sample rates match as needed. I think there is also an interp argument that should be set to 4 for the best interpolation - but it still may not be perfect.
I’m away from my computer, but I’ll check more later to see if this is what is going on.

/*
Josh Parmenter
www.realizedsound.net/josh
*/

Josh, you’re the one who’s answer I was hoping for )) !

BufRateScale does not have any effect in my case because sample rates of the sound files and of scsynth are the same. Where I don’t get succesful null test adding interpolation makes things better but not perfect.
But in cases where null test is perfect no interpolations is needed at all.

I probably should upload audio examples of what I get.

Here are couple of examples from a machine where I can only get good result with TGrains and sampling rate of 48000 Hz.
example wiith SC on 41000 Hz and a soundfile sampling rate is 44100 Hz:

example with SC running on 48000 Hz and a soundfile sampling rate is 48000 Hz:

FWIW, when switching between TGrains and GrainBuf as in this snippet, I hear no strongly evident clicks when MouseX crosses the halfway boundary. So the two are producing basically equivalent audio (on my machine, at 44.1 kHz, with a 44.1 kHz file).

One thing is that you should add half the grain duration to the centerPos in TGrains – this is better than trying to calculate a delay time.

I can’t get complete cancellation in either case, but I wonder why it’s critical? I’d absolutely expect floating point funkiness from any granulator and I’d be incredibly surprised to get a perfect null test in any such case. (That is, the cases where you’re getting a “perfect” result may just be dumb luck.)

(
a = { |bufnum|
	var phase = Phasor.ar(0, BufRateScale.kr(bufnum), 0, BufFrames.kr(bufnum), 0);
	var bufrd = BufRd.ar(1, bufnum, phase, interpolation: 1);
	var gFreq = 20;
	var overlap = 4;
	var gDur = overlap / gFreq;
	var trig = Impulse.ar(gFreq);
	var tgrains = TGrains.ar(2, trig, bufnum, 1,
		// offset phase to the grain's center time -- this is not optional!
		phase / BufSampleRate.kr(bufnum) + (gDur * 0.5),
		gDur, amp: 1, interp: 1);
	var grainbuf = GrainBuf.ar(2, trig, gDur, bufnum, BufRateScale.kr(bufnum),
		phase / BufFrames.kr(bufnum), 1);
	
	var switch = MouseX.kr(0, 1, 0) > 0.5;
	var usedGrains = Select.ar(Lag.kr(switch, 0.1), [tgrains, grainbuf]);
	
	// difference is nonzero, but (on my machine)
	// the two granulators are indistinguishable
	// (usedGrains - (bufrd * 1.40104)) * 0.5
	
	// or listen to the granulators directly
	usedGrains * 0.5
}.play(args: [bufnum: b]);
)

hjh

1 Like

Hi James, agree 100% on grain offset for TGrains, but why overlap of 4? Only 2 works here.

But again on the machines here TGrains and GrainBuf give different results be it your or my example.
Now on one of the machines I have Windows OS so installed SC there and … it has the same behavior.
So I guess the problem is dependent on the hardware and it is something that GrainBuf and TGrains treat differently.

Regarding whether it’s critical. Well there are some effects that can be done this way and having non-regular modulation will have corresponding stamp on aesthetics.
I showed some of them here

What I’m driving at is the phenomenon of the perfect being the enemy of the good.

It’s indeed an interesting question, why the behavior is different in different machines. I’d guess floating point inaccuracies are being resolved differently…? (If that is the case though, then it may not be within SC’s control.) Or maybe the builds are using different processor flags?

There’s a point at which it may be more productive to focus effort on reducing the artefacts to the point where the effect is sonically usable, rather than discarding the effect because it can’t be guaranteed to be perfect.

I used 4 because it has sounded smoother to me in the past. It shouldn’t make a difference for the Hann window vis-à-vis amplitude modulation: two windows will sum to 1, and the other pair will also sum to 1. But you could also call it just a lapse in judgment. You’re more than welcome to change it, if you try that snippet.

So the amplitude modulation is coming from somewhere else, I think not the envelope.

hjh

I’d agree on perfect being enemy of good, but if the whole composition is using say GrainBufs for effect processing in this manner … I’m unsure. Timing accuracy on 20 grains per second, why not at least?

Which processor flags might be of interest in this context?

I have no idea. I’m just speculating that, since the difference between the environments is principally hardware, the difference in the calculations is likely to be either different behavior in the chip, or different configuration in the way the software is using the chip.

I know really nothing about CPUs at that level.

I am skeptical of the reasoning here. It seems to be along the lines of, “Because it is perfect in some cases, and because these are deterministic UGens, it should be perfect in all 1.0x playback cases.” Granulation is complicated; there are many many places for rounding error to crop up and accumulate. If there is error in some scenarios, to me, this is just what happens when you’re scaling and unscaling time values in various units, and running multiple irrational-number envelopes and summing audio across them. If you have infinite precision, perfection is guaranteed. But you don’t have infinite precision. (I’m willing to bet that your perfect null test does have some residual, just too small to be evident in the scope or level meter.)

With that said, it’s an interesting question why it’s different in different hardware, and I’m quite curious what the answer might be. But, that question may ultimately be of limited relevance to the task of getting an effect working. (Or – it’s useful to push tests up to the limit of what’s possible – but the result may simply be to discover that the limit isn’t where you expected.)

In my machine, I was unable to reproduce a significant difference in output between TGrains and GrainBuf. If there were, I’d have heard clicks when Select was switching between them, but I didn’t hear these. There may have been some discrepancy but it would have to be extremely small to be undetectable to the ear. That’s very different from your result (basically – I can’t reproduce the issue).

hjh

I can confirm numerical inaccuracy.

In TGrains – I added a C printf() statement at the moment of creating a new grain, to see the grain’s starting phase and the given center phase. Then I ran my test case again:

-> Synth('temp__1' : 1001)
phase = 0.000000, centerphase = 4410.000000
phase = 2205.000488, centerphase = 6615.000488
phase = 4410.000000, centerphase = 8820.000000
phase = 6615.000000, centerphase = 11025.000000
phase = 8820.000977, centerphase = 13230.000977
phase = 11025.000000, centerphase = 15435.000000
phase = 13229.998047, centerphase = 17639.998047
phase = 15435.000000, centerphase = 19845.000000

Of particular interest is phase = 13229.998047. In theory it should be 11025 + 2205 = 13230, but it’s slightly low.

When using interpolation mode 1 (no interpolation), TGrains_next() uses GRAIN_LOOP_BODY_1. This macro simply truncates the floating-point phase. It does not round. int32 iphase = (int32)phase; Therefore any phase values that are slightly below expected will truncate down to the next whole sample – thus the grain will be out of phase with the original audio coming from BufRd, thus subtraction will not cancel perfectly.

centerphase is also slightly low.

center input = (phasor / bufSampleRate) + (0.5 * graindur)
centerphase = center * bufSampleRate = phasor + (0.5 * graindur * bufSampleRate) = phasor + (half number of grain samples)

phase = centerphase - (0.5 * number of grain samples * playRate)

In theory, phase should then equal the phasor input: center phase adds half the number of samples, and TGrains calculates phase by subtracting half the number of samples. In practice, floating-point precision munges the calculation.

OK, let’s try the same for GrainBuf.

-> Synth('temp__3' : 1000)
phase = 0.000000
phase = 2205.000000
phase = 4410.000000
phase = 6614.999512
phase = 8820.000000
phase = 11025.000000
phase = 13229.999023
phase = 15435.000000

Similar precision problem. And, the source code for GrainBuf shows the same truncation of phase for non-interpolation.

I can also confirm in the SynthDef, with phase.poll(trig);, that the Phasor is incrementing by integers, as expected.

So the conclusion I have to reach is that scaling the phase from samples down to a smaller range and then scaling it back up is inherently inexact. The precise nature of the inexactness should depend on the scaling factor (i.e. buffer size).

hjh

Thank your for the test James. If I got it right this might explain that TGrains’ and GrainBuf’s results might differ because of different ways of calculating grain position value.

The thing that is still under the question is what affects the behavior between different machines. Or from another angle is there a way to make granulator ugens behave the same way on different machines.

Based on above discussion I made couple more tests and it looks like finally could come up with accurate results.

So the conclusion so far is that the key factor is how the division operation is used to calculate position of the grain. And that’s why results are different for different sampling rates and for different machines.

Now I got very close results (maybe identical, didn’t test) between two machines on both 44100 and 48000 Hz rates with following piece of code (which doesn’t use different division for GrainBuf and TGrains):



Ndef(\granular_reconstruct).clear;
Ndef(\granular_reconstruct).ar(2);
Ndef(\granular_reconstruct).set(\bufnum, b.bufnum);


(
Ndef(\granular_reconstruct, { |bufnum, overlap=2, tFreq=20|
	var bufrdPhasor, granPhasor;
	var bufrd, tgrains, grainbuf;
	var env; 
	var bufFrames = BufFrames.ir(bufnum);
	var t = Impulse.ar(tFreq);
	var gDur = overlap / tFreq;

	bufrdPhasor = Phasor.ar(
		rate: BufRateScale.kr(bufnum), 
		start: 0.0, 
		end: bufFrames, 
	);


	granPhasor = Phasor.ar(
		rate: 1 / bufFrames * BufRateScale.kr(bufnum), 
		start: 0.0, 
		end: 1, 
	);



	bufrd = BufRd.ar(
		numChannels: 1, 
		bufnum: bufnum, 
		phase: bufrdPhasor, // more accurate for BufRd
		// phase: granPhasor * bufFrames, 
		interpolation: 1
	);


	tgrains = TGrains.ar(
		numChannels: 1, 
		trigger: t, 
		bufnum: bufnum, 
		rate: 1, 
		centerPos: granPhasor * BufDur.kr(bufnum) + (gDur * 0.50001), 
		// centerPos: granPhasor * BufDur.kr(bufnum) + ( gDur * MouseY.kr(0.49,0.51).poll ), 
		dur: gDur, 
		pan: 0, 
		amp: 1, 
		interp: 1
	);


	

	grainbuf = GrainBuf.ar(
		numChannels: 1, 
		trigger: t, 
		dur: gDur, 
		sndbuf: bufnum, 
		rate: 1, 
		pos: granPhasor, 
		interp: 1, 
		envbufnum: -1, 
	);


	bufrd 
	- 
	Select.ar(MouseX.kr(0,1).round, [ grainbuf, tgrains ])
	!2
	

}).play;
) 

In this code I use a phasor that goes from 0 to 1 with step equal to 1 / bufFrames and multiply it when needed by BufDur.kr(bufnum) for TGrains. This gives me identical behavior between GrainBuf and TGrains on 44100 Hz SR and almost identical on 48000 (they become identical on the second cycle of the musical loop).