mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 12:10:46 +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 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();
|
const auto& slices = sliceManager.getSlices();
|
||||||
for (int i = 0; i < sliceManager.getNumSlices(); ++i)
|
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 i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -68,7 +69,8 @@ void PianoRollDisplay::paint(juce::Graphics& g)
|
||||||
for (int i = 0; i < numSlices; ++i)
|
for (int i = 0; i < numSlices; ++i)
|
||||||
{
|
{
|
||||||
const auto& slice = slices[static_cast<size_t>(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;
|
int row = numRows - 1 - noteRow;
|
||||||
|
|
||||||
float xFrac = static_cast<float>(slice.startSample) / static_cast<float>(total);
|
float xFrac = static_cast<float>(slice.startSample) / static_cast<float>(total);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class PianoRollDisplay : public juce::Component,
|
||||||
public juce::Timer
|
public juce::Timer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static constexpr int numRows = 12;
|
static constexpr int numRows = 25;
|
||||||
static constexpr int baseMidiNote = 60;
|
static constexpr int baseMidiNote = 60;
|
||||||
static constexpr float keyWidth = 50.0f;
|
static constexpr float keyWidth = 50.0f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
: AudioProcessorEditor(p),
|
: AudioProcessorEditor(p),
|
||||||
processorRef(p),
|
processorRef(p),
|
||||||
waveformDisplay(p.getSliceManager())
|
waveformDisplay(p.getSliceManager()),
|
||||||
|
vuMeter(p)
|
||||||
{
|
{
|
||||||
setSize(baseWidth, baseHeight);
|
setSize(baseWidth, baseHeight);
|
||||||
|
setLookAndFeel(&lnf);
|
||||||
|
|
||||||
// Title bar buttons
|
// Title bar buttons
|
||||||
auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) {
|
auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) {
|
||||||
|
|
@ -13,6 +15,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
btn.addListener(this);
|
btn.addListener(this);
|
||||||
btn.setTooltip(tip);
|
btn.setTooltip(tip);
|
||||||
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
||||||
|
btn.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
|
||||||
};
|
};
|
||||||
|
|
||||||
setupTitleBtn(loadSampleButton, "Load audio file");
|
setupTitleBtn(loadSampleButton, "Load audio file");
|
||||||
|
|
@ -36,6 +39,10 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
scaleComboBox.addItem("200%", 6);
|
scaleComboBox.addItem("200%", 6);
|
||||||
scaleComboBox.addListener(this);
|
scaleComboBox.addListener(this);
|
||||||
scaleComboBox.setTooltip("UI Scale");
|
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);
|
scaleAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "uiScale", scaleComboBox);
|
||||||
|
|
||||||
addAndMakeVisible(octaveComboBox);
|
addAndMakeVisible(octaveComboBox);
|
||||||
|
|
@ -52,9 +59,13 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
// Bottom panel buttons
|
// Bottom panel buttons
|
||||||
addAndMakeVisible(autoSliceButton);
|
addAndMakeVisible(autoSliceButton);
|
||||||
autoSliceButton.addListener(this);
|
autoSliceButton.addListener(this);
|
||||||
|
autoSliceButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
||||||
|
autoSliceButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
|
||||||
|
|
||||||
addAndMakeVisible(clearSlicesButton);
|
addAndMakeVisible(clearSlicesButton);
|
||||||
clearSlicesButton.addListener(this);
|
clearSlicesButton.addListener(this);
|
||||||
|
clearSlicesButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
||||||
|
clearSlicesButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
|
||||||
addAndMakeVisible(waveformDisplay);
|
addAndMakeVisible(waveformDisplay);
|
||||||
|
|
||||||
// Status bar labels
|
// Status bar labels
|
||||||
|
|
@ -103,6 +114,12 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
filterTypeComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
|
filterTypeComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
|
||||||
filterTypeComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
|
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
|
// Filter ADSR
|
||||||
attachSlider(filterAttackSlider, filterAttackLabel, "F Atk", "filterAttack", filterAttackAttachment);
|
attachSlider(filterAttackSlider, filterAttackLabel, "F Atk", "filterAttack", filterAttackAttachment);
|
||||||
attachSlider(filterDecaySlider, filterDecayLabel, "F Dcy", "filterDecay", filterDecayAttachment);
|
attachSlider(filterDecaySlider, filterDecayLabel, "F Dcy", "filterDecay", filterDecayAttachment);
|
||||||
|
|
@ -115,7 +132,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
bpmSlider.setTextBoxStyle(juce::Slider::TextBoxLeft, false, 50, 20);
|
bpmSlider.setTextBoxStyle(juce::Slider::TextBoxLeft, false, 50, 20);
|
||||||
bpmSlider.setRange(20.0, 300.0, 1.0);
|
bpmSlider.setRange(20.0, 300.0, 1.0);
|
||||||
bpmSlider.setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack);
|
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::textBoxTextColourId, juce::Colours::white);
|
||||||
bpmSlider.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2a4a));
|
bpmSlider.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2a4a));
|
||||||
bpmSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0xff444466));
|
bpmSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0xff444466));
|
||||||
|
|
@ -140,6 +157,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
addAndMakeVisible(stretchButton);
|
addAndMakeVisible(stretchButton);
|
||||||
stretchButton.addListener(this);
|
stretchButton.addListener(this);
|
||||||
stretchButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
stretchButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
||||||
|
stretchButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
|
||||||
|
|
||||||
// Key shift combo
|
// Key shift combo
|
||||||
addAndMakeVisible(keyShiftComboBox);
|
addAndMakeVisible(keyShiftComboBox);
|
||||||
|
|
@ -157,6 +175,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
keyShiftLabel.attachToComponent(&keyShiftComboBox, false);
|
keyShiftLabel.attachToComponent(&keyShiftComboBox, false);
|
||||||
keyShiftAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "keyShift", keyShiftComboBox);
|
keyShiftAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "keyShift", keyShiftComboBox);
|
||||||
|
|
||||||
|
addAndMakeVisible(vuMeter);
|
||||||
|
|
||||||
auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider,
|
auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider,
|
||||||
&AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider,
|
&AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider,
|
||||||
&filterCutoffSlider, &filterResoSlider, &filterEnvDepthSlider,
|
&filterCutoffSlider, &filterResoSlider, &filterEnvDepthSlider,
|
||||||
|
|
@ -167,8 +187,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
slider->setSliderStyle(juce::Slider::RotaryVerticalDrag);
|
slider->setSliderStyle(juce::Slider::RotaryVerticalDrag);
|
||||||
slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
|
slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
|
||||||
slider->setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack);
|
slider->setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack);
|
||||||
slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xffee8833));
|
slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xff3a73e5));
|
||||||
slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff332211));
|
slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff1a192d));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processorRef.getSampleBuffer().getNumSamples() > 0)
|
if (processorRef.getSampleBuffer().getNumSamples() > 0)
|
||||||
|
|
@ -182,12 +202,11 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
|
|
||||||
waveformDisplay.setOnKeyClick([this](int midiNote) {
|
waveformDisplay.setOnKeyClick([this](int midiNote) {
|
||||||
keyClickNote = midiNote;
|
keyClickNote = midiNote;
|
||||||
int pitchClass = midiNote % 12;
|
|
||||||
int sliceIdx = -1;
|
int sliceIdx = -1;
|
||||||
const auto& slices = processorRef.getSliceManager().getSlices();
|
const auto& slices = processorRef.getSliceManager().getSlices();
|
||||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
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;
|
sliceIdx = i;
|
||||||
break;
|
break;
|
||||||
|
|
@ -195,8 +214,6 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p)
|
||||||
}
|
}
|
||||||
if (sliceIdx >= 0)
|
if (sliceIdx >= 0)
|
||||||
processorRef.triggerSlice(sliceIdx, 0.8f);
|
processorRef.triggerSlice(sliceIdx, 0.8f);
|
||||||
else
|
|
||||||
processorRef.triggerSlice(0, 0.8f);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
startTimerHz(30);
|
startTimerHz(30);
|
||||||
|
|
@ -297,11 +314,23 @@ void MonoslicerEditor::resized()
|
||||||
scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24);
|
scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24);
|
||||||
octaveComboBox.setBounds(titleBar.getRight() - 176, titleBar.getY() + 8, 72, 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);
|
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
|
// 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);
|
auto btnArea = leftArea.removeFromTop(28);
|
||||||
autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24);
|
autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24);
|
||||||
|
|
@ -318,45 +347,39 @@ void MonoslicerEditor::resized()
|
||||||
leftArea.removeFromTop(26);
|
leftArea.removeFromTop(26);
|
||||||
leftArea.removeFromTop(20);
|
leftArea.removeFromTop(20);
|
||||||
keyShiftComboBox.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 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;
|
int k = 0;
|
||||||
for (auto* s : { &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider })
|
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;
|
++k;
|
||||||
}
|
}
|
||||||
}
|
int bottomY = adsrSection.getY() + topRowH + bottomRowGap;
|
||||||
|
k = 0;
|
||||||
bottomRow.removeFromLeft(8);
|
|
||||||
|
|
||||||
auto fltAdsSection = bottomRow.removeFromLeft(230);
|
|
||||||
{
|
|
||||||
int kw = fltAdsSection.getWidth() / 4;
|
|
||||||
int k = 0;
|
|
||||||
for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider })
|
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;
|
++k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomRow.removeFromLeft(8);
|
bottomRow.removeFromLeft(sectionGap);
|
||||||
|
|
||||||
auto filterArea = bottomRow;
|
auto filterArea = bottomRow;
|
||||||
{
|
{
|
||||||
int comboW = 56;
|
int knobAreaX = filterArea.getX() + comboW + comboGap;
|
||||||
int gap = 4;
|
|
||||||
int knobAreaX = filterArea.getX() + comboW + gap;
|
|
||||||
int knobAreaW = filterArea.getWidth() - comboW - gap;
|
|
||||||
int kw = knobAreaW / 3;
|
|
||||||
int topRowH = 80;
|
int topRowH = 80;
|
||||||
int bottomRowGap = 24;
|
int bottomRowGap = 50;
|
||||||
int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap;
|
int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap;
|
||||||
filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20);
|
filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20);
|
||||||
filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH);
|
filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH);
|
||||||
|
|
@ -389,8 +412,8 @@ void MonoslicerEditor::timerCallback()
|
||||||
/ processorRef.getSampleRateLoaded();
|
/ processorRef.getSampleRateLoaded();
|
||||||
int mins = static_cast<int>(duration) / 60;
|
int mins = static_cast<int>(duration) / 60;
|
||||||
int secs = static_cast<int>(duration) % 60;
|
int secs = static_cast<int>(duration) % 60;
|
||||||
fileInfoLabel.setButtonText(file.getFileName() + " "
|
fileInfoLabel.setButtonText("File: " + file.getFileName() + " [Time: "
|
||||||
+ juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2));
|
+ juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2) + "]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -458,11 +481,11 @@ void MonoslicerEditor::buttonClicked(juce::Button* button)
|
||||||
}
|
}
|
||||||
else if (button == &undoButton)
|
else if (button == &undoButton)
|
||||||
{
|
{
|
||||||
processorRef.getSliceManager().undo();
|
processorRef.undo();
|
||||||
}
|
}
|
||||||
else if (button == &redoButton)
|
else if (button == &redoButton)
|
||||||
{
|
{
|
||||||
processorRef.getSliceManager().redo();
|
processorRef.redo();
|
||||||
}
|
}
|
||||||
else if (button == &stretchButton)
|
else if (button == &stretchButton)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,103 @@
|
||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "WaveformDisplay.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,
|
class MonoslicerEditor : public juce::AudioProcessorEditor,
|
||||||
private juce::FileDragAndDropTarget,
|
private juce::FileDragAndDropTarget,
|
||||||
private juce::Button::Listener,
|
private juce::Button::Listener,
|
||||||
|
|
@ -23,6 +120,7 @@ public:
|
||||||
void timerCallback() override;
|
void timerCallback() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
MonoslicerLookAndFeel lnf;
|
||||||
MonoslicerProcessor& processorRef;
|
MonoslicerProcessor& processorRef;
|
||||||
|
|
||||||
WaveformDisplay waveformDisplay;
|
WaveformDisplay waveformDisplay;
|
||||||
|
|
@ -46,8 +144,8 @@ private:
|
||||||
juce::ComboBox scaleComboBox;
|
juce::ComboBox scaleComboBox;
|
||||||
juce::ComboBox octaveComboBox;
|
juce::ComboBox octaveComboBox;
|
||||||
|
|
||||||
static constexpr int baseWidth = 1100;
|
static constexpr int baseWidth = 920;
|
||||||
static constexpr int baseHeight = 570;
|
static constexpr int baseHeight = 606;
|
||||||
|
|
||||||
// Slice knobs
|
// Slice knobs
|
||||||
juce::Slider bassSlider, trebleSlider, sensitivitySlider;
|
juce::Slider bassSlider, trebleSlider, sensitivitySlider;
|
||||||
|
|
@ -59,6 +157,7 @@ private:
|
||||||
|
|
||||||
// Filter type selector
|
// Filter type selector
|
||||||
juce::ComboBox filterTypeComboBox;
|
juce::ComboBox filterTypeComboBox;
|
||||||
|
juce::Label filterTypeLabel;
|
||||||
|
|
||||||
// Filter knobs
|
// Filter knobs
|
||||||
juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider;
|
juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider;
|
||||||
|
|
@ -77,6 +176,7 @@ private:
|
||||||
juce::TextButton stretchButton { "Match Length" };
|
juce::TextButton stretchButton { "Match Length" };
|
||||||
juce::ComboBox keyShiftComboBox;
|
juce::ComboBox keyShiftComboBox;
|
||||||
juce::Label keyShiftLabel;
|
juce::Label keyShiftLabel;
|
||||||
|
VuMeter vuMeter;
|
||||||
std::unique_ptr<SliderAttachment> stretchBpmAttachment;
|
std::unique_ptr<SliderAttachment> stretchBpmAttachment;
|
||||||
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
|
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
|
||||||
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;
|
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ MonoslicerProcessor::MonoslicerProcessor()
|
||||||
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
||||||
{
|
{
|
||||||
formatManager.registerBasicFormats();
|
formatManager.registerBasicFormats();
|
||||||
|
sliceManager.setBeforeChangeCallback([this]() { pushUndoState(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
MonoslicerProcessor::~MonoslicerProcessor() {}
|
MonoslicerProcessor::~MonoslicerProcessor() {}
|
||||||
|
|
@ -444,6 +445,27 @@ void MonoslicerProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::M
|
||||||
}
|
}
|
||||||
if (currentActiveSlice.load() < 0)
|
if (currentActiveSlice.load() < 0)
|
||||||
currentPlaybackSample.store(-1);
|
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)
|
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));
|
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
|
||||||
if (reader)
|
if (reader)
|
||||||
{
|
{
|
||||||
|
pushUndoState();
|
||||||
int numSamples = static_cast<int>(reader->lengthInSamples);
|
int numSamples = static_cast<int>(reader->lengthInSamples);
|
||||||
sampleBuffer.setSize(static_cast<int>(reader->numChannels), numSamples);
|
sampleBuffer.setSize(static_cast<int>(reader->numChannels), numSamples);
|
||||||
reader->read(&sampleBuffer, 0, numSamples, 0, true, true);
|
reader->read(&sampleBuffer, 0, numSamples, 0, true, true);
|
||||||
|
|
@ -509,6 +532,7 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file)
|
||||||
|
|
||||||
void MonoslicerProcessor::clearSample()
|
void MonoslicerProcessor::clearSample()
|
||||||
{
|
{
|
||||||
|
pushUndoState();
|
||||||
sampleBuffer.setSize(0, 0);
|
sampleBuffer.setSize(0, 0);
|
||||||
currentFile = juce::File{};
|
currentFile = juce::File{};
|
||||||
folderFiles.clear();
|
folderFiles.clear();
|
||||||
|
|
@ -517,6 +541,66 @@ void MonoslicerProcessor::clearSample()
|
||||||
selectedSlice.store(-1);
|
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()
|
void MonoslicerProcessor::refreshFolderList()
|
||||||
{
|
{
|
||||||
folderFiles.clear();
|
folderFiles.clear();
|
||||||
|
|
@ -678,6 +762,8 @@ void MonoslicerProcessor::stretchToBeats(int numBeats, float bpm)
|
||||||
{
|
{
|
||||||
if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return;
|
if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return;
|
||||||
|
|
||||||
|
pushUndoState();
|
||||||
|
|
||||||
double sr = static_cast<double>(loadedSampleRate);
|
double sr = static_cast<double>(loadedSampleRate);
|
||||||
int currentLength = sampleBuffer.getNumSamples();
|
int currentLength = sampleBuffer.getNumSamples();
|
||||||
double currentDuration = static_cast<double>(currentLength) / sr;
|
double currentDuration = static_cast<double>(currentLength) / sr;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ public:
|
||||||
void psolaStretch(int targetLength);
|
void psolaStretch(int targetLength);
|
||||||
float getHostBpm() const;
|
float getHostBpm() const;
|
||||||
|
|
||||||
|
void undo();
|
||||||
|
void redo();
|
||||||
|
bool canUndo() const;
|
||||||
|
bool canRedo() const;
|
||||||
|
|
||||||
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
|
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
|
||||||
SliceManager& getSliceManager() { return sliceManager; }
|
SliceManager& getSliceManager() { return sliceManager; }
|
||||||
|
|
||||||
|
|
@ -56,6 +61,8 @@ public:
|
||||||
std::atomic<int> currentPlaybackSample { -1 };
|
std::atomic<int> currentPlaybackSample { -1 };
|
||||||
std::atomic<int> currentActiveSlice { -1 };
|
std::atomic<int> currentActiveSlice { -1 };
|
||||||
std::atomic<int> selectedSlice { -1 };
|
std::atomic<int> selectedSlice { -1 };
|
||||||
|
std::atomic<float> outputLevelL { 0.0f };
|
||||||
|
std::atomic<float> outputLevelR { 0.0f };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
juce::AudioProcessorValueTreeState apvts;
|
juce::AudioProcessorValueTreeState apvts;
|
||||||
|
|
@ -73,6 +80,17 @@ private:
|
||||||
int currentFileIndex = -1;
|
int currentFileIndex = -1;
|
||||||
void refreshFolderList();
|
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 AmpStage { Idle, Attack, Decay, Sustain, Release };
|
||||||
enum class FilterType { LP12, LP24, HP, BP, Notch };
|
enum class FilterType { LP12, LP24, HP, BP, Notch };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ void SliceManager::autoSlice()
|
||||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
pushHistory();
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
|
|
||||||
|
|
||||||
const int numSamples = sampleBuffer->getNumSamples();
|
const int numSamples = sampleBuffer->getNumSamples();
|
||||||
const int numChannels = sampleBuffer->getNumChannels();
|
const int numChannels = sampleBuffer->getNumChannels();
|
||||||
|
|
@ -106,7 +107,8 @@ void SliceManager::autoSlice()
|
||||||
void SliceManager::clearSlices()
|
void SliceManager::clearSlices()
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
pushHistory();
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
|
|
||||||
slices.clear();
|
slices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,12 +123,13 @@ void SliceManager::addSliceManual(int samplePos)
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
if (!sampleBuffer) return;
|
if (!sampleBuffer) return;
|
||||||
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
const int numSamples = sampleBuffer->getNumSamples();
|
const int numSamples = sampleBuffer->getNumSamples();
|
||||||
samplePos = juce::jlimit(1, numSamples - 1, samplePos);
|
samplePos = juce::jlimit(1, numSamples - 1, samplePos);
|
||||||
|
|
||||||
if (slices.empty())
|
if (slices.empty())
|
||||||
{
|
{
|
||||||
pushHistory();
|
|
||||||
slices.push_back({ 0, numSamples, 0 });
|
slices.push_back({ 0, numSamples, 0 });
|
||||||
assignMidiNotes();
|
assignMidiNotes();
|
||||||
return;
|
return;
|
||||||
|
|
@ -135,7 +138,7 @@ void SliceManager::addSliceManual(int samplePos)
|
||||||
int idx = findSliceIndexForSample(samplePos);
|
int idx = findSliceIndexForSample(samplePos);
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
pushHistory();
|
|
||||||
slices.push_back({ samplePos, numSamples, 0 });
|
slices.push_back({ samplePos, numSamples, 0 });
|
||||||
sortSlices();
|
sortSlices();
|
||||||
assignMidiNotes();
|
assignMidiNotes();
|
||||||
|
|
@ -146,7 +149,7 @@ void SliceManager::addSliceManual(int samplePos)
|
||||||
if (samplePos <= parent.startSample || samplePos >= parent.endSample)
|
if (samplePos <= parent.startSample || samplePos >= parent.endSample)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
pushHistory();
|
|
||||||
|
|
||||||
int oldEnd = parent.endSample;
|
int oldEnd = parent.endSample;
|
||||||
parent.endSample = samplePos;
|
parent.endSample = samplePos;
|
||||||
|
|
@ -159,12 +162,13 @@ void SliceManager::addSliceManual(int samplePos)
|
||||||
void SliceManager::removeSliceAt(int samplePos, int tolerance)
|
void SliceManager::removeSliceAt(int samplePos, int tolerance)
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
for (auto it = slices.begin(); it != slices.end(); ++it)
|
for (auto it = slices.begin(); it != slices.end(); ++it)
|
||||||
{
|
{
|
||||||
int dist = std::abs(it->startSample - samplePos);
|
int dist = std::abs(it->startSample - samplePos);
|
||||||
if (dist <= tolerance)
|
if (dist <= tolerance)
|
||||||
{
|
{
|
||||||
pushHistory();
|
|
||||||
int removedEnd = it->endSample;
|
int removedEnd = it->endSample;
|
||||||
auto idx = static_cast<int>(std::distance(slices.begin(), it));
|
auto idx = static_cast<int>(std::distance(slices.begin(), it));
|
||||||
slices.erase(it);
|
slices.erase(it);
|
||||||
|
|
@ -179,11 +183,12 @@ void SliceManager::removeSliceAt(int samplePos, int tolerance)
|
||||||
void SliceManager::moveSlice(int fromSample, int toSample)
|
void SliceManager::moveSlice(int fromSample, int toSample)
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
for (size_t i = 0; i < slices.size(); ++i)
|
for (size_t i = 0; i < slices.size(); ++i)
|
||||||
{
|
{
|
||||||
if (std::abs(slices[i].startSample - fromSample) < 10)
|
if (std::abs(slices[i].startSample - fromSample) < 10)
|
||||||
{
|
{
|
||||||
pushHistory();
|
|
||||||
slices[i].startSample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, toSample);
|
slices[i].startSample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, toSample);
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
slices[i - 1].endSample = slices[i].startSample;
|
slices[i - 1].endSample = slices[i].startSample;
|
||||||
|
|
@ -199,10 +204,11 @@ void SliceManager::trimStart(int sample)
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
||||||
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample);
|
sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample);
|
||||||
if (sample <= 0) return;
|
if (sample <= 0) return;
|
||||||
|
|
||||||
pushHistory();
|
|
||||||
|
|
||||||
const int numChannels = sampleBuffer->getNumChannels();
|
const int numChannels = sampleBuffer->getNumChannels();
|
||||||
const int oldNumSamples = sampleBuffer->getNumSamples();
|
const int oldNumSamples = sampleBuffer->getNumSamples();
|
||||||
|
|
@ -239,11 +245,12 @@ void SliceManager::trimEnd(int sample)
|
||||||
{
|
{
|
||||||
const juce::CriticalSection::ScopedLockType lock(mutex);
|
const juce::CriticalSection::ScopedLockType lock(mutex);
|
||||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
||||||
|
if (beforeChangeCallback) beforeChangeCallback();
|
||||||
const int numSamples = sampleBuffer->getNumSamples();
|
const int numSamples = sampleBuffer->getNumSamples();
|
||||||
sample = juce::jlimit(0, numSamples, sample);
|
sample = juce::jlimit(0, numSamples, sample);
|
||||||
if (sample >= numSamples) return;
|
if (sample >= numSamples) return;
|
||||||
|
|
||||||
pushHistory();
|
|
||||||
|
|
||||||
const int numChannels = sampleBuffer->getNumChannels();
|
const int numChannels = sampleBuffer->getNumChannels();
|
||||||
|
|
||||||
|
|
@ -283,8 +290,10 @@ void SliceManager::sortSlices()
|
||||||
|
|
||||||
void SliceManager::assignMidiNotes()
|
void SliceManager::assignMidiNotes()
|
||||||
{
|
{
|
||||||
|
const int numRows = 25;
|
||||||
|
const int baseMidiNote = 60;
|
||||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
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)
|
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;
|
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 <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class SliceManager
|
class SliceManager
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +13,7 @@ public:
|
||||||
{
|
{
|
||||||
int startSample;
|
int startSample;
|
||||||
int endSample;
|
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();
|
SliceManager();
|
||||||
|
|
@ -31,17 +32,14 @@ public:
|
||||||
void trimStart(int sample);
|
void trimStart(int sample);
|
||||||
void trimEnd(int sample);
|
void trimEnd(int sample);
|
||||||
|
|
||||||
void undo();
|
|
||||||
void redo();
|
|
||||||
bool canUndo() const;
|
|
||||||
bool canRedo() const;
|
|
||||||
|
|
||||||
std::vector<Slice> getSlices() const;
|
std::vector<Slice> getSlices() const;
|
||||||
int getNumSlices() const;
|
int getNumSlices() const;
|
||||||
Slice getSlice(int index) const;
|
Slice getSlice(int index) const;
|
||||||
|
|
||||||
void setSlices(const std::vector<Slice>& newSlices);
|
void setSlices(const std::vector<Slice>& newSlices);
|
||||||
|
|
||||||
|
void setBeforeChangeCallback(std::function<void()> cb) { beforeChangeCallback = std::move(cb); }
|
||||||
|
|
||||||
int findSliceIndexForSample(int samplePos) const;
|
int findSliceIndexForSample(int samplePos) const;
|
||||||
|
|
||||||
juce::AudioBuffer<float>* getMutableBuffer() { return sampleBuffer; }
|
juce::AudioBuffer<float>* getMutableBuffer() { return sampleBuffer; }
|
||||||
|
|
@ -49,14 +47,9 @@ public:
|
||||||
private:
|
private:
|
||||||
void sortSlices();
|
void sortSlices();
|
||||||
void assignMidiNotes();
|
void assignMidiNotes();
|
||||||
void pushHistory();
|
|
||||||
void truncateHistory();
|
|
||||||
|
|
||||||
mutable juce::CriticalSection mutex;
|
mutable juce::CriticalSection mutex;
|
||||||
std::vector<Slice> slices;
|
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;
|
juce::AudioBuffer<float>* sampleBuffer = nullptr;
|
||||||
double currentSampleRate = 44100.0;
|
double currentSampleRate = 44100.0;
|
||||||
|
|
||||||
|
|
@ -64,6 +57,8 @@ private:
|
||||||
float bassGain = 1.0f;
|
float bassGain = 1.0f;
|
||||||
float trebleGain = 1.0f;
|
float trebleGain = 1.0f;
|
||||||
|
|
||||||
|
std::function<void()> beforeChangeCallback;
|
||||||
|
|
||||||
std::vector<float> computeEnvelope(const float* channelData, int numSamples);
|
std::vector<float> computeEnvelope(const float* channelData, int numSamples);
|
||||||
std::vector<float> lowpassFilter(const std::vector<float>& input, float cutoffHz);
|
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> highpassFilter(const std::vector<float>& input, float cutoffHz);
|
||||||
|
|
|
||||||
|
|
@ -171,19 +171,18 @@ int WaveformDisplay::sampleToX(int sample) const
|
||||||
|
|
||||||
int WaveformDisplay::keyRowToMidiNote(int row) const
|
int WaveformDisplay::keyRowToMidiNote(int row) const
|
||||||
{
|
{
|
||||||
const int numRows = 12;
|
const int numRows = 25;
|
||||||
const int baseMidiNote = 60;
|
const int baseMidiNote = 60;
|
||||||
return baseMidiNote + (numRows - 1 - row);
|
return baseMidiNote + (numRows - 1 - row);
|
||||||
}
|
}
|
||||||
|
|
||||||
int WaveformDisplay::midiNoteToSliceIndex(int note) const
|
int WaveformDisplay::midiNoteToSliceIndex(int note) const
|
||||||
{
|
{
|
||||||
int pitchClass = note % 12;
|
|
||||||
const auto& slices = sliceManager.getSlices();
|
const auto& slices = sliceManager.getSlices();
|
||||||
int numSlices = sliceManager.getNumSlices();
|
int numSlices = sliceManager.getNumSlices();
|
||||||
for (int i = 0; i < numSlices; ++i)
|
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 i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -223,12 +222,12 @@ void WaveformDisplay::paint(juce::Graphics& g)
|
||||||
|
|
||||||
auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight);
|
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)
|
// Left key area (matches piano roll)
|
||||||
drawKeys(g, waveBounds.withWidth(keyWidth));
|
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
|
// Waveform and markers in the area to the right of keys
|
||||||
auto waveArea = waveBounds.withTrimmedLeft(keyWidth);
|
auto waveArea = waveBounds.withTrimmedLeft(keyWidth);
|
||||||
drawWaveform(g, waveArea);
|
drawWaveform(g, waveArea);
|
||||||
|
|
@ -240,7 +239,7 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
|
||||||
g.setColour(juce::Colour(0xff222233));
|
g.setColour(juce::Colour(0xff222233));
|
||||||
g.fillRect(bounds);
|
g.fillRect(bounds);
|
||||||
|
|
||||||
const int numRows = 12;
|
const int numRows = 25;
|
||||||
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
|
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
|
||||||
|
|
||||||
juce::Colour markerColours[] = {
|
juce::Colour markerColours[] = {
|
||||||
|
|
@ -545,14 +544,15 @@ void WaveformDisplay::mouseDown(const juce::MouseEvent& e)
|
||||||
}
|
}
|
||||||
|
|
||||||
auto bounds = getLocalBounds().toFloat();
|
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
|
// Check if click is in the key area
|
||||||
if (e.getPosition().getX() < static_cast<int>(keyArea.getWidth()))
|
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);
|
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);
|
int midiNote = keyRowToMidiNote(row);
|
||||||
|
|
||||||
if (onKeyClick)
|
if (onKeyClick)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue