#include "GrainCloud.h" GrainCloud::GrainCloud() {} void GrainCloud::prepare (double sampleRate) { currentSampleRate = sampleRate; reset(); } void GrainCloud::reset() { for (auto& g : grains) g.active = false; samplesUntilNextGrain = 0; } void GrainCloud::spawnGrain (const juce::AudioBuffer& sourceBuffer) { const int numSourceSamples = sourceBuffer.getNumSamples(); if (numSourceSamples == 0) return; // Find free slot Grain* slot = nullptr; for (auto& g : grains) { if (! g.active) { slot = &g; break; } } if (slot == nullptr) return; // all slots busy // Position with scatter float pos = position.load(); float posS = posScatter.load(); float scatteredPos = pos + (rng.nextFloat() * 2.0f - 1.0f) * posS; scatteredPos = juce::jlimit (0.0f, 1.0f, scatteredPos); // Size with scatter float sizeMs = grainSizeMs.load(); float sizeS = sizeScatter.load(); float scatteredSize = sizeMs * (1.0f + (rng.nextFloat() * 2.0f - 1.0f) * sizeS); scatteredSize = juce::jlimit (10.0f, 500.0f, scatteredSize); int lengthSamp = (int) (scatteredSize * 0.001f * currentSampleRate); lengthSamp = std::max (1, lengthSamp); // Pitch with scatter + MIDI offset float pitchST = pitchSemitones.load() + midiPitchOffset; float pitchS = pitchScatter.load(); float scatteredPitch = pitchST + (rng.nextFloat() * 2.0f - 1.0f) * pitchS * 12.0f; float pitchRatio = std::pow (2.0f, scatteredPitch / 12.0f) * sampleRateRatio; // Pan with scatter float p = pan.load(); float panS = panScatter.load(); float scatteredPan = p + (rng.nextFloat() * 2.0f - 1.0f) * panS; scatteredPan = juce::jlimit (-1.0f, 1.0f, scatteredPan); // Direction int dir = direction.load(); bool rev = false; if (dir == 1) rev = true; else if (dir == 2) rev = (rng.nextFloat() > 0.5f); // Setup grain slot->startSample = (int) (scatteredPos * (float) (numSourceSamples - 1)); slot->lengthSamples = lengthSamp; slot->pitchRatio = pitchRatio; slot->readPosition = rev ? (double) (lengthSamp - 1) : 0.0; slot->samplesElapsed = 0; slot->panPosition = scatteredPan; slot->volume = 1.0f; slot->reverse = rev; slot->active = true; } float GrainCloud::readSampleInterpolated (const juce::AudioBuffer& buffer, double pos) const { const int numSamples = buffer.getNumSamples(); if (numSamples == 0) return 0.0f; int i0 = (int) pos; int i1 = i0 + 1; float frac = (float) (pos - (double) i0); // Wrap to valid range i0 = juce::jlimit (0, numSamples - 1, i0); i1 = juce::jlimit (0, numSamples - 1, i1); const float* data = buffer.getReadPointer (0); return data[i0] + frac * (data[i1] - data[i0]); } void GrainCloud::processBlock (juce::AudioBuffer& output, int numSamples, const juce::AudioBuffer& sourceBuffer) { if (sourceBuffer.getNumSamples() == 0) return; float* outL = output.getWritePointer (0); float* outR = output.getNumChannels() > 1 ? output.getWritePointer (1) : outL; for (int samp = 0; samp < numSamples; ++samp) { // Spawn new grains --samplesUntilNextGrain; if (samplesUntilNextGrain <= 0) { spawnGrain (sourceBuffer); float d = density.load(); float interval = (float) currentSampleRate / std::max (1.0f, d); samplesUntilNextGrain = std::max (1, (int) interval); } // Render active grains float mixL = 0.0f, mixR = 0.0f; for (auto& grain : grains) { if (! grain.active) continue; // Window amplitude float phase = (float) grain.samplesElapsed / (float) grain.lengthSamples; float amp = window.getValue (phase) * grain.volume; // Read from source double srcPos = (double) grain.startSample + grain.readPosition; float sample = readSampleInterpolated (sourceBuffer, srcPos) * amp; // Pan float leftGain = std::cos ((grain.panPosition + 1.0f) * 0.25f * juce::MathConstants::pi); float rightGain = std::sin ((grain.panPosition + 1.0f) * 0.25f * juce::MathConstants::pi); mixL += sample * leftGain; mixR += sample * rightGain; // Advance read position if (grain.reverse) grain.readPosition -= (double) grain.pitchRatio; else grain.readPosition += (double) grain.pitchRatio; grain.samplesElapsed++; // Deactivate if done if (grain.samplesElapsed >= grain.lengthSamples) grain.active = false; } outL[samp] += mixL; outR[samp] += mixR; } } std::array GrainCloud::getActiveGrainInfo() const { std::array info; for (int i = 0; i < maxGrains; ++i) { if (grains[i].active) { info[i].startSample = grains[i].startSample; info[i].lengthSamples = grains[i].lengthSamples; info[i].progress = (float) grains[i].samplesElapsed / (float) std::max (1, grains[i].lengthSamples); } else { info[i].startSample = -1; info[i].lengthSamples = 0; info[i].progress = 0.0f; } } return info; } int GrainCloud::getActiveGrainCount() const { int count = 0; for (auto& g : grains) if (g.active) ++count; return count; }