From 8191716891ac2564ef9a366d03f25af004591e2c Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 00:51:18 -0500 Subject: [PATCH 01/10] Add file selector --- AudioFileLoader.cpp | 35 +++++++ AudioFileLoader.hpp | 10 ++ PluginEditor.cpp | 160 +++++++++++++++++++++++++++++ PluginEditor.hpp | 47 +++++++++ PluginProcessor.cpp | 245 ++++++++++++++++++++++++++++++++++++++++++++ PluginProcessor.hpp | 82 +++++++++++++++ 6 files changed, 579 insertions(+) create mode 100644 AudioFileLoader.cpp create mode 100644 AudioFileLoader.hpp create mode 100644 PluginEditor.cpp create mode 100644 PluginEditor.hpp create mode 100644 PluginProcessor.cpp create mode 100644 PluginProcessor.hpp diff --git a/AudioFileLoader.cpp b/AudioFileLoader.cpp new file mode 100644 index 0000000..0c21b5e --- /dev/null +++ b/AudioFileLoader.cpp @@ -0,0 +1,35 @@ +#include "AudioFileLoader.hpp" + +std::vector> +AudioFileLoader::loadAudioFile (const juce::File& file) +{ + juce::AudioFormatManager formatManager; + formatManager.registerBasicFormats(); + + std::unique_ptr reader ( + formatManager.createReaderFor (file) + ); + + if (! reader) + return {}; // failed to read + + const int numChannels = reader->numChannels; + const int64 numSamples = reader->lengthInSamples; + + juce::AudioBuffer buffer ((int) numChannels, (int) numSamples); + reader->read (&buffer, 0, (int) numSamples, 0, true, true); + + std::vector> result; + result.resize (numChannels); + + for (int ch = 0; ch < numChannels; ++ch) + { + result[ch].resize (numSamples); + const float* data = buffer.getReadPointer (ch); + + for (int i = 0; i < numSamples; ++i) + result[ch][i] = static_cast (data[i]); + } + + return result; +} diff --git a/AudioFileLoader.hpp b/AudioFileLoader.hpp new file mode 100644 index 0000000..8662ab8 --- /dev/null +++ b/AudioFileLoader.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +class AudioFileLoader +{ +public: + static std::vector> + loadAudioFile (const juce::File& file); +}; diff --git a/PluginEditor.cpp b/PluginEditor.cpp new file mode 100644 index 0000000..c2ad94a --- /dev/null +++ b/PluginEditor.cpp @@ -0,0 +1,160 @@ +#include "PluginEditor.hpp" +#include "AudioFileLoader.hpp" + +AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( + AdaptiveEchoAudioProcessor &p) + : AudioProcessorEditor(&p), processor(p), + midiKeyboard(processor.getMidiKeyboardState(), + juce::MidiKeyboardComponent::horizontalKeyboard) { + setResizable(true, true); + setSize(500, 220); + + // Volume slider + volumeSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + volumeSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); + volumeSlider.setRange(0.0, 1.0, 0.0); + addAndMakeVisible(volumeSlider); + + volumeLabel.setText("Volume", juce::dontSendNotification); + volumeLabel.setJustificationType(juce::Justification::centred); + addAndMakeVisible(volumeLabel); + + // ADSR + attackSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + attackSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); + attackSlider.setRange(0.01, 5.0, 0.0); + + attackLabel.setJustificationType(juce::Justification::centred); + attackLabel.setText("attack", juce::dontSendNotification); + + addAndMakeVisible(attackSlider); + addAndMakeVisible(attackLabel); + + decaySlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + decaySlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); + decaySlider.setRange(0.01, 5.0, 0.0); + + decayLabel.setJustificationType(juce::Justification::centred); + decayLabel.setText("decay", juce::dontSendNotification); + + addAndMakeVisible(decaySlider); + addAndMakeVisible(decayLabel); + + sustainSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + sustainSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); + sustainSlider.setRange(0.0, 1.0, 0.0); + + sustainLabel.setJustificationType(juce::Justification::centred); + sustainLabel.setText("sustain", juce::dontSendNotification); + + addAndMakeVisible(sustainSlider); + addAndMakeVisible(sustainLabel); + + releaseSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + releaseSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); + releaseSlider.setRange(0.01, 5.0, 0.0); + + releaseLabel.setJustificationType(juce::Justification::centred); + releaseLabel.setText("release", juce::dontSendNotification); + + addAndMakeVisible(releaseSlider); + addAndMakeVisible(releaseLabel); + + volumeAttachment = std::make_unique( + processor.apvts, "volume", volumeSlider); + attackAttachment = std::make_unique( + processor.apvts, "attack", attackSlider); + decayAttachment = std::make_unique(processor.apvts, + "decay", decaySlider); + sustainAttachment = std::make_unique( + processor.apvts, "sustain", sustainSlider); + releaseAttachment = std::make_unique( + processor.apvts, "release", releaseSlider); + + addAndMakeVisible (openFileButton); + + openFileButton.onClick = [this] + { + auto chooser = std::make_shared( + "Select an audio file", + juce::File{}, + "*.wav;*.aiff;*.mp3" + ); + + chooser->launchAsync ( + juce::FileBrowserComponent::openMode + | juce::FileBrowserComponent::canSelectFiles, + [this, chooser] (const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (! file.existsAsFile()) + return; + processor.loadFile (file); + repaint(); // forces paint() to update with new sample text + } + ); + }; + + addAndMakeVisible(midiKeyboard); + midiKeyboard.setAvailableRange(24, 108); +} + +void AdaptiveEchoAudioProcessorEditor::paint(juce::Graphics &g) { + g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); + g.setColour(juce::Colours::white); + g.setFont(16.0f); + g.drawFittedText("Adaptive Echo - Sine Generator Example (w/ MIDI)", + getLocalBounds().reduced(10, 6), + juce::Justification::centredTop, 1); + + // Only display first 10 samples + g.setFont(14.0f); + auto y = 50; + g.drawText("First 10 samples:", 10, y, 400, 20, juce::Justification::left); + y += 20; + + juce::String s; + int numToShow = std::min(10, (int)processor.loadedSamples.size()); + for (int i = 0; i < numToShow; ++i) + s += juce::String(processor.loadedSamples[i], 5) + " "; + g.drawText(s, 10, y, 400, 20, juce::Justification::left); + + // Debug: print a few samples to console only, NOT all + int numToPrint = std::min(1000, (int)processor.loadedSamples.size()); + for (int i = 0; i < numToPrint; ++i) + DBG("Sample " << i << ": " << processor.loadedSamples[i]); +} + +void AdaptiveEchoAudioProcessorEditor::resized() { + auto bounds = getLocalBounds().reduced(12); + midiKeyboard.setBounds(bounds.removeFromBottom(100).reduced(4)); + + // auto header = bounds.removeFromTop(34); + bounds.removeFromTop(34); + auto row = bounds.withSizeKeepingCentre(bounds.getWidth(), 120); + + int sliderWidth = bounds.getWidth() / 5; // one for volume + 4 ADSR + + auto controlArea = row.removeFromTop(120); + volumeSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); + volumeLabel.setBounds(volumeSlider.getX(), volumeSlider.getBottom(), + volumeSlider.getWidth(), 20); + + attackSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); + attackLabel.setBounds(attackSlider.getX(), attackSlider.getBottom(), + attackSlider.getWidth(), 20); + + decaySlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); + decayLabel.setBounds(decaySlider.getX(), decaySlider.getBottom(), + decaySlider.getWidth(), 20); + + sustainSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); + sustainLabel.setBounds(sustainSlider.getX(), sustainSlider.getBottom(), + sustainSlider.getWidth(), 20); + + releaseSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); + releaseLabel.setBounds(releaseSlider.getX(), releaseSlider.getBottom(), + releaseSlider.getWidth(), 20); + + openFileButton.setBounds (10, 10, 120, 24); +} diff --git a/PluginEditor.hpp b/PluginEditor.hpp new file mode 100644 index 0000000..6397a95 --- /dev/null +++ b/PluginEditor.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "PluginProcessor.hpp" +#include + +class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { + public: + explicit AdaptiveEchoAudioProcessorEditor(AdaptiveEchoAudioProcessor &); + ~AdaptiveEchoAudioProcessorEditor() override = default; + + void paint(juce::Graphics &g) override; + void resized() override; + + private: + AdaptiveEchoAudioProcessor &processor; + + juce::Slider volumeSlider; + juce::Label volumeLabel; + + // ADSR + juce::Slider attackSlider; + juce::Slider decaySlider; + juce::Slider sustainSlider; + juce::Slider releaseSlider; + + juce::Label attackLabel; + juce::Label decayLabel; + juce::Label sustainLabel; + juce::Label releaseLabel; + + juce::TextButton openFileButton { "Open File" }; + + juce::MidiKeyboardComponent midiKeyboard{ + processor.getMidiKeyboardState(), + juce::MidiKeyboardComponent::horizontalKeyboard}; + + using SliderAttachment = + juce::AudioProcessorValueTreeState::SliderAttachment; + std::unique_ptr volumeAttachment; + std::unique_ptr attackAttachment; + std::unique_ptr decayAttachment; + std::unique_ptr sustainAttachment; + std::unique_ptr releaseAttachment; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( + AdaptiveEchoAudioProcessorEditor) +}; diff --git a/PluginProcessor.cpp b/PluginProcessor.cpp new file mode 100644 index 0000000..e5053be --- /dev/null +++ b/PluginProcessor.cpp @@ -0,0 +1,245 @@ +#include "PluginProcessor.hpp" +#include "PluginEditor.hpp" + +AdaptiveEchoAudioProcessor::AdaptiveEchoAudioProcessor() + : AudioProcessor(BusesProperties().withOutput( + "Output", juce::AudioChannelSet::stereo(), true)), + apvts(*this, nullptr, "PARAMS", createParameterLayout()) { + volumeSmoothed.reset(currentSampleRate, 0.002); // 2ms smoothing + noteAmpSmoothed.reset(currentSampleRate, 0.002); +} + +juce::AudioProcessorValueTreeState::ParameterLayout +AdaptiveEchoAudioProcessor::createParameterLayout() { + std::vector> params; + + params.push_back(std::make_unique( + "volume", "Volume", + juce::NormalisableRange(0.0f, 1.0f, 0.0f, 1.0f), 0.5f)); + + // ADSR + juce::NormalisableRange ADSRrange = + juce::NormalisableRange(0.1f, 5.0f, 0.01f, 0.3f); + params.push_back(std::make_unique( + "attack", "Attack", ADSRrange, 0.5f)); + params.push_back(std::make_unique( + "decay", "Decay", ADSRrange, 0.5f)); + params.push_back(std::make_unique( + "sustain", "Sustain", + juce::NormalisableRange(0.0f, 1.0f, 0.0f, 1.0f), 0.5f)); + params.push_back(std::make_unique( + "release", "Release", ADSRrange, 0.5f)); + + return {params.begin(), params.end()}; +} + +void AdaptiveEchoAudioProcessor::prepareToPlay(double sampleRate, + int /*samplesPerBlock*/) { + currentSampleRate = sampleRate; + + phase = {0.0, 0.0}; + + volumeSmoothed.reset(currentSampleRate, 0.02); + noteAmpSmoothed.reset(currentSampleRate, 0.02); + + if (auto *volParam = apvts.getRawParameterValue("volume")) + volumeSmoothed.setCurrentAndTargetValue(volParam->load()); + else + volumeSmoothed.setCurrentAndTargetValue(0.5f); + + noteAmpSmoothed.setCurrentAndTargetValue(0.0f); // start silent + + // ADSR parameters + a = d = r = 0.01f; + s = 1.0f; + ac = dc = rc = 1.0f; + + env = ADSREnvelope(a, d, s, r, ac, dc, rc, + static_cast(currentSampleRate)); + env_ptr = std::make_shared(env); + Note activeNote = Note(); +} + +void AdaptiveEchoAudioProcessor::releaseResources() {} + +#ifndef JucePlugin_PreferredChannelConfigurations +bool AdaptiveEchoAudioProcessor::isBusesLayoutSupported( + const BusesLayout &layouts) const { + // Only allow mono or stereo outputs + const auto &mainOut = layouts.getMainOutputChannelSet(); + return mainOut == juce::AudioChannelSet::mono() || + mainOut == juce::AudioChannelSet::stereo(); +} +#endif + +void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, + juce::MidiBuffer &midi) { + juce::ScopedNoDenormals noDenormals; + const int numSamples = buffer.getNumSamples(); + const int numChans = juce::jmin(2, buffer.getNumChannels()); + + // Update envelope params if changed + float new_a = a; + float new_d = d; + float new_s = s; + float new_r = r; + + if (auto *aParam = apvts.getRawParameterValue("attack")) + new_a = aParam->load(); + if (auto *dParam = apvts.getRawParameterValue("decay")) + new_d = dParam->load(); + if (auto *sParam = apvts.getRawParameterValue("sustain")) + new_s = sParam->load(); + if (auto *rParam = apvts.getRawParameterValue("release")) + new_r = rParam->load(); + + // Update ADSR parameters + if (new_a != a || new_d != d || new_s != s || new_r != r) { + a = new_a; + d = new_d; + s = new_s; + r = new_r; + env = ADSREnvelope(a, d, s, r, ac, dc, rc, + static_cast(currentSampleRate)); + env_ptr = std::make_shared(env); + activeNote.set_env(env_ptr); + } + + // Update global volume target + if (auto *volParam = apvts.getRawParameterValue("volume")) + volumeSmoothed.setTargetValue(volParam->load()); + + // Handle MIDI + midiState.processNextMidiBuffer(midi, 0, numSamples, true); + + for (auto metadata : midi) { + const auto msg = metadata.getMessage(); + if (msg.isNoteOn()) { + if (msg.getNoteNumber() != activeNote.num) + activeNote.num = msg.getNoteNumber(); + activeNote.reset(); + uint8_t vel = (uint8_t)msg.getVelocity(); + const double frequency = + juce::MidiMessage::getMidiNoteInHertz(activeNote.num); + phaseInc = juce::MathConstants::twoPi * frequency / + currentSampleRate; + noteAmpSmoothed.setTargetValue((float)vel / 127.0f); + } else if (msg.isNoteOff()) { + activeNote.start_release(); + } + } + + for (int ch = 0; ch < numChans; ++ch) { + float *out = buffer.getWritePointer(ch); + double ph = phase[(size_t)ch]; + + for (int n = 0; n < numSamples; ++n) { + if (!activeNote.is_expired()) { + float noteLevel = noteAmpSmoothed.getNextValue(); + float globalVol = volumeSmoothed.getNextValue(); + + float amp = noteLevel * globalVol; + out[n] = std::sin(ph) * amp; + + ph += phaseInc; + if (ph >= juce::MathConstants::twoPi) + ph -= juce::MathConstants::twoPi; + + std::cout << out[n] << std::endl; + } else { + out[n] = 0.0f; + (void)volumeSmoothed.getNextValue(); + (void)noteAmpSmoothed.getNextValue(); + } + } + phase[(size_t)ch] = ph; + } + + activeNote.applyEnvelopeToBuffer(buffer, 0, numSamples); + + // Clear any extra channels (e.g., if host created more) + for (int ch = numChans; ch < buffer.getNumChannels(); ++ch) + buffer.clear(ch, 0, numSamples); +} + +void AdaptiveEchoAudioProcessor::getStateInformation( + juce::MemoryBlock &destData) { + if (auto state = apvts.copyState(); state.isValid()) { + juce::MemoryOutputStream mos(destData, true); + state.writeToStream(mos); + } +} + +void AdaptiveEchoAudioProcessor::setStateInformation(const void *data, + int sizeInBytes) { + juce::ValueTree tree = + juce::ValueTree::readFromData(data, (size_t)sizeInBytes); + if (tree.isValid()) + apvts.replaceState(tree); + + // Nudge smoothed value to loaded state + if (auto *volParam = apvts.getRawParameterValue("volume")) + volumeSmoothed.setCurrentAndTargetValue(volParam->load()); +} + +juce::AudioProcessorEditor *AdaptiveEchoAudioProcessor::createEditor() { + return new AdaptiveEchoAudioProcessorEditor(*this); +} + +// This factory must be present in the TU with the processor class. +juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter() { + return new AdaptiveEchoAudioProcessor(); +} + +void AdaptiveEchoAudioProcessor::loadFile(const juce::File& f) +{ + juce::AudioFormatManager formatManager; + formatManager.registerBasicFormats(); + + std::unique_ptr reader(formatManager.createReaderFor(f)); + if (!reader) + return; + + auto tempBuffer = std::make_shared>(reader->numChannels, (int)reader->lengthInSamples); + reader->read(tempBuffer.get(), 0, (int)reader->lengthInSamples, 0, true, true); + audioBuffer = tempBuffer; + + // Populate loadedSamples for GUI display + loadedSamples.clear(); + if (audioBuffer) + { + auto& buf = *audioBuffer; + for (int ch = 0; ch < buf.getNumChannels(); ++ch) + { + auto* ptr = buf.getReadPointer(ch); + loadedSamples.insert(loadedSamples.end(), ptr, ptr + buf.getNumSamples()); + } + } + + DBG(juce::String::formatted("Loaded %d samples from %d channels", + (int)reader->lengthInSamples, + reader->numChannels)); +} + +void AdaptiveEchoAudioProcessor::getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) +{ + if (!audioBuffer) + { + bufferToFill.clearActiveBufferRegion(); + return; + } + + const auto& buf = *audioBuffer; // Dereference shared_ptr + + int numChannels = std::min(bufferToFill.buffer->getNumChannels(), buf.getNumChannels()); + int numSamples = std::min(bufferToFill.buffer->getNumSamples(), buf.getNumSamples()); + + for (int ch = 0; ch < numChannels; ++ch) + { + auto* dst = bufferToFill.buffer->getWritePointer(ch); + const float* src = buf.getReadPointer(ch); + + for (int i = 0; i < numSamples; ++i) + dst[i] = src[i]; + } +} diff --git a/PluginProcessor.hpp b/PluginProcessor.hpp new file mode 100644 index 0000000..1882bca --- /dev/null +++ b/PluginProcessor.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include + +#include "Note.hpp" +#include "PluginEnvelope.hpp" +#include + +class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { + public: + AdaptiveEchoAudioProcessor(); + ~AdaptiveEchoAudioProcessor() override = default; + + //============================================================================== + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override; +#ifndef JucePlugin_PreferredChannelConfigurations + bool isBusesLayoutSupported(const BusesLayout &layouts) const override; +#endif + void processBlock(juce::AudioBuffer &, 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 false; } + 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; + + // Parameter/state + static juce::AudioProcessorValueTreeState::ParameterLayout + createParameterLayout(); + juce::AudioProcessorValueTreeState apvts; + + // Expose internal MidiKeyboardState + juce::MidiKeyboardState &getMidiKeyboardState() noexcept { + return midiState; + } + + void loadFile(const juce::File& f); + std::vector loadedSamples; + // audio callback + void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill); + + private: + // Simple sine generator state per channel + std::array phase{0.0, 0.0}; // support up to stereo + double phaseInc = 0.0; // radians per sample + double currentSampleRate = 48000.0; + Note activeNote; + ADSREnvelope env; + std::shared_ptr env_ptr; + std::shared_ptr> audioBuffer; + + // Smoothed volume to avoid zipper noise + juce::SmoothedValue + volumeSmoothed; + juce::SmoothedValue + noteAmpSmoothed; + juce::MidiKeyboardState midiState; + + float a, d, s, r, ac, dc, rc; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AdaptiveEchoAudioProcessor) +}; From fa34caa1839200d118394a46392859df889cce82 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 18:45:45 -0500 Subject: [PATCH 02/10] we currently only supports loading of wav file, but not mp3 files; add an alter window to show the user that mp3 as of now is not supported --- PluginProcessor.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/PluginProcessor.cpp b/PluginProcessor.cpp index e5053be..641b127 100644 --- a/PluginProcessor.cpp +++ b/PluginProcessor.cpp @@ -197,8 +197,22 @@ void AdaptiveEchoAudioProcessor::loadFile(const juce::File& f) formatManager.registerBasicFormats(); std::unique_ptr reader(formatManager.createReaderFor(f)); - if (!reader) + /* Adding a debug message here. Right now we are able to load wav files, + * but not able to load mp3 files. More specifically, our program will load the waveform from a wav file + * and print the first few samples; but for mp3 files, we do not print any samples; + * the reason for that is JUCE does not have an MP3 decoder available, at least on Linux systems. + * Therefore: reader is nullptr, loadFile() exits early, audioBuffer is never set, and loadedSamples remains empty + * */ + if (!reader) { + // DBG("Could not create reader for file: " + f.getFullPathName()); + juce::AlertWindow::showMessageBoxAsync( + juce::AlertWindow::WarningIcon, + "Unsupported Audio Format", + "This plugin supports WAV and AIFF files only.\n\n" + "MP3 decoding is not available on this system." + ); return; + } auto tempBuffer = std::make_shared>(reader->numChannels, (int)reader->lengthInSamples); reader->read(tempBuffer.get(), 0, (int)reader->lengthInSamples, 0, true, true); @@ -242,4 +256,4 @@ void AdaptiveEchoAudioProcessor::getNextAudioBlock(const juce::AudioSourceChanne for (int i = 0; i < numSamples; ++i) dst[i] = src[i]; } -} +} \ No newline at end of file From 3ee5e44d5eb147414f58e0d9532b2995dcf5f1f9 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 19:07:00 -0500 Subject: [PATCH 03/10] Sorry I earlier checked in them into the wrong folder; move them to src folder now --- PluginEditor.cpp | 160 ----------- PluginEditor.hpp | 47 ---- PluginProcessor.cpp | 259 ------------------ PluginProcessor.hpp | 82 ------ .../src/AudioFileLoader.cpp | 0 .../src/AudioFileLoader.hpp | 0 plugin/src/PluginEditor.cpp | 47 +++- plugin/src/PluginEditor.hpp | 4 +- plugin/src/PluginProcessor.cpp | 67 +++++ plugin/src/PluginProcessor.hpp | 7 + 10 files changed, 122 insertions(+), 551 deletions(-) delete mode 100644 PluginEditor.cpp delete mode 100644 PluginEditor.hpp delete mode 100644 PluginProcessor.cpp delete mode 100644 PluginProcessor.hpp rename AudioFileLoader.cpp => plugin/src/AudioFileLoader.cpp (100%) rename AudioFileLoader.hpp => plugin/src/AudioFileLoader.hpp (100%) diff --git a/PluginEditor.cpp b/PluginEditor.cpp deleted file mode 100644 index c2ad94a..0000000 --- a/PluginEditor.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#include "PluginEditor.hpp" -#include "AudioFileLoader.hpp" - -AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( - AdaptiveEchoAudioProcessor &p) - : AudioProcessorEditor(&p), processor(p), - midiKeyboard(processor.getMidiKeyboardState(), - juce::MidiKeyboardComponent::horizontalKeyboard) { - setResizable(true, true); - setSize(500, 220); - - // Volume slider - volumeSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - volumeSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); - volumeSlider.setRange(0.0, 1.0, 0.0); - addAndMakeVisible(volumeSlider); - - volumeLabel.setText("Volume", juce::dontSendNotification); - volumeLabel.setJustificationType(juce::Justification::centred); - addAndMakeVisible(volumeLabel); - - // ADSR - attackSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - attackSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); - attackSlider.setRange(0.01, 5.0, 0.0); - - attackLabel.setJustificationType(juce::Justification::centred); - attackLabel.setText("attack", juce::dontSendNotification); - - addAndMakeVisible(attackSlider); - addAndMakeVisible(attackLabel); - - decaySlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - decaySlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); - decaySlider.setRange(0.01, 5.0, 0.0); - - decayLabel.setJustificationType(juce::Justification::centred); - decayLabel.setText("decay", juce::dontSendNotification); - - addAndMakeVisible(decaySlider); - addAndMakeVisible(decayLabel); - - sustainSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - sustainSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); - sustainSlider.setRange(0.0, 1.0, 0.0); - - sustainLabel.setJustificationType(juce::Justification::centred); - sustainLabel.setText("sustain", juce::dontSendNotification); - - addAndMakeVisible(sustainSlider); - addAndMakeVisible(sustainLabel); - - releaseSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - releaseSlider.setTextBoxStyle(juce::Slider::TextBoxAbove, false, 60, 20); - releaseSlider.setRange(0.01, 5.0, 0.0); - - releaseLabel.setJustificationType(juce::Justification::centred); - releaseLabel.setText("release", juce::dontSendNotification); - - addAndMakeVisible(releaseSlider); - addAndMakeVisible(releaseLabel); - - volumeAttachment = std::make_unique( - processor.apvts, "volume", volumeSlider); - attackAttachment = std::make_unique( - processor.apvts, "attack", attackSlider); - decayAttachment = std::make_unique(processor.apvts, - "decay", decaySlider); - sustainAttachment = std::make_unique( - processor.apvts, "sustain", sustainSlider); - releaseAttachment = std::make_unique( - processor.apvts, "release", releaseSlider); - - addAndMakeVisible (openFileButton); - - openFileButton.onClick = [this] - { - auto chooser = std::make_shared( - "Select an audio file", - juce::File{}, - "*.wav;*.aiff;*.mp3" - ); - - chooser->launchAsync ( - juce::FileBrowserComponent::openMode - | juce::FileBrowserComponent::canSelectFiles, - [this, chooser] (const juce::FileChooser& fc) - { - auto file = fc.getResult(); - if (! file.existsAsFile()) - return; - processor.loadFile (file); - repaint(); // forces paint() to update with new sample text - } - ); - }; - - addAndMakeVisible(midiKeyboard); - midiKeyboard.setAvailableRange(24, 108); -} - -void AdaptiveEchoAudioProcessorEditor::paint(juce::Graphics &g) { - g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); - g.setColour(juce::Colours::white); - g.setFont(16.0f); - g.drawFittedText("Adaptive Echo - Sine Generator Example (w/ MIDI)", - getLocalBounds().reduced(10, 6), - juce::Justification::centredTop, 1); - - // Only display first 10 samples - g.setFont(14.0f); - auto y = 50; - g.drawText("First 10 samples:", 10, y, 400, 20, juce::Justification::left); - y += 20; - - juce::String s; - int numToShow = std::min(10, (int)processor.loadedSamples.size()); - for (int i = 0; i < numToShow; ++i) - s += juce::String(processor.loadedSamples[i], 5) + " "; - g.drawText(s, 10, y, 400, 20, juce::Justification::left); - - // Debug: print a few samples to console only, NOT all - int numToPrint = std::min(1000, (int)processor.loadedSamples.size()); - for (int i = 0; i < numToPrint; ++i) - DBG("Sample " << i << ": " << processor.loadedSamples[i]); -} - -void AdaptiveEchoAudioProcessorEditor::resized() { - auto bounds = getLocalBounds().reduced(12); - midiKeyboard.setBounds(bounds.removeFromBottom(100).reduced(4)); - - // auto header = bounds.removeFromTop(34); - bounds.removeFromTop(34); - auto row = bounds.withSizeKeepingCentre(bounds.getWidth(), 120); - - int sliderWidth = bounds.getWidth() / 5; // one for volume + 4 ADSR - - auto controlArea = row.removeFromTop(120); - volumeSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); - volumeLabel.setBounds(volumeSlider.getX(), volumeSlider.getBottom(), - volumeSlider.getWidth(), 20); - - attackSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); - attackLabel.setBounds(attackSlider.getX(), attackSlider.getBottom(), - attackSlider.getWidth(), 20); - - decaySlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); - decayLabel.setBounds(decaySlider.getX(), decaySlider.getBottom(), - decaySlider.getWidth(), 20); - - sustainSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); - sustainLabel.setBounds(sustainSlider.getX(), sustainSlider.getBottom(), - sustainSlider.getWidth(), 20); - - releaseSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); - releaseLabel.setBounds(releaseSlider.getX(), releaseSlider.getBottom(), - releaseSlider.getWidth(), 20); - - openFileButton.setBounds (10, 10, 120, 24); -} diff --git a/PluginEditor.hpp b/PluginEditor.hpp deleted file mode 100644 index 6397a95..0000000 --- a/PluginEditor.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "PluginProcessor.hpp" -#include - -class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { - public: - explicit AdaptiveEchoAudioProcessorEditor(AdaptiveEchoAudioProcessor &); - ~AdaptiveEchoAudioProcessorEditor() override = default; - - void paint(juce::Graphics &g) override; - void resized() override; - - private: - AdaptiveEchoAudioProcessor &processor; - - juce::Slider volumeSlider; - juce::Label volumeLabel; - - // ADSR - juce::Slider attackSlider; - juce::Slider decaySlider; - juce::Slider sustainSlider; - juce::Slider releaseSlider; - - juce::Label attackLabel; - juce::Label decayLabel; - juce::Label sustainLabel; - juce::Label releaseLabel; - - juce::TextButton openFileButton { "Open File" }; - - juce::MidiKeyboardComponent midiKeyboard{ - processor.getMidiKeyboardState(), - juce::MidiKeyboardComponent::horizontalKeyboard}; - - using SliderAttachment = - juce::AudioProcessorValueTreeState::SliderAttachment; - std::unique_ptr volumeAttachment; - std::unique_ptr attackAttachment; - std::unique_ptr decayAttachment; - std::unique_ptr sustainAttachment; - std::unique_ptr releaseAttachment; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( - AdaptiveEchoAudioProcessorEditor) -}; diff --git a/PluginProcessor.cpp b/PluginProcessor.cpp deleted file mode 100644 index 641b127..0000000 --- a/PluginProcessor.cpp +++ /dev/null @@ -1,259 +0,0 @@ -#include "PluginProcessor.hpp" -#include "PluginEditor.hpp" - -AdaptiveEchoAudioProcessor::AdaptiveEchoAudioProcessor() - : AudioProcessor(BusesProperties().withOutput( - "Output", juce::AudioChannelSet::stereo(), true)), - apvts(*this, nullptr, "PARAMS", createParameterLayout()) { - volumeSmoothed.reset(currentSampleRate, 0.002); // 2ms smoothing - noteAmpSmoothed.reset(currentSampleRate, 0.002); -} - -juce::AudioProcessorValueTreeState::ParameterLayout -AdaptiveEchoAudioProcessor::createParameterLayout() { - std::vector> params; - - params.push_back(std::make_unique( - "volume", "Volume", - juce::NormalisableRange(0.0f, 1.0f, 0.0f, 1.0f), 0.5f)); - - // ADSR - juce::NormalisableRange ADSRrange = - juce::NormalisableRange(0.1f, 5.0f, 0.01f, 0.3f); - params.push_back(std::make_unique( - "attack", "Attack", ADSRrange, 0.5f)); - params.push_back(std::make_unique( - "decay", "Decay", ADSRrange, 0.5f)); - params.push_back(std::make_unique( - "sustain", "Sustain", - juce::NormalisableRange(0.0f, 1.0f, 0.0f, 1.0f), 0.5f)); - params.push_back(std::make_unique( - "release", "Release", ADSRrange, 0.5f)); - - return {params.begin(), params.end()}; -} - -void AdaptiveEchoAudioProcessor::prepareToPlay(double sampleRate, - int /*samplesPerBlock*/) { - currentSampleRate = sampleRate; - - phase = {0.0, 0.0}; - - volumeSmoothed.reset(currentSampleRate, 0.02); - noteAmpSmoothed.reset(currentSampleRate, 0.02); - - if (auto *volParam = apvts.getRawParameterValue("volume")) - volumeSmoothed.setCurrentAndTargetValue(volParam->load()); - else - volumeSmoothed.setCurrentAndTargetValue(0.5f); - - noteAmpSmoothed.setCurrentAndTargetValue(0.0f); // start silent - - // ADSR parameters - a = d = r = 0.01f; - s = 1.0f; - ac = dc = rc = 1.0f; - - env = ADSREnvelope(a, d, s, r, ac, dc, rc, - static_cast(currentSampleRate)); - env_ptr = std::make_shared(env); - Note activeNote = Note(); -} - -void AdaptiveEchoAudioProcessor::releaseResources() {} - -#ifndef JucePlugin_PreferredChannelConfigurations -bool AdaptiveEchoAudioProcessor::isBusesLayoutSupported( - const BusesLayout &layouts) const { - // Only allow mono or stereo outputs - const auto &mainOut = layouts.getMainOutputChannelSet(); - return mainOut == juce::AudioChannelSet::mono() || - mainOut == juce::AudioChannelSet::stereo(); -} -#endif - -void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, - juce::MidiBuffer &midi) { - juce::ScopedNoDenormals noDenormals; - const int numSamples = buffer.getNumSamples(); - const int numChans = juce::jmin(2, buffer.getNumChannels()); - - // Update envelope params if changed - float new_a = a; - float new_d = d; - float new_s = s; - float new_r = r; - - if (auto *aParam = apvts.getRawParameterValue("attack")) - new_a = aParam->load(); - if (auto *dParam = apvts.getRawParameterValue("decay")) - new_d = dParam->load(); - if (auto *sParam = apvts.getRawParameterValue("sustain")) - new_s = sParam->load(); - if (auto *rParam = apvts.getRawParameterValue("release")) - new_r = rParam->load(); - - // Update ADSR parameters - if (new_a != a || new_d != d || new_s != s || new_r != r) { - a = new_a; - d = new_d; - s = new_s; - r = new_r; - env = ADSREnvelope(a, d, s, r, ac, dc, rc, - static_cast(currentSampleRate)); - env_ptr = std::make_shared(env); - activeNote.set_env(env_ptr); - } - - // Update global volume target - if (auto *volParam = apvts.getRawParameterValue("volume")) - volumeSmoothed.setTargetValue(volParam->load()); - - // Handle MIDI - midiState.processNextMidiBuffer(midi, 0, numSamples, true); - - for (auto metadata : midi) { - const auto msg = metadata.getMessage(); - if (msg.isNoteOn()) { - if (msg.getNoteNumber() != activeNote.num) - activeNote.num = msg.getNoteNumber(); - activeNote.reset(); - uint8_t vel = (uint8_t)msg.getVelocity(); - const double frequency = - juce::MidiMessage::getMidiNoteInHertz(activeNote.num); - phaseInc = juce::MathConstants::twoPi * frequency / - currentSampleRate; - noteAmpSmoothed.setTargetValue((float)vel / 127.0f); - } else if (msg.isNoteOff()) { - activeNote.start_release(); - } - } - - for (int ch = 0; ch < numChans; ++ch) { - float *out = buffer.getWritePointer(ch); - double ph = phase[(size_t)ch]; - - for (int n = 0; n < numSamples; ++n) { - if (!activeNote.is_expired()) { - float noteLevel = noteAmpSmoothed.getNextValue(); - float globalVol = volumeSmoothed.getNextValue(); - - float amp = noteLevel * globalVol; - out[n] = std::sin(ph) * amp; - - ph += phaseInc; - if (ph >= juce::MathConstants::twoPi) - ph -= juce::MathConstants::twoPi; - - std::cout << out[n] << std::endl; - } else { - out[n] = 0.0f; - (void)volumeSmoothed.getNextValue(); - (void)noteAmpSmoothed.getNextValue(); - } - } - phase[(size_t)ch] = ph; - } - - activeNote.applyEnvelopeToBuffer(buffer, 0, numSamples); - - // Clear any extra channels (e.g., if host created more) - for (int ch = numChans; ch < buffer.getNumChannels(); ++ch) - buffer.clear(ch, 0, numSamples); -} - -void AdaptiveEchoAudioProcessor::getStateInformation( - juce::MemoryBlock &destData) { - if (auto state = apvts.copyState(); state.isValid()) { - juce::MemoryOutputStream mos(destData, true); - state.writeToStream(mos); - } -} - -void AdaptiveEchoAudioProcessor::setStateInformation(const void *data, - int sizeInBytes) { - juce::ValueTree tree = - juce::ValueTree::readFromData(data, (size_t)sizeInBytes); - if (tree.isValid()) - apvts.replaceState(tree); - - // Nudge smoothed value to loaded state - if (auto *volParam = apvts.getRawParameterValue("volume")) - volumeSmoothed.setCurrentAndTargetValue(volParam->load()); -} - -juce::AudioProcessorEditor *AdaptiveEchoAudioProcessor::createEditor() { - return new AdaptiveEchoAudioProcessorEditor(*this); -} - -// This factory must be present in the TU with the processor class. -juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter() { - return new AdaptiveEchoAudioProcessor(); -} - -void AdaptiveEchoAudioProcessor::loadFile(const juce::File& f) -{ - juce::AudioFormatManager formatManager; - formatManager.registerBasicFormats(); - - std::unique_ptr reader(formatManager.createReaderFor(f)); - /* Adding a debug message here. Right now we are able to load wav files, - * but not able to load mp3 files. More specifically, our program will load the waveform from a wav file - * and print the first few samples; but for mp3 files, we do not print any samples; - * the reason for that is JUCE does not have an MP3 decoder available, at least on Linux systems. - * Therefore: reader is nullptr, loadFile() exits early, audioBuffer is never set, and loadedSamples remains empty - * */ - if (!reader) { - // DBG("Could not create reader for file: " + f.getFullPathName()); - juce::AlertWindow::showMessageBoxAsync( - juce::AlertWindow::WarningIcon, - "Unsupported Audio Format", - "This plugin supports WAV and AIFF files only.\n\n" - "MP3 decoding is not available on this system." - ); - return; - } - - auto tempBuffer = std::make_shared>(reader->numChannels, (int)reader->lengthInSamples); - reader->read(tempBuffer.get(), 0, (int)reader->lengthInSamples, 0, true, true); - audioBuffer = tempBuffer; - - // Populate loadedSamples for GUI display - loadedSamples.clear(); - if (audioBuffer) - { - auto& buf = *audioBuffer; - for (int ch = 0; ch < buf.getNumChannels(); ++ch) - { - auto* ptr = buf.getReadPointer(ch); - loadedSamples.insert(loadedSamples.end(), ptr, ptr + buf.getNumSamples()); - } - } - - DBG(juce::String::formatted("Loaded %d samples from %d channels", - (int)reader->lengthInSamples, - reader->numChannels)); -} - -void AdaptiveEchoAudioProcessor::getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) -{ - if (!audioBuffer) - { - bufferToFill.clearActiveBufferRegion(); - return; - } - - const auto& buf = *audioBuffer; // Dereference shared_ptr - - int numChannels = std::min(bufferToFill.buffer->getNumChannels(), buf.getNumChannels()); - int numSamples = std::min(bufferToFill.buffer->getNumSamples(), buf.getNumSamples()); - - for (int ch = 0; ch < numChannels; ++ch) - { - auto* dst = bufferToFill.buffer->getWritePointer(ch); - const float* src = buf.getReadPointer(ch); - - for (int i = 0; i < numSamples; ++i) - dst[i] = src[i]; - } -} \ No newline at end of file diff --git a/PluginProcessor.hpp b/PluginProcessor.hpp deleted file mode 100644 index 1882bca..0000000 --- a/PluginProcessor.hpp +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Note.hpp" -#include "PluginEnvelope.hpp" -#include - -class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { - public: - AdaptiveEchoAudioProcessor(); - ~AdaptiveEchoAudioProcessor() override = default; - - //============================================================================== - void prepareToPlay(double sampleRate, int samplesPerBlock) override; - void releaseResources() override; -#ifndef JucePlugin_PreferredChannelConfigurations - bool isBusesLayoutSupported(const BusesLayout &layouts) const override; -#endif - void processBlock(juce::AudioBuffer &, 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 false; } - 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; - - // Parameter/state - static juce::AudioProcessorValueTreeState::ParameterLayout - createParameterLayout(); - juce::AudioProcessorValueTreeState apvts; - - // Expose internal MidiKeyboardState - juce::MidiKeyboardState &getMidiKeyboardState() noexcept { - return midiState; - } - - void loadFile(const juce::File& f); - std::vector loadedSamples; - // audio callback - void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill); - - private: - // Simple sine generator state per channel - std::array phase{0.0, 0.0}; // support up to stereo - double phaseInc = 0.0; // radians per sample - double currentSampleRate = 48000.0; - Note activeNote; - ADSREnvelope env; - std::shared_ptr env_ptr; - std::shared_ptr> audioBuffer; - - // Smoothed volume to avoid zipper noise - juce::SmoothedValue - volumeSmoothed; - juce::SmoothedValue - noteAmpSmoothed; - juce::MidiKeyboardState midiState; - - float a, d, s, r, ac, dc, rc; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AdaptiveEchoAudioProcessor) -}; diff --git a/AudioFileLoader.cpp b/plugin/src/AudioFileLoader.cpp similarity index 100% rename from AudioFileLoader.cpp rename to plugin/src/AudioFileLoader.cpp diff --git a/AudioFileLoader.hpp b/plugin/src/AudioFileLoader.hpp similarity index 100% rename from AudioFileLoader.hpp rename to plugin/src/AudioFileLoader.hpp diff --git a/plugin/src/PluginEditor.cpp b/plugin/src/PluginEditor.cpp index 778201d..c2ad94a 100644 --- a/plugin/src/PluginEditor.cpp +++ b/plugin/src/PluginEditor.cpp @@ -1,4 +1,5 @@ #include "PluginEditor.hpp" +#include "AudioFileLoader.hpp" AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( AdaptiveEchoAudioProcessor &p) @@ -70,18 +71,58 @@ AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( releaseAttachment = std::make_unique( processor.apvts, "release", releaseSlider); + addAndMakeVisible (openFileButton); + + openFileButton.onClick = [this] + { + auto chooser = std::make_shared( + "Select an audio file", + juce::File{}, + "*.wav;*.aiff;*.mp3" + ); + + chooser->launchAsync ( + juce::FileBrowserComponent::openMode + | juce::FileBrowserComponent::canSelectFiles, + [this, chooser] (const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (! file.existsAsFile()) + return; + processor.loadFile (file); + repaint(); // forces paint() to update with new sample text + } + ); + }; + addAndMakeVisible(midiKeyboard); midiKeyboard.setAvailableRange(24, 108); } void AdaptiveEchoAudioProcessorEditor::paint(juce::Graphics &g) { - g.fillAll( - getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); + g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); g.setColour(juce::Colours::white); g.setFont(16.0f); g.drawFittedText("Adaptive Echo - Sine Generator Example (w/ MIDI)", getLocalBounds().reduced(10, 6), juce::Justification::centredTop, 1); + + // Only display first 10 samples + g.setFont(14.0f); + auto y = 50; + g.drawText("First 10 samples:", 10, y, 400, 20, juce::Justification::left); + y += 20; + + juce::String s; + int numToShow = std::min(10, (int)processor.loadedSamples.size()); + for (int i = 0; i < numToShow; ++i) + s += juce::String(processor.loadedSamples[i], 5) + " "; + g.drawText(s, 10, y, 400, 20, juce::Justification::left); + + // Debug: print a few samples to console only, NOT all + int numToPrint = std::min(1000, (int)processor.loadedSamples.size()); + for (int i = 0; i < numToPrint; ++i) + DBG("Sample " << i << ": " << processor.loadedSamples[i]); } void AdaptiveEchoAudioProcessorEditor::resized() { @@ -114,4 +155,6 @@ void AdaptiveEchoAudioProcessorEditor::resized() { releaseSlider.setBounds(controlArea.removeFromLeft(sliderWidth).reduced(4)); releaseLabel.setBounds(releaseSlider.getX(), releaseSlider.getBottom(), releaseSlider.getWidth(), 20); + + openFileButton.setBounds (10, 10, 120, 24); } diff --git a/plugin/src/PluginEditor.hpp b/plugin/src/PluginEditor.hpp index 3b8ff85..6397a95 100644 --- a/plugin/src/PluginEditor.hpp +++ b/plugin/src/PluginEditor.hpp @@ -28,6 +28,8 @@ class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { juce::Label sustainLabel; juce::Label releaseLabel; + juce::TextButton openFileButton { "Open File" }; + juce::MidiKeyboardComponent midiKeyboard{ processor.getMidiKeyboardState(), juce::MidiKeyboardComponent::horizontalKeyboard}; @@ -42,4 +44,4 @@ class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( AdaptiveEchoAudioProcessorEditor) -}; \ No newline at end of file +}; diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 3a228af..641b127 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -189,4 +189,71 @@ juce::AudioProcessorEditor *AdaptiveEchoAudioProcessor::createEditor() { // This factory must be present in the TU with the processor class. juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter() { return new AdaptiveEchoAudioProcessor(); +} + +void AdaptiveEchoAudioProcessor::loadFile(const juce::File& f) +{ + juce::AudioFormatManager formatManager; + formatManager.registerBasicFormats(); + + std::unique_ptr reader(formatManager.createReaderFor(f)); + /* Adding a debug message here. Right now we are able to load wav files, + * but not able to load mp3 files. More specifically, our program will load the waveform from a wav file + * and print the first few samples; but for mp3 files, we do not print any samples; + * the reason for that is JUCE does not have an MP3 decoder available, at least on Linux systems. + * Therefore: reader is nullptr, loadFile() exits early, audioBuffer is never set, and loadedSamples remains empty + * */ + if (!reader) { + // DBG("Could not create reader for file: " + f.getFullPathName()); + juce::AlertWindow::showMessageBoxAsync( + juce::AlertWindow::WarningIcon, + "Unsupported Audio Format", + "This plugin supports WAV and AIFF files only.\n\n" + "MP3 decoding is not available on this system." + ); + return; + } + + auto tempBuffer = std::make_shared>(reader->numChannels, (int)reader->lengthInSamples); + reader->read(tempBuffer.get(), 0, (int)reader->lengthInSamples, 0, true, true); + audioBuffer = tempBuffer; + + // Populate loadedSamples for GUI display + loadedSamples.clear(); + if (audioBuffer) + { + auto& buf = *audioBuffer; + for (int ch = 0; ch < buf.getNumChannels(); ++ch) + { + auto* ptr = buf.getReadPointer(ch); + loadedSamples.insert(loadedSamples.end(), ptr, ptr + buf.getNumSamples()); + } + } + + DBG(juce::String::formatted("Loaded %d samples from %d channels", + (int)reader->lengthInSamples, + reader->numChannels)); +} + +void AdaptiveEchoAudioProcessor::getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) +{ + if (!audioBuffer) + { + bufferToFill.clearActiveBufferRegion(); + return; + } + + const auto& buf = *audioBuffer; // Dereference shared_ptr + + int numChannels = std::min(bufferToFill.buffer->getNumChannels(), buf.getNumChannels()); + int numSamples = std::min(bufferToFill.buffer->getNumSamples(), buf.getNumSamples()); + + for (int ch = 0; ch < numChannels; ++ch) + { + auto* dst = bufferToFill.buffer->getWritePointer(ch); + const float* src = buf.getReadPointer(ch); + + for (int i = 0; i < numSamples; ++i) + dst[i] = src[i]; + } } \ No newline at end of file diff --git a/plugin/src/PluginProcessor.hpp b/plugin/src/PluginProcessor.hpp index 0d48418..1882bca 100644 --- a/plugin/src/PluginProcessor.hpp +++ b/plugin/src/PluginProcessor.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "Note.hpp" #include "PluginEnvelope.hpp" @@ -53,6 +54,11 @@ class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { return midiState; } + void loadFile(const juce::File& f); + std::vector loadedSamples; + // audio callback + void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill); + private: // Simple sine generator state per channel std::array phase{0.0, 0.0}; // support up to stereo @@ -61,6 +67,7 @@ class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { Note activeNote; ADSREnvelope env; std::shared_ptr env_ptr; + std::shared_ptr> audioBuffer; // Smoothed volume to avoid zipper noise juce::SmoothedValue From 5c3d35c6eff211d53d5f5f648bce118bdf098bd4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 19:08:32 -0500 Subject: [PATCH 04/10] Fixing compile warning, we used both unsigned int and int, and that caused compiler warning, fixing it by changing lenghtSamples to int and variable x to int --- plugin/src/PluginEnvelope.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/src/PluginEnvelope.hpp b/plugin/src/PluginEnvelope.hpp index 3c88089..1b29979 100644 --- a/plugin/src/PluginEnvelope.hpp +++ b/plugin/src/PluginEnvelope.hpp @@ -11,13 +11,13 @@ class EnvelopeGenerator { float initialValue; float finalValue; float curvature; - unsigned int lengthSamples; // 0 for sustain + int lengthSamples; // 0 for sustain Segment(float iv, float fv, float c, unsigned int l) : initialValue(iv), finalValue(fv), curvature(c), lengthSamples(l) { } - float fx(unsigned int x) const { + float fx(int x) const { if (lengthSamples == 0) return finalValue; // sustain if (x > lengthSamples) @@ -80,4 +80,4 @@ class ADSREnvelope : public EnvelopeGenerator { float a, d, s, r; float ac, dc, rc; int sr; -}; \ No newline at end of file +}; From 8466196c777fc914311e82f94e5dd9e264fe8f86 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 21:17:44 -0500 Subject: [PATCH 05/10] Support multiple oscilator types, let users choose --- plugin/src/PluginEditor.cpp | 17 +++++++++++++++-- plugin/src/PluginEditor.hpp | 4 +++- plugin/src/PluginProcessor.cpp | 22 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/plugin/src/PluginEditor.cpp b/plugin/src/PluginEditor.cpp index c2ad94a..16eb546 100644 --- a/plugin/src/PluginEditor.cpp +++ b/plugin/src/PluginEditor.cpp @@ -71,6 +71,16 @@ AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( releaseAttachment = std::make_unique( processor.apvts, "release", releaseSlider); + // Oscillator type combo box + oscTypeBox.addItem("Sine", 1); + oscTypeBox.addItem("Square", 2); + oscTypeBox.addItem("Saw", 3); + addAndMakeVisible(oscTypeBox); + + // Attach to processor parameter + oscTypeAttachment = std::make_unique( + processor.apvts, "oscType", oscTypeBox); + addAndMakeVisible (openFileButton); openFileButton.onClick = [this] @@ -156,5 +166,8 @@ void AdaptiveEchoAudioProcessorEditor::resized() { releaseLabel.setBounds(releaseSlider.getX(), releaseSlider.getBottom(), releaseSlider.getWidth(), 20); - openFileButton.setBounds (10, 10, 120, 24); -} + auto oscRow = bounds.removeFromTop(40); + oscTypeBox.setBounds(oscRow.removeFromLeft(150).reduced(4)); + + openFileButton.setBounds(10, 10, 120, 24); +} \ No newline at end of file diff --git a/plugin/src/PluginEditor.hpp b/plugin/src/PluginEditor.hpp index 6397a95..7fcd1a9 100644 --- a/plugin/src/PluginEditor.hpp +++ b/plugin/src/PluginEditor.hpp @@ -27,6 +27,8 @@ class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { juce::Label decayLabel; juce::Label sustainLabel; juce::Label releaseLabel; + juce::ComboBox oscTypeBox; + std::unique_ptr oscTypeAttachment; juce::TextButton openFileButton { "Open File" }; @@ -44,4 +46,4 @@ class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( AdaptiveEchoAudioProcessorEditor) -}; +}; \ No newline at end of file diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 641b127..71fe844 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -30,6 +30,10 @@ AdaptiveEchoAudioProcessor::createParameterLayout() { params.push_back(std::make_unique( "release", "Release", ADSRrange, 0.5f)); + params.push_back(std::make_unique( + "oscType", "Oscillator Type", + juce::StringArray{"Sine", "Square", "Saw"}, 0)); // default = Sine + return {params.begin(), params.end()}; } @@ -93,6 +97,10 @@ void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, if (auto *rParam = apvts.getRawParameterValue("release")) new_r = rParam->load(); + int oscType = 0; + if (auto* param = apvts.getRawParameterValue("oscType")) + oscType = static_cast(param->load()); + // Update ADSR parameters if (new_a != a || new_d != d || new_s != s || new_r != r) { a = new_a; @@ -139,7 +147,19 @@ void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, float globalVol = volumeSmoothed.getNextValue(); float amp = noteLevel * globalVol; - out[n] = std::sin(ph) * amp; + float value = 0.0f; + switch (oscType) { + case 0: // Sine + value = std::sin(ph); + break; + case 1: // Square + value = (std::sin(ph) >= 0.0 ? 1.0f : -1.0f); + break; + case 2: // Saw + value = 2.0f * (ph / juce::MathConstants::twoPi) - 1.0f; + break; + } + out[n] = value * amp; ph += phaseInc; if (ph >= juce::MathConstants::twoPi) From 15b6ae289b222e170268e33b3257ec119db9ddce Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 21:21:08 -0500 Subject: [PATCH 06/10] Move the osc type selection box to the right side of the file open button; so that it doesn't overlap with the print sample messages --- plugin/src/PluginEditor.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/src/PluginEditor.cpp b/plugin/src/PluginEditor.cpp index 16eb546..49ee172 100644 --- a/plugin/src/PluginEditor.cpp +++ b/plugin/src/PluginEditor.cpp @@ -166,8 +166,6 @@ void AdaptiveEchoAudioProcessorEditor::resized() { releaseLabel.setBounds(releaseSlider.getX(), releaseSlider.getBottom(), releaseSlider.getWidth(), 20); - auto oscRow = bounds.removeFromTop(40); - oscTypeBox.setBounds(oscRow.removeFromLeft(150).reduced(4)); - openFileButton.setBounds(10, 10, 120, 24); + oscTypeBox.setBounds(openFileButton.getRight() + 10, 10, 100, 24); } \ No newline at end of file From f781822ba8b4ee674d9c3b1b93252920aafd16c2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 21:42:51 -0500 Subject: [PATCH 07/10] Adding the sound category logic --- plugin/src/PluginProcessor.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 71fe844..234b87b 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -30,9 +30,15 @@ AdaptiveEchoAudioProcessor::createParameterLayout() { params.push_back(std::make_unique( "release", "Release", ADSRrange, 0.5f)); + // Oscillator type params.push_back(std::make_unique( - "oscType", "Oscillator Type", - juce::StringArray{"Sine", "Square", "Saw"}, 0)); // default = Sine + "oscType", "Oscillator Type", + juce::StringArray{"Sine", "Square", "Saw"}, 0)); + + // Sound category + params.push_back(std::make_unique( + "soundCategory", "Sound Category", + juce::StringArray{"Happy", "Harsh", "Bright", "Dark"}, 0)); return {params.begin(), params.end()}; } From 11371ff32bae7399cb6616bcbfab26aee7fcf0e5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 21:53:23 -0500 Subject: [PATCH 08/10] Sound category: the UI portion, now users can see a dropdown and from there they can choose the category to be happy, bright, dark, harsh, etc --- plugin/src/PluginEditor.cpp | 15 +++++++++++++-- plugin/src/PluginEditor.hpp | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugin/src/PluginEditor.cpp b/plugin/src/PluginEditor.cpp index 49ee172..6800401 100644 --- a/plugin/src/PluginEditor.cpp +++ b/plugin/src/PluginEditor.cpp @@ -78,8 +78,18 @@ AdaptiveEchoAudioProcessorEditor::AdaptiveEchoAudioProcessorEditor( addAndMakeVisible(oscTypeBox); // Attach to processor parameter - oscTypeAttachment = std::make_unique( - processor.apvts, "oscType", oscTypeBox); + oscTypeAttachment = std::make_unique(processor.apvts, "oscType", oscTypeBox); + + // Sound category combo box + soundCategoryBox.addItem("Happy", 1); + soundCategoryBox.addItem("Harsh", 2); + soundCategoryBox.addItem("Bright", 3); + soundCategoryBox.addItem("Dark", 4); + addAndMakeVisible(soundCategoryBox); + + // Attach to processor parameter + soundCategoryAttachment = + std::make_unique(processor.apvts, "soundCategory", soundCategoryBox); addAndMakeVisible (openFileButton); @@ -168,4 +178,5 @@ void AdaptiveEchoAudioProcessorEditor::resized() { openFileButton.setBounds(10, 10, 120, 24); oscTypeBox.setBounds(openFileButton.getRight() + 10, 10, 100, 24); + soundCategoryBox.setBounds(oscTypeBox.getRight() + 10, 10, 120, 24); } \ No newline at end of file diff --git a/plugin/src/PluginEditor.hpp b/plugin/src/PluginEditor.hpp index 7fcd1a9..d509a8f 100644 --- a/plugin/src/PluginEditor.hpp +++ b/plugin/src/PluginEditor.hpp @@ -29,6 +29,8 @@ class AdaptiveEchoAudioProcessorEditor : public juce::AudioProcessorEditor { juce::Label releaseLabel; juce::ComboBox oscTypeBox; std::unique_ptr oscTypeAttachment; + juce::ComboBox soundCategoryBox; + std::unique_ptr soundCategoryAttachment; juce::TextButton openFileButton { "Open File" }; From 18e773b8d24bff17065896f0b9a32865ccf0df2f Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 22:08:09 -0500 Subject: [PATCH 09/10] Sound category: add code in processBlock so as to adjust sounds based on user-chosen category --- plugin/src/PluginProcessor.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 234b87b..892a703 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -107,6 +107,37 @@ void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, if (auto* param = apvts.getRawParameterValue("oscType")) oscType = static_cast(param->load()); + int soundCategory = 0; + if (auto* param = apvts.getRawParameterValue("soundCategory")) + soundCategory = static_cast(param->load()); + + // Category influence (control-rate) + switch (soundCategory) + { + case 0: // Happy + new_a *= 0.6f; // faster attack + new_d *= 0.7f; + new_s = juce::jlimit(0.6f, 1.0f, new_s + 0.2f); + oscType = 0; // Sine + break; + + case 1: // Harsh + new_a *= 0.2f; // very fast attack + new_s *= 0.6f; + oscType = 1; // Square + break; + + case 2: // Bright + oscType = 2; // Saw + break; + + case 3: // Dark + new_a *= 1.4f; // slower attack + new_d *= 1.5f; + oscType = 0; // Sine + break; + } + // Update ADSR parameters if (new_a != a || new_d != d || new_s != s || new_r != r) { a = new_a; From 7702a14582157e3e8674c6f35bb847460213cddc Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 13 Dec 2025 22:16:33 -0500 Subject: [PATCH 10/10] Wrote a helper function, and moved the sound category handling code into this function; this makes the code structure cleaner and makes it easily to extend and add more categories. --- plugin/src/PluginProcessor.cpp | 68 +++++++++++++++++++++------------- plugin/src/PluginProcessor.hpp | 7 +++- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 892a703..9b76c50 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -82,6 +82,47 @@ bool AdaptiveEchoAudioProcessor::isBusesLayoutSupported( } #endif +void AdaptiveEchoAudioProcessor::applySoundCategory( + int category, + float& a, float& d, float& s, float& r, + int& oscType) +{ + switch (category) + { + case 0: // Happy + a *= 0.6f; // faster attack + d *= 0.7f; + s = std::max(s, 0.7f); + r *= 0.8f; + oscType = 0; // Sine + break; + + case 1: // Harsh + a *= 0.3f; + d *= 0.5f; + s = 1.0f; + r *= 1.2f; + oscType = 1; // Square + break; + + case 2: // Bright + a *= 0.5f; + d *= 0.8f; + oscType = 2; // Saw + break; + + case 3: // Dark + a *= 1.5f; + d *= 1.2f; + s *= 0.8f; + oscType = 0; // Sine + break; + + default: + break; + } +} + void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, juce::MidiBuffer &midi) { juce::ScopedNoDenormals noDenormals; @@ -111,32 +152,7 @@ void AdaptiveEchoAudioProcessor::processBlock(juce::AudioBuffer &buffer, if (auto* param = apvts.getRawParameterValue("soundCategory")) soundCategory = static_cast(param->load()); - // Category influence (control-rate) - switch (soundCategory) - { - case 0: // Happy - new_a *= 0.6f; // faster attack - new_d *= 0.7f; - new_s = juce::jlimit(0.6f, 1.0f, new_s + 0.2f); - oscType = 0; // Sine - break; - - case 1: // Harsh - new_a *= 0.2f; // very fast attack - new_s *= 0.6f; - oscType = 1; // Square - break; - - case 2: // Bright - oscType = 2; // Saw - break; - - case 3: // Dark - new_a *= 1.4f; // slower attack - new_d *= 1.5f; - oscType = 0; // Sine - break; - } + applySoundCategory(soundCategory, new_a, new_d, new_s, new_r, oscType); // Update ADSR parameters if (new_a != a || new_d != d || new_s != s || new_r != r) { diff --git a/plugin/src/PluginProcessor.hpp b/plugin/src/PluginProcessor.hpp index 1882bca..8819855 100644 --- a/plugin/src/PluginProcessor.hpp +++ b/plugin/src/PluginProcessor.hpp @@ -60,6 +60,11 @@ class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill); private: + void applySoundCategory( + int category, + float& a, float& d, float& s, float& r, + int& oscType + ); // Simple sine generator state per channel std::array phase{0.0, 0.0}; // support up to stereo double phaseInc = 0.0; // radians per sample @@ -79,4 +84,4 @@ class AdaptiveEchoAudioProcessor : public juce::AudioProcessor { float a, d, s, r, ac, dc, rc; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AdaptiveEchoAudioProcessor) -}; +}; \ No newline at end of file