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

@ -78,6 +78,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createP
layout.add(std::make_unique<juce::AudioParameterInt>(
"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%" };
layout.add(std::make_unique<juce::AudioParameterChoice>(
"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)
{
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);
if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f)
@ -555,6 +559,36 @@ juce::AudioProcessorEditor* MonoSlicerProcessor::createEditor()
void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData)
{
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);
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 state = juce::ValueTree::readFromStream(stream);
if (state.isValid())
apvts.replaceState(state);
if (!state.isValid()) return;
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()