Monday, April 30, 2012

Of Audio Units and Xcode 4.3

<GladosVoice>If you've dabbled much in Computer Music, you've probably heard of Steinburg's Virtual Studio Technology (aka VST). However, after the end of this paragraph, I will not be talking about that anymore, so you can just forget I mentioned it. It's really not every important at all except that it does pretty much the same thing as Audio Units and almost everybody has heard of it.</GladosVoice>

So back in the day, Apple came up with their own software synthesizer/effect plugin system called Audio Units. An Audio Unit is a plugin (specifically a bundle with a .component extension), written in C++, which can be used with the vast majority of pro audio software written for Mac OS X (GarageBand, Logic Pro, Ableton Live, etc.). Audio Units (part of Core Audio) are also available on iOS devices, but they are created via a different process which is quite different from making a Mac Audio Unit, so we won't be discussing that either (you can EASILY find documentation on how to write an iOS Audio Unit; this blog is for arcane stuff, remember?).

Enough of the (very interesting) background; why are you reading this article? Well, you probably fit into one of two categories. Either 1) You're curious about writing your own software synth (or effect), or 2) you're more than curious about writing your own software synth (or effect). I suspect most of you are in category 2, and furthermore that most of you have scoured the internet looking for a good tutorial on how to make an Audio Unit (there are surprisingly few) Instrument (most tutorials cover effects... why?) in Xcode 4.3+ (Apple broke a BUNCH of crap over the last few years for Audio Units developers and it's nearly impossible to find a tutorial that covers all the stuff they changed and broke). So, what I'm here to do is help you get an Audio Unit instrument project together that will actually compile, link, and make sound! By the way, this article was very helpful in pointing me in the right direction, and I think they deserve some credit: http://sample-hold.com/2011/11/23/getting-started-with-audio-units-on-os-x-lion-and-xcode-4-2-1/

So, let's get started by opening Xcode. Novel idea, eh? Since version 4.3, they've made Xcode a legit "App Bundle" like every other app in the App Store, and in the process they moved some things and left out some extraneous packages. Apparently Core Audio didn't make the cut so we've gotta grab the first. From the Xcode menu, select Open Developer Tool > More Developer Tools. Sign in to ADC with your Apple ID and download the "Audio Tools for Xcode" package. You can dump all that stuff wherever you want, but I've chosen to put it in /DeveloperExtras. I didn't want to use the "/Developer/Extras" location since that's where Xcode used to keep it's stuff, so I went with a new "safe" folder name (/DeveloperExtras). Just copy the whole shebang over to there.

Ok, so now we've got the whole Core Audio SDK. Awesome. Now let's create a project. Back in the day, there was this handy template to create an Audio Unit, but it's not there anymore (which is why you're reading this). Instead, we're gonna select a Mac OS X > Framework & Library > Bundle template (select Cocoa instead of CoreFoundation when prompted) and then add all the junk we need. Call it "My First Synth" or something (that's the name I'll use for the rest of this post). Let's start off with the fun stuff: the obligatory plist. Open it up and add an array called AudioComponents (it is an array, not a dictionary; I've seen several tutorials get this wrong. See the Apple Technical Note here: http://developer.apple.com/library/mac/#technotes/tn2276/_index.html#//apple_ref/doc/uid/DTS40011031). Inside this array, make the first entry a dictionary with the following keys:

  • description: My First Synth
  • factoryFunction: MyFirstSynthFactory
  • manufacturer: OSUM (for now, any 4-letter code will do; if you want to distribute Audio Units, you will need to eventually get an official one from Apple, but I thought that OSUM looked awesome)
  • name: My First Synth
  • subtype: TEST (again, there are rules regarding this field when distributing Audio Units, but for development, we will use TEST)
  • type: aumu (this field DOES matter; aumu is the type code for an Audio Unit MUsical instrument)
  • version: 0xFFFFFFFF (set the type of this field to number; the Apple Tech Note actually gets this wrong in the screenshot! If you want to know how this version number field works, you can find information elsewhere since it's not important now)
Next, head on over to the target's Build Settings and change the following fields:
  • Architectures: Standard (32/64-bit Intel)
  • Build Active Architecture Only: No
  • Other Linker Flags: -bundle
  • Wrapper Extension: component
  • Rez Search Paths (yes; Audio Units is an old SDK since we're referencing CarbonCore)
    • /DeveloperExtras/CoreAudio/AudioUnits/AUPublic/AUBase
    • /System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Versions/A/Headers
  • Precompile Prefix Header: Yes
  • Exported Symbols File: $(SRCROOT)/$(PROJECT_NAME)/AU.exp
Next, we're going to create 4 files. First, add a C++ file and a header file. You can call them whatever (MyFirstSynth.h/cpp)? Then, add a similarly named file that ends in .exp (I used AU.exp above; the template is under Mac OS X > Other > Exports File in the templates browser). Finally, create another file (using the empty file template) that ends in .r (a Carbon Resource file; this is necessary for the plugin to work in 32-bit hosts).

Now we're gonna head back over to the project settings area and check out the build phases. Make sure the C++ file you created got added to the Compile Sources phase, and that the Carbon Resource file got added to the Build Carbon Resources phase. If the previously mentioned phase doesn't exist, not to worry; we can add it via the Add Build Phase button (put this phase after the Compile Sources phase). One more build phase to edit: delete all of the stuff that is in the Link with Binary Libraries phase (including Cocoa.framework) and add the following:
  • AudioUnit.framework
  • AudioToolbox.framework
  • CoreServices.framework
We are almost done with the project setting changes; just one more thing. Now that we've added a carbon resource, we are able to set the Other Rez Flags property in the Build Settings:
  •  -d i386_$i386 -I /System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Versions/A/Headers -I /DeveloperExtras/CoreAudio/AudioUnits/AUPublic/AUBase
One more final bit of boring-ness related to the project setup. Go to the folder where you put the CoreAudio files (/DeveloperExtras/CoreAudio?) and drag the following files into the project. Make sure to uncheck the box to copy the files into the project directory and check the box to add the files to the target.

  • AudioUnits/
    • AUPublic/
      • AUBase/
        • AUBase.cpp
        • AUBase.h
        • AUDispatch.cpp
        • AUDispatch.h
        • AUInputElement.cpp
        • AUInputElement.h
        • AUOutputElement.cpp
        • AUOutputElement.h
        • AUPlugInDispatch.cpp
        • AUPlugInDispatch.h
        • AUScopeElement.cpp
        • AUScopeElement.h
        • ComponentBase.cpp
        • ComponentBase.h
      • AUInstrumentBase/
        • AUInstrumentBase.cpp
        • AUInstrumentBase.h
        • LockFreeFIFO.h
        • MIDIControlHandler.h
        • SynthElement.cpp
        • SynthElement.h
        • SynthEvent.h
        • SynthNote.cpp
        • SynthNote.h
        • SynthNoteList.cpp
        • SynthNoteList.h
      • OtherBases/
        • AUMIDIBase.cpp
        • AUMIDIBase.h
        • MusicDeviceBase.cpp
        • MusicDeviceBase.h
      • Utility/
        • AUBaseHelper.cpp
        • AUBaseHelper.h
        • AUBuffer.cpp
        • AUBuffer.h
        • AUMIDIDefs.h
  • PublicUtility/
    • CAAudioChannelLayout.cpp
    • CAAudioChannelLayout.h
    • CAAUMIDIMap.cpp
    • CAAUMIDIMap.h
    • CAAUMIDIMapManager.cpp
    • CAAUMIDIMapManager.h
    • CAAutoDisposer.h
    • CABufferList.cpp
    • CABufferList.h
    • CADebugMacros.cpp
    • CADebugMacros.h
    • CAHostTimeBase.cpp
    • CAHostTimeBase.h
    • CAMath.h
    • CAStreamBasicDescription.cpp
    • CAStreamBasicDescription.h
    • CAThreadSafeList.h
    • CAVectorUnit.cpp
    • CAVectorUnit.h
    • CAVectorUnitTypes.h
    • CAXException.cpp
    • CAXException.h

<Yawn>... You still awake? <Grabs another cup of coffee>... Now we're done with a good chunk of the boring stuff and we can actually edit some files. Remember that Carbon resource file we made? Well, we're pretty much gonna drop the same info that we put in the plist file in here (just in a slightly different format). Quick note on this part. My resource file is significantly longer than the one found in most tutorials online. This is because I pasted Apple's AUResources.r at the end of the "interesting" stuff. For the life of me, I'm unable to get past the rez error I encountered while trying to include Apple's file normally, so I just pasted it in. Also, I have no idea what the crap RES_ID is. If anyone knows what this is, let me know :)

#include <AudioUnit/AudioUnit.r>

#define RES_ID    1000
#define COMP_TYPE 'aumu'
#define COMP_SUBTYPE 'TEST'
#define COMP_MANUF 'OSUM'
#define VERSION 0xFFFFFFFF
#define NAME "My First Synth"
#define DESCRIPTION "My First Synth"
#define ENTRY_POINT "MyFirstSynthEntry"

/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 // AUResources.r
 //
 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

/* sample macro definitions -- all of these symbols must be defined
 #define RES_ID kHALOutputResID
 #define COMP_TYPE kAudioUnitComponentType
 #define COMP_SUBTYPE kAudioUnitOutputSubType
 #define COMP_MANUF kAudioUnitAudioHardwareOutputSubSubType
 #define VERSION 0x00010000
 #define NAME "AudioHALOutput"
 #define DESCRIPTION "Audio hardware output AudioUnit"
 #define ENTRY_POINT "AUHALEntry"
 */
#define UseExtendedThingResource 1

#include <CoreServices/CoreServices.r>

// this is a define used to indicate that a component has no static data that would mean 
// that no more than one instance could be open at a time - never been true for AUs
#ifndef cmpThreadSafeOnMac
#define cmpThreadSafeOnMac 0x10000000
#endif

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

resource 'STR ' (RES_ID, purgeable) {
NAME
};

resource 'STR ' (RES_ID + 1, purgeable) {
DESCRIPTION
};

resource 'dlle' (RES_ID) {
ENTRY_POINT
};

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

resource 'thng' (RES_ID, NAME) {
COMP_TYPE,
COMP_SUBTYPE,
COMP_MANUF,
0, 0, 0, 0, // no 68K
'STR ', RES_ID,
'STR ', RES_ID + 1,
0, 0, /* icon */
VERSION,
componentHasMultiplePlatforms | componentDoAutoVersion,
0,
{
        #if defined(ppc_YES)
        cmpThreadSafeOnMac,
        'dlle', RES_ID, platformPowerPCNativeEntryPoint
        #define NeedLeadingComma 1
        #endif
        #if defined(ppc64_YES)
#if defined(NeedLeadingComma)
        ,
#endif
        cmpThreadSafeOnMac,
        'dlle', RES_ID, platformPowerPC64NativeEntryPoint
        #define NeedLeadingComma 1
        #endif
        #if defined(i386_YES)
#if defined(NeedLeadingComma)
        ,
#endif
        cmpThreadSafeOnMac,
        'dlle', RES_ID, platformIA32NativeEntryPoint
        #define NeedLeadingComma 1
        #endif
        #if defined(x86_64_YES)
#if defined(NeedLeadingComma)
        ,
#endif
        cmpThreadSafeOnMac,
        'dlle', RES_ID, 8
        #define NeedLeadingComma 1
        #endif
}
};

#undef RES_ID
#undef COMP_TYPE
#undef COMP_SUBTYPE
#undef COMP_MANUF
#undef VERSION
#undef NAME
#undef DESCRIPTION
#undef ENTRY_POINT
#undef NeedLeadingComma


Before we get to a "normal" source file, we've got one more weird file–the exported symbols file. Open it up and add the following lines:

_MyFirstSynthFactory
_MyFirstSynthEntry


Next up is the prefix header file.

#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__)
#include <CoreAudio/CoreAudioTypes.h>
#include <CoreFoundation/CoreFoundation.h>
#else
#include <CoreAudioTypes.h>
#include <CoreFoundation.h>
#endif

#include <CoreServices/CoreServices.h>
#include <CoreAudio/CoreAudio.h>
#include <AudioUnit/AudioUnit.h>


#include <AudioToolbox/AudioToolbox.h>

Ok, we're pretty much done with the "boilerplate" junk now. Here's our first real source file, the Audio Unit header (this goes in MyFirstSynth.h or AU.h or whatever you called it). Most of this is straight from Apple's SinSynth project. You're probably wondering at this point what's wrong with Apple's project and why you can't change a few things like the Tech Note says. As it turns out, I spent about 10 hours trying to figure out why Apple's code wouldn't compile. If I remember correctly, the only major thing that needed fixing in this file (which is reflected in my code below) is that the SynthNote struct's Render method prototype has changed, and SinSynth was never updated. Sorry in advance for the lack of indentation (bad blogger, bad). Xcode will fix it when you paste it in :)

#include "AUInstrumentBase.h"

#ifndef MyFirstSynth_AU_h
#define MyFirstSynth_AU_h

static const UInt32 kNumNotes = 12;

struct TestNote : public SynthNote
{
virtual ~TestNote() {}
    
virtual bool Attack(const MusicDeviceNoteParams &inParams);
virtual void Kill(UInt32 inFrame); // voice is being stolen.
virtual void Release(UInt32 inFrame);
virtual void FastRelease(UInt32 inFrame);
virtual Float32 Amplitude() { return amp; } // used for finding quietest note for voice stealing.
virtual OSStatus Render(UInt64 inAbsoluteSampleFrame, UInt32 inNumFrames, AudioBufferList** inBufferList, UInt32 inOutBusCount);
    
double phase, amp, maxamp, susamp;
double up_slope, decay_slope, dn_slope, fast_dn_slope;
};

class MyFirstSynth : public AUMonotimbralInstrumentBase
{
public:
    MyFirstSynth(ComponentInstance inComponentInstance);
virtual ~MyFirstSynth();
    
virtual OSStatus Initialize();
virtual void Cleanup();
virtual OSStatus Version() { return 0xFFFFFF; }
    
virtual OSStatus GetParameterInfo(AudioUnitScope inScope,
                                                 AudioUnitParameterID inParameterID,
                                                 AudioUnitParameterInfo & outParameterInfo);
private:
TestNote mTestNotes[kNumNotes];
};

#endif

Finally, we get to the C++ source file! The primary thing that I had to change here to get it to compile was again the prototype of the Render method. Because of some parameter changes, there were some slight modifications to the render file's code as well.

I also made a few "improvements" to the basic sine wave synth. Well, the first improvement isn't really an enhancement; I just changed it from a sine wave generator to a square wave generator. As you might imagine, the Render method is going to be the starting point for any customizations. Go read some books on sound design to make some more interesting sounds :)

The other change actually is an improvement. If you've done much with synthesizers, you've probably seen the standard ADSR (Attack, Decay, Sustain, Release) envelope controls. These allow some basic control over the sound since a synth that immediately plays a sound at full volume when the key is pressed, and stops immediately when the key is released is quite boring in most cases. Do a search through the file for kGlobal and you'll get the hang of how to define and use parameters, which will show up in an auto-generated GUI in AU hosts like GarageBand.

#include "AU.h"

AUDIOCOMPONENT_ENTRY(AUMusicDeviceFactory, MyFirstSynth)

static const UInt32 kMaxActiveNotes = 8;

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const double twopi = 2.0 * 3.14159265358979;

inline double pow5(double x) { double x2 = x*x; return x2*x2*x; }

#pragma mark MyFirstSynth Methods

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

static const AudioUnitParameterID kGlobalVolumeParam = 0;
static const CFStringRef kGlobalVolumeName = CFSTR("Volume");

static const AudioUnitParameterID kGlobalAttackParam = 1;
static const CFStringRef kGlobalAttackName = CFSTR("Attack");

static const AudioUnitParameterID kGlobalDecayParam = 2;
static const CFStringRef kGlobalDecayName = CFSTR("Decay");

static const AudioUnitParameterID kGlobalSustainParam = 3;
static const CFStringRef kGlobalSustainName = CFSTR("Sustain");

static const AudioUnitParameterID kGlobalReleaseParam = 4;
static const CFStringRef kGlobalReleaseName = CFSTR("Release");

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// MyFirstSynth::MyFirstSynth
//
// This synth has No inputs, One output
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MyFirstSynth::MyFirstSynth(ComponentInstance inComponentInstance)
: AUMonotimbralInstrumentBase(inComponentInstance, 0, 1)
{
CreateElements();
Globals()->UseIndexedParameters(5); // we're only defining one param
Globals()->SetParameter(kGlobalVolumeParam, 1.0);
Globals()->SetParameter(kGlobalAttackParam, 0.01);
    Globals()->SetParameter(kGlobalDecayParam, 0.75);
    Globals()->SetParameter(kGlobalSustainParam, 0.5);
    Globals()->SetParameter(kGlobalReleaseParam, 0.75);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// MyFirstSynth::~MyFirstSynth
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MyFirstSynth::~MyFirstSynth()
{
}


void MyFirstSynth::Cleanup()
{
#if DEBUG_PRINT
printf("MyFirstSynth::Cleanup\n");
#endif
}

OSStatus MyFirstSynth::Initialize()
{
#if DEBUG_PRINT
printf("->MyFirstSynth::Initialize\n");
#endif
AUMonotimbralInstrumentBase::Initialize();
    
SetNotes(kNumNotes, kMaxActiveNotes, mTestNotes, sizeof(TestNote));
#if DEBUG_PRINT
printf("<-MyFirstSynth::Initialize\n");
#endif
return noErr;
}

OSStatus MyFirstSynth::GetParameterInfo(AudioUnitScope inScope,
                                                   AudioUnitParameterID inParameterID,
                                                   AudioUnitParameterInfo & outParameterInfo)
{
if (inParameterID == kGlobalVolumeParam) {
        if (inScope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope;
        
        outParameterInfo.flags = SetAudioUnitParameterDisplayType (0, kAudioUnitParameterFlag_DisplaySquareRoot);
        outParameterInfo.flags += kAudioUnitParameterFlag_IsWritable;
        outParameterInfo.flags += kAudioUnitParameterFlag_IsReadable;
        
        AUBase::FillInParameterName (outParameterInfo, kGlobalVolumeName, false);
        outParameterInfo.unit = kAudioUnitParameterUnit_LinearGain;
        outParameterInfo.minValue = 0;
        outParameterInfo.maxValue = 1.0;
        outParameterInfo.defaultValue = 1.0;
        return noErr;
    }
    else if (inParameterID == kGlobalAttackParam) {
        if (inScope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope;
        
        outParameterInfo.flags = SetAudioUnitParameterDisplayType (0, kAudioUnitParameterFlag_DisplaySquareRoot);
        outParameterInfo.flags += kAudioUnitParameterFlag_IsWritable;
        outParameterInfo.flags += kAudioUnitParameterFlag_IsReadable;
        
        AUBase::FillInParameterName (outParameterInfo, kGlobalAttackName, false);
        outParameterInfo.unit = kAudioUnitParameterUnit_Seconds;
        outParameterInfo.minValue = 0.001;
        outParameterInfo.maxValue = 3.0;
        outParameterInfo.defaultValue = 0.01;
        return noErr;
    }
    else if (inParameterID == kGlobalDecayParam) {
        if (inScope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope;
        
        outParameterInfo.flags = SetAudioUnitParameterDisplayType (0, kAudioUnitParameterFlag_DisplaySquareRoot);
        outParameterInfo.flags += kAudioUnitParameterFlag_IsWritable;
        outParameterInfo.flags += kAudioUnitParameterFlag_IsReadable;
        
        AUBase::FillInParameterName (outParameterInfo, kGlobalDecayName, false);
        outParameterInfo.unit = kAudioUnitParameterUnit_Seconds;
        outParameterInfo.minValue = 0;
        outParameterInfo.maxValue = 3.0;
        outParameterInfo.defaultValue = 0.75;
        return noErr;
    }
    else if (inParameterID == kGlobalSustainParam) {
        if (inScope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope;
        
        outParameterInfo.flags = SetAudioUnitParameterDisplayType (0, kAudioUnitParameterFlag_DisplaySquareRoot);
        outParameterInfo.flags += kAudioUnitParameterFlag_IsWritable;
        outParameterInfo.flags += kAudioUnitParameterFlag_IsReadable;
        
        AUBase::FillInParameterName (outParameterInfo, kGlobalSustainName, false);
        outParameterInfo.unit = kAudioUnitParameterUnit_LinearGain;
        outParameterInfo.minValue = 0;
        outParameterInfo.maxValue = 1.0;
        outParameterInfo.defaultValue = 0.5;
        return noErr;
    }
    else if (inParameterID == kGlobalReleaseParam) {
        if (inScope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope;
        
        outParameterInfo.flags = SetAudioUnitParameterDisplayType (0, kAudioUnitParameterFlag_DisplaySquareRoot);
        outParameterInfo.flags += kAudioUnitParameterFlag_IsWritable;
        outParameterInfo.flags += kAudioUnitParameterFlag_IsReadable;
        
        AUBase::FillInParameterName (outParameterInfo, kGlobalReleaseName, false);
        outParameterInfo.unit = kAudioUnitParameterUnit_Seconds;
        outParameterInfo.minValue = 0.001;
        outParameterInfo.maxValue = 3.0;
        outParameterInfo.defaultValue = 0.75;
        return noErr;
    }
    else {
        return kAudioUnitErr_InvalidParameter;
    }
}


#pragma mark TestNote Methods

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

bool TestNote::Attack(const MusicDeviceNoteParams &inParams)
#if DEBUG_PRINT
    printf("TestNote::Attack %08X %d\n", this, GetState());
#endif
    double sampleRate = SampleRate();
    phase = 0.;
    amp = 0.;
    maxamp = 0.4 * pow(inParams.mVelocity/127., 3.);
    susamp = GetGlobalParameter(kGlobalSustainParam) * maxamp;
    up_slope = maxamp / (GetGlobalParameter(kGlobalAttackParam) * sampleRate);
    decay_slope = (susamp-maxamp) / (GetGlobalParameter(kGlobalDecayParam) * sampleRate);
    dn_slope = -maxamp / (GetGlobalParameter(kGlobalReleaseParam) * sampleRate);
    fast_dn_slope = -maxamp / (0.005 * sampleRate);
    
    return true;
}

void TestNote::Release(UInt32 inFrame)
{
SynthNote::Release(inFrame);
#if DEBUG_PRINT
printf("TestNote::Release %08X %d\n", this, GetState());
#endif
}

void TestNote::FastRelease(UInt32 inFrame) // voice is being stolen.
{
SynthNote::Release(inFrame);
#if DEBUG_PRINT
printf("TestNote::Release %08X %d\n", this, GetState());
#endif
}

void TestNote::Kill(UInt32 inFrame) // voice is being stolen.
{
SynthNote::Kill(inFrame);
#if DEBUG_PRINT
printf("TestNote::Kill %08X %d\n", this, GetState());
#endif
}

OSStatus TestNote::Render(UInt64 inAbsoluteSampleFrame, UInt32 inNumFrames, AudioBufferList** inBufferList, UInt32 inOutBusCount)
{
float *left, *right;
    /* ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
     Changes to this parameter (kGlobalVolumeParam) are not being de-zippered; 
     Left as an exercise for the reader
     ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ */
float globalVol = GetGlobalParameter(kGlobalVolumeParam);
int numChans = inBufferList[0]->mNumberBuffers;
if (numChans > 2) return -1;
left = (float*)inBufferList[0]->mBuffers[0].mData;
right = numChans == 2 ? (float*)inBufferList[0]->mBuffers[1].mData : 0;
    
double sampleRate = SampleRate();
double freq = Frequency() * (twopi/sampleRate);
    
#if DEBUG_PRINT_RENDER
printf("TestNote::Render %08X %d %g %g\n", this, GetState(), phase, amp);
#endif
    SynthNoteState state = GetState();
    
switch (state)
{
case kNoteState_Attacked :
case kNoteState_Sostenutoed :
case kNoteState_ReleasedButSostenutoed :
case kNoteState_ReleasedButSustained :
        {
            for (UInt32 frame=0; frame<inNumFrames; ++frame)
            {
                if (amp < maxamp && state == kNoteState_Attacked) {
                    amp += up_slope;
                    
                    if (amp >= maxamp) {
                        SetState(kNoteState_Sostenutoed);
                    }
                }
                else if (amp >= susamp && state == kNoteState_Sostenutoed) {
                    amp += decay_slope;
                    dn_slope = -amp / (GetGlobalParameter(kGlobalReleaseParam) * sampleRate);
                }
                
                //float out = pow5(sin(phase)) * amp * globalVol;
                float out = (sin(phase) > 0 ? 1 : -1) * amp * globalVol;
                phase += freq;
                if (phase > twopi) phase -= twopi;
                left[frame] += out;
                if (right) right[frame] += out;
            }
        }
break;
case kNoteState_Released :
        {
            UInt32 endFrame = 0xFFFFFFFF;
            for (UInt32 frame=0; frame<inNumFrames; ++frame)
            {
                if (amp > 0.0) amp += dn_slope;
                else if (endFrame == 0xFFFFFFFF) endFrame = frame;
                //float out = pow5(sin(phase)) * amp * globalVol;
                float out = (sin(phase) > 0 ? 1 : -1) * amp * globalVol;
                phase += freq;
                left[frame] += out;
                if (right) right[frame] += out;
            }
            if (endFrame != 0xFFFFFFFF) {
#if DEBUG_PRINT
                printf("TestNote::NoteEnded  %08X %d %g %g\n", this, GetState(), phase, amp);
#endif
                NoteEnded(endFrame);
            }
        }
break;
case kNoteState_FastReleased :
        {
            UInt32 endFrame = 0xFFFFFFFF;
            for (UInt32 frame=0; frame<inNumFrames; ++frame)
            {
                if (amp > 0.0) amp += fast_dn_slope;
                else if (endFrame == 0xFFFFFFFF) endFrame = frame;
                float out = pow5(sin(phase)) * amp * globalVol;
                phase += freq;
                left[frame] += out;
                if (right) right[frame] += out;
            }
            if (endFrame != 0xFFFFFFFF) {
#if DEBUG_PRINT
                printf("TestNote::NoteEnded  %08X %d %g %g\n", this, GetState(), phase, amp);
#endif
                NoteEnded(endFrame);
            }
        }
break;
default :
break;
}
return noErr;
}

Now, if all goes well, you should be able to build the Audio Unit. If not, please let me know and I'll figure out what I missed in the tutorial. Once you've got a working component, copy it from the build folder into ~/Library/Audio/Plug-Ins/Components. You can validate your new plug-in by issuing the following command from the terminal:
  • auval -v aumu TEST OSUM
If all goes well, you've got a valid Audio Unit that you can test out in AU Lab or GarageBand!


13 comments:

  1. Hello Ian,

    Nice tutorial!

    When starting the Xcode project for the AU, what do I put in the company name/bundle identifier field? I built the tremolo unit sample a few years ago, but obviously, things have changed.

    ReplyDelete
  2. Hey Larry,

    I assume you're talking about the project creation process? For the company name, it doesn't really matter for this project (use your name if you want). The bundle name will be generated automatically based on the product name (which can also be pretty much whatever you want). The important metadata really gets set up when we edit the PLIST file (and whenever there are special restrictions, I have noted them in the article).

    Hope this helps!

    ReplyDelete
    Replies
    1. Thanks for the response. Yeah, there's a text field right under the one for the Product Name that says: Company Identifier. It seems like it's wanting you to enter something like: i.e.,
      com.angryaudio.audiounit.TremoloUnit

      I'm just wondering if I could enter something like com.angryaudio.audiounit.MyFirstSynth, you know what I mean?

      Delete
    2. Yes, that will work perfectly :)

      Delete
  3. Hello again Ian,

    I've followed the instructions precisely in your tutorial. However, when I attempt to build it, I'm left with just one nagging error. For the life of me, I cannot figure it out. The compiler is complaining that there's a Lexical or Preprocessor Issue: 'AUBase.h' file not found. I know the file is there because that was one of the first ones that I dragged into the project following your instruction. There is also a line that says: 1. In file included from CAAUMIDIMapManager.cpp

    I been studying the build settings for hours and everything seems to be set right. I've focused closely on the Other Rez Flags, and I do notice that when I enter those search paths into that key all on one line, and then double-click it again to examine it, it looks like it breaks the search paths that I entered into three or four lines. I wonder if that could be causing the compiler to not read the search paths correctly?

    I'm baffled and stumped. Any further help or suggestions would be greatly appreciated.

    ReplyDelete
  4. I finally got it. If you've clicked on my profile link, you'll see that I have a background in engineering. So, to figure this problem out, I REALLY had to put on my engineer's thinking cap. I basically just had to add a Copy Headers Build Phase to the project and manually drag the header files into that. Once I did that, the build succeeded without issues.

    On a different note, I immediately noticed upon trying out the newly built AU, that it did not show up in logic pro. I then tried it in Garage Band and there it was. So, does that mean that logic pro doesn't auto-generate a UI, but evidently Garage Band does? Are you saying that that specific unit can only be tested in AU Lab or Garage Band?

    ReplyDelete
  5. Hey,

    Thanks for the great tutorial. I have followed your instructions completely. However, I have one problem. When I compile I get an error in CAAUMIDIMap.cpp

    line 57: Use of undeclared identifier 'PTHREAD_ONCE_INIT'

    and line 67: Use of undeclared identifier 'pthread_once'; did you mean 'pthread_kill'?


    I am lost. How could these errors even occur? I am copying my header files and I am doing everything that is in your instructions. Please help as I have spent the past 4 days trying to work with the old template. And you have just informed me that does not work. I will keep trying but I am stuck and I would really appreciate the help.

    Thanks

    ReplyDelete
  6. Does your AudioUnit load in GarageBand or Mainstage ? Now (with XCode 4.5) I'm trying to build my AU doing more or less your steps. I works great with AU Lab, it validates with auval, both in 32bits and 64 bits. But I can't manage to have it load in Mainstage or GarageBand (32 or 64 bits). My Unit is just ignored.

    ReplyDelete
  7. > When I compile I get an error in CAAUMIDIMap.cpp
    > line 57: Use of undeclared identifier 'PTHREAD_ONCE_INIT'

    Same here. I'm trying to work this out now - did you ever figure it out?

    Xcode 4.6.3, Lion 10.7.5...

    ReplyDelete
  8. Ok, to update this, I solved the issue for me and have an audiounit that successfully runs in AU Lab and Logic.

    For the pthread issue, I added a direct include into the *prefix.pch file to include pthread.h: Add this line:
    #include

    Also, to stop Logic complaining about a version mismatch, make sure the main header .h file returns the correct version:
    virtual OSStatus Version() { return 0xFFFFFFFF; }

    (In the code above, this is written as:
    virtual OSStatus Version() { return 0xFFFFFF; }
    ...which gives the version mismatch).

    Hope that helps others on this crazy AudioUnit ride - I'm a newbie at this, but this article was super helpful, wouldn't have got it running without it. Thanks Ian...

    ReplyDelete
  9. Whoops, the pthread line gets hidden because of the pointy brackets.

    #include (pthread.h)
    (But replace the normal brackets with less-than, and greater-than brackets...)

    ReplyDelete
  10. Hi,
    I love your tutorial! I was wondering if you know of a way to create a custom track icon for GarageBand to go with an audio unit. I've torn apart Google trying to come up with an something that'll work but I've only come up with more people with the same question.

    ReplyDelete
  11. Hi, thanks for this quick beauty tuts you made...
    Got a little probs. When I compile this... its has this issues :

    ld: can't open -exported_symbols_list file: /Users/numosh.com/Desktop/NM PARANOIA/PARANOIA JUCE/Paranoia 165/Paranoia165/MyFirstSynth/AU.exp
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

    ReplyDelete