#include "DrumPad.h" DrumPad::DrumPad() {} DrumPad::~DrumPad() {} void DrumPad::prepareToPlay (double sr, int samplesPerBlock) { sampleRate = sr; // Prepare per-pad filter juce::dsp::ProcessSpec spec { sr, (juce::uint32) samplesPerBlock, 1 }; filterL.prepare (spec); filterR.prepare (spec); filterL.reset(); filterR.reset(); lastCutoff = filterCutoff; } 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 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 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> 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 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 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& 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& 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::halfPi); float rightGain = std::sin (panPos * juce::MathConstants::halfPi); // Update filter coefficients if cutoff changed if (std::abs (filterCutoff - lastCutoff) > 1.0f || std::abs (filterReso - filterL.coefficients->coefficients[0]) > 0.01f) { float clampedCutoff = juce::jlimit (20.0f, (float) (sampleRate * 0.49), filterCutoff); auto coeffs = juce::dsp::IIR::Coefficients::makeLowPass (sampleRate, clampedCutoff, filterReso); *filterL.coefficients = *coeffs; *filterR.coefficients = *coeffs; lastCutoff = filterCutoff; } bool useFilter = filterCutoff < 19900.0f; // Skip filter if fully open 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); // Apply per-pad filter if (useFilter) sampleVal = (ch == 0) ? filterL.processSample (sampleVal) : filterR.processSample (sampleVal); float channelGain = (ch == 0) ? leftGain : rightGain; outputBuffer.addSample (ch, startSample + i, sampleVal * gain * channelGain); } } readPosition += pitchRatio; } }