Attention! Translated article might be found on my English blog.

2016年8月3日水曜日

自作v3 AudioUnitのカスタムUIからパラメータを変更する

前回の記事でカスタムUIを表示できるようになったので、
今回はそのUIから実際にパラメータを変更できるようにしてみました。

UIが絡むケースとして、主に
1: UIからAudioUnitのパラメータを変更する
2: 外部から変更されたAudioUnitのパラメータをUIに反映する
この2つがあると思います。2の外部というのは例えばプログラム側でAudioUnitのパラメータを直接変更した場合やプリセットを変更した時が挙げられます。


1: UIからAudioUnitのパラメータを変更する

この場合は典型的なCocoaアプリと同じように、ビューコントローラにアクションを
送ります。
- (IBAction)changeFrequencyAction:(id)sender {
    self.freq.value = [sender floatValue];
}

- (IBAction)changeDepthAction:(id)sender {
    self.depth.value = [sender floatValue];
}
ちなみにsenderはNSSliderです。
freqとdepthプロパティはAudioUnitのparameterTreeにセットしたAUParameterで
- (void)connectViewWithAU {
    AUParameterTree *paramTree = self.unit.parameterTree;
    
    if (paramTree) {
        self.depth = [paramTree valueForKey: VibratoParameterIdentifierDepth];
        self.freq = [paramTree valueForKey: VibratoParameterIdentifierFrequency];
というふうに値をセットしています。
VibratoParameterIdentifierDepthとVibratoParameterIdentifierFrequencyは以前の記事でAudioUnitにパラメータを追加した時に定義したidentifierです。
このidentifierをAUParameterTreeにvalueForKey:のkeyとして指定すると
対応するAUParameterが得られるようです。

さて、AUParameterのvalueが変化すると、以前の記事で実装した

    self.parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) {
        switch (param.address) {
            case VibratoParameterAddressFrequency:
                unit.freq = value;
                break;
            case VibratoParameterAddressDepth:
                unit.depth = value;
            default:
                break;
        }
    };
で書いたblockが実行され、AudioUnit内部のパラメータ(共にfloat)が変更される仕組みになっています。

2: 外部から変更されたAudioUnitのパラメータをUIに反映する

こちらのケースに関しては上手く説明できないので該当部分を全部載せます。orz
- (void)connectViewWithAU {
    AUParameterTree *paramTree = self.unit.parameterTree;
    
    if (paramTree) {
        self.depth = [paramTree valueForKey: VibratoParameterIdentifierDepth];
        self.freq = [paramTree valueForKey: VibratoParameterIdentifierFrequency];
        
        // prevent retain cycle in parameter observer
        __weak AudioUnitViewController *weakSelf = self;
        __weak AUParameter *weakFreq = self.freq;
        __weak AUParameter *weakDepth = self.depth;
        self.token = [paramTree tokenByAddingParameterObserver:^(AUParameterAddress address, AUValue value) {
            _PF;
            __strong AudioUnitViewController *strongSelf = weakSelf;
            __strong AUParameter *strongFreq = weakFreq;
            __strong AUParameter *strongDepth = weakDepth;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                if (address == strongFreq.address){
                    strongSelf->_freqSlider.floatValue = value;
                } else if (address == strongDepth.address) {
                    strongSelf->_depthSlider.floatValue = value;
                }
            });
        }];
        self.freqSlider.floatValue = self.freq.value;
        self.depthSlider.floatValue = self.depth.value;
        
        [self.unit addObserver:self forKeyPath:@"allParameterValues"
                       options:NSKeyValueObservingOptionNew
                       context:self.token];
    } else {
        NSLog(@"paramTree is NULL!\n");
    }
}
まず、下部にあるaddObserver:forKeyPath:options:context:はKey-Value Observingの手法で
allParameterValuesキーの値が変更された時に
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *, id> *)change
                       context:(void *)context
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.freqSlider.floatValue = self.freq.value;
        self.depthSlider.floatValue = self.depth.value;
    });
}
が実行されるのでblockの中でfreqSliderとdepthSliderの値が変更されます。
allParameterValuesに関してはAUAudioUnit.hで
AUAudioUnit has an additional pseudo-property, "allParameterValues", on which KVO
notifications are issued in response to certain events where potentially all parameter
values are invalidated (e.g. selection of a preset or setting fullState).
と書かれており、プリセットが選択された時に変更されるkeyのようですね。

個別にパラメータが変更された場合、tokenByAddingParameterObserver:で書いたblockが実行されるようです。
引数としてAUParameterAddressが渡ってくるので、この値でどのパラメータが変更になったかを判断して、スライダーの値を変更してやります。
なお、この実装はFilterDemoViewControllerを参考にしましたが、
__weakや__strongを使って面倒なことをやっている理由は理解できてません。orz

インスタンス変数を直接使ったり、"->"を使って意地でもプロパティを避けようとしている感じはあるのですが、自分がARCやblock, GCDに慣れてないこともあり、
いまいち理由が分かっていません。

ちなみに、スライダーから値を変更した時にも巡り巡ってtokenByAddingParameterObserver:で書いたblockが実行されるようです。


これでUI <-> AUParameter -> AudioUnitという流れのパラメータの変更は追随できるようになったと思います。
(AUParameter <- AudioUnitという流れに関しては、parameterTree.implementorValueProviderにblockを指定したのである程度できているとは思いますが、即時反映されるわけではないんじゃないかと思います。この辺は全くの未確認です)

というわけで、無事Hosting AUで音を鳴らしながらパラメータをUIから変更できるようになりました!