#include "SliceManager.h" SliceManager::SliceManager() = default; std::vector SliceManager::getSlices() const { const juce::CriticalSection::ScopedLockType lock(mutex); return slices; } int SliceManager::getNumSlices() const { const juce::CriticalSection::ScopedLockType lock(mutex); return static_cast(slices.size()); } SliceManager::Slice SliceManager::getSlice(int index) const { const juce::CriticalSection::ScopedLockType lock(mutex); if (index < 0 || index >= static_cast(slices.size())) return { 0, 0, 0 }; return slices[static_cast(index)]; } void SliceManager::setSampleBuffer(juce::AudioBuffer* 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 mono(static_cast(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(i)] += data[i] / static_cast(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(i)] += (bassComponent[static_cast(i)] - mono[static_cast(i)]) * (bassGain - 1.0f); } if (trebleGain != 1.0f) { auto trebleComponent = highpassFilter(mono, trebleCutoff); for (int i = 0; i < numSamples; ++i) shaped[static_cast(i)] += (trebleComponent[static_cast(i)] - mono[static_cast(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(numSamples / 100)) onsets.insert(onsets.begin(), 0); for (size_t i = 0; i < onsets.size(); ++i) { int start = static_cast(onsets[i]); int end = (i + 1 < onsets.size()) ? static_cast(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& 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(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(std::distance(slices.begin(), it)); slices.erase(it); if (idx > 0 && idx - 1 < static_cast(slices.size())) slices[static_cast(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 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 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(slices.size()); ++i) { if (samplePos >= slices[static_cast(i)].startSample && samplePos < slices[static_cast(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(slices.size()); ++i) slices[static_cast(i)].midiNote = baseMidiNote + (i % numRows); } std::vector SliceManager::computeEnvelope(const float* data, int numSamples) { const int windowSize = juce::jmax(1, static_cast(currentSampleRate * 0.01)); // 10ms window const int numWindows = (numSamples + windowSize - 1) / windowSize; std::vector envelope(static_cast(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(w)] = std::sqrt(sum / static_cast(end - start)); } return envelope; } std::vector SliceManager::lowpassFilter(const std::vector& input, float cutoffHz) { std::vector output(input.size()); float rc = 1.0f / (2.0f * juce::MathConstants::pi * cutoffHz); float dt = 1.0f / static_cast(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 SliceManager::highpassFilter(const std::vector& input, float cutoffHz) { auto lowpassed = lowpassFilter(input, cutoffHz); std::vector output(input.size()); for (size_t i = 0; i < input.size(); ++i) output[i] = input[i] - lowpassed[i]; return output; } std::vector SliceManager::detectOnsets(const std::vector& envelope, float threshold) { std::vector onsets; if (envelope.size() < 2) return onsets; // Compute first derivative of envelope std::vector 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(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(i) * static_cast(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; }