#include "PluginEditor.h" MonoSlicerEditor::MonoSlicerEditor(MonoSlicerProcessor& p) : AudioProcessorEditor(p), processorRef(p), waveformDisplay(p.getSliceManager()) { setSize(baseWidth, baseHeight); // Title bar buttons auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) { addAndMakeVisible(btn); btn.addListener(this); btn.setTooltip(tip); btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a)); }; setupTitleBtn(loadSampleButton, "Load audio file"); setupTitleBtn(prevSampleButton, "Previous sample in folder"); setupTitleBtn(nextSampleButton, "Next sample in folder"); setupTitleBtn(undoButton, "Undo"); setupTitleBtn(redoButton, "Redo"); setupTitleBtn(zoomInButton, "Zoom In"); setupTitleBtn(zoomOutButton, "Zoom Out"); setupTitleBtn(zoomResetButton, "Reset Zoom"); addAndMakeVisible(scaleComboBox); scaleComboBox.addItem("75%", 1); scaleComboBox.addItem("100%", 2); scaleComboBox.addItem("125%", 3); scaleComboBox.addItem("150%", 4); scaleComboBox.addItem("175%", 5); scaleComboBox.addItem("200%", 6); scaleComboBox.addListener(this); scaleComboBox.setTooltip("UI Scale"); scaleAttachment = std::make_unique(processorRef.getAPVTS(), "uiScale", scaleComboBox); // Bottom panel buttons addAndMakeVisible(autoSliceButton); autoSliceButton.addListener(this); addAndMakeVisible(clearSlicesButton); clearSlicesButton.addListener(this); addAndMakeVisible(waveformDisplay); // Slice knobs attachSlider(bassSlider, bassLabel, "Bass", "bass", bassAttachment); attachSlider(trebleSlider, trebleLabel, "Treble", "treble", trebleAttachment); attachSlider(sensitivitySlider, sensitivityLabel, "Sens.", "sensitivity", sensitivityAttachment); // Amp ADSR attachSlider(ampAttackSlider, ampAttackLabel, "A Atk", "ampAttack", ampAttackAttachment); attachSlider(ampDecaySlider, ampDecayLabel, "A Dcy", "ampDecay", ampDecayAttachment); attachSlider(ampSustainSlider, ampSustainLabel, "A Sus", "ampSustain", ampSustainAttachment); attachSlider(ampReleaseSlider, ampReleaseLabel, "A Rel", "ampRelease", ampReleaseAttachment); // Filter knobs attachSlider(filterCutoffSlider, filterCutoffLabel, "Cutoff", "filterCutoff", filterCutoffAttachment); attachSlider(filterResoSlider, filterResoLabel, "Reso", "filterReso", filterResoAttachment); attachSlider(filterEnvDepthSlider, filterEnvDepthLabel, "Env Dph", "filterEnvDepth", filterEnvDepthAttachment); // Filter type combo addAndMakeVisible(filterTypeComboBox); filterTypeComboBox.addItem("LP12", 1); filterTypeComboBox.addItem("LP24", 2); filterTypeComboBox.addItem("HP", 3); filterTypeComboBox.addItem("BP", 4); filterTypeComboBox.addItem("Notch", 5); filterTypeAttachment = std::make_unique(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(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(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(processorRef.getAPVTS(), "keyShift", keyShiftComboBox); auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider, &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider, &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(slices.size()); ++i) { if (slices[static_cast(i)].midiNote == pitchClass) { sliceIdx = i; break; } } if (sliceIdx >= 0) processorRef.triggerSlice(sliceIdx, 0.8f); else processorRef.triggerSlice(0, 0.8f); }); startTimerHz(30); } MonoSlicerEditor::~MonoSlicerEditor() { autoSliceButton.removeListener(this); clearSlicesButton.removeListener(this); scaleComboBox.removeListener(this); loadSampleButton.removeListener(this); prevSampleButton.removeListener(this); nextSampleButton.removeListener(this); undoButton.removeListener(this); redoButton.removeListener(this); zoomInButton.removeListener(this); zoomOutButton.removeListener(this); zoomResetButton.removeListener(this); stretchButton.removeListener(this); } void MonoSlicerEditor::setupSlider(juce::Slider& slider, juce::Label& label, const juce::String& name) { addAndMakeVisible(slider); addAndMakeVisible(label); label.setText(name, juce::dontSendNotification); label.attachToComponent(&slider, false); label.setJustificationType(juce::Justification::centred); label.setFont(juce::Font(juce::FontOptions(13.0f))); } void MonoSlicerEditor::attachSlider(juce::Slider& slider, juce::Label& label, const juce::String& name, const juce::String& paramId, std::unique_ptr& attachment) { setupSlider(slider, label, name); attachment = std::make_unique(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(10, titleBar.getY(), 110, titleBar.getHeight()), juce::Justification::centredLeft); g.setColour(juce::Colour(0xff333355)); g.drawHorizontalLine(40, 0.0f, static_cast(getWidth())); } void MonoSlicerEditor::resized() { float scaleX = static_cast(getWidth()) / static_cast(baseWidth); float scaleY = static_cast(getHeight()) / static_cast(baseHeight); setTransform(juce::AffineTransform::scale(juce::jmin(scaleX, scaleY))); auto bounds = getLocalBounds(); auto titleBar = bounds.removeFromTop(40); int tx = 120; auto placeBtn = [&](juce::Button& btn, int w) { btn.setBounds(tx, titleBar.getY() + 8, w, 24); tx += w + 3; }; placeBtn(loadSampleButton, 44); placeBtn(prevSampleButton, 24); placeBtn(nextSampleButton, 24); tx += 10; placeBtn(undoButton, 44); placeBtn(redoButton, 44); tx += 10; placeBtn(zoomOutButton, 28); placeBtn(zoomResetButton, 32); placeBtn(zoomInButton, 28); scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24); auto bottomPanel = bounds.removeFromBottom(200); auto bottomRow = bottomPanel.reduced(8); // Bottom panel: buttons + stretch | amp ADSR | filter ADSR | filter knobs auto leftArea = bottomRow.removeFromLeft(200); { auto btnArea = leftArea.removeFromTop(28); autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24); clearSlicesButton.setBounds(btnArea.getX() + btnArea.getWidth() / 2 + 2, btnArea.getY(), btnArea.getWidth() / 2 - 2, 24); leftArea.removeFromTop(4); // Stretch controls int stretchW = leftArea.getWidth(); int col1W = stretchW / 2 - 2; int col2W = stretchW / 2 - 2; bpmSlider.setBounds(leftArea.getX(), leftArea.getY(), col1W, 20); stretchBeatsComboBox.setBounds(leftArea.getX() + col1W + 4, leftArea.getY(), col2W, 20); leftArea.removeFromTop(24); stretchButton.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 22); leftArea.removeFromTop(26); leftArea.removeFromTop(20); keyShiftComboBox.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 20); } bottomRow.removeFromLeft(8); auto ampSection = bottomRow.removeFromLeft(230); { int kw = ampSection.getWidth() / 4; int k = 0; for (auto* s : { &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider }) { s->setBounds(ampSection.getX() + k * kw, ampSection.getY(), kw, ampSection.getHeight()); ++k; } } bottomRow.removeFromLeft(8); auto fltAdsSection = bottomRow.removeFromLeft(230); { int kw = fltAdsSection.getWidth() / 4; int k = 0; for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider }) { s->setBounds(fltAdsSection.getX() + k * kw, fltAdsSection.getY(), kw, fltAdsSection.getHeight()); ++k; } } bottomRow.removeFromLeft(8); auto filterArea = bottomRow; { int comboW = 56; int gap = 4; int knobAreaX = filterArea.getX() + comboW + gap; int knobAreaW = filterArea.getWidth() - comboW - gap; int kw = knobAreaW / 3; int topRowH = 80; int bottomRowGap = 24; int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap; filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20); filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH); filterResoSlider.setBounds(knobAreaX + kw, filterArea.getY(), kw, topRowH); filterEnvDepthSlider.setBounds(knobAreaX + kw * 2, filterArea.getY(), kw, topRowH); int bottomY = filterArea.getY() + topRowH + bottomRowGap; bassSlider.setBounds(knobAreaX, bottomY, kw, bottomRowH); trebleSlider.setBounds(knobAreaX + kw, bottomY, kw, bottomRowH); sensitivitySlider.setBounds(knobAreaX + kw * 2, bottomY, kw, bottomRowH); } auto mainArea = bounds.reduced(8); waveformDisplay.setBounds(mainArea); } void MonoSlicerEditor::timerCallback() { waveformDisplay.setPlaybackPosition(processorRef.currentPlaybackSample.load()); waveformDisplay.setActiveSliceIndex(processorRef.currentActiveSlice.load()); waveformDisplay.setSelectedSlice(processorRef.selectedSlice.load()); // Release key click note when mouse is released if (keyClickNote >= 0) { auto& editor = *this; if (!editor.isMouseButtonDown()) { processorRef.releaseSlice(); keyClickNote = -1; } } } void MonoSlicerEditor::buttonClicked(juce::Button* button) { if (button == &loadSampleButton) { fileChooser.launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, [this](const juce::FileChooser& fc) { auto file = fc.getResult(); if (file.existsAsFile()) { processorRef.loadAudioFile(file); waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); } }); } else if (button == &prevSampleButton) { processorRef.loadPreviousSample(); waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); } else if (button == &nextSampleButton) { processorRef.loadNextSample(); waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); } else if (button == &autoSliceButton) { processorRef.getSliceManager().autoSlice(); } else if (button == &clearSlicesButton) { processorRef.getSliceManager().clearSlices(); } else if (button == &undoButton) { processorRef.getSliceManager().undo(); } else if (button == &redoButton) { processorRef.getSliceManager().redo(); } else if (button == &stretchButton) { int beatsId = stretchBeatsComboBox.getSelectedId(); int numBeats = 4; switch (beatsId) { case 1: numBeats = 1; break; case 2: numBeats = 2; break; case 3: numBeats = 4; break; case 4: numBeats = 8; break; case 5: numBeats = 16; break; case 6: numBeats = 32; break; default: numBeats = 4; break; } float bpm = static_cast(bpmSlider.getValue()); processorRef.stretchToBeats(numBeats, bpm); waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); } else if (button == &zoomInButton) { waveformDisplay.zoomIn(); } else if (button == &zoomOutButton) { waveformDisplay.zoomOut(); } else if (button == &zoomResetButton) { waveformDisplay.zoomReset(); } } void MonoSlicerEditor::comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) { if (comboBoxThatHasChanged == &scaleComboBox) { static const float scales[] = { 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; int id = scaleComboBox.getSelectedId(); if (id >= 1 && id <= 6) { float scale = scales[id - 1]; setSize(static_cast(baseWidth * scale), static_cast(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; } } }