monoslicer/Source/PluginProcessor.cpp

820 lines
28 KiB
C++

#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<juce::AudioParameterFloat>(
"bass", "Bass", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"treble", "Treble", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"sensitivity", "Sensitivity", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
// Amp ADSR
layout.add(std::make_unique<juce::AudioParameterFloat>(
"ampAttack", "Amp Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"ampDecay", "Amp Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.1f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"ampSustain", "Amp Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.7f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"ampRelease", "Amp Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.2f));
// Filter
juce::StringArray filterTypeNames { "LP12", "LP24", "HP", "BP", "Notch" };
layout.add(std::make_unique<juce::AudioParameterChoice>(
"filterType", "Filter Type", filterTypeNames, 0));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterCutoff", "Filter Cutoff", juce::NormalisableRange<float>(20.0f, 20000.0f, 1.0f, 0.3f), 20000.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterReso", "Filter Reso", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
// Filter ADSR
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterAttack", "Flt Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterDecay", "Flt Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.3f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterSustain", "Flt Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterRelease", "Flt Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.5f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"filterEnvDepth", "Flt Env Depth", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
layout.add(std::make_unique<juce::AudioParameterBool>(
"selectViaMidi", "Select Via MIDI", false));
layout.add(std::make_unique<juce::AudioParameterFloat>(
"stretchBpm", "Stretch BPM", juce::NormalisableRange<float>(20.0f, 300.0f, 1.0f), 120.0f));
juce::StringArray beatChoices { "1", "2", "4", "8", "16", "32" };
layout.add(std::make_unique<juce::AudioParameterChoice>(
"stretchBeats", "Stretch Beats", beatChoices, 2));
layout.add(std::make_unique<juce::AudioParameterInt>(
"keyShift", "Key Shift", -12, 12, 0));
layout.add(std::make_unique<juce::AudioParameterInt>(
"octaveOffset", "Octave Offset", -3, 3, 0));
juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" };
layout.add(std::make_unique<juce::AudioParameterChoice>(
"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<float>(sampleRate * 0.45), cutoff);
float Q = 0.5f + resonance * 19.5f; // Q range: 0.5 to 20
float w0 = 2.0f * juce::MathConstants<float>::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<int>(attack * currentSampleRate);
int decaySamples = static_cast<int>(decay * currentSampleRate);
int releaseSamples = static_cast<int>(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<float>(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<float>(voice.ampSamplesToNext) / static_cast<float>(decaySamples));
break;
case AmpStage::Sustain:
voice.ampEnv = sustain;
break;
case AmpStage::Release:
voice.ampEnv -= 1.0f / static_cast<float>(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<int>(attack * currentSampleRate);
int decaySamples = static_cast<int>(decay * currentSampleRate);
int releaseSamples = static_cast<int>(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<float>(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<float>(voice.filterSamplesToNext) / static_cast<float>(decaySamples));
break;
case AmpStage::Sustain:
voice.filterEnv = sustain;
break;
case AmpStage::Release:
voice.filterEnv -= 1.0f / static_cast<float>(releaseSamples);
if (voice.filterEnv <= 0.0f) voice.filterEnv = 0.0f;
break;
case AmpStage::Idle:
voice.filterEnv = 0.0f;
break;
}
}
}
void MonoslicerProcessor::processBlock(juce::AudioBuffer<float>& 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<float>(*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<int>(apvts.getRawParameterValue("filterType")->load());
FilterType filterType = static_cast<FilterType>(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<float>(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<float>(currentSampleRate),
b0, b1, b2, a1, a2);
// Key shift: pitch factor = 2^(semitones/12)
int keyShift = static_cast<int>(apvts.getRawParameterValue("keyShift")->load());
float pitchFactor = std::pow(2.0f, static_cast<float>(keyShift) / 12.0f);
const int bufSamples = sampleBuffer.getNumSamples();
bool filterActive = (envDepth > 0.001f)
|| (baseCutoff < static_cast<float>(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<float>(sliceLength))
break;
int idx = static_cast<int>(chPos);
float frac = chPos - static_cast<float>(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<float>(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<int>(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<int>(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<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
if (reader)
{
pushUndoState();
int numSamples = static_cast<int>(reader->lengthInSamples);
sampleBuffer.setSize(static_cast<int>(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<int>(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<juce::var> sliceStarts;
juce::Array<juce::var> 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<size_t>(numSamples) * sizeof(float));
}
}
void MonoslicerProcessor::setStateInformation(const void* data, int sizeInBytes)
{
auto stream = juce::MemoryInputStream(data, static_cast<size_t>(sizeInBytes), false);
auto state = juce::ValueTree::readFromStream(stream);
if (!state.isValid()) return;
apvts.replaceState(state);
loadedSampleRate = static_cast<double>(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<size_t>(totalFloats) * sizeof(float);
if (numChannels > 0 && numChannels <= 8 && numSamples > 0
&& static_cast<size_t>(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<size_t>(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<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);
}
}
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<double>(loadedSampleRate);
int currentLength = sampleBuffer.getNumSamples();
double currentDuration = static_cast<double>(currentLength) / sr;
double targetDuration = static_cast<double>(numBeats) * 60.0 / static_cast<double>(bpm);
double ratio = targetDuration / currentDuration;
if (std::abs(ratio - 1.0) < 0.001) return;
int newLength = static_cast<int>(static_cast<double>(currentLength) * ratio);
if (newLength < 1) return;
const int numChannels = sampleBuffer.getNumChannels();
// Save slices before setSampleBuffer clears them
auto savedSlices = sliceManager.getSlices();
juce::AudioBuffer<float> 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<double>(i) / ratio;
int idx = static_cast<int>(srcPos);
float frac = static_cast<float>(srcPos - static_cast<double>(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<int>(static_cast<double>(s.startSample) * ratio);
int newEnd = static_cast<int>(static_cast<double>(s.endSample) * ratio);
if (newEnd > newStart + 100)
sliceManager.addSliceManual(newStart);
}
selectedSlice.store(-1);
}