jatinchowdhury18 / AnalogTapeModel

Physical modelling signal processing for analog tape recording
GNU General Public License v3.0
1.09k stars 62 forks source link

[FEATURE] Add AUv3 version for iOS #136

Closed samirsd closed 3 years ago

samirsd commented 3 years ago

an iOS integration or documentation on how to adapt to an iOS app would be really helpful. If you point me in the right direction I can take a crack at it.

jatinchowdhury18 commented 3 years ago

Hi @samirsd,

Thanks for the request and for offering to help!

I've actually been working for the past few weeks to port another plugin ChowCentaur to iOS. My original plan was to finish porting that plugin so I could work out all the kinks from that process, and then start working on this one. That said, as of a few days ago, I think that port is nearly finished, pending a couple small bugs (more on this below)!

With that in mind, I'm going to take this opportunity to write out everything I've learned from that porting process, so I can reference it later. From there, if you'd like to help with porting this plugin (or testing the other one), that would be super appreciated! (btw, all of this stuff can be referenced from the ChowCentaur repository).

Anyway, there's a ton of stuff here, so definitely let me know if you have questions. Hopefully this weekend I'll have to time to start working on this as well.


1. Picking a version of JUCE

I usually build my plugins with a custom fork of JUCE, that supports LV2 builds. However, for building for iOS, it's probably best to use the latest tip of the official JUCE repo, that has some fixes for iOS/AUv3. For ChowCentaur, I ended up adding both forks as submodules, and picking which one to use in the CMake configuration. In the example here, I use the fork only when building on Linux:

if(UNIX AND NOT APPLE)
    add_subdirectory(DISTRHO_JUCE)
else()
    add_subdirectory(JUCE)
endif()

It's also probably best to clean up the Plugin directory, by putting all the submodules in their own directory.

2. Editing the CMake configuration

First we need to add AUv3 as one of the plugin formats:

juce_add_plugin(ChowCentaur
    COMPANY_NAME chowdsp
    PLUGIN_MANUFACTURER_CODE Chow
    PLUGIN_CODE Ctr1
    FORMATS AU VST3 Standalone LV2 AUv3 # VST
    ...
)

Next, we need to add a bunch of iOS-specific properties. Basically, we need to set output directories for the plugin targets, plus the targets for any static libraries that are linked in. We also need to specify install locations for the plugins. For more information about this stuff, see here.

if(IOS)
    message(STATUS "Setting iOS-specific properties...")

    foreach(target IN ITEMS BinaryData RTNeural SharedCode ChowCentaur ChowCentaur_Standalone ChowCentaur_AUv3)
        set_target_properties(${target}
            PROPERTIES
                RUNTIME_OUTPUT_DIRECTORY "./"
                ARCHIVE_OUTPUT_DIRECTORY "./"
                LIBRARY_OUTPUT_DIRECTORY "./")
    endforeach()

    set_target_properties(ChowCentaur_Standalone PROPERTIES
            XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)"
            XCODE_ATTRIBUTE_SKIP_INSTALL "NO")

    set_target_properties(ChowCentaur_AUv3 PROPERTIES
            XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)/ChowCentaur.app/PlugIns"
            XCODE_ATTRIBUTE_SKIP_INSTALL "NO")
endif()

We'll also have to adjust how we invoke our CMake command (more below).

3. Making changes in the code.

Next we may need to edit the code a little bit so that the plugin works properly on the iPad.

By default, JUCE plugins expect stereo input/output. This will cause the standalone version to crash on iOS since the default input is mono. While I don't think many people will use the standalone version, if it crashes, then the plugin won't pass the App Store review process. Here's my current solution:

PluginClass
#if JUCE_IOS || JUCE_MAC
    AudioProcessor (juce::JUCEApplicationBase::isStandaloneApp() ?
        BusesProperties().withInput ("Input", juce::AudioChannelSet::mono(), true)
                         .withOutput ("Output", juce::AudioChannelSet::stereo(), true) :
        BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), true)
                         .withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
#else
    AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), true)
                                     .withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
#endif
    vts (*this, nullptr, juce::Identifier ("Parameters"), createParameterLayout())
{
}

However, I don't like this solution very much. For example, when trying to use the Standalone plugin with my audio interface plugged in, I started seeing crashes again. Going to keep trying out some different solutions here.

Also, JUCE ComboBox menu's don't work on iOS by default. Here's a solution for that:

class LookAndFeel
{
    Component* getParentComponentForMenuOptions (const PopupMenu::Options& options) override
    {
#if JUCE_IOS
        if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_AudioUnitv3)
        {
            if (options.getParentComponent() == nullptr && options.getTargetComponent() != nullptr)
                return options.getTargetComponent()->getTopLevelComponent();
        }
#endif
        return LookAndFeel_V2::getParentComponentForMenuOptions (options);
    }
}

4. Packaging

We're finally ready to build and package our plugin! All of the details for this are in my ios_build script.

First we need to invoke CMake with the following arguments:

cmake -Bbuild-ios -GXcode -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=11.4 \
    -DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM="$TEAM_ID" \
    -DCMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY="1,2" \
    -DCMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE="NO"

Note that your "$TEAM_ID" will be specific to you (actually I'm not 100% sure you need it if you're not trying to upload your builds to the app store).

If all you want to do is build the plugin, then from here, you can go to the build-ios directory, and open up the project in XCode. Then connect your iOS device, and go from there!

If you want to package the plugin from distribution, you can use the following command:

xcodebuild -project build-ios/ChowCentaur.xcodeproj \
  -scheme ChowCentaur_Standalone archive -configuration Release \
  -sdk iphoneos -jobs 12 -archivePath ChowCentaur.xcarchive | xcpretty

5. Adding a CI script (BONUS)

Finally, I would prefer if all future changes were tested on iOS, just to make sure I don't break something with a future change. With that in mind, I added a GitHub Actions workflow to build on iOS whenever a change is made. For more info, see here.

jatinchowdhury18 commented 3 years ago

By the way, I do eventually plan to release both plugins as free downloads from the iOS app store. At some point before I do that, I need to review the licenses I'm using for each codebase, and see if I need to tweak some things in order for that to work.

jatinchowdhury18 commented 3 years ago

An AUv3 plugin is now available to try in beta, via TestFlight.

samirsd commented 3 years ago

sweet! tried it out. works on my iPhone 11 Pro. I don't have an audio adapter so it was just the mic and it created a feedback loop but it was a great and warm sounding feedback loop :P sorry I didn't get a chance to work on this yet as I had hoped. I still have to get up to speed on JUCE.

can I build for iOS straight from the master branch or is there a special build parameter I need to pass?

jatinchowdhury18 commented 3 years ago

No problem! To build from source, I would recommend building from the develop branch. I've updated the build instructions to include the necessary commands to build for iOS. Definitely let me know if you run into any issues!

samirsd commented 3 years ago

Was able to build and run from source. Have you tried incorporating this into an iOS app yet? I'm assuming the most straight forward way at this point would be adding it as an extension.

jatinchowdhury18 commented 3 years ago

Awesome! The current build creates both a Standalone app, and an AUv3 plugin (I think it's a sort of app extension). The AUv3 can be loaded into an iOS plugin host like GarageBand or AUM. It should be possible to try the AUv3 plugin with either the TestFlight build (linked above) or with your own builds. Let me know if it works for you!

samirsd commented 3 years ago

Can confirm that the version I built from source works in GarageBand iOS on iOS 14.5.