Reduce plugin width by 102px and fix bottom panel layout to scale proportionally

This commit is contained in:
Armin 2026-07-22 23:10:55 +02:00
commit f035e89ba9
9 changed files with 305 additions and 106 deletions

View file

@ -27,11 +27,12 @@ juce::Colour PianoRollDisplay::sliceColour(int sliceIndex) const
int PianoRollDisplay::rowToSliceIndex(int row) const
{
int noteRow = numRows - 1 - row;
const int baseMidiNote = 60;
int midiNote = baseMidiNote + (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)
if (slices[static_cast<size_t>(i)].midiNote == midiNote)
return i;
}
return -1;
@ -68,7 +69,8 @@ void PianoRollDisplay::paint(juce::Graphics& g)
for (int i = 0; i < numSlices; ++i)
{
const auto& slice = slices[static_cast<size_t>(i)];
int noteRow = slice.midiNote % numRows;
const int baseMidiNote = 60;
int noteRow = slice.midiNote - baseMidiNote;
int row = numRows - 1 - noteRow;
float xFrac = static_cast<float>(slice.startSample) / static_cast<float>(total);

View file

@ -7,7 +7,7 @@ class PianoRollDisplay : public juce::Component,
public juce::Timer
{
public:
static constexpr int numRows = 12;
static constexpr int numRows = 25;
static constexpr int baseMidiNote = 60;
static constexpr float keyWidth = 50.0f;

View file

@ -3,9 +3,11 @@
MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
: AudioProcessorEditor(p),
processorRef(p),
waveformDisplay(p.getSliceManager())
waveformDisplay(p.getSliceManager()),
vuMeter(p)
{
setSize(baseWidth, baseHeight);
setLookAndFeel(&lnf);
// Title bar buttons
auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) {
@ -13,6 +15,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
btn.addListener(this);
btn.setTooltip(tip);
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
btn.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
};
setupTitleBtn(loadSampleButton, "Load audio file");
@ -36,6 +39,10 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
scaleComboBox.addItem("200%", 6);
scaleComboBox.addListener(this);
scaleComboBox.setTooltip("UI Scale");
scaleComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a));
scaleComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white);
scaleComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
scaleComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
scaleAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "uiScale", scaleComboBox);
addAndMakeVisible(octaveComboBox);
@ -52,9 +59,13 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
// Bottom panel buttons
addAndMakeVisible(autoSliceButton);
autoSliceButton.addListener(this);
autoSliceButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
autoSliceButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
addAndMakeVisible(clearSlicesButton);
clearSlicesButton.addListener(this);
clearSlicesButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
clearSlicesButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
addAndMakeVisible(waveformDisplay);
// Status bar labels
@ -103,6 +114,12 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
filterTypeComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
filterTypeComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
addAndMakeVisible(filterTypeLabel);
filterTypeLabel.setText("Filter Type", juce::dontSendNotification);
filterTypeLabel.setJustificationType(juce::Justification::centred);
filterTypeLabel.setFont(juce::Font(juce::FontOptions(13.0f)));
filterTypeLabel.attachToComponent(&filterTypeComboBox, false);
// Filter ADSR
attachSlider(filterAttackSlider, filterAttackLabel, "F Atk", "filterAttack", filterAttackAttachment);
attachSlider(filterDecaySlider, filterDecayLabel, "F Dcy", "filterDecay", filterDecayAttachment);
@ -115,7 +132,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
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::rotarySliderFillColourId, juce::Colour(0xff3a73e5));
bpmSlider.setColour(juce::Slider::textBoxTextColourId, juce::Colours::white);
bpmSlider.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2a4a));
bpmSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0xff444466));
@ -140,6 +157,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
addAndMakeVisible(stretchButton);
stretchButton.addListener(this);
stretchButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
stretchButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
// Key shift combo
addAndMakeVisible(keyShiftComboBox);
@ -157,6 +175,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
keyShiftLabel.attachToComponent(&keyShiftComboBox, false);
keyShiftAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "keyShift", keyShiftComboBox);
addAndMakeVisible(vuMeter);
auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider,
&ampAttackSlider, &ampDecaySlider, &ampSustainSlider, &ampReleaseSlider,
&filterCutoffSlider, &filterResoSlider, &filterEnvDepthSlider,
@ -167,8 +187,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
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));
slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xff3a73e5));
slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff1a192d));
}
if (processorRef.getSampleBuffer().getNumSamples() > 0)
@ -182,12 +202,11 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
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)
if (slices[static_cast<size_t>(i)].midiNote == midiNote)
{
sliceIdx = i;
break;
@ -195,8 +214,6 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
}
if (sliceIdx >= 0)
processorRef.triggerSlice(sliceIdx, 0.8f);
else
processorRef.triggerSlice(0, 0.8f);
});
startTimerHz(30);
@ -297,11 +314,23 @@ void MonoslicerEditor::resized()
scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24);
octaveComboBox.setBounds(titleBar.getRight() - 176, titleBar.getY() + 8, 72, 24);
auto bottomPanel = bounds.removeFromBottom(200);
auto bottomPanel = bounds.removeFromBottom(236);
auto bottomRow = bottomPanel.reduced(8);
bottomRow.removeFromTop(10);
int bw = bottomRow.getWidth();
int sectionGap = 8;
int comboGap = 4;
int contentW = bw - 2 * sectionGap - comboGap;
int leftW = static_cast<int>(contentW * 0.21);
int comboW = static_cast<int>(contentW * 0.12);
int kw = (contentW - leftW - comboW) / 7;
int adsrW = 4 * kw;
int filterW = 3 * kw + comboW + comboGap;
// Bottom panel: buttons + stretch | amp ADSR | filter ADSR | filter knobs
auto leftArea = bottomRow.removeFromLeft(200);
auto leftArea = bottomRow.removeFromLeft(leftW);
{
auto btnArea = leftArea.removeFromTop(28);
autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24);
@ -318,45 +347,39 @@ void MonoslicerEditor::resized()
leftArea.removeFromTop(26);
leftArea.removeFromTop(20);
keyShiftComboBox.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 20);
leftArea.removeFromTop(24);
vuMeter.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 40);
}
bottomRow.removeFromLeft(8);
bottomRow.removeFromLeft(sectionGap);
auto ampSection = bottomRow.removeFromLeft(230);
auto adsrSection = bottomRow.removeFromLeft(adsrW);
{
int kw = ampSection.getWidth() / 4;
int topRowH = 80;
int bottomRowGap = 50;
int bottomRowH = adsrSection.getHeight() - topRowH - bottomRowGap;
int k = 0;
for (auto* s : { &ampAttackSlider, &ampDecaySlider, &ampSustainSlider, &ampReleaseSlider })
{
s->setBounds(ampSection.getX() + k * kw, ampSection.getY(), kw, ampSection.getHeight());
s->setBounds(adsrSection.getX() + k * kw, adsrSection.getY(), kw, topRowH);
++k;
}
}
bottomRow.removeFromLeft(8);
auto fltAdsSection = bottomRow.removeFromLeft(230);
{
int kw = fltAdsSection.getWidth() / 4;
int k = 0;
int bottomY = adsrSection.getY() + topRowH + bottomRowGap;
k = 0;
for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider })
{
s->setBounds(fltAdsSection.getX() + k * kw, fltAdsSection.getY(), kw, fltAdsSection.getHeight());
s->setBounds(adsrSection.getX() + k * kw, bottomY, kw, bottomRowH);
++k;
}
}
bottomRow.removeFromLeft(8);
bottomRow.removeFromLeft(sectionGap);
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 knobAreaX = filterArea.getX() + comboW + comboGap;
int topRowH = 80;
int bottomRowGap = 24;
int bottomRowGap = 50;
int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap;
filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20);
filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH);
@ -389,8 +412,8 @@ void MonoslicerEditor::timerCallback()
/ processorRef.getSampleRateLoaded();
int mins = static_cast<int>(duration) / 60;
int secs = static_cast<int>(duration) % 60;
fileInfoLabel.setButtonText(file.getFileName() + " "
+ juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2));
fileInfoLabel.setButtonText("File: " + file.getFileName() + " [Time: "
+ juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2) + "]");
}
else
{
@ -458,11 +481,11 @@ void MonoslicerEditor::buttonClicked(juce::Button* button)
}
else if (button == &undoButton)
{
processorRef.getSliceManager().undo();
processorRef.undo();
}
else if (button == &redoButton)
{
processorRef.getSliceManager().redo();
processorRef.redo();
}
else if (button == &stretchButton)
{

View file

@ -3,6 +3,103 @@
#include "PluginProcessor.h"
#include "WaveformDisplay.h"
class MonoslicerLookAndFeel : public juce::LookAndFeel_V4
{
public:
void drawButtonBackground(juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool isMouseOverButton, bool isButtonDown) override
{
if (backgroundColour.getAlpha() == 0)
return;
auto bounds = button.getLocalBounds().toFloat().reduced(0.5f);
auto cornerSize = 4.0f;
auto baseColour = backgroundColour.withMultipliedAlpha(button.isEnabled() ? 1.0f : 0.5f);
g.setColour(baseColour);
g.fillRoundedRectangle(bounds, cornerSize);
g.setColour(juce::Colour(0xff444466));
g.drawRoundedRectangle(bounds, cornerSize, 1.0f);
}
void drawButtonText(juce::Graphics& g, juce::TextButton& button,
bool isMouseOverButton, bool isButtonDown) override
{
auto font = getTextButtonFont(button, button.getHeight());
g.setFont(font);
g.setColour(button.findColour(button.getToggleState() ? juce::TextButton::textColourOnId
: juce::TextButton::textColourOffId)
.withMultipliedAlpha(button.isEnabled() ? 1.0f : 0.5f));
auto textArea = button.getLocalBounds().reduced(6, 1);
g.drawText(button.getButtonText(), textArea, juce::Justification::centredLeft, true);
}
};
class VuMeter : public juce::Component, public juce::Timer
{
public:
VuMeter(MonoslicerProcessor& p) : processor(p) { startTimerHz(30); }
~VuMeter() override { stopTimer(); }
void paint(juce::Graphics& g) override
{
auto bounds = getLocalBounds().toFloat();
float legendW = 20.0f;
float legendGap = 5.0f;
float barH = (bounds.getHeight() - 6.0f) / 2.0f;
float gap = 6.0f;
float barX = bounds.getX() + legendW + legendGap;
float barW = bounds.getWidth() - legendW - legendGap;
float levelL = processor.outputLevelL.load();
float levelR = processor.outputLevelR.load();
// Apply falloff decay
peakL = juce::jmax(levelL, peakL - decayRate);
peakR = juce::jmax(levelR, peakR - decayRate);
if (levelL > peakL) peakL = levelL;
if (levelR > peakR) peakR = levelR;
auto drawBar = [&](float y, float level, float peak, const juce::String& legend)
{
float frac = juce::jlimit(0.0f, 1.0f, level);
float peakFrac = juce::jlimit(0.0f, 1.0f, peak);
// Legend text
g.setColour(juce::Colour(0xff888888));
g.setFont(juce::Font(10.0f));
g.drawText(legend, bounds.getX(), y, legendW, barH, juce::Justification::centredRight);
// Inactive background
g.setColour(juce::Colour(0xff1a192d));
g.fillRoundedRectangle(barX, y, barW, barH, 2.0f);
// Active fill from left
float fillW = barW * frac;
g.setColour(juce::Colour(0xff3a73e5));
g.fillRoundedRectangle(barX, y, fillW, barH, 2.0f);
// Peak hold indicator
float peakX = barX + barW * peakFrac - 2.0f;
g.setColour(juce::Colour(0xff6699cc));
g.fillRect(peakX, y + 2.0f, 2.0f, barH - 4.0f);
};
drawBar(bounds.getY(), levelL, peakL, "L");
drawBar(bounds.getY() + barH + gap, levelR, peakR, "R");
}
void timerCallback() override { repaint(); }
private:
MonoslicerProcessor& processor;
float peakL = 0.0f, peakR = 0.0f;
float decayRate = 0.02f;
};
class MonoslicerEditor : public juce::AudioProcessorEditor,
private juce::FileDragAndDropTarget,
private juce::Button::Listener,
@ -23,6 +120,7 @@ public:
void timerCallback() override;
private:
MonoslicerLookAndFeel lnf;
MonoslicerProcessor& processorRef;
WaveformDisplay waveformDisplay;
@ -46,8 +144,8 @@ private:
juce::ComboBox scaleComboBox;
juce::ComboBox octaveComboBox;
static constexpr int baseWidth = 1100;
static constexpr int baseHeight = 570;
static constexpr int baseWidth = 920;
static constexpr int baseHeight = 606;
// Slice knobs
juce::Slider bassSlider, trebleSlider, sensitivitySlider;
@ -59,6 +157,7 @@ private:
// Filter type selector
juce::ComboBox filterTypeComboBox;
juce::Label filterTypeLabel;
// Filter knobs
juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider;
@ -77,6 +176,7 @@ private:
juce::TextButton stretchButton { "Match Length" };
juce::ComboBox keyShiftComboBox;
juce::Label keyShiftLabel;
VuMeter vuMeter;
std::unique_ptr<SliderAttachment> stretchBpmAttachment;
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;

View file

@ -8,6 +8,7 @@ MonoslicerProcessor::MonoslicerProcessor()
apvts(*this, nullptr, "Parameters", createParameterLayout())
{
formatManager.registerBasicFormats();
sliceManager.setBeforeChangeCallback([this]() { pushUndoState(); });
}
MonoslicerProcessor::~MonoslicerProcessor() {}
@ -444,6 +445,27 @@ void MonoslicerProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::M
}
if (currentActiveSlice.load() < 0)
currentPlaybackSample.store(-1);
float peakL = 0.0f;
float peakR = 0.0f;
if (buffer.getNumChannels() > 0)
{
auto* data = buffer.getReadPointer(0);
for (int i = 0; i < buffer.getNumSamples(); ++i)
peakL = juce::jmax(peakL, std::abs(data[i]));
}
if (buffer.getNumChannels() > 1)
{
auto* data = buffer.getReadPointer(1);
for (int i = 0; i < buffer.getNumSamples(); ++i)
peakR = juce::jmax(peakR, std::abs(data[i]));
}
else
{
peakR = peakL;
}
outputLevelL.store(peakL);
outputLevelR.store(peakR);
}
void MonoslicerProcessor::handleNoteOn(int noteNumber, float velocity)
@ -496,6 +518,7 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file)
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
if (reader)
{
pushUndoState();
int numSamples = static_cast<int>(reader->lengthInSamples);
sampleBuffer.setSize(static_cast<int>(reader->numChannels), numSamples);
reader->read(&sampleBuffer, 0, numSamples, 0, true, true);
@ -509,6 +532,7 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file)
void MonoslicerProcessor::clearSample()
{
pushUndoState();
sampleBuffer.setSize(0, 0);
currentFile = juce::File{};
folderFiles.clear();
@ -517,6 +541,66 @@ void MonoslicerProcessor::clearSample()
selectedSlice.store(-1);
}
void MonoslicerProcessor::pushUndoState()
{
if (restoringFromUndo) return;
undoStack.push_back({ currentFile, sliceManager.getSlices() });
if (static_cast<int>(undoStack.size()) > maxUndoDepth)
undoStack.erase(undoStack.begin());
redoStack.clear();
}
void MonoslicerProcessor::undo()
{
if (undoStack.empty()) return;
redoStack.push_back({ currentFile, sliceManager.getSlices() });
auto state = std::move(undoStack.back());
undoStack.pop_back();
restoringFromUndo = true;
if (state.file.existsAsFile())
{
loadAudioFile(state.file);
sliceManager.setSlices(state.slices);
}
else
{
sampleBuffer.setSize(0, 0);
currentFile = juce::File{};
folderFiles.clear();
currentFileIndex = -1;
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
selectedSlice.store(-1);
}
restoringFromUndo = false;
}
void MonoslicerProcessor::redo()
{
if (redoStack.empty()) return;
undoStack.push_back({ currentFile, sliceManager.getSlices() });
auto state = std::move(redoStack.back());
redoStack.pop_back();
restoringFromUndo = true;
if (state.file.existsAsFile())
{
loadAudioFile(state.file);
sliceManager.setSlices(state.slices);
}
else
{
sampleBuffer.setSize(0, 0);
currentFile = juce::File{};
folderFiles.clear();
currentFileIndex = -1;
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
selectedSlice.store(-1);
}
restoringFromUndo = false;
}
bool MonoslicerProcessor::canUndo() const { return !undoStack.empty(); }
bool MonoslicerProcessor::canRedo() const { return !redoStack.empty(); }
void MonoslicerProcessor::refreshFolderList()
{
folderFiles.clear();
@ -678,6 +762,8 @@ void MonoslicerProcessor::stretchToBeats(int numBeats, float bpm)
{
if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return;
pushUndoState();
double sr = static_cast<double>(loadedSampleRate);
int currentLength = sampleBuffer.getNumSamples();
double currentDuration = static_cast<double>(currentLength) / sr;

View file

@ -43,6 +43,11 @@ public:
void psolaStretch(int targetLength);
float getHostBpm() const;
void undo();
void redo();
bool canUndo() const;
bool canRedo() const;
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
SliceManager& getSliceManager() { return sliceManager; }
@ -56,6 +61,8 @@ public:
std::atomic<int> currentPlaybackSample { -1 };
std::atomic<int> currentActiveSlice { -1 };
std::atomic<int> selectedSlice { -1 };
std::atomic<float> outputLevelL { 0.0f };
std::atomic<float> outputLevelR { 0.0f };
private:
juce::AudioProcessorValueTreeState apvts;
@ -73,6 +80,17 @@ private:
int currentFileIndex = -1;
void refreshFolderList();
struct UndoState
{
juce::File file;
std::vector<SliceManager::Slice> slices;
};
std::vector<UndoState> undoStack;
std::vector<UndoState> redoStack;
static constexpr int maxUndoDepth = 10;
void pushUndoState();
bool restoringFromUndo = false;
enum class AmpStage { Idle, Attack, Decay, Sustain, Release };
enum class FilterType { LP12, LP24, HP, BP, Notch };

View file

@ -40,7 +40,8 @@ void SliceManager::autoSlice()
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
return;
pushHistory();
if (beforeChangeCallback) beforeChangeCallback();
const int numSamples = sampleBuffer->getNumSamples();
const int numChannels = sampleBuffer->getNumChannels();
@ -106,7 +107,8 @@ void SliceManager::autoSlice()
void SliceManager::clearSlices()
{
const juce::CriticalSection::ScopedLockType lock(mutex);
pushHistory();
if (beforeChangeCallback) beforeChangeCallback();
slices.clear();
}
@ -121,12 +123,13 @@ 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())
{
pushHistory();
slices.push_back({ 0, numSamples, 0 });
assignMidiNotes();
return;
@ -135,7 +138,7 @@ void SliceManager::addSliceManual(int samplePos)
int idx = findSliceIndexForSample(samplePos);
if (idx < 0)
{
pushHistory();
slices.push_back({ samplePos, numSamples, 0 });
sortSlices();
assignMidiNotes();
@ -146,7 +149,7 @@ void SliceManager::addSliceManual(int samplePos)
if (samplePos <= parent.startSample || samplePos >= parent.endSample)
return;
pushHistory();
int oldEnd = parent.endSample;
parent.endSample = samplePos;
@ -159,12 +162,13 @@ void SliceManager::addSliceManual(int samplePos)
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)
{
pushHistory();
int removedEnd = it->endSample;
auto idx = static_cast<int>(std::distance(slices.begin(), it));
slices.erase(it);
@ -179,11 +183,12 @@ void SliceManager::removeSliceAt(int samplePos, int tolerance)
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)
{
pushHistory();
slices[i].startSample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, toSample);
if (i > 0)
slices[i - 1].endSample = slices[i].startSample;
@ -199,10 +204,11 @@ 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;
pushHistory();
const int numChannels = sampleBuffer->getNumChannels();
const int oldNumSamples = sampleBuffer->getNumSamples();
@ -239,11 +245,12 @@ 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;
pushHistory();
const int numChannels = sampleBuffer->getNumChannels();
@ -283,8 +290,10 @@ void SliceManager::sortSlices()
void SliceManager::assignMidiNotes()
{
const int numRows = 25;
const int baseMidiNote = 60;
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
slices[static_cast<size_t>(i)].midiNote = i % 12;
slices[static_cast<size_t>(i)].midiNote = baseMidiNote + (i % numRows);
}
std::vector<float> SliceManager::computeEnvelope(const float* data, int numSamples)
@ -364,37 +373,3 @@ std::vector<float> SliceManager::detectOnsets(const std::vector<float>& envelope
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()
{
const juce::CriticalSection::ScopedLockType lock(mutex);
if (undoStack.empty()) return;
redoStack.push_back(slices);
slices = undoStack.back();
undoStack.pop_back();
}
void SliceManager::redo()
{
const juce::CriticalSection::ScopedLockType lock(mutex);
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(); }

View file

@ -4,6 +4,7 @@
#include <vector>
#include <algorithm>
#include <cmath>
#include <functional>
class SliceManager
{
@ -12,7 +13,7 @@ public:
{
int startSample;
int endSample;
int midiNote; // pitch class (0 = C, 1 = C#, ..., 11 = B)
int midiNote; // full MIDI note (60 = C4, 61 = C#4, ..., 83 = B5)
};
SliceManager();
@ -31,17 +32,14 @@ public:
void trimStart(int sample);
void trimEnd(int sample);
void undo();
void redo();
bool canUndo() const;
bool canRedo() const;
std::vector<Slice> getSlices() const;
int getNumSlices() const;
Slice getSlice(int index) const;
void setSlices(const std::vector<Slice>& newSlices);
void setBeforeChangeCallback(std::function<void()> cb) { beforeChangeCallback = std::move(cb); }
int findSliceIndexForSample(int samplePos) const;
juce::AudioBuffer<float>* getMutableBuffer() { return sampleBuffer; }
@ -49,14 +47,9 @@ public:
private:
void sortSlices();
void assignMidiNotes();
void pushHistory();
void truncateHistory();
mutable juce::CriticalSection mutex;
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;
@ -64,6 +57,8 @@ private:
float bassGain = 1.0f;
float trebleGain = 1.0f;
std::function<void()> beforeChangeCallback;
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);

View file

@ -171,19 +171,18 @@ int WaveformDisplay::sampleToX(int sample) const
int WaveformDisplay::keyRowToMidiNote(int row) const
{
const int numRows = 12;
const int numRows = 25;
const int baseMidiNote = 60;
return baseMidiNote + (numRows - 1 - row);
}
int WaveformDisplay::midiNoteToSliceIndex(int note) const
{
int pitchClass = note % 12;
const auto& slices = sliceManager.getSlices();
int numSlices = sliceManager.getNumSlices();
for (int i = 0; i < numSlices; ++i)
{
if (slices[static_cast<size_t>(i)].midiNote == pitchClass)
if (slices[static_cast<size_t>(i)].midiNote == note)
return i;
}
return -1;
@ -223,12 +222,12 @@ void WaveformDisplay::paint(juce::Graphics& g)
auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight);
// Ruler across full width (drawn first so keys can overlap)
drawRuler(g, waveBounds.removeFromTop(20.0f));
// 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);
@ -240,7 +239,7 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
g.setColour(juce::Colour(0xff222233));
g.fillRect(bounds);
const int numRows = 12;
const int numRows = 25;
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
juce::Colour markerColours[] = {
@ -545,14 +544,15 @@ void WaveformDisplay::mouseDown(const juce::MouseEvent& e)
}
auto bounds = getLocalBounds().toFloat();
auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight);
auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight).withTrimmedTop(20.0f);
// Check if click is in the key area
if (e.getPosition().getX() < static_cast<int>(keyArea.getWidth()))
{
float rowHeight = keyArea.getHeight() / 12.0f;
const int numRows = 25;
float rowHeight = keyArea.getHeight() / static_cast<float>(numRows);
int row = static_cast<int>((static_cast<float>(e.getPosition().getY()) - keyArea.getY()) / rowHeight);
row = juce::jlimit(0, 11, row);
row = juce::jlimit(0, numRows - 1, row);
int midiNote = keyRowToMidiNote(row);
if (onKeyClick)