From cdf79f2b310e369cf9f5c177ecfff58f54636c10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:57:51 +0000 Subject: [PATCH 1/5] Initial plan From c5dfff340a5a5ae4669ab8ad20983816c779be51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:17:02 +0000 Subject: [PATCH 2/5] Fix streaming logs, audio preview, and Insert into DAW Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> Agent-Logs-Url: https://github.com/audiohacking/acestep-vst/sessions/7e741f4b-4ac1-4335-adaa-507852c47b24 --- ml-bridge/plugin/PluginEditor.cpp | 81 ++++++++++++++++++---------- ml-bridge/plugin/PluginEditor.h | 4 +- ml-bridge/plugin/PluginPreview.cpp | 4 ++ ml-bridge/plugin/PluginProcessor.cpp | 59 ++++++++++++++++++++ ml-bridge/plugin/PluginProcessor.h | 11 ++++ 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/ml-bridge/plugin/PluginEditor.cpp b/ml-bridge/plugin/PluginEditor.cpp index aebe6e9..c195b3b 100644 --- a/ml-bridge/plugin/PluginEditor.cpp +++ b/ml-bridge/plugin/PluginEditor.cpp @@ -276,11 +276,16 @@ AcestepAudioProcessorEditor::AcestepAudioProcessorEditor(AcestepAudioProcessor& generateButton_.onClick = [this] { onGenerateClicked(); }; addChildComponent(generateButton_); - statusLabel_.setColour(juce::Label::textColourId, AcestepColours::textDim); - statusLabel_.setFont(juce::Font(juce::FontOptions().withPointHeight(12.0f))); - statusLabel_.setJustificationType(juce::Justification::topLeft); - statusLabel_.setMinimumHorizontalScale(1.0f); - addChildComponent(statusLabel_); + // Multi-line log area: shows all ace-lm / ace-synth output, autoscrolls. + logEditor_.setMultiLine(true, false); + logEditor_.setReadOnly(true); + logEditor_.setScrollbarsShown(true); + logEditor_.setCaretVisible(false); + logEditor_.setColour(juce::TextEditor::backgroundColourId, AcestepColours::panel); + logEditor_.setColour(juce::TextEditor::textColourId, AcestepColours::textDim); + logEditor_.setColour(juce::TextEditor::outlineColourId, AcestepColours::accentDim); + logEditor_.setFont(juce::Font(juce::FontOptions().withPointHeight(11.0f))); + addChildComponent(logEditor_); // ── Library tab components ──────────────────────────────────────────────── styleSmallButton(refreshLibButton_); @@ -491,6 +496,22 @@ void AcestepAudioProcessorEditor::timerCallback() if (loopButton_.getToggleState() != processorRef.isLoopPlayback()) loopButton_.setToggleState(processorRef.isLoopPlayback(), juce::dontSendNotification); + // Drain any new log lines from the processor and append them to the log editor. + // Do this on every timer tick (4 Hz) so output is visible as soon as possible. + { + juce::String newLog = processorRef.getAndClearNewLog(); + if (newLog.isNotEmpty()) + { + // Ensure the Generate tab log area is cleared at the start of a new + // generation (the processor clears pendingLog_ at that point so we + // detect it via a "clear" sentinel embedded in the very first chunk). + logEditor_.moveCaretToEnd(); + logEditor_.insertTextAtCaret(newLog); + // Autoscroll to the last line so the user always sees the latest output. + logEditor_.moveCaretToEnd(); + } + } + // Refresh library cache once per second (4 Hz timer → every 4 ticks) ++libraryRefreshTick_; if (libraryRefreshTick_ >= 4) @@ -520,29 +541,21 @@ void AcestepAudioProcessorEditor::updateStatusFromProcessor() { const auto state = processorRef.getState(); - // Engine readiness + // Engine readiness (shown in the header, always visible) if (processorRef.areBinariesReady()) engineStatusLabel_.setText("Engine: ready", juce::dontSendNotification); else if (state == AcestepAudioProcessor::State::Failed) - engineStatusLabel_.setText("Engine: error \xe2\x80\x94 see status", juce::dontSendNotification); + engineStatusLabel_.setText("Engine: error \xe2\x80\x94 see log", juce::dontSendNotification); else engineStatusLabel_.setText("Engine: binaries not found \xe2\x80\x94 see Settings", juce::dontSendNotification); - // Status label (with temporary feedback messages) + // Temporary feedback messages are shown in the engine status bar. if (feedbackCountdown_ > 0) { - statusLabel_.setText(feedbackMsg_, juce::dontSendNotification); - statusLabel_.setColour(juce::Label::textColourId, AcestepColours::ok); + engineStatusLabel_.setText(feedbackMsg_, juce::dontSendNotification); --feedbackCountdown_; } - else - { - statusLabel_.setText(processorRef.getStatusText(), juce::dontSendNotification); - statusLabel_.setColour(juce::Label::textColourId, - state == AcestepAudioProcessor::State::Failed - ? AcestepColours::err : AcestepColours::textDim); - } // Generate / preview buttons: disable while engine is busy const bool busy = (state == AcestepAudioProcessor::State::Submitting @@ -586,6 +599,9 @@ void AcestepAudioProcessorEditor::onGenerateClicked() coverStrength = static_cast(coverStrengthSlider_.getValue()); } + // Clear the log editor now so the new generation starts fresh. + logEditor_.clear(); + processorRef.startGeneration( promptEditor_.getText(), durationSec > 0 ? durationSec : 10, @@ -676,12 +692,23 @@ void AcestepAudioProcessorEditor::onInsertDawClicked() const juce::File& file = libraryCache_[static_cast(row)].file; if (!file.existsAsFile()) { showFeedback("File not found."); return; } - // Reveal the file in Finder/Explorer so the user can drag it directly into - // the DAW timeline at the desired position. Also copy the path to the - // clipboard as a convenience fallback. - file.revealToUser(); - juce::SystemClipboard::copyTextToClipboard(file.getFullPathName()); - showFeedback("Revealed in file manager \xe2\x80\x94 drag into your DAW timeline. Path also copied."); + // Initiate an OS-level external drag so the DAW can receive the audio clip. + // performExternalDragDropOfFiles works from a button-click context on all + // platforms (the button's onClick is dispatched inside a mouse-up event). + // If the drag is declined (e.g. desktop is unfocused), fall back to + // revealing the file so the user can drag it manually. + const bool dragOk = juce::DragAndDropContainer::performExternalDragDropOfFiles( + juce::StringArray(file.getFullPathName()), + /*canMoveFiles=*/false, + &insertDawButton_); + + if (!dragOk) + { + // Fallback: open Finder/Explorer and copy path to clipboard. + file.revealToUser(); + juce::SystemClipboard::copyTextToClipboard(file.getFullPathName()); + showFeedback("Revealed in file manager \xe2\x80\x94 drag into your DAW. Path also copied."); + } } void AcestepAudioProcessorEditor::onRevealClicked() @@ -852,7 +879,7 @@ void AcestepAudioProcessorEditor::hideAllTabComponents() &genModeButton_, &coverModeButton_, &refFileLabel_, &browseRefButton_, &clearRefButton_, &coverStrengthLabel_, &coverStrengthSlider_, - &generateButton_, &statusLabel_, + &generateButton_, &logEditor_, &refreshLibButton_, &importButton_, &libraryList_, &previewButton_, &stopButton_, &loopButton_, @@ -952,9 +979,9 @@ void AcestepAudioProcessorEditor::layoutGenerateTab(juce::Rectangle r) generateButton_.setBounds(r.removeFromTop(30)); r.removeFromTop(6); - // Status — fill all remaining space (minimum 40px) - statusLabel_.setVisible(true); - statusLabel_.setBounds(r.removeFromTop(juce::jmax(40, r.getHeight()))); + // Log area — fill all remaining space (minimum 80px) to show subprocess output. + logEditor_.setVisible(true); + logEditor_.setBounds(r.removeFromTop(juce::jmax(80, r.getHeight()))); } void AcestepAudioProcessorEditor::layoutLibraryTab(juce::Rectangle r) diff --git a/ml-bridge/plugin/PluginEditor.h b/ml-bridge/plugin/PluginEditor.h index 8aacf65..89c383d 100644 --- a/ml-bridge/plugin/PluginEditor.h +++ b/ml-bridge/plugin/PluginEditor.h @@ -115,7 +115,9 @@ class AcestepAudioProcessorEditor : public juce::AudioProcessorEditor, juce::Slider coverStrengthSlider_; juce::TextButton generateButton_{ "Generate" }; - juce::Label statusLabel_; + // Read-only multi-line log that shows all subprocess output (ace-lm / ace-synth) + // and status messages; autoscrolls to the last line on each update. + juce::TextEditor logEditor_; // ── Library tab ─────────────────────────────────────────────────────────── juce::TextButton refreshLibButton_{ "Refresh" }; diff --git a/ml-bridge/plugin/PluginPreview.cpp b/ml-bridge/plugin/PluginPreview.cpp index 91accc3..5480e70 100644 --- a/ml-bridge/plugin/PluginPreview.cpp +++ b/ml-bridge/plugin/PluginPreview.cpp @@ -5,6 +5,10 @@ PluginPreview::PluginPreview() { formatManager_.registerBasicFormats(); + // Ensure the transport source is always prepared with safe defaults so that + // setSource() called from loadFile() will properly call prepareToPlay() on + // the reader source even if the DAW hasn't yet called prepareToPlay() on us. + transportSource_.prepareToPlay(512, 44100.0); } PluginPreview::~PluginPreview() diff --git a/ml-bridge/plugin/PluginProcessor.cpp b/ml-bridge/plugin/PluginProcessor.cpp index a2efcd7..66215f0 100644 --- a/ml-bridge/plugin/PluginProcessor.cpp +++ b/ml-bridge/plugin/PluginProcessor.cpp @@ -32,6 +32,13 @@ void writeToLogFile(const juce::String& message) void logError(const juce::String& msg) { writeToLogFile("ERROR: " + msg); } void logTrace(const juce::String& msg) { writeToLogFile("TRACE: " + msg); } +// ── Readable timestamp ───────────────────────────────────────────────────────── + +static juce::String logTimestamp() +{ + return juce::Time::getCurrentTime().formatted("%H:%M:%S"); +} + // ── State label ─────────────────────────────────────────────────────────────── const char* stateToString(AcestepAudioProcessor::State s) @@ -103,6 +110,27 @@ AcestepAudioProcessor::~AcestepAudioProcessor() cancelPendingUpdate(); } +// ═════════════════════════════════════════════════════════════════════════════ +// Streaming log +// ═════════════════════════════════════════════════════════════════════════════ + +void AcestepAudioProcessor::appendToLog(const juce::String& text) +{ + if (text.isEmpty()) return; + juce::ScopedLock l(logLock_); + pendingLog_ += text; + if (!pendingLog_.endsWithChar('\n')) + pendingLog_ += "\n"; +} + +juce::String AcestepAudioProcessor::getAndClearNewLog() +{ + juce::ScopedLock l(logLock_); + juce::String result = pendingLog_; + pendingLog_.clear(); + return result; +} + // ═════════════════════════════════════════════════════════════════════════════ // Path helpers // ═════════════════════════════════════════════════════════════════════════════ @@ -236,6 +264,9 @@ void AcestepAudioProcessor::startGeneration(const juce::String& prompt, juce::ScopedLock l(statusLock_); statusText_ = stateToString(State::Submitting); } + // Clear the log so each generation starts with a fresh output panel. + { juce::ScopedLock l(logLock_); pendingLog_.clear(); } + appendToLog("[" + logTimestamp() + "] Starting generation\xe2\x80\xa6"); triggerAsyncUpdate(); std::thread t(&AcestepAudioProcessor::runGenerationThread, this, prompt, durationSeconds, inferenceSteps, @@ -360,6 +391,9 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio } triggerAsyncUpdate(); + appendToLog("[" + logTimestamp() + "] Running ace-lm (LLM step)\xe2\x80\xa6"); + triggerAsyncUpdate(); + juce::StringArray lmArgs; lmArgs.add(aceLm.getFullPathName()); lmArgs.add("--request"); lmArgs.add(requestFile.getFullPathName()); @@ -373,12 +407,20 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio lastError_ = "Failed to start ace-lm: " + aceLm.getFullPathName(); statusText_ = lastError_; logError(lastError_); + appendToLog("[" + logTimestamp() + "] ERROR: " + lastError_); triggerAsyncUpdate(); tmpDir.deleteRecursively(); return; } lmProc.waitForProcessToFinish(kLmTimeoutMs); + { + // Capture all output produced by ace-lm and stream it to the log panel. + const juce::String lmOutput = lmProc.readAllProcessOutput().trimEnd(); + if (lmOutput.isNotEmpty()) + appendToLog(lmOutput); + } const int lmExit = static_cast(lmProc.getExitCode()); + appendToLog("[" + logTimestamp() + "] ace-lm exit=" + juce::String(lmExit)); logTrace("ace-lm exit=" + juce::String(lmExit)); if (lmExit != 0) { @@ -411,6 +453,7 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio juce::ScopedLock l(statusLock_); statusText_ = stateToString(State::Running); } + appendToLog("[" + logTimestamp() + "] Running ace-synth (DiT+VAE step)\xe2\x80\xa6"); triggerAsyncUpdate(); juce::StringArray ditArgs; @@ -435,12 +478,20 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio lastError_ = "Failed to start ace-synth: " + aceSynth.getFullPathName(); statusText_ = lastError_; logError(lastError_); + appendToLog("[" + logTimestamp() + "] ERROR: " + lastError_); triggerAsyncUpdate(); tmpDir.deleteRecursively(); return; } ditProc.waitForProcessToFinish(kDitTimeoutMs); + { + // Capture all output produced by ace-synth and stream it to the log panel. + const juce::String ditOutput = ditProc.readAllProcessOutput().trimEnd(); + if (ditOutput.isNotEmpty()) + appendToLog(ditOutput); + } const int ditExit = static_cast(ditProc.getExitCode()); + appendToLog("[" + logTimestamp() + "] ace-synth exit=" + juce::String(ditExit)); logTrace("ace-synth exit=" + juce::String(ditExit)); if (ditExit != 0) { @@ -510,6 +561,8 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio addToLibrary(destFile, prompt); logTrace("library file: " + destFile.getFullPathName()); + appendToLog("[" + logTimestamp() + "] Generation complete \xe2\x80\x94 saved to: " + + destFile.getFileName()); { juce::ScopedLock l(pendingLibraryFileLock_); @@ -545,7 +598,13 @@ void AcestepAudioProcessor::previewLibraryEntry(const juce::File& file) return; if (!preview_.loadFile(file)) + { + juce::ScopedLock l(statusLock_); + lastError_ = "Preview failed: could not load " + file.getFileName(); + statusText_ = lastError_; + logError(lastError_); return; + } preview_.play(loopPlayback_.load(std::memory_order_relaxed)); juce::ScopedLock l(statusLock_); diff --git a/ml-bridge/plugin/PluginProcessor.h b/ml-bridge/plugin/PluginProcessor.h index a173deb..84ff945 100644 --- a/ml-bridge/plugin/PluginProcessor.h +++ b/ml-bridge/plugin/PluginProcessor.h @@ -88,6 +88,11 @@ class AcestepAudioProcessor : public juce::AudioProcessor, juce::String getStatusText() const; juce::String getLastError() const; + // ── Streaming log (background thread writes; message thread drains) ─────── + // Returns any new log lines appended since the last call, then clears the + // pending buffer. Safe to call from the message thread at any frequency. + juce::String getAndClearNewLog(); + // ── Host BPM — updated every processBlock() from the DAW playhead ───────── double getHostBpm() const { return hostBpm_.load(std::memory_order_relaxed); } @@ -132,5 +137,11 @@ class AcestepAudioProcessor : public juce::AudioProcessor, mutable juce::CriticalSection pendingLibraryFileLock_; juce::File pendingLibraryFile_; + // ── Streaming log ───────────────────────────────────────────────────────── + // Background thread appends lines; message thread drains via getAndClearNewLog(). + mutable juce::CriticalSection logLock_; + juce::String pendingLog_; + void appendToLog(const juce::String& text); + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AcestepAudioProcessor) }; From 2e1bd4d0cd14a3d70167a20f5fe7b0addb70db67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:23:26 +0000 Subject: [PATCH 3/5] Address code review feedback: fix comment accuracy and platform qualification Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> Agent-Logs-Url: https://github.com/audiohacking/acestep-vst/sessions/7e741f4b-4ac1-4335-adaa-507852c47b24 --- ml-bridge/plugin/PluginEditor.cpp | 14 +++++++------- ml-bridge/plugin/PluginProcessor.cpp | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ml-bridge/plugin/PluginEditor.cpp b/ml-bridge/plugin/PluginEditor.cpp index c195b3b..91beba8 100644 --- a/ml-bridge/plugin/PluginEditor.cpp +++ b/ml-bridge/plugin/PluginEditor.cpp @@ -498,13 +498,12 @@ void AcestepAudioProcessorEditor::timerCallback() // Drain any new log lines from the processor and append them to the log editor. // Do this on every timer tick (4 Hz) so output is visible as soon as possible. + // The logEditor_ itself is cleared in onGenerateClicked() at the start of each + // generation; here we only ever append new content. { juce::String newLog = processorRef.getAndClearNewLog(); if (newLog.isNotEmpty()) { - // Ensure the Generate tab log area is cleared at the start of a new - // generation (the processor clears pendingLog_ at that point so we - // detect it via a "clear" sentinel embedded in the very first chunk). logEditor_.moveCaretToEnd(); logEditor_.insertTextAtCaret(newLog); // Autoscroll to the last line so the user always sees the latest output. @@ -693,10 +692,11 @@ void AcestepAudioProcessorEditor::onInsertDawClicked() if (!file.existsAsFile()) { showFeedback("File not found."); return; } // Initiate an OS-level external drag so the DAW can receive the audio clip. - // performExternalDragDropOfFiles works from a button-click context on all - // platforms (the button's onClick is dispatched inside a mouse-up event). - // If the drag is declined (e.g. desktop is unfocused), fall back to - // revealing the file so the user can drag it manually. + // performExternalDragDropOfFiles is called from a button-click context which + // is dispatched inside a mouse-up event; this works on macOS and Windows. + // On Linux the behaviour depends on the window manager / DAW. + // If the drag is declined, fall back to revealing the file so the user can + // drag it manually. const bool dragOk = juce::DragAndDropContainer::performExternalDragDropOfFiles( juce::StringArray(file.getFullPathName()), /*canMoveFiles=*/false, diff --git a/ml-bridge/plugin/PluginProcessor.cpp b/ml-bridge/plugin/PluginProcessor.cpp index 66215f0..4a02e54 100644 --- a/ml-bridge/plugin/PluginProcessor.cpp +++ b/ml-bridge/plugin/PluginProcessor.cpp @@ -119,6 +119,7 @@ void AcestepAudioProcessor::appendToLog(const juce::String& text) if (text.isEmpty()) return; juce::ScopedLock l(logLock_); pendingLog_ += text; + // Ensure the buffer always ends with a newline so log entries stay separated. if (!pendingLog_.endsWithChar('\n')) pendingLog_ += "\n"; } From 250592fb12e0ecf44b539e205589420624b74ed5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:33:51 +0000 Subject: [PATCH 4/5] Add audio pass-through; clean up clearLog and redundant triggerAsyncUpdate calls Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> Agent-Logs-Url: https://github.com/audiohacking/acestep-vst/sessions/7e741f4b-4ac1-4335-adaa-507852c47b24 --- ml-bridge/plugin/PluginPreview.cpp | 12 ++++++++++-- ml-bridge/plugin/PluginProcessor.cpp | 13 +++++++++---- ml-bridge/plugin/PluginProcessor.h | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ml-bridge/plugin/PluginPreview.cpp b/ml-bridge/plugin/PluginPreview.cpp index 5480e70..db4ecc2 100644 --- a/ml-bridge/plugin/PluginPreview.cpp +++ b/ml-bridge/plugin/PluginPreview.cpp @@ -38,12 +38,20 @@ void PluginPreview::releaseResources() void PluginPreview::render(juce::AudioBuffer& buffer) { // Use a try-lock so the audio thread never blocks waiting for the message - // thread (e.g. during loadFile). If we can't take the lock we just leave - // the buffer as-is (it is already cleared by processBlock). + // thread (e.g. during loadFile). If we can't take the lock, or nothing is + // loaded / playing, leave the buffer as-is so input audio passes through. const juce::ScopedTryLock tryLock(lock_); if (!tryLock.isLocked() || readerSource_ == nullptr) return; + // Only overwrite the buffer while a clip is actually playing. + // AudioTransportSource::getNextAudioBlock fills with silence when stopped, + // which would kill the pass-through signal, so we guard against that here. + if (!transportSource_.isPlaying()) + return; + + // Clear the input first so we output clean preview audio, not a mix. + buffer.clear(); juce::AudioSourceChannelInfo info(&buffer, 0, buffer.getNumSamples()); transportSource_.getNextAudioBlock(info); } diff --git a/ml-bridge/plugin/PluginProcessor.cpp b/ml-bridge/plugin/PluginProcessor.cpp index 4a02e54..5dca5b2 100644 --- a/ml-bridge/plugin/PluginProcessor.cpp +++ b/ml-bridge/plugin/PluginProcessor.cpp @@ -124,6 +124,12 @@ void AcestepAudioProcessor::appendToLog(const juce::String& text) pendingLog_ += "\n"; } +void AcestepAudioProcessor::clearLog() +{ + juce::ScopedLock l(logLock_); + pendingLog_.clear(); +} + juce::String AcestepAudioProcessor::getAndClearNewLog() { juce::ScopedLock l(logLock_); @@ -240,7 +246,8 @@ void AcestepAudioProcessor::processBlock(juce::AudioBuffer& buffer, if (buffer.getNumChannels() == 0) return; - buffer.clear(); + // Pass through the input audio unchanged when preview is not active. + // render() will clear and overwrite the buffer only while a clip is playing. preview_.render(buffer); } @@ -266,7 +273,7 @@ void AcestepAudioProcessor::startGeneration(const juce::String& prompt, statusText_ = stateToString(State::Submitting); } // Clear the log so each generation starts with a fresh output panel. - { juce::ScopedLock l(logLock_); pendingLog_.clear(); } + clearLog(); appendToLog("[" + logTimestamp() + "] Starting generation\xe2\x80\xa6"); triggerAsyncUpdate(); std::thread t(&AcestepAudioProcessor::runGenerationThread, this, @@ -393,7 +400,6 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio triggerAsyncUpdate(); appendToLog("[" + logTimestamp() + "] Running ace-lm (LLM step)\xe2\x80\xa6"); - triggerAsyncUpdate(); juce::StringArray lmArgs; lmArgs.add(aceLm.getFullPathName()); @@ -455,7 +461,6 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio statusText_ = stateToString(State::Running); } appendToLog("[" + logTimestamp() + "] Running ace-synth (DiT+VAE step)\xe2\x80\xa6"); - triggerAsyncUpdate(); juce::StringArray ditArgs; ditArgs.add(aceSynth.getFullPathName()); diff --git a/ml-bridge/plugin/PluginProcessor.h b/ml-bridge/plugin/PluginProcessor.h index 84ff945..6ab036d 100644 --- a/ml-bridge/plugin/PluginProcessor.h +++ b/ml-bridge/plugin/PluginProcessor.h @@ -142,6 +142,7 @@ class AcestepAudioProcessor : public juce::AudioProcessor, mutable juce::CriticalSection logLock_; juce::String pendingLog_; void appendToLog(const juce::String& text); + void clearLog(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AcestepAudioProcessor) }; From 89fe9c2ca19a41ff7f69204477607220b9cfa070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:54:57 +0000 Subject: [PATCH 5/5] Add audio format option (WAV default / MP3) for ace-synth output Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> Agent-Logs-Url: https://github.com/audiohacking/acestep-vst/sessions/509fe7ae-4ed4-4f45-b079-37977d1bc8f9 --- ml-bridge/plugin/PluginEditor.cpp | 37 ++++++++++++++++++++++++++++ ml-bridge/plugin/PluginEditor.h | 2 ++ ml-bridge/plugin/PluginProcessor.cpp | 29 +++++++++++++++++++--- ml-bridge/plugin/PluginProcessor.h | 14 +++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/ml-bridge/plugin/PluginEditor.cpp b/ml-bridge/plugin/PluginEditor.cpp index 91beba8..8f7e8f1 100644 --- a/ml-bridge/plugin/PluginEditor.cpp +++ b/ml-bridge/plugin/PluginEditor.cpp @@ -18,6 +18,23 @@ static const juce::Colour ok { 0xff55cc88 }; static const juce::Colour err { 0xffff6655 }; } // namespace +// ───────────────────────────────────────────────────────────────────────────── +// Audio format combo box helpers +// ───────────────────────────────────────────────────────────────────────────── +static constexpr int kFmtIdWav = 1; +static constexpr int kFmtIdMp3 = 2; + +static AcestepAudioProcessor::AudioFormat audioFormatFromComboId(int id) +{ + return (id == kFmtIdMp3) ? AcestepAudioProcessor::AudioFormat::MP3 + : AcestepAudioProcessor::AudioFormat::WAV; +} + +static int comboIdFromAudioFormat(AcestepAudioProcessor::AudioFormat fmt) +{ + return (fmt == AcestepAudioProcessor::AudioFormat::MP3) ? kFmtIdMp3 : kFmtIdWav; +} + // ───────────────────────────────────────────────────────────────────────────── // Helper: configure a small text button // ───────────────────────────────────────────────────────────────────────────── @@ -399,6 +416,15 @@ AcestepAudioProcessorEditor::AcestepAudioProcessorEditor(AcestepAudioProcessor& juce::dontSendNotification); }; + styleLabel(audioFormatLabel_); + audioFormatLabel_.setText("Output format:", juce::dontSendNotification); + addChildComponent(audioFormatLabel_); + audioFormatCombo_.addItem("WAV (48 kHz, lossless \xe2\x80\x94 recommended)", kFmtIdWav); + audioFormatCombo_.addItem("MP3 (lossy)", kFmtIdMp3); + audioFormatCombo_.setColour(juce::ComboBox::backgroundColourId, AcestepColours::panel); + audioFormatCombo_.setColour(juce::ComboBox::textColourId, AcestepColours::textMain); + addChildComponent(audioFormatCombo_); + styleSmallButton(applySettingsButton_, AcestepColours::accent, AcestepColours::accent.brighter(0.2f)); applySettingsButton_.onClick = [this] { onApplySettingsClicked(); }; @@ -420,6 +446,9 @@ AcestepAudioProcessorEditor::AcestepAudioProcessorEditor(AcestepAudioProcessor& binPathEditor_.setText(processorRef.getBinariesPath(), juce::dontSendNotification); modelsPathEditor_.setText(processorRef.getModelsPath(), juce::dontSendNotification); outputPathEditor_.setText(processorRef.getOutputPath(), juce::dontSendNotification); + audioFormatCombo_.setSelectedId( + comboIdFromAudioFormat(processorRef.getAudioFormat()), + juce::dontSendNotification); refreshLibraryCache(); // Open Settings on first run (binaries not yet configured) so the user can @@ -458,6 +487,9 @@ void AcestepAudioProcessorEditor::selectTab(int tab) binPathEditor_.setText(processorRef.getBinariesPath(), juce::dontSendNotification); modelsPathEditor_.setText(processorRef.getModelsPath(), juce::dontSendNotification); outputPathEditor_.setText(processorRef.getOutputPath(), juce::dontSendNotification); + audioFormatCombo_.setSelectedId( + comboIdFromAudioFormat(processorRef.getAudioFormat()), + juce::dontSendNotification); } resized(); repaint(); @@ -740,6 +772,7 @@ void AcestepAudioProcessorEditor::onApplySettingsClicked() processorRef.setBinariesPath(binPathEditor_.getText().trim()); processorRef.setModelsPath(modelsPathEditor_.getText().trim()); processorRef.setOutputPath(outputPathEditor_.getText().trim()); + processorRef.setAudioFormat(audioFormatFromComboId(audioFormatCombo_.getSelectedId())); showFeedback("Settings applied."); // Refresh library in case output path changed refreshLibraryCache(); @@ -889,6 +922,7 @@ void AcestepAudioProcessorEditor::hideAllTabComponents() &binPathLabel_, &binDetectedLabel_, &binPathEditor_, &binBrowseButton_, &modelsPathLabel_, &modelsPathEditor_, &modelsBrowseButton_, &outputPathLabel_, &outputPathEditor_, &outputBrowseButton_, + &audioFormatLabel_, &audioFormatCombo_, &applySettingsButton_, &settingsInfoLabel_ }) { c->setVisible(false); @@ -1033,6 +1067,9 @@ void AcestepAudioProcessorEditor::layoutSettingsTab(juce::Rectangle r) outputBrowseButton_.setVisible(true); outputBrowseButton_.setBounds(row.removeFromRight(80)); row.removeFromRight(4); outputPathEditor_.setVisible(true); outputPathEditor_.setBounds(row); + audioFormatLabel_.setVisible(true); audioFormatLabel_.setBounds(labelRow()); + audioFormatCombo_.setVisible(true); audioFormatCombo_.setBounds(fieldRow()); + applySettingsButton_.setVisible(true); applySettingsButton_.setBounds(r.removeFromTop(28)); r.removeFromTop(8); diff --git a/ml-bridge/plugin/PluginEditor.h b/ml-bridge/plugin/PluginEditor.h index 89c383d..f45ef36 100644 --- a/ml-bridge/plugin/PluginEditor.h +++ b/ml-bridge/plugin/PluginEditor.h @@ -153,6 +153,8 @@ class AcestepAudioProcessorEditor : public juce::AudioProcessorEditor, juce::Label outputPathLabel_; juce::TextEditor outputPathEditor_; juce::TextButton outputBrowseButton_{ "Browse\xe2\x80\xa6" }; + juce::Label audioFormatLabel_; + juce::ComboBox audioFormatCombo_; juce::TextButton applySettingsButton_{ "Apply Settings" }; juce::Label settingsInfoLabel_; diff --git a/ml-bridge/plugin/PluginProcessor.cpp b/ml-bridge/plugin/PluginProcessor.cpp index 5dca5b2..daa910c 100644 --- a/ml-bridge/plugin/PluginProcessor.cpp +++ b/ml-bridge/plugin/PluginProcessor.cpp @@ -172,6 +172,16 @@ juce::String AcestepAudioProcessor::getOutputPath() const juce::ScopedLock l(pathsLock_); return outputPath_; } +void AcestepAudioProcessor::setAudioFormat(AudioFormat fmt) +{ + { juce::ScopedLock l(pathsLock_); audioFormat_ = fmt; } + saveSettingsToGlobalConfig(); +} +AcestepAudioProcessor::AudioFormat AcestepAudioProcessor::getAudioFormat() const +{ + juce::ScopedLock l(pathsLock_); return audioFormat_; +} + juce::File AcestepAudioProcessor::getBundledBinariesDirectory() { // The CI build embeds ace-lm and ace-synth alongside the plugin's own binary @@ -295,7 +305,8 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio { // ── 1. Resolve binary and model directories ─────────────────────────────── juce::String bp, mp; - { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; } + AudioFormat fmt; + { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; fmt = audioFormat_; } juce::File binDir = bp.isEmpty() ? getBundledBinariesDirectory() : juce::File(bp); @@ -469,6 +480,11 @@ void AcestepAudioProcessor::runGenerationThread(juce::String prompt, int duratio ditArgs.add("--dit"); ditArgs.add(ditModel.getFullPathName()); ditArgs.add("--vae"); ditArgs.add(vaeModel.getFullPathName()); + // Request WAV output (48 kHz PCM) when the user has chosen WAV format. + // Without this flag ace-synth produces MP3 by default. + if (fmt == AudioFormat::WAV) + ditArgs.add("--wav"); + // Pass source audio for cover / repaint mode if (coverFile.existsAsFile()) { @@ -709,11 +725,13 @@ bool AcestepAudioProcessor::importAudioFile(const juce::File& sourceFile) void AcestepAudioProcessor::saveSettingsToGlobalConfig() const { juce::String bp, mp, op; - { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; op = outputPath_; } + AudioFormat fmt; + { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; op = outputPath_; fmt = audioFormat_; } auto xml = std::make_unique("AcestepConfig"); xml->setAttribute("binariesPath", bp); xml->setAttribute("modelsPath", mp); xml->setAttribute("outputPath", op); + xml->setAttribute("audioFormat", fmt == AudioFormat::WAV ? "wav" : "mp3"); juce::File cfg = getGlobalConfigFile(); cfg.getParentDirectory().createDirectory(); xml->writeTo(cfg); @@ -730,6 +748,8 @@ void AcestepAudioProcessor::loadSettingsFromGlobalConfig() binariesPath_ = xml->getStringAttribute("binariesPath"); modelsPath_ = xml->getStringAttribute("modelsPath"); outputPath_ = xml->getStringAttribute("outputPath"); + // Default to WAV when loading an old config that has no audioFormat key. + audioFormat_ = parseAudioFormat(xml->getStringAttribute("audioFormat", "wav")); logTrace("Global config loaded"); } } @@ -780,11 +800,13 @@ juce::AudioProcessorEditor* AcestepAudioProcessor::createEditor() void AcestepAudioProcessor::getStateInformation(juce::MemoryBlock& destData) { juce::String bp, mp, op; - { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; op = outputPath_; } + AudioFormat fmt; + { juce::ScopedLock l(pathsLock_); bp = binariesPath_; mp = modelsPath_; op = outputPath_; fmt = audioFormat_; } auto xml = std::make_unique("AcestepState"); xml->setAttribute("binariesPath", bp); xml->setAttribute("modelsPath", mp); xml->setAttribute("outputPath", op); + xml->setAttribute("audioFormat", fmt == AudioFormat::WAV ? "wav" : "mp3"); copyXmlToBinary(*xml, destData); } @@ -798,6 +820,7 @@ void AcestepAudioProcessor::setStateInformation(const void* data, int sizeInByte binariesPath_ = xml->getStringAttribute("binariesPath"); modelsPath_ = xml->getStringAttribute("modelsPath"); outputPath_ = xml->getStringAttribute("outputPath"); + audioFormat_ = parseAudioFormat(xml->getStringAttribute("audioFormat", "wav")); } // Keep global config in sync so paths survive fresh projects too. saveSettingsToGlobalConfig(); diff --git a/ml-bridge/plugin/PluginProcessor.h b/ml-bridge/plugin/PluginProcessor.h index 6ab036d..e0cff39 100644 --- a/ml-bridge/plugin/PluginProcessor.h +++ b/ml-bridge/plugin/PluginProcessor.h @@ -64,6 +64,14 @@ class AcestepAudioProcessor : public juce::AudioProcessor, const juce::String& lyrics = "[Instrumental]", int seed = -1); + // ── Audio format ────────────────────────────────────────────────────────── + // WAV → passes --wav to ace-synth (48 kHz PCM, lossless) + // MP3 → default ace-synth output (lossy) + enum class AudioFormat { WAV = 0, MP3 = 1 }; + + void setAudioFormat(AudioFormat fmt); + AudioFormat getAudioFormat() const; + // ── Path configuration (persisted in DAW project state) ────────────────── void setBinariesPath(const juce::String& path); juce::String getBinariesPath() const; @@ -117,6 +125,7 @@ class AcestepAudioProcessor : public juce::AudioProcessor, // ── Settings paths ──────────────────────────────────────────────────────── mutable juce::CriticalSection pathsLock_; juce::String binariesPath_, modelsPath_, outputPath_; + AudioFormat audioFormat_{ AudioFormat::WAV }; // default WAV (48 kHz) // ── State machine ───────────────────────────────────────────────────────── std::atomic state_{ State::Idle }; @@ -144,5 +153,10 @@ class AcestepAudioProcessor : public juce::AudioProcessor, void appendToLog(const juce::String& text); void clearLog(); + static AudioFormat parseAudioFormat(const juce::String& s) + { + return (s == "mp3") ? AudioFormat::MP3 : AudioFormat::WAV; + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AcestepAudioProcessor) };