Clouds: Understanding the source code

I’ve been looking at and trying to understand how the GranularProcessor works.

I have a question regarding the Prepare() function. I see that it’s being called at every iteration of the while loop. Is that necessary if I know that the quality and playback settings are not going to change…Can just call it once before the start.

The role of this function is to perform operations at a lower priority than the main audio rendering interrupt - because these operations take much longer than the 1ms of CPU allocated to the rendering of a block of 32 samples and/or because these operations require a longer block of samples to be buffered. This includes:

  1. Initializing and zeroing buffers when changing modes.
  2. Finding the periodicity of the content of the buffer to perform time-stretching (in time-stretching mode).
  3. Doing the FFT, spectral processing, inverse FFT, and overlap-add (in spectral madness mode).

It is wrong to assume that these are only initialization tasks and that Prepare() should only be called when the settings are changed.

Thank you very much pichenettes for the detailed answer.
Hmm, so It has to be done before every block. I guess my ears fooled me when I tried without it and seamed its working!!. I should have tried the other modes you mentioned.

If I may bother you with another question. I’m trying to introduce a 5th quality mode where the sample rate can be selectively chosen other than 32Khz and 16Khz. I failed with this big time. I changed the constant 32000 to a variable and tried increasing what seamed to be buffers in the code but to no avail. I guess there is much more to it?

Edit: just to clarify this is run on software/PC. So the host can indeed run at much higher rates.

It is needed once only if you are using the granular and looping delay modes.

Yes, that’s a reasonable rate. Note that sometimes it’ll return immediately (when there is nothing to do, just buffering more samples), and that sometimes it’ll take a lot of time (especially when it’s doing FFTs). You might consider running it in a separate thread with lower priority (which is exactly what putting it in main() achieves in the original code).

What didn’t work exactly? Did it crash? Did it sound bad?

It’s hard to tell without seeing your code, but I’m not available for a software development consulting job!

44.1Khz seams to work fine. But as soon as you go higher, say 88.2Khz or 192Khz. The sound becomes more and more, stuttery, grainy and noisy.

I’m sorry. I should have said that this is going to be open sourced. Please ignore my questions if you don’t feel like it.

It’s basically a port from VCVRack’s version of clouds. Here is the relevant processing part.

// Get input
if (!inputBuffer.full()) 
	inputFrame.samples[0] = ppinl->in * kingain->value;
	inputFrame.samples[1] = ppinr->num_cables ? ppinr->in * kingain->value : inputFrame.samples[0];

// Trigger
if (pptrig->in >= 1.0) triggered = true;

// Render frames
if (outputBuffer.empty()) 
{	clouds::ShortFrame input[32] = {};
	dsp::Frame<2> inputFrames[32];
	if (quality!=4)
	{	// Convert input buffer
		int inLen = inputBuffer.size();
		int outLen = 32;
		inputSrc.process(inputBuffer.startData(), &inLen, inputFrames, &outLen);

		// We might not fill all of the input buffer if there is a deficiency, but this cannot be avoided due to imprecisions between the input and output SRC.
		for (int i = 0; i < outLen; i++) {
			input[i].l = CLIP(inputFrames[i].samples[0] * 32767.0f, -32768.0f, 32767.0f);
			input[i].r = CLIP(inputFrames[i].samples[1] * 32767.0f, -32768.0f, 32767.0f);
	{	// No sample rate convertion
		int blen = mmin(32,inputBuffer.size());
		for (int i = 0; i < blen; i++)
			input[i].l = CLIP(inputBuffer.startData()[i].samples[0] * 32767.0f, -32768.0f, 32767.0f);
			input[i].r = CLIP(inputBuffer.startData()[i].samples[1] * 32767.0f, -32768.0f, 32767.0f);


	clouds::Parameters* p = processor->mutable_parameters();
	p->trigger = triggered;
	p->gate = triggered;
	p->freeze = freeze || (ppfreeze->in >= 1.0);
	p->position = CLIP(kposition->value + ppposition->in, 0.0f, 1.0f);
	p->size = CLIP(ksize->value + ppsize->in, 0.0f, 1.0f);
	p->pitch = CLIP((SCALE(kpitch->value,-2.f,2.f) + pppitch->in*MIDI_TUNE_FACTOR) * 12.0f, -48.0f, 48.0f);
	p->density = CLIP(kdensity->value + ppdensity->in, 0.0f, 1.0f);
	p->texture = CLIP(ktexture->value + pptexture->in, 0.0f, 1.0f);
	p->dry_wet = kblend->value;
	p->stereo_spread = kspread->value;
	p->feedback = kfeedback->value;

	p->reverb = kreverb->value;
	float blend = ppblend->in;

	switch (blendMode) 
	case 0:
		p->dry_wet += blend;
		p->dry_wet = CLIP(p->dry_wet, 0.0f, 1.0f);
	case 1:
		p->stereo_spread += blend;
		p->stereo_spread = CLIP(p->stereo_spread, 0.0f, 1.0f);
	case 2:
		p->feedback += blend;
		p->feedback = CLIP(p->feedback, 0.0f, 1.0f);
	case 3:
		p->reverb += blend;
		p->reverb = CLIP(p->reverb, 0.0f, 1.0f);

	clouds::ShortFrame output[32];
	processor->Process(input, output, 32);

	if (quality != 4)
	// Convert output buffer
	{	dsp::Frame<2> outputFrames[32];
		for (int i = 0; i < 32; i++) {
			outputFrames[i].samples[0] = output[i].l / 32768.0;
			outputFrames[i].samples[1] = output[i].r / 32768.0;

		int inLen = 32;
		int outLen = outputBuffer.capacity();
		outputSrc.process(outputFrames, &inLen, outputBuffer.endData(), &outLen);
	{	// No sample rate convertion
		for (int i = 0; i < 32; i++) 
		{	outputBuffer.endData()[i].samples[0] = output[i].l / 32768.0;
			outputBuffer.endData()[i].samples[1] = output[i].r / 32768.0;
	triggered = false;

// Set output
outputFrame = outputBuffer.shift();
ppoutl->out = outputFrame.samples[0];
ppoutr->out = outputFrame.samples[1];

This is just a guess, but:

int blen = mmin(32,inputBuffer.size());

So effectively, if inputBuffer.size() is smaller than 32, you send an incompletely filled buffer to Clouds.

Yeah but this happens only at the first sample when the inputbuffer hasn’t been filled. The rest of those initial samples are zeros (AFAIK from VCV code). The outputbuffer will always have 32 samples. This will give time for inputbuffer to fill during the next 31 samples.

then inputbuffer will be filled at witch point the outputbuffer should have emptied, then inputBuffer.size() should always be 32.

Then I don’t know :slight_smile:

I would typically approach a problem like this one by sending a sine wave to the “module” with dry/wet balance set to fully dry, just to see what kind of missing, or duplicated samples there are.

Thanks allot :smile:. by the way, when I send a sine while on fully dry it passed through perfectly. The artifacts happens on wet.

Other things I can think of:

Thanks again :+1:. I’ll play with those and see what I can do.