Initial commit: InstaDrums VST3 drum sampler plugin
- 12-pad drum sampler with 4x3 grid (expandable by 4) - Velocity layers with round-robin (Salamander-style filename parsing) - Rhythm Engine-style GUI: pad grid (left), sample editor (right top), FX panel (right bottom), master panel (bottom) - Waveform thumbnails on pads + large waveform in sample editor - ADSR envelope, pitch, pan per pad - Drag & drop sample/folder loading - Kit save/load (.drumkit XML presets) - Load Folder with smart name matching (kick, snare, hihat, etc.) - Choke groups, one-shot/polyphonic mode - Dark modern LookAndFeel with neon accent colors - Built with JUCE framework, CMake, MSVC 2022 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
build/
|
||||
Samples/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
CMakeSettings.json
|
||||
out/
|
||||
58
CMakeLists.txt
Normal file
58
CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(InstaDrums 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(InstaDrums
|
||||
COMPANY_NAME "InstaDrums"
|
||||
IS_SYNTH TRUE
|
||||
NEEDS_MIDI_INPUT TRUE
|
||||
NEEDS_MIDI_OUTPUT TRUE
|
||||
PLUGIN_MANUFACTURER_CODE Inst
|
||||
PLUGIN_CODE Idrm
|
||||
FORMATS VST3 Standalone
|
||||
PRODUCT_NAME "InstaDrums"
|
||||
COPY_PLUGIN_AFTER_BUILD FALSE
|
||||
)
|
||||
|
||||
juce_generate_juce_header(InstaDrums)
|
||||
|
||||
target_sources(InstaDrums
|
||||
PRIVATE
|
||||
Source/PluginProcessor.cpp
|
||||
Source/PluginEditor.cpp
|
||||
Source/DrumPad.cpp
|
||||
Source/PadComponent.cpp
|
||||
Source/LookAndFeel.cpp
|
||||
Source/WaveformDisplay.cpp
|
||||
Source/SampleEditorPanel.cpp
|
||||
Source/FxPanel.cpp
|
||||
Source/MasterPanel.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(InstaDrums
|
||||
PUBLIC
|
||||
JUCE_WEB_BROWSER=0
|
||||
JUCE_USE_CURL=0
|
||||
JUCE_VST3_CAN_REPLACE_VST2=0
|
||||
)
|
||||
|
||||
target_link_libraries(InstaDrums
|
||||
PRIVATE
|
||||
juce::juce_audio_basics
|
||||
juce::juce_audio_devices
|
||||
juce::juce_audio_formats
|
||||
juce::juce_audio_processors
|
||||
juce::juce_audio_utils
|
||||
juce::juce_core
|
||||
juce::juce_dsp
|
||||
juce::juce_graphics
|
||||
juce::juce_gui_basics
|
||||
juce::juce_gui_extra
|
||||
PUBLIC
|
||||
juce::juce_recommended_config_flags
|
||||
juce::juce_recommended_warning_flags
|
||||
)
|
||||
385
Source/DrumPad.cpp
Normal file
385
Source/DrumPad.cpp
Normal file
@@ -0,0 +1,385 @@
|
||||
#include "DrumPad.h"
|
||||
|
||||
DrumPad::DrumPad() {}
|
||||
DrumPad::~DrumPad() {}
|
||||
|
||||
void DrumPad::prepareToPlay (double sr, int /*samplesPerBlock*/)
|
||||
{
|
||||
sampleRate = sr;
|
||||
}
|
||||
|
||||
void DrumPad::releaseResources()
|
||||
{
|
||||
playing = false;
|
||||
activeSample = nullptr;
|
||||
envStage = EnvelopeStage::Idle;
|
||||
envLevel = 0.0f;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Velocity tag parsing from Salamander-style filenames
|
||||
// Tags: Ghost, PP, P, MP, F, FF
|
||||
// ============================================================
|
||||
|
||||
float DrumPad::velocityTagToLow (const juce::String& tag)
|
||||
{
|
||||
if (tag == "Ghost") return 0.0f;
|
||||
if (tag == "PP") return 0.05f;
|
||||
if (tag == "P") return 0.15f;
|
||||
if (tag == "MP") return 0.35f;
|
||||
if (tag == "F") return 0.55f;
|
||||
if (tag == "FF") return 0.80f;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float DrumPad::velocityTagToHigh (const juce::String& tag)
|
||||
{
|
||||
if (tag == "Ghost") return 0.05f;
|
||||
if (tag == "PP") return 0.15f;
|
||||
if (tag == "P") return 0.35f;
|
||||
if (tag == "MP") return 0.55f;
|
||||
if (tag == "F") return 0.80f;
|
||||
if (tag == "FF") return 1.0f;
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Single sample loading (one layer, full velocity range)
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::loadSample (const juce::File& file, juce::AudioFormatManager& formatManager)
|
||||
{
|
||||
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
|
||||
if (reader == nullptr) return;
|
||||
|
||||
layers.clear();
|
||||
activeSample = nullptr;
|
||||
|
||||
auto* layer = new VelocityLayer();
|
||||
layer->velocityLow = 0.0f;
|
||||
layer->velocityHigh = 1.0f;
|
||||
|
||||
auto* sample = new Sample();
|
||||
sample->buffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples);
|
||||
reader->read (&sample->buffer, 0, (int) reader->lengthInSamples, 0, true, true);
|
||||
sample->sampleRate = reader->sampleRate;
|
||||
sample->file = file;
|
||||
|
||||
layer->samples.add (sample);
|
||||
layers.add (layer);
|
||||
|
||||
loadedFileName = file.getFileName();
|
||||
loadedFile = file;
|
||||
readPosition = 0.0;
|
||||
playing = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Velocity layer loading from folder
|
||||
// Expects filenames like: snare_OH_FF_1.flac, snare_OH_Ghost_3.flac
|
||||
// Groups by velocity tag, each group becomes round-robin variations
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::loadLayersFromFolder (const juce::File& folder, juce::AudioFormatManager& formatManager)
|
||||
{
|
||||
if (! folder.isDirectory()) return;
|
||||
|
||||
layers.clear();
|
||||
activeSample = nullptr;
|
||||
|
||||
// Collect audio files
|
||||
juce::Array<juce::File> audioFiles;
|
||||
for (auto& f : folder.findChildFiles (juce::File::findFiles, false))
|
||||
{
|
||||
auto ext = f.getFileExtension().toLowerCase();
|
||||
if (ext == ".wav" || ext == ".aiff" || ext == ".aif" || ext == ".flac"
|
||||
|| ext == ".ogg" || ext == ".mp3")
|
||||
audioFiles.add (f);
|
||||
}
|
||||
|
||||
if (audioFiles.isEmpty()) return;
|
||||
|
||||
// Known velocity tags to look for in filenames
|
||||
static const juce::StringArray velocityTags = { "Ghost", "PP", "P", "MP", "F", "FF" };
|
||||
|
||||
// Group files by velocity tag
|
||||
std::map<juce::String, juce::Array<juce::File>> groups;
|
||||
|
||||
for (auto& file : audioFiles)
|
||||
{
|
||||
auto nameNoExt = file.getFileNameWithoutExtension();
|
||||
// Split by underscore and look for velocity tags
|
||||
juce::String foundTag = "FF"; // default if no tag found
|
||||
|
||||
auto parts = juce::StringArray::fromTokens (nameNoExt, "_", "");
|
||||
for (auto& part : parts)
|
||||
{
|
||||
if (velocityTags.contains (part))
|
||||
{
|
||||
foundTag = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
groups[foundTag].add (file);
|
||||
}
|
||||
|
||||
// If only one group found with no velocity differentiation, treat as single layer
|
||||
if (groups.size() == 1 && groups.begin()->first == "FF")
|
||||
{
|
||||
// All files are round-robin for a single full-velocity layer
|
||||
auto* layer = new VelocityLayer();
|
||||
layer->velocityLow = 0.0f;
|
||||
layer->velocityHigh = 1.0f;
|
||||
|
||||
for (auto& file : groups.begin()->second)
|
||||
{
|
||||
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
|
||||
if (reader != nullptr)
|
||||
{
|
||||
auto* sample = new Sample();
|
||||
sample->buffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples);
|
||||
reader->read (&sample->buffer, 0, (int) reader->lengthInSamples, 0, true, true);
|
||||
sample->sampleRate = reader->sampleRate;
|
||||
sample->file = file;
|
||||
layer->samples.add (sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (! layer->samples.isEmpty())
|
||||
layers.add (layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple velocity groups — create one layer per group
|
||||
for (auto& [tag, files] : groups)
|
||||
{
|
||||
auto* layer = new VelocityLayer();
|
||||
layer->velocityLow = velocityTagToLow (tag);
|
||||
layer->velocityHigh = velocityTagToHigh (tag);
|
||||
|
||||
files.sort();
|
||||
for (auto& file : files)
|
||||
{
|
||||
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
|
||||
if (reader != nullptr)
|
||||
{
|
||||
auto* sample = new Sample();
|
||||
sample->buffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples);
|
||||
reader->read (&sample->buffer, 0, (int) reader->lengthInSamples, 0, true, true);
|
||||
sample->sampleRate = reader->sampleRate;
|
||||
sample->file = file;
|
||||
layer->samples.add (sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (! layer->samples.isEmpty())
|
||||
layers.add (layer);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort layers by velocity range
|
||||
std::sort (layers.begin(), layers.end(),
|
||||
[] (const VelocityLayer* a, const VelocityLayer* b)
|
||||
{ return a->velocityLow < b->velocityLow; });
|
||||
|
||||
loadedFileName = folder.getFileName() + " (" + juce::String (layers.size()) + " layers)";
|
||||
loadedFile = folder;
|
||||
readPosition = 0.0;
|
||||
playing = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// State queries
|
||||
// ============================================================
|
||||
|
||||
bool DrumPad::hasSample() const
|
||||
{
|
||||
for (auto* layer : layers)
|
||||
if (! layer->samples.isEmpty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const juce::AudioBuffer<float>& DrumPad::getSampleBuffer() const
|
||||
{
|
||||
if (activeSample != nullptr)
|
||||
return activeSample->buffer;
|
||||
|
||||
// Return first available sample buffer for waveform display
|
||||
for (auto* layer : layers)
|
||||
if (! layer->samples.isEmpty())
|
||||
return layer->samples[0]->buffer;
|
||||
|
||||
return emptyBuffer;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Velocity layer selection
|
||||
// ============================================================
|
||||
|
||||
DrumPad::VelocityLayer* DrumPad::findLayerForVelocity (float velocity)
|
||||
{
|
||||
// Find the layer whose range contains this velocity
|
||||
for (auto* layer : layers)
|
||||
if (velocity >= layer->velocityLow && velocity <= layer->velocityHigh)
|
||||
return layer;
|
||||
|
||||
// Fallback: closest layer
|
||||
VelocityLayer* closest = nullptr;
|
||||
float minDist = 2.0f;
|
||||
for (auto* layer : layers)
|
||||
{
|
||||
float mid = (layer->velocityLow + layer->velocityHigh) * 0.5f;
|
||||
float dist = std::abs (velocity - mid);
|
||||
if (dist < minDist)
|
||||
{
|
||||
minDist = dist;
|
||||
closest = layer;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Trigger / Stop
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::trigger (float velocity)
|
||||
{
|
||||
if (! hasSample()) return;
|
||||
|
||||
auto* layer = findLayerForVelocity (velocity);
|
||||
if (layer == nullptr) return;
|
||||
|
||||
activeSample = layer->getNextSample();
|
||||
if (activeSample == nullptr) return;
|
||||
|
||||
currentVelocity = velocity;
|
||||
readPosition = 0.0;
|
||||
envStage = EnvelopeStage::Attack;
|
||||
envLevel = 0.0f;
|
||||
playing = true;
|
||||
}
|
||||
|
||||
void DrumPad::stop()
|
||||
{
|
||||
if (playing)
|
||||
envStage = EnvelopeStage::Release;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ADSR Envelope
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::advanceEnvelope()
|
||||
{
|
||||
float attackSamples = std::max (1.0f, attack * (float) sampleRate);
|
||||
float decaySamples = std::max (1.0f, decay * (float) sampleRate);
|
||||
float releaseSamples = std::max (1.0f, release * (float) sampleRate);
|
||||
|
||||
switch (envStage)
|
||||
{
|
||||
case EnvelopeStage::Attack:
|
||||
envLevel += 1.0f / attackSamples;
|
||||
if (envLevel >= 1.0f)
|
||||
{
|
||||
envLevel = 1.0f;
|
||||
envStage = EnvelopeStage::Decay;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvelopeStage::Decay:
|
||||
envLevel -= (1.0f - sustain) / decaySamples;
|
||||
if (envLevel <= sustain)
|
||||
{
|
||||
envLevel = sustain;
|
||||
envStage = EnvelopeStage::Sustain;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvelopeStage::Sustain:
|
||||
envLevel = sustain;
|
||||
break;
|
||||
|
||||
case EnvelopeStage::Release:
|
||||
envLevel -= envLevel / releaseSamples;
|
||||
if (envLevel < 0.001f)
|
||||
{
|
||||
envLevel = 0.0f;
|
||||
envStage = EnvelopeStage::Idle;
|
||||
playing = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvelopeStage::Idle:
|
||||
envLevel = 0.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Audio rendering
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
|
||||
{
|
||||
if (! playing || activeSample == nullptr)
|
||||
return;
|
||||
|
||||
const auto& sampleBuffer = activeSample->buffer;
|
||||
const int sampleLength = sampleBuffer.getNumSamples();
|
||||
const int srcChannels = sampleBuffer.getNumChannels();
|
||||
const double sourceSR = activeSample->sampleRate;
|
||||
|
||||
double pitchRatio = std::pow (2.0, (double) pitch / 12.0) * (sourceSR / sampleRate);
|
||||
|
||||
// Constant power pan law
|
||||
float panPos = (pan + 1.0f) * 0.5f;
|
||||
float leftGain = std::cos (panPos * juce::MathConstants<float>::halfPi);
|
||||
float rightGain = std::sin (panPos * juce::MathConstants<float>::halfPi);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (! playing) break;
|
||||
|
||||
int pos0 = (int) readPosition;
|
||||
if (pos0 >= sampleLength)
|
||||
{
|
||||
if (oneShot)
|
||||
{
|
||||
playing = false;
|
||||
envStage = EnvelopeStage::Idle;
|
||||
envLevel = 0.0f;
|
||||
activeSample = nullptr;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
envStage = EnvelopeStage::Release;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos0 < sampleLength)
|
||||
{
|
||||
advanceEnvelope();
|
||||
float gain = volume * currentVelocity * envLevel;
|
||||
|
||||
int pos1 = std::min (pos0 + 1, sampleLength - 1);
|
||||
float frac = (float) (readPosition - (double) pos0);
|
||||
|
||||
for (int ch = 0; ch < outputBuffer.getNumChannels(); ++ch)
|
||||
{
|
||||
int srcCh = std::min (ch, srcChannels - 1);
|
||||
float s0 = sampleBuffer.getSample (srcCh, pos0);
|
||||
float s1 = sampleBuffer.getSample (srcCh, pos1);
|
||||
float sampleVal = s0 + frac * (s1 - s0);
|
||||
|
||||
float channelGain = (ch == 0) ? leftGain : rightGain;
|
||||
outputBuffer.addSample (ch, startSample + i, sampleVal * gain * channelGain);
|
||||
}
|
||||
}
|
||||
|
||||
readPosition += pitchRatio;
|
||||
}
|
||||
}
|
||||
106
Source/DrumPad.h
Normal file
106
Source/DrumPad.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class DrumPad
|
||||
{
|
||||
public:
|
||||
// A single sample with its audio data and source sample rate
|
||||
struct Sample
|
||||
{
|
||||
juce::AudioBuffer<float> buffer;
|
||||
double sampleRate = 44100.0;
|
||||
juce::File file;
|
||||
};
|
||||
|
||||
// A velocity layer: velocity range + multiple round-robin samples
|
||||
struct VelocityLayer
|
||||
{
|
||||
float velocityLow = 0.0f; // 0.0 - 1.0
|
||||
float velocityHigh = 1.0f;
|
||||
juce::OwnedArray<Sample> samples; // round-robin variations
|
||||
int nextRoundRobin = 0;
|
||||
|
||||
Sample* getNextSample()
|
||||
{
|
||||
if (samples.isEmpty()) return nullptr;
|
||||
auto* s = samples[nextRoundRobin % samples.size()];
|
||||
nextRoundRobin = (nextRoundRobin + 1) % samples.size();
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
DrumPad();
|
||||
~DrumPad();
|
||||
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock);
|
||||
void releaseResources();
|
||||
|
||||
// Single sample loading (backwards compatible)
|
||||
void loadSample (const juce::File& file, juce::AudioFormatManager& formatManager);
|
||||
|
||||
// Velocity layer loading from a folder
|
||||
void loadLayersFromFolder (const juce::File& folder, juce::AudioFormatManager& formatManager);
|
||||
|
||||
bool hasSample() const;
|
||||
|
||||
void trigger (float velocity = 1.0f);
|
||||
void stop();
|
||||
|
||||
void renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples);
|
||||
|
||||
// Pad properties
|
||||
juce::String name;
|
||||
int midiNote = 36;
|
||||
float volume = 1.0f;
|
||||
float pan = 0.0f;
|
||||
float pitch = 0.0f;
|
||||
bool oneShot = true;
|
||||
int chokeGroup = -1;
|
||||
juce::Colour colour { 0xff00ff88 };
|
||||
|
||||
// ADSR
|
||||
float attack = 0.001f;
|
||||
float decay = 0.1f;
|
||||
float sustain = 1.0f;
|
||||
float release = 0.05f;
|
||||
|
||||
// State
|
||||
bool isPlaying() const { return playing; }
|
||||
juce::String getLoadedFileName() const { return loadedFileName; }
|
||||
juce::File getLoadedFile() const { return loadedFile; }
|
||||
int getNumLayers() const { return layers.size(); }
|
||||
|
||||
const juce::AudioBuffer<float>& getSampleBuffer() const;
|
||||
|
||||
private:
|
||||
// Velocity layers (sorted by velocity range)
|
||||
juce::OwnedArray<VelocityLayer> layers;
|
||||
|
||||
// Currently playing sample reference
|
||||
Sample* activeSample = nullptr;
|
||||
|
||||
// Fallback empty buffer for getSampleBuffer when nothing loaded
|
||||
juce::AudioBuffer<float> emptyBuffer;
|
||||
|
||||
double sampleRate = 44100.0;
|
||||
double readPosition = 0.0;
|
||||
bool playing = false;
|
||||
float currentVelocity = 1.0f;
|
||||
|
||||
// ADSR state
|
||||
enum class EnvelopeStage { Idle, Attack, Decay, Sustain, Release };
|
||||
EnvelopeStage envStage = EnvelopeStage::Idle;
|
||||
float envLevel = 0.0f;
|
||||
|
||||
juce::String loadedFileName;
|
||||
juce::File loadedFile;
|
||||
|
||||
void advanceEnvelope();
|
||||
VelocityLayer* findLayerForVelocity (float velocity);
|
||||
|
||||
// Parse velocity tag from filename (e.g. "snare_OH_FF_1" -> FF)
|
||||
static float velocityTagToLow (const juce::String& tag);
|
||||
static float velocityTagToHigh (const juce::String& tag);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DrumPad)
|
||||
};
|
||||
116
Source/FxPanel.cpp
Normal file
116
Source/FxPanel.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "FxPanel.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
FxPanel::FxPanel()
|
||||
{
|
||||
setupTitle (compTitle, "COMPRESSOR");
|
||||
setupTitle (eqTitle, "EQ");
|
||||
setupTitle (distTitle, "DISTORTION");
|
||||
setupTitle (reverbTitle, "REVERB");
|
||||
|
||||
setupKnob (compThreshSlider, compThreshLabel, "Threshold", -60.0, 0.0, -12.0, 0.5);
|
||||
setupKnob (compRatioSlider, compRatioLabel, "Ratio", 1.0, 20.0, 4.0, 0.1);
|
||||
setupKnob (eqLoSlider, eqLoLabel, "Lo", -12.0, 12.0, 0.0, 0.1);
|
||||
setupKnob (eqMidSlider, eqMidLabel, "Mid", -12.0, 12.0, 0.0, 0.1);
|
||||
setupKnob (eqHiSlider, eqHiLabel, "Hi", -12.0, 12.0, 0.0, 0.1);
|
||||
setupKnob (distDriveSlider, distDriveLabel, "Drive", 0.0, 1.0, 0.0, 0.01);
|
||||
setupKnob (distMixSlider, distMixLabel, "Mix", 0.0, 1.0, 0.0, 0.01);
|
||||
setupKnob (reverbSizeSlider, reverbSizeLabel, "Size", 0.0, 1.0, 0.3, 0.01);
|
||||
setupKnob (reverbDecaySlider, reverbDecayLabel, "Decay", 0.0, 1.0, 0.5, 0.01);
|
||||
}
|
||||
|
||||
void FxPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step)
|
||||
{
|
||||
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
s.setRange (min, max, step);
|
||||
s.setValue (val, juce::dontSendNotification);
|
||||
addAndMakeVisible (s);
|
||||
|
||||
l.setText (name, juce::dontSendNotification);
|
||||
l.setFont (juce::FontOptions (9.0f));
|
||||
l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
l.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (l);
|
||||
}
|
||||
|
||||
void FxPanel::setupTitle (juce::Label& l, const juce::String& text)
|
||||
{
|
||||
l.setText (text, juce::dontSendNotification);
|
||||
l.setFont (juce::FontOptions (10.0f, juce::Font::bold));
|
||||
l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent);
|
||||
l.setJustificationType (juce::Justification::centredLeft);
|
||||
addAndMakeVisible (l);
|
||||
}
|
||||
|
||||
void FxPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaDrumsLookAndFeel::bgMedium);
|
||||
g.fillRoundedRectangle (bounds, 6.0f);
|
||||
g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.5f));
|
||||
g.drawRoundedRectangle (bounds, 6.0f, 1.0f);
|
||||
|
||||
// "FX" header
|
||||
g.setColour (InstaDrumsLookAndFeel::textSecondary);
|
||||
g.setFont (juce::FontOptions (14.0f, juce::Font::bold));
|
||||
g.drawText ("FX", bounds.reduced (6, 4).removeFromTop (18), juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
void FxPanel::resized()
|
||||
{
|
||||
auto area = getLocalBounds().reduced (6);
|
||||
area.removeFromTop (20); // FX header
|
||||
|
||||
int halfW = area.getWidth() / 2;
|
||||
int rowH = area.getHeight() / 2;
|
||||
|
||||
// Top row: Compressor | EQ
|
||||
auto topRow = area.removeFromTop (rowH);
|
||||
{
|
||||
auto compArea = topRow.removeFromLeft (halfW).reduced (2);
|
||||
compTitle.setBounds (compArea.removeFromTop (14));
|
||||
int kw = compArea.getWidth() / 2;
|
||||
auto c1 = compArea.removeFromLeft (kw);
|
||||
compThreshLabel.setBounds (c1.removeFromBottom (12));
|
||||
compThreshSlider.setBounds (c1);
|
||||
compRatioLabel.setBounds (compArea.removeFromBottom (12));
|
||||
compRatioSlider.setBounds (compArea);
|
||||
}
|
||||
{
|
||||
auto eqArea = topRow.reduced (2);
|
||||
eqTitle.setBounds (eqArea.removeFromTop (14));
|
||||
int kw = eqArea.getWidth() / 3;
|
||||
auto c1 = eqArea.removeFromLeft (kw);
|
||||
eqLoLabel.setBounds (c1.removeFromBottom (12));
|
||||
eqLoSlider.setBounds (c1);
|
||||
auto c2 = eqArea.removeFromLeft (kw);
|
||||
eqMidLabel.setBounds (c2.removeFromBottom (12));
|
||||
eqMidSlider.setBounds (c2);
|
||||
eqHiLabel.setBounds (eqArea.removeFromBottom (12));
|
||||
eqHiSlider.setBounds (eqArea);
|
||||
}
|
||||
|
||||
// Bottom row: Distortion | Reverb
|
||||
{
|
||||
auto distArea = area.removeFromLeft (halfW).reduced (2);
|
||||
distTitle.setBounds (distArea.removeFromTop (14));
|
||||
int kw = distArea.getWidth() / 2;
|
||||
auto c1 = distArea.removeFromLeft (kw);
|
||||
distDriveLabel.setBounds (c1.removeFromBottom (12));
|
||||
distDriveSlider.setBounds (c1);
|
||||
distMixLabel.setBounds (distArea.removeFromBottom (12));
|
||||
distMixSlider.setBounds (distArea);
|
||||
}
|
||||
{
|
||||
auto revArea = area.reduced (2);
|
||||
reverbTitle.setBounds (revArea.removeFromTop (14));
|
||||
int kw = revArea.getWidth() / 2;
|
||||
auto c1 = revArea.removeFromLeft (kw);
|
||||
reverbSizeLabel.setBounds (c1.removeFromBottom (12));
|
||||
reverbSizeSlider.setBounds (c1);
|
||||
reverbDecayLabel.setBounds (revArea.removeFromBottom (12));
|
||||
reverbDecaySlider.setBounds (revArea);
|
||||
}
|
||||
}
|
||||
45
Source/FxPanel.h
Normal file
45
Source/FxPanel.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class FxPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
FxPanel();
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
// FX parameter getters (for processor to read)
|
||||
float getCompThreshold() const { return (float) compThreshSlider.getValue(); }
|
||||
float getCompRatio() const { return (float) compRatioSlider.getValue(); }
|
||||
float getEqLo() const { return (float) eqLoSlider.getValue(); }
|
||||
float getEqMid() const { return (float) eqMidSlider.getValue(); }
|
||||
float getEqHi() const { return (float) eqHiSlider.getValue(); }
|
||||
float getDistDrive() const { return (float) distDriveSlider.getValue(); }
|
||||
float getDistMix() const { return (float) distMixSlider.getValue(); }
|
||||
float getReverbSize() const { return (float) reverbSizeSlider.getValue(); }
|
||||
float getReverbDecay() const { return (float) reverbDecaySlider.getValue(); }
|
||||
|
||||
private:
|
||||
// Compressor
|
||||
juce::Slider compThreshSlider, compRatioSlider;
|
||||
juce::Label compThreshLabel, compRatioLabel, compTitle;
|
||||
|
||||
// EQ
|
||||
juce::Slider eqLoSlider, eqMidSlider, eqHiSlider;
|
||||
juce::Label eqLoLabel, eqMidLabel, eqHiLabel, eqTitle;
|
||||
|
||||
// Distortion
|
||||
juce::Slider distDriveSlider, distMixSlider;
|
||||
juce::Label distDriveLabel, distMixLabel, distTitle;
|
||||
|
||||
// Reverb
|
||||
juce::Slider reverbSizeSlider, reverbDecaySlider;
|
||||
juce::Label reverbSizeLabel, reverbDecayLabel, reverbTitle;
|
||||
|
||||
void setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step = 0.01);
|
||||
void setupTitle (juce::Label& l, const juce::String& text);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FxPanel)
|
||||
};
|
||||
44
Source/LookAndFeel.cpp
Normal file
44
Source/LookAndFeel.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
InstaDrumsLookAndFeel::InstaDrumsLookAndFeel()
|
||||
{
|
||||
setColour (juce::ResizableWindow::backgroundColourId, bgDark);
|
||||
setColour (juce::Label::textColourId, textPrimary);
|
||||
setColour (juce::TextButton::buttonColourId, bgMedium);
|
||||
setColour (juce::TextButton::textColourOffId, textPrimary);
|
||||
}
|
||||
|
||||
void InstaDrumsLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPos, float rotaryStartAngle,
|
||||
float rotaryEndAngle, juce::Slider& slider)
|
||||
{
|
||||
auto bounds = juce::Rectangle<int> (x, y, width, height).toFloat().reduced (2.0f);
|
||||
auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f;
|
||||
auto centreX = bounds.getCentreX();
|
||||
auto centreY = bounds.getCentreY();
|
||||
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
|
||||
|
||||
// Background arc
|
||||
juce::Path bgArc;
|
||||
bgArc.addCentredArc (centreX, centreY, radius - 2, radius - 2,
|
||||
0.0f, rotaryStartAngle, rotaryEndAngle, true);
|
||||
g.setColour (bgLight);
|
||||
g.strokePath (bgArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
|
||||
// Value arc
|
||||
juce::Path valueArc;
|
||||
valueArc.addCentredArc (centreX, centreY, radius - 2, radius - 2,
|
||||
0.0f, rotaryStartAngle, angle, true);
|
||||
g.setColour (accent);
|
||||
g.strokePath (valueArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
|
||||
// Pointer
|
||||
juce::Path pointer;
|
||||
auto pointerLength = radius * 0.5f;
|
||||
pointer.addRectangle (-1.5f, -pointerLength, 3.0f, pointerLength);
|
||||
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (centreX, centreY));
|
||||
g.setColour (textPrimary);
|
||||
g.fillPath (pointer);
|
||||
}
|
||||
20
Source/LookAndFeel.h
Normal file
20
Source/LookAndFeel.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class InstaDrumsLookAndFeel : 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 };
|
||||
|
||||
InstaDrumsLookAndFeel();
|
||||
|
||||
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPosProportional, float rotaryStartAngle,
|
||||
float rotaryEndAngle, juce::Slider& slider) override;
|
||||
};
|
||||
62
Source/MasterPanel.cpp
Normal file
62
Source/MasterPanel.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "MasterPanel.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
MasterPanel::MasterPanel()
|
||||
{
|
||||
masterTitle.setFont (juce::FontOptions (12.0f, juce::Font::bold));
|
||||
masterTitle.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (masterTitle);
|
||||
|
||||
setupKnob (volumeSlider, volumeLabel, "Volume", 0.0, 2.0, 1.0, 0.01);
|
||||
setupKnob (tuneSlider, tuneLabel, "Tune", -12.0, 12.0, 0.0, 0.1);
|
||||
setupKnob (panSlider, panLabel, "Pan", -1.0, 1.0, 0.0, 0.01);
|
||||
|
||||
addAndMakeVisible (vuMeter);
|
||||
}
|
||||
|
||||
void MasterPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step)
|
||||
{
|
||||
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
s.setRange (min, max, step);
|
||||
s.setValue (val, juce::dontSendNotification);
|
||||
addAndMakeVisible (s);
|
||||
|
||||
l.setText (name, juce::dontSendNotification);
|
||||
l.setFont (juce::FontOptions (9.0f));
|
||||
l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
l.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (l);
|
||||
}
|
||||
|
||||
void MasterPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaDrumsLookAndFeel::bgMedium.darker (0.2f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
|
||||
void MasterPanel::resized()
|
||||
{
|
||||
auto area = getLocalBounds().reduced (4);
|
||||
|
||||
masterTitle.setBounds (area.removeFromLeft (55).reduced (0, 2));
|
||||
|
||||
// VU meter on the right
|
||||
vuMeter.setBounds (area.removeFromRight (24).reduced (0, 2));
|
||||
area.removeFromRight (4);
|
||||
|
||||
// Knobs
|
||||
int knobW = area.getWidth() / 3;
|
||||
juce::Slider* sliders[] = { &volumeSlider, &tuneSlider, &panSlider };
|
||||
juce::Label* labels[] = { &volumeLabel, &tuneLabel, &panLabel };
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
auto col = area.removeFromLeft (knobW);
|
||||
labels[i]->setBounds (col.removeFromBottom (12));
|
||||
sliders[i]->setBounds (col);
|
||||
}
|
||||
}
|
||||
29
Source/MasterPanel.h
Normal file
29
Source/MasterPanel.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "VuMeter.h"
|
||||
|
||||
class MasterPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
MasterPanel();
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
float getMasterVolume() const { return (float) volumeSlider.getValue(); }
|
||||
float getMasterTune() const { return (float) tuneSlider.getValue(); }
|
||||
float getMasterPan() const { return (float) panSlider.getValue(); }
|
||||
|
||||
VuMeter& getVuMeter() { return vuMeter; }
|
||||
|
||||
private:
|
||||
juce::Slider volumeSlider, tuneSlider, panSlider;
|
||||
juce::Label volumeLabel, tuneLabel, panLabel;
|
||||
juce::Label masterTitle { {}, "MASTER" };
|
||||
VuMeter vuMeter;
|
||||
|
||||
void setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step = 0.01);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MasterPanel)
|
||||
};
|
||||
166
Source/PadComponent.cpp
Normal file
166
Source/PadComponent.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
#include "PadComponent.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
PadComponent::PadComponent (DrumPad& pad, std::function<void(int, const juce::File&)> loadCallback, int padIndex)
|
||||
: drumPad (pad), onLoadSample (std::move (loadCallback)), index (padIndex)
|
||||
{
|
||||
}
|
||||
|
||||
void PadComponent::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat().reduced (2.0f);
|
||||
float cornerSize = 6.0f;
|
||||
|
||||
// Background
|
||||
float alpha = isPressed ? 0.85f : (isDragOver ? 0.65f : (selected ? 0.55f : 0.3f));
|
||||
g.setColour (drumPad.colour.withAlpha (alpha));
|
||||
g.fillRoundedRectangle (bounds, cornerSize);
|
||||
|
||||
// Border — selected = bright accent, normal = pad colour
|
||||
if (selected)
|
||||
{
|
||||
g.setColour (juce::Colour (0xff00aaff)); // blue selection
|
||||
g.drawRoundedRectangle (bounds, cornerSize, 2.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour (drumPad.colour.withAlpha (0.6f));
|
||||
g.drawRoundedRectangle (bounds, cornerSize, 1.0f);
|
||||
}
|
||||
|
||||
// Pad number (top-left, small)
|
||||
g.setColour (InstaDrumsLookAndFeel::textSecondary);
|
||||
g.setFont (juce::Font (juce::FontOptions (9.0f, juce::Font::bold)));
|
||||
g.drawText (juce::String (index + 1), bounds.reduced (4, 3), juce::Justification::topLeft);
|
||||
|
||||
// Waveform thumbnail (center area)
|
||||
if (drumPad.hasSample())
|
||||
{
|
||||
auto waveArea = bounds.reduced (4, 16);
|
||||
drawWaveformThumbnail (g, waveArea);
|
||||
}
|
||||
|
||||
// Pad name (bottom)
|
||||
g.setColour (InstaDrumsLookAndFeel::textPrimary);
|
||||
g.setFont (juce::Font (juce::FontOptions (10.0f, juce::Font::bold)));
|
||||
g.drawText (drumPad.name, bounds.reduced (4, 2), juce::Justification::centredBottom);
|
||||
|
||||
// Playing flash
|
||||
if (drumPad.isPlaying())
|
||||
{
|
||||
g.setColour (juce::Colours::white.withAlpha (0.12f));
|
||||
g.fillRoundedRectangle (bounds, cornerSize);
|
||||
}
|
||||
}
|
||||
|
||||
void PadComponent::drawWaveformThumbnail (juce::Graphics& g, juce::Rectangle<float> area)
|
||||
{
|
||||
auto& buf = drumPad.getSampleBuffer();
|
||||
if (buf.getNumSamples() == 0) return;
|
||||
|
||||
const float* data = buf.getReadPointer (0);
|
||||
const int numSamples = buf.getNumSamples();
|
||||
const float w = area.getWidth();
|
||||
const float h = area.getHeight();
|
||||
const float midY = area.getCentreY();
|
||||
|
||||
juce::Path path;
|
||||
int blockSize = std::max (1, numSamples / (int) w);
|
||||
|
||||
for (int x = 0; x < (int) w; ++x)
|
||||
{
|
||||
int si = (int) ((float) x / w * numSamples);
|
||||
si = juce::jlimit (0, numSamples - 1, si);
|
||||
|
||||
float maxVal = 0.0f;
|
||||
for (int j = 0; j < blockSize && (si + j) < numSamples; ++j)
|
||||
maxVal = std::max (maxVal, std::abs (data[si + j]));
|
||||
|
||||
float topY = midY - maxVal * (h * 0.45f);
|
||||
float botY = midY + maxVal * (h * 0.45f);
|
||||
|
||||
if (x == 0)
|
||||
path.startNewSubPath (area.getX() + (float) x, topY);
|
||||
|
||||
path.lineTo (area.getX() + (float) x, topY);
|
||||
}
|
||||
// Mirror bottom
|
||||
for (int x = (int) w - 1; x >= 0; --x)
|
||||
{
|
||||
int si = (int) ((float) x / w * numSamples);
|
||||
si = juce::jlimit (0, numSamples - 1, si);
|
||||
float maxVal = 0.0f;
|
||||
for (int j = 0; j < blockSize && (si + j) < numSamples; ++j)
|
||||
maxVal = std::max (maxVal, std::abs (data[si + j]));
|
||||
float botY = midY + maxVal * (h * 0.45f);
|
||||
path.lineTo (area.getX() + (float) x, botY);
|
||||
}
|
||||
path.closeSubPath();
|
||||
|
||||
// Greenish waveform tint
|
||||
g.setColour (juce::Colour (0xff44cc88).withAlpha (0.4f));
|
||||
g.fillPath (path);
|
||||
g.setColour (juce::Colour (0xff44cc88).withAlpha (0.7f));
|
||||
g.strokePath (path, juce::PathStrokeType (0.8f));
|
||||
}
|
||||
|
||||
void PadComponent::resized() {}
|
||||
|
||||
void PadComponent::mouseDown (const juce::MouseEvent& event)
|
||||
{
|
||||
if (event.mods.isRightButtonDown())
|
||||
return;
|
||||
|
||||
// Select this pad
|
||||
if (onSelected)
|
||||
onSelected (index);
|
||||
|
||||
isPressed = true;
|
||||
drumPad.trigger (1.0f);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void PadComponent::mouseUp (const juce::MouseEvent& event)
|
||||
{
|
||||
if (event.mods.isRightButtonDown())
|
||||
return;
|
||||
|
||||
isPressed = false;
|
||||
if (! drumPad.oneShot)
|
||||
drumPad.stop();
|
||||
repaint();
|
||||
}
|
||||
|
||||
bool PadComponent::isInterestedInFileDrag (const juce::StringArray& files)
|
||||
{
|
||||
for (auto& f : files)
|
||||
{
|
||||
juce::File file (f);
|
||||
if (file.isDirectory()) return true;
|
||||
auto ext = file.getFileExtension().toLowerCase();
|
||||
if (ext == ".wav" || ext == ".aiff" || ext == ".aif" || ext == ".flac"
|
||||
|| ext == ".ogg" || ext == ".mp3")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PadComponent::filesDropped (const juce::StringArray& files, int, int)
|
||||
{
|
||||
isDragOver = false;
|
||||
if (! files.isEmpty() && onLoadSample)
|
||||
onLoadSample (index, juce::File (files[0]));
|
||||
repaint();
|
||||
}
|
||||
|
||||
void PadComponent::fileDragEnter (const juce::StringArray&, int, int)
|
||||
{
|
||||
isDragOver = true;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void PadComponent::fileDragExit (const juce::StringArray&)
|
||||
{
|
||||
isDragOver = false;
|
||||
repaint();
|
||||
}
|
||||
40
Source/PadComponent.h
Normal file
40
Source/PadComponent.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "DrumPad.h"
|
||||
|
||||
class PadComponent : public juce::Component,
|
||||
public juce::FileDragAndDropTarget
|
||||
{
|
||||
public:
|
||||
PadComponent (DrumPad& pad, std::function<void(int, const juce::File&)> loadCallback, int padIndex);
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
void mouseDown (const juce::MouseEvent& event) override;
|
||||
void mouseUp (const juce::MouseEvent& event) override;
|
||||
|
||||
// Drag & Drop
|
||||
bool isInterestedInFileDrag (const juce::StringArray& files) override;
|
||||
void filesDropped (const juce::StringArray& files, int x, int y) override;
|
||||
void fileDragEnter (const juce::StringArray& files, int x, int y) override;
|
||||
void fileDragExit (const juce::StringArray& files) override;
|
||||
|
||||
void setSelected (bool sel) { selected = sel; repaint(); }
|
||||
bool isSelected() const { return selected; }
|
||||
|
||||
// Callback when pad is selected (left click)
|
||||
std::function<void(int)> onSelected;
|
||||
|
||||
private:
|
||||
DrumPad& drumPad;
|
||||
std::function<void(int, const juce::File&)> onLoadSample;
|
||||
int index;
|
||||
bool isPressed = false;
|
||||
bool isDragOver = false;
|
||||
bool selected = false;
|
||||
|
||||
void drawWaveformThumbnail (juce::Graphics& g, juce::Rectangle<float> area);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PadComponent)
|
||||
};
|
||||
217
Source/PluginEditor.cpp
Normal file
217
Source/PluginEditor.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "PluginEditor.h"
|
||||
|
||||
InstaDrumsEditor::InstaDrumsEditor (InstaDrumsProcessor& p)
|
||||
: AudioProcessorEditor (&p), processor (p)
|
||||
{
|
||||
setLookAndFeel (&customLookAndFeel);
|
||||
|
||||
// Title
|
||||
titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold));
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent);
|
||||
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
versionLabel.setFont (juce::FontOptions (10.0f));
|
||||
versionLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
versionLabel.setJustificationType (juce::Justification::centredRight);
|
||||
addAndMakeVisible (versionLabel);
|
||||
|
||||
padsLabel.setFont (juce::FontOptions (10.0f, juce::Font::bold));
|
||||
padsLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (padsLabel);
|
||||
|
||||
// Buttons
|
||||
auto styleBtn = [this] (juce::TextButton& btn) {
|
||||
btn.setColour (juce::TextButton::buttonColourId, InstaDrumsLookAndFeel::bgLight);
|
||||
btn.setColour (juce::TextButton::textColourOffId, InstaDrumsLookAndFeel::textPrimary);
|
||||
addAndMakeVisible (btn);
|
||||
};
|
||||
|
||||
styleBtn (loadSampleButton);
|
||||
styleBtn (saveKitButton);
|
||||
styleBtn (loadKitButton);
|
||||
styleBtn (loadFolderButton);
|
||||
|
||||
loadSampleButton.onClick = [this]
|
||||
{
|
||||
fileChooser = std::make_unique<juce::FileChooser> ("Load Sample", juce::File{},
|
||||
"*.wav;*.aiff;*.aif;*.flac;*.ogg;*.mp3");
|
||||
fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
|
||||
[this] (const juce::FileChooser& fc) {
|
||||
auto file = fc.getResult();
|
||||
if (file.existsAsFile())
|
||||
{
|
||||
processor.loadSample (selectedPadIndex, file);
|
||||
sampleEditor.updateFromPad();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveKitButton.onClick = [this]
|
||||
{
|
||||
fileChooser = std::make_unique<juce::FileChooser> ("Save Kit", juce::File{}, "*.drumkit");
|
||||
fileChooser->launchAsync (juce::FileBrowserComponent::saveMode,
|
||||
[this] (const juce::FileChooser& fc) {
|
||||
auto file = fc.getResult();
|
||||
if (file != juce::File{})
|
||||
processor.saveKitPreset (file.hasFileExtension (".drumkit") ? file : file.withFileExtension ("drumkit"));
|
||||
});
|
||||
};
|
||||
|
||||
loadKitButton.onClick = [this]
|
||||
{
|
||||
fileChooser = std::make_unique<juce::FileChooser> ("Load Kit", juce::File{}, "*.drumkit");
|
||||
fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
|
||||
[this] (const juce::FileChooser& fc) {
|
||||
auto file = fc.getResult();
|
||||
if (file.existsAsFile())
|
||||
{
|
||||
processor.loadKitPreset (file);
|
||||
rebuildPadGrid();
|
||||
selectPad (0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadFolderButton.onClick = [this]
|
||||
{
|
||||
fileChooser = std::make_unique<juce::FileChooser> ("Select Sample Folder", juce::File{});
|
||||
fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectDirectories,
|
||||
[this] (const juce::FileChooser& fc) {
|
||||
auto folder = fc.getResult();
|
||||
if (folder.isDirectory())
|
||||
{
|
||||
processor.loadKitFromFolder (folder);
|
||||
rebuildPadGrid();
|
||||
selectPad (0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Panels
|
||||
addAndMakeVisible (sampleEditor);
|
||||
addAndMakeVisible (fxPanel);
|
||||
addAndMakeVisible (masterPanel);
|
||||
|
||||
rebuildPadGrid();
|
||||
selectPad (0);
|
||||
|
||||
// Sizing
|
||||
constrainer.setMinimumSize (800, 500);
|
||||
constrainer.setMaximumSize (1920, 1080);
|
||||
setConstrainer (&constrainer);
|
||||
setSize (960, 600);
|
||||
setResizable (true, true);
|
||||
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
InstaDrumsEditor::~InstaDrumsEditor()
|
||||
{
|
||||
setLookAndFeel (nullptr);
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::rebuildPadGrid()
|
||||
{
|
||||
padComponents.clear();
|
||||
|
||||
auto loadCallback = [this] (int padIndex, const juce::File& file) {
|
||||
processor.loadSample (padIndex, file);
|
||||
if (padIndex == selectedPadIndex)
|
||||
sampleEditor.updateFromPad();
|
||||
};
|
||||
|
||||
for (int i = 0; i < processor.getNumPads(); ++i)
|
||||
{
|
||||
auto* pc = new PadComponent (processor.getPad (i), loadCallback, i);
|
||||
pc->onSelected = [this] (int idx) { selectPad (idx); };
|
||||
addAndMakeVisible (pc);
|
||||
padComponents.add (pc);
|
||||
}
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::selectPad (int index)
|
||||
{
|
||||
selectedPadIndex = index;
|
||||
|
||||
for (int i = 0; i < padComponents.size(); ++i)
|
||||
padComponents[i]->setSelected (i == index);
|
||||
|
||||
if (index >= 0 && index < processor.getNumPads())
|
||||
sampleEditor.setPad (&processor.getPad (index));
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (InstaDrumsLookAndFeel::bgDark);
|
||||
|
||||
// Subtle divider lines
|
||||
auto bounds = getLocalBounds();
|
||||
int rightPanelX = (int) (bounds.getWidth() * 0.52f);
|
||||
int bottomPanelY = bounds.getHeight() - 56;
|
||||
|
||||
g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawVerticalLine (rightPanelX - 2, 30, (float) bottomPanelY);
|
||||
g.drawHorizontalLine (bottomPanelY - 1, 0, (float) bounds.getWidth());
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::resized()
|
||||
{
|
||||
auto area = getLocalBounds();
|
||||
|
||||
// Top bar (30px)
|
||||
auto topBar = area.removeFromTop (30).reduced (6, 4);
|
||||
titleLabel.setBounds (topBar.removeFromLeft (150));
|
||||
versionLabel.setBounds (topBar.removeFromRight (40));
|
||||
loadFolderButton.setBounds (topBar.removeFromRight (90).reduced (1));
|
||||
loadKitButton.setBounds (topBar.removeFromRight (70).reduced (1));
|
||||
saveKitButton.setBounds (topBar.removeFromRight (70).reduced (1));
|
||||
loadSampleButton.setBounds (topBar.removeFromRight (95).reduced (1));
|
||||
|
||||
// Bottom master bar (52px)
|
||||
masterPanel.setBounds (area.removeFromBottom (52).reduced (4, 2));
|
||||
|
||||
// Left panel: pad grid (~52% width)
|
||||
int rightPanelX = (int) (area.getWidth() * 0.52f);
|
||||
auto leftArea = area.removeFromLeft (rightPanelX).reduced (4);
|
||||
|
||||
// Pads label
|
||||
auto padsHeader = leftArea.removeFromTop (16);
|
||||
padsLabel.setBounds (padsHeader);
|
||||
|
||||
// Pad grid
|
||||
int numPads = padComponents.size();
|
||||
if (numPads > 0)
|
||||
{
|
||||
int rows = (numPads + padColumns - 1) / padColumns;
|
||||
int padW = leftArea.getWidth() / padColumns;
|
||||
int padH = leftArea.getHeight() / rows;
|
||||
|
||||
for (int i = 0; i < numPads; ++i)
|
||||
{
|
||||
int row = i / padColumns;
|
||||
int col = i % padColumns;
|
||||
padComponents[i]->setBounds (leftArea.getX() + col * padW,
|
||||
leftArea.getY() + row * padH,
|
||||
padW, padH);
|
||||
}
|
||||
}
|
||||
|
||||
// Right panel: sample editor (top ~55%) + FX (bottom ~45%)
|
||||
auto rightArea = area.reduced (4);
|
||||
int editorHeight = (int) (rightArea.getHeight() * 0.55f);
|
||||
sampleEditor.setBounds (rightArea.removeFromTop (editorHeight).reduced (0, 2));
|
||||
fxPanel.setBounds (rightArea.reduced (0, 2));
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::timerCallback()
|
||||
{
|
||||
for (auto* pc : padComponents)
|
||||
pc->repaint();
|
||||
|
||||
// Update VU meter from processor output levels
|
||||
// (simplified: just repaint for now)
|
||||
masterPanel.getVuMeter().repaint();
|
||||
}
|
||||
58
Source/PluginEditor.h
Normal file
58
Source/PluginEditor.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "PadComponent.h"
|
||||
#include "SampleEditorPanel.h"
|
||||
#include "FxPanel.h"
|
||||
#include "MasterPanel.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
class InstaDrumsEditor : public juce::AudioProcessorEditor,
|
||||
private juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit InstaDrumsEditor (InstaDrumsProcessor&);
|
||||
~InstaDrumsEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
InstaDrumsProcessor& processor;
|
||||
InstaDrumsLookAndFeel customLookAndFeel;
|
||||
|
||||
// Pad grid (left side)
|
||||
juce::OwnedArray<PadComponent> padComponents;
|
||||
static constexpr int padColumns = 4;
|
||||
|
||||
// Right side panels
|
||||
SampleEditorPanel sampleEditor;
|
||||
FxPanel fxPanel;
|
||||
|
||||
// Bottom
|
||||
MasterPanel masterPanel;
|
||||
|
||||
// Top bar buttons
|
||||
juce::TextButton loadSampleButton { "LOAD SAMPLE" };
|
||||
juce::TextButton saveKitButton { "SAVE KIT" };
|
||||
juce::TextButton loadKitButton { "LOAD KIT" };
|
||||
juce::TextButton loadFolderButton { "LOAD FOLDER" };
|
||||
|
||||
juce::Label titleLabel { {}, "INSTADRUMS" };
|
||||
juce::Label versionLabel { {}, "v1.0" };
|
||||
juce::Label padsLabel { {}, "DRUM SAMPLER" };
|
||||
|
||||
// State
|
||||
int selectedPadIndex = 0;
|
||||
|
||||
void rebuildPadGrid();
|
||||
void selectPad (int index);
|
||||
void timerCallback() override;
|
||||
|
||||
std::unique_ptr<juce::FileChooser> fileChooser;
|
||||
|
||||
// Resizable
|
||||
juce::ComponentBoundsConstrainer constrainer;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaDrumsEditor)
|
||||
};
|
||||
347
Source/PluginProcessor.cpp
Normal file
347
Source/PluginProcessor.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
InstaDrumsProcessor::InstaDrumsProcessor()
|
||||
: AudioProcessor (BusesProperties()
|
||||
.withOutput ("Main", juce::AudioChannelSet::stereo(), true))
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
initializeDefaults();
|
||||
}
|
||||
|
||||
InstaDrumsProcessor::~InstaDrumsProcessor() {}
|
||||
|
||||
void InstaDrumsProcessor::initializeDefaults()
|
||||
{
|
||||
// GM Drum Map defaults for first 12 pads
|
||||
struct PadDefault { int note; const char* name; juce::uint32 colour; };
|
||||
static const PadDefault defaults[] = {
|
||||
{ 36, "Kick", 0xffff4444 }, // Red
|
||||
{ 38, "Snare", 0xffff8844 }, // Orange
|
||||
{ 42, "CH Hat", 0xffffff44 }, // Yellow
|
||||
{ 46, "OH Hat", 0xff88ff44 }, // Green
|
||||
{ 45, "Low Tom", 0xff44ffaa }, // Teal
|
||||
{ 48, "Mid Tom", 0xff44ddff }, // Cyan
|
||||
{ 50, "Hi Tom", 0xff4488ff }, // Blue
|
||||
{ 49, "Crash", 0xff8844ff }, // Purple
|
||||
{ 51, "Ride", 0xffcc44ff }, // Magenta
|
||||
{ 39, "Clap", 0xffff44cc }, // Pink
|
||||
{ 56, "Cowbell", 0xffff8888 }, // Light red
|
||||
{ 37, "Rimshot", 0xffaaaaff }, // Light blue
|
||||
};
|
||||
|
||||
for (int i = 0; i < defaultNumPads && i < (int) std::size (defaults); ++i)
|
||||
{
|
||||
pads[i].midiNote = defaults[i].note;
|
||||
pads[i].name = defaults[i].name;
|
||||
pads[i].colour = juce::Colour (defaults[i].colour);
|
||||
}
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
pads[i].prepareToPlay (sampleRate, samplesPerBlock);
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::releaseResources()
|
||||
{
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
pads[i].releaseResources();
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
buffer.clear();
|
||||
|
||||
// Process MIDI messages
|
||||
for (const auto metadata : midiMessages)
|
||||
{
|
||||
auto msg = metadata.getMessage();
|
||||
if (msg.isNoteOn())
|
||||
{
|
||||
auto* pad = findPadForNote (msg.getNoteNumber());
|
||||
if (pad != nullptr)
|
||||
{
|
||||
// Handle choke groups
|
||||
if (pad->chokeGroup >= 0)
|
||||
{
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
{
|
||||
if (&pads[i] != pad && pads[i].chokeGroup == pad->chokeGroup)
|
||||
pads[i].stop();
|
||||
}
|
||||
}
|
||||
pad->trigger (msg.getFloatVelocity());
|
||||
}
|
||||
}
|
||||
else if (msg.isNoteOff())
|
||||
{
|
||||
auto* pad = findPadForNote (msg.getNoteNumber());
|
||||
if (pad != nullptr && ! pad->oneShot)
|
||||
pad->stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Render audio from all pads
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
pads[i].renderNextBlock (buffer, 0, buffer.getNumSamples());
|
||||
}
|
||||
|
||||
DrumPad* InstaDrumsProcessor::findPadForNote (int midiNote)
|
||||
{
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
if (pads[i].midiNote == midiNote)
|
||||
return &pads[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::loadSample (int padIndex, const juce::File& file)
|
||||
{
|
||||
if (padIndex < 0 || padIndex >= numActivePads)
|
||||
return;
|
||||
|
||||
if (file.isDirectory())
|
||||
pads[padIndex].loadLayersFromFolder (file, formatManager);
|
||||
else
|
||||
pads[padIndex].loadSample (file, formatManager);
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::addPads (int count)
|
||||
{
|
||||
int newCount = std::min (numActivePads + count, maxPads);
|
||||
for (int i = numActivePads; i < newCount; ++i)
|
||||
{
|
||||
pads[i].name = "Pad " + juce::String (i + 1);
|
||||
pads[i].midiNote = 36 + i; // Sequential mapping
|
||||
pads[i].colour = juce::Colour::fromHSV ((float) i / 16.0f, 0.7f, 1.0f, 1.0f);
|
||||
}
|
||||
numActivePads = newCount;
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
juce::XmlElement xml ("InstaDrumsState");
|
||||
xml.setAttribute ("numPads", numActivePads);
|
||||
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
{
|
||||
auto* padXml = xml.createNewChildElement ("Pad");
|
||||
padXml->setAttribute ("index", i);
|
||||
padXml->setAttribute ("name", pads[i].name);
|
||||
padXml->setAttribute ("midiNote", pads[i].midiNote);
|
||||
padXml->setAttribute ("volume", (double) pads[i].volume);
|
||||
padXml->setAttribute ("pan", (double) pads[i].pan);
|
||||
padXml->setAttribute ("pitch", (double) pads[i].pitch);
|
||||
padXml->setAttribute ("oneShot", pads[i].oneShot);
|
||||
padXml->setAttribute ("chokeGroup", pads[i].chokeGroup);
|
||||
padXml->setAttribute ("attack", (double) pads[i].attack);
|
||||
padXml->setAttribute ("decay", (double) pads[i].decay);
|
||||
padXml->setAttribute ("sustain", (double) pads[i].sustain);
|
||||
padXml->setAttribute ("release", (double) pads[i].release);
|
||||
padXml->setAttribute ("colour", (int) pads[i].colour.getARGB());
|
||||
|
||||
auto lf = pads[i].getLoadedFile();
|
||||
if (lf.existsAsFile() || lf.isDirectory())
|
||||
padXml->setAttribute ("samplePath", lf.getFullPathName());
|
||||
}
|
||||
|
||||
copyXmlToBinary (xml, destData);
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
auto xml = getXmlFromBinary (data, sizeInBytes);
|
||||
if (xml != nullptr && xml->hasTagName ("InstaDrumsState"))
|
||||
{
|
||||
numActivePads = xml->getIntAttribute ("numPads", defaultNumPads);
|
||||
|
||||
for (auto* padXml : xml->getChildWithTagNameIterator ("Pad"))
|
||||
{
|
||||
int index = padXml->getIntAttribute ("index", -1);
|
||||
if (index < 0 || index >= numActivePads)
|
||||
continue;
|
||||
|
||||
pads[index].name = padXml->getStringAttribute ("name", "Pad");
|
||||
pads[index].midiNote = padXml->getIntAttribute ("midiNote", 36 + index);
|
||||
pads[index].volume = (float) padXml->getDoubleAttribute ("volume", 1.0);
|
||||
pads[index].pan = (float) padXml->getDoubleAttribute ("pan", 0.0);
|
||||
pads[index].pitch = (float) padXml->getDoubleAttribute ("pitch", 0.0);
|
||||
pads[index].oneShot = padXml->getBoolAttribute ("oneShot", true);
|
||||
pads[index].chokeGroup = padXml->getIntAttribute ("chokeGroup", -1);
|
||||
pads[index].attack = (float) padXml->getDoubleAttribute ("attack", 0.001);
|
||||
pads[index].decay = (float) padXml->getDoubleAttribute ("decay", 0.1);
|
||||
pads[index].sustain = (float) padXml->getDoubleAttribute ("sustain", 1.0);
|
||||
pads[index].release = (float) padXml->getDoubleAttribute ("release", 0.05);
|
||||
pads[index].colour = juce::Colour ((juce::uint32) padXml->getIntAttribute ("colour", 0xff00ff88));
|
||||
|
||||
juce::String path = padXml->getStringAttribute ("samplePath");
|
||||
if (path.isNotEmpty())
|
||||
{
|
||||
juce::File sampleFile (path);
|
||||
if (sampleFile.isDirectory())
|
||||
pads[index].loadLayersFromFolder (sampleFile, formatManager);
|
||||
else if (sampleFile.existsAsFile())
|
||||
pads[index].loadSample (sampleFile, formatManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::loadKitFromFolder (const juce::File& folder)
|
||||
{
|
||||
if (! folder.isDirectory())
|
||||
return;
|
||||
|
||||
// Collect audio files from the folder
|
||||
juce::Array<juce::File> audioFiles;
|
||||
for (auto& f : folder.findChildFiles (juce::File::findFiles, false))
|
||||
{
|
||||
auto ext = f.getFileExtension().toLowerCase();
|
||||
if (ext == ".wav" || ext == ".aiff" || ext == ".aif" || ext == ".flac"
|
||||
|| ext == ".ogg" || ext == ".mp3")
|
||||
audioFiles.add (f);
|
||||
}
|
||||
|
||||
audioFiles.sort();
|
||||
|
||||
// Try to match files to pads by name (kick, snare, etc.)
|
||||
auto matchPad = [&] (const juce::String& fileName) -> int
|
||||
{
|
||||
auto lower = fileName.toLowerCase();
|
||||
struct NameMatch { const char* keyword; int padIndex; };
|
||||
static const NameMatch matches[] = {
|
||||
{ "kick", 0 }, { "bass", 0 }, { "bd", 0 },
|
||||
{ "snare", 1 }, { "sn", 1 }, { "sd", 1 },
|
||||
{ "closedhihat", 2 }, { "closedhi", 2 }, { "chh", 2 }, { "ch hat", 2 },
|
||||
{ "openhihat", 3 }, { "openhi", 3 }, { "ohh", 3 }, { "oh hat", 3 },
|
||||
{ "lowtom", 4 }, { "low tom", 4 }, { "lt", 4 },
|
||||
{ "midtom", 5 }, { "mid tom", 5 }, { "mt", 5 },
|
||||
{ "hitom", 6 }, { "hi tom", 6 }, { "ht", 6 },
|
||||
{ "crash", 7 },
|
||||
{ "ride", 8 },
|
||||
{ "clap", 9 },
|
||||
{ "cowbell", 10 }, { "bell", 10 },
|
||||
{ "rim", 11 },
|
||||
};
|
||||
|
||||
for (auto& m : matches)
|
||||
if (lower.contains (m.keyword))
|
||||
return m.padIndex;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// First pass: match by name
|
||||
juce::Array<bool> assigned;
|
||||
assigned.resize (numActivePads);
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
assigned.set (i, false);
|
||||
|
||||
for (auto& file : audioFiles)
|
||||
{
|
||||
int idx = matchPad (file.getFileNameWithoutExtension());
|
||||
if (idx >= 0 && idx < numActivePads && ! assigned[idx])
|
||||
{
|
||||
pads[idx].loadSample (file, formatManager);
|
||||
assigned.set (idx, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign remaining files to unassigned pads
|
||||
int nextPad = 0;
|
||||
for (auto& file : audioFiles)
|
||||
{
|
||||
int idx = matchPad (file.getFileNameWithoutExtension());
|
||||
if (idx >= 0 && idx < numActivePads && assigned[idx])
|
||||
continue; // Already assigned
|
||||
|
||||
while (nextPad < numActivePads && assigned[nextPad])
|
||||
nextPad++;
|
||||
|
||||
if (nextPad < numActivePads)
|
||||
{
|
||||
pads[nextPad].loadSample (file, formatManager);
|
||||
assigned.set (nextPad, true);
|
||||
nextPad++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::saveKitPreset (const juce::File& file)
|
||||
{
|
||||
juce::XmlElement xml ("InstaDrumsKit");
|
||||
xml.setAttribute ("version", "1.0");
|
||||
xml.setAttribute ("numPads", numActivePads);
|
||||
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
{
|
||||
auto* padXml = xml.createNewChildElement ("Pad");
|
||||
padXml->setAttribute ("index", i);
|
||||
padXml->setAttribute ("name", pads[i].name);
|
||||
padXml->setAttribute ("midiNote", pads[i].midiNote);
|
||||
padXml->setAttribute ("volume", (double) pads[i].volume);
|
||||
padXml->setAttribute ("pan", (double) pads[i].pan);
|
||||
padXml->setAttribute ("pitch", (double) pads[i].pitch);
|
||||
padXml->setAttribute ("oneShot", pads[i].oneShot);
|
||||
padXml->setAttribute ("chokeGroup", pads[i].chokeGroup);
|
||||
padXml->setAttribute ("attack", (double) pads[i].attack);
|
||||
padXml->setAttribute ("decay", (double) pads[i].decay);
|
||||
padXml->setAttribute ("sustain", (double) pads[i].sustain);
|
||||
padXml->setAttribute ("release", (double) pads[i].release);
|
||||
padXml->setAttribute ("colour", (int) pads[i].colour.getARGB());
|
||||
|
||||
auto lf = pads[i].getLoadedFile();
|
||||
if (lf.existsAsFile() || lf.isDirectory())
|
||||
padXml->setAttribute ("samplePath", lf.getFullPathName());
|
||||
}
|
||||
|
||||
xml.writeTo (file);
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::loadKitPreset (const juce::File& file)
|
||||
{
|
||||
auto xml = juce::XmlDocument::parse (file);
|
||||
if (xml == nullptr || ! xml->hasTagName ("InstaDrumsKit"))
|
||||
return;
|
||||
|
||||
numActivePads = xml->getIntAttribute ("numPads", defaultNumPads);
|
||||
|
||||
for (auto* padXml : xml->getChildWithTagNameIterator ("Pad"))
|
||||
{
|
||||
int index = padXml->getIntAttribute ("index", -1);
|
||||
if (index < 0 || index >= numActivePads)
|
||||
continue;
|
||||
|
||||
pads[index].name = padXml->getStringAttribute ("name", pads[index].name);
|
||||
pads[index].midiNote = padXml->getIntAttribute ("midiNote", pads[index].midiNote);
|
||||
pads[index].volume = (float) padXml->getDoubleAttribute ("volume", 1.0);
|
||||
pads[index].pan = (float) padXml->getDoubleAttribute ("pan", 0.0);
|
||||
pads[index].pitch = (float) padXml->getDoubleAttribute ("pitch", 0.0);
|
||||
pads[index].oneShot = padXml->getBoolAttribute ("oneShot", true);
|
||||
pads[index].chokeGroup = padXml->getIntAttribute ("chokeGroup", -1);
|
||||
pads[index].attack = (float) padXml->getDoubleAttribute ("attack", 0.001);
|
||||
pads[index].decay = (float) padXml->getDoubleAttribute ("decay", 0.1);
|
||||
pads[index].sustain = (float) padXml->getDoubleAttribute ("sustain", 1.0);
|
||||
pads[index].release = (float) padXml->getDoubleAttribute ("release", 0.05);
|
||||
pads[index].colour = juce::Colour ((juce::uint32) padXml->getIntAttribute ("colour", (int) pads[index].colour.getARGB()));
|
||||
|
||||
juce::String path = padXml->getStringAttribute ("samplePath");
|
||||
if (path.isNotEmpty())
|
||||
{
|
||||
juce::File sampleFile (path);
|
||||
if (sampleFile.existsAsFile())
|
||||
pads[index].loadSample (sampleFile, formatManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* InstaDrumsProcessor::createEditor()
|
||||
{
|
||||
return new InstaDrumsEditor (*this);
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new InstaDrumsProcessor();
|
||||
}
|
||||
60
Source/PluginProcessor.h
Normal file
60
Source/PluginProcessor.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "DrumPad.h"
|
||||
|
||||
class InstaDrumsProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
static constexpr int defaultNumPads = 12;
|
||||
static constexpr int maxPads = 64;
|
||||
|
||||
InstaDrumsProcessor();
|
||||
~InstaDrumsProcessor() override;
|
||||
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() 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 true; }
|
||||
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;
|
||||
|
||||
// Pad management
|
||||
int getNumPads() const { return numActivePads; }
|
||||
DrumPad& getPad (int index) { return pads[index]; }
|
||||
void addPads (int count = 4);
|
||||
void loadSample (int padIndex, const juce::File& file);
|
||||
|
||||
juce::AudioFormatManager& getFormatManager() { return formatManager; }
|
||||
|
||||
// Find pad by MIDI note
|
||||
DrumPad* findPadForNote (int midiNote);
|
||||
|
||||
// Kit management
|
||||
void loadKitFromFolder (const juce::File& folder);
|
||||
void saveKitPreset (const juce::File& file);
|
||||
void loadKitPreset (const juce::File& file);
|
||||
|
||||
private:
|
||||
std::array<DrumPad, maxPads> pads;
|
||||
int numActivePads = defaultNumPads;
|
||||
juce::AudioFormatManager formatManager;
|
||||
|
||||
// Default MIDI mapping (GM drum map)
|
||||
void initializeDefaults();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaDrumsProcessor)
|
||||
};
|
||||
144
Source/SampleEditorPanel.cpp
Normal file
144
Source/SampleEditorPanel.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "SampleEditorPanel.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
SampleEditorPanel::SampleEditorPanel()
|
||||
{
|
||||
titleLabel.setFont (juce::FontOptions (14.0f, juce::Font::bold));
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
padNameLabel.setFont (juce::FontOptions (13.0f, juce::Font::bold));
|
||||
padNameLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent);
|
||||
addAndMakeVisible (padNameLabel);
|
||||
|
||||
waveform.setShowADSR (true);
|
||||
addAndMakeVisible (waveform);
|
||||
|
||||
setupKnob (attackSlider, attackLabel, "Attack", 0.0, 1.0, 0.001, 0.001);
|
||||
setupKnob (decaySlider, decayLabel, "Decay", 0.0, 2.0, 0.1, 0.01);
|
||||
setupKnob (sustainSlider, sustainLabel, "Sustain", 0.0, 1.0, 1.0, 0.01);
|
||||
setupKnob (releaseSlider, releaseLabel, "Release", 0.0, 2.0, 0.05, 0.01);
|
||||
setupKnob (pitchSlider, pitchLabel, "Pitch", -24.0, 24.0, 0.0, 0.1);
|
||||
setupKnob (panSlider, panLabel, "Pan", -1.0, 1.0, 0.0, 0.01);
|
||||
setupKnob (cutoffSlider, cutoffLabel, "Cutoff", 20.0, 20000.0, 20000.0, 1.0);
|
||||
setupKnob (resoSlider, resoLabel, "Reso", 0.1, 10.0, 0.707, 0.01);
|
||||
|
||||
cutoffSlider.setSkewFactorFromMidPoint (1000.0);
|
||||
}
|
||||
|
||||
void SampleEditorPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step)
|
||||
{
|
||||
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
s.setRange (min, max, step);
|
||||
s.setValue (val, juce::dontSendNotification);
|
||||
s.addListener (this);
|
||||
addAndMakeVisible (s);
|
||||
|
||||
l.setText (name, juce::dontSendNotification);
|
||||
l.setFont (juce::FontOptions (9.0f));
|
||||
l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary);
|
||||
l.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (l);
|
||||
}
|
||||
|
||||
void SampleEditorPanel::setPad (DrumPad* pad)
|
||||
{
|
||||
currentPad = pad;
|
||||
updateFromPad();
|
||||
}
|
||||
|
||||
void SampleEditorPanel::updateFromPad()
|
||||
{
|
||||
if (currentPad == nullptr) return;
|
||||
|
||||
padNameLabel.setText (currentPad->name, juce::dontSendNotification);
|
||||
|
||||
attackSlider.setValue (currentPad->attack, juce::dontSendNotification);
|
||||
decaySlider.setValue (currentPad->decay, juce::dontSendNotification);
|
||||
sustainSlider.setValue (currentPad->sustain, juce::dontSendNotification);
|
||||
releaseSlider.setValue (currentPad->release, juce::dontSendNotification);
|
||||
pitchSlider.setValue (currentPad->pitch, juce::dontSendNotification);
|
||||
panSlider.setValue (currentPad->pan, juce::dontSendNotification);
|
||||
|
||||
auto& buf = currentPad->getSampleBuffer();
|
||||
waveform.setBuffer (&buf);
|
||||
waveform.setColour (currentPad->colour);
|
||||
waveform.setADSR (currentPad->attack, currentPad->decay, currentPad->sustain, currentPad->release);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void SampleEditorPanel::sliderValueChanged (juce::Slider* slider)
|
||||
{
|
||||
if (currentPad == nullptr) return;
|
||||
|
||||
if (slider == &attackSlider) currentPad->attack = (float) slider->getValue();
|
||||
else if (slider == &decaySlider) currentPad->decay = (float) slider->getValue();
|
||||
else if (slider == &sustainSlider) currentPad->sustain = (float) slider->getValue();
|
||||
else if (slider == &releaseSlider) currentPad->release = (float) slider->getValue();
|
||||
else if (slider == &pitchSlider) currentPad->pitch = (float) slider->getValue();
|
||||
else if (slider == &panSlider) currentPad->pan = (float) slider->getValue();
|
||||
|
||||
// Update ADSR overlay
|
||||
waveform.setADSR (currentPad->attack, currentPad->decay, currentPad->sustain, currentPad->release);
|
||||
}
|
||||
|
||||
void SampleEditorPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaDrumsLookAndFeel::bgMedium);
|
||||
g.fillRoundedRectangle (bounds, 6.0f);
|
||||
g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.5f));
|
||||
g.drawRoundedRectangle (bounds, 6.0f, 1.0f);
|
||||
}
|
||||
|
||||
void SampleEditorPanel::resized()
|
||||
{
|
||||
auto area = getLocalBounds().reduced (6);
|
||||
|
||||
// Header
|
||||
auto header = area.removeFromTop (20);
|
||||
titleLabel.setBounds (header.removeFromLeft (100));
|
||||
padNameLabel.setBounds (header);
|
||||
|
||||
area.removeFromTop (2);
|
||||
|
||||
// Waveform (top portion ~40%)
|
||||
int waveHeight = std::max (60, (int) (area.getHeight() * 0.38f));
|
||||
waveform.setBounds (area.removeFromTop (waveHeight));
|
||||
|
||||
area.removeFromTop (4);
|
||||
|
||||
// ADSR knobs row
|
||||
int knobH = std::max (40, (int) (area.getHeight() * 0.45f));
|
||||
auto adsrRow = area.removeFromTop (knobH);
|
||||
int knobW = adsrRow.getWidth() / 4;
|
||||
{
|
||||
juce::Slider* s[] = { &attackSlider, &decaySlider, &sustainSlider, &releaseSlider };
|
||||
juce::Label* l[] = { &attackLabel, &decayLabel, &sustainLabel, &releaseLabel };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
auto col = adsrRow.removeFromLeft (knobW);
|
||||
l[i]->setBounds (col.removeFromBottom (14));
|
||||
s[i]->setBounds (col);
|
||||
}
|
||||
}
|
||||
|
||||
area.removeFromTop (2);
|
||||
|
||||
// Bottom row: Pitch, Pan, Cutoff, Reso
|
||||
auto bottomRow = area;
|
||||
knobW = bottomRow.getWidth() / 4;
|
||||
{
|
||||
juce::Slider* s[] = { &pitchSlider, &panSlider, &cutoffSlider, &resoSlider };
|
||||
juce::Label* l[] = { &pitchLabel, &panLabel, &cutoffLabel, &resoLabel };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
auto col = bottomRow.removeFromLeft (knobW);
|
||||
l[i]->setBounds (col.removeFromBottom (14));
|
||||
s[i]->setBounds (col);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Source/SampleEditorPanel.h
Normal file
41
Source/SampleEditorPanel.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "DrumPad.h"
|
||||
#include "WaveformDisplay.h"
|
||||
|
||||
class SampleEditorPanel : public juce::Component,
|
||||
public juce::Slider::Listener
|
||||
{
|
||||
public:
|
||||
SampleEditorPanel();
|
||||
|
||||
void setPad (DrumPad* pad);
|
||||
DrumPad* getCurrentPad() const { return currentPad; }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
void sliderValueChanged (juce::Slider* slider) override;
|
||||
|
||||
void updateFromPad();
|
||||
|
||||
private:
|
||||
DrumPad* currentPad = nullptr;
|
||||
|
||||
WaveformDisplay waveform;
|
||||
|
||||
// ADSR knobs
|
||||
juce::Slider attackSlider, decaySlider, sustainSlider, releaseSlider;
|
||||
juce::Label attackLabel, decayLabel, sustainLabel, releaseLabel;
|
||||
|
||||
// Sample controls
|
||||
juce::Slider pitchSlider, panSlider, cutoffSlider, resoSlider;
|
||||
juce::Label pitchLabel, panLabel, cutoffLabel, resoLabel;
|
||||
|
||||
juce::Label titleLabel { {}, "Sample Editor" };
|
||||
juce::Label padNameLabel { {}, "" };
|
||||
|
||||
void setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step = 0.01);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SampleEditorPanel)
|
||||
};
|
||||
47
Source/VuMeter.h
Normal file
47
Source/VuMeter.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class VuMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setLevel (float left, float right)
|
||||
{
|
||||
levelL = left;
|
||||
levelR = right;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void paint (juce::Graphics& g) override
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat().reduced (1);
|
||||
float halfW = bounds.getWidth() / 2.0f - 1;
|
||||
auto leftBar = bounds.removeFromLeft (halfW);
|
||||
bounds.removeFromLeft (2);
|
||||
auto rightBar = bounds;
|
||||
|
||||
drawBar (g, leftBar, levelL);
|
||||
drawBar (g, rightBar, levelR);
|
||||
}
|
||||
|
||||
private:
|
||||
float levelL = 0.0f, levelR = 0.0f;
|
||||
|
||||
void drawBar (juce::Graphics& g, juce::Rectangle<float> bar, float level)
|
||||
{
|
||||
g.setColour (juce::Colour (0xff222233));
|
||||
g.fillRoundedRectangle (bar, 2.0f);
|
||||
|
||||
float h = bar.getHeight() * juce::jlimit (0.0f, 1.0f, level);
|
||||
auto filled = bar.removeFromBottom (h);
|
||||
|
||||
// Green -> Yellow -> Red gradient
|
||||
if (level < 0.6f)
|
||||
g.setColour (juce::Colour (0xff00cc44));
|
||||
else if (level < 0.85f)
|
||||
g.setColour (juce::Colour (0xffcccc00));
|
||||
else
|
||||
g.setColour (juce::Colour (0xffff3333));
|
||||
|
||||
g.fillRoundedRectangle (filled, 2.0f);
|
||||
}
|
||||
};
|
||||
106
Source/WaveformDisplay.cpp
Normal file
106
Source/WaveformDisplay.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "WaveformDisplay.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
WaveformDisplay::WaveformDisplay() {}
|
||||
|
||||
void WaveformDisplay::setBuffer (const juce::AudioBuffer<float>* buffer, double sampleRate)
|
||||
{
|
||||
audioBuffer = buffer;
|
||||
bufferSampleRate = sampleRate;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
|
||||
// Background
|
||||
g.setColour (InstaDrumsLookAndFeel::bgDark.darker (0.3f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
if (audioBuffer == nullptr || audioBuffer->getNumSamples() == 0)
|
||||
return;
|
||||
|
||||
const int numSamples = audioBuffer->getNumSamples();
|
||||
const int startSample = (int) (startPos * numSamples);
|
||||
const int endSample = (int) (endPos * numSamples);
|
||||
const int visibleSamples = std::max (1, endSample - startSample);
|
||||
const float width = bounds.getWidth();
|
||||
const float height = bounds.getHeight();
|
||||
const float midY = bounds.getCentreY();
|
||||
|
||||
// Draw waveform
|
||||
juce::Path wavePath;
|
||||
const float* data = audioBuffer->getReadPointer (0);
|
||||
|
||||
for (int x = 0; x < (int) width; ++x)
|
||||
{
|
||||
int sampleIndex = startSample + (int) ((float) x / width * visibleSamples);
|
||||
sampleIndex = juce::jlimit (0, numSamples - 1, sampleIndex);
|
||||
|
||||
// Find min/max in a small range for better visualization
|
||||
int blockSize = std::max (1, visibleSamples / (int) width);
|
||||
float minVal = 1.0f, maxVal = -1.0f;
|
||||
for (int j = 0; j < blockSize && (sampleIndex + j) < numSamples; ++j)
|
||||
{
|
||||
float v = data[sampleIndex + j];
|
||||
minVal = std::min (minVal, v);
|
||||
maxVal = std::max (maxVal, v);
|
||||
}
|
||||
|
||||
float topY = midY - maxVal * (height * 0.45f);
|
||||
float botY = midY - minVal * (height * 0.45f);
|
||||
|
||||
if (x == 0)
|
||||
wavePath.startNewSubPath ((float) x + bounds.getX(), topY);
|
||||
|
||||
wavePath.lineTo ((float) x + bounds.getX(), topY);
|
||||
|
||||
if (x == (int) width - 1)
|
||||
{
|
||||
// Close the path by going back along bottom
|
||||
for (int bx = (int) width - 1; bx >= 0; --bx)
|
||||
{
|
||||
int si = startSample + (int) ((float) bx / width * visibleSamples);
|
||||
si = juce::jlimit (0, numSamples - 1, si);
|
||||
float mn = 1.0f;
|
||||
for (int j = 0; j < blockSize && (si + j) < numSamples; ++j)
|
||||
mn = std::min (mn, data[si + j]);
|
||||
float by = midY - mn * (height * 0.45f);
|
||||
wavePath.lineTo ((float) bx + bounds.getX(), by);
|
||||
}
|
||||
wavePath.closeSubPath();
|
||||
}
|
||||
}
|
||||
|
||||
// Fill waveform
|
||||
g.setColour (waveColour.withAlpha (0.5f));
|
||||
g.fillPath (wavePath);
|
||||
g.setColour (waveColour.withAlpha (0.9f));
|
||||
g.strokePath (wavePath, juce::PathStrokeType (1.0f));
|
||||
|
||||
// Draw ADSR overlay
|
||||
if (showADSR)
|
||||
{
|
||||
float totalSeconds = (float) numSamples / (float) bufferSampleRate;
|
||||
float ax = adsrA / totalSeconds;
|
||||
float dx = adsrD / totalSeconds;
|
||||
float sx = 0.4f; // sustain portion
|
||||
float rx = adsrR / totalSeconds;
|
||||
float total = ax + dx + sx + rx;
|
||||
|
||||
// Normalize to width
|
||||
juce::Path adsrPath;
|
||||
float x0 = bounds.getX();
|
||||
float w = bounds.getWidth();
|
||||
|
||||
adsrPath.startNewSubPath (x0, bounds.getBottom());
|
||||
adsrPath.lineTo (x0 + (ax / total) * w, bounds.getY() + 4); // attack peak
|
||||
adsrPath.lineTo (x0 + ((ax + dx) / total) * w, midY - (adsrS - 0.5f) * height * 0.8f); // decay to sustain
|
||||
adsrPath.lineTo (x0 + ((ax + dx + sx) / total) * w, midY - (adsrS - 0.5f) * height * 0.8f); // sustain hold
|
||||
adsrPath.lineTo (x0 + w, bounds.getBottom()); // release to 0
|
||||
|
||||
g.setColour (InstaDrumsLookAndFeel::accent.withAlpha (0.7f));
|
||||
g.strokePath (adsrPath, juce::PathStrokeType (2.0f));
|
||||
}
|
||||
}
|
||||
26
Source/WaveformDisplay.h
Normal file
26
Source/WaveformDisplay.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class WaveformDisplay : public juce::Component
|
||||
{
|
||||
public:
|
||||
WaveformDisplay();
|
||||
|
||||
void setBuffer (const juce::AudioBuffer<float>* buffer, double sampleRate = 44100.0);
|
||||
void setColour (juce::Colour c) { waveColour = c; repaint(); }
|
||||
void setStartEnd (float start, float end) { startPos = start; endPos = end; repaint(); }
|
||||
void setADSR (float a, float d, float s, float r) { adsrA = a; adsrD = d; adsrS = s; adsrR = r; repaint(); }
|
||||
void setShowADSR (bool show) { showADSR = show; repaint(); }
|
||||
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
const juce::AudioBuffer<float>* audioBuffer = nullptr;
|
||||
double bufferSampleRate = 44100.0;
|
||||
juce::Colour waveColour { 0xffff8844 };
|
||||
float startPos = 0.0f, endPos = 1.0f;
|
||||
float adsrA = 0.001f, adsrD = 0.1f, adsrS = 1.0f, adsrR = 0.05f;
|
||||
bool showADSR = false;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveformDisplay)
|
||||
};
|
||||
Reference in New Issue
Block a user