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

130
.github/workflows/build.yml vendored Normal file
Fájl megtekintése

@@ -0,0 +1,130 @@
name: Build InstaGrain
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Clone JUCE
run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE
- name: Configure CMake
run: cmake -B build -G "Visual Studio 17 2022" -A x64
- name: Build Release
run: cmake --build build --config Release
- name: Package VST3
run: Compress-Archive -Path "build/InstaGrain_artefacts/Release/VST3/InstaGrain.vst3" -DestinationPath "InstaGrain-VST3-Win64.zip"
- name: Upload VST3
uses: actions/upload-artifact@v4
with:
name: InstaGrain-VST3-Win64
path: InstaGrain-VST3-Win64.zip
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Clone JUCE
run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE
- name: Configure CMake (Universal Binary)
run: cmake -B build -G Xcode -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
- name: Build Release
run: cmake --build build --config Release
- name: Package VST3
working-directory: build/InstaGrain_artefacts/Release
run: zip -r $GITHUB_WORKSPACE/InstaGrain-VST3-macOS.zip VST3/InstaGrain.vst3
- name: Package AU
working-directory: build/InstaGrain_artefacts/Release
run: zip -r $GITHUB_WORKSPACE/InstaGrain-AU-macOS.zip AU/InstaGrain.component
- name: Upload VST3
uses: actions/upload-artifact@v4
with:
name: InstaGrain-VST3-macOS
path: InstaGrain-VST3-macOS.zip
- name: Upload AU
uses: actions/upload-artifact@v4
with:
name: InstaGrain-AU-macOS
path: InstaGrain-AU-macOS.zip
build-linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake git libasound2-dev \
libfreetype6-dev libx11-dev libxrandr-dev libxcursor-dev \
libxinerama-dev libwebkit2gtk-4.1-dev libcurl4-openssl-dev
- name: Clone JUCE
run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build Release
run: cmake --build build --config Release --parallel $(nproc)
- name: Package VST3
working-directory: build/InstaGrain_artefacts/Release
run: zip -r $GITHUB_WORKSPACE/InstaGrain-VST3-Linux-x64.zip VST3/InstaGrain.vst3
- name: Package LV2
working-directory: build/InstaGrain_artefacts/Release
run: zip -r $GITHUB_WORKSPACE/InstaGrain-LV2-Linux-x64.zip LV2/InstaGrain.lv2
- name: Upload VST3
uses: actions/upload-artifact@v4
with:
name: InstaGrain-VST3-Linux-x64
path: InstaGrain-VST3-Linux-x64.zip
- name: Upload LV2
uses: actions/upload-artifact@v4
with:
name: InstaGrain-LV2-Linux-x64
path: InstaGrain-LV2-Linux-x64.zip
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-windows, build-macos, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/InstaGrain-VST3-Win64/InstaGrain-VST3-Win64.zip
artifacts/InstaGrain-VST3-macOS/InstaGrain-VST3-macOS.zip
artifacts/InstaGrain-AU-macOS/InstaGrain-AU-macOS.zip
artifacts/InstaGrain-VST3-Linux-x64/InstaGrain-VST3-Linux-x64.zip
artifacts/InstaGrain-LV2-Linux-x64/InstaGrain-LV2-Linux-x64.zip
generate_release_notes: true

1
.gitignore vendored Normal file
Fájl megtekintése

@@ -0,0 +1 @@
build/

65
CMakeLists.txt Normal file
Fájl megtekintése

@@ -0,0 +1,65 @@
cmake_minimum_required(VERSION 3.22)
project(InstaGrain VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../JUCE ${CMAKE_CURRENT_BINARY_DIR}/JUCE)
juce_add_plugin(InstaGrain
COMPANY_NAME "InstaGrain"
IS_SYNTH TRUE
NEEDS_MIDI_INPUT TRUE
NEEDS_MIDI_OUTPUT FALSE
PLUGIN_MANUFACTURER_CODE Inst
PLUGIN_CODE Igrn
FORMATS VST3 AU LV2
LV2URI "https://github.com/hariel1985/InstaGrain"
PRODUCT_NAME "InstaGrain"
COPY_PLUGIN_AFTER_BUILD FALSE
)
juce_generate_juce_header(InstaGrain)
juce_add_binary_data(InstaGrainData SOURCES
Resources/Rajdhani-Regular.ttf
Resources/Rajdhani-Medium.ttf
Resources/Rajdhani-Bold.ttf
)
target_sources(InstaGrain
PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/LookAndFeel.cpp
Source/GrainCloud.cpp
Source/GrainVoice.cpp
Source/GrainEngine.cpp
Source/WaveformDisplay.cpp
Source/GrainControlPanel.cpp
Source/ScatterPanel.cpp
Source/EnvelopePanel.cpp
Source/EffectsPanel.cpp
Source/MasterPanel.cpp
)
target_compile_definitions(InstaGrain
PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
)
target_link_libraries(InstaGrain
PRIVATE
InstaGrainData
juce::juce_audio_basics
juce::juce_audio_devices
juce::juce_audio_formats
juce::juce_audio_processors
juce::juce_audio_utils
juce::juce_dsp
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_warning_flags
)

22
LICENSE Normal file
Fájl megtekintése

@@ -0,0 +1,22 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>

34
README.md Normal file
Fájl megtekintése

@@ -0,0 +1,34 @@
# InstaGrain
Granular synthesizer plugin (VST3/AU/LV2) built with JUCE.
Loads audio samples and splits them into tiny grains, manipulating position, size, density, pitch, and pan to create evolving textures and entirely new sounds. MIDI-controlled, polyphonic (8 voices), with drag & drop sample loading.
## Features
- **Granular Engine** — 128 grain pool per voice, Hann windowing, linear interpolation
- **8-voice polyphony** — MIDI note maps to pitch offset relative to configurable root note
- **Root Note selector** — set which MIDI note the loaded sample represents
- **Scatter controls** — randomize position, size, pitch, and pan per grain
- **Direction modes** — Forward, Reverse, PingPong
- **Freeze** — lock grain position for continuous texture generation
- **ADSR envelope** — per-voice amplitude shaping
- **Global effects** — State Variable filter (LP/HP/BP) + Reverb
- **Sustain pedal** — full MIDI CC64 support with proper voice management
- **Sample rate correction** — automatic pitch compensation for sample rate mismatches
- **Waveform display** — real-time grain visualization with position indicator and scatter range
- **Drag & drop** — load WAV, AIFF, MP3, FLAC, OGG files
- **State save/restore** — sample path and all parameters persist with DAW session
## Build
Requires [JUCE](https://github.com/juce-framework/JUCE) cloned at `../JUCE` relative to this project.
```bash
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
```
## License
GPL-3.0

BINáris
Resources/Rajdhani-Bold.ttf Normal file

Binary file not shown.

BINáris
Resources/Rajdhani-Medium.ttf Normal file

Binary file not shown.

BINáris
Resources/Rajdhani-Regular.ttf Normal file

Binary file not shown.

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