From 699138adec214d960a1e909b804e61d4b10224f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Oct 2025 19:41:05 +0000 Subject: [PATCH 1/2] Add level meters and improve timer responsiveness Co-authored-by: eric --- Source/PluginEditor.cpp | 44 ++++++++++++- Source/PluginEditor.h | 122 +++++++++++++++++++++++++++++++++++++ Source/PluginProcessor.cpp | 31 ++++++++++ Source/PluginProcessor.h | 8 +++ 4 files changed, 202 insertions(+), 3 deletions(-) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 89d6dc6..707cb9c 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -4,7 +4,7 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p) : AudioProcessorEditor(&p), audioProcessor(p) { - setSize(400, 500); + setSize(500, 500); titleLabel.setText("FreeCaster", juce::dontSendNotification); titleLabel.setFont(juce::Font(24.0f, juce::Font::bold)); @@ -41,6 +41,20 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p) bufferHealthLabel.setFont(juce::Font(11.0f)); bufferHealthLabel.setColour(juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible(bufferHealthLabel); + + // Set up level meters + addAndMakeVisible(inputMeter); + addAndMakeVisible(outputMeter); + + inputMeterLabel.setText("Input", juce::dontSendNotification); + inputMeterLabel.setJustificationType(juce::Justification::centred); + inputMeterLabel.setFont(juce::Font(11.0f)); + addAndMakeVisible(inputMeterLabel); + + outputMeterLabel.setText("Output", juce::dontSendNotification); + outputMeterLabel.setJustificationType(juce::Justification::centred); + outputMeterLabel.setFont(juce::Font(11.0f)); + addAndMakeVisible(outputMeterLabel); // Set up error callbacks audioProcessor.getAirPlayManager().onError = [this](const juce::String& error) @@ -56,7 +70,7 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p) audioProcessor.getDeviceDiscovery().addListener(this); updateDeviceList(); - startTimer(500); // Update more frequently for better responsiveness + startTimer(16); // ~60Hz for smooth meter updates } AirPlayPluginEditor::~AirPlayPluginEditor() @@ -72,6 +86,10 @@ void AirPlayPluginEditor::paint(juce::Graphics& g) void AirPlayPluginEditor::resized() { auto area = getLocalBounds().reduced(10); + + // Reserve space for meters on the right + auto metersArea = area.removeFromRight(100); + area.removeFromRight(10); // Gap titleLabel.setBounds(area.removeFromTop(40)); area.removeFromTop(5); @@ -89,15 +107,32 @@ void AirPlayPluginEditor::resized() area.removeFromTop(10); auto buttonArea = area.removeFromTop(40); - connectButton.setBounds(buttonArea.removeFromLeft(180)); + connectButton.setBounds(buttonArea.removeFromLeft(165)); buttonArea.removeFromLeft(10); disconnectButton.setBounds(buttonArea); + + // Layout meters vertically on the right side + metersArea.removeFromTop(40); // Align with title + + auto inputMeterArea = metersArea.removeFromLeft(40); + metersArea.removeFromLeft(10); + auto outputMeterArea = metersArea.removeFromLeft(40); + + inputMeterLabel.setBounds(inputMeterArea.removeFromTop(20)); + inputMeter.setBounds(inputMeterArea.removeFromTop(340)); + + outputMeterLabel.setBounds(outputMeterArea.removeFromTop(20)); + outputMeter.setBounds(outputMeterArea.removeFromTop(340)); } void AirPlayPluginEditor::timerCallback() { updateStatusDisplay(); updateBufferHealth(); + + // Update level meters + inputMeter.setLevel(audioProcessor.getInputLevel()); + outputMeter.setLevel(audioProcessor.getOutputLevel()); } void AirPlayPluginEditor::updateStatusDisplay() @@ -208,4 +243,7 @@ void AirPlayPluginEditor::connectButtonClicked() void AirPlayPluginEditor::disconnectButtonClicked() { audioProcessor.getAirPlayManager().disconnectFromDevice(); + + // Reset meters when disconnected + outputMeter.reset(); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index a369fd0..cd5761f 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -3,6 +3,122 @@ #include #include "PluginProcessor.h" +//============================================================================== +// Professional vertical level meter component +class LevelMeter : public juce::Component +{ +public: + LevelMeter() + { + setOpaque(false); + } + + void paint(juce::Graphics& g) override + { + auto bounds = getLocalBounds().toFloat(); + + // Draw background + g.setColour(juce::Colour(0xFF1a1a1a)); + g.fillRoundedRectangle(bounds, 4.0f); + + // Draw scale markings + g.setColour(juce::Colour(0xFF404040)); + g.setFont(9.0f); + + const float dbMarks[] = { 0.0f, -6.0f, -12.0f, -24.0f, -48.0f }; + for (auto db : dbMarks) + { + float yPos = dbToY(db, bounds.getHeight()); + g.drawLine(bounds.getX(), yPos, bounds.getX() + 3, yPos, 1.0f); + g.drawText(juce::String((int)db), bounds.getX() + bounds.getWidth() + 2, yPos - 6, 30, 12, + juce::Justification::left, false); + } + + // Calculate meter height + float meterHeight = bounds.getHeight() * currentLevel; + float peakHeight = bounds.getHeight() * peakLevel; + + if (meterHeight > 0.0f) + { + auto meterBounds = bounds.withTop(bounds.getBottom() - meterHeight).reduced(2.0f); + + // Color gradient based on level + juce::ColourGradient gradient( + juce::Colour(0xFF00ff00), meterBounds.getX(), meterBounds.getBottom(), + juce::Colour(0xFFffff00), meterBounds.getX(), meterBounds.getY(), + false); + + gradient.addColour(0.7, juce::Colour(0xFF00ff00)); // Green + gradient.addColour(0.85, juce::Colour(0xFFffff00)); // Yellow + gradient.addColour(1.0, juce::Colour(0xFFff0000)); // Red + + g.setGradientFill(gradient); + g.fillRoundedRectangle(meterBounds, 2.0f); + } + + // Draw peak hold indicator + if (peakLevel > 0.0f) + { + float peakY = bounds.getBottom() - peakHeight; + g.setColour(peakLevel > 0.9f ? juce::Colours::red : juce::Colours::white); + g.fillRect(bounds.getX() + 1, peakY - 1, bounds.getWidth() - 2, 2.0f); + } + + // Draw border + g.setColour(juce::Colour(0xFF505050)); + g.drawRoundedRectangle(bounds, 4.0f, 1.0f); + } + + void setLevel(float newLevel) + { + // Convert to normalized 0-1 range (assuming input is in 0-1 linear amplitude) + currentLevel = juce::jlimit(0.0f, 1.0f, newLevel); + + // Update peak hold + if (currentLevel > peakLevel) + { + peakLevel = currentLevel; + peakHoldCounter = 60; // Hold for 60 frames (~1 second at 60Hz) + } + else if (peakHoldCounter > 0) + { + peakHoldCounter--; + } + else + { + // Decay peak + peakLevel *= 0.95f; + if (peakLevel < 0.001f) + peakLevel = 0.0f; + } + + repaint(); + } + + void reset() + { + currentLevel = 0.0f; + peakLevel = 0.0f; + peakHoldCounter = 0; + repaint(); + } + +private: + float dbToY(float db, float height) const + { + // Map dB to Y position (0dB at top, -inf at bottom) + float normalized = (db + 60.0f) / 60.0f; // -60dB to 0dB range + return height * (1.0f - juce::jlimit(0.0f, 1.0f, normalized)); + } + + float currentLevel = 0.0f; + float peakLevel = 0.0f; + int peakHoldCounter = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LevelMeter) +}; + +//============================================================================== class AirPlayPluginEditor : public juce::AudioProcessorEditor, public juce::Timer, public DeviceDiscovery::Listener @@ -40,6 +156,12 @@ class AirPlayPluginEditor : public juce::AudioProcessorEditor, juce::Label errorLabel; juce::Label bufferHealthLabel; + // Level meters + LevelMeter inputMeter; + LevelMeter outputMeter; + juce::Label inputMeterLabel; + juce::Label outputMeterLabel; + class DeviceListModel : public juce::ListBoxModel { public: diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index b15a9b7..8713a0a 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -54,9 +54,40 @@ void AirPlayPluginProcessor::processBlock(juce::AudioBuffer& buffer, juce { juce::ScopedNoDenormals noDenormals; + // Calculate input level (RMS) + float inputRMS = 0.0f; + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + { + const float* channelData = buffer.getReadPointer(channel); + float channelRMS = 0.0f; + + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + { + channelRMS += channelData[sample] * channelData[sample]; + } + + channelRMS = std::sqrt(channelRMS / buffer.getNumSamples()); + inputRMS = juce::jmax(inputRMS, channelRMS); + } + + // Convert RMS to normalized level (0-1) with some headroom + // RMS of 0.7 (~-3dB) maps to full scale + float normalizedInput = juce::jlimit(0.0f, 1.0f, inputRMS / 0.7f); + inputLevel.store(normalizedInput); + + // Send to AirPlay if connected if (airPlayManager.isConnected()) { airPlayManager.pushAudioData(buffer, buffer.getNumSamples()); + + // Output level is the same as input for a pass-through plugin + // (In a real scenario with processing, this might differ) + outputLevel.store(normalizedInput); + } + else + { + // Reset output level when not connected + outputLevel.store(0.0f); } } diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 8bcdedf..e1d412f 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -39,9 +39,17 @@ class AirPlayPluginProcessor : public juce::AudioProcessor AirPlayManager& getAirPlayManager() { return airPlayManager; } DeviceDiscovery& getDeviceDiscovery() { return deviceDiscovery; } + // Level meter accessors + float getInputLevel() const { return inputLevel.load(); } + float getOutputLevel() const { return outputLevel.load(); } + private: AirPlayManager airPlayManager; DeviceDiscovery deviceDiscovery; + // Level tracking for meters (thread-safe) + std::atomic inputLevel{ 0.0f }; + std::atomic outputLevel{ 0.0f }; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AirPlayPluginProcessor) }; From c23a710288e7df8b0f549f74cebf8d364eae6856 Mon Sep 17 00:00:00 2001 From: Eric Dahl <132486009+edahl_UND@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:05:38 -0400 Subject: [PATCH 2/2] update --- Source/PluginEditor.cpp | 84 ++++++++++++++++++++++---------- Source/PluginEditor.h | 104 +++++++++++++++++++++------------------- test_plugin.py | 53 ++++++++++++++++++++ 3 files changed, 168 insertions(+), 73 deletions(-) create mode 100644 test_plugin.py diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 707cb9c..1467745 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -4,7 +4,10 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p) : AudioProcessorEditor(&p), audioProcessor(p) { - setSize(500, 500); + // Set a reasonable size that works for both standalone and plugin contexts + setSize(600, 500); + setResizable(true, true); + setResizeLimits(400, 400, 800, 600); titleLabel.setText("FreeCaster", juce::dontSendNotification); titleLabel.setFont(juce::Font(24.0f, juce::Font::bold)); @@ -41,27 +44,38 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p) bufferHealthLabel.setFont(juce::Font(11.0f)); bufferHealthLabel.setColour(juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible(bufferHealthLabel); - + // Set up level meters addAndMakeVisible(inputMeter); addAndMakeVisible(outputMeter); - + + // Ensure meters are visible + inputMeter.setVisible(true); + outputMeter.setVisible(true); + inputMeterLabel.setText("Input", juce::dontSendNotification); inputMeterLabel.setJustificationType(juce::Justification::centred); inputMeterLabel.setFont(juce::Font(11.0f)); addAndMakeVisible(inputMeterLabel); - + outputMeterLabel.setText("Output", juce::dontSendNotification); outputMeterLabel.setJustificationType(juce::Justification::centred); outputMeterLabel.setFont(juce::Font(11.0f)); addAndMakeVisible(outputMeterLabel); + // Test audio button + testAudioButton.setButtonText("Test Meters"); + testAudioButton.onClick = [this] { + testLevel = (testLevel > 0.0f) ? 0.0f : 0.75f; + }; + addAndMakeVisible(testAudioButton); + // Set up error callbacks audioProcessor.getAirPlayManager().onError = [this](const juce::String& error) { showError(error); }; - + audioProcessor.getAirPlayManager().onStatusChange = [this](const juce::String& status) { showStatus(status); @@ -86,7 +100,7 @@ void AirPlayPluginEditor::paint(juce::Graphics& g) void AirPlayPluginEditor::resized() { auto area = getLocalBounds().reduced(10); - + // Reserve space for meters on the right auto metersArea = area.removeFromRight(100); area.removeFromRight(10); // Gap @@ -96,10 +110,10 @@ void AirPlayPluginEditor::resized() statusLabel.setBounds(area.removeFromTop(25)); area.removeFromTop(3); - + errorLabel.setBounds(area.removeFromTop(20)); area.removeFromTop(3); - + bufferHealthLabel.setBounds(area.removeFromTop(20)); area.removeFromTop(10); @@ -107,45 +121,67 @@ void AirPlayPluginEditor::resized() area.removeFromTop(10); auto buttonArea = area.removeFromTop(40); - connectButton.setBounds(buttonArea.removeFromLeft(165)); - buttonArea.removeFromLeft(10); - disconnectButton.setBounds(buttonArea); - + connectButton.setBounds(buttonArea.removeFromLeft(110)); + buttonArea.removeFromLeft(5); + disconnectButton.setBounds(buttonArea.removeFromLeft(110)); + buttonArea.removeFromLeft(5); + testAudioButton.setBounds(buttonArea); + // Layout meters vertically on the right side metersArea.removeFromTop(40); // Align with title - + auto inputMeterArea = metersArea.removeFromLeft(40); metersArea.removeFromLeft(10); auto outputMeterArea = metersArea.removeFromLeft(40); - + inputMeterLabel.setBounds(inputMeterArea.removeFromTop(20)); - inputMeter.setBounds(inputMeterArea.removeFromTop(340)); - + inputMeter.setBounds(inputMeterArea); // Use remaining height + outputMeterLabel.setBounds(outputMeterArea.removeFromTop(20)); - outputMeter.setBounds(outputMeterArea.removeFromTop(340)); + outputMeter.setBounds(outputMeterArea); // Use remaining height + + // Debug: Log meter bounds + DBG("=== RESIZE DEBUG ==="); + DBG("Window size: " + juce::String(getWidth()) + "x" + juce::String(getHeight())); + DBG("Input meter bounds: " + inputMeter.getBounds().toString()); + DBG("Output meter bounds: " + outputMeter.getBounds().toString()); + DBG("Input meter visible: " + juce::String(inputMeter.isVisible())); + DBG("Output meter visible: " + juce::String(outputMeter.isVisible())); + DBG("Meters area initial: " + juce::String(metersArea.getWidth()) + "x" + juce::String(metersArea.getHeight())); } void AirPlayPluginEditor::timerCallback() { updateStatusDisplay(); updateBufferHealth(); - + // Update level meters - inputMeter.setLevel(audioProcessor.getInputLevel()); - outputMeter.setLevel(audioProcessor.getOutputLevel()); + float inputLevel = (testLevel > 0.0f) ? testLevel : audioProcessor.getInputLevel(); + float outputLevel = (testLevel > 0.0f) ? testLevel : audioProcessor.getOutputLevel(); + + inputMeter.setLevel(inputLevel); + outputMeter.setLevel(outputLevel); + + // Debug: Log levels occasionally + static int debugCounter = 0; + if (++debugCounter % 60 == 0) // Log every second + { + DBG("Input level: " + juce::String(inputLevel) + ", Output level: " + juce::String(outputLevel)); + DBG("Test level: " + juce::String(testLevel)); + } } void AirPlayPluginEditor::updateStatusDisplay() { auto& manager = audioProcessor.getAirPlayManager(); - + if (manager.isConnected()) { statusLabel.setText(manager.getConnectionStatus(), juce::dontSendNotification); statusLabel.setColour(juce::Label::textColourId, juce::Colours::lightgreen); connectButton.setEnabled(false); disconnectButton.setEnabled(true); - + // Clear error if connected successfully juce::String lastError = manager.getLastError(); if (lastError.isEmpty()) @@ -157,7 +193,7 @@ void AirPlayPluginEditor::updateStatusDisplay() statusLabel.setColour(juce::Label::textColourId, juce::Colours::white); connectButton.setEnabled(true); disconnectButton.setEnabled(false); - + // Show error if present juce::String lastError = manager.getLastError(); if (lastError.isNotEmpty()) @@ -243,7 +279,7 @@ void AirPlayPluginEditor::connectButtonClicked() void AirPlayPluginEditor::disconnectButtonClicked() { audioProcessor.getAirPlayManager().disconnectFromDevice(); - + // Reset meters when disconnected outputMeter.reset(); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index cd5761f..57f581f 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -10,70 +10,74 @@ class LevelMeter : public juce::Component public: LevelMeter() { - setOpaque(false); + // Make sure the meter is visible + setOpaque(true); + setVisible(true); } - + void paint(juce::Graphics& g) override { auto bounds = getLocalBounds().toFloat(); - - // Draw background - g.setColour(juce::Colour(0xFF1a1a1a)); - g.fillRoundedRectangle(bounds, 4.0f); - - // Draw scale markings - g.setColour(juce::Colour(0xFF404040)); - g.setFont(9.0f); - + + // ALWAYS draw a visible background for debugging + g.fillAll(juce::Colour(0xFF3a3a3a)); + + // Draw a bright border for visibility + g.setColour(juce::Colours::cyan); + g.drawRect(bounds, 3.0f); + + // Draw scale markings on the meter itself + g.setColour(juce::Colour(0xFF606060)); + g.setFont(8.0f); + const float dbMarks[] = { 0.0f, -6.0f, -12.0f, -24.0f, -48.0f }; for (auto db : dbMarks) { float yPos = dbToY(db, bounds.getHeight()); - g.drawLine(bounds.getX(), yPos, bounds.getX() + 3, yPos, 1.0f); - g.drawText(juce::String((int)db), bounds.getX() + bounds.getWidth() + 2, yPos - 6, 30, 12, - juce::Justification::left, false); + g.drawLine(2, yPos, 8, yPos, 1.0f); } - + // Calculate meter height float meterHeight = bounds.getHeight() * currentLevel; float peakHeight = bounds.getHeight() * peakLevel; - + if (meterHeight > 0.0f) { - auto meterBounds = bounds.withTop(bounds.getBottom() - meterHeight).reduced(2.0f); - + auto meterBounds = bounds.withTop(bounds.getBottom() - meterHeight).reduced(3.0f); + // Color gradient based on level juce::ColourGradient gradient( juce::Colour(0xFF00ff00), meterBounds.getX(), meterBounds.getBottom(), juce::Colour(0xFFffff00), meterBounds.getX(), meterBounds.getY(), false); - + gradient.addColour(0.7, juce::Colour(0xFF00ff00)); // Green gradient.addColour(0.85, juce::Colour(0xFFffff00)); // Yellow gradient.addColour(1.0, juce::Colour(0xFFff0000)); // Red - + g.setGradientFill(gradient); - g.fillRoundedRectangle(meterBounds, 2.0f); + g.fillRect(meterBounds); } - + // Draw peak hold indicator if (peakLevel > 0.0f) { float peakY = bounds.getBottom() - peakHeight; g.setColour(peakLevel > 0.9f ? juce::Colours::red : juce::Colours::white); - g.fillRect(bounds.getX() + 1, peakY - 1, bounds.getWidth() - 2, 2.0f); + g.fillRect(3.0f, peakY - 1, bounds.getWidth() - 6, 2.0f); } - - // Draw border - g.setColour(juce::Colour(0xFF505050)); - g.drawRoundedRectangle(bounds, 4.0f, 1.0f); + + // Debug text + g.setColour(juce::Colours::white); + g.setFont(9.0f); + g.drawText(juce::String(currentLevel, 2), bounds.reduced(2), juce::Justification::centredTop, false); } - + void setLevel(float newLevel) { // Convert to normalized 0-1 range (assuming input is in 0-1 linear amplitude) currentLevel = juce::jlimit(0.0f, 1.0f, newLevel); - + // Update peak hold if (currentLevel > peakLevel) { @@ -91,10 +95,10 @@ class LevelMeter : public juce::Component if (peakLevel < 0.001f) peakLevel = 0.0f; } - + repaint(); } - + void reset() { currentLevel = 0.0f; @@ -102,7 +106,7 @@ class LevelMeter : public juce::Component peakHoldCounter = 0; repaint(); } - + private: float dbToY(float db, float height) const { @@ -110,11 +114,11 @@ class LevelMeter : public juce::Component float normalized = (db + 60.0f) / 60.0f; // -60dB to 0dB range return height * (1.0f - juce::jlimit(0.0f, 1.0f, normalized)); } - + float currentLevel = 0.0f; float peakLevel = 0.0f; int peakHoldCounter = 0; - + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LevelMeter) }; @@ -126,15 +130,15 @@ class AirPlayPluginEditor : public juce::AudioProcessorEditor, public: AirPlayPluginEditor(AirPlayPluginProcessor&); ~AirPlayPluginEditor() override; - + void paint(juce::Graphics&) override; void resized() override; - + private: void timerCallback() override; void deviceFound(const AirPlayDevice& device) override; void deviceLost(const AirPlayDevice& device) override; - + void updateDeviceList(); void connectButtonClicked(); void disconnectButtonClicked(); @@ -142,45 +146,47 @@ class AirPlayPluginEditor : public juce::AudioProcessorEditor, void updateBufferHealth(); void showError(const juce::String& error); void showStatus(const juce::String& status); - + AirPlayPluginProcessor& audioProcessor; - + juce::ListBox deviceListBox; juce::StringArray deviceNames; juce::Array devices; - + juce::TextButton connectButton; juce::TextButton disconnectButton; juce::Label statusLabel; juce::Label titleLabel; juce::Label errorLabel; juce::Label bufferHealthLabel; - + // Level meters LevelMeter inputMeter; LevelMeter outputMeter; juce::Label inputMeterLabel; juce::Label outputMeterLabel; - + juce::TextButton testAudioButton; + float testLevel = 0.0f; + class DeviceListModel : public juce::ListBoxModel { public: DeviceListModel(AirPlayPluginEditor& e) : editor(e) {} - + int getNumRows() override { return editor.deviceNames.size(); } - + void paintListBoxItem(int rowNumber, juce::Graphics& g, int width, int height, bool rowIsSelected) override { if (rowIsSelected) g.fillAll(juce::Colours::lightblue); - + g.setColour(juce::Colours::black); g.setFont(14.0f); - + if (rowNumber < editor.deviceNames.size()) { g.drawText(editor.deviceNames[rowNumber], @@ -188,12 +194,12 @@ class AirPlayPluginEditor : public juce::AudioProcessorEditor, juce::Justification::centredLeft, true); } } - + private: AirPlayPluginEditor& editor; }; - + std::unique_ptr listModel; - + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AirPlayPluginEditor) }; diff --git a/test_plugin.py b/test_plugin.py new file mode 100644 index 0000000..32b1f1b --- /dev/null +++ b/test_plugin.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Simple test script to verify FreeCaster plugin loads correctly +""" +import subprocess +import sys +import os + +def test_plugin(): + print("Testing FreeCaster Plugin...") + + # Check if VST3 plugin exists + vst3_path = "/Users/edahl/Library/Audio/Plug-Ins/VST3/FreeCaster.vst3" + if os.path.exists(vst3_path): + print("✅ VST3 plugin found") + else: + print("❌ VST3 plugin not found") + return False + + # Check if AU plugin exists + au_path = "/Users/edahl/Library/Audio/Plug-Ins/Components/FreeCaster.component" + if os.path.exists(au_path): + print("✅ AU plugin found") + else: + print("❌ AU plugin not found") + return False + + # Check if standalone app exists + standalone_path = "/Users/edahl/FreeCaster/build/FreeCaster_artefacts/Standalone/FreeCaster.app" + if os.path.exists(standalone_path): + print("✅ Standalone app found") + else: + print("❌ Standalone app not found") + return False + + print("\n🎵 Plugin Installation Summary:") + print(" • VST3: Available for Ableton Live, Logic Pro, etc.") + print(" • AU: Available for Logic Pro, GarageBand, etc.") + print(" • Standalone: Available for direct testing") + + print("\n📋 Testing Instructions for Ableton Live:") + print(" 1. Open Ableton Live 11") + print(" 2. Create a new audio track") + print(" 3. Add FreeCaster as an Audio Effect") + print(" 4. Look for two vertical meters on the right side") + print(" 5. Click 'Test Meters' button to verify functionality") + + return True + +if __name__ == "__main__": + success = test_plugin() + sys.exit(0 if success else 1) +