mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 04:10:45 +02:00
Reduce plugin width by 102px and fix bottom panel layout to scale proportionally
This commit is contained in:
parent
d9302ad77a
commit
f035e89ba9
9 changed files with 305 additions and 106 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
&AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider,
|
||||
&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 : { &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider })
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue