From ac6f17d9ae25f473811587c25a2983ee4a997234 Mon Sep 17 00:00:00 2001 From: Armin Date: Tue, 21 Jul 2026 23:15:37 +0200 Subject: [PATCH] Fix LP24 filter with proper 2-stage cascade, thread-safe SliceManager, improve state save/load --- Source/PluginProcessor.cpp | 116 +++++++++++++++++++------------------ Source/PluginProcessor.h | 1 + Source/SliceManager.cpp | 41 +++++++++++++ Source/SliceManager.h | 9 ++- 4 files changed, 109 insertions(+), 58 deletions(-) diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index ab21e51..a5f8df3 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -121,25 +121,13 @@ void MonoSlicerProcessor::computeBiquadCoeffs(FilterType type, float cutoff, flo } case FilterType::LP24: { - // Two cascaded LP12 + // Single LP12 stage; cascaded in processBlock via filterL2/filterR2 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; + 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: @@ -384,10 +372,16 @@ void MonoSlicerProcessor::processBlock(juce::AudioBuffer& buffer, juce::M { 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; @@ -410,7 +404,12 @@ void MonoSlicerProcessor::processBlock(juce::AudioBuffer& buffer, juce::M float output; if (filterActive) - output = filt.process(dry) * voice.ampEnv; + { + output = filt.process(dry); + if (filterType == FilterType::LP24) + output = filt2.process(output); + output *= voice.ampEnv; + } else output = dry * voice.ampEnv; @@ -473,6 +472,8 @@ void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity) voice.filterL.reset(); voice.filterR.reset(); + voice.filterL2.reset(); + voice.filterR2.reset(); return; } } @@ -573,24 +574,22 @@ void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData) 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; - juce::MemoryOutputStream audioStream; - audioStream.writeInt(numChannels); - audioStream.writeInt(numSamples); + stream.writeInt(numChannels); + stream.writeInt(numSamples); for (int ch = 0; ch < numChannels; ++ch) - audioStream.write(sampleBuffer.getReadPointer(ch), - static_cast(numSamples) * sizeof(float)); - - audioStream.flush(); - state.setProperty("audioData", juce::var(juce::MemoryBlock(audioStream.getData(), audioStream.getDataSize())), nullptr); + stream.write(sampleBuffer.getReadPointer(ch), + static_cast(numSamples) * sizeof(float)); } - - juce::MemoryOutputStream stream(destData, false); - state.writeToStream(stream); } void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes) @@ -603,29 +602,32 @@ void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes) loadedSampleRate = static_cast(state.getProperty("sampleRate", 44100.0)); - auto audioVar = state.getProperty("audioData", {}); - auto* audioBlock = audioVar.getBinaryData(); - if (audioBlock != nullptr && audioBlock->getSize() > 0) + // Try to read embedded audio data appended after the ValueTree + bool loadedFromEmbedded = false; + if (stream.getTotalLength() - stream.getPosition() > 8) { - const auto* buf = static_cast(audioBlock->getData()); - size_t offset = 0; + int numChannels = stream.readInt(); + int numSamples = stream.readInt(); + int totalFloats = numChannels * numSamples; + size_t expectedBytes = static_cast(totalFloats) * sizeof(float); - int numChannels = juce::ByteOrder::bigEndianInt(buf + offset); offset += 4; - int numSamples = juce::ByteOrder::bigEndianInt(buf + offset); offset += 4; - - sampleBuffer.setSize(numChannels, numSamples, false, false, false); - for (int ch = 0; ch < numChannels; ++ch) + if (numChannels > 0 && numChannels <= 8 && numSamples > 0 + && static_cast(stream.getTotalLength() - stream.getPosition()) >= expectedBytes) { - std::memcpy(sampleBuffer.getWritePointer(ch), buf + offset, - static_cast(numSamples) * sizeof(float)); - offset += static_cast(numSamples) * sizeof(float); - } + 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); + currentFile = juce::File(state.getProperty("filePath", "").toString()); + refreshFolderList(); + sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); + loadedFromEmbedded = true; + } } - else + + // Fallback: load from file path + if (!loadedFromEmbedded) { juce::String path = state.getProperty("filePath", "").toString(); if (path.isNotEmpty()) @@ -637,16 +639,20 @@ void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes) } auto sliceStarts = state.getProperty("sliceStarts", {}); - if (sliceStarts.isArray()) + auto sliceEnds = state.getProperty("sliceEnds", {}); + if (sliceStarts.isArray() && sliceEnds.isArray()) { auto* starts = sliceStarts.getArray(); - int numSavedSlices = starts->size(); - - if (numSavedSlices > 0) + auto* ends = sliceEnds.getArray(); + int n = starts->size(); + if (n > 0 && ends->size() == n) { - sliceManager.clearSlices(); - for (int i = 1; i < numSavedSlices; ++i) - sliceManager.addSliceManual(static_cast((*starts)[i])); + 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); } } diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 717e188..a3ac892 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -109,6 +109,7 @@ private: int filterSamplesToNext = 0; BiquadState filterL, filterR; + BiquadState filterL2, filterR2; }; static constexpr int maxVoices = 16; diff --git a/Source/SliceManager.cpp b/Source/SliceManager.cpp index bf092ae..469a95f 100644 --- a/Source/SliceManager.cpp +++ b/Source/SliceManager.cpp @@ -2,8 +2,29 @@ SliceManager::SliceManager() = default; +std::vector SliceManager::getSlices() const +{ + const juce::CriticalSection::ScopedLockType lock(mutex); + return slices; +} + +int SliceManager::getNumSlices() const +{ + const juce::CriticalSection::ScopedLockType lock(mutex); + return static_cast(slices.size()); +} + +SliceManager::Slice SliceManager::getSlice(int index) const +{ + const juce::CriticalSection::ScopedLockType lock(mutex); + if (index < 0 || index >= static_cast(slices.size())) + return { 0, 0, 0 }; + return slices[static_cast(index)]; +} + void SliceManager::setSampleBuffer(juce::AudioBuffer* buffer, double sampleRate) { + const juce::CriticalSection::ScopedLockType lock(mutex); sampleBuffer = buffer; currentSampleRate = sampleRate; slices.clear(); @@ -15,6 +36,7 @@ void SliceManager::setTrebleGain(float g) { trebleGain = juce::jlimit(0.0f, 2. void SliceManager::autoSlice() { + const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; @@ -83,12 +105,21 @@ void SliceManager::autoSlice() void SliceManager::clearSlices() { + const juce::CriticalSection::ScopedLockType lock(mutex); pushHistory(); slices.clear(); } +void SliceManager::setSlices(const std::vector& newSlices) +{ + const juce::CriticalSection::ScopedLockType lock(mutex); + slices = newSlices; + assignMidiNotes(); +} + void SliceManager::addSliceManual(int samplePos) { + const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer) return; const int numSamples = sampleBuffer->getNumSamples(); samplePos = juce::jlimit(1, numSamples - 1, samplePos); @@ -127,13 +158,18 @@ void SliceManager::addSliceManual(int samplePos) void SliceManager::removeSliceAt(int samplePos, int tolerance) { + const juce::CriticalSection::ScopedLockType lock(mutex); for (auto it = slices.begin(); it != slices.end(); ++it) { int dist = std::abs(it->startSample - samplePos); if (dist <= tolerance) { pushHistory(); + int removedEnd = it->endSample; + auto idx = static_cast(std::distance(slices.begin(), it)); slices.erase(it); + if (idx > 0 && idx - 1 < static_cast(slices.size())) + slices[static_cast(idx - 1)].endSample = removedEnd; assignMidiNotes(); return; } @@ -142,6 +178,7 @@ void SliceManager::removeSliceAt(int samplePos, int tolerance) void SliceManager::moveSlice(int fromSample, int toSample) { + const juce::CriticalSection::ScopedLockType lock(mutex); for (size_t i = 0; i < slices.size(); ++i) { if (std::abs(slices[i].startSample - fromSample) < 10) @@ -160,6 +197,7 @@ void SliceManager::moveSlice(int fromSample, int toSample) void SliceManager::trimStart(int sample) { + const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample); if (sample <= 0) return; @@ -199,6 +237,7 @@ void SliceManager::trimStart(int sample) void SliceManager::trimEnd(int sample) { + const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; const int numSamples = sampleBuffer->getNumSamples(); sample = juce::jlimit(0, numSamples, sample); @@ -341,6 +380,7 @@ void SliceManager::truncateHistory() void SliceManager::undo() { + const juce::CriticalSection::ScopedLockType lock(mutex); if (undoStack.empty()) return; redoStack.push_back(slices); slices = undoStack.back(); @@ -349,6 +389,7 @@ void SliceManager::undo() void SliceManager::redo() { + const juce::CriticalSection::ScopedLockType lock(mutex); if (redoStack.empty()) return; undoStack.push_back(slices); slices = redoStack.back(); diff --git a/Source/SliceManager.h b/Source/SliceManager.h index 726be7f..1e222df 100644 --- a/Source/SliceManager.h +++ b/Source/SliceManager.h @@ -36,9 +36,11 @@ public: bool canUndo() const; bool canRedo() const; - const std::vector& getSlices() const { return slices; } - int getNumSlices() const { return static_cast(slices.size()); } - const Slice& getSlice(int index) const { return slices[static_cast(index)]; } + std::vector getSlices() const; + int getNumSlices() const; + Slice getSlice(int index) const; + + void setSlices(const std::vector& newSlices); int findSliceIndexForSample(int samplePos) const; @@ -50,6 +52,7 @@ private: void pushHistory(); void truncateHistory(); + mutable juce::CriticalSection mutex; std::vector slices; std::vector> undoStack; std::vector> redoStack;