Tutorial: Getting set up to build Logic Pro X MIDI FX plugins

Here's how I set myself up for a happy MFX workflow using Xcode 6.2, I'm going to write it out as a full tutorial for the benefit of others (and future me, for when I've forgotten how I got things working)

1. Work your way through Jules' instructions on patching Xcode for AU builds

2. Start a new Introjucer project, choose "audio plugin"

3. In Introjucer's project config:
   • ​
leave "Build AudioUnit" checked
   • leave "Build VST" checked (so you can test your plugin in Jules' PluginHost.app)
   
• check "Is a Synth"
   • 
check "Plugin wants midi input"
   • 
check "Plugin produces midi output"
   • 
set "Plugin AU Main Type" to 'aumi' (including the single quotes)
   • 
leave channel configuration as "{1, 1}, {2, 2}
   • set the various PluginName and CompanyName fields

4. In Introjucer's Xcode config:
   • add the following "post-build shell script" to automagically copy plugins to the correct places when you build

# This script takes the build product and copies it to the AU, VST, VST3, RTAS and AAX folders, depending on
# which plugin types you've built
original=$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME
# this looks inside the binary to detect which platforms are needed..
copyAU=`nm -g "$CONFIGURATION_BUILD_DIR/$EXECUTABLE_PATH" | grep -i 'AudioUnit' | wc -l`
copyVST=`nm -g "$CONFIGURATION_BUILD_DIR/$EXECUTABLE_PATH" | grep -i 'VSTPlugin' | wc -l`
copyVST3=`nm -g "$CONFIGURATION_BUILD_DIR/$EXECUTABLE_PATH" | grep -i 'GetPluginFactory' | wc -l`
copyRTAS=`nm -g "$CONFIGURATION_BUILD_DIR/$EXECUTABLE_PATH" | grep -i 'CProcess' | wc -l`
copyAAX=`nm -g "$CONFIGURATION_BUILD_DIR/$EXECUTABLE_PATH" | grep -i 'ACFStartup' | wc -l`
if [ $copyAU -gt 0 ]; then
  echo "Copying to AudioUnit folder..."
  AU=~/Library/Audio/Plug-Ins/Components/$PRODUCT_NAME.component
  if [ -d "$AU" ]; then
    rm -r "$AU"
  fi
  cp -r "$original" "$AU"
  sed -i "" -e 's/TDMwPTul/BNDLPTul/g' "$AU/Contents/PkgInfo"
  sed -i "" -e 's/TDMw/BNDL/g' "$AU/Contents/$INFOPLIST_FILE"
  # Fix info.plist for AUs built with Xcode 3
  if [ -f "$DEVELOPER_DIR/Library/Developer/CoreAudio/AudioUnits/AUPublic/AUBase/AUPlugInDispatch.cpp" ]; then
    echo
  else
    echo "Removing AudioComponents entry from Info.plist because this is not a new-format AU"
    /usr/libexec/PlistBuddy -c "Delete AudioComponents" "$AU/Contents/Info.plist"
  fi
fi
if [ $copyVST -gt 0 ]; then
  echo "Copying to VST folder..."
  VST=~/Library/Audio/Plug-Ins/VST/$PRODUCT_NAME.vst
  if [ -d "$VST" ]; then
    rm -r "$VST"
  fi
  cp -r "$original" "$VST"
  sed -i "" -e 's/TDMwPTul/BNDLPTul/g' "$VST/Contents/PkgInfo"
  sed -i "" -e 's/TDMw/BNDL/g' "$VST/Contents/$INFOPLIST_FILE"
fi
if [ $copyVST3 -gt 0 ]; then
  echo "Copying to VST3 folder..."
  VST3=~/Library/Audio/Plug-Ins/VST3/$PRODUCT_NAME.vst3
  if [ -d "$VST3" ]; then
    rm -r "$VST3"
  fi
  cp -r "$original" "$VST3"
  sed -i "" -e 's/TDMwPTul/BNDLPTul/g' "$VST3/Contents/PkgInfo"
  sed -i "" -e 's/TDMw/BNDL/g' "$VST3/Contents/$INFOPLIST_FILE"
fi
if [ $copyRTAS -gt 0 ]; then
  echo "Copying to RTAS folder..."
  RTAS=/Library/Application\ Support/Digidesign/Plug-Ins/$PRODUCT_NAME.dpm
  if [ -d "$RTAS" ]; then
    rm -r "$RTAS"
  fi
  cp -r "$original" "$RTAS"
fi
if [ $copyAAX -gt 0 ]; then
  echo "Copying to AAX folder..."
  if [ -d "/Applications/ProTools_3PDev/Plug-Ins" ]; then
    AAX1="/Applications/ProTools_3PDev/Plug-Ins/$PRODUCT_NAME.aaxplugin"
    if [ -d "$AAX1" ]; then
      rm -r "$AAX1"
    fi
    cp -R -H "$original" "$AAX1"
  fi
  if [ -d "/Library/Application Support/Avid/Audio/Plug-Ins" ]; then
    AAX2="/Library/Application Support/Avid/Audio/Plug-Ins/$PRODUCT_NAME.aaxplugin"
    if [ -d "$AAX2" ]; then
      rm -r "$AAX2"
    fi
    cp -R -H "$original" "$AAX2"
  fi
fi

   • make sure "VST Folder" is pointing to Steinberg's VST SDK
   • for both "Debug" and "Release", set your "Compatability Version" to your minimum target OS (I'm targetting 10.9+)
   • 
leave "Base SDK Version" on "Use Default"

5. Make a build of Jules' PluginHost.app. I modified the source code slightly so the app always opens the last saved preset (which massively speeds up development for me). I added the following code to the end of MainHostWindow's constructor in MainHostWindow.cpp

//====================================================================================
// Hack to automatically load most recent filtergraph when opening the app
// Added by My Name 2015/XX/XX
GraphDocumentComponent* const graphEditor = getGraphEditor();
RecentlyOpenedFilesList recentFiles;
recentFiles.restoreFromString (getAppProperties().getUserSettings()
                               ->getValue ("recentFilterGraphFiles"));
if (graphEditor != nullptr && graphEditor->graph.saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
    graphEditor->graph.loadFrom (recentFiles.getFile (0), true);
//====================================================================================

6. Back in your Introjucer project, hit "Save Project and Open in Xcode..."

7. Set up debugging in Xcode using the PluginHost.app you built in step 5...
   • Go Product > Scheme > Edit Scheme
   • Select "Run" in the left hand pane
   • From the "Exectuable" drop down, choose other, point it to the PluginHost.app you built in step 5
   • Make sure "Debug executable" is checked
   • Click "Close"

   • Build and Run. The Plugin Host app should open with an empty window
   • In Plugin Host, go Options > Edit the list of available plug-ins...
   • Click Options... > Scan for new or updated VST plug-ins
​   • Ensure that "/Users/myusername/Library/Audio/Plug-Ins/VST" is on the list and click "Scan"
​   • Right click on the background of the Plugin Host and your plugin should be in the list :)
​   • Add an instance of your plugin (we have to use the VST version as Plugin Host doesn't support MFX plugins)
   • Wire up the MIDI In, and the MIDI Out to a VI with a MIDI Monitor of some sort (I use NI Kontakt)
Juce Plugin-Host Configuration
   • Save this preset in the Plugin Host (it should get automagically opened on the next build as per step 5)
   • Quit the Plugin Host

8. At this point the VST build should already function as a MIDI Thru, but the AU MFX build (if you try inserting it in LPX) will not pass any MIDI, time to patch AuBase.cpp as per Oscar1771's fix.
   • Back in Xcode, open the source tree to "Juce Library Code / Juce AU Wrapper" and open AuBase.cpp
   • Search for the following (you should get one match around line 1357):

if (output->GetStreamFormat().NumberChannelStreams() != ioData.mNumberBuffers)

   • On the line before the matching if statement, add:

//====================================================================================
// Hack to make MFX plugin call AudioProcessor::ProcessBlock()
// Added by My Name on 2015/XX/XX
ioData.mNumberBuffers = 1;
//====================================================================================

Bear in mind that AuBase.cpp resides at /Applications/Xcode.app/Contents/Developer/Extras/CoreAudio/AudioUnits/AUPublic/AUBase and will likely need to be re-patched when updating Xcode

9. Now for a little MIDI Processor implementation.

   • Open Xcode's source tree to "My Plugin / Source" and open PluginProcessor.cpp
   • Find AudioProcessor::processBlock and replace it with:

void <MyPluginNameHere>AudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    
    buffer.clear(); // ignore all audio input, output only silence
    
    MidiBuffer processedMidi; // our processed MIDI output
    int time; // the timestamp of the current MIDI message in the for loop
    MidiMessage m; // the current MIDI message in the for loop
    
    const uint8 myFixedVelocity = 50; // a fixed velocity to apply to note messages
    
    // iterate through the input buffer, process our MIDI, add it to the output buffer
    for (MidiBuffer::Iterator i (midiMessages); i.getNextEvent (m, time);)
    {
        
        if (m.isNoteOn())
        {
            m = MidiMessage::noteOn(m.getChannel(), m.getNoteNumber(), myFixedVelocity);
        }
        processedMidi.addEvent (m, time);
        
    }
    
    // replace the outgoing MIDI buffer with our processed MIDI buffer
    midiMessages.swapWith (processedMidi);
}

    • Build and Run. In Plugin Host, test the VST version of your plugin for the desired behaviour (you should only see notes coming out with velocity 50).
   • Open LPX and insert the MFX version of your plugin (it should have the same behaviour as the VST version you tested in Plugin Host).

2 Likes

Works great - cheers! 

If ever you find yourself NYC - round(s) of beers on me!

Just reminded by how much this helped me out.  Seriously.  Beers on me!

Curious if there is a new way to do this now introjucer is part of projucer and Jules’ patching instructions tutorial link is missing?

Hi @gmcerveny. Indeed, almost everything I wrote is no longer necessary. AFAIK, the new tutorial would be:

  • Install JUCE
  • Open Projucer, start new project choosing “Audio Plug-In”
  • Check the box that says “Plugin is a MIDI effect plugin”
  • Code your plugin
3 Likes