commit f466c5cdad873481e1f7d67d119cbb8b31f277c0 Author: claude-agent Date: Sun May 3 21:06:31 2026 +0200 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. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..480b69e --- /dev/null +++ b/CMakeLists.txt @@ -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 +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6d4242 --- /dev/null +++ b/README.md @@ -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?) diff --git a/Source/HomApiClient.cpp b/Source/HomApiClient.cpp new file mode 100644 index 0000000..f5cc69d --- /dev/null +++ b/Source/HomApiClient.cpp @@ -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."); + }); +} diff --git a/Source/HomApiClient.h b/Source/HomApiClient.h new file mode 100644 index 0000000..038e8c9 --- /dev/null +++ b/Source/HomApiClient.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +/** + * 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; + using DoneCallback = std::function; + using ErrorCallback = std::function; + + 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; +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..954461c --- /dev/null +++ b/Source/PluginEditor.cpp @@ -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 ( + "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); +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..0a8221b --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#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) +}; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..87f5a48 --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -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& 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(); +} diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..5fe87ea --- /dev/null +++ b/Source/PluginProcessor.h @@ -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 +#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&, 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) +};