diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 5f74337..f93a8af 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -35,6 +35,17 @@ MonoSlicerEditor::MonoSlicerEditor(MonoSlicerProcessor& p) scaleComboBox.setTooltip("UI Scale"); scaleAttachment = std::make_unique(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(processorRef.getAPVTS(), "octaveOffset", octaveComboBox); + // Bottom panel buttons addAndMakeVisible(autoSliceButton); autoSliceButton.addListener(this); @@ -251,6 +262,7 @@ void MonoSlicerEditor::resized() placeBtn(zoomInButton, 28); 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 bottomRow = bottomPanel.reduced(8); @@ -329,6 +341,11 @@ void MonoSlicerEditor::resized() void MonoSlicerEditor::timerCallback() { + if (processorRef.stateRestored.exchange(false)) + { + waveformDisplay.setSampleBuffer(&processorRef.getSampleBuffer(), processorRef.getSampleRateLoaded()); + } + waveformDisplay.setPlaybackPosition(processorRef.currentPlaybackSample.load()); waveformDisplay.setActiveSliceIndex(processorRef.currentActiveSlice.load()); waveformDisplay.setSelectedSlice(processorRef.selectedSlice.load()); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index ca57d07..dae946f 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -41,6 +41,7 @@ private: juce::TextButton zoomOutButton { "-" }; juce::TextButton zoomResetButton { "1:1" }; juce::ComboBox scaleComboBox; + juce::ComboBox octaveComboBox; static constexpr int baseWidth = 1100; static constexpr int baseHeight = 550; @@ -77,6 +78,7 @@ private: std::unique_ptr stretchBeatsAttachment; std::unique_ptr keyShiftAttachment; std::unique_ptr scaleAttachment; + std::unique_ptr octaveAttachment; std::unique_ptr bassAttachment, trebleAttachment, sensitivityAttachment; std::unique_ptr ampAttackAttachment, ampDecayAttachment, ampSustainAttachment, ampReleaseAttachment; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 36a5495..ab21e51 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -78,6 +78,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createP layout.add(std::make_unique( "keyShift", "Key Shift", -12, 12, 0)); + layout.add(std::make_unique( + "octaveOffset", "Octave Offset", -3, 3, 0)); + juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" }; layout.add(std::make_unique( "uiScale", "UI Scale", scaleChoices, 2)); @@ -446,7 +449,8 @@ void MonoSlicerProcessor::processBlock(juce::AudioBuffer& buffer, juce::M void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity) { - int sliceIdx = noteNumber - 60; + int offset = static_cast(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 sliceStarts; + juce::Array 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(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(sizeInBytes), false); auto state = juce::ValueTree::readFromStream(stream); - if (state.isValid()) - apvts.replaceState(state); + if (!state.isValid()) return; + + apvts.replaceState(state); + + loadedSampleRate = static_cast(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(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(numSamples) * sizeof(float)); + offset += static_cast(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((*starts)[i])); + } + } + + stateRestored.store(true); } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 10433da..717e188 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -51,6 +51,7 @@ public: int getCurrentFileIndex() const { return currentFileIndex; } int getNumFilesInFolder() const { return static_cast(folderFiles.size()); } + std::atomic stateRestored { false }; std::atomic currentPlaybackSample { -1 }; std::atomic currentActiveSlice { -1 }; std::atomic selectedSlice { -1 }; diff --git a/Source/SliceManager.h b/Source/SliceManager.h index 8acd1c2..726be7f 100644 --- a/Source/SliceManager.h +++ b/Source/SliceManager.h @@ -12,7 +12,7 @@ public: { int startSample; 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(); diff --git a/Source/WaveformDisplay.cpp b/Source/WaveformDisplay.cpp index 11b1447..11ff48c 100644 --- a/Source/WaveformDisplay.cpp +++ b/Source/WaveformDisplay.cpp @@ -163,17 +163,18 @@ int WaveformDisplay::sampleToX(int sample) const int WaveformDisplay::keyRowToMidiNote(int row) const { const int numRows = 12; - const int baseMidiNote = 48; + const int baseMidiNote = 60; return baseMidiNote + (numRows - 1 - row); } int WaveformDisplay::midiNoteToSliceIndex(int note) const { + int pitchClass = note % 12; const auto& slices = sliceManager.getSlices(); int numSlices = sliceManager.getNumSlices(); for (int i = 0; i < numSlices; ++i) { - if (slices[static_cast(i)].midiNote == note) + if (slices[static_cast(i)].midiNote == pitchClass) return i; } return -1; @@ -241,6 +242,13 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle bounds) const int numRows = 12; float rowHeight = bounds.getHeight() / static_cast(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) { float y = bounds.getY() + static_cast(row) * rowHeight; @@ -252,15 +260,10 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle bounds) 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); + baseCol = markerColours[sliceIdx % 12].withAlpha(0.25f); } g.setColour(baseCol); @@ -269,6 +272,15 @@ void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle bounds) g.setColour(juce::Colour(0xffaaaaaa)); 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" }; int noteIdx = midiNote % 12; int octave = midiNote / 12 - 1;