Fix LP24 filter with proper 2-stage cascade, thread-safe SliceManager, improve state save/load

This commit is contained in:
Armin 2026-07-21 23:15:37 +02:00
commit ac6f17d9ae
4 changed files with 109 additions and 58 deletions

View file

@ -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<float>& 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<float>& 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<size_t>(numSamples) * sizeof(float));
audioStream.flush();
state.setProperty("audioData", juce::var(juce::MemoryBlock(audioStream.getData(), audioStream.getDataSize())), nullptr);
stream.write(sampleBuffer.getReadPointer(ch),
static_cast<size_t>(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<double>(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<const uint8_t*>(audioBlock->getData());
size_t offset = 0;
int numChannels = stream.readInt();
int numSamples = stream.readInt();
int totalFloats = numChannels * numSamples;
size_t expectedBytes = static_cast<size_t>(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<size_t>(stream.getTotalLength() - stream.getPosition()) >= expectedBytes)
{
std::memcpy(sampleBuffer.getWritePointer(ch), buf + offset,
static_cast<size_t>(numSamples) * sizeof(float));
offset += static_cast<size_t>(numSamples) * sizeof(float);
}
sampleBuffer.setSize(numChannels, numSamples, false, false, false);
for (int ch = 0; ch < numChannels; ++ch)
stream.read(sampleBuffer.getWritePointer(ch),
static_cast<size_t>(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<int>((*starts)[i]));
std::vector<SliceManager::Slice> restored;
restored.reserve(static_cast<size_t>(n));
for (int i = 0; i < n; ++i)
restored.push_back({ static_cast<int>((*starts)[i]),
static_cast<int>((*ends)[i]), 0 });
sliceManager.setSlices(restored);
}
}