#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(); sliceManager.setBeforeChangeCallback([this]() { pushUndoState(); }); } 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)); layout.add(std::make_unique( "octaveOffset", "Octave Offset", -3, 3, 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: { // Single LP12 stage; cascaded in processBlock via filterL2/filterR2 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::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; auto& filt2 = (ch == 0) ? voice.filterL2 : voice.filterR2; if (filterActive) { filt.b0 = b0; filt.b1 = b1; filt.b2 = b2; filt.a1 = a1; filt.a2 = a2; if (filterType == FilterType::LP24) { filt2.b0 = b0; filt2.b1 = b1; filt2.b2 = b2; filt2.a1 = a1; filt2.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); if (filterType == FilterType::LP24) output = filt2.process(output); output *= 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); float peakL = 0.0f; float peakR = 0.0f; if (buffer.getNumChannels() > 0) { auto* data = buffer.getReadPointer(0); for (int i = 0; i < buffer.getNumSamples(); ++i) peakL = juce::jmax(peakL, std::abs(data[i])); } if (buffer.getNumChannels() > 1) { auto* data = buffer.getReadPointer(1); for (int i = 0; i < buffer.getNumSamples(); ++i) peakR = juce::jmax(peakR, std::abs(data[i])); } else { peakR = peakL; } outputLevelL.store(peakL); outputLevelR.store(peakR); } void MonoslicerProcessor::handleNoteOn(int noteNumber, float velocity) { int offset = static_cast(apvts.getRawParameterValue("octaveOffset")->load()) * 12; int sliceIdx = noteNumber - 60 + offset; 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(); voice.filterL2.reset(); voice.filterR2.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) { pushUndoState(); 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::clearSample() { pushUndoState(); sampleBuffer.setSize(0, 0); currentFile = juce::File{}; folderFiles.clear(); currentFileIndex = -1; sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); selectedSlice.store(-1); } void MonoslicerProcessor::pushUndoState() { if (restoringFromUndo) return; undoStack.push_back({ currentFile, sliceManager.getSlices() }); if (static_cast(undoStack.size()) > maxUndoDepth) undoStack.erase(undoStack.begin()); redoStack.clear(); } void MonoslicerProcessor::undo() { if (undoStack.empty()) return; redoStack.push_back({ currentFile, sliceManager.getSlices() }); auto state = std::move(undoStack.back()); undoStack.pop_back(); restoringFromUndo = true; if (state.file.existsAsFile()) { loadAudioFile(state.file); sliceManager.setSlices(state.slices); } else { sampleBuffer.setSize(0, 0); currentFile = juce::File{}; folderFiles.clear(); currentFileIndex = -1; sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); selectedSlice.store(-1); } restoringFromUndo = false; } void MonoslicerProcessor::redo() { if (redoStack.empty()) return; undoStack.push_back({ currentFile, sliceManager.getSlices() }); auto state = std::move(redoStack.back()); redoStack.pop_back(); restoringFromUndo = true; if (state.file.existsAsFile()) { loadAudioFile(state.file); sliceManager.setSlices(state.slices); } else { sampleBuffer.setSize(0, 0); currentFile = juce::File{}; folderFiles.clear(); currentFileIndex = -1; sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); selectedSlice.store(-1); } restoringFromUndo = false; } bool MonoslicerProcessor::canUndo() const { return !undoStack.empty(); } bool MonoslicerProcessor::canRedo() const { return !redoStack.empty(); } 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(); state.setProperty("filePath", currentFile.getFullPathName(), nullptr); state.setProperty("sampleRate", loadedSampleRate, nullptr); juce::Array sliceStarts; juce::Array sliceEnds; for (const auto& s : sliceManager.getSlices()) { sliceStarts.add(s.startSample); sliceEnds.add(s.endSample); } state.setProperty("sliceStarts", sliceStarts, nullptr); state.setProperty("sliceEnds", sliceEnds, nullptr); juce::MemoryOutputStream stream(destData, false); state.writeToStream(stream); // Append raw audio data after the ValueTree if (sampleBuffer.getNumSamples() > 0) { int numChannels = sampleBuffer.getNumChannels(); int numSamples = sampleBuffer.getNumSamples(); int totalFloats = numChannels * numSamples; stream.writeInt(numChannels); stream.writeInt(numSamples); for (int ch = 0; ch < numChannels; ++ch) stream.write(sampleBuffer.getReadPointer(ch), static_cast(numSamples) * sizeof(float)); } } 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()) return; apvts.replaceState(state); loadedSampleRate = static_cast(state.getProperty("sampleRate", 44100.0)); // Try to read embedded audio data appended after the ValueTree bool loadedFromEmbedded = false; if (stream.getTotalLength() - stream.getPosition() > 8) { int numChannels = stream.readInt(); int numSamples = stream.readInt(); int totalFloats = numChannels * numSamples; size_t expectedBytes = static_cast(totalFloats) * sizeof(float); if (numChannels > 0 && numChannels <= 8 && numSamples > 0 && static_cast(stream.getTotalLength() - stream.getPosition()) >= expectedBytes) { sampleBuffer.setSize(numChannels, numSamples, false, false, false); for (int ch = 0; ch < numChannels; ++ch) stream.read(sampleBuffer.getWritePointer(ch), static_cast(numSamples) * sizeof(float)); currentFile = juce::File(state.getProperty("filePath", "").toString()); refreshFolderList(); sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); loadedFromEmbedded = true; } } // Fallback: load from file path if (!loadedFromEmbedded) { juce::String path = state.getProperty("filePath", "").toString(); if (path.isNotEmpty()) { juce::File file(path); if (file.existsAsFile()) loadAudioFile(file); } } auto sliceStarts = state.getProperty("sliceStarts", {}); auto sliceEnds = state.getProperty("sliceEnds", {}); if (sliceStarts.isArray() && sliceEnds.isArray()) { auto* starts = sliceStarts.getArray(); auto* ends = sliceEnds.getArray(); int n = starts->size(); if (n > 0 && ends->size() == n) { std::vector restored; restored.reserve(static_cast(n)); for (int i = 0; i < n; ++i) restored.push_back({ static_cast((*starts)[i]), static_cast((*ends)[i]), 0 }); sliceManager.setSlices(restored); } } stateRestored.store(true); } 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; pushUndoState(); 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); }