cmajor icon indicating copy to clipboard operation
cmajor copied to clipboard

Issue: No Simple Way to Do AU Resizing on macOS

Open aaronventure opened this issue 4 months ago • 2 comments

VST3 and CLAP can easily be resized simply by resizing the plugin window. But Apple says: "Screw you!", so AU can only be resized when the plugin itself makes a call to set a different window size. This goes for AU in general in all hosts. Reaper will let you scale the window, but the actual plugin interface will not change. Logic won't even let you resize the window at all -- it shows the mouse cursor but nothing happens when you drag it.

I created a patcher that patches the exported JUCE code and adds a method that sets the size and updates the WebView holder size as well.

It also adds a check for the file format which can be made from the interface, in case the resize control needs to be disabled on non-AU formats.

Like with other cases, leaving this here for posterity or in case you think this is worth implementing and documenting.

This is the patcher (.cmake):

# CMake script to patch the exported JUCE code for AU plugin resizing
# This script modifies the CMajor-generated JUCE plugin to:
# 1. Add a requestResize method to the Editor class that calls AudioProcessorEditor::setSize()
# 2. Bind this method to the WebView so it can be called from JavaScript
# 3. Enable proper AU plugin window resizing through programmatic calls

# Read the exported JUCE plugin file
set(JUCE_PLUGIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build/cmajor-juce-export/include/cmajor/helpers/cmaj_JUCEPlugin.h")

# Check if file exists
if(NOT EXISTS "${JUCE_PLUGIN_FILE}")
    message(FATAL_ERROR "JUCE plugin file not found: ${JUCE_PLUGIN_FILE}")
endif()

file(READ "${JUCE_PLUGIN_FILE}" JUCE_PLUGIN_CONTENT)

# Check if patch has already been applied
string(FIND "${JUCE_PLUGIN_CONTENT}" "// PATCHED: Add requestResize method for AU plugin resizing" PATCH_ALREADY_APPLIED)
if(NOT PATCH_ALREADY_APPLIED EQUAL -1)
    message(STATUS "✅ AU resize patch already applied, skipping...")
    return()
endif()

# ═══════════════════════════════════════════════════════════════════════════════
# PATCH 1: Add requestResize method to the Editor class
# ═══════════════════════════════════════════════════════════════════════════════

# Find the Editor class and add the requestResize method after the constructor
# We need to place it outside the constructor, as a proper class method

# First, let's add the method declaration after the constructor
set(RESIZE_METHOD_CODE "
        // PATCHED: Add requestResize method for AU plugin resizing
        void requestResize (int newWidth, int newHeight)
        {
            // No size limits - allow any dimensions
            // Call AudioProcessorEditor::setSize to resize the plugin window
            // This is the proper way to resize AU plugins programmatically
            setSize (newWidth, newHeight);

            // Also update the WebView holder size to match
            if (patchWebViewHolder && patchWebViewHolder->isVisible())
            {
                patchWebViewHolder->setSize (newWidth, newHeight - DerivedType::extraCompHeight);
            }
        }")

# Find the addAndMakeVisible call in onPatchChanged and add the binding setup after it
string(REPLACE
    "                addAndMakeVisible (*patchWebViewHolder);
                childBoundsChanged (nullptr);"
    "                addAndMakeVisible (*patchWebViewHolder);

                // PATCHED: Set up resize binding for AU plugin resizing
                setupResizeBinding();

                childBoundsChanged (nullptr);"
    JUCE_PLUGIN_CONTENT "${JUCE_PLUGIN_CONTENT}")

# Find the end of the destructor and add the methods after it
string(REPLACE
    "        ~Editor() override
        {
            owner.editorBeingDeleted (this);
            setLookAndFeel (nullptr);
            patchWebViewHolder.reset();
            patchWebView.reset();
        }"
    "        ~Editor() override
        {
            owner.editorBeingDeleted (this);
            setLookAndFeel (nullptr);
            patchWebViewHolder.reset();
            patchWebView.reset();
        }
${RESIZE_METHOD_CODE}"
    JUCE_PLUGIN_CONTENT "${JUCE_PLUGIN_CONTENT}")

# ═══════════════════════════════════════════════════════════════════════════════
# PATCH 2: Add setupResizeBinding method
# ═══════════════════════════════════════════════════════════════════════════════

set(RESIZE_BINDING_METHOD "
        // PATCHED: Setup resize binding for WebView communication
        void setupResizeBinding()
        {
            if (patchWebView)
            {
                auto& webView = patchWebView->getWebView();

                // Bind resize function
                webView.bind (\"cmaj_requestResize\", [this] (const choc::value::ValueView& args) -> choc::value::Value
                {
                    if (args.isArray() && args.size() >= 2)
                    {
                        try
                        {
                            int width = (int) args[0].getWithDefault<double> (800);
                            int height = (int) args[1].getWithDefault<double> (600);
                            requestResize (width, height);
                        }
                        catch (const std::exception& e)
                        {
                            std::cout << \"Error in cmaj_requestResize: \" << e.what() << std::endl;
                        }
                    }
                    return {};
                });

                // Bind plugin format detection function
                webView.bind (\"cmaj_getPluginFormat\", [this] (const choc::value::ValueView& args) -> choc::value::Value
                {
                    // Cast owner to AudioProcessor to access wrapperType member
                    auto* processor = static_cast<juce::AudioProcessor*>(&owner);
                    if (processor)
                    {
                        auto wrapperType = processor->wrapperType;

                        switch (wrapperType)
                        {
                            case juce::AudioProcessor::wrapperType_VST3:
                                return choc::value::Value (\"VST3\");
                            case juce::AudioProcessor::wrapperType_AudioUnit:
                            case juce::AudioProcessor::wrapperType_AudioUnitv3:
                                return choc::value::Value (\"AU\");
                            case juce::AudioProcessor::wrapperType_LV2:
                                return choc::value::Value (\"LV2\");
                            case juce::AudioProcessor::wrapperType_VST:
                                return choc::value::Value (\"VST2\");
                            case juce::AudioProcessor::wrapperType_AAX:
                                return choc::value::Value (\"AAX\");
                            case juce::AudioProcessor::wrapperType_Standalone:
                                return choc::value::Value (\"Standalone\");
                            default:
                                // Check if it's CLAP (CLAP doesn't have a specific JUCE wrapper type yet)
                                // We can detect it by checking if it's not any of the above known types
                                return choc::value::Value (\"CLAP\");
                        }
                    }
                    return choc::value::Value (\"Unknown\");
                });
            }
        }")

# Add the setupResizeBinding method after the requestResize method
string(REPLACE
    "${RESIZE_METHOD_CODE}"
    "${RESIZE_METHOD_CODE}
${RESIZE_BINDING_METHOD}"
    JUCE_PLUGIN_CONTENT "${JUCE_PLUGIN_CONTENT}")

# Write the patched content back to the file
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/build/cmajor-juce-export/include/cmajor/helpers/cmaj_JUCEPlugin.h" "${JUCE_PLUGIN_CONTENT}")

message(STATUS "✅ Patched CMajor JUCE plugin for AU resizing:")

and in the index.js bridge you do something like this or whatever fits your chosen framework.

    /**
     * Request a resize of the plugin window from the interface
     *
     * Uses the patched CMajor WebView binding that calls AudioProcessorEditor::setSize()
     * This is the only method needed for proper resizing across all plugin formats:
     * - VST3: Works automatically with window resizing
     * - AU: Requires programmatic resize via AudioProcessorEditor::setSize()
     * - CLAP: Works automatically with window resizing
     *
     * The cmaj_requestResize binding is added by PatchAUResize.cmake during build.
     */
    requestResize(width, height) {
        try {
            // Call the patched CMajor WebView binding
            if (typeof window.cmaj_requestResize === 'function') {
                window.cmaj_requestResize(width, height);
            }
        } catch (error) {
            // Silently handle errors - resizing is not critical
        }
    }

    /**
     * Send plugin format to interface
     */
    async sendPluginFormatToInterface() {
        try {
            const pluginFormat = await this.getPluginFormat();

            if (this.iframe && this.iframe.contentWindow) {
                this.iframe.contentWindow.postMessage({
                    type: 'pluginFormat',
                    format: pluginFormat
                }, '*');
            }
        } catch (error) {
            // Silently handle errors - plugin format detection is not critical
        }
    }

In my case, using Svelte, talking to the index.js which hosts it goes like this (in a mouse drag function of a resize handle icon):

		window.parent?.postMessage(
			{
				type: 'requestResize',
				width: newWidth,
				height: newHeight
			},
			'*'
		);

aaronventure avatar Aug 26 '25 04:08 aaronventure

This all sounds pretty sensible and I'd be happy to add something along these lines.. It's a bit hard to unpick the changes from these scripts though - do you have a version of the edited files so I can try it out and see the diffs?

julianstorer avatar Aug 28 '25 07:08 julianstorer

Hey sorry for the late reply, seeing this just now. This is just a cmake patch for the cmaj_JUCEPlugin.h that is generated with every Cmajor JUCE export. The Cmajor patch content is irrelevant, the JUCE template is the issue.

Copy the code into a .cmake file. It looks for the .h file in this location set(JUCE_PLUGIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build/cmajor-juce-export/include/cmajor/helpers/cmaj_JUCEPlugin.h") so export an empty Cmajor patch, change the path in the script and run it.

The patch binds the cmaj_requestResize function so it can be called from the webview interface. In the interface you then simply call it in the window object (standard implementation is a component in the bottom right corner that takes the current size, calculates mouse delta and calls this function with the new values).

You can check the changes and just have the Cmaj export generate it like that. But that still leaves the dev to implement the calling of it in the index.js bridge and then in their interface, so you'd just have to document the function and that it exists, so they don't have to go and figure out how to resize AU.

It also adds sendPluginFormatToInterface if you wanna go hardcore and skip showing the graphic for non-AU plugin types since they react to window resizing.

Just feed this entire thread into Codex/Claude if I'm not making any sense 😂

aaronventure avatar Sep 26 '25 04:09 aaronventure