Skip to content
Merged
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
118 changes: 91 additions & 27 deletions ml-bridge/plugin/PluginEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -276,11 +293,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_);
Expand Down Expand Up @@ -394,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(); };
Expand All @@ -415,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
Expand Down Expand Up @@ -453,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();
Expand Down Expand Up @@ -491,6 +528,21 @@ 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.
// 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())
{
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)
Expand Down Expand Up @@ -520,29 +572,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
Expand Down Expand Up @@ -586,6 +630,9 @@ void AcestepAudioProcessorEditor::onGenerateClicked()
coverStrength = static_cast<float>(coverStrengthSlider_.getValue());
}

// Clear the log editor now so the new generation starts fresh.
logEditor_.clear();

processorRef.startGeneration(
promptEditor_.getText(),
durationSec > 0 ? durationSec : 10,
Expand Down Expand Up @@ -676,12 +723,24 @@ void AcestepAudioProcessorEditor::onInsertDawClicked()
const juce::File& file = libraryCache_[static_cast<size_t>(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 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,
&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()
Expand Down Expand Up @@ -713,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();
Expand Down Expand Up @@ -852,7 +912,7 @@ void AcestepAudioProcessorEditor::hideAllTabComponents()
&genModeButton_, &coverModeButton_,
&refFileLabel_, &browseRefButton_, &clearRefButton_,
&coverStrengthLabel_, &coverStrengthSlider_,
&generateButton_, &statusLabel_,
&generateButton_, &logEditor_,
&refreshLibButton_, &importButton_,
&libraryList_,
&previewButton_, &stopButton_, &loopButton_,
Expand All @@ -862,6 +922,7 @@ void AcestepAudioProcessorEditor::hideAllTabComponents()
&binPathLabel_, &binDetectedLabel_, &binPathEditor_, &binBrowseButton_,
&modelsPathLabel_, &modelsPathEditor_, &modelsBrowseButton_,
&outputPathLabel_, &outputPathEditor_, &outputBrowseButton_,
&audioFormatLabel_, &audioFormatCombo_,
&applySettingsButton_, &settingsInfoLabel_ })
{
c->setVisible(false);
Expand Down Expand Up @@ -952,9 +1013,9 @@ void AcestepAudioProcessorEditor::layoutGenerateTab(juce::Rectangle<int> 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<int> r)
Expand Down Expand Up @@ -1006,6 +1067,9 @@ void AcestepAudioProcessorEditor::layoutSettingsTab(juce::Rectangle<int> 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);

Expand Down
6 changes: 5 additions & 1 deletion ml-bridge/plugin/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down Expand Up @@ -151,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_;

Expand Down
16 changes: 14 additions & 2 deletions ml-bridge/plugin/PluginPreview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -34,12 +38,20 @@ void PluginPreview::releaseResources()
void PluginPreview::render(juce::AudioBuffer<float>& 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);
}
Expand Down
Loading
Loading