diff --git a/README.md b/README.md index 902ba46..b831495 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,16 @@ Free, open-source VST3 drum sampler plugin built with JUCE. - Reverb (Size, Decay) - Each FX toggleable with animated switches +### Multi-Output Routing +- 7 stereo output buses: Main, Kick, Snare, HiHat, Toms, Cymbals, Perc +- Each pad pre-assigned to its bus (configurable) +- In REAPER: enable additional outputs via routing for separate track processing +- Pads with inactive buses automatically fall back to Main + ### Master Bus - Master Volume, Tune, Pan - Output Limiter (0dB brickwall, toggleable) -- VU meter with peak hold +- Peak VU meter with hold indicator ### GUI - Dark modern UI inspired by hardware drum machines diff --git a/Source/DrumPad.h b/Source/DrumPad.h index f2c3e4a..7f94112 100644 --- a/Source/DrumPad.h +++ b/Source/DrumPad.h @@ -50,6 +50,7 @@ public: float pitch = 0.0f; bool oneShot = true; int chokeGroup = -1; + int outputBus = 0; // 0 = Main, 1 = Kick, 2 = Snare, etc. juce::Colour colour { 0xff00ff88 }; // ADSR diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index c14d596..5e88fc9 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -3,7 +3,13 @@ InstaDrumsProcessor::InstaDrumsProcessor() : AudioProcessor (BusesProperties() - .withOutput ("Main", juce::AudioChannelSet::stereo(), true)) + .withOutput ("Main", juce::AudioChannelSet::stereo(), true) + .withOutput ("Kick", juce::AudioChannelSet::stereo(), true) + .withOutput ("Snare", juce::AudioChannelSet::stereo(), true) + .withOutput ("HiHat", juce::AudioChannelSet::stereo(), true) + .withOutput ("Toms", juce::AudioChannelSet::stereo(), true) + .withOutput ("Cymbals", juce::AudioChannelSet::stereo(), true) + .withOutput ("Perc", juce::AudioChannelSet::stereo(), true)) { formatManager.registerBasicFormats(); initializeDefaults(); @@ -11,30 +17,52 @@ InstaDrumsProcessor::InstaDrumsProcessor() InstaDrumsProcessor::~InstaDrumsProcessor() {} +const char* const InstaDrumsProcessor::outputBusNames[numOutputBuses] = { + "Main", "Kick", "Snare", "HiHat", "Toms", "Cymbals", "Perc" +}; + +bool InstaDrumsProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + // Main output must be stereo + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + + // Aux outputs can be stereo or disabled + for (int i = 1; i < layouts.outputBuses.size(); ++i) + { + auto set = layouts.outputBuses[i]; + if (! set.isDisabled() && set != juce::AudioChannelSet::stereo()) + return false; + } + return true; +} + void InstaDrumsProcessor::initializeDefaults() { // GM Drum Map defaults for first 12 pads - struct PadDefault { int note; const char* name; juce::uint32 colour; }; + // Bus IDs: 0=Main, 1=Kick, 2=Snare, 3=HiHat, 4=Toms, 5=Cymbals, 6=Perc + struct PadDefault { int note; const char* name; juce::uint32 colour; int bus; }; 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 + { 36, "Kick", 0xffff4444, 1 }, // -> Kick bus + { 38, "Snare", 0xffff8844, 2 }, // -> Snare bus + { 42, "CH Hat", 0xffffff44, 3 }, // -> HiHat bus + { 46, "OH Hat", 0xff88ff44, 3 }, // -> HiHat bus + { 45, "Low Tom", 0xff44ffaa, 4 }, // -> Toms bus + { 48, "Mid Tom", 0xff44ddff, 4 }, // -> Toms bus + { 50, "Hi Tom", 0xff4488ff, 4 }, // -> Toms bus + { 49, "Crash", 0xff8844ff, 5 }, // -> Cymbals bus + { 51, "Ride", 0xffcc44ff, 5 }, // -> Cymbals bus + { 39, "Clap", 0xffff44cc, 6 }, // -> Perc bus + { 56, "Cowbell", 0xffff8888, 6 }, // -> Perc bus + { 37, "Rimshot", 0xffaaaaff, 6 }, // -> Perc bus }; 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); + pads[i].midiNote = defaults[i].note; + pads[i].name = defaults[i].name; + pads[i].colour = juce::Colour (defaults[i].colour); + pads[i].outputBus = defaults[i].bus; } } @@ -88,12 +116,55 @@ void InstaDrumsProcessor::processBlock (juce::AudioBuffer& buffer, juce:: } } - // Render audio from all pads - for (int i = 0; i < numActivePads; ++i) - pads[i].renderNextBlock (buffer, 0, buffer.getNumSamples()); + const int numSamples = buffer.getNumSamples(); + const int totalChannels = buffer.getNumChannels(); - // Apply master FX chain - applyMasterFx (buffer); + // Render each pad to its assigned output bus (or Main if bus inactive) + for (int i = 0; i < numActivePads; ++i) + { + int bus = pads[i].outputBus; + int chOffset = 0; + + // Calculate channel offset for the target bus + // Each bus is 2 channels (stereo): bus 0 = ch 0-1, bus 1 = ch 2-3, etc. + if (bus > 0) + { + int targetOffset = bus * 2; + if (targetOffset + 1 < totalChannels) + chOffset = targetOffset; // Bus is active, use its channels + // else: bus not active, chOffset stays 0 (Main) + } + + // Render pad into the correct channel pair + // We need a temporary view of the buffer at the right offset + if (chOffset == 0) + { + // Render to Main (channels 0-1) + pads[i].renderNextBlock (buffer, 0, numSamples); + } + else + { + // Create a sub-buffer pointing to the target bus channels + float* channelPtrs[2] = { + buffer.getWritePointer (chOffset), + buffer.getWritePointer (chOffset + 1) + }; + juce::AudioBuffer busBuffer (channelPtrs, 2, numSamples); + pads[i].renderNextBlock (busBuffer, 0, numSamples); + } + } + + // Apply master FX to Main bus only (channels 0-1) + if (totalChannels >= 2) + { + float* mainPtrs[2] = { buffer.getWritePointer (0), buffer.getWritePointer (1) }; + juce::AudioBuffer mainBuf (mainPtrs, 2, numSamples); + applyMasterFx (mainBuf); + } + else + { + applyMasterFx (buffer); + } } void InstaDrumsProcessor::applyMasterFx (juce::AudioBuffer& buffer) @@ -131,16 +202,15 @@ void InstaDrumsProcessor::applyMasterFx (juce::AudioBuffer& buffer) } } - // --- VU Meter --- - float rmsL = 0.0f, rmsR = 0.0f; + // --- VU Meter (peak level) --- + float peakL = 0.0f, peakR = 0.0f; if (buffer.getNumChannels() >= 1) - rmsL = buffer.getRMSLevel (0, 0, numSamples); + peakL = buffer.getMagnitude (0, 0, numSamples); if (buffer.getNumChannels() >= 2) - rmsR = buffer.getRMSLevel (1, 0, numSamples); + peakR = buffer.getMagnitude (1, 0, numSamples); - // Smooth VU (simple exponential) - vuLevelL.store (vuLevelL.load() * 0.8f + rmsL * 0.2f); - vuLevelR.store (vuLevelR.load() * 0.8f + rmsR * 0.2f); + vuLevelL.store (peakL); + vuLevelR.store (peakR); } DrumPad* InstaDrumsProcessor::findPadForNote (int midiNote) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 7b8d111..8dda339 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -21,8 +21,13 @@ public: const juce::String getName() const override { return JucePlugin_Name; } bool acceptsMidi() const override { return true; } bool producesMidi() const override { return true; } + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; double getTailLengthSeconds() const override { return 0.0; } + // Output bus names + static constexpr int numOutputBuses = 7; + static const char* const outputBusNames[numOutputBuses]; + int getNumPrograms() override { return 1; } int getCurrentProgram() override { return 0; } void setCurrentProgram (int) override {} diff --git a/Source/VuMeter.h b/Source/VuMeter.h index ac18053..fb2c395 100644 --- a/Source/VuMeter.h +++ b/Source/VuMeter.h @@ -12,8 +12,9 @@ public: if (right > peakR) peakR = right; else peakR *= 0.995f; - levelL = left; - levelR = right; + // Smooth level (fast attack, medium release) + levelL = std::max (left, levelL * 0.85f); + levelR = std::max (right, levelR * 0.85f); repaint(); } @@ -28,19 +29,6 @@ public: drawBar (g, leftBar, levelL, peakL); drawBar (g, rightBar, levelR, peakR); - - // Scale markers - g.setColour (juce::Colour (0x44ffffff)); - g.setFont (juce::FontOptions (9.0f)); - float totalH = getLocalBounds().toFloat().getHeight(); - // dB markers: 0dB = top, -6, -12, -24, -48 - float dbLevels[] = { 1.0f, 0.5f, 0.25f, 0.063f, 0.004f }; - const char* dbLabels[] = { "0", "-6", "-12", "-24", "-48" }; - for (int i = 0; i < 5; ++i) - { - float yPos = (1.0f - dbLevels[i]) * totalH; - g.drawHorizontalLine ((int) yPos, 0.0f, (float) getWidth()); - } } private: @@ -53,14 +41,15 @@ private: g.setColour (juce::Colour (0xff111122)); g.fillRoundedRectangle (bar, 2.0f); - float clampedLevel = juce::jlimit (0.0f, 1.0f, level); - float h = bar.getHeight() * clampedLevel; + // Scale level to dB-ish display (boost low levels for visibility) + 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); // Segmented colour - if (clampedLevel < 0.6f) + if (displayLevel < 0.6f) g.setColour (juce::Colour (0xff00cc44)); - else if (clampedLevel < 0.85f) + else if (displayLevel < 0.85f) g.setColour (juce::Colour (0xffcccc00)); else g.setColour (juce::Colour (0xffff3333)); @@ -68,10 +57,10 @@ private: g.fillRoundedRectangle (filled, 2.0f); // Peak hold line - float clampedPeak = juce::jlimit (0.0f, 1.0f, peak); - if (clampedPeak > 0.01f) + float displayPeak = std::pow (juce::jlimit (0.0f, 1.0f, peak), 0.5f); + if (displayPeak > 0.01f) { - float peakY = bar.getBottom() - bar.getHeight() * clampedPeak; + float peakY = bar.getBottom() - bar.getHeight() * displayPeak; g.setColour (juce::Colours::white.withAlpha (0.8f)); g.fillRect (bar.getX(), peakY, bar.getWidth(), 1.5f); }