initial scaffold: JUCE VST3/AU plugin (Path 2)

Cross-platform DAW plugin that uploads audio to
registry.houseofmixtape.com and imports stems back into the host DAW.

Skeleton — does not build yet. To build:
  1. Clone JUCE next to this repo: git clone https://github.com/juce-framework/JUCE
  2. cmake -B build -DJUCE_DIR=../JUCE
  3. cmake --build build --target HouseOfMixtape_VST3

Files:
  - CMakeLists.txt              — juce_add_plugin VST3+AU+Standalone
  - Source/PluginProcessor.{h,cpp} — passthrough AudioProcessor
  - Source/PluginEditor.{h,cpp}    — drag-drop + Upload btn + progress (Paper&Ink dark)
  - Source/HomApiClient.{h,cpp}    — HTTP skeleton for /api/jobs/* (real wiring deferred)
  - README.md                   — full build instructions + roadmap

This commit is the scaffold checkpoint. Real HTTP wiring + DAW
file-import API integration land in subsequent commits.
This commit is contained in:
claude-agent 2026-05-03 21:06:31 +02:00
commit f466c5cdad
8 changed files with 439 additions and 0 deletions

61
CMakeLists.txt Normal file
View File

@ -0,0 +1,61 @@
cmake_minimum_required(VERSION 3.22)
project(HouseOfMixtape VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# JUCE is expected as a sibling directory by default. Override with
# -DJUCE_DIR=/path/to/JUCE if you keep it elsewhere.
if(NOT DEFINED JUCE_DIR)
set(JUCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../JUCE")
endif()
if(NOT EXISTS "${JUCE_DIR}/CMakeLists.txt")
message(FATAL_ERROR
"JUCE not found at ${JUCE_DIR}. "
"Clone https://github.com/juce-framework/JUCE next to this dir, "
"or pass -DJUCE_DIR=/path/to/JUCE."
)
endif()
add_subdirectory(${JUCE_DIR} JUCE)
juce_add_plugin(HouseOfMixtape
COMPANY_NAME "House of Mixtape"
BUNDLE_ID "com.houseofmixtape.daw"
PLUGIN_MANUFACTURER_CODE HoMx
PLUGIN_CODE HoM1
FORMATS VST3 AU Standalone
PRODUCT_NAME "House of Mixtape"
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_SYNTH FALSE
EDITOR_WANTS_KEYBOARD_FOCUS TRUE
COPY_PLUGIN_AFTER_BUILD TRUE
VST3_CATEGORIES "Fx" "Tools"
AU_MAIN_TYPE "kAudioUnitType_Effect"
)
target_sources(HouseOfMixtape PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/HomApiClient.cpp
)
target_compile_definitions(HouseOfMixtape PRIVATE
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=1
JUCE_VST3_CAN_REPLACE_VST2=0
)
target_link_libraries(HouseOfMixtape PRIVATE
juce::juce_audio_utils
juce::juce_dsp
juce::juce_gui_extra
juce::juce_audio_formats
juce::juce_audio_devices
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
)

91
README.md Normal file
View File

@ -0,0 +1,91 @@
# House of Mixtape — DAW Plugin (Path 2)
Cross-platform VST3/AU plugin that lets the user upload audio from inside
their DAW and receive Phoenix-extracted stems + MIDI directly into the
arrangement. Targets Win/Mac/Linux. AAX (Pro Tools) is a v2 ambition.
## Status: scaffolding (2026-05-03)
This directory holds the JUCE project skeleton + build instructions. The
actual plugin is **not yet compiled** — that's a separate sprint that
needs:
1. JUCE installed (clone https://github.com/juce-framework/JUCE)
2. CMake 3.22+
3. Per-OS toolchain (MSVC on Win, Xcode on macOS, gcc/clang on Linux)
The skeleton is enough to verify the build pipeline. Once a developer
has JUCE + CMake set up locally, `cmake -B build && cmake --build build`
should produce a working empty plugin that loads in Ableton/FL/Logic.
## Why JUCE (vs alternatives)
- **JUCE** : industry standard for VST3/AU plugins. Free for open-source,
paid licence ($800/yr indie) for commercial. Cross-platform out of the
box. Used by everyone (Pro-Q, Serum, Massive, ToneBoosters…).
- iPlug2 : MIT-licenced alternative. Smaller community. Considered.
- DPF : niche.
- Faust : DSP-only, not for HTTP-talking plugins.
JUCE wins on docs + support + the fact that we'll integrate HTTP +
file-import in C++, which is well-trod ground there.
## What the plugin does
```
┌───────────── DAW (Ableton/FL/Logic/Reaper) ─────────────┐
│ │
│ ┌──────────────────────────────────┐ │
│ │ House of Mixtape (VST3 plugin) │ │
│ │ │ │
│ │ 1. User drag-drops audio │ │
│ │ 2. POST registry.../jobs/upload │ │
│ │ 3. POST .../jobs/submit │ │
│ │ 4. Poll /status (progress bar) │ │
│ │ 5. GET /output → stems + MIDI │ │
│ │ 6. Drop stems into DAW tracks │ │
│ │ (via VST3 host import API) │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
## Files
| Path | Purpose |
|---|---|
| `CMakeLists.txt` | JUCE+CMake project. References JUCE submodule. |
| `Source/PluginProcessor.{h,cpp}` | AudioProcessor — does no DSP yet, just hosts the editor. |
| `Source/PluginEditor.{h,cpp}` | The plugin UI. Skeleton: Upload button + status text. |
| `Source/HomApiClient.{h,cpp}` | HTTP client for `registry.houseofmixtape.com/api/jobs/*`. Skeleton stubs. |
## Build (when ready)
```bash
# Clone JUCE next to this dir
cd ..
git clone --depth 1 https://github.com/juce-framework/JUCE.git
cd daw-plugin
# Configure + build (Linux example)
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DJUCE_BUILD_TESTS=OFF \
-DJUCE_DIR=$(pwd)/../JUCE
cmake --build build --target HouseOfMixtape_VST3 -j
# Output:
# build/HouseOfMixtape_artefacts/Release/VST3/HouseOfMixtape.vst3/
```
## Roadmap
- [x] Scaffold (this commit)
- [ ] HelloWorld build verified on at least one OS
- [ ] HTTP client wired to `registry.houseofmixtape.com`
- [ ] Bearer auth flow
- [ ] Audio drag-drop in plugin window
- [ ] Status polling + progress bar
- [ ] Stems import to DAW tracks (VST3 host file-drop API)
- [ ] AU build (macOS)
- [ ] Code signing + notarization (macOS)
- [ ] Win installer
- [ ] Distribution (Gumroad? Free? Subscription?)

35
Source/HomApiClient.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "HomApiClient.h"
HomApiClient::HomApiClient (juce::String registryBaseUrl)
: baseUrl (std::move (registryBaseUrl))
{
}
void HomApiClient::submitJobAsync (const juce::File& audio,
ProgressCallback onProgress,
DoneCallback onDone,
ErrorCallback onError)
{
juce::ignoreUnused (audio);
// Skeleton — fire a fake progress sweep then signal "not implemented".
// Real implementation:
// 1) POST baseUrl + "/jobs/upload" with multipart audio file
// 2) POST baseUrl + "/jobs/submit" with the returned key
// 3) Poll baseUrl + "/jobs/{id}/status" every ~3s
// 4) GET baseUrl + "/jobs/{id}/output" → stems URLs
// 5) Download each stem to a temp dir
// 6) Trigger the host DAW to import them as new tracks
// (FileDragAndDropHandling on the host's edit window)
juce::Thread::launch ([onProgress, onError]
{
for (double p : { 0.05, 0.20, 0.50, 0.80 })
{
juce::Thread::sleep (300);
if (onProgress) onProgress (p);
}
if (onError)
onError ("API client skeleton — wire HTTP in next sprint.");
});
}

32
Source/HomApiClient.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <juce_core/juce_core.h>
#include <juce_audio_basics/juce_audio_basics.h>
#include <functional>
/**
* House of Mixtape API client (Path 2 plugin registry.houseofmixtape.com).
* Skeleton submit/poll/output endpoints are stubbed; the actual
* HTTP/JUCE URL wiring lands in a follow-up commit.
*/
class HomApiClient
{
public:
using ProgressCallback = std::function<void (double)>;
using DoneCallback = std::function<void (juce::var)>;
using ErrorCallback = std::function<void (const juce::String&)>;
explicit HomApiClient (juce::String registryBaseUrl);
// Async: upload + submit + poll until done|error.
void submitJobAsync (const juce::File& audio,
ProgressCallback onProgress,
DoneCallback onDone,
ErrorCallback onError);
void setBearerToken (const juce::String& token) { bearer = token; }
private:
juce::String baseUrl;
juce::String bearer;
};

98
Source/PluginEditor.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "PluginEditor.h"
HomAudioProcessorEditor::HomAudioProcessorEditor (HomAudioProcessor& p)
: AudioProcessorEditor (&p), processor (p),
progressBar (progressValue)
{
setSize (440, 240);
setResizable (true, true);
addAndMakeVisible (uploadBtn);
uploadBtn.onClick = [this] { onUploadClicked(); };
addAndMakeVisible (statusLbl);
statusLbl.setText ("Drop an audio file or click the button.",
juce::dontSendNotification);
statusLbl.setJustificationType (juce::Justification::centred);
addAndMakeVisible (progressBar);
progressBar.setVisible (false);
}
void HomAudioProcessorEditor::paint (juce::Graphics& g)
{
// Paper&Ink dark
g.fillAll (juce::Colour (0xff1A1715));
g.setColour (juce::Colour (0xffD4A574));
g.setFont (16.0f);
g.drawFittedText ("House of Mixtape",
getLocalBounds().removeFromTop (40),
juce::Justification::centred, 1);
}
void HomAudioProcessorEditor::resized()
{
auto r = getLocalBounds().reduced (16);
r.removeFromTop (40); // brand header
statusLbl.setBounds (r.removeFromTop (32));
r.removeFromTop (12);
uploadBtn.setBounds (r.removeFromTop (44).reduced (40, 0));
r.removeFromTop (16);
progressBar.setBounds (r.removeFromTop (24));
}
bool HomAudioProcessorEditor::isInterestedInFileDrag (const juce::StringArray& files)
{
for (auto& f : files)
if (f.endsWithIgnoreCase (".wav") || f.endsWithIgnoreCase (".mp3")
|| f.endsWithIgnoreCase (".flac") || f.endsWithIgnoreCase (".m4a"))
return true;
return false;
}
void HomAudioProcessorEditor::filesDropped (const juce::StringArray& files, int, int)
{
if (files.isEmpty()) return;
setStatus ("Uploading " + juce::File (files[0]).getFileName() + "");
progressBar.setVisible (true);
progressValue = 0.05;
// Hand off to the API client (skeleton — actual HTTP wiring lands in
// a subsequent commit once Path 2 sprint resumes).
processor.getApiClient().submitJobAsync (
juce::File (files[0]),
/* onProgress */ [this] (double p) { progressValue = p; },
/* onDone */ [this] (juce::var output) {
juce::ignoreUnused (output);
progressBar.setVisible (false);
setStatus ("Done — stems imported into the project.");
},
/* onError */ [this] (const juce::String& msg) {
progressBar.setVisible (false);
setStatus ("Error: " + msg);
}
);
}
void HomAudioProcessorEditor::onUploadClicked()
{
auto chooser = std::make_shared<juce::FileChooser> (
"Choose an audio file",
juce::File::getSpecialLocation (juce::File::userMusicDirectory),
"*.wav;*.mp3;*.flac;*.m4a"
);
auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles;
chooser->launchAsync (flags, [this, chooser] (const juce::FileChooser& fc) {
auto files = fc.getResults();
if (files.isEmpty()) return;
juce::StringArray paths;
paths.add (files[0].getFullPathName());
filesDropped (paths, 0, 0);
});
}
void HomAudioProcessorEditor::setStatus (const juce::String& msg)
{
statusLbl.setText (msg, juce::dontSendNotification);
}

32
Source/PluginEditor.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "PluginProcessor.h"
class HomAudioProcessorEditor : public juce::AudioProcessorEditor,
public juce::FileDragAndDropTarget
{
public:
explicit HomAudioProcessorEditor (HomAudioProcessor&);
~HomAudioProcessorEditor() override = default;
void paint (juce::Graphics&) override;
void resized() override;
// FileDragAndDropTarget
bool isInterestedInFileDrag (const juce::StringArray& files) override;
void filesDropped (const juce::StringArray& files, int x, int y) override;
private:
HomAudioProcessor& processor;
juce::TextButton uploadBtn { "Drop / browse audio" };
juce::Label statusLbl;
juce::ProgressBar progressBar;
double progressValue { 0.0 };
void onUploadClicked();
void setStatus (const juce::String& msg);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HomAudioProcessorEditor)
};

View File

@ -0,0 +1,43 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
HomAudioProcessor::HomAudioProcessor()
: AudioProcessor (BusesProperties()
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
apiClient ("https://registry.houseofmixtape.com/api")
{
}
void HomAudioProcessor::prepareToPlay (double, int) {}
void HomAudioProcessor::releaseResources() {}
bool HomAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
// Mono or stereo only (passthrough — we don't process audio yet)
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
return layouts.getMainInputChannelSet() == layouts.getMainOutputChannelSet();
}
void HomAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
// Passthrough — the plugin's value is in the GUI (upload + receive stems),
// not in real-time DSP.
juce::ignoreUnused (buffer);
}
juce::AudioProcessorEditor* HomAudioProcessor::createEditor()
{
return new HomAudioProcessorEditor (*this);
}
void HomAudioProcessor::getStateInformation (juce::MemoryBlock&) {}
void HomAudioProcessor::setStateInformation (const void*, int) {}
// JUCE plugin entry point
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new HomAudioProcessor();
}

47
Source/PluginProcessor.h Normal file
View File

@ -0,0 +1,47 @@
// House of Mixtape — DAW Plugin (Path 2)
// Skeleton AudioProcessor. Does no DSP yet; lives only to host the editor.
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "HomApiClient.h"
class HomAudioProcessor : public juce::AudioProcessor
{
public:
HomAudioProcessor();
~HomAudioProcessor() override = default;
// ── AudioProcessor lifecycle ──────────────────────────────────────
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported (const BusesLayout&) const override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
// ── Boilerplate JUCE host queries ─────────────────────────────────
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "House of Mixtape"; }
bool acceptsMidi() const override { return false; }
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&) override;
void setStateInformation (const void*, int) override;
// ── House of Mixtape — exposed to the editor ──────────────────────
HomApiClient& getApiClient() { return apiClient; }
private:
HomApiClient apiClient;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HomAudioProcessor)
};