diff --git a/.gitignore b/.gitignore index d040773..5cdea88 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ DerivedData/ *.wav *.aiff *.mp3 +_codeql_detected_source_root diff --git a/apps/standalone/Main.cpp b/apps/standalone/Main.cpp index 553546d..1b5beb3 100644 --- a/apps/standalone/Main.cpp +++ b/apps/standalone/Main.cpp @@ -28,6 +28,7 @@ class StandaloneMainComponent : public juce::AudioAppComponent addAndMakeVisible(mainView); mainView.setOnLoadFile([this] { loadFile(); }); + mainView.setOnFileDrop([this](const juce::File& file) { loadFromFile(file); }); mainView.setLoadEnabled(true); mainView.setMonoState(false); @@ -70,33 +71,38 @@ class StandaloneMainComponent : public juce::AudioAppComponent } private: + void loadFromFile(const juce::File& file) + { + if (! file.existsAsFile()) + return; + + std::unique_ptr reader(formatManager.createReaderFor(file)); + if (reader == nullptr) + { + mainView.setFileName("Unsupported file"); + mainView.clearWaveform(); + return; + } + + const auto sourceSampleRate = reader->sampleRate; + auto newSource = std::make_unique(reader.release(), true); + transportSource.stop(); + transportSource.setSource(newSource.get(), 0, nullptr, sourceSampleRate); + readerSource = std::move(newSource); + transportSource.start(); + + mainView.setFileName(file.getFileName()); + mainView.setWaveformFile(file); + } + void loadFile() { - fileChooser = std::make_unique("Select an audio file", juce::File{}, "*.wav;*.aiff;*.aif"); + fileChooser = std::make_unique("Select an audio file", juce::File{}, + "*.wav;*.aiff;*.aif;*.mp3;*.flac;*.ogg;*.caf;*.mp4;*.m4a"); const auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles; fileChooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { - const auto file = chooser.getResult(); - if (! file.existsAsFile()) - return; - - std::unique_ptr reader(formatManager.createReaderFor(file)); - if (reader == nullptr) - { - mainView.setFileName("Unsupported file"); - mainView.clearWaveform(); - return; - } - - const auto sourceSampleRate = reader->sampleRate; - auto newSource = std::make_unique(reader.release(), true); - transportSource.stop(); - transportSource.setSource(newSource.get(), 0, nullptr, sourceSampleRate); - readerSource = std::move(newSource); - transportSource.start(); - - mainView.setFileName(file.getFileName()); - mainView.setWaveformFile(file); + loadFromFile(chooser.getResult()); }); } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 781cd59..24a89c6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -16,6 +16,14 @@ - UI polls snapshots on a timer and renders meters + spectrum bars. - Education cards are loaded from JSON and rotated on user interaction. +## File Import / Sharing + +- Accepted formats: WAV, AIFF, MP3, FLAC, OGG, CAF, MP4/M4A (whatever `registerBasicFormats()` plus platform decoders support). +- **Standalone**: "Load Audio" button opens a file picker; files can also be dragged from Finder directly onto the waveform area. +- **Plugin**: "Load Audio" button and drag-and-drop are both enabled for a *reference waveform* — the file is displayed visually only. Live audio analysis continues from the DAW input bus via `processBlock`. +- `WaveformView` implements `juce::FileDragAndDropTarget` and exposes an `onFileDropped` callback; `KindPathMainComponent` exposes `setOnFileDrop` to wire callers in. + + ## Shared Boundaries - `AnalysisEngine` is shared across app and plugin. - UI is shared and wired to analysis + education data in each target. diff --git a/plugins/kindpath-q/PluginEditor.cpp b/plugins/kindpath-q/PluginEditor.cpp index a31e702..b500ecf 100644 --- a/plugins/kindpath-q/PluginEditor.cpp +++ b/plugins/kindpath-q/PluginEditor.cpp @@ -19,9 +19,13 @@ KindPathQAudioProcessorEditor::KindPathQAudioProcessorEditor(KindPathQAudioProce audioProcessor(p), mainView(audioProcessor.getAnalysisEngine()) { + formatManager.registerBasicFormats(); + addAndMakeVisible(mainView); - mainView.setLoadEnabled(false); - mainView.setFileName("Live input"); + mainView.setLoadEnabled(true); + mainView.setOnLoadFile([this] { loadFile(); }); + mainView.setOnFileDrop([this](const juce::File& file) { loadFromFile(file); }); + mainView.setFileName("Live input — drop a reference file to compare"); mainView.setMonoState(false); loadEducationDeck(); @@ -41,6 +45,28 @@ void KindPathQAudioProcessorEditor::resized() mainView.setBounds(getLocalBounds()); } +void KindPathQAudioProcessorEditor::loadFromFile(const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + // In the plugin, we display the waveform as a visual reference only. + // Live audio analysis continues via processBlock from the DAW input bus. + mainView.setFileName(file.getFileName()); + mainView.setWaveformFile(file); +} + +void KindPathQAudioProcessorEditor::loadFile() +{ + fileChooser = std::make_unique("Select an audio file", juce::File{}, + "*.wav;*.aiff;*.aif;*.mp3;*.flac;*.ogg;*.caf;*.mp4;*.m4a"); + const auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles; + fileChooser->launchAsync(flags, [this](const juce::FileChooser& chooser) + { + loadFromFile(chooser.getResult()); + }); +} + void KindPathQAudioProcessorEditor::loadEducationDeck() { juce::String errorMessage; diff --git a/plugins/kindpath-q/PluginEditor.h b/plugins/kindpath-q/PluginEditor.h index a5647f0..60dcc0d 100644 --- a/plugins/kindpath-q/PluginEditor.h +++ b/plugins/kindpath-q/PluginEditor.h @@ -15,11 +15,16 @@ class KindPathQAudioProcessorEditor : public juce::AudioProcessorEditor void resized() override; private: + void loadFile(); + void loadFromFile(const juce::File& file); void loadEducationDeck(); KindPathQAudioProcessor& audioProcessor; kindpath::education::EducationDeck deck; kindpath::ui::KindPathMainComponent mainView; + juce::AudioFormatManager formatManager; + std::unique_ptr fileChooser; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KindPathQAudioProcessorEditor) }; diff --git a/ui/KindPathMainComponent.cpp b/ui/KindPathMainComponent.cpp index bbfd146..e0a7c8d 100644 --- a/ui/KindPathMainComponent.cpp +++ b/ui/KindPathMainComponent.cpp @@ -57,6 +57,11 @@ namespace kindpath::ui waveformView.clear(); } + void KindPathMainComponent::setOnFileDrop(std::function handler) + { + waveformView.onFileDropped = std::move(handler); + } + void KindPathMainComponent::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff0b0d12)); diff --git a/ui/KindPathMainComponent.h b/ui/KindPathMainComponent.h index fc0b37b..cf68162 100644 --- a/ui/KindPathMainComponent.h +++ b/ui/KindPathMainComponent.h @@ -23,6 +23,7 @@ namespace kindpath::ui void setMonoState(bool enabled); void setWaveformFile(const juce::File& file); void clearWaveform(); + void setOnFileDrop(std::function handler); void resized() override; void paint(juce::Graphics& g) override; diff --git a/ui/WaveformView.cpp b/ui/WaveformView.cpp index 524c1f6..02860ba 100644 --- a/ui/WaveformView.cpp +++ b/ui/WaveformView.cpp @@ -1,5 +1,17 @@ #include "WaveformView.h" +namespace +{ + const juce::StringArray& audioExtensions() + { + static const juce::StringArray exts { + ".wav", ".aiff", ".aif", ".mp3", + ".flac", ".ogg", ".caf", ".mp4", ".m4a" + }; + return exts; + } +} + namespace kindpath::ui { WaveformView::WaveformView(juce::AudioFormatManager& formatManager) @@ -28,6 +40,44 @@ namespace kindpath::ui repaint(); } + bool WaveformView::isInterestedInFileDrag(const juce::StringArray& files) + { + for (const auto& f : files) + { + if (audioExtensions().contains(juce::File(f).getFileExtension().toLowerCase())) + return true; + } + return false; + } + + void WaveformView::fileDragEnter(const juce::StringArray&, int, int) + { + isDragOver = true; + repaint(); + } + + void WaveformView::fileDragExit(const juce::StringArray&) + { + isDragOver = false; + repaint(); + } + + void WaveformView::filesDropped(const juce::StringArray& files, int, int) + { + isDragOver = false; + for (const auto& f : files) + { + const juce::File file(f); + if (audioExtensions().contains(file.getFileExtension().toLowerCase())) + { + if (onFileDropped) + onFileDropped(file); + break; + } + } + repaint(); + } + void WaveformView::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff0f1115)); @@ -45,6 +95,16 @@ namespace kindpath::ui g.setFont(15.0f); g.drawFittedText(statusText, getLocalBounds().reduced(12), juce::Justification::centred, 2); } + + if (isDragOver) + { + g.setColour(juce::Colour(0x447ed0ff)); + g.fillRect(getLocalBounds()); + g.setColour(juce::Colour(0xff7ed0ff)); + g.drawRect(getLocalBounds(), 2); + g.setFont(15.0f); + g.drawFittedText("Drop audio file here", getLocalBounds().reduced(12), juce::Justification::centred, 2); + } } void WaveformView::resized() diff --git a/ui/WaveformView.h b/ui/WaveformView.h index da218a7..17d25ea 100644 --- a/ui/WaveformView.h +++ b/ui/WaveformView.h @@ -4,7 +4,8 @@ namespace kindpath::ui { - class WaveformView : public juce::Component + class WaveformView : public juce::Component, + public juce::FileDragAndDropTarget { public: explicit WaveformView(juce::AudioFormatManager& formatManager); @@ -12,12 +13,22 @@ namespace kindpath::ui void loadFile(const juce::File& file); void clear(); + /** Called on the message thread when an audio file is dropped onto this view. */ + std::function onFileDropped; + void paint(juce::Graphics& g) override; void resized() override; + // FileDragAndDropTarget + bool isInterestedInFileDrag(const juce::StringArray& files) override; + void fileDragEnter(const juce::StringArray& files, int x, int y) override; + void fileDragExit(const juce::StringArray& files) override; + void filesDropped(const juce::StringArray& files, int x, int y) override; + private: juce::AudioThumbnailCache thumbnailCache { 8 }; juce::AudioThumbnail thumbnail; juce::String statusText = "Load audio to see the waveform"; + bool isDragOver = false; }; }