swiftaudiokit

How to Implement a Custom Audio Effect Node in AudioKit?


I'm developing a feature that uses AudioKit 5.6 to apply real-time effects on the mic, and the user can change it's values.

Here's an example:

private func initializeEffects() {
        guard let inputNode = engine.input else { return }
        fader = Fader(inputNode, gain: 1)
        
        compressor = Compressor(fader)
        compressor.masterGain = 0

        dynamicRangeCompressor = DynamicRangeCompressor(compressor)
        dynamicRangeCompressor.ratio = 100
        dynamicRangeCompressor.attackDuration = 0.01
        dynamicRangeCompressor.releaseDuration = 0.1
        
        dynamicRangeCompressorDryWetMixer = DryWetMixer(compressor, dynamicRangeCompressor)
        dynamicRangeCompressorDryWetMixer.balance = 0.75
        
        bandPassButterworth = BandPassButterworthFilter(dynamicRangeCompressorDryWetMixer)
        bandPassButterworth.bandwidth = 4000
        bandPassButterworth.centerFrequency = 0
        
        delay = Delay(bandPassButterworth)
        delay.feedback = 20
        delay.time = 0.25
        delay.dryWetMix = 100
        
        delayDryWetMixer = DryWetMixer(bandPassButterworth, delay)
        delayDryWetMixer.balance = 0
        
        reverb = Reverb(delayDryWetMixer)
        reverb.dryWetMix = 0
        reverb.loadFactoryPreset(.largeChamber)
        
        pitchShifter = PitchShifter(reverb)
        
        flanger = Flanger(pitchShifter)
        flanger.frequency = 1
        flanger.depth = 1
        flanger.feedback = 0
        flanger.dryWetMix = 1
        
        flangerDryWetMixer = DryWetMixer(pitchShifter, flanger)
        flangerDryWetMixer.balance = 0.5
        
        stringResonator = StringResonator(flangerDryWetMixer)
        stringResonator.feedback = 0.9
        
        stringResonatorDryWetMixer = DryWetMixer(flangerDryWetMixer, stringResonator)
        stringResonatorDryWetMixer.balance = 0
        
        engine.output = stringResonatorDryWetMixer
    }

Now I want to create a custom pitch correction effect and add it to the chain. I have an Objective-C++ library that takes an audio buffer and returns a modified one:

- (void)processStereo:(const float *)srcL leftDestination:(float *)dstL rightSource:(const float *)srcR rightDestination:(float *)dstR numSamples:(int)nsmp;

I have no idea how to go about it. Any suggestions?

EDIT: Based on @mahal's answer, I created a DSP:

#include "SoundpipeDSPBase.h"
#include "ParameterRamper.h"
#include "Soundpipe.h"
#include "CSoundpipeAudioKit.h"
#include "tuna.h"

enum TunaParameter : AUParameterAddress {
    TunaFilterPitchCorrectionParameterSpeed,
};

class TunaDSP : public SoundpipeDSPBase {
private:
    float speed = 0;
    ParameterRamper speedRamp;
    tuna *tuner;
    
    
public:
    TunaDSP() {
        parameters[TunaFilterPitchCorrectionParameterSpeed] = &speedRamp;
    }
    
    void setSpeed(float speed) {
        this->speed = speed;
        tuner->setpar(tuna::TUNA_SPEED, speed);
        reset();
    }
    
    void init(int channelCount, double sampleRate) override {
        SoundpipeDSPBase::init(channelCount, sampleRate);
        tuner = new tuna();
        tuner->setpar(tuna::TUNA_SPEED, 1); 
    }
    
    void deinit() override { 
        SoundpipeDSPBase::deinit(); 
    }
    
    void reset() override {
        SoundpipeDSPBase::reset();
        if (!isInitialized) return;
        tuner = new tuna(); 
    }
    
#define CHUNKSIZE 8     // defines ramp interval
    
    void process(FrameRange range) override {
        const float *inBuffers[2];
        float *outBuffers[2];
        inBuffers[0]  = (const float *)inputBufferLists[0]->mBuffers[0].mData + range.start;
        inBuffers[1]  = (const float *)inputBufferLists[0]->mBuffers[1].mData + range.start;
        outBuffers[0] = (float *)outputBufferList->mBuffers[0].mData + range.start;
        outBuffers[1] = (float *)outputBufferList->mBuffers[1].mData + range.start;
        //unsigned inChannelCount = inputBufferLists[0]->mNumberBuffers;
        //unsigned outChannelCount = outputBufferList->mNumberBuffers;
        
        if (!isStarted)
        {
            // effect bypassed: just copy input to output
            memcpy(outBuffers[0], inBuffers[0], range.count * sizeof(float));
            memcpy(outBuffers[1], inBuffers[1], range.count * sizeof(float));
            return;
        }
        
        // process in chunks of maximum length CHUNKSIZE
        for (int frameIndex = 0; frameIndex < range.count; frameIndex += CHUNKSIZE)
        {
            int chunkSize = range.count - frameIndex;
            if (chunkSize > CHUNKSIZE) chunkSize = CHUNKSIZE;
            
            tuner->process(inBuffers[0], inBuffers[1], outBuffers[0], outBuffers[1], chunkSize);
            
            // advance pointers
            inBuffers[0] += chunkSize;
            inBuffers[1] += chunkSize;
            outBuffers[0] += chunkSize;
            outBuffers[1] += chunkSize;
        }
    }
};

void akTunaFilterPitchCorrectionSetSpeed(DSPRef dspRef, float speed) {
    auto dsp = dynamic_cast<TunaDSP *>(dspRef);
    assert(dsp);
    dsp->setSpeed(speed);
}

AK_REGISTER_DSP(TunaDSP, "tuna")
AK_REGISTER_PARAMETER(TunaFilterPitchCorrectionParameterSpeed)

And a Node:

import Foundation
import AudioKit
import AudioKitEX
import CAudioKitEX

public class PitchCorrectionAudioKitNode: Node {
    let input: Node
    
    /// Connected nodes
    public var connections: [Node] { [input] }
    
    /// Underlying AVAudioNode
    public var avAudioNode = instantiate(effect: "tuna")
    
    // MARK: - Parameters
    
    /// Specification details for feedback
    public static let speedDef = NodeParameterDef(
        identifier: "speed",
        name: "Speed",
        address: akGetParameterAddress("TunaFilterPitchCorrectionParameterSpeed"),
        defaultValue: 0.6,
        range: 0.0 ... 1.0,
        unit: .percent
    )
    
    @Parameter(speedDef) public var speed: AUValue
    
    // MARK: - Initialization
    
    /// Initialize this pitch correction node
    ///
    /// - Parameters:
    ///   - input: Input node to process
    ///
    public init(
        _ input: Node,
        speed: AUValue = speedDef.defaultValue
    ) {
        self.input = input
        
        setupParameters()
        
        self.speed = speed
    }
}

Now it works.


Solution

  • Not trivial because you want to call the c code outside swift in a AudioUnit that wraps your effect. have a look at DunneAudioKit, for example: