mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 12:10:46 +02:00
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:
parent
8dc07d2568
commit
73c4095139
6 changed files with 130 additions and 12 deletions
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue