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>
This commit is contained in:
182
Source/GrainCloud.cpp
Normal file
182
Source/GrainCloud.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user