mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 04:10:45 +02:00
Initial commit
This commit is contained in:
commit
638cd05ea8
14 changed files with 2783 additions and 0 deletions
359
Source/SliceManager.cpp
Normal file
359
Source/SliceManager.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
#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(); }
|
||||
Loading…
Add table
Add a link
Reference in a new issue