mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 04:10:45 +02:00
375 lines
12 KiB
C++
375 lines
12 KiB
C++
#include "SliceManager.h"
|
|
|
|
SliceManager::SliceManager() = default;
|
|
|
|
std::vector<SliceManager::Slice> SliceManager::getSlices() const
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
return slices;
|
|
}
|
|
|
|
int SliceManager::getNumSlices() const
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
return static_cast<int>(slices.size());
|
|
}
|
|
|
|
SliceManager::Slice SliceManager::getSlice(int index) const
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (index < 0 || index >= static_cast<int>(slices.size()))
|
|
return { 0, 0, 0 };
|
|
return slices[static_cast<size_t>(index)];
|
|
}
|
|
|
|
void SliceManager::setSampleBuffer(juce::AudioBuffer<float>* buffer, double sampleRate)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
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()
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
|
return;
|
|
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
|
|
|
|
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()
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
|
|
slices.clear();
|
|
}
|
|
|
|
void SliceManager::setSlices(const std::vector<Slice>& newSlices)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
slices = newSlices;
|
|
assignMidiNotes();
|
|
}
|
|
|
|
void SliceManager::addSliceManual(int samplePos)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (!sampleBuffer) return;
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
const int numSamples = sampleBuffer->getNumSamples();
|
|
samplePos = juce::jlimit(1, numSamples - 1, samplePos);
|
|
|
|
if (slices.empty())
|
|
{
|
|
|
|
slices.push_back({ 0, numSamples, 0 });
|
|
assignMidiNotes();
|
|
return;
|
|
}
|
|
|
|
int idx = findSliceIndexForSample(samplePos);
|
|
if (idx < 0)
|
|
{
|
|
|
|
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;
|
|
|
|
|
|
|
|
int oldEnd = parent.endSample;
|
|
parent.endSample = samplePos;
|
|
|
|
slices.insert(slices.begin() + idx + 1, { samplePos, oldEnd, 0 });
|
|
|
|
assignMidiNotes();
|
|
}
|
|
|
|
void SliceManager::removeSliceAt(int samplePos, int tolerance)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
for (auto it = slices.begin(); it != slices.end(); ++it)
|
|
{
|
|
int dist = std::abs(it->startSample - samplePos);
|
|
if (dist <= tolerance)
|
|
{
|
|
|
|
int removedEnd = it->endSample;
|
|
auto idx = static_cast<int>(std::distance(slices.begin(), it));
|
|
slices.erase(it);
|
|
if (idx > 0 && idx - 1 < static_cast<int>(slices.size()))
|
|
slices[static_cast<size_t>(idx - 1)].endSample = removedEnd;
|
|
assignMidiNotes();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SliceManager::moveSlice(int fromSample, int toSample)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
for (size_t i = 0; i < slices.size(); ++i)
|
|
{
|
|
if (std::abs(slices[i].startSample - fromSample) < 10)
|
|
{
|
|
|
|
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)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample);
|
|
if (sample <= 0) return;
|
|
|
|
|
|
|
|
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)
|
|
{
|
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
|
if (beforeChangeCallback) beforeChangeCallback();
|
|
const int numSamples = sampleBuffer->getNumSamples();
|
|
sample = juce::jlimit(0, numSamples, sample);
|
|
if (sample >= numSamples) return;
|
|
|
|
|
|
|
|
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()
|
|
{
|
|
const int numRows = 25;
|
|
const int baseMidiNote = 60;
|
|
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
|
slices[static_cast<size_t>(i)].midiNote = baseMidiNote + (i % numRows);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|