#include "PluginProcessor.h" #include "PluginEditor.h" InstaDrumsProcessor::InstaDrumsProcessor() : AudioProcessor (BusesProperties() .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(); } 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 // 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, 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].outputBus = defaults[i].bus; } } void InstaDrumsProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; for (int i = 0; i < numActivePads; ++i) pads[i].prepareToPlay (sampleRate, samplesPerBlock); // Per-pad FX is prepared in DrumPad::prepareToPlay() } void InstaDrumsProcessor::releaseResources() { for (int i = 0; i < numActivePads; ++i) pads[i].releaseResources(); } void InstaDrumsProcessor::processBlock (juce::AudioBuffer& 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(); } } const int numSamples = buffer.getNumSamples(); const int totalChannels = buffer.getNumChannels(); // 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) { const int numSamples = buffer.getNumSamples(); // Per-pad FX is now in DrumPad::applyPadFx() // Master chain: just Volume + Pan + Output Limiter // --- Master Volume + Pan --- float mVol = masterVolume.load(); float mPan = masterPan.load(); float panPos = (mPan + 1.0f) * 0.5f; float mLGain = std::cos (panPos * juce::MathConstants::halfPi) * mVol; float mRGain = std::sin (panPos * juce::MathConstants::halfPi) * mVol; if (buffer.getNumChannels() >= 2) { buffer.applyGain (0, 0, numSamples, mLGain); buffer.applyGain (1, 0, numSamples, mRGain); } else { buffer.applyGain (mVol); } // --- Output Limiter (brickwall at 0dB) --- if (outputLimiterEnabled.load()) { for (int ch = 0; ch < buffer.getNumChannels(); ++ch) { float* data = buffer.getWritePointer (ch); for (int i = 0; i < numSamples; ++i) data[i] = juce::jlimit (-1.0f, 1.0f, data[i]); } } // --- VU Meter (peak level) --- float peakL = 0.0f, peakR = 0.0f; if (buffer.getNumChannels() >= 1) peakL = buffer.getMagnitude (0, 0, numSamples); if (buffer.getNumChannels() >= 2) peakR = buffer.getMagnitude (1, 0, numSamples); vuLevelL.store (peakL); vuLevelR.store (peakR); } 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 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 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(); }