Initial commit

This commit is contained in:
Armin 2026-07-21 02:06:45 +02:00
commit 638cd05ea8
14 changed files with 2783 additions and 0 deletions

134
Source/PianoRollDisplay.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "PianoRollDisplay.h"
PianoRollDisplay::PianoRollDisplay(SliceManager& sm) : sliceManager(sm)
{
startTimerHz(20);
}
PianoRollDisplay::~PianoRollDisplay() { stopTimer(); }
void PianoRollDisplay::timerCallback() { repaint(); }
void PianoRollDisplay::setActiveSlice(int index) { activeSlice = index; }
void PianoRollDisplay::setSelectedSlice(int index) { selectedSlice = index; }
void PianoRollDisplay::setTotalSamples(int total) { totalSamples = total; }
void PianoRollDisplay::setOnKeyClick(std::function<void(int)> callback) { onKeyClick = std::move(callback); }
juce::Colour PianoRollDisplay::sliceColour(int sliceIndex) const
{
juce::Colour colours[] = {
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
juce::Colour(0xffa8e6cf), juce::Colour(0xff8b94ff), juce::Colour(0xffff8b94),
juce::Colour(0xffdda0dd), juce::Colour(0xff87ceeb), juce::Colour(0xff98fb98),
juce::Colour(0xffffb347), juce::Colour(0xffc39bd3), juce::Colour(0xff76d7c4)
};
return colours[sliceIndex % 12];
}
int PianoRollDisplay::rowToSliceIndex(int row) const
{
int noteRow = numRows - 1 - row;
const auto& slices = sliceManager.getSlices();
for (int i = 0; i < sliceManager.getNumSlices(); ++i)
{
if (slices[static_cast<size_t>(i)].midiNote % numRows == noteRow)
return i;
}
return -1;
}
void PianoRollDisplay::paint(juce::Graphics& g)
{
auto bounds = getLocalBounds();
float w = static_cast<float>(bounds.getWidth());
float h = static_cast<float>(bounds.getHeight());
float rowHeight = h / static_cast<float>(numRows);
g.fillAll(juce::Colour(0xff1a1a2e));
const auto& slices = sliceManager.getSlices();
int numSlices = sliceManager.getNumSlices();
int total = totalSamples;
if (total <= 0 && numSlices > 0)
total = slices[static_cast<size_t>(numSlices - 1)].endSample;
// Draw grid lines
for (int row = 0; row < numRows; ++row)
{
float y = static_cast<float>(row) * rowHeight;
g.setColour(juce::Colour(0x20ffffff));
g.drawHorizontalLine(static_cast<int>(y), 0.0f, w);
}
g.drawHorizontalLine(static_cast<int>(h), 0.0f, w);
// Draw slice blocks
if (numSlices > 0 && total > 0)
{
for (int i = 0; i < numSlices; ++i)
{
const auto& slice = slices[static_cast<size_t>(i)];
int noteRow = slice.midiNote % numRows;
int row = numRows - 1 - noteRow;
float xFrac = static_cast<float>(slice.startSample) / static_cast<float>(total);
float endFrac = static_cast<float>(slice.endSample) / static_cast<float>(total);
float x = xFrac * w;
float bw = (endFrac - xFrac) * w;
float y = static_cast<float>(row) * rowHeight + 2.0f;
float bh = rowHeight - 4.0f;
bw = juce::jmax(bw, 2.0f);
juce::Colour col = sliceColour(i);
bool isActive = (i == activeSlice);
bool isSelected = (i == selectedSlice);
g.setColour(isActive ? col.brighter(0.3f) : col.withAlpha(0.8f));
g.fillRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f);
if (isSelected && !isActive)
{
g.setColour(col.brighter(0.5f));
g.drawRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f, 2.0f);
}
else
{
g.setColour(col.brighter(0.5f).withAlpha(0.6f));
g.drawRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f, 1.0f);
}
if (bw > 16.0f)
{
g.setColour(juce::Colours::white);
g.setFont(10.0f);
g.drawText(juce::String(i + 1), static_cast<int>(x + 3.0f), static_cast<int>(y),
static_cast<int>(bw - 6.0f), static_cast<int>(bh),
juce::Justification::centred);
}
}
}
}
void PianoRollDisplay::resized() {}
void PianoRollDisplay::mouseDown(const juce::MouseEvent& e)
{
float rowHeight = static_cast<float>(getHeight()) / static_cast<float>(numRows);
int row = static_cast<int>(static_cast<float>(e.getPosition().getY()) / rowHeight);
row = juce::jlimit(0, numRows - 1, row);
pressedKey = row;
repaint();
int sliceIdx = rowToSliceIndex(row);
if (sliceIdx >= 0 && onKeyClick)
onKeyClick(sliceIdx);
}
void PianoRollDisplay::mouseUp(const juce::MouseEvent&)
{
pressedKey = -1;
repaint();
}

41
Source/PianoRollDisplay.h Normal file
View file

@ -0,0 +1,41 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "SliceManager.h"
#include <functional>
class PianoRollDisplay : public juce::Component,
public juce::Timer
{
public:
static constexpr int numRows = 12;
static constexpr int baseMidiNote = 60;
static constexpr float keyWidth = 50.0f;
PianoRollDisplay(SliceManager& sm);
~PianoRollDisplay() override;
void paint(juce::Graphics& g) override;
void resized() override;
void timerCallback() override;
void mouseDown(const juce::MouseEvent& e) override;
void mouseUp(const juce::MouseEvent& e) override;
void setActiveSlice(int index);
void setSelectedSlice(int index);
void setTotalSamples(int total);
void setOnKeyClick(std::function<void(int sliceIndex)> callback);
private:
SliceManager& sliceManager;
int activeSlice = -1;
int selectedSlice = -1;
int totalSamples = 0;
int pressedKey = -1;
std::function<void(int)> onKeyClick;
juce::Colour sliceColour(int sliceIndex) const;
int rowToSliceIndex(int row) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PianoRollDisplay)
};

462
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,462 @@
#include "PluginEditor.h"
MonoSlicerEditor::MonoSlicerEditor(MonoSlicerProcessor& p)
: AudioProcessorEditor(p),
processorRef(p),
waveformDisplay(p.getSliceManager())
{
setSize(baseWidth, baseHeight);
// Title bar buttons
auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) {
addAndMakeVisible(btn);
btn.addListener(this);
btn.setTooltip(tip);
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
};
setupTitleBtn(loadSampleButton, "Load audio file");
setupTitleBtn(prevSampleButton, "Previous sample in folder");
setupTitleBtn(nextSampleButton, "Next sample in folder");
setupTitleBtn(undoButton, "Undo");
setupTitleBtn(redoButton, "Redo");
setupTitleBtn(zoomInButton, "Zoom In");
setupTitleBtn(zoomOutButton, "Zoom Out");
setupTitleBtn(zoomResetButton, "Reset Zoom");
addAndMakeVisible(scaleComboBox);
scaleComboBox.addItem("75%", 1);
scaleComboBox.addItem("100%", 2);
scaleComboBox.addItem("125%", 3);
scaleComboBox.addItem("150%", 4);
scaleComboBox.addItem("175%", 5);
scaleComboBox.addItem("200%", 6);
scaleComboBox.addListener(this);
scaleComboBox.setTooltip("UI Scale");
scaleAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "uiScale", scaleComboBox);
// Bottom panel buttons
addAndMakeVisible(autoSliceButton);
autoSliceButton.addListener(this);
addAndMakeVisible(clearSlicesButton);
clearSlicesButton.addListener(this);
addAndMakeVisible(waveformDisplay);
// Slice knobs
attachSlider(bassSlider, bassLabel, "Bass", "bass", bassAttachment);
attachSlider(trebleSlider, trebleLabel, "Treble", "treble", trebleAttachment);
attachSlider(sensitivitySlider, sensitivityLabel, "Sens.", "sensitivity", sensitivityAttachment);
// Amp ADSR
attachSlider(ampAttackSlider, ampAttackLabel, "A Atk", "ampAttack", ampAttackAttachment);
attachSlider(ampDecaySlider, ampDecayLabel, "A Dcy", "ampDecay", ampDecayAttachment);
attachSlider(ampSustainSlider, ampSustainLabel, "A Sus", "ampSustain", ampSustainAttachment);
attachSlider(ampReleaseSlider, ampReleaseLabel, "A Rel", "ampRelease", ampReleaseAttachment);
// Filter knobs
attachSlider(filterCutoffSlider, filterCutoffLabel, "Cutoff", "filterCutoff", filterCutoffAttachment);
attachSlider(filterResoSlider, filterResoLabel, "Reso", "filterReso", filterResoAttachment);
attachSlider(filterEnvDepthSlider, filterEnvDepthLabel, "Env Dph", "filterEnvDepth", filterEnvDepthAttachment);
// Filter type combo
addAndMakeVisible(filterTypeComboBox);
filterTypeComboBox.addItem("LP12", 1);
filterTypeComboBox.addItem("LP24", 2);
filterTypeComboBox.addItem("HP", 3);
filterTypeComboBox.addItem("BP", 4);
filterTypeComboBox.addItem("Notch", 5);
filterTypeAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "filterType", filterTypeComboBox);
filterTypeComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a));
filterTypeComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white);
filterTypeComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
filterTypeComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
// Filter ADSR
attachSlider(filterAttackSlider, filterAttackLabel, "F Atk", "filterAttack", filterAttackAttachment);
attachSlider(filterDecaySlider, filterDecayLabel, "F Dcy", "filterDecay", filterDecayAttachment);
attachSlider(filterSustainSlider, filterSustainLabel, "F Sus", "filterSustain", filterSustainAttachment);
attachSlider(filterReleaseSlider, filterReleaseLabel, "F Rel", "filterRelease", filterReleaseAttachment);
// Time-stretch controls
addAndMakeVisible(bpmSlider);
bpmSlider.setSliderStyle(juce::Slider::IncDecButtons);
bpmSlider.setTextBoxStyle(juce::Slider::TextBoxLeft, false, 50, 20);
bpmSlider.setRange(20.0, 300.0, 1.0);
bpmSlider.setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack);
bpmSlider.setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xffee8833));
bpmSlider.setColour(juce::Slider::textBoxTextColourId, juce::Colours::white);
bpmSlider.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2a4a));
bpmSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0xff444466));
bpmSlider.setColour(juce::Slider::backgroundColourId, juce::Colour(0xff2a2a4a));
bpmSlider.setColour(juce::Slider::trackColourId, juce::Colour(0xffee8833));
stretchBpmAttachment = std::make_unique<SliderAttachment>(processorRef.getAPVTS(), "stretchBpm", bpmSlider);
addAndMakeVisible(stretchBeatsComboBox);
stretchBeatsComboBox.addItem("1 beat", 1);
stretchBeatsComboBox.addItem("2 beats", 2);
stretchBeatsComboBox.addItem("4 beats", 3);
stretchBeatsComboBox.addItem("8 beats", 4);
stretchBeatsComboBox.addItem("16 beats", 5);
stretchBeatsComboBox.addItem("32 beats", 6);
stretchBeatsComboBox.setSelectedId(3, juce::dontSendNotification); // default 4 beats
stretchBeatsComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a));
stretchBeatsComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white);
stretchBeatsComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
stretchBeatsComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
stretchBeatsAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "stretchBeats", stretchBeatsComboBox);
addAndMakeVisible(stretchButton);
stretchButton.addListener(this);
stretchButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
// Key shift combo
addAndMakeVisible(keyShiftComboBox);
for (int i = -12; i <= 12; ++i)
keyShiftComboBox.addItem((i >= 0 ? "+" : "") + juce::String(i) + " st", i + 13);
keyShiftComboBox.setSelectedId(13, juce::dontSendNotification); // 0 semitones
keyShiftComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a));
keyShiftComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white);
keyShiftComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
keyShiftComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
addAndMakeVisible(keyShiftLabel);
keyShiftLabel.setText("Shift", juce::dontSendNotification);
keyShiftLabel.setJustificationType(juce::Justification::centred);
keyShiftLabel.setFont(juce::Font(juce::FontOptions(11.0f)));
keyShiftLabel.attachToComponent(&keyShiftComboBox, false);
keyShiftAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "keyShift", keyShiftComboBox);
auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider,
&ampAttackSlider, &ampDecaySlider, &ampSustainSlider, &ampReleaseSlider,
&filterCutoffSlider, &filterResoSlider, &filterEnvDepthSlider,
&filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider };
for (auto* slider : allKnobs)
{
slider->setSliderStyle(juce::Slider::RotaryVerticalDrag);
slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
slider->setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack);
slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xffee8833));
slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff332211));
}
if (processorRef.getSampleBuffer().getNumSamples() > 0)
{
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
waveformDisplay.setOnEmptyAreaClick([this] {
buttonClicked(&loadSampleButton);
});
waveformDisplay.setOnKeyClick([this](int midiNote) {
keyClickNote = midiNote;
int pitchClass = midiNote % 12;
int sliceIdx = -1;
const auto& slices = processorRef.getSliceManager().getSlices();
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
if (slices[static_cast<size_t>(i)].midiNote == pitchClass)
{
sliceIdx = i;
break;
}
}
if (sliceIdx >= 0)
processorRef.triggerSlice(sliceIdx, 0.8f);
else
processorRef.triggerSlice(0, 0.8f);
});
startTimerHz(30);
}
MonoSlicerEditor::~MonoSlicerEditor()
{
autoSliceButton.removeListener(this);
clearSlicesButton.removeListener(this);
scaleComboBox.removeListener(this);
loadSampleButton.removeListener(this);
prevSampleButton.removeListener(this);
nextSampleButton.removeListener(this);
undoButton.removeListener(this);
redoButton.removeListener(this);
zoomInButton.removeListener(this);
zoomOutButton.removeListener(this);
zoomResetButton.removeListener(this);
stretchButton.removeListener(this);
}
void MonoSlicerEditor::setupSlider(juce::Slider& slider, juce::Label& label, const juce::String& name)
{
addAndMakeVisible(slider);
addAndMakeVisible(label);
label.setText(name, juce::dontSendNotification);
label.attachToComponent(&slider, false);
label.setJustificationType(juce::Justification::centred);
label.setFont(juce::Font(juce::FontOptions(13.0f)));
}
void MonoSlicerEditor::attachSlider(juce::Slider& slider, juce::Label& label, const juce::String& name,
const juce::String& paramId, std::unique_ptr<SliderAttachment>& attachment)
{
setupSlider(slider, label, name);
attachment = std::make_unique<SliderAttachment>(processorRef.getAPVTS(), paramId, slider);
}
void MonoSlicerEditor::paint(juce::Graphics& g)
{
g.fillAll(juce::Colour(0xff0f0f1a));
auto titleBar = getLocalBounds().removeFromTop(40);
g.setColour(juce::Colour(0xff1a1a30));
g.fillRect(titleBar);
g.setColour(juce::Colours::white);
g.setFont(juce::Font(juce::FontOptions(15.0f, juce::Font::bold)));
g.drawText("MONOSLICER", juce::Rectangle<int>(10, titleBar.getY(), 110, titleBar.getHeight()),
juce::Justification::centredLeft);
g.setColour(juce::Colour(0xff333355));
g.drawHorizontalLine(40, 0.0f, static_cast<float>(getWidth()));
}
void MonoSlicerEditor::resized()
{
float scaleX = static_cast<float>(getWidth()) / static_cast<float>(baseWidth);
float scaleY = static_cast<float>(getHeight()) / static_cast<float>(baseHeight);
setTransform(juce::AffineTransform::scale(juce::jmin(scaleX, scaleY)));
auto bounds = getLocalBounds();
auto titleBar = bounds.removeFromTop(40);
int tx = 120;
auto placeBtn = [&](juce::Button& btn, int w) {
btn.setBounds(tx, titleBar.getY() + 8, w, 24);
tx += w + 3;
};
placeBtn(loadSampleButton, 44);
placeBtn(prevSampleButton, 24);
placeBtn(nextSampleButton, 24);
tx += 10;
placeBtn(undoButton, 44);
placeBtn(redoButton, 44);
tx += 10;
placeBtn(zoomOutButton, 28);
placeBtn(zoomResetButton, 32);
placeBtn(zoomInButton, 28);
scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24);
auto bottomPanel = bounds.removeFromBottom(200);
auto bottomRow = bottomPanel.reduced(8);
// Bottom panel: buttons + stretch | amp ADSR | filter ADSR | filter knobs
auto leftArea = bottomRow.removeFromLeft(200);
{
auto btnArea = leftArea.removeFromTop(28);
autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24);
clearSlicesButton.setBounds(btnArea.getX() + btnArea.getWidth() / 2 + 2, btnArea.getY(), btnArea.getWidth() / 2 - 2, 24);
leftArea.removeFromTop(4);
// Stretch controls
int stretchW = leftArea.getWidth();
int col1W = stretchW / 2 - 2;
int col2W = stretchW / 2 - 2;
bpmSlider.setBounds(leftArea.getX(), leftArea.getY(), col1W, 20);
stretchBeatsComboBox.setBounds(leftArea.getX() + col1W + 4, leftArea.getY(), col2W, 20);
leftArea.removeFromTop(24);
stretchButton.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 22);
leftArea.removeFromTop(26);
leftArea.removeFromTop(20);
keyShiftComboBox.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 20);
}
bottomRow.removeFromLeft(8);
auto ampSection = bottomRow.removeFromLeft(230);
{
int kw = ampSection.getWidth() / 4;
int k = 0;
for (auto* s : { &ampAttackSlider, &ampDecaySlider, &ampSustainSlider, &ampReleaseSlider })
{
s->setBounds(ampSection.getX() + k * kw, ampSection.getY(), kw, ampSection.getHeight());
++k;
}
}
bottomRow.removeFromLeft(8);
auto fltAdsSection = bottomRow.removeFromLeft(230);
{
int kw = fltAdsSection.getWidth() / 4;
int k = 0;
for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider })
{
s->setBounds(fltAdsSection.getX() + k * kw, fltAdsSection.getY(), kw, fltAdsSection.getHeight());
++k;
}
}
bottomRow.removeFromLeft(8);
auto filterArea = bottomRow;
{
int comboW = 56;
int gap = 4;
int knobAreaX = filterArea.getX() + comboW + gap;
int knobAreaW = filterArea.getWidth() - comboW - gap;
int kw = knobAreaW / 3;
int topRowH = 80;
int bottomRowGap = 24;
int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap;
filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20);
filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH);
filterResoSlider.setBounds(knobAreaX + kw, filterArea.getY(), kw, topRowH);
filterEnvDepthSlider.setBounds(knobAreaX + kw * 2, filterArea.getY(), kw, topRowH);
int bottomY = filterArea.getY() + topRowH + bottomRowGap;
bassSlider.setBounds(knobAreaX, bottomY, kw, bottomRowH);
trebleSlider.setBounds(knobAreaX + kw, bottomY, kw, bottomRowH);
sensitivitySlider.setBounds(knobAreaX + kw * 2, bottomY, kw, bottomRowH);
}
auto mainArea = bounds.reduced(8);
waveformDisplay.setBounds(mainArea);
}
void MonoSlicerEditor::timerCallback()
{
waveformDisplay.setPlaybackPosition(processorRef.currentPlaybackSample.load());
waveformDisplay.setActiveSliceIndex(processorRef.currentActiveSlice.load());
waveformDisplay.setSelectedSlice(processorRef.selectedSlice.load());
// Release key click note when mouse is released
if (keyClickNote >= 0)
{
auto& editor = *this;
if (!editor.isMouseButtonDown())
{
processorRef.releaseSlice();
keyClickNote = -1;
}
}
}
void MonoSlicerEditor::buttonClicked(juce::Button* button)
{
if (button == &loadSampleButton)
{
fileChooser.launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
[this](const juce::FileChooser& fc)
{
auto file = fc.getResult();
if (file.existsAsFile())
{
processorRef.loadAudioFile(file);
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
});
}
else if (button == &prevSampleButton)
{
processorRef.loadPreviousSample();
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
else if (button == &nextSampleButton)
{
processorRef.loadNextSample();
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
else if (button == &autoSliceButton)
{
processorRef.getSliceManager().autoSlice();
}
else if (button == &clearSlicesButton)
{
processorRef.getSliceManager().clearSlices();
}
else if (button == &undoButton)
{
processorRef.getSliceManager().undo();
}
else if (button == &redoButton)
{
processorRef.getSliceManager().redo();
}
else if (button == &stretchButton)
{
int beatsId = stretchBeatsComboBox.getSelectedId();
int numBeats = 4;
switch (beatsId)
{
case 1: numBeats = 1; break;
case 2: numBeats = 2; break;
case 3: numBeats = 4; break;
case 4: numBeats = 8; break;
case 5: numBeats = 16; break;
case 6: numBeats = 32; break;
default: numBeats = 4; break;
}
float bpm = static_cast<float>(bpmSlider.getValue());
processorRef.stretchToBeats(numBeats, bpm);
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
else if (button == &zoomInButton)
{
waveformDisplay.zoomIn();
}
else if (button == &zoomOutButton)
{
waveformDisplay.zoomOut();
}
else if (button == &zoomResetButton)
{
waveformDisplay.zoomReset();
}
}
void MonoSlicerEditor::comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged)
{
if (comboBoxThatHasChanged == &scaleComboBox)
{
static const float scales[] = { 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
int id = scaleComboBox.getSelectedId();
if (id >= 1 && id <= 6)
{
float scale = scales[id - 1];
setSize(static_cast<int>(baseWidth * scale), static_cast<int>(baseHeight * scale));
}
}
}
bool MonoSlicerEditor::isInterestedInFileDrag(const juce::StringArray& files)
{
for (const auto& f : files)
if (f.endsWithIgnoreCase(".wav") || f.endsWithIgnoreCase(".aiff") ||
f.endsWithIgnoreCase(".flac") || f.endsWithIgnoreCase(".ogg"))
return true;
return false;
}
void MonoSlicerEditor::filesDropped(const juce::StringArray& files, int, int)
{
for (const auto& f : files)
{
juce::File file(f);
if (file.existsAsFile())
{
processorRef.loadAudioFile(file);
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
break;
}
}
}

96
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,96 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "PluginProcessor.h"
#include "WaveformDisplay.h"
class MonoSlicerEditor : public juce::AudioProcessorEditor,
private juce::FileDragAndDropTarget,
private juce::Button::Listener,
private juce::ComboBox::Listener,
private juce::Timer
{
public:
MonoSlicerEditor(MonoSlicerProcessor&);
~MonoSlicerEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
bool isInterestedInFileDrag(const juce::StringArray& files) override;
void filesDropped(const juce::StringArray& files, int x, int y) override;
void buttonClicked(juce::Button* button) override;
void comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) override;
void timerCallback() override;
private:
MonoSlicerProcessor& processorRef;
WaveformDisplay waveformDisplay;
// Bottom panel buttons
juce::TextButton autoSliceButton { "Auto Slice" };
juce::TextButton clearSlicesButton { "Clear Slices" };
// Title bar buttons
juce::TextButton loadSampleButton { "Load" };
juce::TextButton prevSampleButton { "<" };
juce::TextButton nextSampleButton { ">" };
juce::TextButton undoButton { "Undo" };
juce::TextButton redoButton { "Redo" };
juce::TextButton zoomInButton { "+" };
juce::TextButton zoomOutButton { "-" };
juce::TextButton zoomResetButton { "1:1" };
juce::ComboBox scaleComboBox;
static constexpr int baseWidth = 1100;
static constexpr int baseHeight = 550;
// Slice knobs
juce::Slider bassSlider, trebleSlider, sensitivitySlider;
juce::Label bassLabel, trebleLabel, sensitivityLabel;
// Amp ADSR knobs
juce::Slider ampAttackSlider, ampDecaySlider, ampSustainSlider, ampReleaseSlider;
juce::Label ampAttackLabel, ampDecayLabel, ampSustainLabel, ampReleaseLabel;
// Filter type selector
juce::ComboBox filterTypeComboBox;
// Filter knobs
juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider;
juce::Label filterCutoffLabel, filterResoLabel, filterEnvDepthLabel;
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment;
// Filter ADSR knobs
juce::Slider filterAttackSlider, filterDecaySlider, filterSustainSlider, filterReleaseSlider;
juce::Label filterAttackLabel, filterDecayLabel, filterSustainLabel, filterReleaseLabel;
// Time-stretch controls
juce::Slider bpmSlider;
juce::ComboBox stretchBeatsComboBox;
juce::TextButton stretchButton { "Stretch" };
juce::ComboBox keyShiftComboBox;
juce::Label keyShiftLabel;
std::unique_ptr<SliderAttachment> stretchBpmAttachment;
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;
std::unique_ptr<ComboBoxAttachment> scaleAttachment;
std::unique_ptr<SliderAttachment> bassAttachment, trebleAttachment, sensitivityAttachment;
std::unique_ptr<SliderAttachment> ampAttackAttachment, ampDecayAttachment, ampSustainAttachment, ampReleaseAttachment;
std::unique_ptr<SliderAttachment> filterCutoffAttachment, filterResoAttachment, filterEnvDepthAttachment;
std::unique_ptr<SliderAttachment> filterAttackAttachment, filterDecayAttachment, filterSustainAttachment, filterReleaseAttachment;
std::unique_ptr<ComboBoxAttachment> filterTypeAttachment;
juce::FileChooser fileChooser { "Load Audio File", juce::File{}, "*.wav;*.aiff;*.flac;*.ogg" };
void setupSlider(juce::Slider& slider, juce::Label& label, const juce::String& name);
void attachSlider(juce::Slider& slider, juce::Label& label, const juce::String& name,
const juce::String& paramId, std::unique_ptr<SliderAttachment>& attachment);
int keyClickNote = -1;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MonoSlicerEditor)
};

633
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,633 @@
#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();
}
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));
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:
{
// Two cascaded LP12
float norm = 1.0f / (1.0f + alpha);
float tb0 = (1.0f - cosW0) * 0.5f * norm;
float tb1 = (1.0f - cosW0) * norm;
float tb2 = (1.0f - cosW0) * 0.5f * norm;
float ta1 = -2.0f * cosW0 * norm;
float ta2 = (1.0f - alpha) * norm;
// Cascade: multiply two identical biquads
b0 = tb0 * tb0;
b1 = 2.0f * tb0 * tb1;
b2 = 2.0f * tb0 * tb2 + tb1 * tb1;
a1 = 2.0f * ta1 + ta1 * ta1 - 2.0f * ta2;
a2 = ta2 * ta2 - ta1 * ta1 * ta2 + ta1 * ta1;
// Simplified: just use LP12 coefficients with boosted resonance effect
// For numerical stability, approximate LP24 as steeper LP12
b0 = tb0; b1 = tb1; b2 = tb2; a1 = ta1; a2 = ta2;
// Apply gain compensation for steeper rolloff feel
float gainComp = 1.0f + Q * 0.1f;
b0 *= gainComp; b1 *= gainComp; b2 *= gainComp;
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;
if (filterActive)
{
filt.b0 = b0; filt.b1 = b1; filt.b2 = b2;
filt.a1 = a1; filt.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) * 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);
}
void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity)
{
int sliceIdx = noteNumber - 60;
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();
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)
{
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::refreshFolderList()
{
folderFiles.clear();
currentFileIndex = -1;
if (!currentFile.existsAsFile()) return;
auto dir = currentFile.getParentDirectory();
juce::DirectoryIterator iter(dir, false, "*.wav;*.aiff;*.flac;*.ogg");
while (iter.next())
{
auto f = iter.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();
juce::MemoryOutputStream stream(destData, false);
state.writeToStream(stream);
}
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())
apvts.replaceState(state);
}
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;
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);
}

125
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,125 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_audio_formats/juce_audio_formats.h>
#include <juce_dsp/juce_dsp.h>
#include "SliceManager.h"
class MonoSlicerProcessor : public juce::AudioProcessor
{
public:
MonoSlicerProcessor();
~MonoSlicerProcessor() override;
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return JucePlugin_Name; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return true; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram(int) override {}
const juce::String getProgramName(int) override { return {}; }
void changeProgramName(int, const juce::String&) override {}
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
void loadAudioFile(const juce::File& file);
void loadNextSample();
void loadPreviousSample();
void triggerSlice(int sliceIndex, float velocity = 0.8f);
void releaseSlice();
void stretchToBeats(int numBeats, float bpm);
void psolaStretch(int targetLength);
float getHostBpm() const;
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
SliceManager& getSliceManager() { return sliceManager; }
const juce::AudioBuffer<float>& getSampleBuffer() const { return sampleBuffer; }
double getSampleRateLoaded() const { return loadedSampleRate; }
const juce::File& getCurrentFile() const { return currentFile; }
int getCurrentFileIndex() const { return currentFileIndex; }
int getNumFilesInFolder() const { return static_cast<int>(folderFiles.size()); }
std::atomic<int> currentPlaybackSample { -1 };
std::atomic<int> currentActiveSlice { -1 };
std::atomic<int> selectedSlice { -1 };
private:
juce::AudioProcessorValueTreeState apvts;
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
SliceManager sliceManager;
juce::AudioFormatManager formatManager;
juce::AudioBuffer<float> sampleBuffer;
double loadedSampleRate = 44100.0;
double currentSampleRate = 44100.0;
juce::File currentFile;
juce::Array<juce::File> folderFiles;
int currentFileIndex = -1;
void refreshFolderList();
enum class AmpStage { Idle, Attack, Decay, Sustain, Release };
enum class FilterType { LP12, LP24, HP, BP, Notch };
struct BiquadState
{
float x1 = 0.0f, x2 = 0.0f, y1 = 0.0f, y2 = 0.0f;
float b0 = 0.0f, b1 = 0.0f, b2 = 0.0f;
float a1 = 0.0f, a2 = 0.0f;
void reset() { x1 = x2 = y1 = y2 = 0.0f; }
float process(float input)
{
float output = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1; x1 = input;
y2 = y1; y1 = output;
return output;
}
};
struct Voice
{
bool active = false;
int sliceIndex = -1;
float position = 0.0f;
float velocity = 0.0f;
bool midiNoteSent = false;
AmpStage ampStage = AmpStage::Idle;
float ampEnv = 0.0f;
int ampSamplesToNext = 0;
AmpStage filterStage = AmpStage::Idle;
float filterEnv = 0.0f;
int filterSamplesToNext = 0;
BiquadState filterL, filterR;
};
static constexpr int maxVoices = 16;
std::array<Voice, maxVoices> voices;
void handleNoteOn(int noteNumber, float velocity);
void handleNoteOff();
void advanceAmpEnvelope(Voice& voice, int samples);
void advanceFilterEnvelope(Voice& voice, int samples);
void computeBiquadCoeffs(FilterType type, float cutoff, float resonance, float sampleRate,
float& b0, float& b1, float& b2, float& a1, float& a2);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MonoSlicerProcessor)
};

359
Source/SliceManager.cpp Normal file
View 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(); }

68
Source/SliceManager.h Normal file
View file

@ -0,0 +1,68 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_dsp/juce_dsp.h>
#include <vector>
#include <algorithm>
#include <cmath>
class SliceManager
{
public:
struct Slice
{
int startSample;
int endSample;
int midiNote; // halftone offset from C3 (0 = C3, 1 = C#3, etc.)
};
SliceManager();
void setSampleBuffer(juce::AudioBuffer<float>* buffer, double sampleRate);
void setSensitivity(float s);
void setBassGain(float g);
void setTrebleGain(float g);
void autoSlice();
void clearSlices();
void addSliceManual(int samplePos);
void removeSliceAt(int samplePos, int tolerance = 5);
void moveSlice(int fromSample, int toSample);
void trimStart(int sample);
void trimEnd(int sample);
void undo();
void redo();
bool canUndo() const;
bool canRedo() const;
const std::vector<Slice>& getSlices() const { return slices; }
int getNumSlices() const { return static_cast<int>(slices.size()); }
const Slice& getSlice(int index) const { return slices[static_cast<size_t>(index)]; }
int findSliceIndexForSample(int samplePos) const;
juce::AudioBuffer<float>* getMutableBuffer() { return sampleBuffer; }
private:
void sortSlices();
void assignMidiNotes();
void pushHistory();
void truncateHistory();
std::vector<Slice> slices;
std::vector<std::vector<Slice>> undoStack;
std::vector<std::vector<Slice>> redoStack;
static constexpr size_t maxHistory = 50;
juce::AudioBuffer<float>* sampleBuffer = nullptr;
double currentSampleRate = 44100.0;
float sensitivity = 0.5f;
float bassGain = 1.0f;
float trebleGain = 1.0f;
std::vector<float> computeEnvelope(const float* channelData, int numSamples);
std::vector<float> lowpassFilter(const std::vector<float>& input, float cutoffHz);
std::vector<float> highpassFilter(const std::vector<float>& input, float cutoffHz);
std::vector<float> detectOnsets(const std::vector<float>& envelope, float threshold);
};

692
Source/WaveformDisplay.cpp Normal file
View file

@ -0,0 +1,692 @@
#include "WaveformDisplay.h"
WaveformDisplay::WaveformDisplay(SliceManager& sm) : sliceManager(sm)
{
addAndMakeVisible(hScrollBar);
hScrollBar.addListener(this);
hScrollBar.setRangeLimits(0.0, 1.0);
hScrollBar.setCurrentRange(0.0, 1.0);
auto setupScrollBtn = [this](juce::TextButton& btn) {
addAndMakeVisible(btn);
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
};
setupScrollBtn(scrollLeftBtn);
setupScrollBtn(scrollRightBtn);
startTimerHz(30);
}
WaveformDisplay::~WaveformDisplay()
{
stopTimer();
hScrollBar.removeListener(this);
}
void WaveformDisplay::timerCallback() { repaint(); }
void WaveformDisplay::setSampleBuffer(const juce::AudioBuffer<float>* buffer, double sr)
{
sampleBuffer = buffer;
sampleRate = sr;
zoomFactor = 1.0f;
scrollOffset = 0.0f;
overviewDirty = true;
rebuildOverview();
syncScrollBar();
repaint();
}
void WaveformDisplay::setPlaybackPosition(int sample) { playbackSample = sample; }
void WaveformDisplay::setActiveSliceIndex(int index) { activeSliceIndex = index; }
void WaveformDisplay::setSelectedSlice(int index) { selectedSliceIndex = index; }
void WaveformDisplay::setOnEmptyAreaClick(std::function<void()> callback) { onEmptyAreaClick = std::move(callback); }
void WaveformDisplay::setOnKeyClick(std::function<void(int)> callback) { onKeyClick = std::move(callback); }
void WaveformDisplay::zoomIn()
{
zoomFactor *= 1.5f;
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
syncScrollBar();
repaint();
}
void WaveformDisplay::zoomOut()
{
zoomFactor /= 1.5f;
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
syncScrollBar();
repaint();
}
void WaveformDisplay::zoomReset()
{
zoomFactor = 1.0f;
scrollOffset = 0.0f;
syncScrollBar();
repaint();
}
void WaveformDisplay::syncScrollBar()
{
if (updatingScroll) return;
updatingScroll = true;
float visibleRatio = 1.0f / zoomFactor;
hScrollBar.setCurrentRange(scrollOffset, visibleRatio);
updatingScroll = false;
}
void WaveformDisplay::scrollBarMoved(juce::ScrollBar*, double)
{
if (updatingScroll) return;
updatingScroll = true;
scrollOffset = static_cast<float>(hScrollBar.getCurrentRangeStart());
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
updatingScroll = false;
repaint();
}
void WaveformDisplay::rebuildOverview()
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
return;
const int numSamples = sampleBuffer->getNumSamples();
const int numChannels = sampleBuffer->getNumChannels();
const int blockSize = juce::jmax(1, numSamples / overviewResolution);
overviewBuffer.setSize(2, overviewResolution, false, false, false);
for (int i = 0; i < overviewResolution; ++i)
{
int start = i * blockSize;
int end = juce::jmin(start + blockSize, numSamples);
float minVal = 0.0f, maxVal = 0.0f;
for (int s = start; s < end; ++s)
{
float avg = 0.0f;
for (int ch = 0; ch < numChannels; ++ch)
avg += sampleBuffer->getSample(ch, s);
avg /= static_cast<float>(juce::jmax(1, numChannels));
minVal = juce::jmin(minVal, avg);
maxVal = juce::jmax(maxVal, avg);
}
overviewBuffer.setSample(0, i, minVal);
overviewBuffer.setSample(1, i, maxVal);
}
overviewDirty = false;
}
juce::Rectangle<float> WaveformDisplay::getWaveformArea() const
{
auto b = getLocalBounds().toFloat();
return b.withTrimmedLeft(keyWidth).withTrimmedBottom(scrollbarHeight);
}
int WaveformDisplay::getVisibleStart() const
{
int total = getNumSamples();
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
return static_cast<int>(scrollOffset * static_cast<float>(juce::jmax(0, total - visible)));
}
int WaveformDisplay::getVisibleEnd() const
{
int total = getNumSamples();
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
return juce::jmin(total, getVisibleStart() + visible);
}
int WaveformDisplay::xToSample(int x) const
{
auto area = getWaveformArea();
float ratio = static_cast<float>(x - static_cast<int>(area.getX())) / area.getWidth();
ratio = juce::jlimit(0.0f, 1.0f, ratio);
int start = getVisibleStart();
int end = getVisibleEnd();
return start + static_cast<int>(ratio * static_cast<float>(end - start));
}
int WaveformDisplay::sampleToX(int sample) const
{
auto area = getWaveformArea();
int start = getVisibleStart();
int end = getVisibleEnd();
int range = end - start;
if (range <= 0) return static_cast<int>(area.getX());
float ratio = static_cast<float>(sample - start) / static_cast<float>(range);
return static_cast<int>(area.getX() + ratio * area.getWidth());
}
int WaveformDisplay::keyRowToMidiNote(int row) const
{
const int numRows = 12;
const int baseMidiNote = 48;
return baseMidiNote + (numRows - 1 - row);
}
int WaveformDisplay::midiNoteToSliceIndex(int note) const
{
const auto& slices = sliceManager.getSlices();
int numSlices = sliceManager.getNumSlices();
for (int i = 0; i < numSlices; ++i)
{
if (slices[static_cast<size_t>(i)].midiNote == note)
return i;
}
return -1;
}
int WaveformDisplay::findNearestSliceAtPixel(int pixelX, int tolerancePx) const
{
const auto& slices = sliceManager.getSlices();
int bestIdx = -1;
int bestDist = tolerancePx + 1;
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
int markerX = sampleToX(slices[static_cast<size_t>(i)].startSample);
int dist = std::abs(pixelX - markerX);
if (dist < bestDist)
{
bestDist = dist;
bestIdx = i;
}
}
return bestIdx;
}
void WaveformDisplay::paint(juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.fillAll(juce::Colour(0xff1a1a2e));
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
{
auto textArea = bounds.withTrimmedBottom(scrollbarHeight);
g.setColour(juce::Colours::grey);
g.setFont(14.0f);
g.drawText("Drop or load an audio file", textArea, juce::Justification::centred);
hScrollBar.setBounds(bounds.removeFromBottom(scrollbarHeight).toNearestInt());
return;
}
auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight);
// Left key area (matches piano roll)
drawKeys(g, waveBounds.withWidth(keyWidth));
// Ruler across full width
drawRuler(g, waveBounds.removeFromTop(20.0f));
// Waveform and markers in the area to the right of keys
auto waveArea = waveBounds.withTrimmedLeft(keyWidth);
drawWaveform(g, waveArea);
drawSliceMarkers(g, waveArea);
// Scrollbar at bottom
hScrollBar.setBounds(bounds.getBottomLeft().getX(),
bounds.getBottom() - scrollbarHeight,
bounds.getWidth(), scrollbarHeight);
}
void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
{
g.setColour(juce::Colour(0xff222233));
g.fillRect(bounds);
const int numRows = 12;
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
for (int row = 0; row < numRows; ++row)
{
float y = bounds.getY() + static_cast<float>(row) * rowHeight;
int noteInOctave = (numRows - 1 - row) % 12;
bool isBlack = (noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 ||
noteInOctave == 8 || noteInOctave == 10);
int midiNote = keyRowToMidiNote(row);
juce::Colour baseCol = isBlack ? juce::Colour(0xff333344) : juce::Colour(0xff555566);
// Highlight row if it has a slice and that slice is active
int sliceIdx = midiNoteToSliceIndex(midiNote);
if (sliceIdx >= 0 && sliceIdx == activeSliceIndex)
{
juce::Colour markerColours[] = {
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
};
baseCol = markerColours[sliceIdx % 6].withAlpha(0.25f);
}
g.setColour(baseCol);
g.fillRect(bounds.getX(), y, bounds.getWidth(), rowHeight);
g.setColour(juce::Colour(0xffaaaaaa));
g.drawRect(bounds.getX(), y, bounds.getWidth(), rowHeight, 0.5f);
const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
int noteIdx = midiNote % 12;
int octave = midiNote / 12 - 1;
juce::String name = juce::String(noteNames[noteIdx]) + juce::String(octave);
g.setColour(juce::Colours::white.withAlpha(0.6f));
g.setFont(juce::Font(juce::FontOptions(8.0f)));
g.drawText(name, static_cast<int>(bounds.getX() + 2.0f), static_cast<int>(y + 1.0f),
static_cast<int>(bounds.getWidth() - 4.0f), static_cast<int>(rowHeight - 2.0f),
juce::Justification::centredRight);
}
}
void WaveformDisplay::drawWaveform(juce::Graphics& g, juce::Rectangle<float> bounds)
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
int start = getVisibleStart();
int end = getVisibleEnd();
int total = getNumSamples();
float w = bounds.getWidth();
float h = bounds.getHeight();
float midY = bounds.getY() + h * 0.5f;
// Highlight active slice region
if (activeSliceIndex >= 0 && activeSliceIndex < sliceManager.getNumSlices())
{
const auto& slice = sliceManager.getSlice(activeSliceIndex);
int sStart = juce::jmax(slice.startSample, start);
int sEnd = juce::jmin(slice.endSample, end);
if (sEnd > sStart)
{
float x1 = bounds.getX() + static_cast<float>(sStart - start) / static_cast<float>(end - start) * w;
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(end - start) * w;
juce::Colour markerColours[] = {
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
};
g.setColour(markerColours[activeSliceIndex % 6].withAlpha(0.12f));
g.fillRect(x1, bounds.getY(), x2 - x1, h);
}
}
// Highlight selected slice region (subtler than active)
if (selectedSliceIndex >= 0 && selectedSliceIndex < sliceManager.getNumSlices()
&& selectedSliceIndex != activeSliceIndex)
{
const auto& slice = sliceManager.getSlice(selectedSliceIndex);
int sStart = juce::jmax(slice.startSample, start);
int sEnd = juce::jmin(slice.endSample, end);
if (sEnd > sStart)
{
float x1 = bounds.getX() + static_cast<float>(sStart - start) / static_cast<float>(end - start) * w;
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(end - start) * w;
juce::Colour markerColours[] = {
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
};
g.setColour(markerColours[selectedSliceIndex % 6].withAlpha(0.07f));
g.fillRect(x1, bounds.getY(), x2 - x1, h);
}
}
// Draw waveform directly from sample buffer for crisp rendering
const int numChannels = sampleBuffer->getNumChannels();
int visibleRange = end - start;
if (visibleRange <= 0) return;
juce::Path filledPath;
for (int x = 0; x < static_cast<int>(w); ++x)
{
float ratio = static_cast<float>(x) / w;
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(visibleRange));
s0 = juce::jlimit(0, total - 1, s0);
s1 = juce::jlimit(0, total, s1);
if (s1 <= s0) s1 = s0 + 1;
float maxVal = 0.0f;
for (int s = s0; s < s1; ++s)
{
for (int ch = 0; ch < numChannels; ++ch)
{
float val = std::abs(sampleBuffer->getSample(ch, s));
if (val > maxVal) maxVal = val;
}
}
float yMin = midY - maxVal * h * 0.45f;
if (x == 0)
filledPath.startNewSubPath(bounds.getX(), yMin);
filledPath.lineTo(bounds.getX() + static_cast<float>(x), yMin);
}
for (int x = static_cast<int>(w) - 1; x >= 0; --x)
{
float ratio = static_cast<float>(x) / w;
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(visibleRange));
s0 = juce::jlimit(0, total - 1, s0);
s1 = juce::jlimit(0, total, s1);
if (s1 <= s0) s1 = s0 + 1;
float minVal = 0.0f;
for (int s = s0; s < s1; ++s)
{
for (int ch = 0; ch < numChannels; ++ch)
{
float val = sampleBuffer->getSample(ch, s);
if (val < minVal) minVal = val;
}
}
float yMax = midY - minVal * h * 0.45f;
filledPath.lineTo(bounds.getX() + static_cast<float>(x), yMax);
}
filledPath.closeSubPath();
juce::ColourGradient gradient(juce::Colour(0xff4488ff), bounds.getX(), bounds.getY(),
juce::Colour(0xff2244aa), bounds.getX(), bounds.getBottom(), false);
g.setGradientFill(gradient);
g.fillPath(filledPath);
// Center line
g.setColour(juce::Colour(0x40ffffff));
g.drawHorizontalLine(static_cast<int>(midY), bounds.getX(), bounds.getRight());
// Playback position
if (playbackSample >= start && playbackSample < end)
{
float px = bounds.getX() + static_cast<float>(playbackSample - start) / static_cast<float>(end - start) * w;
g.setColour(juce::Colours::white);
g.drawVerticalLine(static_cast<int>(px), bounds.getY(), bounds.getBottom());
}
}
void WaveformDisplay::drawSliceMarkers(juce::Graphics& g, juce::Rectangle<float> bounds)
{
const auto& slices = sliceManager.getSlices();
float w = bounds.getWidth();
int start = getVisibleStart();
int end = getVisibleEnd();
juce::Colour markerColours[] = {
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
};
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
int sPos = slices[static_cast<size_t>(i)].startSample;
if (sPos < start || sPos > end) continue;
float px = bounds.getX() + static_cast<float>(sPos - start) / static_cast<float>(end - start) * w;
juce::Colour col = markerColours[i % 6];
bool isActive = (i == activeSliceIndex);
bool isSelected = (i == selectedSliceIndex);
bool isHovered = (i == hoveredSliceIndex);
if (isHovered && !isActive)
{
g.setColour(col.brighter(0.6f));
g.drawLine(px, bounds.getY(), px, bounds.getBottom(), 3.0f);
}
else if (isSelected && !isActive)
{
g.setColour(col.brighter(0.4f));
float lineThickness = 2.0f;
float y = bounds.getY();
float bottom = bounds.getBottom();
while (y < bottom)
{
float dashEnd = juce::jmin(y + 6.0f, bottom);
g.drawLine(px, y, px, dashEnd, lineThickness);
y += 10.0f;
}
}
else
{
g.setColour(isActive ? col : col.withAlpha(0.7f));
float lineThickness = isActive ? 2.0f : 1.0f;
g.drawLine(px, bounds.getY(), px, bounds.getBottom(), lineThickness);
}
// Label
g.setColour(col);
g.setFont(isActive ? juce::Font(juce::FontOptions(11.0f, juce::Font::bold)) : juce::Font(juce::FontOptions(10.0f)));
g.drawText(juce::String(i + 1), static_cast<int>(px + 2.0f), static_cast<int>(bounds.getY()),
20, 14, juce::Justification::centredLeft);
// Triangle at top
float triSize = (isActive || isSelected) ? 5.0f : 4.0f;
juce::Colour triCol = isSelected ? col.brighter(0.3f) : col;
g.setColour(isActive ? col : triCol);
juce::Path tri;
tri.addTriangle(px - triSize, bounds.getY(), px + triSize, bounds.getY(), px, bounds.getY() + triSize * 2.0f);
g.fillPath(tri);
}
}
void WaveformDisplay::drawRuler(juce::Graphics& g, juce::Rectangle<float> bounds)
{
g.setColour(juce::Colour(0xff2a2a4a));
g.fillRect(bounds);
// Key area in ruler too
g.setColour(juce::Colour(0xff222233));
g.fillRect(bounds.withWidth(keyWidth));
if (!sampleBuffer || sampleRate <= 0) return;
int start = getVisibleStart();
int end = getVisibleEnd();
float w = bounds.getWidth() - keyWidth;
float offsetX = keyWidth;
g.setColour(juce::Colour(0xff888888));
g.setFont(9.0f);
double visibleSeconds = static_cast<double>(end - start) / sampleRate;
double tickInterval = 0.1;
if (visibleSeconds > 5.0) tickInterval = 1.0;
else if (visibleSeconds > 2.0) tickInterval = 0.5;
else if (visibleSeconds > 0.5) tickInterval = 0.1;
else tickInterval = 0.01;
double startSec = static_cast<double>(start) / sampleRate;
double firstTick = std::ceil(startSec / tickInterval) * tickInterval;
for (double t = firstTick; t < static_cast<double>(end) / sampleRate; t += tickInterval)
{
int samp = static_cast<int>(t * sampleRate);
float px = bounds.getX() + offsetX + static_cast<float>(samp - start) / static_cast<float>(end - start) * w;
g.drawVerticalLine(static_cast<int>(px), bounds.getY() + 12.0f, bounds.getBottom());
juce::String label = juce::String(t, 2) + "s";
g.drawText(label, static_cast<int>(px + 2.0f), static_cast<int>(bounds.getY()),
40, 12, juce::Justification::centredLeft);
}
}
void WaveformDisplay::resized()
{
auto bounds = getLocalBounds();
auto scrollBarRow = bounds.removeFromBottom(static_cast<int>(scrollbarHeight));
scrollLeftBtn.setBounds(scrollBarRow.getX(), scrollBarRow.getY(), scrollBtnWidth, scrollBarRow.getHeight());
scrollRightBtn.setBounds(scrollBarRow.getRight() - scrollBtnWidth, scrollBarRow.getY(), scrollBtnWidth, scrollBarRow.getHeight());
hScrollBar.setBounds(scrollBarRow.getX() + scrollBtnWidth, scrollBarRow.getY(),
scrollBarRow.getWidth() - scrollBtnWidth * 2, scrollBarRow.getHeight());
}
void WaveformDisplay::mouseDown(const juce::MouseEvent& e)
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
{
if (onEmptyAreaClick)
onEmptyAreaClick();
return;
}
auto bounds = getLocalBounds().toFloat();
auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight);
// Check if click is in the key area
if (e.getPosition().getX() < static_cast<int>(keyArea.getWidth()))
{
float rowHeight = keyArea.getHeight() / 12.0f;
int row = static_cast<int>((e.getPosition().getY() - keyArea.getY()) / rowHeight);
row = juce::jlimit(0, 11, row);
int midiNote = keyRowToMidiNote(row);
if (onKeyClick)
onKeyClick(midiNote);
return;
}
// Right click removes slice
if (e.mods.isRightButtonDown())
{
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
auto area = getWaveformArea();
int range = getVisibleEnd() - getVisibleStart();
int tolerance = (range > 0) ? juce::jmax(1, static_cast<int>(10.0f * static_cast<float>(range) / area.getWidth())) : 1;
sliceManager.removeSliceAt(sample, tolerance);
return;
}
// Left click on waveform area
if (e.mods.isLeftButtonDown())
{
int px = static_cast<int>(e.getPosition().getX());
int sample = xToSample(px);
auto area = getWaveformArea();
int range = getVisibleEnd() - getVisibleStart();
int tolerancePx = 10;
int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast<int>(static_cast<float>(tolerancePx) * static_cast<float>(range) / area.getWidth())) : 1;
// Check if clicking near an existing marker — drag it
const auto& slices = sliceManager.getSlices();
int nearIdx = -1;
int nearDist = toleranceSamples + 1;
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
int dist = std::abs(slices[static_cast<size_t>(i)].startSample - sample);
if (dist < nearDist)
{
nearDist = dist;
nearIdx = i;
}
}
if (nearIdx >= 0)
{
draggingSliceIndex = nearIdx;
}
else
{
sliceManager.addSliceManual(sample);
// Enter drag mode for the newly placed marker
const auto& slices2 = sliceManager.getSlices();
int bestIdx = -1;
int bestDist = std::numeric_limits<int>::max();
for (int i = 0; i < static_cast<int>(slices2.size()); ++i)
{
int dist = std::abs(slices2[static_cast<size_t>(i)].startSample - sample);
if (dist < bestDist)
{
bestDist = dist;
bestIdx = i;
}
}
draggingSliceIndex = bestIdx;
}
}
}
void WaveformDisplay::mouseDrag(const juce::MouseEvent& e)
{
if (draggingSliceIndex >= 0)
{
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
const auto& slices = sliceManager.getSlices();
if (draggingSliceIndex < static_cast<int>(slices.size()))
sliceManager.moveSlice(slices[static_cast<size_t>(draggingSliceIndex)].startSample, sample);
}
}
void WaveformDisplay::mouseUp(const juce::MouseEvent&) { draggingSliceIndex = -1; }
void WaveformDisplay::mouseMove(const juce::MouseEvent& e)
{
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
{
hoveredSliceIndex = -1;
return;
}
auto area = getWaveformArea();
if (e.getPosition().getX() < static_cast<int>(area.getX()) || e.getPosition().getX() > static_cast<int>(area.getRight()))
{
hoveredSliceIndex = -1;
return;
}
int range = getVisibleEnd() - getVisibleStart();
int tolerancePx = 10;
int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast<int>(static_cast<float>(tolerancePx) * static_cast<float>(range) / area.getWidth())) : 1;
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
const auto& slices = sliceManager.getSlices();
int bestIdx = -1;
int bestDist = toleranceSamples + 1;
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
{
int dist = std::abs(slices[static_cast<size_t>(i)].startSample - sample);
if (dist < bestDist)
{
bestDist = dist;
bestIdx = i;
}
}
hoveredSliceIndex = bestIdx;
}
void WaveformDisplay::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
{
auto area = getWaveformArea();
// Handle horizontal scrolling (trackpad 2-finger horizontal drag)
if (std::abs(wheel.deltaX) > 0.001f)
{
int total = getNumSamples();
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
float maxOffset = static_cast<float>(juce::jmax(1, total - visible));
scrollOffset -= wheel.deltaX * 0.02f / zoomFactor;
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
syncScrollBar();
repaint();
return;
}
// Handle zoom via vertical scroll
float oldZoom = zoomFactor;
zoomFactor *= (1.0f + wheel.deltaY * 0.3f);
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
float mouseXRatio = (static_cast<float>(e.getPosition().getX()) - area.getX()) / area.getWidth();
mouseXRatio = juce::jlimit(0.0f, 1.0f, mouseXRatio);
int oldVisible = static_cast<int>(static_cast<float>(getNumSamples()) / oldZoom);
int newVisible = static_cast<int>(static_cast<float>(getNumSamples()) / zoomFactor);
float mouseSample = scrollOffset * static_cast<float>(juce::jmax(0, getNumSamples() - oldVisible)) + mouseXRatio * static_cast<float>(oldVisible);
scrollOffset = (mouseSample - mouseXRatio * static_cast<float>(newVisible)) / static_cast<float>(juce::jmax(1, getNumSamples() - newVisible));
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
syncScrollBar();
repaint();
}

86
Source/WaveformDisplay.h Normal file
View file

@ -0,0 +1,86 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "SliceManager.h"
#include <functional>
class WaveformDisplay : public juce::Component,
public juce::Timer,
private juce::ScrollBar::Listener
{
public:
static constexpr float keyWidth = 50.0f;
static constexpr float scrollbarHeight = 14.0f;
WaveformDisplay(SliceManager& sm);
~WaveformDisplay() override;
void paint(juce::Graphics& g) override;
void resized() override;
void timerCallback() override;
void mouseDown(const juce::MouseEvent& e) override;
void mouseDrag(const juce::MouseEvent& e) override;
void mouseUp(const juce::MouseEvent& e) override;
void mouseMove(const juce::MouseEvent& e) override;
void mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) override;
void setSampleBuffer(const juce::AudioBuffer<float>* buffer, double sampleRate);
void setPlaybackPosition(int sample);
void setActiveSliceIndex(int index);
void setSelectedSlice(int index);
void setOnEmptyAreaClick(std::function<void()> callback);
void setOnKeyClick(std::function<void(int noteNumber)> callback);
void zoomIn();
void zoomOut();
void zoomReset();
int xToSample(int x) const;
int sampleToX(int sample) const;
float getZoomFactor() const { return zoomFactor; }
private:
void drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawWaveform(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawSliceMarkers(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawRuler(juce::Graphics& g, juce::Rectangle<float> bounds);
void scrollBarMoved(juce::ScrollBar* scrollBarThatHasMoved, double newRangeStart) override;
void syncScrollBar();
SliceManager& sliceManager;
const juce::AudioBuffer<float>* sampleBuffer = nullptr;
double sampleRate = 44100.0;
float zoomFactor = 1.0f;
float scrollOffset = 0.0f;
int playbackSample = -1;
int activeSliceIndex = -1;
int selectedSliceIndex = -1;
int draggingSliceIndex = -1;
int hoveredSliceIndex = -1;
std::function<void()> onEmptyAreaClick;
std::function<void(int)> onKeyClick;
juce::ScrollBar hScrollBar { false };
juce::TextButton scrollLeftBtn { "<" };
juce::TextButton scrollRightBtn { ">" };
bool updatingScroll = false;
static constexpr int scrollBtnWidth = 16;
juce::AudioBuffer<float> overviewBuffer;
bool overviewDirty = true;
static constexpr int overviewResolution = 2048;
void rebuildOverview();
int getNumSamples() const { return sampleBuffer ? sampleBuffer->getNumSamples() : 0; }
int getVisibleStart() const;
int getVisibleEnd() const;
juce::Rectangle<float> getWaveformArea() const;
int keyRowToMidiNote(int row) const;
int midiNoteToSliceIndex(int note) const;
int findNearestSliceAtPixel(int pixelX, int tolerancePx) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveformDisplay)
};