mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 04:10:45 +02:00
Initial commit
This commit is contained in:
commit
638cd05ea8
14 changed files with 2783 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
48
CMakeLists.txt
Normal file
48
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
cmake_minimum_required(VERSION 3.22)
|
||||
project(MonoSlicer VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_subdirectory(lib/JUCE)
|
||||
|
||||
juce_add_plugin(MonoSlicer
|
||||
COMPANY_NAME "MonoSlicer"
|
||||
PLUGIN_MANUFACTURER_CODE RcSp
|
||||
PLUGIN_CODE MsP1
|
||||
FORMATS AU VST3 Standalone
|
||||
PRODUCT_NAME "MonoSlicer"
|
||||
IS_SYNTH TRUE
|
||||
NEEDS_MIDI_INPUT TRUE
|
||||
NEEDS_MIDI_OUTPUT FALSE
|
||||
IS_MIDI_EFFECT FALSE
|
||||
EDITOR_WANTS_KEYBOARD_FOCUS TRUE
|
||||
COPY_PLUGIN_AFTER_BUILD FALSE
|
||||
VST3_CATEGORIES Instrument Sampler
|
||||
)
|
||||
|
||||
target_sources(MonoSlicer PRIVATE
|
||||
Source/PluginProcessor.cpp
|
||||
Source/PluginEditor.cpp
|
||||
Source/SliceManager.cpp
|
||||
Source/WaveformDisplay.cpp
|
||||
Source/PianoRollDisplay.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(MonoSlicer PUBLIC
|
||||
JUCE_WEB_BROWSER=0
|
||||
JUCE_USE_CURL=0
|
||||
JUCE_VST3_CAN_REPLACE_VST2=0
|
||||
)
|
||||
|
||||
target_link_libraries(MonoSlicer
|
||||
PRIVATE
|
||||
juce::juce_audio_utils
|
||||
juce::juce_audio_processors
|
||||
juce::juce_audio_formats
|
||||
juce::juce_dsp
|
||||
PUBLIC
|
||||
juce::juce_recommended_config_flags
|
||||
juce::juce_recommended_lto_flags
|
||||
juce::juce_recommended_warning_flags
|
||||
)
|
||||
37
Makefile
Normal file
37
Makefile
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
JUCE_REPO := https://github.com/juce-framework/JUCE.git
|
||||
JUCE_DIR := lib/JUCE
|
||||
JUCE_TAG := 8.0.9
|
||||
BUILD_DIR := build
|
||||
CMAKE_ARGS := -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
.PHONY: all clean fetch configure build install
|
||||
|
||||
all: install
|
||||
|
||||
$(JUCE_DIR)/CMakeLists.txt:
|
||||
@echo ">>> Fetching JUCE $(JUCE_TAG)..."
|
||||
git clone --depth 1 --branch $(JUCE_TAG) $(JUCE_REPO) $(JUCE_DIR)
|
||||
|
||||
fetch: $(JUCE_DIR)/CMakeLists.txt
|
||||
|
||||
configure: fetch
|
||||
@echo ">>> Configuring..."
|
||||
mkdir -p $(BUILD_DIR)
|
||||
cmake -B $(BUILD_DIR) -S . $(CMAKE_ARGS)
|
||||
|
||||
build: configure
|
||||
@echo ">>> Building MonoSlicer..."
|
||||
cmake --build $(BUILD_DIR) -j$$(sysctl -n hw.ncpu)
|
||||
|
||||
install: build
|
||||
@echo ">>> Installing plugins..."
|
||||
mkdir -p ~/Library/Audio/Plug-Ins/Components
|
||||
mkdir -p ~/Library/Audio/Plug-Ins/VST3
|
||||
cp -R $(BUILD_DIR)/MonoSlicer_artefacts/Release/AU/MonoSlicer.component ~/Library/Audio/Plug-Ins/Components/
|
||||
cp -R $(BUILD_DIR)/MonoSlicer_artefacts/Release/VST3/MonoSlicer.vst3 ~/Library/Audio/Plug-Ins/VST3/
|
||||
@echo ">>> Rescanning Audio Units..."
|
||||
killall -9 AudioComponentRegistrar 2>/dev/null; true
|
||||
@echo ">>> Done. MonoSlicer installed as AU and VST3."
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
134
Source/PianoRollDisplay.cpp
Normal file
134
Source/PianoRollDisplay.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "PianoRollDisplay.h"
|
||||
|
||||
PianoRollDisplay::PianoRollDisplay(SliceManager& sm) : sliceManager(sm)
|
||||
{
|
||||
startTimerHz(20);
|
||||
}
|
||||
|
||||
PianoRollDisplay::~PianoRollDisplay() { stopTimer(); }
|
||||
|
||||
void PianoRollDisplay::timerCallback() { repaint(); }
|
||||
|
||||
void PianoRollDisplay::setActiveSlice(int index) { activeSlice = index; }
|
||||
void PianoRollDisplay::setSelectedSlice(int index) { selectedSlice = index; }
|
||||
void PianoRollDisplay::setTotalSamples(int total) { totalSamples = total; }
|
||||
void PianoRollDisplay::setOnKeyClick(std::function<void(int)> callback) { onKeyClick = std::move(callback); }
|
||||
|
||||
juce::Colour PianoRollDisplay::sliceColour(int sliceIndex) const
|
||||
{
|
||||
juce::Colour colours[] = {
|
||||
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
|
||||
juce::Colour(0xffa8e6cf), juce::Colour(0xff8b94ff), juce::Colour(0xffff8b94),
|
||||
juce::Colour(0xffdda0dd), juce::Colour(0xff87ceeb), juce::Colour(0xff98fb98),
|
||||
juce::Colour(0xffffb347), juce::Colour(0xffc39bd3), juce::Colour(0xff76d7c4)
|
||||
};
|
||||
return colours[sliceIndex % 12];
|
||||
}
|
||||
|
||||
int PianoRollDisplay::rowToSliceIndex(int row) const
|
||||
{
|
||||
int noteRow = numRows - 1 - row;
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
for (int i = 0; i < sliceManager.getNumSlices(); ++i)
|
||||
{
|
||||
if (slices[static_cast<size_t>(i)].midiNote % numRows == noteRow)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void PianoRollDisplay::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
float w = static_cast<float>(bounds.getWidth());
|
||||
float h = static_cast<float>(bounds.getHeight());
|
||||
float rowHeight = h / static_cast<float>(numRows);
|
||||
|
||||
g.fillAll(juce::Colour(0xff1a1a2e));
|
||||
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
int numSlices = sliceManager.getNumSlices();
|
||||
|
||||
int total = totalSamples;
|
||||
if (total <= 0 && numSlices > 0)
|
||||
total = slices[static_cast<size_t>(numSlices - 1)].endSample;
|
||||
|
||||
// Draw grid lines
|
||||
for (int row = 0; row < numRows; ++row)
|
||||
{
|
||||
float y = static_cast<float>(row) * rowHeight;
|
||||
g.setColour(juce::Colour(0x20ffffff));
|
||||
g.drawHorizontalLine(static_cast<int>(y), 0.0f, w);
|
||||
}
|
||||
g.drawHorizontalLine(static_cast<int>(h), 0.0f, w);
|
||||
|
||||
// Draw slice blocks
|
||||
if (numSlices > 0 && total > 0)
|
||||
{
|
||||
for (int i = 0; i < numSlices; ++i)
|
||||
{
|
||||
const auto& slice = slices[static_cast<size_t>(i)];
|
||||
int noteRow = slice.midiNote % numRows;
|
||||
int row = numRows - 1 - noteRow;
|
||||
|
||||
float xFrac = static_cast<float>(slice.startSample) / static_cast<float>(total);
|
||||
float endFrac = static_cast<float>(slice.endSample) / static_cast<float>(total);
|
||||
float x = xFrac * w;
|
||||
float bw = (endFrac - xFrac) * w;
|
||||
|
||||
float y = static_cast<float>(row) * rowHeight + 2.0f;
|
||||
float bh = rowHeight - 4.0f;
|
||||
|
||||
bw = juce::jmax(bw, 2.0f);
|
||||
|
||||
juce::Colour col = sliceColour(i);
|
||||
bool isActive = (i == activeSlice);
|
||||
bool isSelected = (i == selectedSlice);
|
||||
|
||||
g.setColour(isActive ? col.brighter(0.3f) : col.withAlpha(0.8f));
|
||||
g.fillRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f);
|
||||
|
||||
if (isSelected && !isActive)
|
||||
{
|
||||
g.setColour(col.brighter(0.5f));
|
||||
g.drawRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f, 2.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour(col.brighter(0.5f).withAlpha(0.6f));
|
||||
g.drawRoundedRectangle(x + 1.0f, y, bw - 2.0f, bh, 3.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (bw > 16.0f)
|
||||
{
|
||||
g.setColour(juce::Colours::white);
|
||||
g.setFont(10.0f);
|
||||
g.drawText(juce::String(i + 1), static_cast<int>(x + 3.0f), static_cast<int>(y),
|
||||
static_cast<int>(bw - 6.0f), static_cast<int>(bh),
|
||||
juce::Justification::centred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PianoRollDisplay::resized() {}
|
||||
|
||||
void PianoRollDisplay::mouseDown(const juce::MouseEvent& e)
|
||||
{
|
||||
float rowHeight = static_cast<float>(getHeight()) / static_cast<float>(numRows);
|
||||
int row = static_cast<int>(static_cast<float>(e.getPosition().getY()) / rowHeight);
|
||||
row = juce::jlimit(0, numRows - 1, row);
|
||||
|
||||
pressedKey = row;
|
||||
repaint();
|
||||
|
||||
int sliceIdx = rowToSliceIndex(row);
|
||||
if (sliceIdx >= 0 && onKeyClick)
|
||||
onKeyClick(sliceIdx);
|
||||
}
|
||||
|
||||
void PianoRollDisplay::mouseUp(const juce::MouseEvent&)
|
||||
{
|
||||
pressedKey = -1;
|
||||
repaint();
|
||||
}
|
||||
41
Source/PianoRollDisplay.h
Normal file
41
Source/PianoRollDisplay.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "SliceManager.h"
|
||||
#include <functional>
|
||||
|
||||
class PianoRollDisplay : public juce::Component,
|
||||
public juce::Timer
|
||||
{
|
||||
public:
|
||||
static constexpr int numRows = 12;
|
||||
static constexpr int baseMidiNote = 60;
|
||||
static constexpr float keyWidth = 50.0f;
|
||||
|
||||
PianoRollDisplay(SliceManager& sm);
|
||||
~PianoRollDisplay() override;
|
||||
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
void mouseDown(const juce::MouseEvent& e) override;
|
||||
void mouseUp(const juce::MouseEvent& e) override;
|
||||
|
||||
void setActiveSlice(int index);
|
||||
void setSelectedSlice(int index);
|
||||
void setTotalSamples(int total);
|
||||
void setOnKeyClick(std::function<void(int sliceIndex)> callback);
|
||||
|
||||
private:
|
||||
SliceManager& sliceManager;
|
||||
int activeSlice = -1;
|
||||
int selectedSlice = -1;
|
||||
int totalSamples = 0;
|
||||
int pressedKey = -1;
|
||||
|
||||
std::function<void(int)> onKeyClick;
|
||||
|
||||
juce::Colour sliceColour(int sliceIndex) const;
|
||||
int rowToSliceIndex(int row) const;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PianoRollDisplay)
|
||||
};
|
||||
462
Source/PluginEditor.cpp
Normal file
462
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
#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<ComboBoxAttachment>(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<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,
|
||||
&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<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);
|
||||
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<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);
|
||||
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Source/PluginEditor.h
Normal file
96
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "WaveformDisplay.h"
|
||||
|
||||
class MonoSlicerEditor : public juce::AudioProcessorEditor,
|
||||
private juce::FileDragAndDropTarget,
|
||||
private juce::Button::Listener,
|
||||
private juce::ComboBox::Listener,
|
||||
private juce::Timer
|
||||
{
|
||||
public:
|
||||
MonoSlicerEditor(MonoSlicerProcessor&);
|
||||
~MonoSlicerEditor() override;
|
||||
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
bool isInterestedInFileDrag(const juce::StringArray& files) override;
|
||||
void filesDropped(const juce::StringArray& files, int x, int y) override;
|
||||
void buttonClicked(juce::Button* button) override;
|
||||
void comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) override;
|
||||
void timerCallback() override;
|
||||
|
||||
private:
|
||||
MonoSlicerProcessor& processorRef;
|
||||
|
||||
WaveformDisplay waveformDisplay;
|
||||
|
||||
// Bottom panel buttons
|
||||
juce::TextButton autoSliceButton { "Auto Slice" };
|
||||
juce::TextButton clearSlicesButton { "Clear Slices" };
|
||||
|
||||
// Title bar buttons
|
||||
juce::TextButton loadSampleButton { "Load" };
|
||||
juce::TextButton prevSampleButton { "<" };
|
||||
juce::TextButton nextSampleButton { ">" };
|
||||
juce::TextButton undoButton { "Undo" };
|
||||
juce::TextButton redoButton { "Redo" };
|
||||
juce::TextButton zoomInButton { "+" };
|
||||
juce::TextButton zoomOutButton { "-" };
|
||||
juce::TextButton zoomResetButton { "1:1" };
|
||||
juce::ComboBox scaleComboBox;
|
||||
|
||||
static constexpr int baseWidth = 1100;
|
||||
static constexpr int baseHeight = 550;
|
||||
|
||||
// Slice knobs
|
||||
juce::Slider bassSlider, trebleSlider, sensitivitySlider;
|
||||
juce::Label bassLabel, trebleLabel, sensitivityLabel;
|
||||
|
||||
// Amp ADSR knobs
|
||||
juce::Slider ampAttackSlider, ampDecaySlider, ampSustainSlider, ampReleaseSlider;
|
||||
juce::Label ampAttackLabel, ampDecayLabel, ampSustainLabel, ampReleaseLabel;
|
||||
|
||||
// Filter type selector
|
||||
juce::ComboBox filterTypeComboBox;
|
||||
|
||||
// Filter knobs
|
||||
juce::Slider filterCutoffSlider, filterResoSlider, filterEnvDepthSlider;
|
||||
juce::Label filterCutoffLabel, filterResoLabel, filterEnvDepthLabel;
|
||||
|
||||
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
|
||||
using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment;
|
||||
|
||||
// Filter ADSR knobs
|
||||
juce::Slider filterAttackSlider, filterDecaySlider, filterSustainSlider, filterReleaseSlider;
|
||||
juce::Label filterAttackLabel, filterDecayLabel, filterSustainLabel, filterReleaseLabel;
|
||||
|
||||
// Time-stretch controls
|
||||
juce::Slider bpmSlider;
|
||||
juce::ComboBox stretchBeatsComboBox;
|
||||
juce::TextButton stretchButton { "Stretch" };
|
||||
juce::ComboBox keyShiftComboBox;
|
||||
juce::Label keyShiftLabel;
|
||||
std::unique_ptr<SliderAttachment> stretchBpmAttachment;
|
||||
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
|
||||
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;
|
||||
std::unique_ptr<ComboBoxAttachment> scaleAttachment;
|
||||
|
||||
std::unique_ptr<SliderAttachment> bassAttachment, trebleAttachment, sensitivityAttachment;
|
||||
std::unique_ptr<SliderAttachment> ampAttackAttachment, ampDecayAttachment, ampSustainAttachment, ampReleaseAttachment;
|
||||
std::unique_ptr<SliderAttachment> filterCutoffAttachment, filterResoAttachment, filterEnvDepthAttachment;
|
||||
std::unique_ptr<SliderAttachment> filterAttackAttachment, filterDecayAttachment, filterSustainAttachment, filterReleaseAttachment;
|
||||
std::unique_ptr<ComboBoxAttachment> filterTypeAttachment;
|
||||
|
||||
juce::FileChooser fileChooser { "Load Audio File", juce::File{}, "*.wav;*.aiff;*.flac;*.ogg" };
|
||||
|
||||
void setupSlider(juce::Slider& slider, juce::Label& label, const juce::String& name);
|
||||
void attachSlider(juce::Slider& slider, juce::Label& label, const juce::String& name,
|
||||
const juce::String& paramId, std::unique_ptr<SliderAttachment>& attachment);
|
||||
|
||||
int keyClickNote = -1;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MonoSlicerEditor)
|
||||
};
|
||||
633
Source/PluginProcessor.cpp
Normal file
633
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
MonoSlicerProcessor::MonoSlicerProcessor()
|
||||
: AudioProcessor(BusesProperties()
|
||||
.withInput("Input", juce::AudioChannelSet::stereo(), true)
|
||||
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
}
|
||||
|
||||
MonoSlicerProcessor::~MonoSlicerProcessor() {}
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createParameterLayout()
|
||||
{
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"bass", "Bass", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"treble", "Treble", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"sensitivity", "Sensitivity", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
|
||||
|
||||
// Amp ADSR
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"ampAttack", "Amp Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"ampDecay", "Amp Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.1f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"ampSustain", "Amp Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.7f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"ampRelease", "Amp Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.2f));
|
||||
|
||||
// Filter
|
||||
juce::StringArray filterTypeNames { "LP12", "LP24", "HP", "BP", "Notch" };
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
"filterType", "Filter Type", filterTypeNames, 0));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterCutoff", "Filter Cutoff", juce::NormalisableRange<float>(20.0f, 20000.0f, 1.0f, 0.3f), 20000.0f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterReso", "Filter Reso", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
||||
|
||||
// Filter ADSR
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterAttack", "Flt Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterDecay", "Flt Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.3f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterSustain", "Flt Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterRelease", "Flt Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.5f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"filterEnvDepth", "Flt Env Depth", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterBool>(
|
||||
"selectViaMidi", "Select Via MIDI", false));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
"stretchBpm", "Stretch BPM", juce::NormalisableRange<float>(20.0f, 300.0f, 1.0f), 120.0f));
|
||||
|
||||
juce::StringArray beatChoices { "1", "2", "4", "8", "16", "32" };
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
"stretchBeats", "Stretch Beats", beatChoices, 2));
|
||||
|
||||
layout.add(std::make_unique<juce::AudioParameterInt>(
|
||||
"keyShift", "Key Shift", -12, 12, 0));
|
||||
|
||||
juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" };
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
"uiScale", "UI Scale", scaleChoices, 2));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
currentSampleRate = sampleRate;
|
||||
juce::ignoreUnused(samplesPerBlock);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::releaseResources() {}
|
||||
|
||||
void MonoSlicerProcessor::computeBiquadCoeffs(FilterType type, float cutoff, float resonance,
|
||||
float sampleRate, float& b0, float& b1, float& b2,
|
||||
float& a1, float& a2)
|
||||
{
|
||||
cutoff = juce::jlimit(20.0f, static_cast<float>(sampleRate * 0.45), cutoff);
|
||||
float Q = 0.5f + resonance * 19.5f; // Q range: 0.5 to 20
|
||||
float w0 = 2.0f * juce::MathConstants<float>::pi * cutoff / sampleRate;
|
||||
float cosW0 = std::cos(w0);
|
||||
float sinW0 = std::sin(w0);
|
||||
float alpha = sinW0 / (2.0f * Q);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FilterType::LP12:
|
||||
{
|
||||
float norm = 1.0f / (1.0f + alpha);
|
||||
b0 = (1.0f - cosW0) * 0.5f * norm;
|
||||
b1 = (1.0f - cosW0) * norm;
|
||||
b2 = (1.0f - cosW0) * 0.5f * norm;
|
||||
a1 = -2.0f * cosW0 * norm;
|
||||
a2 = (1.0f - alpha) * norm;
|
||||
break;
|
||||
}
|
||||
case FilterType::LP24:
|
||||
{
|
||||
// Two cascaded LP12
|
||||
float norm = 1.0f / (1.0f + alpha);
|
||||
float tb0 = (1.0f - cosW0) * 0.5f * norm;
|
||||
float tb1 = (1.0f - cosW0) * norm;
|
||||
float tb2 = (1.0f - cosW0) * 0.5f * norm;
|
||||
float ta1 = -2.0f * cosW0 * norm;
|
||||
float ta2 = (1.0f - alpha) * norm;
|
||||
// Cascade: multiply two identical biquads
|
||||
b0 = tb0 * tb0;
|
||||
b1 = 2.0f * tb0 * tb1;
|
||||
b2 = 2.0f * tb0 * tb2 + tb1 * tb1;
|
||||
a1 = 2.0f * ta1 + ta1 * ta1 - 2.0f * ta2;
|
||||
a2 = ta2 * ta2 - ta1 * ta1 * ta2 + ta1 * ta1;
|
||||
// Simplified: just use LP12 coefficients with boosted resonance effect
|
||||
// For numerical stability, approximate LP24 as steeper LP12
|
||||
b0 = tb0; b1 = tb1; b2 = tb2; a1 = ta1; a2 = ta2;
|
||||
// Apply gain compensation for steeper rolloff feel
|
||||
float gainComp = 1.0f + Q * 0.1f;
|
||||
b0 *= gainComp; b1 *= gainComp; b2 *= gainComp;
|
||||
break;
|
||||
}
|
||||
case FilterType::HP:
|
||||
{
|
||||
float norm = 1.0f / (1.0f + alpha);
|
||||
b0 = (1.0f + cosW0) * 0.5f * norm;
|
||||
b1 = -(1.0f + cosW0) * norm;
|
||||
b2 = (1.0f + cosW0) * 0.5f * norm;
|
||||
a1 = -2.0f * cosW0 * norm;
|
||||
a2 = (1.0f - alpha) * norm;
|
||||
break;
|
||||
}
|
||||
case FilterType::BP:
|
||||
{
|
||||
float norm = 1.0f / (1.0f + alpha);
|
||||
b0 = alpha * norm;
|
||||
b1 = 0.0f;
|
||||
b2 = -alpha * norm;
|
||||
a1 = -2.0f * cosW0 * norm;
|
||||
a2 = (1.0f - alpha) * norm;
|
||||
break;
|
||||
}
|
||||
case FilterType::Notch:
|
||||
{
|
||||
float norm = 1.0f / (1.0f + alpha);
|
||||
b0 = norm;
|
||||
b1 = -2.0f * cosW0 * norm;
|
||||
b2 = norm;
|
||||
a1 = -2.0f * cosW0 * norm;
|
||||
a2 = (1.0f - alpha) * norm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::advanceAmpEnvelope(Voice& voice, int samples)
|
||||
{
|
||||
float attack = apvts.getRawParameterValue("ampAttack")->load();
|
||||
float decay = apvts.getRawParameterValue("ampDecay")->load();
|
||||
float sustain = apvts.getRawParameterValue("ampSustain")->load();
|
||||
float release = apvts.getRawParameterValue("ampRelease")->load();
|
||||
|
||||
int attackSamples = static_cast<int>(attack * currentSampleRate);
|
||||
int decaySamples = static_cast<int>(decay * currentSampleRate);
|
||||
int releaseSamples = static_cast<int>(release * currentSampleRate);
|
||||
if (attackSamples < 1) attackSamples = 1;
|
||||
if (decaySamples < 1) decaySamples = 1;
|
||||
if (releaseSamples < 1) releaseSamples = 1;
|
||||
|
||||
for (int i = 0; i < samples; ++i)
|
||||
{
|
||||
switch (voice.ampStage)
|
||||
{
|
||||
case AmpStage::Attack:
|
||||
voice.ampEnv += 1.0f / static_cast<float>(attackSamples);
|
||||
if (voice.ampEnv >= 1.0f)
|
||||
{
|
||||
voice.ampEnv = 1.0f;
|
||||
voice.ampStage = AmpStage::Decay;
|
||||
voice.ampSamplesToNext = decaySamples;
|
||||
}
|
||||
break;
|
||||
|
||||
case AmpStage::Decay:
|
||||
voice.ampSamplesToNext--;
|
||||
if (voice.ampSamplesToNext <= 0)
|
||||
voice.ampStage = AmpStage::Sustain;
|
||||
else
|
||||
voice.ampEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast<float>(voice.ampSamplesToNext) / static_cast<float>(decaySamples));
|
||||
break;
|
||||
|
||||
case AmpStage::Sustain:
|
||||
voice.ampEnv = sustain;
|
||||
break;
|
||||
|
||||
case AmpStage::Release:
|
||||
voice.ampEnv -= 1.0f / static_cast<float>(releaseSamples);
|
||||
if (voice.ampEnv <= 0.0f)
|
||||
{
|
||||
voice.ampEnv = 0.0f;
|
||||
voice.ampStage = AmpStage::Idle;
|
||||
voice.active = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case AmpStage::Idle:
|
||||
voice.ampEnv = 0.0f;
|
||||
voice.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::advanceFilterEnvelope(Voice& voice, int samples)
|
||||
{
|
||||
float attack = apvts.getRawParameterValue("filterAttack")->load();
|
||||
float decay = apvts.getRawParameterValue("filterDecay")->load();
|
||||
float sustain = apvts.getRawParameterValue("filterSustain")->load();
|
||||
float release = apvts.getRawParameterValue("filterRelease")->load();
|
||||
|
||||
int attackSamples = static_cast<int>(attack * currentSampleRate);
|
||||
int decaySamples = static_cast<int>(decay * currentSampleRate);
|
||||
int releaseSamples = static_cast<int>(release * currentSampleRate);
|
||||
if (attackSamples < 1) attackSamples = 1;
|
||||
if (decaySamples < 1) decaySamples = 1;
|
||||
if (releaseSamples < 1) releaseSamples = 1;
|
||||
|
||||
for (int i = 0; i < samples; ++i)
|
||||
{
|
||||
switch (voice.filterStage)
|
||||
{
|
||||
case AmpStage::Attack:
|
||||
voice.filterEnv += 1.0f / static_cast<float>(attackSamples);
|
||||
if (voice.filterEnv >= 1.0f)
|
||||
{
|
||||
voice.filterEnv = 1.0f;
|
||||
voice.filterStage = AmpStage::Decay;
|
||||
voice.filterSamplesToNext = decaySamples;
|
||||
}
|
||||
break;
|
||||
|
||||
case AmpStage::Decay:
|
||||
voice.filterSamplesToNext--;
|
||||
if (voice.filterSamplesToNext <= 0)
|
||||
voice.filterStage = AmpStage::Sustain;
|
||||
else
|
||||
voice.filterEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast<float>(voice.filterSamplesToNext) / static_cast<float>(decaySamples));
|
||||
break;
|
||||
|
||||
case AmpStage::Sustain:
|
||||
voice.filterEnv = sustain;
|
||||
break;
|
||||
|
||||
case AmpStage::Release:
|
||||
voice.filterEnv -= 1.0f / static_cast<float>(releaseSamples);
|
||||
if (voice.filterEnv <= 0.0f) voice.filterEnv = 0.0f;
|
||||
break;
|
||||
|
||||
case AmpStage::Idle:
|
||||
voice.filterEnv = 0.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
buffer.clear();
|
||||
|
||||
if (auto* ph = getPlayHead())
|
||||
{
|
||||
auto posInfo = ph->getPosition();
|
||||
if (posInfo.hasValue())
|
||||
{
|
||||
auto bpmOpt = posInfo->getBpm();
|
||||
if (bpmOpt.hasValue() && *bpmOpt > 0)
|
||||
{
|
||||
float hostBpm = juce::jlimit(20.0f, 300.0f, static_cast<float>(*bpmOpt));
|
||||
if (auto* p = apvts.getParameter("stretchBpm"))
|
||||
p->setValueNotifyingHost(p->convertTo0to1(hostBpm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sliceManager.setSensitivity(apvts.getRawParameterValue("sensitivity")->load());
|
||||
sliceManager.setBassGain(apvts.getRawParameterValue("bass")->load());
|
||||
sliceManager.setTrebleGain(apvts.getRawParameterValue("treble")->load());
|
||||
|
||||
if (sampleBuffer.getNumSamples() == 0)
|
||||
return;
|
||||
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const int numOutputChannels = buffer.getNumChannels();
|
||||
|
||||
for (const auto metadata : midiMessages)
|
||||
{
|
||||
auto msg = metadata.getMessage();
|
||||
if (msg.isNoteOn())
|
||||
handleNoteOn(msg.getNoteNumber(), msg.getFloatVelocity());
|
||||
else if (msg.isNoteOff())
|
||||
handleNoteOff();
|
||||
}
|
||||
|
||||
for (auto& voice : voices)
|
||||
{
|
||||
if (!voice.active) continue;
|
||||
|
||||
int numSlices = sliceManager.getNumSlices();
|
||||
int startSample = 0;
|
||||
int endSample = sampleBuffer.getNumSamples();
|
||||
|
||||
if (numSlices > 0)
|
||||
{
|
||||
if (voice.sliceIndex < 0 || voice.sliceIndex >= numSlices)
|
||||
{
|
||||
voice.active = false;
|
||||
continue;
|
||||
}
|
||||
const auto& slice = sliceManager.getSlice(voice.sliceIndex);
|
||||
startSample = slice.startSample;
|
||||
endSample = slice.endSample;
|
||||
}
|
||||
|
||||
int sliceLength = endSample - startSample;
|
||||
if (sliceLength <= 0) { voice.active = false; continue; }
|
||||
|
||||
advanceAmpEnvelope(voice, numSamples);
|
||||
advanceFilterEnvelope(voice, numSamples);
|
||||
|
||||
if (!voice.active) continue;
|
||||
|
||||
int filterTypeIdx = static_cast<int>(apvts.getRawParameterValue("filterType")->load());
|
||||
FilterType filterType = static_cast<FilterType>(juce::jlimit(0, 4, filterTypeIdx));
|
||||
float baseCutoff = apvts.getRawParameterValue("filterCutoff")->load();
|
||||
float resonance = apvts.getRawParameterValue("filterReso")->load();
|
||||
float envDepth = apvts.getRawParameterValue("filterEnvDepth")->load();
|
||||
float minCutoff = 20.0f;
|
||||
float maxCutoff = juce::jmin(baseCutoff, static_cast<float>(currentSampleRate * 0.45));
|
||||
|
||||
float effectiveCutoff = maxCutoff - (maxCutoff - minCutoff) * envDepth * voice.filterEnv;
|
||||
effectiveCutoff = juce::jlimit(20.0f, maxCutoff, effectiveCutoff);
|
||||
|
||||
float b0, b1, b2, a1, a2;
|
||||
computeBiquadCoeffs(filterType, effectiveCutoff, resonance, static_cast<float>(currentSampleRate),
|
||||
b0, b1, b2, a1, a2);
|
||||
|
||||
// Key shift: pitch factor = 2^(semitones/12)
|
||||
int keyShift = static_cast<int>(apvts.getRawParameterValue("keyShift")->load());
|
||||
float pitchFactor = std::pow(2.0f, static_cast<float>(keyShift) / 12.0f);
|
||||
|
||||
const int bufSamples = sampleBuffer.getNumSamples();
|
||||
|
||||
bool filterActive = (envDepth > 0.001f)
|
||||
|| (baseCutoff < static_cast<float>(currentSampleRate * 0.45) - 1.0f)
|
||||
|| (resonance > 0.001f);
|
||||
|
||||
float startPos = voice.position;
|
||||
float endPos = startPos;
|
||||
|
||||
for (int ch = 0; ch < numOutputChannels; ++ch)
|
||||
{
|
||||
float* outData = buffer.getWritePointer(ch);
|
||||
auto& filt = (ch == 0) ? voice.filterL : voice.filterR;
|
||||
if (filterActive)
|
||||
{
|
||||
filt.b0 = b0; filt.b1 = b1; filt.b2 = b2;
|
||||
filt.a1 = a1; filt.a2 = a2;
|
||||
}
|
||||
|
||||
float chPos = startPos;
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (chPos >= static_cast<float>(sliceLength))
|
||||
break;
|
||||
|
||||
int idx = static_cast<int>(chPos);
|
||||
float frac = chPos - static_cast<float>(idx);
|
||||
|
||||
int sampleIdx = startSample + idx;
|
||||
if (sampleIdx >= bufSamples)
|
||||
break;
|
||||
|
||||
int bufCh = juce::jmin(ch, sampleBuffer.getNumChannels() - 1);
|
||||
float s0 = sampleBuffer.getSample(bufCh, sampleIdx);
|
||||
float s1 = (sampleIdx + 1 < bufSamples) ? sampleBuffer.getSample(bufCh, sampleIdx + 1) : s0;
|
||||
float dry = (s0 + frac * (s1 - s0)) * voice.velocity;
|
||||
|
||||
float output;
|
||||
if (filterActive)
|
||||
output = filt.process(dry) * voice.ampEnv;
|
||||
else
|
||||
output = dry * voice.ampEnv;
|
||||
|
||||
outData[i] += output;
|
||||
chPos += pitchFactor;
|
||||
}
|
||||
endPos = chPos;
|
||||
}
|
||||
voice.position = endPos;
|
||||
|
||||
if (voice.position >= static_cast<float>(sliceLength))
|
||||
{
|
||||
voice.ampStage = AmpStage::Release;
|
||||
voice.filterStage = AmpStage::Release;
|
||||
voice.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
currentActiveSlice.store(-1);
|
||||
for (auto& voice : voices)
|
||||
{
|
||||
if (voice.active)
|
||||
{
|
||||
int numSlices = sliceManager.getNumSlices();
|
||||
int baseStart = 0;
|
||||
if (numSlices > 0 && voice.sliceIndex >= 0 && voice.sliceIndex < numSlices)
|
||||
baseStart = sliceManager.getSlice(voice.sliceIndex).startSample;
|
||||
currentPlaybackSample.store(baseStart + static_cast<int>(voice.position));
|
||||
currentActiveSlice.store(voice.sliceIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (currentActiveSlice.load() < 0)
|
||||
currentPlaybackSample.store(-1);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity)
|
||||
{
|
||||
int sliceIdx = noteNumber - 60;
|
||||
sliceIdx = juce::jlimit(0, juce::jmax(0, sliceManager.getNumSlices() - 1), sliceIdx);
|
||||
|
||||
if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f)
|
||||
selectedSlice.store(sliceIdx);
|
||||
|
||||
for (auto& voice : voices)
|
||||
{
|
||||
if (!voice.active)
|
||||
{
|
||||
voice.active = true;
|
||||
voice.sliceIndex = sliceIdx;
|
||||
voice.position = 0;
|
||||
voice.velocity = velocity;
|
||||
|
||||
voice.ampStage = AmpStage::Attack;
|
||||
voice.ampEnv = 0.0f;
|
||||
|
||||
voice.filterStage = AmpStage::Attack;
|
||||
voice.filterEnv = 0.0f;
|
||||
|
||||
voice.filterL.reset();
|
||||
voice.filterR.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::handleNoteOff()
|
||||
{
|
||||
for (auto& voice : voices)
|
||||
{
|
||||
if (voice.active && voice.ampStage != AmpStage::Release && voice.ampStage != AmpStage::Idle)
|
||||
{
|
||||
voice.ampStage = AmpStage::Release;
|
||||
voice.filterStage = AmpStage::Release;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::loadAudioFile(const juce::File& file)
|
||||
{
|
||||
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
|
||||
if (reader)
|
||||
{
|
||||
int numSamples = static_cast<int>(reader->lengthInSamples);
|
||||
sampleBuffer.setSize(static_cast<int>(reader->numChannels), numSamples);
|
||||
reader->read(&sampleBuffer, 0, numSamples, 0, true, true);
|
||||
loadedSampleRate = reader->sampleRate;
|
||||
currentFile = file;
|
||||
refreshFolderList();
|
||||
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
|
||||
selectedSlice.store(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::refreshFolderList()
|
||||
{
|
||||
folderFiles.clear();
|
||||
currentFileIndex = -1;
|
||||
|
||||
if (!currentFile.existsAsFile()) return;
|
||||
|
||||
auto dir = currentFile.getParentDirectory();
|
||||
juce::DirectoryIterator iter(dir, false, "*.wav;*.aiff;*.flac;*.ogg");
|
||||
while (iter.next())
|
||||
{
|
||||
auto f = iter.getFile();
|
||||
if (f.existsAsFile())
|
||||
{
|
||||
folderFiles.add(f);
|
||||
if (f == currentFile)
|
||||
currentFileIndex = folderFiles.size() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::loadNextSample()
|
||||
{
|
||||
if (folderFiles.isEmpty()) return;
|
||||
int nextIdx = (currentFileIndex + 1) % folderFiles.size();
|
||||
loadAudioFile(folderFiles[nextIdx]);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::loadPreviousSample()
|
||||
{
|
||||
if (folderFiles.isEmpty()) return;
|
||||
int prevIdx = (currentFileIndex - 1 + folderFiles.size()) % folderFiles.size();
|
||||
loadAudioFile(folderFiles[prevIdx]);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::triggerSlice(int sliceIndex, float velocity)
|
||||
{
|
||||
int noteNumber = sliceIndex + 60;
|
||||
handleNoteOn(noteNumber, velocity);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::releaseSlice()
|
||||
{
|
||||
handleNoteOff();
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* MonoSlicerProcessor::createEditor()
|
||||
{
|
||||
return new MonoSlicerEditor(*this);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
juce::MemoryOutputStream stream(destData, false);
|
||||
state.writeToStream(stream);
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes)
|
||||
{
|
||||
auto stream = juce::MemoryInputStream(data, static_cast<size_t>(sizeInBytes), false);
|
||||
auto state = juce::ValueTree::readFromStream(stream);
|
||||
if (state.isValid())
|
||||
apvts.replaceState(state);
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new MonoSlicerProcessor();
|
||||
}
|
||||
|
||||
void MonoSlicerProcessor::stretchToBeats(int numBeats, float bpm)
|
||||
{
|
||||
if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return;
|
||||
|
||||
double sr = static_cast<double>(loadedSampleRate);
|
||||
int currentLength = sampleBuffer.getNumSamples();
|
||||
double currentDuration = static_cast<double>(currentLength) / sr;
|
||||
double targetDuration = static_cast<double>(numBeats) * 60.0 / static_cast<double>(bpm);
|
||||
double ratio = targetDuration / currentDuration;
|
||||
|
||||
if (std::abs(ratio - 1.0) < 0.001) return;
|
||||
|
||||
int newLength = static_cast<int>(static_cast<double>(currentLength) * ratio);
|
||||
if (newLength < 1) return;
|
||||
|
||||
const int numChannels = sampleBuffer.getNumChannels();
|
||||
|
||||
// Save slices before setSampleBuffer clears them
|
||||
auto savedSlices = sliceManager.getSlices();
|
||||
|
||||
juce::AudioBuffer<float> newBuffer(numChannels, newLength);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
{
|
||||
const float* inData = sampleBuffer.getReadPointer(ch);
|
||||
float* outData = newBuffer.getWritePointer(ch);
|
||||
|
||||
for (int i = 0; i < newLength; ++i)
|
||||
{
|
||||
double srcPos = static_cast<double>(i) / ratio;
|
||||
int idx = static_cast<int>(srcPos);
|
||||
float frac = static_cast<float>(srcPos - static_cast<double>(idx));
|
||||
if (idx >= currentLength - 1)
|
||||
{
|
||||
outData[i] = inData[currentLength - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
outData[i] = inData[idx] * (1.0f - frac) + inData[idx + 1] * frac;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sampleBuffer.setSize(numChannels, newLength, false, false, false);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
sampleBuffer.copyFrom(ch, 0, newBuffer, ch, 0, newLength);
|
||||
|
||||
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
|
||||
|
||||
for (const auto& s : savedSlices)
|
||||
{
|
||||
int newStart = static_cast<int>(static_cast<double>(s.startSample) * ratio);
|
||||
int newEnd = static_cast<int>(static_cast<double>(s.endSample) * ratio);
|
||||
if (newEnd > newStart + 100)
|
||||
sliceManager.addSliceManual(newStart);
|
||||
}
|
||||
|
||||
selectedSlice.store(-1);
|
||||
}
|
||||
125
Source/PluginProcessor.h
Normal file
125
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_audio_formats/juce_audio_formats.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include "SliceManager.h"
|
||||
|
||||
class MonoSlicerProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
MonoSlicerProcessor();
|
||||
~MonoSlicerProcessor() override;
|
||||
|
||||
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
const juce::String getName() const override { return JucePlugin_Name; }
|
||||
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return true; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram(int) override {}
|
||||
const juce::String getProgramName(int) override { return {}; }
|
||||
void changeProgramName(int, const juce::String&) override {}
|
||||
|
||||
void getStateInformation(juce::MemoryBlock& destData) override;
|
||||
void setStateInformation(const void* data, int sizeInBytes) override;
|
||||
|
||||
void loadAudioFile(const juce::File& file);
|
||||
void loadNextSample();
|
||||
void loadPreviousSample();
|
||||
void triggerSlice(int sliceIndex, float velocity = 0.8f);
|
||||
void releaseSlice();
|
||||
void stretchToBeats(int numBeats, float bpm);
|
||||
void psolaStretch(int targetLength);
|
||||
float getHostBpm() const;
|
||||
|
||||
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
|
||||
SliceManager& getSliceManager() { return sliceManager; }
|
||||
|
||||
const juce::AudioBuffer<float>& getSampleBuffer() const { return sampleBuffer; }
|
||||
double getSampleRateLoaded() const { return loadedSampleRate; }
|
||||
const juce::File& getCurrentFile() const { return currentFile; }
|
||||
int getCurrentFileIndex() const { return currentFileIndex; }
|
||||
int getNumFilesInFolder() const { return static_cast<int>(folderFiles.size()); }
|
||||
|
||||
std::atomic<int> currentPlaybackSample { -1 };
|
||||
std::atomic<int> currentActiveSlice { -1 };
|
||||
std::atomic<int> selectedSlice { -1 };
|
||||
|
||||
private:
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||
|
||||
SliceManager sliceManager;
|
||||
|
||||
juce::AudioFormatManager formatManager;
|
||||
juce::AudioBuffer<float> sampleBuffer;
|
||||
double loadedSampleRate = 44100.0;
|
||||
double currentSampleRate = 44100.0;
|
||||
|
||||
juce::File currentFile;
|
||||
juce::Array<juce::File> folderFiles;
|
||||
int currentFileIndex = -1;
|
||||
void refreshFolderList();
|
||||
|
||||
enum class AmpStage { Idle, Attack, Decay, Sustain, Release };
|
||||
enum class FilterType { LP12, LP24, HP, BP, Notch };
|
||||
|
||||
struct BiquadState
|
||||
{
|
||||
float x1 = 0.0f, x2 = 0.0f, y1 = 0.0f, y2 = 0.0f;
|
||||
float b0 = 0.0f, b1 = 0.0f, b2 = 0.0f;
|
||||
float a1 = 0.0f, a2 = 0.0f;
|
||||
|
||||
void reset() { x1 = x2 = y1 = y2 = 0.0f; }
|
||||
|
||||
float process(float input)
|
||||
{
|
||||
float output = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
||||
x2 = x1; x1 = input;
|
||||
y2 = y1; y1 = output;
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct Voice
|
||||
{
|
||||
bool active = false;
|
||||
int sliceIndex = -1;
|
||||
float position = 0.0f;
|
||||
float velocity = 0.0f;
|
||||
bool midiNoteSent = false;
|
||||
|
||||
AmpStage ampStage = AmpStage::Idle;
|
||||
float ampEnv = 0.0f;
|
||||
int ampSamplesToNext = 0;
|
||||
|
||||
AmpStage filterStage = AmpStage::Idle;
|
||||
float filterEnv = 0.0f;
|
||||
int filterSamplesToNext = 0;
|
||||
|
||||
BiquadState filterL, filterR;
|
||||
};
|
||||
|
||||
static constexpr int maxVoices = 16;
|
||||
std::array<Voice, maxVoices> voices;
|
||||
|
||||
void handleNoteOn(int noteNumber, float velocity);
|
||||
void handleNoteOff();
|
||||
|
||||
void advanceAmpEnvelope(Voice& voice, int samples);
|
||||
void advanceFilterEnvelope(Voice& voice, int samples);
|
||||
void computeBiquadCoeffs(FilterType type, float cutoff, float resonance, float sampleRate,
|
||||
float& b0, float& b1, float& b2, float& a1, float& a2);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MonoSlicerProcessor)
|
||||
};
|
||||
359
Source/SliceManager.cpp
Normal file
359
Source/SliceManager.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
#include "SliceManager.h"
|
||||
|
||||
SliceManager::SliceManager() = default;
|
||||
|
||||
void SliceManager::setSampleBuffer(juce::AudioBuffer<float>* buffer, double sampleRate)
|
||||
{
|
||||
sampleBuffer = buffer;
|
||||
currentSampleRate = sampleRate;
|
||||
slices.clear();
|
||||
}
|
||||
|
||||
void SliceManager::setSensitivity(float s) { sensitivity = juce::jlimit(0.0f, 1.0f, s); }
|
||||
void SliceManager::setBassGain(float g) { bassGain = juce::jlimit(0.0f, 2.0f, g); }
|
||||
void SliceManager::setTrebleGain(float g) { trebleGain = juce::jlimit(0.0f, 2.0f, g); }
|
||||
|
||||
void SliceManager::autoSlice()
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||
return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
const int numSamples = sampleBuffer->getNumSamples();
|
||||
const int numChannels = sampleBuffer->getNumChannels();
|
||||
|
||||
// Mix down to mono
|
||||
std::vector<float> mono(static_cast<size_t>(numSamples), 0.0f);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
{
|
||||
const float* data = sampleBuffer->getReadPointer(ch);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
mono[static_cast<size_t>(i)] += data[i] / static_cast<float>(numChannels);
|
||||
}
|
||||
|
||||
// Apply bass/treble shaping
|
||||
float bassCutoff = 200.0f;
|
||||
float trebleCutoff = 3000.0f;
|
||||
|
||||
auto shaped = mono;
|
||||
if (bassGain != 1.0f)
|
||||
{
|
||||
auto bassComponent = lowpassFilter(mono, bassCutoff);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
shaped[static_cast<size_t>(i)] += (bassComponent[static_cast<size_t>(i)] - mono[static_cast<size_t>(i)]) * (bassGain - 1.0f);
|
||||
}
|
||||
if (trebleGain != 1.0f)
|
||||
{
|
||||
auto trebleComponent = highpassFilter(mono, trebleCutoff);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
shaped[static_cast<size_t>(i)] += (trebleComponent[static_cast<size_t>(i)] - mono[static_cast<size_t>(i)]) * (trebleGain - 1.0f);
|
||||
}
|
||||
|
||||
// Compute envelope
|
||||
auto envelope = computeEnvelope(shaped.data(), numSamples);
|
||||
|
||||
// Detect onsets
|
||||
float threshold = 0.05f + (1.0f - sensitivity) * 0.45f;
|
||||
auto onsets = detectOnsets(envelope, threshold);
|
||||
|
||||
slices.clear();
|
||||
if (onsets.empty())
|
||||
{
|
||||
// Fallback: single slice
|
||||
slices.push_back({ 0, numSamples, 0 });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add beginning if not close to first onset
|
||||
if (onsets[0] > static_cast<float>(numSamples / 100))
|
||||
onsets.insert(onsets.begin(), 0);
|
||||
|
||||
for (size_t i = 0; i < onsets.size(); ++i)
|
||||
{
|
||||
int start = static_cast<int>(onsets[i]);
|
||||
int end = (i + 1 < onsets.size()) ? static_cast<int>(onsets[i + 1]) : numSamples;
|
||||
if (end > start + 100) // minimum slice length
|
||||
slices.push_back({ start, end, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
assignMidiNotes();
|
||||
}
|
||||
|
||||
void SliceManager::clearSlices()
|
||||
{
|
||||
pushHistory();
|
||||
slices.clear();
|
||||
}
|
||||
|
||||
void SliceManager::addSliceManual(int samplePos)
|
||||
{
|
||||
if (!sampleBuffer) return;
|
||||
const int numSamples = sampleBuffer->getNumSamples();
|
||||
samplePos = juce::jlimit(1, numSamples - 1, samplePos);
|
||||
|
||||
if (slices.empty())
|
||||
{
|
||||
pushHistory();
|
||||
slices.push_back({ 0, numSamples, 0 });
|
||||
assignMidiNotes();
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = findSliceIndexForSample(samplePos);
|
||||
if (idx < 0)
|
||||
{
|
||||
pushHistory();
|
||||
slices.push_back({ samplePos, numSamples, 0 });
|
||||
sortSlices();
|
||||
assignMidiNotes();
|
||||
return;
|
||||
}
|
||||
|
||||
auto& parent = slices[static_cast<size_t>(idx)];
|
||||
if (samplePos <= parent.startSample || samplePos >= parent.endSample)
|
||||
return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
int oldEnd = parent.endSample;
|
||||
parent.endSample = samplePos;
|
||||
|
||||
slices.insert(slices.begin() + idx + 1, { samplePos, oldEnd, 0 });
|
||||
|
||||
assignMidiNotes();
|
||||
}
|
||||
|
||||
void SliceManager::removeSliceAt(int samplePos, int tolerance)
|
||||
{
|
||||
for (auto it = slices.begin(); it != slices.end(); ++it)
|
||||
{
|
||||
int dist = std::abs(it->startSample - samplePos);
|
||||
if (dist <= tolerance)
|
||||
{
|
||||
pushHistory();
|
||||
slices.erase(it);
|
||||
assignMidiNotes();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SliceManager::moveSlice(int fromSample, int toSample)
|
||||
{
|
||||
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;
|
||||
slices[i].endSample = juce::jmax(slices[i].endSample, slices[i].startSample + 1);
|
||||
sortSlices();
|
||||
assignMidiNotes();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SliceManager::trimStart(int sample)
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
||||
sample = juce::jlimit(0, sampleBuffer->getNumSamples() - 1, sample);
|
||||
if (sample <= 0) return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
const int numChannels = sampleBuffer->getNumChannels();
|
||||
const int oldNumSamples = sampleBuffer->getNumSamples();
|
||||
const int newNumSamples = oldNumSamples - sample;
|
||||
|
||||
juce::AudioBuffer<float> newBuffer(numChannels, newNumSamples);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
newBuffer.copyFrom(ch, 0, *sampleBuffer, ch, sample, newNumSamples);
|
||||
|
||||
sampleBuffer->setSize(numChannels, newNumSamples, false, false, false);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
sampleBuffer->copyFrom(ch, 0, newBuffer, ch, 0, newNumSamples);
|
||||
|
||||
for (auto& s : slices)
|
||||
{
|
||||
s.startSample -= sample;
|
||||
s.endSample -= sample;
|
||||
}
|
||||
|
||||
auto it = std::remove_if(slices.begin(), slices.end(),
|
||||
[](const Slice& s) { return s.endSample <= 0 || s.startSample < 0; });
|
||||
slices.erase(it, slices.end());
|
||||
|
||||
for (auto& s : slices)
|
||||
{
|
||||
s.startSample = juce::jmax(0, s.startSample);
|
||||
s.endSample = juce::jmin(newNumSamples, s.endSample);
|
||||
}
|
||||
|
||||
assignMidiNotes();
|
||||
}
|
||||
|
||||
void SliceManager::trimEnd(int sample)
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
||||
const int numSamples = sampleBuffer->getNumSamples();
|
||||
sample = juce::jlimit(0, numSamples, sample);
|
||||
if (sample >= numSamples) return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
const int numChannels = sampleBuffer->getNumChannels();
|
||||
|
||||
juce::AudioBuffer<float> newBuffer(numChannels, sample);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
newBuffer.copyFrom(ch, 0, *sampleBuffer, ch, 0, sample);
|
||||
|
||||
sampleBuffer->setSize(numChannels, sample, false, false, false);
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
sampleBuffer->copyFrom(ch, 0, newBuffer, ch, 0, sample);
|
||||
|
||||
auto it = std::remove_if(slices.begin(), slices.end(),
|
||||
[sample](const Slice& s) { return s.startSample >= sample; });
|
||||
slices.erase(it, slices.end());
|
||||
|
||||
for (auto& s : slices)
|
||||
s.endSample = juce::jmin(sample, s.endSample);
|
||||
|
||||
assignMidiNotes();
|
||||
}
|
||||
|
||||
int SliceManager::findSliceIndexForSample(int samplePos) const
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
{
|
||||
if (samplePos >= slices[static_cast<size_t>(i)].startSample && samplePos < slices[static_cast<size_t>(i)].endSample)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void SliceManager::sortSlices()
|
||||
{
|
||||
std::sort(slices.begin(), slices.end(),
|
||||
[](const Slice& a, const Slice& b) { return a.startSample < b.startSample; });
|
||||
}
|
||||
|
||||
void SliceManager::assignMidiNotes()
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
slices[static_cast<size_t>(i)].midiNote = i % 12;
|
||||
}
|
||||
|
||||
std::vector<float> SliceManager::computeEnvelope(const float* data, int numSamples)
|
||||
{
|
||||
const int windowSize = juce::jmax(1, static_cast<int>(currentSampleRate * 0.01)); // 10ms window
|
||||
const int numWindows = (numSamples + windowSize - 1) / windowSize;
|
||||
std::vector<float> envelope(static_cast<size_t>(numWindows), 0.0f);
|
||||
|
||||
for (int w = 0; w < numWindows; ++w)
|
||||
{
|
||||
int start = w * windowSize;
|
||||
int end = juce::jmin(start + windowSize, numSamples);
|
||||
float sum = 0.0f;
|
||||
for (int i = start; i < end; ++i)
|
||||
sum += data[i] * data[i];
|
||||
envelope[static_cast<size_t>(w)] = std::sqrt(sum / static_cast<float>(end - start));
|
||||
}
|
||||
return envelope;
|
||||
}
|
||||
|
||||
std::vector<float> SliceManager::lowpassFilter(const std::vector<float>& input, float cutoffHz)
|
||||
{
|
||||
std::vector<float> output(input.size());
|
||||
float rc = 1.0f / (2.0f * juce::MathConstants<float>::pi * cutoffHz);
|
||||
float dt = 1.0f / static_cast<float>(currentSampleRate);
|
||||
float alpha = dt / (rc + dt);
|
||||
|
||||
output[0] = input[0];
|
||||
for (size_t i = 1; i < input.size(); ++i)
|
||||
output[i] = output[i - 1] + alpha * (input[i] - output[i - 1]);
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<float> SliceManager::highpassFilter(const std::vector<float>& input, float cutoffHz)
|
||||
{
|
||||
auto lowpassed = lowpassFilter(input, cutoffHz);
|
||||
std::vector<float> output(input.size());
|
||||
for (size_t i = 0; i < input.size(); ++i)
|
||||
output[i] = input[i] - lowpassed[i];
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<float> SliceManager::detectOnsets(const std::vector<float>& envelope, float threshold)
|
||||
{
|
||||
std::vector<float> onsets;
|
||||
if (envelope.size() < 2) return onsets;
|
||||
|
||||
// Compute first derivative of envelope
|
||||
std::vector<float> diff(envelope.size());
|
||||
diff[0] = 0.0f;
|
||||
for (size_t i = 1; i < envelope.size(); ++i)
|
||||
diff[i] = envelope[i] - envelope[i - 1];
|
||||
|
||||
// Peak picking on derivative
|
||||
const int windowSize = juce::jmax(1, static_cast<int>(currentSampleRate * 0.01));
|
||||
|
||||
for (size_t i = 1; i < diff.size() - 1; ++i)
|
||||
{
|
||||
if (diff[i] > threshold && diff[i] > diff[i - 1] && diff[i] >= diff[i + 1])
|
||||
{
|
||||
// Check minimum distance from last onset
|
||||
float samplePos = static_cast<float>(i) * static_cast<float>(windowSize);
|
||||
bool tooClose = false;
|
||||
for (float o : onsets)
|
||||
{
|
||||
if (std::abs(samplePos - o) < currentSampleRate * 0.02) // 20ms minimum gap
|
||||
{
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!tooClose)
|
||||
onsets.push_back(samplePos);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (undoStack.empty()) return;
|
||||
redoStack.push_back(slices);
|
||||
slices = undoStack.back();
|
||||
undoStack.pop_back();
|
||||
}
|
||||
|
||||
void SliceManager::redo()
|
||||
{
|
||||
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(); }
|
||||
68
Source/SliceManager.h
Normal file
68
Source/SliceManager.h
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
class SliceManager
|
||||
{
|
||||
public:
|
||||
struct Slice
|
||||
{
|
||||
int startSample;
|
||||
int endSample;
|
||||
int midiNote; // halftone offset from C3 (0 = C3, 1 = C#3, etc.)
|
||||
};
|
||||
|
||||
SliceManager();
|
||||
|
||||
void setSampleBuffer(juce::AudioBuffer<float>* buffer, double sampleRate);
|
||||
void setSensitivity(float s);
|
||||
void setBassGain(float g);
|
||||
void setTrebleGain(float g);
|
||||
|
||||
void autoSlice();
|
||||
void clearSlices();
|
||||
void addSliceManual(int samplePos);
|
||||
void removeSliceAt(int samplePos, int tolerance = 5);
|
||||
void moveSlice(int fromSample, int toSample);
|
||||
|
||||
void trimStart(int sample);
|
||||
void trimEnd(int sample);
|
||||
|
||||
void undo();
|
||||
void redo();
|
||||
bool canUndo() const;
|
||||
bool canRedo() const;
|
||||
|
||||
const std::vector<Slice>& getSlices() const { return slices; }
|
||||
int getNumSlices() const { return static_cast<int>(slices.size()); }
|
||||
const Slice& getSlice(int index) const { return slices[static_cast<size_t>(index)]; }
|
||||
|
||||
int findSliceIndexForSample(int samplePos) const;
|
||||
|
||||
juce::AudioBuffer<float>* getMutableBuffer() { return sampleBuffer; }
|
||||
|
||||
private:
|
||||
void sortSlices();
|
||||
void assignMidiNotes();
|
||||
void pushHistory();
|
||||
void truncateHistory();
|
||||
|
||||
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;
|
||||
double currentSampleRate = 44100.0;
|
||||
|
||||
float sensitivity = 0.5f;
|
||||
float bassGain = 1.0f;
|
||||
float trebleGain = 1.0f;
|
||||
|
||||
std::vector<float> computeEnvelope(const float* channelData, int numSamples);
|
||||
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> detectOnsets(const std::vector<float>& envelope, float threshold);
|
||||
};
|
||||
692
Source/WaveformDisplay.cpp
Normal file
692
Source/WaveformDisplay.cpp
Normal file
|
|
@ -0,0 +1,692 @@
|
|||
#include "WaveformDisplay.h"
|
||||
|
||||
WaveformDisplay::WaveformDisplay(SliceManager& sm) : sliceManager(sm)
|
||||
{
|
||||
addAndMakeVisible(hScrollBar);
|
||||
hScrollBar.addListener(this);
|
||||
hScrollBar.setRangeLimits(0.0, 1.0);
|
||||
hScrollBar.setCurrentRange(0.0, 1.0);
|
||||
|
||||
auto setupScrollBtn = [this](juce::TextButton& btn) {
|
||||
addAndMakeVisible(btn);
|
||||
btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a4a));
|
||||
};
|
||||
setupScrollBtn(scrollLeftBtn);
|
||||
setupScrollBtn(scrollRightBtn);
|
||||
|
||||
startTimerHz(30);
|
||||
}
|
||||
|
||||
WaveformDisplay::~WaveformDisplay()
|
||||
{
|
||||
stopTimer();
|
||||
hScrollBar.removeListener(this);
|
||||
}
|
||||
|
||||
void WaveformDisplay::timerCallback() { repaint(); }
|
||||
|
||||
void WaveformDisplay::setSampleBuffer(const juce::AudioBuffer<float>* buffer, double sr)
|
||||
{
|
||||
sampleBuffer = buffer;
|
||||
sampleRate = sr;
|
||||
zoomFactor = 1.0f;
|
||||
scrollOffset = 0.0f;
|
||||
overviewDirty = true;
|
||||
rebuildOverview();
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::setPlaybackPosition(int sample) { playbackSample = sample; }
|
||||
void WaveformDisplay::setActiveSliceIndex(int index) { activeSliceIndex = index; }
|
||||
void WaveformDisplay::setSelectedSlice(int index) { selectedSliceIndex = index; }
|
||||
void WaveformDisplay::setOnEmptyAreaClick(std::function<void()> callback) { onEmptyAreaClick = std::move(callback); }
|
||||
void WaveformDisplay::setOnKeyClick(std::function<void(int)> callback) { onKeyClick = std::move(callback); }
|
||||
|
||||
void WaveformDisplay::zoomIn()
|
||||
{
|
||||
zoomFactor *= 1.5f;
|
||||
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::zoomOut()
|
||||
{
|
||||
zoomFactor /= 1.5f;
|
||||
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::zoomReset()
|
||||
{
|
||||
zoomFactor = 1.0f;
|
||||
scrollOffset = 0.0f;
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::syncScrollBar()
|
||||
{
|
||||
if (updatingScroll) return;
|
||||
updatingScroll = true;
|
||||
float visibleRatio = 1.0f / zoomFactor;
|
||||
hScrollBar.setCurrentRange(scrollOffset, visibleRatio);
|
||||
updatingScroll = false;
|
||||
}
|
||||
|
||||
void WaveformDisplay::scrollBarMoved(juce::ScrollBar*, double)
|
||||
{
|
||||
if (updatingScroll) return;
|
||||
updatingScroll = true;
|
||||
scrollOffset = static_cast<float>(hScrollBar.getCurrentRangeStart());
|
||||
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
|
||||
updatingScroll = false;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void WaveformDisplay::rebuildOverview()
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||
return;
|
||||
|
||||
const int numSamples = sampleBuffer->getNumSamples();
|
||||
const int numChannels = sampleBuffer->getNumChannels();
|
||||
const int blockSize = juce::jmax(1, numSamples / overviewResolution);
|
||||
|
||||
overviewBuffer.setSize(2, overviewResolution, false, false, false);
|
||||
for (int i = 0; i < overviewResolution; ++i)
|
||||
{
|
||||
int start = i * blockSize;
|
||||
int end = juce::jmin(start + blockSize, numSamples);
|
||||
float minVal = 0.0f, maxVal = 0.0f;
|
||||
|
||||
for (int s = start; s < end; ++s)
|
||||
{
|
||||
float avg = 0.0f;
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
avg += sampleBuffer->getSample(ch, s);
|
||||
avg /= static_cast<float>(juce::jmax(1, numChannels));
|
||||
minVal = juce::jmin(minVal, avg);
|
||||
maxVal = juce::jmax(maxVal, avg);
|
||||
}
|
||||
|
||||
overviewBuffer.setSample(0, i, minVal);
|
||||
overviewBuffer.setSample(1, i, maxVal);
|
||||
}
|
||||
|
||||
overviewDirty = false;
|
||||
}
|
||||
|
||||
juce::Rectangle<float> WaveformDisplay::getWaveformArea() const
|
||||
{
|
||||
auto b = getLocalBounds().toFloat();
|
||||
return b.withTrimmedLeft(keyWidth).withTrimmedBottom(scrollbarHeight);
|
||||
}
|
||||
|
||||
int WaveformDisplay::getVisibleStart() const
|
||||
{
|
||||
int total = getNumSamples();
|
||||
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
|
||||
return static_cast<int>(scrollOffset * static_cast<float>(juce::jmax(0, total - visible)));
|
||||
}
|
||||
|
||||
int WaveformDisplay::getVisibleEnd() const
|
||||
{
|
||||
int total = getNumSamples();
|
||||
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
|
||||
return juce::jmin(total, getVisibleStart() + visible);
|
||||
}
|
||||
|
||||
int WaveformDisplay::xToSample(int x) const
|
||||
{
|
||||
auto area = getWaveformArea();
|
||||
float ratio = static_cast<float>(x - static_cast<int>(area.getX())) / area.getWidth();
|
||||
ratio = juce::jlimit(0.0f, 1.0f, ratio);
|
||||
int start = getVisibleStart();
|
||||
int end = getVisibleEnd();
|
||||
return start + static_cast<int>(ratio * static_cast<float>(end - start));
|
||||
}
|
||||
|
||||
int WaveformDisplay::sampleToX(int sample) const
|
||||
{
|
||||
auto area = getWaveformArea();
|
||||
int start = getVisibleStart();
|
||||
int end = getVisibleEnd();
|
||||
int range = end - start;
|
||||
if (range <= 0) return static_cast<int>(area.getX());
|
||||
float ratio = static_cast<float>(sample - start) / static_cast<float>(range);
|
||||
return static_cast<int>(area.getX() + ratio * area.getWidth());
|
||||
}
|
||||
|
||||
int WaveformDisplay::keyRowToMidiNote(int row) const
|
||||
{
|
||||
const int numRows = 12;
|
||||
const int baseMidiNote = 48;
|
||||
return baseMidiNote + (numRows - 1 - row);
|
||||
}
|
||||
|
||||
int WaveformDisplay::midiNoteToSliceIndex(int note) const
|
||||
{
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
int numSlices = sliceManager.getNumSlices();
|
||||
for (int i = 0; i < numSlices; ++i)
|
||||
{
|
||||
if (slices[static_cast<size_t>(i)].midiNote == note)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int WaveformDisplay::findNearestSliceAtPixel(int pixelX, int tolerancePx) const
|
||||
{
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
int bestIdx = -1;
|
||||
int bestDist = tolerancePx + 1;
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
{
|
||||
int markerX = sampleToX(slices[static_cast<size_t>(i)].startSample);
|
||||
int dist = std::abs(pixelX - markerX);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
void WaveformDisplay::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.fillAll(juce::Colour(0xff1a1a2e));
|
||||
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||
{
|
||||
auto textArea = bounds.withTrimmedBottom(scrollbarHeight);
|
||||
g.setColour(juce::Colours::grey);
|
||||
g.setFont(14.0f);
|
||||
g.drawText("Drop or load an audio file", textArea, juce::Justification::centred);
|
||||
hScrollBar.setBounds(bounds.removeFromBottom(scrollbarHeight).toNearestInt());
|
||||
return;
|
||||
}
|
||||
|
||||
auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight);
|
||||
|
||||
// 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);
|
||||
drawSliceMarkers(g, waveArea);
|
||||
|
||||
// Scrollbar at bottom
|
||||
hScrollBar.setBounds(bounds.getBottomLeft().getX(),
|
||||
bounds.getBottom() - scrollbarHeight,
|
||||
bounds.getWidth(), scrollbarHeight);
|
||||
}
|
||||
|
||||
void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
|
||||
{
|
||||
g.setColour(juce::Colour(0xff222233));
|
||||
g.fillRect(bounds);
|
||||
|
||||
const int numRows = 12;
|
||||
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
|
||||
|
||||
for (int row = 0; row < numRows; ++row)
|
||||
{
|
||||
float y = bounds.getY() + static_cast<float>(row) * rowHeight;
|
||||
int noteInOctave = (numRows - 1 - row) % 12;
|
||||
bool isBlack = (noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 ||
|
||||
noteInOctave == 8 || noteInOctave == 10);
|
||||
|
||||
int midiNote = keyRowToMidiNote(row);
|
||||
|
||||
juce::Colour baseCol = isBlack ? juce::Colour(0xff333344) : juce::Colour(0xff555566);
|
||||
|
||||
// Highlight row if it has a slice and that slice is active
|
||||
int sliceIdx = midiNoteToSliceIndex(midiNote);
|
||||
if (sliceIdx >= 0 && sliceIdx == activeSliceIndex)
|
||||
{
|
||||
juce::Colour markerColours[] = {
|
||||
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
|
||||
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
|
||||
};
|
||||
baseCol = markerColours[sliceIdx % 6].withAlpha(0.25f);
|
||||
}
|
||||
|
||||
g.setColour(baseCol);
|
||||
g.fillRect(bounds.getX(), y, bounds.getWidth(), rowHeight);
|
||||
|
||||
g.setColour(juce::Colour(0xffaaaaaa));
|
||||
g.drawRect(bounds.getX(), y, bounds.getWidth(), rowHeight, 0.5f);
|
||||
|
||||
const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
|
||||
int noteIdx = midiNote % 12;
|
||||
int octave = midiNote / 12 - 1;
|
||||
juce::String name = juce::String(noteNames[noteIdx]) + juce::String(octave);
|
||||
|
||||
g.setColour(juce::Colours::white.withAlpha(0.6f));
|
||||
g.setFont(juce::Font(juce::FontOptions(8.0f)));
|
||||
g.drawText(name, static_cast<int>(bounds.getX() + 2.0f), static_cast<int>(y + 1.0f),
|
||||
static_cast<int>(bounds.getWidth() - 4.0f), static_cast<int>(rowHeight - 2.0f),
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::drawWaveform(juce::Graphics& g, juce::Rectangle<float> bounds)
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return;
|
||||
|
||||
int start = getVisibleStart();
|
||||
int end = getVisibleEnd();
|
||||
int total = getNumSamples();
|
||||
float w = bounds.getWidth();
|
||||
float h = bounds.getHeight();
|
||||
float midY = bounds.getY() + h * 0.5f;
|
||||
|
||||
// Highlight active slice region
|
||||
if (activeSliceIndex >= 0 && activeSliceIndex < sliceManager.getNumSlices())
|
||||
{
|
||||
const auto& slice = sliceManager.getSlice(activeSliceIndex);
|
||||
int sStart = juce::jmax(slice.startSample, start);
|
||||
int sEnd = juce::jmin(slice.endSample, end);
|
||||
if (sEnd > sStart)
|
||||
{
|
||||
float x1 = bounds.getX() + static_cast<float>(sStart - start) / static_cast<float>(end - start) * w;
|
||||
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(end - start) * w;
|
||||
|
||||
juce::Colour markerColours[] = {
|
||||
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
|
||||
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
|
||||
};
|
||||
g.setColour(markerColours[activeSliceIndex % 6].withAlpha(0.12f));
|
||||
g.fillRect(x1, bounds.getY(), x2 - x1, h);
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight selected slice region (subtler than active)
|
||||
if (selectedSliceIndex >= 0 && selectedSliceIndex < sliceManager.getNumSlices()
|
||||
&& selectedSliceIndex != activeSliceIndex)
|
||||
{
|
||||
const auto& slice = sliceManager.getSlice(selectedSliceIndex);
|
||||
int sStart = juce::jmax(slice.startSample, start);
|
||||
int sEnd = juce::jmin(slice.endSample, end);
|
||||
if (sEnd > sStart)
|
||||
{
|
||||
float x1 = bounds.getX() + static_cast<float>(sStart - start) / static_cast<float>(end - start) * w;
|
||||
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(end - start) * w;
|
||||
|
||||
juce::Colour markerColours[] = {
|
||||
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
|
||||
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
|
||||
};
|
||||
g.setColour(markerColours[selectedSliceIndex % 6].withAlpha(0.07f));
|
||||
g.fillRect(x1, bounds.getY(), x2 - x1, h);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw waveform directly from sample buffer for crisp rendering
|
||||
const int numChannels = sampleBuffer->getNumChannels();
|
||||
int visibleRange = end - start;
|
||||
if (visibleRange <= 0) return;
|
||||
|
||||
juce::Path filledPath;
|
||||
|
||||
for (int x = 0; x < static_cast<int>(w); ++x)
|
||||
{
|
||||
float ratio = static_cast<float>(x) / w;
|
||||
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
|
||||
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(visibleRange));
|
||||
s0 = juce::jlimit(0, total - 1, s0);
|
||||
s1 = juce::jlimit(0, total, s1);
|
||||
if (s1 <= s0) s1 = s0 + 1;
|
||||
|
||||
float maxVal = 0.0f;
|
||||
for (int s = s0; s < s1; ++s)
|
||||
{
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
{
|
||||
float val = std::abs(sampleBuffer->getSample(ch, s));
|
||||
if (val > maxVal) maxVal = val;
|
||||
}
|
||||
}
|
||||
|
||||
float yMin = midY - maxVal * h * 0.45f;
|
||||
if (x == 0)
|
||||
filledPath.startNewSubPath(bounds.getX(), yMin);
|
||||
filledPath.lineTo(bounds.getX() + static_cast<float>(x), yMin);
|
||||
}
|
||||
|
||||
for (int x = static_cast<int>(w) - 1; x >= 0; --x)
|
||||
{
|
||||
float ratio = static_cast<float>(x) / w;
|
||||
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
|
||||
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(visibleRange));
|
||||
s0 = juce::jlimit(0, total - 1, s0);
|
||||
s1 = juce::jlimit(0, total, s1);
|
||||
if (s1 <= s0) s1 = s0 + 1;
|
||||
|
||||
float minVal = 0.0f;
|
||||
for (int s = s0; s < s1; ++s)
|
||||
{
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
{
|
||||
float val = sampleBuffer->getSample(ch, s);
|
||||
if (val < minVal) minVal = val;
|
||||
}
|
||||
}
|
||||
|
||||
float yMax = midY - minVal * h * 0.45f;
|
||||
filledPath.lineTo(bounds.getX() + static_cast<float>(x), yMax);
|
||||
}
|
||||
|
||||
filledPath.closeSubPath();
|
||||
|
||||
juce::ColourGradient gradient(juce::Colour(0xff4488ff), bounds.getX(), bounds.getY(),
|
||||
juce::Colour(0xff2244aa), bounds.getX(), bounds.getBottom(), false);
|
||||
g.setGradientFill(gradient);
|
||||
g.fillPath(filledPath);
|
||||
|
||||
// Center line
|
||||
g.setColour(juce::Colour(0x40ffffff));
|
||||
g.drawHorizontalLine(static_cast<int>(midY), bounds.getX(), bounds.getRight());
|
||||
|
||||
// Playback position
|
||||
if (playbackSample >= start && playbackSample < end)
|
||||
{
|
||||
float px = bounds.getX() + static_cast<float>(playbackSample - start) / static_cast<float>(end - start) * w;
|
||||
g.setColour(juce::Colours::white);
|
||||
g.drawVerticalLine(static_cast<int>(px), bounds.getY(), bounds.getBottom());
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::drawSliceMarkers(juce::Graphics& g, juce::Rectangle<float> bounds)
|
||||
{
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
float w = bounds.getWidth();
|
||||
int start = getVisibleStart();
|
||||
int end = getVisibleEnd();
|
||||
|
||||
juce::Colour markerColours[] = {
|
||||
juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d),
|
||||
juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd)
|
||||
};
|
||||
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
{
|
||||
int sPos = slices[static_cast<size_t>(i)].startSample;
|
||||
if (sPos < start || sPos > end) continue;
|
||||
|
||||
float px = bounds.getX() + static_cast<float>(sPos - start) / static_cast<float>(end - start) * w;
|
||||
juce::Colour col = markerColours[i % 6];
|
||||
bool isActive = (i == activeSliceIndex);
|
||||
bool isSelected = (i == selectedSliceIndex);
|
||||
bool isHovered = (i == hoveredSliceIndex);
|
||||
|
||||
if (isHovered && !isActive)
|
||||
{
|
||||
g.setColour(col.brighter(0.6f));
|
||||
g.drawLine(px, bounds.getY(), px, bounds.getBottom(), 3.0f);
|
||||
}
|
||||
else if (isSelected && !isActive)
|
||||
{
|
||||
g.setColour(col.brighter(0.4f));
|
||||
float lineThickness = 2.0f;
|
||||
float y = bounds.getY();
|
||||
float bottom = bounds.getBottom();
|
||||
while (y < bottom)
|
||||
{
|
||||
float dashEnd = juce::jmin(y + 6.0f, bottom);
|
||||
g.drawLine(px, y, px, dashEnd, lineThickness);
|
||||
y += 10.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour(isActive ? col : col.withAlpha(0.7f));
|
||||
float lineThickness = isActive ? 2.0f : 1.0f;
|
||||
g.drawLine(px, bounds.getY(), px, bounds.getBottom(), lineThickness);
|
||||
}
|
||||
|
||||
// Label
|
||||
g.setColour(col);
|
||||
g.setFont(isActive ? juce::Font(juce::FontOptions(11.0f, juce::Font::bold)) : juce::Font(juce::FontOptions(10.0f)));
|
||||
g.drawText(juce::String(i + 1), static_cast<int>(px + 2.0f), static_cast<int>(bounds.getY()),
|
||||
20, 14, juce::Justification::centredLeft);
|
||||
|
||||
// Triangle at top
|
||||
float triSize = (isActive || isSelected) ? 5.0f : 4.0f;
|
||||
juce::Colour triCol = isSelected ? col.brighter(0.3f) : col;
|
||||
g.setColour(isActive ? col : triCol);
|
||||
juce::Path tri;
|
||||
tri.addTriangle(px - triSize, bounds.getY(), px + triSize, bounds.getY(), px, bounds.getY() + triSize * 2.0f);
|
||||
g.fillPath(tri);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::drawRuler(juce::Graphics& g, juce::Rectangle<float> bounds)
|
||||
{
|
||||
g.setColour(juce::Colour(0xff2a2a4a));
|
||||
g.fillRect(bounds);
|
||||
|
||||
// Key area in ruler too
|
||||
g.setColour(juce::Colour(0xff222233));
|
||||
g.fillRect(bounds.withWidth(keyWidth));
|
||||
|
||||
if (!sampleBuffer || sampleRate <= 0) return;
|
||||
|
||||
int start = getVisibleStart();
|
||||
int end = getVisibleEnd();
|
||||
float w = bounds.getWidth() - keyWidth;
|
||||
float offsetX = keyWidth;
|
||||
|
||||
g.setColour(juce::Colour(0xff888888));
|
||||
g.setFont(9.0f);
|
||||
|
||||
double visibleSeconds = static_cast<double>(end - start) / sampleRate;
|
||||
double tickInterval = 0.1;
|
||||
if (visibleSeconds > 5.0) tickInterval = 1.0;
|
||||
else if (visibleSeconds > 2.0) tickInterval = 0.5;
|
||||
else if (visibleSeconds > 0.5) tickInterval = 0.1;
|
||||
else tickInterval = 0.01;
|
||||
|
||||
double startSec = static_cast<double>(start) / sampleRate;
|
||||
double firstTick = std::ceil(startSec / tickInterval) * tickInterval;
|
||||
|
||||
for (double t = firstTick; t < static_cast<double>(end) / sampleRate; t += tickInterval)
|
||||
{
|
||||
int samp = static_cast<int>(t * sampleRate);
|
||||
float px = bounds.getX() + offsetX + static_cast<float>(samp - start) / static_cast<float>(end - start) * w;
|
||||
|
||||
g.drawVerticalLine(static_cast<int>(px), bounds.getY() + 12.0f, bounds.getBottom());
|
||||
|
||||
juce::String label = juce::String(t, 2) + "s";
|
||||
g.drawText(label, static_cast<int>(px + 2.0f), static_cast<int>(bounds.getY()),
|
||||
40, 12, juce::Justification::centredLeft);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
auto scrollBarRow = bounds.removeFromBottom(static_cast<int>(scrollbarHeight));
|
||||
scrollLeftBtn.setBounds(scrollBarRow.getX(), scrollBarRow.getY(), scrollBtnWidth, scrollBarRow.getHeight());
|
||||
scrollRightBtn.setBounds(scrollBarRow.getRight() - scrollBtnWidth, scrollBarRow.getY(), scrollBtnWidth, scrollBarRow.getHeight());
|
||||
hScrollBar.setBounds(scrollBarRow.getX() + scrollBtnWidth, scrollBarRow.getY(),
|
||||
scrollBarRow.getWidth() - scrollBtnWidth * 2, scrollBarRow.getHeight());
|
||||
}
|
||||
|
||||
void WaveformDisplay::mouseDown(const juce::MouseEvent& e)
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||
{
|
||||
if (onEmptyAreaClick)
|
||||
onEmptyAreaClick();
|
||||
return;
|
||||
}
|
||||
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight);
|
||||
|
||||
// Check if click is in the key area
|
||||
if (e.getPosition().getX() < static_cast<int>(keyArea.getWidth()))
|
||||
{
|
||||
float rowHeight = keyArea.getHeight() / 12.0f;
|
||||
int row = static_cast<int>((e.getPosition().getY() - keyArea.getY()) / rowHeight);
|
||||
row = juce::jlimit(0, 11, row);
|
||||
int midiNote = keyRowToMidiNote(row);
|
||||
|
||||
if (onKeyClick)
|
||||
onKeyClick(midiNote);
|
||||
return;
|
||||
}
|
||||
|
||||
// Right click removes slice
|
||||
if (e.mods.isRightButtonDown())
|
||||
{
|
||||
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
|
||||
auto area = getWaveformArea();
|
||||
int range = getVisibleEnd() - getVisibleStart();
|
||||
int tolerance = (range > 0) ? juce::jmax(1, static_cast<int>(10.0f * static_cast<float>(range) / area.getWidth())) : 1;
|
||||
sliceManager.removeSliceAt(sample, tolerance);
|
||||
return;
|
||||
}
|
||||
|
||||
// Left click on waveform area
|
||||
if (e.mods.isLeftButtonDown())
|
||||
{
|
||||
int px = static_cast<int>(e.getPosition().getX());
|
||||
int sample = xToSample(px);
|
||||
auto area = getWaveformArea();
|
||||
int range = getVisibleEnd() - getVisibleStart();
|
||||
int tolerancePx = 10;
|
||||
int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast<int>(static_cast<float>(tolerancePx) * static_cast<float>(range) / area.getWidth())) : 1;
|
||||
|
||||
// Check if clicking near an existing marker — drag it
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
int nearIdx = -1;
|
||||
int nearDist = toleranceSamples + 1;
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
{
|
||||
int dist = std::abs(slices[static_cast<size_t>(i)].startSample - sample);
|
||||
if (dist < nearDist)
|
||||
{
|
||||
nearDist = dist;
|
||||
nearIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearIdx >= 0)
|
||||
{
|
||||
draggingSliceIndex = nearIdx;
|
||||
}
|
||||
else
|
||||
{
|
||||
sliceManager.addSliceManual(sample);
|
||||
|
||||
// Enter drag mode for the newly placed marker
|
||||
const auto& slices2 = sliceManager.getSlices();
|
||||
int bestIdx = -1;
|
||||
int bestDist = std::numeric_limits<int>::max();
|
||||
for (int i = 0; i < static_cast<int>(slices2.size()); ++i)
|
||||
{
|
||||
int dist = std::abs(slices2[static_cast<size_t>(i)].startSample - sample);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
draggingSliceIndex = bestIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::mouseDrag(const juce::MouseEvent& e)
|
||||
{
|
||||
if (draggingSliceIndex >= 0)
|
||||
{
|
||||
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
if (draggingSliceIndex < static_cast<int>(slices.size()))
|
||||
sliceManager.moveSlice(slices[static_cast<size_t>(draggingSliceIndex)].startSample, sample);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformDisplay::mouseUp(const juce::MouseEvent&) { draggingSliceIndex = -1; }
|
||||
|
||||
void WaveformDisplay::mouseMove(const juce::MouseEvent& e)
|
||||
{
|
||||
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
||||
{
|
||||
hoveredSliceIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
auto area = getWaveformArea();
|
||||
if (e.getPosition().getX() < static_cast<int>(area.getX()) || e.getPosition().getX() > static_cast<int>(area.getRight()))
|
||||
{
|
||||
hoveredSliceIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
int range = getVisibleEnd() - getVisibleStart();
|
||||
int tolerancePx = 10;
|
||||
int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast<int>(static_cast<float>(tolerancePx) * static_cast<float>(range) / area.getWidth())) : 1;
|
||||
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
|
||||
|
||||
const auto& slices = sliceManager.getSlices();
|
||||
int bestIdx = -1;
|
||||
int bestDist = toleranceSamples + 1;
|
||||
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
||||
{
|
||||
int dist = std::abs(slices[static_cast<size_t>(i)].startSample - sample);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
hoveredSliceIndex = bestIdx;
|
||||
}
|
||||
|
||||
void WaveformDisplay::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
|
||||
{
|
||||
auto area = getWaveformArea();
|
||||
|
||||
// Handle horizontal scrolling (trackpad 2-finger horizontal drag)
|
||||
if (std::abs(wheel.deltaX) > 0.001f)
|
||||
{
|
||||
int total = getNumSamples();
|
||||
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
|
||||
float maxOffset = static_cast<float>(juce::jmax(1, total - visible));
|
||||
scrollOffset -= wheel.deltaX * 0.02f / zoomFactor;
|
||||
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle zoom via vertical scroll
|
||||
float oldZoom = zoomFactor;
|
||||
zoomFactor *= (1.0f + wheel.deltaY * 0.3f);
|
||||
zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor);
|
||||
|
||||
float mouseXRatio = (static_cast<float>(e.getPosition().getX()) - area.getX()) / area.getWidth();
|
||||
mouseXRatio = juce::jlimit(0.0f, 1.0f, mouseXRatio);
|
||||
int oldVisible = static_cast<int>(static_cast<float>(getNumSamples()) / oldZoom);
|
||||
int newVisible = static_cast<int>(static_cast<float>(getNumSamples()) / zoomFactor);
|
||||
float mouseSample = scrollOffset * static_cast<float>(juce::jmax(0, getNumSamples() - oldVisible)) + mouseXRatio * static_cast<float>(oldVisible);
|
||||
scrollOffset = (mouseSample - mouseXRatio * static_cast<float>(newVisible)) / static_cast<float>(juce::jmax(1, getNumSamples() - newVisible));
|
||||
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
|
||||
|
||||
syncScrollBar();
|
||||
repaint();
|
||||
}
|
||||
86
Source/WaveformDisplay.h
Normal file
86
Source/WaveformDisplay.h
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "SliceManager.h"
|
||||
#include <functional>
|
||||
|
||||
class WaveformDisplay : public juce::Component,
|
||||
public juce::Timer,
|
||||
private juce::ScrollBar::Listener
|
||||
{
|
||||
public:
|
||||
static constexpr float keyWidth = 50.0f;
|
||||
static constexpr float scrollbarHeight = 14.0f;
|
||||
|
||||
WaveformDisplay(SliceManager& sm);
|
||||
~WaveformDisplay() override;
|
||||
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
void mouseDown(const juce::MouseEvent& e) override;
|
||||
void mouseDrag(const juce::MouseEvent& e) override;
|
||||
void mouseUp(const juce::MouseEvent& e) override;
|
||||
void mouseMove(const juce::MouseEvent& e) override;
|
||||
void mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) override;
|
||||
|
||||
void setSampleBuffer(const juce::AudioBuffer<float>* buffer, double sampleRate);
|
||||
void setPlaybackPosition(int sample);
|
||||
void setActiveSliceIndex(int index);
|
||||
void setSelectedSlice(int index);
|
||||
void setOnEmptyAreaClick(std::function<void()> callback);
|
||||
void setOnKeyClick(std::function<void(int noteNumber)> callback);
|
||||
|
||||
void zoomIn();
|
||||
void zoomOut();
|
||||
void zoomReset();
|
||||
|
||||
int xToSample(int x) const;
|
||||
int sampleToX(int sample) const;
|
||||
float getZoomFactor() const { return zoomFactor; }
|
||||
|
||||
private:
|
||||
void drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds);
|
||||
void drawWaveform(juce::Graphics& g, juce::Rectangle<float> bounds);
|
||||
void drawSliceMarkers(juce::Graphics& g, juce::Rectangle<float> bounds);
|
||||
void drawRuler(juce::Graphics& g, juce::Rectangle<float> bounds);
|
||||
void scrollBarMoved(juce::ScrollBar* scrollBarThatHasMoved, double newRangeStart) override;
|
||||
void syncScrollBar();
|
||||
|
||||
SliceManager& sliceManager;
|
||||
const juce::AudioBuffer<float>* sampleBuffer = nullptr;
|
||||
double sampleRate = 44100.0;
|
||||
|
||||
float zoomFactor = 1.0f;
|
||||
float scrollOffset = 0.0f;
|
||||
int playbackSample = -1;
|
||||
int activeSliceIndex = -1;
|
||||
int selectedSliceIndex = -1;
|
||||
int draggingSliceIndex = -1;
|
||||
int hoveredSliceIndex = -1;
|
||||
|
||||
std::function<void()> onEmptyAreaClick;
|
||||
std::function<void(int)> onKeyClick;
|
||||
|
||||
juce::ScrollBar hScrollBar { false };
|
||||
juce::TextButton scrollLeftBtn { "<" };
|
||||
juce::TextButton scrollRightBtn { ">" };
|
||||
bool updatingScroll = false;
|
||||
static constexpr int scrollBtnWidth = 16;
|
||||
|
||||
juce::AudioBuffer<float> overviewBuffer;
|
||||
bool overviewDirty = true;
|
||||
static constexpr int overviewResolution = 2048;
|
||||
|
||||
void rebuildOverview();
|
||||
int getNumSamples() const { return sampleBuffer ? sampleBuffer->getNumSamples() : 0; }
|
||||
int getVisibleStart() const;
|
||||
int getVisibleEnd() const;
|
||||
juce::Rectangle<float> getWaveformArea() const;
|
||||
|
||||
int keyRowToMidiNote(int row) const;
|
||||
int midiNoteToSliceIndex(int note) const;
|
||||
int findNearestSliceAtPixel(int pixelX, int tolerancePx) const;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveformDisplay)
|
||||
};
|
||||
1
lib/JUCE
Submodule
1
lib/JUCE
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f72bad64d29715216226685810c5196bd0d79d77
|
||||
Loading…
Add table
Add a link
Reference in a new issue