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
31 changes: 23 additions & 8 deletions Source/PluginEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -56,6 +59,13 @@ AirPlayPluginEditor::AirPlayPluginEditor(AirPlayPluginProcessor& p)
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)
{
Expand Down Expand Up @@ -107,9 +117,11 @@ 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
Expand All @@ -119,10 +131,10 @@ void AirPlayPluginEditor::resized()
auto outputMeterArea = metersArea.removeFromLeft(40);

inputMeterLabel.setBounds(inputMeterArea.removeFromTop(20));
inputMeter.setBounds(inputMeterArea.removeFromTop(340));
inputMeter.setBounds(inputMeterArea); // Use remaining height

Copilot AI Oct 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Comment states 'Use remaining height' but the previous line inputMeterArea.removeFromTop(20) already consumed 20 pixels for the label, so this uses the height after label removal. Consider clarifying: 'Use height remaining after label'.

Copilot uses AI. Check for mistakes.

outputMeterLabel.setBounds(outputMeterArea.removeFromTop(20));
outputMeter.setBounds(outputMeterArea.removeFromTop(340));
outputMeter.setBounds(outputMeterArea); // Use remaining height

Copilot AI Oct 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Comment states 'Use remaining height' but the previous line already removed 20 pixels for the label. Consider clarifying: 'Use height remaining after label' to match the actual behavior.

Suggested change
outputMeter.setBounds(outputMeterArea); // Use remaining height
outputMeter.setBounds(outputMeterArea); // Use height remaining after label

Copilot uses AI. Check for mistakes.
}

void AirPlayPluginEditor::timerCallback()
Expand All @@ -131,8 +143,11 @@ void AirPlayPluginEditor::timerCallback()
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);
}

void AirPlayPluginEditor::updateStatusDisplay()
Expand Down
65 changes: 30 additions & 35 deletions Source/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,70 +10,67 @@ class LevelMeter : public juce::Component
public:
LevelMeter()
{
setOpaque(false);
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);

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,
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)
{
Expand All @@ -91,30 +88,27 @@ class LevelMeter : public juce::Component
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)
};

Expand All @@ -126,74 +120,75 @@ 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();
void updateStatusDisplay();
void updateBufferHealth();
void showError(const juce::String& error);
void showStatus(const juce::String& status);

AirPlayPluginProcessor& audioProcessor;

juce::ListBox deviceListBox;
juce::StringArray deviceNames;
juce::Array<AirPlayDevice> 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],
5, 0, width - 10, height,
juce::Justification::centredLeft, true);
}
}

private:
AirPlayPluginEditor& editor;
};

std::unique_ptr<DeviceListModel> listModel;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AirPlayPluginEditor)
};
53 changes: 53 additions & 0 deletions test_plugin.py
Original file line number Diff line number Diff line change
@@ -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"

Copilot AI Oct 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded absolute path is user-specific and will fail for other developers. Consider using environment variables, configuration files, or relative paths based on the user's home directory (e.g., os.path.expanduser('~/Library/Audio/Plug-Ins/VST3/FreeCaster.vst3')).

Copilot uses AI. Check for mistakes.
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"

Copilot AI Oct 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded absolute path is user-specific and will fail for other developers. Consider using os.path.expanduser() to make the path portable across different user accounts.

Suggested change
au_path = "/Users/edahl/Library/Audio/Plug-Ins/Components/FreeCaster.component"
au_path = os.path.expanduser("~/Library/Audio/Plug-Ins/Components/FreeCaster.component")

Copilot uses AI. Check for mistakes.
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"

Copilot AI Oct 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded absolute path is user-specific and will fail for other developers. Consider using a relative path from the script location or environment variables to make this test portable.

Copilot uses AI. Check for mistakes.
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)