Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ DerivedData/
*.wav
*.aiff
*.mp3
_codeql_detected_source_root
50 changes: 28 additions & 22 deletions apps/standalone/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -70,33 +71,38 @@ class StandaloneMainComponent : public juce::AudioAppComponent
}

private:
void loadFromFile(const juce::File& file)
{
if (! file.existsAsFile())
return;

std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
if (reader == nullptr)
{
mainView.setFileName("Unsupported file");
mainView.clearWaveform();
return;
}

const auto sourceSampleRate = reader->sampleRate;
auto newSource = std::make_unique<juce::AudioFormatReaderSource>(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<juce::FileChooser>("Select an audio file", juce::File{}, "*.wav;*.aiff;*.aif");
fileChooser = std::make_unique<juce::FileChooser>("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<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
if (reader == nullptr)
{
mainView.setFileName("Unsupported file");
mainView.clearWaveform();
return;
}

const auto sourceSampleRate = reader->sampleRate;
auto newSource = std::make_unique<juce::AudioFormatReaderSource>(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());
});
}

Expand Down
8 changes: 8 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 28 additions & 2 deletions plugins/kindpath-q/PluginEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<juce::FileChooser>("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;
Expand Down
5 changes: 5 additions & 0 deletions plugins/kindpath-q/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<juce::FileChooser> fileChooser;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KindPathQAudioProcessorEditor)
};
5 changes: 5 additions & 0 deletions ui/KindPathMainComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ namespace kindpath::ui
waveformView.clear();
}

void KindPathMainComponent::setOnFileDrop(std::function<void(const juce::File&)> handler)
{
waveformView.onFileDropped = std::move(handler);
}

void KindPathMainComponent::paint(juce::Graphics& g)
{
g.fillAll(juce::Colour(0xff0b0d12));
Expand Down
1 change: 1 addition & 0 deletions ui/KindPathMainComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace kindpath::ui
void setMonoState(bool enabled);
void setWaveformFile(const juce::File& file);
void clearWaveform();
void setOnFileDrop(std::function<void(const juce::File&)> handler);

void resized() override;
void paint(juce::Graphics& g) override;
Expand Down
60 changes: 60 additions & 0 deletions ui/WaveformView.cpp
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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));
Expand All @@ -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()
Expand Down
13 changes: 12 additions & 1 deletion ui/WaveformView.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,31 @@

namespace kindpath::ui
{
class WaveformView : public juce::Component
class WaveformView : public juce::Component,
public juce::FileDragAndDropTarget
{
public:
explicit WaveformView(juce::AudioFormatManager& formatManager);

void loadFile(const juce::File& file);
void clear();

/** Called on the message thread when an audio file is dropped onto this view. */
std::function<void(const juce::File&)> 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;
};
}