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:
hariel1985
2026-03-26 17:26:06 +01:00
commit 55b5f89ac5
34 fájl változott, egészen pontosan 2728 új sor hozzáadva és 0 régi sor törölve

93
Source/EffectsPanel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,93 @@
#include "EffectsPanel.h"
EffectsPanel::EffectsPanel()
{
titleLabel.setText ("FILTER / REVERB", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
addAndMakeVisible (titleLabel);
filterTypeBox.addItem ("LP", 1);
filterTypeBox.addItem ("HP", 2);
filterTypeBox.addItem ("BP", 3);
filterTypeBox.setSelectedId (1);
addAndMakeVisible (filterTypeBox);
filterLabel.setText ("Type", juce::dontSendNotification);
filterLabel.setJustificationType (juce::Justification::centred);
filterLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (filterLabel);
auto setupKnob = [this] (juce::Slider& knob, juce::Label& label, const juce::String& name,
double min, double max, double def, const juce::String& type,
const juce::String& suffix = "")
{
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 55, 14);
knob.setRange (min, max);
knob.setValue (def);
knob.setTextValueSuffix (suffix);
knob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, type);
addAndMakeVisible (knob);
label.setText (name, juce::dontSendNotification);
label.setJustificationType (juce::Justification::centred);
label.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (label);
};
setupKnob (cutoffKnob, cutoffLabel, "Cutoff", 20.0, 20000.0, 20000.0, "orange", " Hz");
cutoffKnob.setSkewFactorFromMidPoint (1000.0);
setupKnob (resoKnob, resoLabel, "Reso", 0.1, 10.0, 0.707, "dark");
resoKnob.setSkewFactorFromMidPoint (1.5);
setupKnob (reverbSizeKnob, revSizeLabel, "Rev Size", 0.0, 1.0, 0.0, "dark");
setupKnob (reverbDecayKnob, revDecayLabel, "Rev Decay", 0.0, 1.0, 0.0, "dark");
}
void EffectsPanel::resized()
{
auto bounds = getLocalBounds().reduced (4);
titleLabel.setBounds (bounds.removeFromTop (20));
int colW = bounds.getWidth() / 5;
// Filter type combo
auto typeCol = bounds.removeFromLeft (colW);
filterLabel.setBounds (typeCol.removeFromTop (14));
filterTypeBox.setBounds (typeCol.reduced (4, 2).removeFromTop (24));
// Cutoff
auto cutCol = bounds.removeFromLeft (colW);
auto cutKnobArea = cutCol.withTrimmedBottom (16);
cutoffKnob.setBounds (cutKnobArea.reduced (2));
cutoffLabel.setBounds (cutCol.getX(), cutKnobArea.getBottom() - 2, cutCol.getWidth(), 16);
// Reso
auto resCol = bounds.removeFromLeft (colW);
auto resKnobArea = resCol.withTrimmedBottom (16);
resoKnob.setBounds (resKnobArea.reduced (2));
resoLabel.setBounds (resCol.getX(), resKnobArea.getBottom() - 2, resCol.getWidth(), 16);
// Reverb Size
auto rsCol = bounds.removeFromLeft (colW);
auto rsKnobArea = rsCol.withTrimmedBottom (16);
reverbSizeKnob.setBounds (rsKnobArea.reduced (2));
revSizeLabel.setBounds (rsCol.getX(), rsKnobArea.getBottom() - 2, rsCol.getWidth(), 16);
// Reverb Decay
auto rdCol = bounds;
auto rdKnobArea = rdCol.withTrimmedBottom (16);
reverbDecayKnob.setBounds (rdKnobArea.reduced (2));
revDecayLabel.setBounds (rdCol.getX(), rdKnobArea.getBottom() - 2, rdCol.getWidth(), 16);
}
void EffectsPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}

20
Source/EffectsPanel.h Normal file
Fájl megtekintése

@@ -0,0 +1,20 @@
#pragma once
#include <JuceHeader.h>
#include "LookAndFeel.h"
class EffectsPanel : public juce::Component
{
public:
EffectsPanel();
void resized() override;
void paint (juce::Graphics& g) override;
juce::ComboBox filterTypeBox;
juce::Slider cutoffKnob, resoKnob, reverbSizeKnob, reverbDecayKnob;
private:
juce::Label filterLabel, cutoffLabel, resoLabel, revSizeLabel, revDecayLabel;
juce::Label titleLabel;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EffectsPanel)
};

63
Source/EnvelopePanel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,63 @@
#include "EnvelopePanel.h"
EnvelopePanel::EnvelopePanel()
{
titleLabel.setText ("ENVELOPE", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
addAndMakeVisible (titleLabel);
setupKnob (attackKnob, aLabel, "A", 0.001, 2.0, 0.01, " s");
setupKnob (decayKnob, dLabel, "D", 0.001, 2.0, 0.1, " s");
setupKnob (sustainKnob, sLabel, "S", 0.0, 1.0, 1.0);
setupKnob (releaseKnob, rLabel, "R", 0.01, 5.0, 0.3, " s");
attackKnob.setSkewFactorFromMidPoint (0.2);
decayKnob.setSkewFactorFromMidPoint (0.3);
releaseKnob.setSkewFactorFromMidPoint (0.5);
}
void EnvelopePanel::setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name,
double min, double max, double def, const juce::String& suffix)
{
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 50, 14);
knob.setRange (min, max);
knob.setValue (def);
knob.setTextValueSuffix (suffix);
knob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, "orange");
addAndMakeVisible (knob);
label.setText (name, juce::dontSendNotification);
label.setJustificationType (juce::Justification::centred);
label.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (label);
}
void EnvelopePanel::resized()
{
auto bounds = getLocalBounds().reduced (4);
titleLabel.setBounds (bounds.removeFromTop (20));
int knobW = bounds.getWidth() / 4;
juce::Slider* knobs[] = { &attackKnob, &decayKnob, &sustainKnob, &releaseKnob };
juce::Label* labels[] = { &aLabel, &dLabel, &sLabel, &rLabel };
for (int i = 0; i < 4; ++i)
{
auto col = bounds.removeFromLeft (knobW);
auto knobArea = col.withTrimmedBottom (16);
knobs[i]->setBounds (knobArea.reduced (2));
labels[i]->setBounds (col.getX(), knobArea.getBottom() - 2, col.getWidth(), 16);
}
}
void EnvelopePanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}

22
Source/EnvelopePanel.h Normal file
Fájl megtekintése

@@ -0,0 +1,22 @@
#pragma once
#include <JuceHeader.h>
#include "LookAndFeel.h"
class EnvelopePanel : public juce::Component
{
public:
EnvelopePanel();
void resized() override;
void paint (juce::Graphics& g) override;
juce::Slider attackKnob, decayKnob, sustainKnob, releaseKnob;
private:
juce::Label aLabel, dLabel, sLabel, rLabel;
juce::Label titleLabel;
void setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name,
double min, double max, double def, const juce::String& suffix = "");
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EnvelopePanel)
};

45
Source/Grain.h Normal file
Fájl megtekintése

@@ -0,0 +1,45 @@
#pragma once
#include <JuceHeader.h>
// ============================================================
// Hann window lookup table (1024 points, computed once)
// ============================================================
struct GrainWindow
{
static constexpr int tableSize = 1024;
float table[tableSize];
GrainWindow()
{
for (int i = 0; i < tableSize; ++i)
{
float phase = (float) i / (float) (tableSize - 1);
table[i] = 0.5f * (1.0f - std::cos (juce::MathConstants<float>::twoPi * phase));
}
}
float getValue (float phase) const
{
float index = juce::jlimit (0.0f, 1.0f, phase) * (float) (tableSize - 1);
int i0 = (int) index;
int i1 = std::min (i0 + 1, tableSize - 1);
float frac = index - (float) i0;
return table[i0] + frac * (table[i1] - table[i0]);
}
};
// ============================================================
// Single grain
// ============================================================
struct Grain
{
int startSample = 0; // where in source buffer
int lengthSamples = 0; // grain duration in samples
float pitchRatio = 1.0f; // playback speed
double readPosition = 0.0; // current fractional read position
int samplesElapsed = 0; // output samples generated
float panPosition = 0.0f; // -1 left, +1 right
float volume = 1.0f;
bool reverse = false;
bool active = false;
};

182
Source/GrainCloud.cpp Normal file
Fájl megtekintése

@@ -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;
}

52
Source/GrainCloud.h Normal file
Fájl megtekintése

@@ -0,0 +1,52 @@
#pragma once
#include <JuceHeader.h>
#include "Grain.h"
class GrainCloud
{
public:
static constexpr int maxGrains = 128;
enum class Direction { Forward, Reverse, PingPong };
GrainCloud();
void prepare (double sampleRate);
void processBlock (juce::AudioBuffer<float>& output, int numSamples,
const juce::AudioBuffer<float>& sourceBuffer);
void reset();
// Parameters (atomic — GUI writes, audio reads)
std::atomic<float> position { 0.5f }; // 0-1
std::atomic<float> grainSizeMs { 100.0f }; // 10-500
std::atomic<float> density { 10.0f }; // 1-100 grains/sec
std::atomic<float> pitchSemitones { 0.0f }; // -24..+24
std::atomic<float> pan { 0.0f }; // -1..+1
std::atomic<float> posScatter { 0.0f }; // 0-1
std::atomic<float> sizeScatter { 0.0f }; // 0-1
std::atomic<float> pitchScatter { 0.0f }; // 0-1
std::atomic<float> panScatter { 0.0f }; // 0-1
std::atomic<int> direction { 0 }; // 0=Fwd, 1=Rev, 2=PingPong
std::atomic<bool> freeze { false };
// Extra pitch offset from MIDI note
float midiPitchOffset = 0.0f;
// Sample rate correction: sourceSampleRate / dawSampleRate
float sampleRateRatio = 1.0f;
// For visualization — snapshot of active grains
struct GrainInfo { int startSample; int lengthSamples; float progress; };
std::array<GrainInfo, maxGrains> getActiveGrainInfo() const;
int getActiveGrainCount() const;
private:
double currentSampleRate = 44100.0;
std::array<Grain, maxGrains> grains;
GrainWindow window;
int samplesUntilNextGrain = 0;
juce::Random rng;
void spawnGrain (const juce::AudioBuffer<float>& sourceBuffer);
float readSampleInterpolated (const juce::AudioBuffer<float>& buffer, double position) const;
};

Fájl megtekintése

@@ -0,0 +1,83 @@
#include "GrainControlPanel.h"
GrainControlPanel::GrainControlPanel()
{
titleLabel.setText ("GRAIN", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
addAndMakeVisible (titleLabel);
setupKnob (positionKnob, posLabel, "Position", 0.0, 1.0, 0.5);
setupKnob (sizeKnob, sizeLabel, "Size", 10.0, 500.0, 100.0, " ms");
setupKnob (densityKnob, densityLabel, "Density", 1.0, 100.0, 10.0, " g/s");
setupKnob (pitchKnob, pitchLabel, "Pitch", -24.0, 24.0, 0.0, " st");
setupKnob (panKnob, panLabel, "Pan", -1.0, 1.0, 0.0);
// Root Note selector (MIDI 0-127)
const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
for (int i = 0; i <= 127; ++i)
{
int octave = (i / 12) - 1;
juce::String name = juce::String (noteNames[i % 12]) + juce::String (octave);
rootNoteBox.addItem (name, i + 1); // ComboBox IDs start at 1
}
rootNoteBox.setSelectedId (60 + 1); // Default: C4
addAndMakeVisible (rootNoteBox);
rootNoteLabel.setText ("Root", juce::dontSendNotification);
rootNoteLabel.setJustificationType (juce::Justification::centred);
rootNoteLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (rootNoteLabel);
}
void GrainControlPanel::setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name,
double min, double max, double def, const juce::String& suffix)
{
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 14);
knob.setRange (min, max);
knob.setValue (def);
knob.setTextValueSuffix (suffix);
knob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, "orange");
addAndMakeVisible (knob);
label.setText (name, juce::dontSendNotification);
label.setJustificationType (juce::Justification::centred);
label.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (label);
}
void GrainControlPanel::resized()
{
auto bounds = getLocalBounds().reduced (4);
titleLabel.setBounds (bounds.removeFromTop (20));
int knobW = bounds.getWidth() / 6;
int knobH = bounds.getHeight() - 16;
auto row = bounds.removeFromTop (knobH);
juce::Slider* knobs[] = { &positionKnob, &sizeKnob, &densityKnob, &pitchKnob, &panKnob };
juce::Label* labels[] = { &posLabel, &sizeLabel, &densityLabel, &pitchLabel, &panLabel };
for (int i = 0; i < 5; ++i)
{
auto col = row.removeFromLeft (knobW);
knobs[i]->setBounds (col.reduced (2));
labels[i]->setBounds (col.getX(), col.getBottom() - 2, col.getWidth(), 16);
}
// Root Note combo in the remaining space
auto rootCol = row;
rootNoteLabel.setBounds (rootCol.removeFromTop (14));
rootNoteBox.setBounds (rootCol.reduced (4, 2).removeFromTop (24));
}
void GrainControlPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}

Fájl megtekintése

@@ -0,0 +1,24 @@
#pragma once
#include <JuceHeader.h>
#include "LookAndFeel.h"
class GrainControlPanel : public juce::Component
{
public:
GrainControlPanel();
void resized() override;
void paint (juce::Graphics& g) override;
juce::Slider positionKnob, sizeKnob, densityKnob, pitchKnob, panKnob;
juce::ComboBox rootNoteBox;
private:
juce::Label posLabel, sizeLabel, densityLabel, pitchLabel, panLabel;
juce::Label rootNoteLabel;
juce::Label titleLabel;
void setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name,
double min, double max, double def, const juce::String& suffix = "");
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GrainControlPanel)
};

305
Source/GrainEngine.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,305 @@
#include "GrainEngine.h"
GrainEngine::GrainEngine()
{
formatManager.registerBasicFormats();
}
void GrainEngine::prepare (double sampleRate, int samplesPerBlock)
{
currentSampleRate = sampleRate;
currentBlockSize = samplesPerBlock;
for (auto& voice : voices)
voice.prepare (sampleRate);
// Filter
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = (juce::uint32) samplesPerBlock;
spec.numChannels = 2;
filter.prepare (spec);
filter.setType (juce::dsp::StateVariableTPTFilterType::lowpass);
filter.setCutoffFrequency (20000.0f);
filter.setResonance (0.707f);
// Reverb
reverb.setSampleRate (sampleRate);
reverbParams.roomSize = 0.0f;
reverbParams.damping = 0.5f;
reverbParams.wetLevel = 0.0f;
reverbParams.dryLevel = 1.0f;
reverbParams.width = 1.0f;
reverb.setParameters (reverbParams);
}
void GrainEngine::loadSample (const juce::File& file)
{
auto* reader = formatManager.createReaderFor (file);
if (reader == nullptr) return;
juce::AudioBuffer<float> tempBuffer ((int) reader->numChannels, (int) reader->lengthInSamples);
reader->read (&tempBuffer, 0, (int) reader->lengthInSamples, 0, true, true);
// Convert to mono if stereo
if (tempBuffer.getNumChannels() > 1)
{
juce::AudioBuffer<float> monoBuffer (1, tempBuffer.getNumSamples());
monoBuffer.clear();
for (int ch = 0; ch < tempBuffer.getNumChannels(); ++ch)
monoBuffer.addFrom (0, 0, tempBuffer, ch, 0, tempBuffer.getNumSamples(),
1.0f / (float) tempBuffer.getNumChannels());
sampleBuffer = std::move (monoBuffer);
}
else
{
sampleBuffer = std::move (tempBuffer);
}
loadedSamplePath = file.getFullPathName();
sourceSampleRate = reader->sampleRate;
// Reset all voices
for (auto& voice : voices)
voice.getCloud().reset();
delete reader;
}
void GrainEngine::loadSample (const void* data, size_t dataSize, const juce::String& /*formatName*/)
{
auto stream = std::make_unique<juce::MemoryInputStream> (data, dataSize, false);
auto* reader = formatManager.createReaderFor (std::move (stream));
if (reader == nullptr) return;
juce::AudioBuffer<float> tempBuffer ((int) reader->numChannels, (int) reader->lengthInSamples);
reader->read (&tempBuffer, 0, (int) reader->lengthInSamples, 0, true, true);
if (tempBuffer.getNumChannels() > 1)
{
juce::AudioBuffer<float> monoBuffer (1, tempBuffer.getNumSamples());
monoBuffer.clear();
for (int ch = 0; ch < tempBuffer.getNumChannels(); ++ch)
monoBuffer.addFrom (0, 0, tempBuffer, ch, 0, tempBuffer.getNumSamples(),
1.0f / (float) tempBuffer.getNumChannels());
sampleBuffer = std::move (monoBuffer);
}
else
{
sampleBuffer = std::move (tempBuffer);
}
loadedSamplePath = "";
sourceSampleRate = reader->sampleRate;
for (auto& voice : voices)
voice.getCloud().reset();
delete reader;
}
void GrainEngine::syncVoiceParameters()
{
for (auto& voice : voices)
{
auto& cloud = voice.getCloud();
cloud.position.store (position.load());
cloud.grainSizeMs.store (grainSizeMs.load());
cloud.density.store (density.load());
cloud.pitchSemitones.store (pitchSemitones.load());
cloud.pan.store (pan.load());
cloud.posScatter.store (posScatter.load());
cloud.sizeScatter.store (sizeScatter.load());
cloud.pitchScatter.store (pitchScatter.load());
cloud.panScatter.store (panScatter.load());
cloud.direction.store (direction.load());
cloud.freeze.store (freeze.load());
cloud.sampleRateRatio = (float) (sourceSampleRate / currentSampleRate);
voice.rootNote.store (rootNote.load());
voice.attackTime.store (attackTime.load());
voice.decayTime.store (decayTime.load());
voice.sustainLevel.store (sustainLevel.load());
voice.releaseTime.store (releaseTime.load());
}
}
void GrainEngine::handleNoteOff (int note)
{
// Release ALL voices playing this note (not just the first)
// Prevents stuck notes when the same key is pressed multiple times with sustain pedal
for (int i = 0; i < maxVoices; ++i)
{
if (voices[i].isActive() && voices[i].getCurrentNote() == note)
{
if (sustainPedalDown)
sustainedVoices[i] = true;
else
voices[i].noteOff();
}
}
}
void GrainEngine::handleMidiEvent (const juce::MidiMessage& msg)
{
if (msg.isNoteOn())
{
// Velocity 0 = note-off (standard MIDI convention)
if (msg.getFloatVelocity() == 0.0f)
{
handleNoteOff (msg.getNoteNumber());
return;
}
int note = msg.getNoteNumber();
float vel = msg.getFloatVelocity();
// Find free voice or steal oldest
int targetIdx = -1;
for (int i = 0; i < maxVoices; ++i)
{
if (! voices[i].isActive())
{
targetIdx = i;
break;
}
}
// If no free voice, steal first
if (targetIdx < 0)
targetIdx = 0;
// Clear sustained flag when stealing/reusing a voice slot
sustainedVoices[targetIdx] = false;
voices[targetIdx].noteOn (note, vel);
}
else if (msg.isNoteOff())
{
handleNoteOff (msg.getNoteNumber());
}
else if (msg.isSustainPedalOn())
{
sustainPedalDown = true;
}
else if (msg.isSustainPedalOff())
{
sustainPedalDown = false;
for (int i = 0; i < maxVoices; ++i)
{
if (sustainedVoices[i])
{
voices[i].noteOff();
sustainedVoices[i] = false;
}
}
}
else if (msg.isAllNotesOff())
{
sustainPedalDown = false;
for (int i = 0; i < maxVoices; ++i)
{
sustainedVoices[i] = false;
if (voices[i].isActive())
voices[i].noteOff();
}
}
else if (msg.isAllSoundOff())
{
// Immediate kill — no release tail
sustainPedalDown = false;
for (int i = 0; i < maxVoices; ++i)
{
sustainedVoices[i] = false;
voices[i].forceStop();
}
}
}
void GrainEngine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
const int numSamples = buffer.getNumSamples();
// Sync parameters
syncVoiceParameters();
// Handle MIDI
for (const auto metadata : midiMessages)
handleMidiEvent (metadata.getMessage());
// Clear output
buffer.clear();
// Process all voices
for (auto& voice : voices)
{
if (voice.isActive())
voice.processBlock (buffer, numSamples, sampleBuffer);
}
// Global filter
{
float cutoff = filterCutoff.load();
float reso = filterReso.load();
int fType = filterType.load();
filter.setCutoffFrequency (cutoff);
filter.setResonance (reso);
switch (fType)
{
case 0: filter.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
case 1: filter.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
case 2: filter.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
}
// Only apply if cutoff < 19999 (otherwise skip for efficiency)
if (cutoff < 19999.0f || fType != 0)
{
juce::dsp::AudioBlock<float> block (buffer);
juce::dsp::ProcessContextReplacing<float> context (block);
filter.process (context);
}
}
// Global reverb
{
float size = reverbSize.load();
float decay = reverbDecay.load();
if (size > 0.001f || decay > 0.001f)
{
reverbParams.roomSize = size;
reverbParams.damping = 1.0f - decay;
reverbParams.wetLevel = std::max (size, decay) * 0.5f;
reverbParams.dryLevel = 1.0f - reverbParams.wetLevel * 0.3f;
reverb.setParameters (reverbParams);
reverb.processStereo (buffer.getWritePointer (0), buffer.getWritePointer (1), numSamples);
}
}
// Master volume
float vol = masterVolume.load();
buffer.applyGain (vol);
// VU meter
vuLevelL.store (buffer.getMagnitude (0, 0, numSamples));
if (buffer.getNumChannels() > 1)
vuLevelR.store (buffer.getMagnitude (1, 0, numSamples));
else
vuLevelR.store (vuLevelL.load());
}
std::vector<GrainEngine::ActiveGrainInfo> GrainEngine::getActiveGrainInfo() const
{
std::vector<ActiveGrainInfo> result;
for (const auto& voice : voices)
{
if (! voice.isActive()) continue;
auto cloudInfo = voice.getCloud().getActiveGrainInfo();
for (const auto& gi : cloudInfo)
{
if (gi.startSample >= 0)
result.push_back ({ gi.startSample, gi.lengthSamples, gi.progress });
}
}
return result;
}

86
Source/GrainEngine.h Normal file
Fájl megtekintése

@@ -0,0 +1,86 @@
#pragma once
#include <JuceHeader.h>
#include "GrainVoice.h"
class GrainEngine
{
public:
static constexpr int maxVoices = 8;
GrainEngine();
void prepare (double sampleRate, int samplesPerBlock);
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages);
void loadSample (const juce::File& file);
void loadSample (const void* data, size_t dataSize, const juce::String& formatName);
const juce::AudioBuffer<float>& getSampleBuffer() const { return sampleBuffer; }
double getSampleRate() const { return currentSampleRate; }
bool hasSample() const { return sampleBuffer.getNumSamples() > 0; }
juce::String getSamplePath() const { return loadedSamplePath; }
// Root note — which MIDI note the sample represents (default C4 = 60)
std::atomic<int> rootNote { 60 };
// Grain parameters (GUI → audio)
std::atomic<float> position { 0.5f };
std::atomic<float> grainSizeMs { 100.0f };
std::atomic<float> density { 10.0f };
std::atomic<float> pitchSemitones { 0.0f };
std::atomic<float> pan { 0.0f };
std::atomic<float> posScatter { 0.0f };
std::atomic<float> sizeScatter { 0.0f };
std::atomic<float> pitchScatter { 0.0f };
std::atomic<float> panScatter { 0.0f };
std::atomic<int> direction { 0 };
std::atomic<bool> freeze { false };
// ADSR
std::atomic<float> attackTime { 0.01f };
std::atomic<float> decayTime { 0.1f };
std::atomic<float> sustainLevel { 1.0f };
std::atomic<float> releaseTime { 0.3f };
// Filter
std::atomic<int> filterType { 0 }; // 0=LP, 1=HP, 2=BP
std::atomic<float> filterCutoff { 20000.0f };
std::atomic<float> filterReso { 0.707f };
// Reverb
std::atomic<float> reverbSize { 0.0f };
std::atomic<float> reverbDecay { 0.0f };
// Master
std::atomic<float> masterVolume { 1.0f };
// VU meter levels (audio → GUI)
std::atomic<float> vuLevelL { 0.0f };
std::atomic<float> vuLevelR { 0.0f };
// Grain visualization
struct ActiveGrainInfo { int startSample; int lengthSamples; float progress; };
std::vector<ActiveGrainInfo> getActiveGrainInfo() const;
private:
std::array<GrainVoice, maxVoices> voices;
juce::AudioBuffer<float> sampleBuffer;
juce::String loadedSamplePath;
double currentSampleRate = 44100.0;
double sourceSampleRate = 44100.0;
int currentBlockSize = 512;
juce::AudioFormatManager formatManager;
// Global effects
juce::dsp::StateVariableTPTFilter<float> filter;
juce::Reverb reverb;
juce::Reverb::Parameters reverbParams;
void handleMidiEvent (const juce::MidiMessage& msg);
void handleNoteOff (int note);
void syncVoiceParameters();
// Sustain pedal
bool sustainPedalDown = false;
std::array<bool, maxVoices> sustainedVoices {}; // voices held by pedal
};

78
Source/GrainVoice.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,78 @@
#include "GrainVoice.h"
GrainVoice::GrainVoice() {}
void GrainVoice::prepare (double sampleRate)
{
currentSampleRate = sampleRate;
cloud.prepare (sampleRate);
adsr.setSampleRate (sampleRate);
}
void GrainVoice::noteOn (int midiNote, float velocity)
{
currentNote = midiNote;
velocityGain = velocity;
voiceActive = true;
// Pitch offset relative to sample's root note
cloud.midiPitchOffset = (float) (midiNote - rootNote.load());
// Update ADSR
adsrParams.attack = attackTime.load();
adsrParams.decay = decayTime.load();
adsrParams.sustain = sustainLevel.load();
adsrParams.release = releaseTime.load();
adsr.setParameters (adsrParams);
adsr.noteOn();
cloud.reset();
}
void GrainVoice::noteOff()
{
adsr.noteOff();
}
void GrainVoice::forceStop()
{
adsr.reset();
voiceActive = false;
currentNote = -1;
cloud.reset();
}
void GrainVoice::processBlock (juce::AudioBuffer<float>& output, int numSamples,
const juce::AudioBuffer<float>& sourceBuffer)
{
if (! voiceActive) return;
// Render cloud into temp buffer
juce::AudioBuffer<float> voiceBuffer (output.getNumChannels(), numSamples);
voiceBuffer.clear();
cloud.processBlock (voiceBuffer, numSamples, sourceBuffer);
// Compute ADSR envelope once per sample (not per channel!)
std::vector<float> envBuffer ((size_t) numSamples);
for (int i = 0; i < numSamples; ++i)
envBuffer[(size_t) i] = adsr.getNextSample() * velocityGain;
// Apply envelope and mix into output
for (int ch = 0; ch < output.getNumChannels(); ++ch)
{
const float* voiceData = voiceBuffer.getReadPointer (ch);
float* outData = output.getWritePointer (ch);
for (int i = 0; i < numSamples; ++i)
outData[i] += voiceData[i] * envBuffer[(size_t) i];
}
// Check if ADSR has finished
if (! adsr.isActive())
{
voiceActive = false;
currentNote = -1;
cloud.reset();
}
}

40
Source/GrainVoice.h Normal file
Fájl megtekintése

@@ -0,0 +1,40 @@
#pragma once
#include <JuceHeader.h>
#include "GrainCloud.h"
class GrainVoice
{
public:
GrainVoice();
void prepare (double sampleRate);
void noteOn (int midiNote, float velocity);
void noteOff();
void forceStop();
void processBlock (juce::AudioBuffer<float>& output, int numSamples,
const juce::AudioBuffer<float>& sourceBuffer);
bool isActive() const { return voiceActive; }
int getCurrentNote() const { return currentNote; }
GrainCloud& getCloud() { return cloud; }
const GrainCloud& getCloud() const { return cloud; }
// ADSR parameters (set from processor)
// Root note reference (set from engine)
std::atomic<int> rootNote { 60 };
// ADSR parameters (set from processor)
std::atomic<float> attackTime { 0.01f };
std::atomic<float> decayTime { 0.1f };
std::atomic<float> sustainLevel { 1.0f };
std::atomic<float> releaseTime { 0.3f };
private:
GrainCloud cloud;
juce::ADSR adsr;
juce::ADSR::Parameters adsrParams;
float velocityGain = 1.0f;
int currentNote = -1;
bool voiceActive = false;
double currentSampleRate = 44100.0;
};

372
Source/LookAndFeel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,372 @@
#include "LookAndFeel.h"
#include "BinaryData.h"
InstaGrainLookAndFeel::InstaGrainLookAndFeel()
{
typefaceRegular = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniRegular_ttf, BinaryData::RajdhaniRegular_ttfSize);
typefaceMedium = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniMedium_ttf, BinaryData::RajdhaniMedium_ttfSize);
typefaceBold = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniBold_ttf, BinaryData::RajdhaniBold_ttfSize);
setColour (juce::ResizableWindow::backgroundColourId, bgDark);
setColour (juce::Label::textColourId, textPrimary);
setColour (juce::TextButton::buttonColourId, bgMedium);
setColour (juce::TextButton::textColourOffId, textPrimary);
setColour (juce::ComboBox::backgroundColourId, bgMedium);
setColour (juce::ComboBox::textColourId, textPrimary);
setColour (juce::ComboBox::outlineColourId, bgLight);
setColour (juce::PopupMenu::backgroundColourId, bgMedium);
setColour (juce::PopupMenu::textColourId, textPrimary);
setColour (juce::PopupMenu::highlightedBackgroundColourId, bgLight);
generateNoiseTexture();
}
juce::Typeface::Ptr InstaGrainLookAndFeel::getTypefaceForFont (const juce::Font& font)
{
if (font.isBold())
return typefaceBold;
return typefaceRegular;
}
juce::Font InstaGrainLookAndFeel::getRegularFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceRegular).withHeight (height));
}
juce::Font InstaGrainLookAndFeel::getMediumFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceMedium).withHeight (height));
}
juce::Font InstaGrainLookAndFeel::getBoldFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceBold).withHeight (height));
}
void InstaGrainLookAndFeel::generateNoiseTexture()
{
const int texW = 256, texH = 256;
noiseTexture = juce::Image (juce::Image::ARGB, texW, texH, true);
juce::Random rng (42);
for (int y = 0; y < texH; ++y)
{
for (int x = 0; x < texW; ++x)
{
float noise = rng.nextFloat() * 0.06f;
bool crossA = ((x + y) % 4 == 0);
bool crossB = ((x - y + 256) % 4 == 0);
float pattern = (crossA || crossB) ? 0.03f : 0.0f;
float alpha = noise + pattern;
noiseTexture.setPixelAt (x, y, juce::Colour::fromFloatRGBA (1.0f, 1.0f, 1.0f, alpha));
}
}
}
void InstaGrainLookAndFeel::drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area)
{
for (int y = area.getY(); y < area.getBottom(); y += noiseTexture.getHeight())
for (int x = area.getX(); x < area.getRight(); x += noiseTexture.getWidth())
g.drawImageAt (noiseTexture, x, y);
}
// ============================================================
// Rotary slider (3D metal knob)
// ============================================================
void InstaGrainLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider& slider)
{
float knobSize = std::min ((float) width, (float) height);
float s = knobSize / 60.0f;
float margin = std::max (4.0f, 6.0f * s);
auto bounds = juce::Rectangle<int> (x, y, width, height).toFloat().reduced (margin);
auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f;
auto cx = bounds.getCentreX();
auto cy = bounds.getCentreY();
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
auto knobType = slider.getProperties() [knobTypeProperty].toString();
bool isDark = (knobType == "dark");
juce::Colour arcColour = isDark ? juce::Colour (0xff4488ff) : juce::Colour (0xffff8833);
juce::Colour arcBgColour = isDark ? juce::Colour (0xff1a2a44) : juce::Colour (0xff2a1a0a);
juce::Colour bodyTop = isDark ? juce::Colour (0xff3a3a4a) : juce::Colour (0xff5a4a3a);
juce::Colour bodyBottom = isDark ? juce::Colour (0xff1a1a2a) : juce::Colour (0xff2a1a0a);
juce::Colour rimColour = isDark ? juce::Colour (0xff555566) : juce::Colour (0xff886644);
juce::Colour highlightCol = isDark ? juce::Colour (0x33aabbff) : juce::Colour (0x44ffcc88);
juce::Colour pointerColour = isDark ? juce::Colour (0xff66aaff) : juce::Colour (0xffffaa44);
float arcW = std::max (1.5f, 2.5f * s);
float glowW1 = std::max (3.0f, 10.0f * s);
float hotW = std::max (0.8f, 1.2f * s);
float ptrW = std::max (1.2f, 2.0f * s);
float bodyRadius = radius * 0.72f;
// 1. Drop shadow
g.setColour (juce::Colours::black.withAlpha (0.35f));
g.fillEllipse (cx - bodyRadius + 1, cy - bodyRadius + 2, bodyRadius * 2, bodyRadius * 2);
// 2. Outer arc track
{
juce::Path arcBg;
arcBg.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
rotaryStartAngle, rotaryEndAngle, true);
g.setColour (arcBgColour);
g.strokePath (arcBg, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
// 3. Outer arc value with glow
if (sliderPos > 0.01f)
{
juce::Path arcVal;
arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
rotaryStartAngle, angle, true);
const int numGlowLayers = 8;
for (int i = 0; i < numGlowLayers; ++i)
{
float t = (float) i / (float) (numGlowLayers - 1);
float layerWidth = glowW1 * (1.0f - t * 0.7f);
float layerAlpha = 0.03f + t * t * 0.35f;
g.setColour (arcColour.withAlpha (layerAlpha));
g.strokePath (arcVal, juce::PathStrokeType (layerWidth, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
g.setColour (arcColour);
g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f));
g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
// 4. Knob body
{
juce::ColourGradient bodyGrad (bodyTop, cx, cy - bodyRadius * 0.5f,
bodyBottom, cx, cy + bodyRadius, true);
g.setGradientFill (bodyGrad);
g.fillEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2);
}
// 5. Rim
g.setColour (rimColour.withAlpha (0.6f));
g.drawEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2, std::max (0.8f, 1.2f * s));
// 6. Inner shadow
{
float innerR = bodyRadius * 0.85f;
juce::ColourGradient innerGrad (juce::Colours::black.withAlpha (0.15f), cx, cy - innerR * 0.3f,
juce::Colours::transparentBlack, cx, cy + innerR, true);
g.setGradientFill (innerGrad);
g.fillEllipse (cx - innerR, cy - innerR, innerR * 2, innerR * 2);
}
// 7. Top highlight
{
float hlRadius = bodyRadius * 0.55f;
float hlY = cy - bodyRadius * 0.35f;
juce::ColourGradient hlGrad (highlightCol, cx, hlY - hlRadius * 0.5f,
juce::Colours::transparentBlack, cx, hlY + hlRadius, true);
g.setGradientFill (hlGrad);
g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f);
}
// 8. Pointer with glow
{
float pointerLen = bodyRadius * 0.75f;
for (int i = 0; i < 4; ++i)
{
float t = (float) i / 3.0f;
float gw = ptrW * (2.0f - t * 1.5f);
float alpha = 0.02f + t * t * 0.15f;
juce::Path glowLayer;
glowLayer.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, gw * 0.5f);
glowLayer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour.withAlpha (alpha));
g.fillPath (glowLayer);
}
{
juce::Path pointer;
pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f);
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour);
g.fillPath (pointer);
}
{
juce::Path hotCenter;
float hw = ptrW * 0.3f;
hotCenter.addRoundedRectangle (-hw, -pointerLen, hw * 2, pointerLen * 0.5f, hw);
hotCenter.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour.brighter (0.7f).withAlpha (0.6f));
g.fillPath (hotCenter);
}
}
// 9. Center cap
{
float capR = bodyRadius * 0.18f;
juce::ColourGradient capGrad (rimColour.brighter (0.3f), cx, cy - capR,
bodyBottom, cx, cy + capR, false);
g.setGradientFill (capGrad);
g.fillEllipse (cx - capR, cy - capR, capR * 2, capR * 2);
}
}
// ============================================================
// Button style
// ============================================================
void InstaGrainLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown)
{
auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
auto baseColour = backgroundColour;
if (shouldDrawButtonAsDown)
baseColour = baseColour.brighter (0.2f);
else if (shouldDrawButtonAsHighlighted)
baseColour = baseColour.brighter (0.1f);
juce::ColourGradient grad (baseColour.brighter (0.05f), 0, bounds.getY(),
baseColour.darker (0.1f), 0, bounds.getBottom(), false);
g.setGradientFill (grad);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}
// ============================================================
// Toggle button — glowing switch
// ============================================================
void InstaGrainLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool shouldDrawButtonAsHighlighted,
bool /*shouldDrawButtonAsDown*/)
{
auto bounds = button.getLocalBounds().toFloat();
float h = std::min (bounds.getHeight() * 0.6f, 14.0f);
float w = h * 1.8f;
float trackR = h * 0.5f;
float sx = bounds.getX() + (bounds.getWidth() - w) * 0.5f;
float sy = bounds.getCentreY() - h * 0.5f;
auto trackBounds = juce::Rectangle<float> (sx, sy, w, h);
bool isOn = button.getToggleState();
auto onColour = accent;
auto offColour = bgLight;
float targetPos = isOn ? 1.0f : 0.0f;
float animPos = (float) button.getProperties().getWithDefault ("animPos", targetPos);
animPos += (targetPos - animPos) * 0.25f;
if (std::abs (animPos - targetPos) < 0.01f) animPos = targetPos;
button.getProperties().set ("animPos", animPos);
if (std::abs (animPos - targetPos) > 0.005f)
button.repaint();
float thumbR = h * 0.4f;
float thumbX = sx + trackR + animPos * (w - trackR * 2);
float thumbY = sy + h * 0.5f;
float glowIntensity = animPos;
if (glowIntensity > 0.01f)
{
for (int i = 0; i < 3; ++i)
{
float t = (float) i / 2.0f;
float expand = (1.0f - t) * 1.5f;
float alpha = (0.04f + t * t * 0.1f) * glowIntensity;
g.setColour (onColour.withAlpha (alpha));
g.fillRoundedRectangle (trackBounds.expanded (expand), trackR + expand);
}
}
{
juce::Colour offCol = offColour.withAlpha (0.3f);
juce::Colour onCol = onColour.withAlpha (0.35f);
juce::Colour trackCol = offCol.interpolatedWith (onCol, glowIntensity);
if (shouldDrawButtonAsHighlighted)
trackCol = trackCol.brighter (0.15f);
g.setColour (trackCol);
g.fillRoundedRectangle (trackBounds, trackR);
g.setColour (offColour.withAlpha (0.4f).interpolatedWith (onColour.withAlpha (0.5f), glowIntensity));
g.drawRoundedRectangle (trackBounds, trackR, 0.8f);
}
if (glowIntensity > 0.01f)
{
for (int i = 0; i < 3; ++i)
{
float t = (float) i / 2.0f;
float r = thumbR * (1.5f - t * 0.5f);
float alpha = (0.05f + t * t * 0.12f) * glowIntensity;
g.setColour (onColour.withAlpha (alpha));
g.fillEllipse (thumbX - r, thumbY - r, r * 2, r * 2);
}
}
{
juce::Colour thumbTopOff (0xff555566), thumbBotOff (0xff333344);
juce::Colour thumbTopOn = onColour.brighter (0.3f), thumbBotOn = onColour.darker (0.2f);
juce::ColourGradient thumbGrad (
thumbTopOff.interpolatedWith (thumbTopOn, glowIntensity), thumbX, thumbY - thumbR,
thumbBotOff.interpolatedWith (thumbBotOn, glowIntensity), thumbX, thumbY + thumbR, false);
g.setGradientFill (thumbGrad);
g.fillEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2);
g.setColour (juce::Colour (0xff666677).withAlpha (0.5f).interpolatedWith (onColour.withAlpha (0.6f), glowIntensity));
g.drawEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2, 0.8f);
float hlR = thumbR * 0.4f;
g.setColour (juce::Colours::white.withAlpha (0.1f + 0.15f * glowIntensity));
g.fillEllipse (thumbX - hlR, thumbY - thumbR * 0.6f - hlR * 0.3f, hlR * 2, hlR * 1.2f);
}
}
// ============================================================
// ComboBox style
// ============================================================
void InstaGrainLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool /*isButtonDown*/,
int /*buttonX*/, int /*buttonY*/, int /*buttonW*/, int /*buttonH*/,
juce::ComboBox& box)
{
auto bounds = juce::Rectangle<float> (0, 0, (float) width, (float) height);
g.setColour (bgMedium);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (bgLight.withAlpha (0.5f));
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
// Arrow
float arrowSize = height * 0.3f;
float arrowX = (float) width - height * 0.6f;
float arrowY = ((float) height - arrowSize) * 0.5f;
juce::Path arrow;
arrow.addTriangle (arrowX, arrowY,
arrowX + arrowSize, arrowY,
arrowX + arrowSize * 0.5f, arrowY + arrowSize);
g.setColour (textSecondary);
g.fillPath (arrow);
}

54
Source/LookAndFeel.h Normal file
Fájl megtekintése

@@ -0,0 +1,54 @@
#pragma once
#include <JuceHeader.h>
class InstaGrainLookAndFeel : public juce::LookAndFeel_V4
{
public:
// Colour palette
static inline const juce::Colour bgDark { 0xff1a1a2e };
static inline const juce::Colour bgMedium { 0xff16213e };
static inline const juce::Colour bgLight { 0xff0f3460 };
static inline const juce::Colour textPrimary { 0xffe0e0e0 };
static inline const juce::Colour textSecondary { 0xff888899 };
static inline const juce::Colour accent { 0xff00ff88 };
// Knob type property key
static constexpr const char* knobTypeProperty = "knobType";
InstaGrainLookAndFeel();
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPosProportional, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider& slider) override;
void drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;
void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;
void drawComboBox (juce::Graphics& g, int width, int height, bool isButtonDown,
int buttonX, int buttonY, int buttonW, int buttonH,
juce::ComboBox& box) override;
// Custom fonts
juce::Font getRegularFont (float height) const;
juce::Font getMediumFont (float height) const;
juce::Font getBoldFont (float height) const;
// Background texture
void drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area);
juce::Typeface::Ptr getTypefaceForFont (const juce::Font& font) override;
private:
juce::Typeface::Ptr typefaceRegular;
juce::Typeface::Ptr typefaceMedium;
juce::Typeface::Ptr typefaceBold;
juce::Image noiseTexture;
void generateNoiseTexture();
};

49
Source/MasterPanel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,49 @@
#include "MasterPanel.h"
MasterPanel::MasterPanel()
{
titleLabel.setText ("MASTER", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
addAndMakeVisible (titleLabel);
volumeKnob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
volumeKnob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 55, 14);
volumeKnob.setRange (0.0, 2.0);
volumeKnob.setValue (1.0);
volumeKnob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, "orange");
addAndMakeVisible (volumeKnob);
volLabel.setText ("Volume", juce::dontSendNotification);
volLabel.setJustificationType (juce::Justification::centred);
volLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (volLabel);
addAndMakeVisible (vuMeter);
}
void MasterPanel::resized()
{
auto bounds = getLocalBounds().reduced (4);
titleLabel.setBounds (bounds.removeFromTop (20));
int halfW = bounds.getWidth() / 2;
// Volume knob
auto knobArea = bounds.removeFromLeft (halfW);
auto knobRect = knobArea.withTrimmedBottom (16);
volumeKnob.setBounds (knobRect.reduced (2));
volLabel.setBounds (knobArea.getX(), knobRect.getBottom() - 2, knobArea.getWidth(), 16);
// VU meter
vuMeter.setBounds (bounds.reduced (8, 4));
}
void MasterPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}

21
Source/MasterPanel.h Normal file
Fájl megtekintése

@@ -0,0 +1,21 @@
#pragma once
#include <JuceHeader.h>
#include "LookAndFeel.h"
#include "VuMeter.h"
class MasterPanel : public juce::Component
{
public:
MasterPanel();
void resized() override;
void paint (juce::Graphics& g) override;
juce::Slider volumeKnob;
VuMeter vuMeter;
private:
juce::Label volLabel;
juce::Label titleLabel;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MasterPanel)
};

252
Source/PluginEditor.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,252 @@
#include "PluginEditor.h"
InstaGrainEditor::InstaGrainEditor (InstaGrainProcessor& p)
: AudioProcessorEditor (&p), processor (p)
{
setLookAndFeel (&lookAndFeel);
setSize (900, 650);
setResizable (true, true);
setResizeLimits (700, 500, 1400, 1000);
// Title
titleLabel.setText ("INSTAGRAIN", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::accent);
addAndMakeVisible (titleLabel);
// Version
versionLabel.setText (kInstaGrainVersion, juce::dontSendNotification);
versionLabel.setJustificationType (juce::Justification::centredLeft);
versionLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (versionLabel);
// Load button
loadButton.onClick = [this]
{
fileChooser = std::make_unique<juce::FileChooser> (
"Load Sample", juce::File(), "*.wav;*.aif;*.aiff;*.mp3;*.flac;*.ogg");
fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
[this] (const juce::FileChooser& fc)
{
auto file = fc.getResult();
if (file.existsAsFile())
loadSampleFile (file);
});
};
addAndMakeVisible (loadButton);
// Bypass
bypassButton.setToggleState (processor.bypass.load(), juce::dontSendNotification);
addAndMakeVisible (bypassButton);
bypassLabel.setText ("BYPASS", juce::dontSendNotification);
bypassLabel.setJustificationType (juce::Justification::centredLeft);
bypassLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (bypassLabel);
// Panels
addAndMakeVisible (waveformDisplay);
addAndMakeVisible (grainPanel);
addAndMakeVisible (scatterPanel);
addAndMakeVisible (envelopePanel);
addAndMakeVisible (effectsPanel);
addAndMakeVisible (masterPanel);
// Waveform position callback
waveformDisplay.onPositionChanged = [this] (float pos)
{
processor.getEngine().position.store (pos);
grainPanel.positionKnob.setValue (pos, juce::dontSendNotification);
};
// Initialize waveform if sample already loaded
if (processor.getEngine().hasSample())
waveformDisplay.setBuffer (&processor.getEngine().getSampleBuffer());
// Sync knobs from engine state
syncKnobsToEngine();
// Start timer (30 Hz)
startTimerHz (30);
}
InstaGrainEditor::~InstaGrainEditor()
{
setLookAndFeel (nullptr);
stopTimer();
}
void InstaGrainEditor::loadSampleFile (const juce::File& file)
{
processor.getEngine().loadSample (file);
waveformDisplay.setBuffer (&processor.getEngine().getSampleBuffer());
}
void InstaGrainEditor::syncKnobsToEngine()
{
auto& eng = processor.getEngine();
grainPanel.positionKnob.setValue (eng.position.load(), juce::dontSendNotification);
grainPanel.sizeKnob.setValue (eng.grainSizeMs.load(), juce::dontSendNotification);
grainPanel.densityKnob.setValue (eng.density.load(), juce::dontSendNotification);
grainPanel.pitchKnob.setValue (eng.pitchSemitones.load(), juce::dontSendNotification);
grainPanel.panKnob.setValue (eng.pan.load(), juce::dontSendNotification);
grainPanel.rootNoteBox.setSelectedId (eng.rootNote.load() + 1, juce::dontSendNotification);
scatterPanel.posScatterKnob.setValue (eng.posScatter.load(), juce::dontSendNotification);
scatterPanel.sizeScatterKnob.setValue (eng.sizeScatter.load(), juce::dontSendNotification);
scatterPanel.pitchScatterKnob.setValue (eng.pitchScatter.load(), juce::dontSendNotification);
scatterPanel.panScatterKnob.setValue (eng.panScatter.load(), juce::dontSendNotification);
scatterPanel.directionBox.setSelectedId (eng.direction.load() + 1, juce::dontSendNotification);
scatterPanel.freezeButton.setToggleState (eng.freeze.load(), juce::dontSendNotification);
envelopePanel.attackKnob.setValue (eng.attackTime.load(), juce::dontSendNotification);
envelopePanel.decayKnob.setValue (eng.decayTime.load(), juce::dontSendNotification);
envelopePanel.sustainKnob.setValue (eng.sustainLevel.load(), juce::dontSendNotification);
envelopePanel.releaseKnob.setValue (eng.releaseTime.load(), juce::dontSendNotification);
effectsPanel.filterTypeBox.setSelectedId (eng.filterType.load() + 1, juce::dontSendNotification);
effectsPanel.cutoffKnob.setValue (eng.filterCutoff.load(), juce::dontSendNotification);
effectsPanel.resoKnob.setValue (eng.filterReso.load(), juce::dontSendNotification);
effectsPanel.reverbSizeKnob.setValue (eng.reverbSize.load(), juce::dontSendNotification);
effectsPanel.reverbDecayKnob.setValue (eng.reverbDecay.load(), juce::dontSendNotification);
masterPanel.volumeKnob.setValue (eng.masterVolume.load(), juce::dontSendNotification);
bypassButton.setToggleState (processor.bypass.load(), juce::dontSendNotification);
}
void InstaGrainEditor::syncEngineFromKnobs()
{
auto& eng = processor.getEngine();
eng.position.store ((float) grainPanel.positionKnob.getValue());
eng.grainSizeMs.store ((float) grainPanel.sizeKnob.getValue());
eng.density.store ((float) grainPanel.densityKnob.getValue());
eng.pitchSemitones.store ((float) grainPanel.pitchKnob.getValue());
eng.pan.store ((float) grainPanel.panKnob.getValue());
eng.rootNote.store (grainPanel.rootNoteBox.getSelectedId() - 1);
eng.posScatter.store ((float) scatterPanel.posScatterKnob.getValue());
eng.sizeScatter.store ((float) scatterPanel.sizeScatterKnob.getValue());
eng.pitchScatter.store ((float) scatterPanel.pitchScatterKnob.getValue());
eng.panScatter.store ((float) scatterPanel.panScatterKnob.getValue());
eng.direction.store (scatterPanel.directionBox.getSelectedId() - 1);
eng.freeze.store (scatterPanel.freezeButton.getToggleState());
eng.attackTime.store ((float) envelopePanel.attackKnob.getValue());
eng.decayTime.store ((float) envelopePanel.decayKnob.getValue());
eng.sustainLevel.store ((float) envelopePanel.sustainKnob.getValue());
eng.releaseTime.store ((float) envelopePanel.releaseKnob.getValue());
eng.filterType.store (effectsPanel.filterTypeBox.getSelectedId() - 1);
eng.filterCutoff.store ((float) effectsPanel.cutoffKnob.getValue());
eng.filterReso.store ((float) effectsPanel.resoKnob.getValue());
eng.reverbSize.store ((float) effectsPanel.reverbSizeKnob.getValue());
eng.reverbDecay.store ((float) effectsPanel.reverbDecayKnob.getValue());
eng.masterVolume.store ((float) masterPanel.volumeKnob.getValue());
processor.bypass.store (bypassButton.getToggleState());
}
void InstaGrainEditor::timerCallback()
{
// GUI → Engine
syncEngineFromKnobs();
// Update waveform visualization
auto& eng = processor.getEngine();
waveformDisplay.setGrainPosition (eng.position.load());
waveformDisplay.setScatterRange (eng.posScatter.load());
waveformDisplay.setActiveGrains (eng.getActiveGrainInfo());
// Update VU meter
masterPanel.vuMeter.setLevel (eng.vuLevelL.load(), eng.vuLevelR.load());
}
void InstaGrainEditor::paint (juce::Graphics& g)
{
// Background
g.fillAll (InstaGrainLookAndFeel::bgDark);
lookAndFeel.drawBackgroundTexture (g, getLocalBounds());
// Top bar background
float scale = (float) getHeight() / 650.0f;
int topBarH = (int) (36.0f * scale);
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.7f));
g.fillRect (0, 0, getWidth(), topBarH);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawHorizontalLine (topBarH, 0, (float) getWidth());
}
void InstaGrainEditor::resized()
{
auto bounds = getLocalBounds();
float scale = (float) getHeight() / 650.0f;
int topBarH = (int) (36.0f * scale);
int padding = (int) (6.0f * scale);
// Top bar
auto topBar = bounds.removeFromTop (topBarH).reduced (padding, 0);
titleLabel.setFont (lookAndFeel.getBoldFont (20.0f * scale));
titleLabel.setBounds (topBar.removeFromLeft ((int) (130 * scale)));
versionLabel.setFont (lookAndFeel.getRegularFont (13.0f * scale));
versionLabel.setBounds (topBar.removeFromLeft ((int) (50 * scale)));
// Right side: bypass + load
auto bypassArea = topBar.removeFromRight ((int) (30 * scale));
bypassButton.setBounds (bypassArea);
bypassLabel.setFont (lookAndFeel.getRegularFont (11.0f * scale));
bypassLabel.setBounds (topBar.removeFromRight ((int) (50 * scale)));
topBar.removeFromRight (padding);
loadButton.setBounds (topBar.removeFromRight ((int) (120 * scale)).reduced (0, 2));
auto content = bounds.reduced (padding);
// Waveform display (~40%)
int waveH = (int) (content.getHeight() * 0.40f);
waveformDisplay.setBounds (content.removeFromTop (waveH));
content.removeFromTop (padding);
// Middle row: Grain + Scatter (~25%)
int midH = (int) (content.getHeight() * 0.45f);
auto midRow = content.removeFromTop (midH);
int halfW = midRow.getWidth() / 2;
grainPanel.setBounds (midRow.removeFromLeft (halfW - padding / 2));
midRow.removeFromLeft (padding);
scatterPanel.setBounds (midRow);
content.removeFromTop (padding);
// Bottom row: Envelope + Effects + Master
auto botRow = content;
int thirdW = botRow.getWidth() / 3;
envelopePanel.setBounds (botRow.removeFromLeft (thirdW - padding / 2).reduced (0, 0));
botRow.removeFromLeft (padding);
effectsPanel.setBounds (botRow.removeFromLeft (thirdW - padding / 2).reduced (0, 0));
botRow.removeFromLeft (padding);
masterPanel.setBounds (botRow);
}
bool InstaGrainEditor::isInterestedInFileDrag (const juce::StringArray& files)
{
for (auto& f : files)
{
auto ext = juce::File (f).getFileExtension().toLowerCase();
if (ext == ".wav" || ext == ".aif" || ext == ".aiff" || ext == ".mp3"
|| ext == ".flac" || ext == ".ogg")
return true;
}
return false;
}
void InstaGrainEditor::filesDropped (const juce::StringArray& files, int /*x*/, int /*y*/)
{
for (auto& f : files)
{
juce::File file (f);
auto ext = file.getFileExtension().toLowerCase();
if (ext == ".wav" || ext == ".aif" || ext == ".aiff" || ext == ".mp3"
|| ext == ".flac" || ext == ".ogg")
{
loadSampleFile (file);
break;
}
}
}

58
Source/PluginEditor.h Normal file
Fájl megtekintése

@@ -0,0 +1,58 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "LookAndFeel.h"
#include "WaveformDisplay.h"
#include "GrainControlPanel.h"
#include "ScatterPanel.h"
#include "EnvelopePanel.h"
#include "EffectsPanel.h"
#include "MasterPanel.h"
static constexpr const char* kInstaGrainVersion = "v1.0";
class InstaGrainEditor : public juce::AudioProcessorEditor,
public juce::FileDragAndDropTarget,
public juce::Timer
{
public:
explicit InstaGrainEditor (InstaGrainProcessor&);
~InstaGrainEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
// FileDragAndDropTarget
bool isInterestedInFileDrag (const juce::StringArray& files) override;
void filesDropped (const juce::StringArray& files, int x, int y) override;
// Timer
void timerCallback() override;
private:
InstaGrainProcessor& processor;
InstaGrainLookAndFeel lookAndFeel;
// Top bar
juce::Label titleLabel;
juce::Label versionLabel;
juce::TextButton loadButton { "LOAD SAMPLE" };
juce::ToggleButton bypassButton;
juce::Label bypassLabel;
// Panels
WaveformDisplay waveformDisplay;
GrainControlPanel grainPanel;
ScatterPanel scatterPanel;
EnvelopePanel envelopePanel;
EffectsPanel effectsPanel;
MasterPanel masterPanel;
std::unique_ptr<juce::FileChooser> fileChooser;
void loadSampleFile (const juce::File& file);
void syncKnobsToEngine();
void syncEngineFromKnobs();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaGrainEditor)
};

119
Source/PluginProcessor.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,119 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
InstaGrainProcessor::InstaGrainProcessor()
: AudioProcessor (BusesProperties()
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
{
}
InstaGrainProcessor::~InstaGrainProcessor() {}
void InstaGrainProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
engine.prepare (sampleRate, samplesPerBlock);
}
void InstaGrainProcessor::releaseResources() {}
bool InstaGrainProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
return true;
}
void InstaGrainProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
if (bypass.load())
{
buffer.clear();
return;
}
engine.processBlock (buffer, midiMessages);
}
juce::AudioProcessorEditor* InstaGrainProcessor::createEditor()
{
return new InstaGrainEditor (*this);
}
void InstaGrainProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto xml = std::make_unique<juce::XmlElement> ("InstaGrainState");
xml->setAttribute ("samplePath", engine.getSamplePath());
xml->setAttribute ("rootNote", engine.rootNote.load());
xml->setAttribute ("position", (double) engine.position.load());
xml->setAttribute ("grainSize", (double) engine.grainSizeMs.load());
xml->setAttribute ("density", (double) engine.density.load());
xml->setAttribute ("pitch", (double) engine.pitchSemitones.load());
xml->setAttribute ("pan", (double) engine.pan.load());
xml->setAttribute ("posScatter", (double) engine.posScatter.load());
xml->setAttribute ("sizeScatter", (double) engine.sizeScatter.load());
xml->setAttribute ("pitchScatter", (double) engine.pitchScatter.load());
xml->setAttribute ("panScatter", (double) engine.panScatter.load());
xml->setAttribute ("direction", engine.direction.load());
xml->setAttribute ("freeze", engine.freeze.load());
xml->setAttribute ("attack", (double) engine.attackTime.load());
xml->setAttribute ("decay", (double) engine.decayTime.load());
xml->setAttribute ("sustain", (double) engine.sustainLevel.load());
xml->setAttribute ("release", (double) engine.releaseTime.load());
xml->setAttribute ("filterType", engine.filterType.load());
xml->setAttribute ("filterCutoff", (double) engine.filterCutoff.load());
xml->setAttribute ("filterReso", (double) engine.filterReso.load());
xml->setAttribute ("reverbSize", (double) engine.reverbSize.load());
xml->setAttribute ("reverbDecay", (double) engine.reverbDecay.load());
xml->setAttribute ("masterVolume", (double) engine.masterVolume.load());
xml->setAttribute ("bypass", bypass.load());
copyXmlToBinary (*xml, destData);
}
void InstaGrainProcessor::setStateInformation (const void* data, int sizeInBytes)
{
auto xml = getXmlFromBinary (data, sizeInBytes);
if (xml == nullptr || ! xml->hasTagName ("InstaGrainState"))
return;
// Load sample
juce::String samplePath = xml->getStringAttribute ("samplePath", "");
if (samplePath.isNotEmpty())
{
juce::File f (samplePath);
if (f.existsAsFile())
engine.loadSample (f);
}
engine.rootNote.store (xml->getIntAttribute ("rootNote", 60));
engine.position.store ((float) xml->getDoubleAttribute ("position", 0.5));
engine.grainSizeMs.store ((float) xml->getDoubleAttribute ("grainSize", 100.0));
engine.density.store ((float) xml->getDoubleAttribute ("density", 10.0));
engine.pitchSemitones.store ((float) xml->getDoubleAttribute ("pitch", 0.0));
engine.pan.store ((float) xml->getDoubleAttribute ("pan", 0.0));
engine.posScatter.store ((float) xml->getDoubleAttribute ("posScatter", 0.0));
engine.sizeScatter.store ((float) xml->getDoubleAttribute ("sizeScatter", 0.0));
engine.pitchScatter.store ((float) xml->getDoubleAttribute ("pitchScatter", 0.0));
engine.panScatter.store ((float) xml->getDoubleAttribute ("panScatter", 0.0));
engine.direction.store (xml->getIntAttribute ("direction", 0));
engine.freeze.store (xml->getBoolAttribute ("freeze", false));
engine.attackTime.store ((float) xml->getDoubleAttribute ("attack", 0.01));
engine.decayTime.store ((float) xml->getDoubleAttribute ("decay", 0.1));
engine.sustainLevel.store ((float) xml->getDoubleAttribute ("sustain", 1.0));
engine.releaseTime.store ((float) xml->getDoubleAttribute ("release", 0.3));
engine.filterType.store (xml->getIntAttribute ("filterType", 0));
engine.filterCutoff.store ((float) xml->getDoubleAttribute ("filterCutoff", 20000.0));
engine.filterReso.store ((float) xml->getDoubleAttribute ("filterReso", 0.707));
engine.reverbSize.store ((float) xml->getDoubleAttribute ("reverbSize", 0.0));
engine.reverbDecay.store ((float) xml->getDoubleAttribute ("reverbDecay", 0.0));
engine.masterVolume.store ((float) xml->getDoubleAttribute ("masterVolume", 1.0));
bypass.store (xml->getBoolAttribute ("bypass", false));
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new InstaGrainProcessor();
}

43
Source/PluginProcessor.h Normal file
Fájl megtekintése

@@ -0,0 +1,43 @@
#pragma once
#include <JuceHeader.h>
#include "GrainEngine.h"
class InstaGrainProcessor : public juce::AudioProcessor
{
public:
InstaGrainProcessor();
~InstaGrainProcessor() override;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return JucePlugin_Name; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram (int) override {}
const juce::String getProgramName (int) override { return {}; }
void changeProgramName (int, const juce::String&) override {}
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
GrainEngine& getEngine() { return engine; }
std::atomic<bool> bypass { false };
private:
GrainEngine engine;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaGrainProcessor)
};

90
Source/ScatterPanel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,90 @@
#include "ScatterPanel.h"
ScatterPanel::ScatterPanel()
{
titleLabel.setText ("SCATTER", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredLeft);
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
addAndMakeVisible (titleLabel);
setupKnob (posScatterKnob, posScatLabel, "Pos");
setupKnob (sizeScatterKnob, sizeScatLabel, "Size");
setupKnob (pitchScatterKnob, pitchScatLabel, "Pitch");
setupKnob (panScatterKnob, panScatLabel, "Pan");
directionBox.addItem ("Forward", 1);
directionBox.addItem ("Reverse", 2);
directionBox.addItem ("PingPong", 3);
directionBox.setSelectedId (1);
addAndMakeVisible (directionBox);
dirLabel.setText ("Direction", juce::dontSendNotification);
dirLabel.setJustificationType (juce::Justification::centred);
dirLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (dirLabel);
freezeButton.setButtonText ("Freeze");
addAndMakeVisible (freezeButton);
freezeLabel.setText ("Freeze", juce::dontSendNotification);
freezeLabel.setJustificationType (juce::Justification::centred);
freezeLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (freezeLabel);
}
void ScatterPanel::setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name)
{
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 50, 14);
knob.setRange (0.0, 1.0);
knob.setValue (0.0);
knob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, "dark");
addAndMakeVisible (knob);
label.setText (name, juce::dontSendNotification);
label.setJustificationType (juce::Justification::centred);
label.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
addAndMakeVisible (label);
}
void ScatterPanel::resized()
{
auto bounds = getLocalBounds().reduced (4);
titleLabel.setBounds (bounds.removeFromTop (20));
// Top row: 4 knobs
int knobW = bounds.getWidth() / 4;
int knobH = (bounds.getHeight() - 30) * 2 / 3;
auto topRow = bounds.removeFromTop (knobH);
juce::Slider* knobs[] = { &posScatterKnob, &sizeScatterKnob, &pitchScatterKnob, &panScatterKnob };
juce::Label* labels[] = { &posScatLabel, &sizeScatLabel, &pitchScatLabel, &panScatLabel };
for (int i = 0; i < 4; ++i)
{
auto col = topRow.removeFromLeft (knobW);
knobs[i]->setBounds (col.reduced (2));
labels[i]->setBounds (col.getX(), col.getBottom() - 2, col.getWidth(), 16);
}
// Bottom row: Direction combo + Freeze toggle
auto botRow = bounds.reduced (2);
int halfW = botRow.getWidth() / 2;
auto dirArea = botRow.removeFromLeft (halfW);
dirLabel.setBounds (dirArea.removeFromTop (14));
directionBox.setBounds (dirArea.reduced (4, 2));
auto freezeArea = botRow;
freezeLabel.setBounds (freezeArea.removeFromTop (14));
freezeButton.setBounds (freezeArea.reduced (4, 2));
}
void ScatterPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}

24
Source/ScatterPanel.h Normal file
Fájl megtekintése

@@ -0,0 +1,24 @@
#pragma once
#include <JuceHeader.h>
#include "LookAndFeel.h"
class ScatterPanel : public juce::Component
{
public:
ScatterPanel();
void resized() override;
void paint (juce::Graphics& g) override;
juce::Slider posScatterKnob, sizeScatterKnob, pitchScatterKnob, panScatterKnob;
juce::ComboBox directionBox;
juce::ToggleButton freezeButton;
private:
juce::Label posScatLabel, sizeScatLabel, pitchScatLabel, panScatLabel;
juce::Label dirLabel, freezeLabel;
juce::Label titleLabel;
void setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScatterPanel)
};

62
Source/VuMeter.h Normal file
Fájl megtekintése

@@ -0,0 +1,62 @@
#pragma once
#include <JuceHeader.h>
class VuMeter : public juce::Component
{
public:
void setLevel (float left, float right)
{
if (left > peakL) peakL = left;
else peakL *= 0.995f;
if (right > peakR) peakR = right;
else peakR *= 0.995f;
levelL = std::max (left, levelL * 0.85f);
levelR = std::max (right, levelR * 0.85f);
repaint();
}
void paint (juce::Graphics& g) override
{
auto bounds = getLocalBounds().toFloat().reduced (1);
float barGap = 2.0f;
float halfW = (bounds.getWidth() - barGap) / 2.0f;
auto leftBar = bounds.removeFromLeft (halfW);
bounds.removeFromLeft (barGap);
auto rightBar = bounds;
drawBar (g, leftBar, levelL, peakL);
drawBar (g, rightBar, levelR, peakR);
}
private:
float levelL = 0.0f, levelR = 0.0f;
float peakL = 0.0f, peakR = 0.0f;
void drawBar (juce::Graphics& g, juce::Rectangle<float> bar, float level, float peak)
{
g.setColour (juce::Colour (0xff111122));
g.fillRoundedRectangle (bar, 2.0f);
float displayLevel = std::pow (juce::jlimit (0.0f, 1.0f, level), 0.5f);
float h = bar.getHeight() * displayLevel;
auto filled = bar.withTop (bar.getBottom() - h);
if (displayLevel < 0.6f)
g.setColour (juce::Colour (0xff00cc44));
else if (displayLevel < 0.85f)
g.setColour (juce::Colour (0xffcccc00));
else
g.setColour (juce::Colour (0xffff3333));
g.fillRoundedRectangle (filled, 2.0f);
float displayPeak = std::pow (juce::jlimit (0.0f, 1.0f, peak), 0.5f);
if (displayPeak > 0.01f)
{
float peakY = bar.getBottom() - bar.getHeight() * displayPeak;
g.setColour (juce::Colours::white.withAlpha (0.8f));
g.fillRect (bar.getX(), peakY, bar.getWidth(), 1.5f);
}
}
};

201
Source/WaveformDisplay.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,201 @@
#include "WaveformDisplay.h"
#include "LookAndFeel.h"
WaveformDisplay::WaveformDisplay() {}
void WaveformDisplay::setBuffer (const juce::AudioBuffer<float>* buffer)
{
audioBuffer = buffer;
if (buffer != nullptr)
{
totalSourceSamples = buffer->getNumSamples();
lastBufferSize = totalSourceSamples;
}
else
{
totalSourceSamples = 0;
lastBufferSize = 0;
}
pathDirty = true;
repaint();
}
void WaveformDisplay::setActiveGrains (const std::vector<GrainEngine::ActiveGrainInfo>& grains)
{
activeGrains = grains;
repaint();
}
void WaveformDisplay::resized()
{
pathDirty = true;
}
void WaveformDisplay::rebuildWaveformPath (juce::Rectangle<float> bounds)
{
cachedWaveformPath.clear();
if (audioBuffer == nullptr || audioBuffer->getNumSamples() == 0)
return;
const int numSamples = audioBuffer->getNumSamples();
const float width = bounds.getWidth();
const float height = bounds.getHeight();
const float midY = bounds.getCentreY();
const float* data = audioBuffer->getReadPointer (0);
// Build filled waveform path
int blockSize = std::max (1, numSamples / (int) width);
// Top line (max values)
cachedWaveformPath.startNewSubPath (bounds.getX(), midY);
for (int x = 0; x < (int) width; ++x)
{
int sampleIndex = (int) ((float) x / width * (float) numSamples);
sampleIndex = juce::jlimit (0, numSamples - 1, sampleIndex);
float maxVal = -1.0f;
for (int j = 0; j < blockSize && (sampleIndex + j) < numSamples; ++j)
maxVal = std::max (maxVal, data[sampleIndex + j]);
float topY = midY - maxVal * (height * 0.45f);
cachedWaveformPath.lineTo ((float) x + bounds.getX(), topY);
}
// Bottom line (min values, reversed)
for (int x = (int) width - 1; x >= 0; --x)
{
int sampleIndex = (int) ((float) x / width * (float) numSamples);
sampleIndex = juce::jlimit (0, numSamples - 1, sampleIndex);
float minVal = 1.0f;
for (int j = 0; j < blockSize && (sampleIndex + j) < numSamples; ++j)
minVal = std::min (minVal, data[sampleIndex + j]);
float botY = midY - minVal * (height * 0.45f);
cachedWaveformPath.lineTo ((float) x + bounds.getX(), botY);
}
cachedWaveformPath.closeSubPath();
lastWidth = getWidth();
lastHeight = getHeight();
pathDirty = false;
}
void WaveformDisplay::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
// Background gradient
{
juce::ColourGradient bgGrad (InstaGrainLookAndFeel::bgDark.darker (0.4f), 0, bounds.getY(),
InstaGrainLookAndFeel::bgDark.darker (0.2f), 0, bounds.getBottom(), false);
g.setGradientFill (bgGrad);
g.fillRoundedRectangle (bounds, 4.0f);
}
// Border
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
// Grid lines
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.12f));
for (int i = 1; i < 8; ++i)
{
float xLine = bounds.getX() + bounds.getWidth() * (float) i / 8.0f;
g.drawVerticalLine ((int) xLine, bounds.getY(), bounds.getBottom());
}
// Center line
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.2f));
float midY = bounds.getCentreY();
g.drawHorizontalLine ((int) midY, bounds.getX(), bounds.getRight());
if (audioBuffer == nullptr || audioBuffer->getNumSamples() == 0)
{
// "Drop sample here" text
g.setColour (InstaGrainLookAndFeel::textSecondary);
g.setFont (16.0f);
g.drawText ("Drop a sample here or click Load Sample", bounds, juce::Justification::centred);
return;
}
// Rebuild path if needed
if (pathDirty || lastWidth != getWidth() || lastHeight != getHeight()
|| lastBufferSize != audioBuffer->getNumSamples())
{
lastBufferSize = audioBuffer->getNumSamples();
rebuildWaveformPath (bounds);
}
// Scatter range highlight
if (scatterRange > 0.001f)
{
float posX = bounds.getX() + grainPosition * bounds.getWidth();
float rangeW = scatterRange * bounds.getWidth();
float left = std::max (bounds.getX(), posX - rangeW);
float right = std::min (bounds.getRight(), posX + rangeW);
g.setColour (juce::Colour (0x1800ff88));
g.fillRect (left, bounds.getY(), right - left, bounds.getHeight());
}
// Waveform fill + stroke
juce::Colour waveColour (0xffff8844);
g.setColour (waveColour.withAlpha (0.5f));
g.fillPath (cachedWaveformPath);
g.setColour (waveColour.withAlpha (0.9f));
g.strokePath (cachedWaveformPath, juce::PathStrokeType (1.0f));
// Active grain rectangles
if (totalSourceSamples > 0)
{
for (const auto& grain : activeGrains)
{
if (grain.startSample < 0) continue;
float gx = bounds.getX() + ((float) grain.startSample / (float) totalSourceSamples) * bounds.getWidth();
float gw = ((float) grain.lengthSamples / (float) totalSourceSamples) * bounds.getWidth();
gw = std::max (2.0f, gw);
float alpha = 0.6f * (1.0f - grain.progress); // fade as grain progresses
g.setColour (InstaGrainLookAndFeel::accent.withAlpha (alpha));
g.fillRect (gx, bounds.getY() + 2, gw, bounds.getHeight() - 4);
}
}
// Position indicator line
{
float posX = bounds.getX() + grainPosition * bounds.getWidth();
g.setColour (InstaGrainLookAndFeel::accent);
g.drawVerticalLine ((int) posX, bounds.getY(), bounds.getBottom());
// Glow
g.setColour (InstaGrainLookAndFeel::accent.withAlpha (0.3f));
g.fillRect (posX - 2.0f, bounds.getY(), 5.0f, bounds.getHeight());
}
}
void WaveformDisplay::updatePositionFromMouse (const juce::MouseEvent& e)
{
auto bounds = getLocalBounds().toFloat();
float pos = (float) (e.x - bounds.getX()) / bounds.getWidth();
pos = juce::jlimit (0.0f, 1.0f, pos);
grainPosition = pos;
if (onPositionChanged)
onPositionChanged (pos);
repaint();
}
void WaveformDisplay::mouseDown (const juce::MouseEvent& e)
{
if (audioBuffer != nullptr && audioBuffer->getNumSamples() > 0)
updatePositionFromMouse (e);
}
void WaveformDisplay::mouseDrag (const juce::MouseEvent& e)
{
if (audioBuffer != nullptr && audioBuffer->getNumSamples() > 0)
updatePositionFromMouse (e);
}

38
Source/WaveformDisplay.h Normal file
Fájl megtekintése

@@ -0,0 +1,38 @@
#pragma once
#include <JuceHeader.h>
#include "GrainEngine.h"
class WaveformDisplay : public juce::Component
{
public:
WaveformDisplay();
void setBuffer (const juce::AudioBuffer<float>* buffer);
void setGrainPosition (float pos) { grainPosition = pos; repaint(); }
void setScatterRange (float range) { scatterRange = range; repaint(); }
void setActiveGrains (const std::vector<GrainEngine::ActiveGrainInfo>& grains);
std::function<void (float)> onPositionChanged;
void paint (juce::Graphics& g) override;
void resized() override;
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
private:
const juce::AudioBuffer<float>* audioBuffer = nullptr;
juce::Path cachedWaveformPath;
bool pathDirty = true;
int lastWidth = 0, lastHeight = 0;
int lastBufferSize = 0;
float grainPosition = 0.5f;
float scatterRange = 0.0f;
std::vector<GrainEngine::ActiveGrainInfo> activeGrains;
int totalSourceSamples = 0;
void rebuildWaveformPath (juce::Rectangle<float> bounds);
void updatePositionFromMouse (const juce::MouseEvent& e);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveformDisplay)
};