monoslicer/Source/SliceManager.cpp
2026-07-21 02:06:45 +02:00

359 lines
11 KiB
C++

#include "SliceManager.h"
SliceManager::SliceManager() = default;
void SliceManager::setSampleBuffer(juce::AudioBuffer<float>* buffer, double sampleRate)
{
sampleBuffer = buffer;
currentSampleRate = sampleRate;
slices.clear();
}
void SliceManager::setSensitivity(float s) { sensitivity = juce::jlimit(0.0f, 1.0f, s); }
void SliceManager::setBassGain(float g) { bassGain = juce::jlimit(0.0f, 2.0f, g); }
void SliceManager::setTrebleGain(float g) { trebleGain = juce::jlimit(0.0f, 2.0f, g); }
void SliceManager::autoSlice()
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
return;
pushHistory();
const int numSamples = sampleBuffer->getNumSamples();
const int numChannels = sampleBuffer->getNumChannels();
// Mix down to mono
std::vector<float> mono(static_cast<size_t>(numSamples), 0.0f);
for (int ch = 0; ch < numChannels; ++ch)
{
const float* data = sampleBuffer->getReadPointer(ch);
for (int i = 0; i < numSamples; ++i)
mono[static_cast<size_t>(i)] += data[i] / static_cast<float>(numChannels);
}
// Apply bass/treble shaping
float bassCutoff = 200.0f;
float trebleCutoff = 3000.0f;
auto shaped = mono;
if (bassGain != 1.0f)
{
auto bassComponent = lowpassFilter(mono, bassCutoff);
for (int i = 0; i < numSamples; ++i)
shaped[static_cast<size_t>(i)] += (bassComponent[static_cast<size_t>(i)] - mono[static_cast<size_t>(i)]) * (bassGain - 1.0f);
}
if (trebleGain != 1.0f)
{
auto trebleComponent = highpassFilter(mono, trebleCutoff);
for (int i = 0; i < numSamples; ++i)
shaped[static_cast<size_t>(i)] += (trebleComponent[static_cast<size_t>(i)] - mono[static_cast<size_t>(i)]) * (trebleGain - 1.0f);
}
// Compute envelope
auto envelope = computeEnvelope(shaped.data(), numSamples);
// Detect onsets
float threshold = 0.05f + (1.0f - sensitivity) * 0.45f;
auto onsets = detectOnsets(envelope, threshold);
slices.clear();
if (onsets.empty())
{
// Fallback: single slice
slices.push_back({ 0, numSamples, 0 });
}
else
{
// Add beginning if not close to first onset
if (onsets[0] > static_cast<float>(numSamples / 100))
onsets.insert(onsets.begin(), 0);
for (size_t i = 0; i < onsets.size(); ++i)
{
int start = static_cast<int>(onsets[i]);
int end = (i + 1 < onsets.size()) ? static_cast<int>(onsets[i + 1]) : numSamples;
if (end > start + 100) // minimum slice length
slices.push_back({ start, end, 0 });
}
}
assignMidiNotes();
}
void SliceManager::clearSlices()
{
pushHistory();
slices.clear();
}
void SliceManager::addSliceManual(int samplePos)
{
if (!sampleBuffer) return;
const int numSamples = sampleBuffer->getNumSamples();
samplePos = juce::jlimit(1, numSamples - 1, samplePos);
if (slices.empty())
{
pushHistory();
slices.push_back({ 0, numSamples, 0 });
assignMidiNotes();
return;
}
int idx = findSliceIndexForSample(samplePos);
if (idx < 0)
{
pushHistory();
slices.push_back({ samplePos, numSamples, 0 });
sortSlices();
assignMidiNotes();
return;
}
auto& parent = slices[static_cast<size_t>(idx)];
if (samplePos <= parent.startSample || samplePos >= parent.endSample)
return;
pushHistory();
int oldEnd = parent.endSample;
parent.endSample = samplePos;
slices.insert(slices.begin() + idx + 1, { samplePos, oldEnd, 0 });
assignMidiNotes();
}
void SliceManager::removeSliceAt(int samplePos, int tolerance)
{
for (auto it = slices.begin(); it != slices.end(); ++it)
{
int dist = std::abs(it->startSample - samplePos);
if (dist <= tolerance)
{
pushHistory();
slices.erase(it);
assignMidiNotes();
return;
}
}
}
void SliceManager::moveSlice(int fromSample, int toSample)
{
for (size_t i = 0; i < slices.size(); ++i)
{
if (std::abs(slices[i].startSample - fromSample) < 10)
{
pushHistory();
slices[i].startSample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, toSample);
if (i > 0)
slices[i - 1].endSample = slices[i].startSample;
slices[i].endSample = juce::jmax(slices[i].endSample, slices[i].startSample + 1);
sortSlices();
assignMidiNotes();
return;
}
}
}
void SliceManager::trimStart(int sample)
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample);
if (sample <= 0) return;
pushHistory();
const int numChannels = sampleBuffer->getNumChannels();
const int oldNumSamples = sampleBuffer->getNumSamples();
const int newNumSamples = oldNumSamples - sample;
juce::AudioBuffer<float> newBuffer(numChannels, newNumSamples);
for (int ch = 0; ch < numChannels; ++ch)
newBuffer.copyFrom(ch, 0, *sampleBuffer, ch, sample, newNumSamples);
sampleBuffer->setSize(numChannels, newNumSamples, false, false, false);
for (int ch = 0; ch < numChannels; ++ch)
sampleBuffer->copyFrom(ch, 0, newBuffer, ch, 0, newNumSamples);
for (auto& s : slices)
{
s.startSample -= sample;
s.endSample -= sample;
}
auto it = std::remove_if(slices.begin(), slices.end(),
[](const Slice& s) { return s.endSample <= 0 || s.startSample < 0; });
slices.erase(it, slices.end());
for (auto& s : slices)
{
s.startSample = juce::jmax(0, s.startSample);
s.endSample = juce::jmin(newNumSamples, s.endSample);
}
assignMidiNotes();
}
void SliceManager::trimEnd(int sample)
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
const int numSamples = sampleBuffer->getNumSamples();
sample = juce::jlimit(0, numSamples, sample);
if (sample >= numSamples) return;
pushHistory();
const int numChannels = sampleBuffer->getNumChannels();
juce::AudioBuffer<float> newBuffer(numChannels, sample);
for (int ch = 0; ch < numChannels; ++ch)
newBuffer.copyFrom(ch, 0, *sampleBuffer, ch, 0, sample);
sampleBuffer->setSize(numChannels, sample, false, false, false);
for (int ch = 0; ch < numChannels; ++ch)
sampleBuffer->copyFrom(ch, 0, newBuffer, ch, 0, sample);
auto it = std::remove_if(slices.begin(), slices.end(),
[sample](const Slice& s) { return s.startSample >= sample; });
slices.erase(it, slices.end());
for (auto& s : slices)
s.endSample = juce::jmin(sample, s.endSample);
assignMidiNotes();
}
int SliceManager::findSliceIndexForSample(int samplePos) const
{
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
if (samplePos >= slices[static_cast<size_t>(i)].startSample && samplePos < slices[static_cast<size_t>(i)].endSample)
return i;
}
return -1;
}
void SliceManager::sortSlices()
{
std::sort(slices.begin(), slices.end(),
[](const Slice& a, const Slice& b) { return a.startSample < b.startSample; });
}
void SliceManager::assignMidiNotes()
{
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
slices[static_cast<size_t>(i)].midiNote = i % 12;
}
std::vector<float> SliceManager::computeEnvelope(const float* data, int numSamples)
{
const int windowSize = juce::jmax(1, static_cast<int>(currentSampleRate * 0.01)); // 10ms window
const int numWindows = (numSamples + windowSize - 1) / windowSize;
std::vector<float> envelope(static_cast<size_t>(numWindows), 0.0f);
for (int w = 0; w < numWindows; ++w)
{
int start = w * windowSize;
int end = juce::jmin(start + windowSize, numSamples);
float sum = 0.0f;
for (int i = start; i < end; ++i)
sum += data[i] * data[i];
envelope[static_cast<size_t>(w)] = std::sqrt(sum / static_cast<float>(end - start));
}
return envelope;
}
std::vector<float> SliceManager::lowpassFilter(const std::vector<float>& input, float cutoffHz)
{
std::vector<float> output(input.size());
float rc = 1.0f / (2.0f * juce::MathConstants<float>::pi * cutoffHz);
float dt = 1.0f / static_cast<float>(currentSampleRate);
float alpha = dt / (rc + dt);
output[0] = input[0];
for (size_t i = 1; i < input.size(); ++i)
output[i] = output[i - 1] + alpha * (input[i] - output[i - 1]);
return output;
}
std::vector<float> SliceManager::highpassFilter(const std::vector<float>& input, float cutoffHz)
{
auto lowpassed = lowpassFilter(input, cutoffHz);
std::vector<float> output(input.size());
for (size_t i = 0; i < input.size(); ++i)
output[i] = input[i] - lowpassed[i];
return output;
}
std::vector<float> SliceManager::detectOnsets(const std::vector<float>& envelope, float threshold)
{
std::vector<float> onsets;
if (envelope.size() < 2) return onsets;
// Compute first derivative of envelope
std::vector<float> diff(envelope.size());
diff[0] = 0.0f;
for (size_t i = 1; i < envelope.size(); ++i)
diff[i] = envelope[i] - envelope[i - 1];
// Peak picking on derivative
const int windowSize = juce::jmax(1, static_cast<int>(currentSampleRate * 0.01));
for (size_t i = 1; i < diff.size() - 1; ++i)
{
if (diff[i] > threshold && diff[i] > diff[i - 1] && diff[i] >= diff[i + 1])
{
// Check minimum distance from last onset
float samplePos = static_cast<float>(i) * static_cast<float>(windowSize);
bool tooClose = false;
for (float o : onsets)
{
if (std::abs(samplePos - o) < currentSampleRate * 0.02) // 20ms minimum gap
{
tooClose = true;
break;
}
}
if (!tooClose)
onsets.push_back(samplePos);
}
}
return onsets;
}
void SliceManager::pushHistory()
{
undoStack.push_back(slices);
if (undoStack.size() > maxHistory)
undoStack.erase(undoStack.begin());
redoStack.clear();
}
void SliceManager::truncateHistory()
{
undoStack.clear();
redoStack.clear();
}
void SliceManager::undo()
{
if (undoStack.empty()) return;
redoStack.push_back(slices);
slices = undoStack.back();
undoStack.pop_back();
}
void SliceManager::redo()
{
if (redoStack.empty()) return;
undoStack.push_back(slices);
slices = redoStack.back();
redoStack.pop_back();
}
bool SliceManager::canUndo() const { return !undoStack.empty(); }
bool SliceManager::canRedo() const { return !redoStack.empty(); }