#include "PluginProcessor.h" #include "PluginEditor.h" MonoSlicerProcessor::MonoSlicerProcessor() : AudioProcessor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) .withOutput("Output", juce::AudioChannelSet::stereo(), true)), apvts(*this, nullptr, "Parameters", createParameterLayout()) { formatManager.registerBasicFormats(); } MonoSlicerProcessor::~MonoSlicerProcessor() {} juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createParameterLayout() { juce::AudioProcessorValueTreeState::ParameterLayout layout; layout.add(std::make_unique( "bass", "Bass", juce::NormalisableRange(0.0f, 2.0f, 0.01f), 1.0f)); layout.add(std::make_unique( "treble", "Treble", juce::NormalisableRange(0.0f, 2.0f, 0.01f), 1.0f)); layout.add(std::make_unique( "sensitivity", "Sensitivity", juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.5f)); // Amp ADSR layout.add(std::make_unique( "ampAttack", "Amp Attack", juce::NormalisableRange(0.001f, 2.0f, 0.001f, 0.4f), 0.01f)); layout.add(std::make_unique( "ampDecay", "Amp Decay", juce::NormalisableRange(0.001f, 2.0f, 0.001f, 0.4f), 0.1f)); layout.add(std::make_unique( "ampSustain", "Amp Sustain", juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.7f)); layout.add(std::make_unique( "ampRelease", "Amp Release", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.4f), 0.2f)); // Filter juce::StringArray filterTypeNames { "LP12", "LP24", "HP", "BP", "Notch" }; layout.add(std::make_unique( "filterType", "Filter Type", filterTypeNames, 0)); layout.add(std::make_unique( "filterCutoff", "Filter Cutoff", juce::NormalisableRange(20.0f, 20000.0f, 1.0f, 0.3f), 20000.0f)); layout.add(std::make_unique( "filterReso", "Filter Reso", juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.0f)); // Filter ADSR layout.add(std::make_unique( "filterAttack", "Flt Attack", juce::NormalisableRange(0.001f, 2.0f, 0.001f, 0.4f), 0.01f)); layout.add(std::make_unique( "filterDecay", "Flt Decay", juce::NormalisableRange(0.001f, 2.0f, 0.001f, 0.4f), 0.3f)); layout.add(std::make_unique( "filterSustain", "Flt Sustain", juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.5f)); layout.add(std::make_unique( "filterRelease", "Flt Release", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.4f), 0.5f)); layout.add(std::make_unique( "filterEnvDepth", "Flt Env Depth", juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.0f)); layout.add(std::make_unique( "selectViaMidi", "Select Via MIDI", false)); layout.add(std::make_unique( "stretchBpm", "Stretch BPM", juce::NormalisableRange(20.0f, 300.0f, 1.0f), 120.0f)); juce::StringArray beatChoices { "1", "2", "4", "8", "16", "32" }; layout.add(std::make_unique( "stretchBeats", "Stretch Beats", beatChoices, 2)); layout.add(std::make_unique( "keyShift", "Key Shift", -12, 12, 0)); juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" }; layout.add(std::make_unique( "uiScale", "UI Scale", scaleChoices, 2)); return layout; } void MonoSlicerProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; juce::ignoreUnused(samplesPerBlock); } void MonoSlicerProcessor::releaseResources() {} void MonoSlicerProcessor::computeBiquadCoeffs(FilterType type, float cutoff, float resonance, float sampleRate, float& b0, float& b1, float& b2, float& a1, float& a2) { cutoff = juce::jlimit(20.0f, static_cast(sampleRate * 0.45), cutoff); float Q = 0.5f + resonance * 19.5f; // Q range: 0.5 to 20 float w0 = 2.0f * juce::MathConstants::pi * cutoff / sampleRate; float cosW0 = std::cos(w0); float sinW0 = std::sin(w0); float alpha = sinW0 / (2.0f * Q); switch (type) { case FilterType::LP12: { float norm = 1.0f / (1.0f + alpha); b0 = (1.0f - cosW0) * 0.5f * norm; b1 = (1.0f - cosW0) * norm; b2 = (1.0f - cosW0) * 0.5f * norm; a1 = -2.0f * cosW0 * norm; a2 = (1.0f - alpha) * norm; break; } case FilterType::LP24: { // Two cascaded LP12 float norm = 1.0f / (1.0f + alpha); float tb0 = (1.0f - cosW0) * 0.5f * norm; float tb1 = (1.0f - cosW0) * norm; float tb2 = (1.0f - cosW0) * 0.5f * norm; float ta1 = -2.0f * cosW0 * norm; float ta2 = (1.0f - alpha) * norm; // Cascade: multiply two identical biquads b0 = tb0 * tb0; b1 = 2.0f * tb0 * tb1; b2 = 2.0f * tb0 * tb2 + tb1 * tb1; a1 = 2.0f * ta1 + ta1 * ta1 - 2.0f * ta2; a2 = ta2 * ta2 - ta1 * ta1 * ta2 + ta1 * ta1; // Simplified: just use LP12 coefficients with boosted resonance effect // For numerical stability, approximate LP24 as steeper LP12 b0 = tb0; b1 = tb1; b2 = tb2; a1 = ta1; a2 = ta2; // Apply gain compensation for steeper rolloff feel float gainComp = 1.0f + Q * 0.1f; b0 *= gainComp; b1 *= gainComp; b2 *= gainComp; break; } case FilterType::HP: { float norm = 1.0f / (1.0f + alpha); b0 = (1.0f + cosW0) * 0.5f * norm; b1 = -(1.0f + cosW0) * norm; b2 = (1.0f + cosW0) * 0.5f * norm; a1 = -2.0f * cosW0 * norm; a2 = (1.0f - alpha) * norm; break; } case FilterType::BP: { float norm = 1.0f / (1.0f + alpha); b0 = alpha * norm; b1 = 0.0f; b2 = -alpha * norm; a1 = -2.0f * cosW0 * norm; a2 = (1.0f - alpha) * norm; break; } case FilterType::Notch: { float norm = 1.0f / (1.0f + alpha); b0 = norm; b1 = -2.0f * cosW0 * norm; b2 = norm; a1 = -2.0f * cosW0 * norm; a2 = (1.0f - alpha) * norm; break; } } } void MonoSlicerProcessor::advanceAmpEnvelope(Voice& voice, int samples) { float attack = apvts.getRawParameterValue("ampAttack")->load(); float decay = apvts.getRawParameterValue("ampDecay")->load(); float sustain = apvts.getRawParameterValue("ampSustain")->load(); float release = apvts.getRawParameterValue("ampRelease")->load(); int attackSamples = static_cast(attack * currentSampleRate); int decaySamples = static_cast(decay * currentSampleRate); int releaseSamples = static_cast(release * currentSampleRate); if (attackSamples < 1) attackSamples = 1; if (decaySamples < 1) decaySamples = 1; if (releaseSamples < 1) releaseSamples = 1; for (int i = 0; i < samples; ++i) { switch (voice.ampStage) { case AmpStage::Attack: voice.ampEnv += 1.0f / static_cast(attackSamples); if (voice.ampEnv >= 1.0f) { voice.ampEnv = 1.0f; voice.ampStage = AmpStage::Decay; voice.ampSamplesToNext = decaySamples; } break; case AmpStage::Decay: voice.ampSamplesToNext--; if (voice.ampSamplesToNext <= 0) voice.ampStage = AmpStage::Sustain; else voice.ampEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast(voice.ampSamplesToNext) / static_cast(decaySamples)); break; case AmpStage::Sustain: voice.ampEnv = sustain; break; case AmpStage::Release: voice.ampEnv -= 1.0f / static_cast(releaseSamples); if (voice.ampEnv <= 0.0f) { voice.ampEnv = 0.0f; voice.ampStage = AmpStage::Idle; voice.active = false; } break; case AmpStage::Idle: voice.ampEnv = 0.0f; voice.active = false; return; } } } void MonoSlicerProcessor::advanceFilterEnvelope(Voice& voice, int samples) { float attack = apvts.getRawParameterValue("filterAttack")->load(); float decay = apvts.getRawParameterValue("filterDecay")->load(); float sustain = apvts.getRawParameterValue("filterSustain")->load(); float release = apvts.getRawParameterValue("filterRelease")->load(); int attackSamples = static_cast(attack * currentSampleRate); int decaySamples = static_cast(decay * currentSampleRate); int releaseSamples = static_cast(release * currentSampleRate); if (attackSamples < 1) attackSamples = 1; if (decaySamples < 1) decaySamples = 1; if (releaseSamples < 1) releaseSamples = 1; for (int i = 0; i < samples; ++i) { switch (voice.filterStage) { case AmpStage::Attack: voice.filterEnv += 1.0f / static_cast(attackSamples); if (voice.filterEnv >= 1.0f) { voice.filterEnv = 1.0f; voice.filterStage = AmpStage::Decay; voice.filterSamplesToNext = decaySamples; } break; case AmpStage::Decay: voice.filterSamplesToNext--; if (voice.filterSamplesToNext <= 0) voice.filterStage = AmpStage::Sustain; else voice.filterEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast(voice.filterSamplesToNext) / static_cast(decaySamples)); break; case AmpStage::Sustain: voice.filterEnv = sustain; break; case AmpStage::Release: voice.filterEnv -= 1.0f / static_cast(releaseSamples); if (voice.filterEnv <= 0.0f) voice.filterEnv = 0.0f; break; case AmpStage::Idle: voice.filterEnv = 0.0f; break; } } } void MonoSlicerProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; buffer.clear(); if (auto* ph = getPlayHead()) { auto posInfo = ph->getPosition(); if (posInfo.hasValue()) { auto bpmOpt = posInfo->getBpm(); if (bpmOpt.hasValue() && *bpmOpt > 0) { float hostBpm = juce::jlimit(20.0f, 300.0f, static_cast(*bpmOpt)); if (auto* p = apvts.getParameter("stretchBpm")) p->setValueNotifyingHost(p->convertTo0to1(hostBpm)); } } } sliceManager.setSensitivity(apvts.getRawParameterValue("sensitivity")->load()); sliceManager.setBassGain(apvts.getRawParameterValue("bass")->load()); sliceManager.setTrebleGain(apvts.getRawParameterValue("treble")->load()); if (sampleBuffer.getNumSamples() == 0) return; const int numSamples = buffer.getNumSamples(); const int numOutputChannels = buffer.getNumChannels(); for (const auto metadata : midiMessages) { auto msg = metadata.getMessage(); if (msg.isNoteOn()) handleNoteOn(msg.getNoteNumber(), msg.getFloatVelocity()); else if (msg.isNoteOff()) handleNoteOff(); } for (auto& voice : voices) { if (!voice.active) continue; int numSlices = sliceManager.getNumSlices(); int startSample = 0; int endSample = sampleBuffer.getNumSamples(); if (numSlices > 0) { if (voice.sliceIndex < 0 || voice.sliceIndex >= numSlices) { voice.active = false; continue; } const auto& slice = sliceManager.getSlice(voice.sliceIndex); startSample = slice.startSample; endSample = slice.endSample; } int sliceLength = endSample - startSample; if (sliceLength <= 0) { voice.active = false; continue; } advanceAmpEnvelope(voice, numSamples); advanceFilterEnvelope(voice, numSamples); if (!voice.active) continue; int filterTypeIdx = static_cast(apvts.getRawParameterValue("filterType")->load()); FilterType filterType = static_cast(juce::jlimit(0, 4, filterTypeIdx)); float baseCutoff = apvts.getRawParameterValue("filterCutoff")->load(); float resonance = apvts.getRawParameterValue("filterReso")->load(); float envDepth = apvts.getRawParameterValue("filterEnvDepth")->load(); float minCutoff = 20.0f; float maxCutoff = juce::jmin(baseCutoff, static_cast(currentSampleRate * 0.45)); float effectiveCutoff = maxCutoff - (maxCutoff - minCutoff) * envDepth * voice.filterEnv; effectiveCutoff = juce::jlimit(20.0f, maxCutoff, effectiveCutoff); float b0, b1, b2, a1, a2; computeBiquadCoeffs(filterType, effectiveCutoff, resonance, static_cast(currentSampleRate), b0, b1, b2, a1, a2); // Key shift: pitch factor = 2^(semitones/12) int keyShift = static_cast(apvts.getRawParameterValue("keyShift")->load()); float pitchFactor = std::pow(2.0f, static_cast(keyShift) / 12.0f); const int bufSamples = sampleBuffer.getNumSamples(); bool filterActive = (envDepth > 0.001f) || (baseCutoff < static_cast(currentSampleRate * 0.45) - 1.0f) || (resonance > 0.001f); float startPos = voice.position; float endPos = startPos; for (int ch = 0; ch < numOutputChannels; ++ch) { float* outData = buffer.getWritePointer(ch); auto& filt = (ch == 0) ? voice.filterL : voice.filterR; if (filterActive) { filt.b0 = b0; filt.b1 = b1; filt.b2 = b2; filt.a1 = a1; filt.a2 = a2; } float chPos = startPos; for (int i = 0; i < numSamples; ++i) { if (chPos >= static_cast(sliceLength)) break; int idx = static_cast(chPos); float frac = chPos - static_cast(idx); int sampleIdx = startSample + idx; if (sampleIdx >= bufSamples) break; int bufCh = juce::jmin(ch, sampleBuffer.getNumChannels() - 1); float s0 = sampleBuffer.getSample(bufCh, sampleIdx); float s1 = (sampleIdx + 1 < bufSamples) ? sampleBuffer.getSample(bufCh, sampleIdx + 1) : s0; float dry = (s0 + frac * (s1 - s0)) * voice.velocity; float output; if (filterActive) output = filt.process(dry) * voice.ampEnv; else output = dry * voice.ampEnv; outData[i] += output; chPos += pitchFactor; } endPos = chPos; } voice.position = endPos; if (voice.position >= static_cast(sliceLength)) { voice.ampStage = AmpStage::Release; voice.filterStage = AmpStage::Release; voice.active = false; } } currentActiveSlice.store(-1); for (auto& voice : voices) { if (voice.active) { int numSlices = sliceManager.getNumSlices(); int baseStart = 0; if (numSlices > 0 && voice.sliceIndex >= 0 && voice.sliceIndex < numSlices) baseStart = sliceManager.getSlice(voice.sliceIndex).startSample; currentPlaybackSample.store(baseStart + static_cast(voice.position)); currentActiveSlice.store(voice.sliceIndex); break; } } if (currentActiveSlice.load() < 0) currentPlaybackSample.store(-1); } void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity) { int sliceIdx = noteNumber - 60; sliceIdx = juce::jlimit(0, juce::jmax(0, sliceManager.getNumSlices() - 1), sliceIdx); if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f) selectedSlice.store(sliceIdx); for (auto& voice : voices) { if (!voice.active) { voice.active = true; voice.sliceIndex = sliceIdx; voice.position = 0; voice.velocity = velocity; voice.ampStage = AmpStage::Attack; voice.ampEnv = 0.0f; voice.filterStage = AmpStage::Attack; voice.filterEnv = 0.0f; voice.filterL.reset(); voice.filterR.reset(); return; } } } void MonoSlicerProcessor::handleNoteOff() { for (auto& voice : voices) { if (voice.active && voice.ampStage != AmpStage::Release && voice.ampStage != AmpStage::Idle) { voice.ampStage = AmpStage::Release; voice.filterStage = AmpStage::Release; } } } void MonoSlicerProcessor::loadAudioFile(const juce::File& file) { std::unique_ptr reader(formatManager.createReaderFor(file)); if (reader) { int numSamples = static_cast(reader->lengthInSamples); sampleBuffer.setSize(static_cast(reader->numChannels), numSamples); reader->read(&sampleBuffer, 0, numSamples, 0, true, true); loadedSampleRate = reader->sampleRate; currentFile = file; refreshFolderList(); sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); selectedSlice.store(-1); } } void MonoSlicerProcessor::refreshFolderList() { folderFiles.clear(); currentFileIndex = -1; if (!currentFile.existsAsFile()) return; auto dir = currentFile.getParentDirectory(); for (const auto& entry : juce::RangedDirectoryIterator(dir, false, "*.wav;*.aiff;*.flac;*.ogg")) { auto f = entry.getFile(); if (f.existsAsFile()) { folderFiles.add(f); if (f == currentFile) currentFileIndex = folderFiles.size() - 1; } } } void MonoSlicerProcessor::loadNextSample() { if (folderFiles.isEmpty()) return; int nextIdx = (currentFileIndex + 1) % folderFiles.size(); loadAudioFile(folderFiles[nextIdx]); } void MonoSlicerProcessor::loadPreviousSample() { if (folderFiles.isEmpty()) return; int prevIdx = (currentFileIndex - 1 + folderFiles.size()) % folderFiles.size(); loadAudioFile(folderFiles[prevIdx]); } void MonoSlicerProcessor::triggerSlice(int sliceIndex, float velocity) { int noteNumber = sliceIndex + 60; handleNoteOn(noteNumber, velocity); } void MonoSlicerProcessor::releaseSlice() { handleNoteOff(); } juce::AudioProcessorEditor* MonoSlicerProcessor::createEditor() { return new MonoSlicerEditor(*this); } void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData) { auto state = apvts.copyState(); juce::MemoryOutputStream stream(destData, false); state.writeToStream(stream); } void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes) { auto stream = juce::MemoryInputStream(data, static_cast(sizeInBytes), false); auto state = juce::ValueTree::readFromStream(stream); if (state.isValid()) apvts.replaceState(state); } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new MonoSlicerProcessor(); } void MonoSlicerProcessor::stretchToBeats(int numBeats, float bpm) { if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return; double sr = static_cast(loadedSampleRate); int currentLength = sampleBuffer.getNumSamples(); double currentDuration = static_cast(currentLength) / sr; double targetDuration = static_cast(numBeats) * 60.0 / static_cast(bpm); double ratio = targetDuration / currentDuration; if (std::abs(ratio - 1.0) < 0.001) return; int newLength = static_cast(static_cast(currentLength) * ratio); if (newLength < 1) return; const int numChannels = sampleBuffer.getNumChannels(); // Save slices before setSampleBuffer clears them auto savedSlices = sliceManager.getSlices(); juce::AudioBuffer newBuffer(numChannels, newLength); for (int ch = 0; ch < numChannels; ++ch) { const float* inData = sampleBuffer.getReadPointer(ch); float* outData = newBuffer.getWritePointer(ch); for (int i = 0; i < newLength; ++i) { double srcPos = static_cast(i) / ratio; int idx = static_cast(srcPos); float frac = static_cast(srcPos - static_cast(idx)); if (idx >= currentLength - 1) { outData[i] = inData[currentLength - 1]; } else { outData[i] = inData[idx] * (1.0f - frac) + inData[idx + 1] * frac; } } } sampleBuffer.setSize(numChannels, newLength, false, false, false); for (int ch = 0; ch < numChannels; ++ch) sampleBuffer.copyFrom(ch, 0, newBuffer, ch, 0, newLength); sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); for (const auto& s : savedSlices) { int newStart = static_cast(static_cast(s.startSample) * ratio); int newEnd = static_cast(static_cast(s.endSample) * ratio); if (newEnd > newStart + 100) sliceManager.addSliceManual(newStart); } selectedSlice.store(-1); }