3

Audio Units (AUv3) MIDI extension – Part 3: Presets

 3 years ago
source link: http://www.rockhoppertech.com/blog/audio-units-auv3-midi-extension-part-3-presets/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

AUv3 MIDI Presets

In previous blog posts I created a working AUv3 MIDI processor that adds an interval to incoming MIDI note messages. The interval can be customized via an AUParameter.

Now I’ll show you how to make v3 presets.

Introduction

Table of Contents

Take a look at AuAudioUnit.h if you haven’t yet. A lot of the “new” (AUv3( Audio Unit API is here.

This time look for what it offers for presets.

Minus the comments, we have this regarding presets:

@property (NS_NONATOMIC_IOSONLY, readonly, copy, nullable) NSArray<AUAudioUnitPreset *> *factoryPresets;
@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSDictionary<NSString *, id> *fullState;
@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSDictionary<NSString *, id> *fullStateForDocument;
@property (NS_NONATOMIC_IOSONLY, retain, nullable) AUAudioUnitPreset *currentPreset;

The AUAudioUnitPreset class is not complicated. It is simply a name and number.

@interface AUAudioUnitPreset : NSObject <NSSecureCoding>
@property (NS_NONATOMIC_IOSONLY) NSInteger number;
@property (NS_NONATOMIC_IOSONLY, copy) NSString *name;

Let’s go through these members.

Defining Factory presets

Table of Contents

We see that our Audio Unit’s superclass defines an array of AUAudioUnitPresets named factoryPresets. Clever naming. So, we need to create this array and return it in this property.

My first shot at this was to synthesize the inherited property into an array instance variable. Then, I created a method to instantiate the AUAudioUnitPresets.

NSArray<AUAudioUnitPreset *> *_presets;
@synthesize factoryPresets = _presets;
- (void) setupFactoryPresets {
    NSMutableArray* presetItems = [NSMutableArray new];
    AUAudioUnitPreset* newPreset = [AUAudioUnitPreset new];
    newPreset.number = 0;
    newPreset.name = @"Frobnozz";
    [presetItems addObject:newPreset];
    newPreset = [AUAudioUnitPreset new];
    newPreset.number = 1;
    newPreset.name = @"SnizzWhiz";
    [presetItems addObject:newPreset];
    _presets = [NSArray arrayWithArray:presetItems];
    _currentFactoryPresetIndex = 0;
    _currentPreset = _presets[_currentFactoryPresetIndex];

If I had a lot of presets to create, this code looks pretty wet. So let’s DRY it out.

- (AUAudioUnitPreset*)createPreset:(NSInteger)number name:(NSString*)name {
    AUAudioUnitPreset* newPreset = [AUAudioUnitPreset new];
    newPreset.number = number;
    newPreset.name = name;
    return newPreset;

The thing is, we don’t really need a method to create the presets. Why not just override the array?

- (NSArray*)factoryPresets {
    NSArray *presetArray = @[
                             [self createPreset:0 name:@"Frobnozz"],
                             [self createPreset:1 name:@"SnizzWhiz"],
    return presetArray;

It’s works! Here they are.

Image-1-1-225x300.jpg

What does FilterDemo do?

(Remember the previous blog post on C++? The FilterDemo’s filtering code is actually in a C++ class named FilterDSPKernel. You’ll see this a lot in audio unit code).

FilterDemo does this for two of the inherited properties: factoryPresets and currentPreset. If you don’t call @synthesize on a property, you will get a variable with the property name prefixed with an underscore. So, we see that the property currentPreset is not synthesized, but the variable _currentPreset is defined. They also define an array of AUAudioUnitPresets, and then “point” the inherited property factoryPresets to it. In their implementation they have an array of hard-coded presets, so they keep an index into it in order to set them.

@implementation AUv3FilterDemo {
// C++ members need to be ivars; they would be copied on access if they were properties.
    FilterDSPKernel  _kernel;
    BufferedInputBus _inputBus;
    AUAudioUnitPreset   *_currentPreset;
    NSInteger           _currentFactoryPresetIndex;
    NSArray<AUAudioUnitPreset *> *_presets;
@synthesize parameterTree = _parameterTree;
@synthesize factoryPresets = _presets;

So, for the interval MIDI plugin, I’ll do the same thing.

@implementation AUParamsPresetsAudioUnit {
    // C++ members need to be ivars; they would be copied on access if they were properties.
    IntervalPlugin *_intervalPlugin;
    AUParameter *intervalParam;
    AUAudioUnitPreset *_currentPreset;
    NSInteger _currentFactoryPresetIndex;
    NSArray<AUAudioUnitPreset *> *_presets;
@synthesize parameterTree = _parameterTree;
@synthesize factoryPresets = _presets;

Now, let’s set them up in initWithComponentDescription.

I defined kDefaultFactoryPreset to be 0 which I used to initialize _currentFactoryPresetIndex.

I create the _presets array (which factoryPresets points to) using the createPreset method.

The _currentPreset is then set to the first preset.

_currentFactoryPresetIndex = kDefaultFactoryPreset;
_presets = @[
             [self createPreset:0 name:@"Minor Second"],
             [self createPreset:1 name:@"Major Second"],
             [self createPreset:2 name:@"Minor Third"],
             [self createPreset:3 name:@"Major Third"],
             [self createPreset:4 name:@"Fourth"],
             [self createPreset:5 name:@"Tritone"],
             [self createPreset:6 name:@"Fifth"],
             [self createPreset:7 name:@"Minor Sixth"],
             [self createPreset:8 name:@"Major Sixth"],
             [self createPreset:9 name:@"Minor Seventh"],
             [self createPreset:10 name:@"Major Seventh"],
             [self createPreset:11 name:@"Octave"]
_currentPreset = self.factoryPresets[_currentFactoryPresetIndex];

Cool. We’re set up now.

How does FilterDemo handle accessing the currentPreset?

Here is their code. Actually there is nothing we need to change in this.
Borrow it!

- (AUAudioUnitPreset *)currentPreset
    if (_currentPreset.number >= 0) {
        NSLog(@"Returning Current Factory Preset: %ld\n", (long)_currentFactoryPresetIndex);
        return [_presets objectAtIndex:_currentFactoryPresetIndex];
    } else {
        NSLog(@"Returning Current Custom Preset: %ld, %@\n", (long)_currentPreset.number, _currentPreset.name);
        return _currentPreset;

What we can see here is that if the preset number is < 0, then the system considers it to be a User Defined Preset. We just return it since the system keeps track of it.

How does FilterDemo handle setting the current preset? Once again, it checks to see if the preset number is a User Defined Preset or not - the handling is different. For a factory preset, it loops through the _preset array to find the matching preset, The parameters are retrieved from the tree and their values are set to the factory values. Then a bit of housekeeping to update _currentPreset and _currentFactoryPresetIndex.

For User Defined Presets, _currentPreset is simply set to the preset passed in. More on User Presets below.

- (void)setCurrentPreset:(AUAudioUnitPreset *)currentPreset
    if (nil == currentPreset) { NSLog(@"nil passed to setCurrentPreset!"); return; }
    if (currentPreset.number >= 0) {
        // factory preset
        for (AUAudioUnitPreset *factoryPreset in _presets) {
            if (currentPreset.number == factoryPreset.number) {
                AUParameter *cutoffParameter = [self.parameterTree valueForKey: @"cutoff"];
                AUParameter *resonanceParameter = [self.parameterTree valueForKey: @"resonance"];
                cutoffParameter.value = presetParameters[factoryPreset.number].cutoffValue;
                resonanceParameter.value = presetParameters[factoryPreset.number].resonanceValue;
                // set factory preset as current
                _currentPreset = currentPreset;
                _currentFactoryPresetIndex = factoryPreset.number;
                NSLog(@"currentPreset Factory: %ld, %@\n", (long)_currentFactoryPresetIndex, factoryPreset.name);
                break;
    } else if (nil != currentPreset.name) {
        // set custom preset as current
        _currentPreset = currentPreset;
        NSLog(@"currentPreset Custom: %ld, %@\n", (long)_currentPreset.number, _currentPreset.name);
    } else {
        NSLog(@"setCurrentPreset not set! - invalid AUAudioUnitPreset\n");

What’s that “presetParameters” I’m using to set the value?

To actually define the factory preset values, I created a struct to hold the values. It is really simple in this plugin since there is only one parameter (the interval). But you can imagine that in a synth there would be a metric ton of them.

static const UInt8 kNumberOfPresets = 12;
static const NSInteger kDefaultFactoryPreset = 0;
typedef struct FactoryPresetParameters {
    AUValue intervalValue;
} FactoryPresetParameters;
static const FactoryPresetParameters presetParameters[kNumberOfPresets] = {
    { 10 },
    { 11 },
    { 12 },

My modification to FilterDemo’s current preset setter is to change the AUParameter being updated of course.

if (currentPreset.number == factoryPreset.number) {
    AUParameter *intervalParameter = [self.parameterTree valueForKey: @"intervalParameter"];
    intervalParameter.value = presetParameters[factoryPreset.number].intervalValue;

Try it out!

Image-1-2-300x225.jpg

Choose a preset and you’ll set it updating!

Defining User presets

Table of Contents

Your audio unit inherits this property:

@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSDictionary<NSString *, id> *fullState;

When you save a new preset, e.g. in AUM press the + in the Presets title bar, this property will be accessed. By default, the keys in this dictionary are: manufacturer, data, type, subtype, and version. What we need to do is add keys/values for our parameters to this dictionary. So, create a mutable dictionary, add to that and return it. You could just call setObject:forKey for each parameter, but adding a params dictionary is tidier if you have many parameters.

- (NSDictionary<NSString *,id> *)fullState {
    NSLog(@"calling: %s", __PRETTY_FUNCTION__ );
    NSMutableDictionary *state = [[NSMutableDictionary alloc] initWithDictionary: super.fullState];
    // you can do just a setObject:forKey on state, but in real life you will probably have many parameters.
    // so, add a param dictionary to fullState.
    NSDictionary<NSString*, id> *params = @{
                                            @"intervalParameter": [NSNumber numberWithInt: intervalParam.value],
    state[@"fullStateParams"] = [NSKeyedArchiver archivedDataWithRootObject: params];
    return state;

When the user preset is selected, the setter is called. So, grab the archived data you wrote in the getter, and then extract the parameter values. Here there is only one parameter, but you may have many.

- (void)setFullState:(NSDictionary<NSString *,id> *)fullState {
    NSLog(@"calling: %s", __PRETTY_FUNCTION__ );
    NSData *data = (NSData *)fullState[@"fullStateParams"];
    NSDictionary *params = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    intervalParam.value = [(NSNumber *)params[@"intervalParameter"] intValue];

There is also a fullStateForDocument property for “global” properties such as master tuning. If you need this, define accessors for it using the same procedure I described for fullState.

To test, set an interval from the segmented control, then save it as a “user defined preset”. (Press the + in AUM’s title bar). Choose a factory preset, note the value, then press your user preset. It should have the value you saved. If not, pet the cat and grab another espresso.

Summary

Table of Contents

There is a new API for using presets that is bridged to the v2 API for compatibility. It’s actually quite a bit easier now.

Resources

Table of Contents


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK