Files
InstaGrain/Source/GrainCloud.cpp
hariel1985 55b5f89ac5 Initial release — InstaGrain granular synthesizer v1.0
8-voice polyphonic granular synth (VST3/AU/LV2) with:
- 128 grain pool per voice, Hann windowing, linear interpolation
- Root note selector, sample rate correction, sustain pedal (CC64)
- Scatter controls, direction modes (Fwd/Rev/PingPong), freeze
- ADSR envelope, global filter (LP/HP/BP), reverb
- Waveform display with grain visualization
- Drag & drop sample loading, full state save/restore
- CI/CD for Windows/macOS/Linux

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:26:06 +01:00

183 sor
5.6 KiB
C++

#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<float>& 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<float>& 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<float>& output, int numSamples,
const juce::AudioBuffer<float>& 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<float>::pi);
float rightGain = std::sin ((grain.panPosition + 1.0f) * 0.25f * juce::MathConstants<float>::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::GrainInfo, GrainCloud::maxGrains> GrainCloud::getActiveGrainInfo() const
{
std::array<GrainInfo, maxGrains> 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;
}