monoslicer/Source/PluginEditor.cpp

549 lines
22 KiB
C++

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