From f035e89ba9e39e83867ddd7ab0b6077457ac1d5d Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 22 Jul 2026 23:10:55 +0200 Subject: [PATCH] Reduce plugin width by 102px and fix bottom panel layout to scale proportionally --- Source/PianoRollDisplay.cpp | 8 +-- Source/PianoRollDisplay.h | 2 +- Source/PluginEditor.cpp | 93 ++++++++++++++++++++------------ Source/PluginEditor.h | 104 +++++++++++++++++++++++++++++++++++- Source/PluginProcessor.cpp | 86 +++++++++++++++++++++++++++++ Source/PluginProcessor.h | 18 +++++++ Source/SliceManager.cpp | 63 +++++++--------------- Source/SliceManager.h | 17 +++--- Source/WaveformDisplay.cpp | 20 +++---- 9 files changed, 305 insertions(+), 106 deletions(-) diff --git a/Source/PianoRollDisplay.cpp b/Source/PianoRollDisplay.cpp index d414ead..91d9995 100644 --- a/Source/PianoRollDisplay.cpp +++ b/Source/PianoRollDisplay.cpp @@ -27,11 +27,12 @@ juce::Colour PianoRollDisplay::sliceColour(int sliceIndex) const int PianoRollDisplay::rowToSliceIndex(int row) const { - int noteRow = numRows - 1 - row; + const int baseMidiNote = 60; + int midiNote = baseMidiNote + (numRows - 1 - row); const auto& slices = sliceManager.getSlices(); for (int i = 0; i < sliceManager.getNumSlices(); ++i) { - if (slices[static_cast(i)].midiNote % numRows == noteRow) + if (slices[static_cast(i)].midiNote == midiNote) return i; } return -1; @@ -68,7 +69,8 @@ void PianoRollDisplay::paint(juce::Graphics& g) for (int i = 0; i < numSlices; ++i) { const auto& slice = slices[static_cast(i)]; - int noteRow = slice.midiNote % numRows; + const int baseMidiNote = 60; + int noteRow = slice.midiNote - baseMidiNote; int row = numRows - 1 - noteRow; float xFrac = static_cast(slice.startSample) / static_cast(total); diff --git a/Source/PianoRollDisplay.h b/Source/PianoRollDisplay.h index e343a5f..f840ca7 100644 --- a/Source/PianoRollDisplay.h +++ b/Source/PianoRollDisplay.h @@ -7,7 +7,7 @@ class PianoRollDisplay : public juce::Component, public juce::Timer { public: - static constexpr int numRows = 12; + static constexpr int numRows = 25; static constexpr int baseMidiNote = 60; static constexpr float keyWidth = 50.0f; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 806cd6c..922a866 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -3,9 +3,11 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) : AudioProcessorEditor(p), processorRef(p), - waveformDisplay(p.getSliceManager()) + waveformDisplay(p.getSliceManager()), + vuMeter(p) { setSize(baseWidth, baseHeight); + setLookAndFeel(&lnf); // Title bar buttons auto setupTitleBtn = [this](juce::Button& btn, const juce::String& tip) { @@ -13,6 +15,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) btn.addListener(this); btn.setTooltip(tip); btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a)); + btn.setColour(juce::TextButton::textColourOffId, juce::Colours::white); }; setupTitleBtn(loadSampleButton, "Load audio file"); @@ -36,6 +39,10 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) scaleComboBox.addItem("200%", 6); scaleComboBox.addListener(this); scaleComboBox.setTooltip("UI Scale"); + scaleComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a)); + scaleComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white); + scaleComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466)); + scaleComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white); scaleAttachment = std::make_unique(processorRef.getAPVTS(), "uiScale", scaleComboBox); addAndMakeVisible(octaveComboBox); @@ -52,9 +59,13 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) // Bottom panel buttons addAndMakeVisible(autoSliceButton); autoSliceButton.addListener(this); + autoSliceButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a)); + autoSliceButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white); + addAndMakeVisible(clearSlicesButton); clearSlicesButton.addListener(this); - + clearSlicesButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a)); + clearSlicesButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white); addAndMakeVisible(waveformDisplay); // Status bar labels @@ -103,6 +114,12 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) filterTypeComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466)); filterTypeComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white); + addAndMakeVisible(filterTypeLabel); + filterTypeLabel.setText("Filter Type", juce::dontSendNotification); + filterTypeLabel.setJustificationType(juce::Justification::centred); + filterTypeLabel.setFont(juce::Font(juce::FontOptions(13.0f))); + filterTypeLabel.attachToComponent(&filterTypeComboBox, false); + // Filter ADSR attachSlider(filterAttackSlider, filterAttackLabel, "F Atk", "filterAttack", filterAttackAttachment); attachSlider(filterDecaySlider, filterDecayLabel, "F Dcy", "filterDecay", filterDecayAttachment); @@ -115,7 +132,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) bpmSlider.setTextBoxStyle(juce::Slider::TextBoxLeft, false, 50, 20); bpmSlider.setRange(20.0, 300.0, 1.0); bpmSlider.setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack); - bpmSlider.setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xffee8833)); + bpmSlider.setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xff3a73e5)); bpmSlider.setColour(juce::Slider::textBoxTextColourId, juce::Colours::white); bpmSlider.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2a4a)); bpmSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0xff444466)); @@ -140,6 +157,7 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) addAndMakeVisible(stretchButton); stretchButton.addListener(this); stretchButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a)); + stretchButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white); // Key shift combo addAndMakeVisible(keyShiftComboBox); @@ -157,6 +175,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) keyShiftLabel.attachToComponent(&keyShiftComboBox, false); keyShiftAttachment = std::make_unique(processorRef.getAPVTS(), "keyShift", keyShiftComboBox); + addAndMakeVisible(vuMeter); + auto allKnobs = { &bassSlider, &trebleSlider, &sensitivitySlider, &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider, &filterCutoffSlider, &filterResoSlider, &filterEnvDepthSlider, @@ -167,8 +187,8 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) slider->setSliderStyle(juce::Slider::RotaryVerticalDrag); slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14); slider->setColour(juce::Slider::thumbColourId, juce::Colours::transparentBlack); - slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xffee8833)); - slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff332211)); + slider->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xff3a73e5)); + slider->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0xff1a192d)); } if (processorRef.getSampleBuffer().getNumSamples() > 0) @@ -182,12 +202,11 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) waveformDisplay.setOnKeyClick([this](int midiNote) { keyClickNote = midiNote; - int pitchClass = midiNote % 12; int sliceIdx = -1; const auto& slices = processorRef.getSliceManager().getSlices(); for (int i = 0; i < static_cast(slices.size()); ++i) { - if (slices[static_cast(i)].midiNote == pitchClass) + if (slices[static_cast(i)].midiNote == midiNote) { sliceIdx = i; break; @@ -195,8 +214,6 @@ MonoslicerEditor::MonoslicerEditor(MonoslicerProcessor& p) } if (sliceIdx >= 0) processorRef.triggerSlice(sliceIdx, 0.8f); - else - processorRef.triggerSlice(0, 0.8f); }); startTimerHz(30); @@ -297,11 +314,23 @@ void MonoslicerEditor::resized() scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24); octaveComboBox.setBounds(titleBar.getRight() - 176, titleBar.getY() + 8, 72, 24); - auto bottomPanel = bounds.removeFromBottom(200); + auto bottomPanel = bounds.removeFromBottom(236); auto bottomRow = bottomPanel.reduced(8); + bottomRow.removeFromTop(10); + + int bw = bottomRow.getWidth(); + int sectionGap = 8; + int comboGap = 4; + int contentW = bw - 2 * sectionGap - comboGap; + + int leftW = static_cast(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(200); + auto leftArea = bottomRow.removeFromLeft(leftW); { auto btnArea = leftArea.removeFromTop(28); autoSliceButton.setBounds(btnArea.getX(), btnArea.getY(), btnArea.getWidth() / 2 - 2, 24); @@ -318,45 +347,39 @@ void MonoslicerEditor::resized() leftArea.removeFromTop(26); leftArea.removeFromTop(20); keyShiftComboBox.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 20); + leftArea.removeFromTop(24); + vuMeter.setBounds(leftArea.getX(), leftArea.getY(), stretchW, 40); } - bottomRow.removeFromLeft(8); + bottomRow.removeFromLeft(sectionGap); - auto ampSection = bottomRow.removeFromLeft(230); + auto adsrSection = bottomRow.removeFromLeft(adsrW); { - int kw = ampSection.getWidth() / 4; + int topRowH = 80; + int bottomRowGap = 50; + int bottomRowH = adsrSection.getHeight() - topRowH - bottomRowGap; int k = 0; for (auto* s : { &AttackSlider, &DecaySlider, &SustainSlider, &ReleaseSlider }) { - s->setBounds(ampSection.getX() + k * kw, ampSection.getY(), kw, ampSection.getHeight()); + s->setBounds(adsrSection.getX() + k * kw, adsrSection.getY(), kw, topRowH); ++k; } - } - - bottomRow.removeFromLeft(8); - - auto fltAdsSection = bottomRow.removeFromLeft(230); - { - int kw = fltAdsSection.getWidth() / 4; - int k = 0; + int bottomY = adsrSection.getY() + topRowH + bottomRowGap; + k = 0; for (auto* s : { &filterAttackSlider, &filterDecaySlider, &filterSustainSlider, &filterReleaseSlider }) { - s->setBounds(fltAdsSection.getX() + k * kw, fltAdsSection.getY(), kw, fltAdsSection.getHeight()); + s->setBounds(adsrSection.getX() + k * kw, bottomY, kw, bottomRowH); ++k; } } - bottomRow.removeFromLeft(8); + bottomRow.removeFromLeft(sectionGap); auto filterArea = bottomRow; { - int comboW = 56; - int gap = 4; - int knobAreaX = filterArea.getX() + comboW + gap; - int knobAreaW = filterArea.getWidth() - comboW - gap; - int kw = knobAreaW / 3; + int knobAreaX = filterArea.getX() + comboW + comboGap; int topRowH = 80; - int bottomRowGap = 24; + int bottomRowGap = 50; int bottomRowH = filterArea.getHeight() - topRowH - bottomRowGap; filterTypeComboBox.setBounds(filterArea.getX(), filterArea.getY() + 2, comboW, 20); filterCutoffSlider.setBounds(knobAreaX, filterArea.getY(), kw, topRowH); @@ -389,8 +412,8 @@ void MonoslicerEditor::timerCallback() / processorRef.getSampleRateLoaded(); int mins = static_cast(duration) / 60; int secs = static_cast(duration) % 60; - fileInfoLabel.setButtonText(file.getFileName() + " " - + juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2)); + fileInfoLabel.setButtonText("File: " + file.getFileName() + " [Time: " + + juce::String(mins) + ":" + juce::String(secs).paddedLeft('0', 2) + "]"); } else { @@ -458,11 +481,11 @@ void MonoslicerEditor::buttonClicked(juce::Button* button) } else if (button == &undoButton) { - processorRef.getSliceManager().undo(); + processorRef.undo(); } else if (button == &redoButton) { - processorRef.getSliceManager().redo(); + processorRef.redo(); } else if (button == &stretchButton) { diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 34e2b51..3d42ff6 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -3,6 +3,103 @@ #include "PluginProcessor.h" #include "WaveformDisplay.h" +class MonoslicerLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + void drawButtonBackground(juce::Graphics& g, juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, bool isButtonDown) override + { + if (backgroundColour.getAlpha() == 0) + return; + + auto bounds = button.getLocalBounds().toFloat().reduced(0.5f); + auto cornerSize = 4.0f; + + auto baseColour = backgroundColour.withMultipliedAlpha(button.isEnabled() ? 1.0f : 0.5f); + g.setColour(baseColour); + g.fillRoundedRectangle(bounds, cornerSize); + + g.setColour(juce::Colour(0xff444466)); + g.drawRoundedRectangle(bounds, cornerSize, 1.0f); + } + + void drawButtonText(juce::Graphics& g, juce::TextButton& button, + bool isMouseOverButton, bool isButtonDown) override + { + auto font = getTextButtonFont(button, button.getHeight()); + g.setFont(font); + g.setColour(button.findColour(button.getToggleState() ? juce::TextButton::textColourOnId + : juce::TextButton::textColourOffId) + .withMultipliedAlpha(button.isEnabled() ? 1.0f : 0.5f)); + + auto textArea = button.getLocalBounds().reduced(6, 1); + g.drawText(button.getButtonText(), textArea, juce::Justification::centredLeft, true); + } +}; + +class VuMeter : public juce::Component, public juce::Timer +{ +public: + VuMeter(MonoslicerProcessor& p) : processor(p) { startTimerHz(30); } + ~VuMeter() override { stopTimer(); } + + void paint(juce::Graphics& g) override + { + auto bounds = getLocalBounds().toFloat(); + float legendW = 20.0f; + float legendGap = 5.0f; + float barH = (bounds.getHeight() - 6.0f) / 2.0f; + float gap = 6.0f; + float barX = bounds.getX() + legendW + legendGap; + float barW = bounds.getWidth() - legendW - legendGap; + + float levelL = processor.outputLevelL.load(); + float levelR = processor.outputLevelR.load(); + + // Apply falloff decay + peakL = juce::jmax(levelL, peakL - decayRate); + peakR = juce::jmax(levelR, peakR - decayRate); + if (levelL > peakL) peakL = levelL; + if (levelR > peakR) peakR = levelR; + + auto drawBar = [&](float y, float level, float peak, const juce::String& legend) + { + float frac = juce::jlimit(0.0f, 1.0f, level); + float peakFrac = juce::jlimit(0.0f, 1.0f, peak); + + // Legend text + g.setColour(juce::Colour(0xff888888)); + g.setFont(juce::Font(10.0f)); + g.drawText(legend, bounds.getX(), y, legendW, barH, juce::Justification::centredRight); + + // Inactive background + g.setColour(juce::Colour(0xff1a192d)); + g.fillRoundedRectangle(barX, y, barW, barH, 2.0f); + + // Active fill from left + float fillW = barW * frac; + g.setColour(juce::Colour(0xff3a73e5)); + g.fillRoundedRectangle(barX, y, fillW, barH, 2.0f); + + // Peak hold indicator + float peakX = barX + barW * peakFrac - 2.0f; + g.setColour(juce::Colour(0xff6699cc)); + g.fillRect(peakX, y + 2.0f, 2.0f, barH - 4.0f); + }; + + drawBar(bounds.getY(), levelL, peakL, "L"); + drawBar(bounds.getY() + barH + gap, levelR, peakR, "R"); + } + + void timerCallback() override { repaint(); } + +private: + MonoslicerProcessor& processor; + float peakL = 0.0f, peakR = 0.0f; + float decayRate = 0.02f; +}; + class MonoslicerEditor : public juce::AudioProcessorEditor, private juce::FileDragAndDropTarget, private juce::Button::Listener, @@ -23,6 +120,7 @@ public: void timerCallback() override; private: + MonoslicerLookAndFeel lnf; MonoslicerProcessor& processorRef; WaveformDisplay waveformDisplay; @@ -46,8 +144,8 @@ private: juce::ComboBox scaleComboBox; juce::ComboBox octaveComboBox; - static constexpr int baseWidth = 1100; - static constexpr int baseHeight = 570; + static constexpr int baseWidth = 920; + static constexpr int baseHeight = 606; // Slice knobs juce::Slider bassSlider, trebleSlider, sensitivitySlider; @@ -59,6 +157,7 @@ private: // Filter type selector juce::ComboBox filterTypeComboBox; + juce::Label filterTypeLabel; // Filter knobs juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider; @@ -77,6 +176,7 @@ private: juce::TextButton stretchButton { "Match Length" }; juce::ComboBox keyShiftComboBox; juce::Label keyShiftLabel; + VuMeter vuMeter; std::unique_ptr stretchBpmAttachment; std::unique_ptr stretchBeatsAttachment; std::unique_ptr keyShiftAttachment; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index ee9d99c..7ddc743 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -8,6 +8,7 @@ MonoslicerProcessor::MonoslicerProcessor() apvts(*this, nullptr, "Parameters", createParameterLayout()) { formatManager.registerBasicFormats(); + sliceManager.setBeforeChangeCallback([this]() { pushUndoState(); }); } MonoslicerProcessor::~MonoslicerProcessor() {} @@ -444,6 +445,27 @@ void MonoslicerProcessor::processBlock(juce::AudioBuffer& buffer, juce::M } if (currentActiveSlice.load() < 0) currentPlaybackSample.store(-1); + + float peakL = 0.0f; + float peakR = 0.0f; + if (buffer.getNumChannels() > 0) + { + auto* data = buffer.getReadPointer(0); + for (int i = 0; i < buffer.getNumSamples(); ++i) + peakL = juce::jmax(peakL, std::abs(data[i])); + } + if (buffer.getNumChannels() > 1) + { + auto* data = buffer.getReadPointer(1); + for (int i = 0; i < buffer.getNumSamples(); ++i) + peakR = juce::jmax(peakR, std::abs(data[i])); + } + else + { + peakR = peakL; + } + outputLevelL.store(peakL); + outputLevelR.store(peakR); } void MonoslicerProcessor::handleNoteOn(int noteNumber, float velocity) @@ -496,6 +518,7 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file) std::unique_ptr reader(formatManager.createReaderFor(file)); if (reader) { + pushUndoState(); int numSamples = static_cast(reader->lengthInSamples); sampleBuffer.setSize(static_cast(reader->numChannels), numSamples); reader->read(&sampleBuffer, 0, numSamples, 0, true, true); @@ -509,6 +532,7 @@ void MonoslicerProcessor::loadAudioFile(const juce::File& file) void MonoslicerProcessor::clearSample() { + pushUndoState(); sampleBuffer.setSize(0, 0); currentFile = juce::File{}; folderFiles.clear(); @@ -517,6 +541,66 @@ void MonoslicerProcessor::clearSample() selectedSlice.store(-1); } +void MonoslicerProcessor::pushUndoState() +{ + if (restoringFromUndo) return; + undoStack.push_back({ currentFile, sliceManager.getSlices() }); + if (static_cast(undoStack.size()) > maxUndoDepth) + undoStack.erase(undoStack.begin()); + redoStack.clear(); +} + +void MonoslicerProcessor::undo() +{ + if (undoStack.empty()) return; + redoStack.push_back({ currentFile, sliceManager.getSlices() }); + auto state = std::move(undoStack.back()); + undoStack.pop_back(); + restoringFromUndo = true; + if (state.file.existsAsFile()) + { + loadAudioFile(state.file); + sliceManager.setSlices(state.slices); + } + else + { + sampleBuffer.setSize(0, 0); + currentFile = juce::File{}; + folderFiles.clear(); + currentFileIndex = -1; + sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); + selectedSlice.store(-1); + } + restoringFromUndo = false; +} + +void MonoslicerProcessor::redo() +{ + if (redoStack.empty()) return; + undoStack.push_back({ currentFile, sliceManager.getSlices() }); + auto state = std::move(redoStack.back()); + redoStack.pop_back(); + restoringFromUndo = true; + if (state.file.existsAsFile()) + { + loadAudioFile(state.file); + sliceManager.setSlices(state.slices); + } + else + { + sampleBuffer.setSize(0, 0); + currentFile = juce::File{}; + folderFiles.clear(); + currentFileIndex = -1; + sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate); + selectedSlice.store(-1); + } + restoringFromUndo = false; +} + +bool MonoslicerProcessor::canUndo() const { return !undoStack.empty(); } +bool MonoslicerProcessor::canRedo() const { return !redoStack.empty(); } + void MonoslicerProcessor::refreshFolderList() { folderFiles.clear(); @@ -678,6 +762,8 @@ void MonoslicerProcessor::stretchToBeats(int numBeats, float bpm) { if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return; + pushUndoState(); + double sr = static_cast(loadedSampleRate); int currentLength = sampleBuffer.getNumSamples(); double currentDuration = static_cast(currentLength) / sr; diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 22b364a..0b41b63 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -43,6 +43,11 @@ public: void psolaStretch(int targetLength); float getHostBpm() const; + void undo(); + void redo(); + bool canUndo() const; + bool canRedo() const; + juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; } SliceManager& getSliceManager() { return sliceManager; } @@ -56,6 +61,8 @@ public: std::atomic currentPlaybackSample { -1 }; std::atomic currentActiveSlice { -1 }; std::atomic selectedSlice { -1 }; + std::atomic outputLevelL { 0.0f }; + std::atomic outputLevelR { 0.0f }; private: juce::AudioProcessorValueTreeState apvts; @@ -73,6 +80,17 @@ private: int currentFileIndex = -1; void refreshFolderList(); + struct UndoState + { + juce::File file; + std::vector slices; + }; + std::vector undoStack; + std::vector redoStack; + static constexpr int maxUndoDepth = 10; + void pushUndoState(); + bool restoringFromUndo = false; + enum class AmpStage { Idle, Attack, Decay, Sustain, Release }; enum class FilterType { LP12, LP24, HP, BP, Notch }; diff --git a/Source/SliceManager.cpp b/Source/SliceManager.cpp index 469a95f..b48ce5f 100644 --- a/Source/SliceManager.cpp +++ b/Source/SliceManager.cpp @@ -40,7 +40,8 @@ void SliceManager::autoSlice() if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; - pushHistory(); + if (beforeChangeCallback) beforeChangeCallback(); + const int numSamples = sampleBuffer->getNumSamples(); const int numChannels = sampleBuffer->getNumChannels(); @@ -106,7 +107,8 @@ void SliceManager::autoSlice() void SliceManager::clearSlices() { const juce::CriticalSection::ScopedLockType lock(mutex); - pushHistory(); + if (beforeChangeCallback) beforeChangeCallback(); + slices.clear(); } @@ -121,12 +123,13 @@ void SliceManager::addSliceManual(int samplePos) { const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer) return; + if (beforeChangeCallback) beforeChangeCallback(); const int numSamples = sampleBuffer->getNumSamples(); samplePos = juce::jlimit(1, numSamples - 1, samplePos); if (slices.empty()) { - pushHistory(); + slices.push_back({ 0, numSamples, 0 }); assignMidiNotes(); return; @@ -135,7 +138,7 @@ void SliceManager::addSliceManual(int samplePos) int idx = findSliceIndexForSample(samplePos); if (idx < 0) { - pushHistory(); + slices.push_back({ samplePos, numSamples, 0 }); sortSlices(); assignMidiNotes(); @@ -146,7 +149,7 @@ void SliceManager::addSliceManual(int samplePos) if (samplePos <= parent.startSample || samplePos >= parent.endSample) return; - pushHistory(); + int oldEnd = parent.endSample; parent.endSample = samplePos; @@ -159,12 +162,13 @@ void SliceManager::addSliceManual(int samplePos) void SliceManager::removeSliceAt(int samplePos, int tolerance) { const juce::CriticalSection::ScopedLockType lock(mutex); + if (beforeChangeCallback) beforeChangeCallback(); for (auto it = slices.begin(); it != slices.end(); ++it) { int dist = std::abs(it->startSample - samplePos); if (dist <= tolerance) { - pushHistory(); + int removedEnd = it->endSample; auto idx = static_cast(std::distance(slices.begin(), it)); slices.erase(it); @@ -179,11 +183,12 @@ void SliceManager::removeSliceAt(int samplePos, int tolerance) void SliceManager::moveSlice(int fromSample, int toSample) { const juce::CriticalSection::ScopedLockType lock(mutex); + if (beforeChangeCallback) beforeChangeCallback(); for (size_t i = 0; i < slices.size(); ++i) { if (std::abs(slices[i].startSample - fromSample) < 10) { - pushHistory(); + slices[i].startSample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, toSample); if (i > 0) slices[i - 1].endSample = slices[i].startSample; @@ -199,10 +204,11 @@ void SliceManager::trimStart(int sample) { const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; + if (beforeChangeCallback) beforeChangeCallback(); sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample); if (sample <= 0) return; - pushHistory(); + const int numChannels = sampleBuffer->getNumChannels(); const int oldNumSamples = sampleBuffer->getNumSamples(); @@ -239,11 +245,12 @@ void SliceManager::trimEnd(int sample) { const juce::CriticalSection::ScopedLockType lock(mutex); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; + if (beforeChangeCallback) beforeChangeCallback(); const int numSamples = sampleBuffer->getNumSamples(); sample = juce::jlimit(0, numSamples, sample); if (sample >= numSamples) return; - pushHistory(); + const int numChannels = sampleBuffer->getNumChannels(); @@ -283,8 +290,10 @@ void SliceManager::sortSlices() void SliceManager::assignMidiNotes() { + const int numRows = 25; + const int baseMidiNote = 60; for (int i = 0; i < static_cast(slices.size()); ++i) - slices[static_cast(i)].midiNote = i % 12; + slices[static_cast(i)].midiNote = baseMidiNote + (i % numRows); } std::vector SliceManager::computeEnvelope(const float* data, int numSamples) @@ -364,37 +373,3 @@ std::vector SliceManager::detectOnsets(const std::vector& envelope return onsets; } -void SliceManager::pushHistory() -{ - undoStack.push_back(slices); - if (undoStack.size() > maxHistory) - undoStack.erase(undoStack.begin()); - redoStack.clear(); -} - -void SliceManager::truncateHistory() -{ - undoStack.clear(); - redoStack.clear(); -} - -void SliceManager::undo() -{ - const juce::CriticalSection::ScopedLockType lock(mutex); - if (undoStack.empty()) return; - redoStack.push_back(slices); - slices = undoStack.back(); - undoStack.pop_back(); -} - -void SliceManager::redo() -{ - const juce::CriticalSection::ScopedLockType lock(mutex); - if (redoStack.empty()) return; - undoStack.push_back(slices); - slices = redoStack.back(); - redoStack.pop_back(); -} - -bool SliceManager::canUndo() const { return !undoStack.empty(); } -bool SliceManager::canRedo() const { return !redoStack.empty(); } diff --git a/Source/SliceManager.h b/Source/SliceManager.h index 1e222df..b584efa 100644 --- a/Source/SliceManager.h +++ b/Source/SliceManager.h @@ -4,6 +4,7 @@ #include #include #include +#include class SliceManager { @@ -12,7 +13,7 @@ public: { int startSample; int endSample; - int midiNote; // pitch class (0 = C, 1 = C#, ..., 11 = B) + int midiNote; // full MIDI note (60 = C4, 61 = C#4, ..., 83 = B5) }; SliceManager(); @@ -31,17 +32,14 @@ public: void trimStart(int sample); void trimEnd(int sample); - void undo(); - void redo(); - bool canUndo() const; - bool canRedo() const; - std::vector getSlices() const; int getNumSlices() const; Slice getSlice(int index) const; void setSlices(const std::vector& newSlices); + void setBeforeChangeCallback(std::function cb) { beforeChangeCallback = std::move(cb); } + int findSliceIndexForSample(int samplePos) const; juce::AudioBuffer* getMutableBuffer() { return sampleBuffer; } @@ -49,14 +47,9 @@ public: private: void sortSlices(); void assignMidiNotes(); - void pushHistory(); - void truncateHistory(); mutable juce::CriticalSection mutex; std::vector slices; - std::vector> undoStack; - std::vector> redoStack; - static constexpr size_t maxHistory = 50; juce::AudioBuffer* sampleBuffer = nullptr; double currentSampleRate = 44100.0; @@ -64,6 +57,8 @@ private: float bassGain = 1.0f; float trebleGain = 1.0f; + std::function beforeChangeCallback; + std::vector computeEnvelope(const float* channelData, int numSamples); std::vector lowpassFilter(const std::vector& input, float cutoffHz); std::vector highpassFilter(const std::vector& input, float cutoffHz); diff --git a/Source/WaveformDisplay.cpp b/Source/WaveformDisplay.cpp index 1ee465d..11ba1f9 100644 --- a/Source/WaveformDisplay.cpp +++ b/Source/WaveformDisplay.cpp @@ -171,19 +171,18 @@ int WaveformDisplay::sampleToX(int sample) const int WaveformDisplay::keyRowToMidiNote(int row) const { - const int numRows = 12; + const int numRows = 25; const int baseMidiNote = 60; return baseMidiNote + (numRows - 1 - row); } int WaveformDisplay::midiNoteToSliceIndex(int note) const { - int pitchClass = note % 12; const auto& slices = sliceManager.getSlices(); int numSlices = sliceManager.getNumSlices(); for (int i = 0; i < numSlices; ++i) { - if (slices[static_cast(i)].midiNote == pitchClass) + if (slices[static_cast(i)].midiNote == note) return i; } return -1; @@ -223,12 +222,12 @@ void WaveformDisplay::paint(juce::Graphics& g) auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight); + // Ruler across full width (drawn first so keys can overlap) + drawRuler(g, waveBounds.removeFromTop(20.0f)); + // Left key area (matches piano roll) drawKeys(g, waveBounds.withWidth(keyWidth)); - // Ruler across full width - drawRuler(g, waveBounds.removeFromTop(20.0f)); - // Waveform and markers in the area to the right of keys auto waveArea = waveBounds.withTrimmedLeft(keyWidth); drawWaveform(g, waveArea); @@ -240,7 +239,7 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle bounds) g.setColour(juce::Colour(0xff222233)); g.fillRect(bounds); - const int numRows = 12; + const int numRows = 25; float rowHeight = bounds.getHeight() / static_cast(numRows); juce::Colour markerColours[] = { @@ -545,14 +544,15 @@ void WaveformDisplay::mouseDown(const juce::MouseEvent& e) } auto bounds = getLocalBounds().toFloat(); - auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight); + auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight).withTrimmedTop(20.0f); // Check if click is in the key area if (e.getPosition().getX() < static_cast(keyArea.getWidth())) { - float rowHeight = keyArea.getHeight() / 12.0f; + const int numRows = 25; + float rowHeight = keyArea.getHeight() / static_cast(numRows); int row = static_cast((static_cast(e.getPosition().getY()) - keyArea.getY()) / rowHeight); - row = juce::jlimit(0, 11, row); + row = juce::jlimit(0, numRows - 1, row); int midiNote = keyRowToMidiNote(row); if (onKeyClick)