Add octave offset dropdown, fix waveform sidebar, embed audio in plugin state

- Add octave offset combo (-3 to +3) in title bar for MIDI input transposition
- Fix waveform sidebar base note from C3 to C4
- Fix midiNoteToSliceIndex to compare pitch classes instead of raw MIDI notes
- Add colored slice indicator dots on waveform sidebar keys
- Embed audio data, file path, sample rate, and slices in plugin state for
  DAW project save/restore (falls back to file path if embedded data missing)
This commit is contained in:
Armin 2026-07-21 15:20:49 +02:00
commit 73c4095139
6 changed files with 130 additions and 12 deletions

View file

@ -35,6 +35,17 @@ MonoSlicerEditor::MonoSlicerEditor(MonoSlicerProcessor& p)
scaleComboBox.setTooltip("UI Scale"); scaleComboBox.setTooltip("UI Scale");
scaleAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "uiScale", scaleComboBox); scaleAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "uiScale", scaleComboBox);
addAndMakeVisible(octaveComboBox);
for (int i = -3; i <= 3; ++i)
octaveComboBox.addItem((i >= 0 ? "+" : "") + juce::String(i) + " oct", i + 4);
octaveComboBox.setSelectedId(4, juce::dontSendNotification);
octaveComboBox.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a4a));
octaveComboBox.setColour(juce::ComboBox::textColourId, juce::Colours::white);
octaveComboBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff444466));
octaveComboBox.setColour(juce::ComboBox::arrowColourId, juce::Colours::white);
octaveComboBox.setTooltip("Octave offset for MIDI input");
octaveAttachment = std::make_unique<ComboBoxAttachment>(processorRef.getAPVTS(), "octaveOffset", octaveComboBox);
// Bottom panel buttons // Bottom panel buttons
addAndMakeVisible(autoSliceButton); addAndMakeVisible(autoSliceButton);
autoSliceButton.addListener(this); autoSliceButton.addListener(this);
@ -251,6 +262,7 @@ void MonoSlicerEditor::resized()
placeBtn(zoomInButton, 28); placeBtn(zoomInButton, 28);
scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24); scaleComboBox.setBounds(titleBar.getRight() - 100, titleBar.getY() + 8, 90, 24);
octaveComboBox.setBounds(titleBar.getRight() - 176, titleBar.getY() + 8, 72, 24);
auto bottomPanel = bounds.removeFromBottom(200); auto bottomPanel = bounds.removeFromBottom(200);
auto bottomRow = bottomPanel.reduced(8); auto bottomRow = bottomPanel.reduced(8);
@ -329,6 +341,11 @@ void MonoSlicerEditor::resized()
void MonoSlicerEditor::timerCallback() void MonoSlicerEditor::timerCallback()
{ {
if (processorRef.stateRestored.exchange(false))
{
waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded());
}
waveformDisplay.setPlaybackPosition(processorRef.currentPlaybackSample.load()); waveformDisplay.setPlaybackPosition(processorRef.currentPlaybackSample.load());
waveformDisplay.setActiveSliceIndex(processorRef.currentActiveSlice.load()); waveformDisplay.setActiveSliceIndex(processorRef.currentActiveSlice.load());
waveformDisplay.setSelectedSlice(processorRef.selectedSlice.load()); waveformDisplay.setSelectedSlice(processorRef.selectedSlice.load());

View file

@ -41,6 +41,7 @@ private:
juce::TextButton zoomOutButton { "-" }; juce::TextButton zoomOutButton { "-" };
juce::TextButton zoomResetButton { "1:1" }; juce::TextButton zoomResetButton { "1:1" };
juce::ComboBox scaleComboBox; juce::ComboBox scaleComboBox;
juce::ComboBox octaveComboBox;
static constexpr int baseWidth = 1100; static constexpr int baseWidth = 1100;
static constexpr int baseHeight = 550; static constexpr int baseHeight = 550;
@ -77,6 +78,7 @@ private:
std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment; std::unique_ptr<ComboBoxAttachment> stretchBeatsAttachment;
std::unique_ptr<ComboBoxAttachment> keyShiftAttachment; std::unique_ptr<ComboBoxAttachment> keyShiftAttachment;
std::unique_ptr<ComboBoxAttachment> scaleAttachment; std::unique_ptr<ComboBoxAttachment> scaleAttachment;
std::unique_ptr<ComboBoxAttachment> octaveAttachment;
std::unique_ptr<SliderAttachment> bassAttachment, trebleAttachment, sensitivityAttachment; std::unique_ptr<SliderAttachment> bassAttachment, trebleAttachment, sensitivityAttachment;
std::unique_ptr<SliderAttachment> ampAttackAttachment, ampDecayAttachment, ampSustainAttachment, ampReleaseAttachment; std::unique_ptr<SliderAttachment> ampAttackAttachment, ampDecayAttachment, ampSustainAttachment, ampReleaseAttachment;

View file

@ -78,6 +78,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createP
layout.add(std::make_unique<juce::AudioParameterInt>( layout.add(std::make_unique<juce::AudioParameterInt>(
"keyShift", "Key Shift", -12, 12, 0)); "keyShift", "Key Shift", -12, 12, 0));
layout.add(std::make_unique<juce::AudioParameterInt>(
"octaveOffset", "Octave Offset", -3, 3, 0));
juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" }; juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" };
layout.add(std::make_unique<juce::AudioParameterChoice>( layout.add(std::make_unique<juce::AudioParameterChoice>(
"uiScale", "UI Scale", scaleChoices, 2)); "uiScale", "UI Scale", scaleChoices, 2));
@ -446,7 +449,8 @@ void MonoSlicerProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::M
void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity) void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity)
{ {
int sliceIdx = noteNumber - 60; int offset = static_cast<int>(apvts.getRawParameterValue("octaveOffset")->load()) * 12;
int sliceIdx = noteNumber - 60 + offset;
sliceIdx = juce::jlimit(0, juce::jmax(0, sliceManager.getNumSlices() - 1), sliceIdx); sliceIdx = juce::jlimit(0, juce::jmax(0, sliceManager.getNumSlices() - 1), sliceIdx);
if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f) if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f)
@ -555,6 +559,36 @@ juce::AudioProcessorEditor* MonoSlicerProcessor::createEditor()
void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData) void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData)
{ {
auto state = apvts.copyState(); auto state = apvts.copyState();
state.setProperty("filePath", currentFile.getFullPathName(), nullptr);
state.setProperty("sampleRate", loadedSampleRate, nullptr);
juce::Array<juce::var> sliceStarts;
juce::Array<juce::var> sliceEnds;
for (const auto& s : sliceManager.getSlices())
{
sliceStarts.add(s.startSample);
sliceEnds.add(s.endSample);
}
state.setProperty("sliceStarts", sliceStarts, nullptr);
state.setProperty("sliceEnds", sliceEnds, nullptr);
if (sampleBuffer.getNumSamples() > 0)
{
int numChannels = sampleBuffer.getNumChannels();
int numSamples = sampleBuffer.getNumSamples();
juce::MemoryOutputStream audioStream;
audioStream.writeInt(numChannels);
audioStream.writeInt(numSamples);
for (int ch = 0; ch < numChannels; ++ch)
audioStream.write(sampleBuffer.getReadPointer(ch),
static_cast<size_t>(numSamples) * sizeof(float));
audioStream.flush();
state.setProperty("audioData", juce::var(juce::MemoryBlock(audioStream.getData(), audioStream.getDataSize())), nullptr);
}
juce::MemoryOutputStream stream(destData, false); juce::MemoryOutputStream stream(destData, false);
state.writeToStream(stream); state.writeToStream(stream);
} }
@ -563,8 +597,60 @@ void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes)
{ {
auto stream = juce::MemoryInputStream(data, static_cast<size_t>(sizeInBytes), false); auto stream = juce::MemoryInputStream(data, static_cast<size_t>(sizeInBytes), false);
auto state = juce::ValueTree::readFromStream(stream); auto state = juce::ValueTree::readFromStream(stream);
if (state.isValid()) if (!state.isValid()) return;
apvts.replaceState(state);
apvts.replaceState(state);
loadedSampleRate = static_cast<double>(state.getProperty("sampleRate", 44100.0));
auto audioVar = state.getProperty("audioData", {});
auto* audioBlock = audioVar.getBinaryData();
if (audioBlock != nullptr && audioBlock->getSize() > 0)
{
const auto* buf = static_cast<const uint8_t*>(audioBlock->getData());
size_t offset = 0;
int numChannels = juce::ByteOrder::bigEndianInt(buf + offset); offset += 4;
int numSamples = juce::ByteOrder::bigEndianInt(buf + offset); offset += 4;
sampleBuffer.setSize(numChannels, numSamples, false, false, false);
for (int ch = 0; ch < numChannels; ++ch)
{
std::memcpy(sampleBuffer.getWritePointer(ch), buf + offset,
static_cast<size_t>(numSamples) * sizeof(float));
offset += static_cast<size_t>(numSamples) * sizeof(float);
}
currentFile = juce::File(state.getProperty("filePath", "").toString());
refreshFolderList();
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
}
else
{
juce::String path = state.getProperty("filePath", "").toString();
if (path.isNotEmpty())
{
juce::File file(path);
if (file.existsAsFile())
loadAudioFile(file);
}
}
auto sliceStarts = state.getProperty("sliceStarts", {});
if (sliceStarts.isArray())
{
auto* starts = sliceStarts.getArray();
int numSavedSlices = starts->size();
if (numSavedSlices > 0)
{
sliceManager.clearSlices();
for (int i = 1; i < numSavedSlices; ++i)
sliceManager.addSliceManual(static_cast<int>((*starts)[i]));
}
}
stateRestored.store(true);
} }
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()

View file

@ -51,6 +51,7 @@ public:
int getCurrentFileIndex() const { return currentFileIndex; } int getCurrentFileIndex() const { return currentFileIndex; }
int getNumFilesInFolder() const { return static_cast<int>(folderFiles.size()); } int getNumFilesInFolder() const { return static_cast<int>(folderFiles.size()); }
std::atomic<bool> stateRestored { false };
std::atomic<int> currentPlaybackSample { -1 }; std::atomic<int> currentPlaybackSample { -1 };
std::atomic<int> currentActiveSlice { -1 }; std::atomic<int> currentActiveSlice { -1 };
std::atomic<int> selectedSlice { -1 }; std::atomic<int> selectedSlice { -1 };

View file

@ -12,7 +12,7 @@ public:
{ {
int startSample; int startSample;
int endSample; int endSample;
int midiNote; // halftone offset from C3 (0 = C3, 1 = C#3, etc.) int midiNote; // pitch class (0 = C, 1 = C#, ..., 11 = B)
}; };
SliceManager(); SliceManager();

View file

@ -163,17 +163,18 @@ int WaveformDisplay::sampleToX(int sample) const
int WaveformDisplay::keyRowToMidiNote(int row) const int WaveformDisplay::keyRowToMidiNote(int row) const
{ {
const int numRows = 12; const int numRows = 12;
const int baseMidiNote = 48; const int baseMidiNote = 60;
return baseMidiNote + (numRows - 1 - row); return baseMidiNote + (numRows - 1 - row);
} }
int WaveformDisplay::midiNoteToSliceIndex(int note) const int WaveformDisplay::midiNoteToSliceIndex(int note) const
{ {
int pitchClass = note % 12;
const auto& slices = sliceManager.getSlices(); const auto& slices = sliceManager.getSlices();
int numSlices = sliceManager.getNumSlices(); int numSlices = sliceManager.getNumSlices();
for (int i = 0; i < numSlices; ++i) for (int i = 0; i < numSlices; ++i)
{ {
if (slices[static_cast<size_t>(i)].midiNote == note) if (slices[static_cast<size_t>(i)].midiNote == pitchClass)
return i; return i;
} }
return -1; return -1;
@ -241,6 +242,13 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
const int numRows = 12; const int numRows = 12;
float rowHeight = bounds.getHeight() / static_cast<float>(numRows); float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
juce::Colour markerColours[] = {
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)
};
for (int row = 0; row < numRows; ++row) for (int row = 0; row < numRows; ++row)
{ {
float y = bounds.getY() + static_cast<float>(row) * rowHeight; float y = bounds.getY() + static_cast<float>(row) * rowHeight;
@ -252,15 +260,10 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
juce::Colour baseCol = isBlack ? juce::Colour(0xff333344) : juce::Colour(0xff555566); 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); int sliceIdx = midiNoteToSliceIndex(midiNote);
if (sliceIdx >= 0 && sliceIdx == activeSliceIndex) if (sliceIdx >= 0 && sliceIdx == activeSliceIndex)
{ {
juce::Colour markerColours[] = { baseCol = markerColours[sliceIdx % 12].withAlpha(0.25f);
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.setColour(baseCol);
@ -269,6 +272,15 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle<float> bounds)
g.setColour(juce::Colour(0xffaaaaaa)); g.setColour(juce::Colour(0xffaaaaaa));
g.drawRect(bounds.getX(), y, bounds.getWidth(), rowHeight, 0.5f); g.drawRect(bounds.getX(), y, bounds.getWidth(), rowHeight, 0.5f);
if (sliceIdx >= 0)
{
float dotR = rowHeight * 0.2f;
float dotX = bounds.getX() + bounds.getWidth() * 0.5f;
float dotY = y + rowHeight * 0.5f;
g.setColour(markerColours[sliceIdx % 12].withAlpha(0.7f));
g.fillEllipse(dotX - dotR, dotY - dotR, dotR * 2.0f, dotR * 2.0f);
}
const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
int noteIdx = midiNote % 12; int noteIdx = midiNote % 12;
int octave = midiNote / 12 - 1; int octave = midiNote / 12 - 1;