#include "PluginEditor.h" MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) : AudioProcessorEditor(p), processorRef(p), waveformDisplay(p.getSliceManager()), vuMeter(p) { setSize(baseWidth, baseHeight); setLookAndFeel(&lnf); // 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)); btn.setColour(juce::TextButton::textColourOffId, juce::Colours::white); }; 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"); 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(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(processorRef.getAPVTS(), "octaveOffset", octaveComboBox); // 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 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(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); 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); 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(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)); 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)); stretchButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white); // 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); addAndMakeVisible(vuMeter); 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(0xff3a73e5)); slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff1a192d)); } if (processorRef.getSampleBuffer().getNumSamples() > 0) { waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); } waveformDisplay.setOnEmptyAreaClick([this] { buttonClicked(&loadSampleButton); }); waveformDisplay.setOnKeyClick([this](int midiNote) { keyClickNote = midiNote; 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 == midiNote) { sliceIdx = i; break; } } if (sliceIdx >= 0) processorRef.triggerSlice(sliceIdx, 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& 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); 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(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(contentW * 0.21); int comboW = static_cast(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(leftW); { 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); leftArea.removeFromTop(24); vuMeter.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 40); } bottomRow.removeFromLeft(sectionGap); auto adsrSection = bottomRow.removeFromLeft(adsrW); { int topRowH = 80; int bottomRowGap = 50; int bottomRowH = adsrSection.getHeight() - topRowH - bottomRowGap; int k = 0; for (auto* s : { &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider }) { s->setBounds(adsrSection.getX() + k * kw, adsrSection.getY(), kw, topRowH); ++k; } int bottomY = adsrSection.getY() + topRowH + bottomRowGap; k = 0; for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider }) { s->setBounds(adsrSection.getX() + k * kw, bottomY, kw, bottomRowH); ++k; } } bottomRow.removeFromLeft(sectionGap); auto filterArea = bottomRow; { int knobAreaX = filterArea.getX() + comboW + comboGap; int topRowH = 80; 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); 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(processorRef.getSampleBuffer().getNumSamples()) / processorRef.getSampleRateLoaded(); int mins = static_cast(duration) / 60; int secs = static_cast(duration) % 60; fileInfoLabel.setButtonText("File: " + file.getFileName() + " [Time: " + 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.undo(); } else if (button == &redoButton) { processorRef.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(); } 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(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; } } }