Intensifier

Back in June 2021, I saw a video of an audio effect called a transient shaper (https://www.youtube.com/watch?v=3QpZYSUljmw) and found it fascinating. It's like a dynamic range compressor but with superpowers. I immediately got to work making my own.

The first step was figuring out how the signal processing worked and how the parameters, such as attack time, release time, etc. were mapped. I was lucky to find this prototype of one in Max/MSP by David Braun: https://www.maxforlive.com/library/device/1775/transient-designer. It was made up of signal objects which were mostly part of the cyclone library written in C++: https://github.com/porres/pd-cyclone. Thus, I was able to port it to work in an Audio Unit processing block pretty quickly with some Objective-C interoperability. Well... Almost... I noticed the memory usage graph going up when debugging the plugin and was able to find the leak using the "Leaks" tool in Xcode Instruments.

Memory Leak

A picture of Xcode instruments showing the memory leak

After fixing the leak, I had another challenge to face: deciding the user interface and if this would eventually be a cross-platform plugin. Then, an idea hit me: why not use the web browser as the canvas? I found loads of designs on code pen I was inspired by. A couple stood out to me in particular. I liked the simple appeal of UI cards like this for a container: https://codepen.io/JavaScriptJunkie/pen/jvRGZy. I saw the control interface of this demo fitting really well on one of those: https://codepen.io/jaromvogel/pen/jWjWqN.

With a web UI and a signal processing prototype in place, I connected the two by using JavaScript listeners in a WKUserContentController and piping those into the Swift code which interfaced with the C++ of the signal processing code. See this link for the full implementation: https://github.com/emurray2/Intensifier-AUv3/blob/main/IntensifierAUv3/Shared/AUv3IntensifierViewController.swift.

Tap to view Swift Code (Javascript Listener Code)

private func connectViewToAU() {
	// ... Initialization code

	// Observe value changes made to the parameters.
    parameterObserverToken = paramTree.token(byAddingParameterObserver: { [weak self] address, value in
    guard let self = self else { return }

	    // This closure is being called by an arbitrary queue. Ensure
	    // all UI updates are dispatched back to the main thread.
	    if [inputAmount.address,
	        attackAmount.address,
	        releaseAmount.address,
	        attackTime.address,
	        releaseTime.address,
	        outputAmount.address].contains(address) {
	        DispatchQueue.main.async {
	            if self.webPageLoaded {
	                self.updateUI()
	            }
	        }
	    }
	})
}

public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "typeListener" {
        let stringToGet = message.body as? NSString
        type = stringToGet?.substring(from: 0) ?? ""
    }
    if message.name == "valueListener" {
        let valueToGet = message.body as? NSNumber
        let boolValue = message.body as? Bool
        value = valueToGet?.floatValue ?? 0
        if type != "" {
            switch type {
            case "Input Amount":
            inputAmountParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Attack Amount":
            attackAmountParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Release Amount":
            releaseAmountParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Attack Time":
            attackTimeParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Release Time":
            releaseTimeParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Output Amount":
            outputAmountParameter.setValue(value, originator: nil, atHostTime: 0, eventType: .touch)
            case "Toggle":
                delegate?.toggleValueDidChange(value: boolValue ?? false)
            case "Preset":
                if let au = audioUnitCreated {
                    au.setPreset(number: valueToGet?.intValue ?? 0)
                }
            default:
                break
            }
        }
    }
}
						
Tap to view Objective-C Code (DSP Kernel Interface)

#import <AVFoundation/AVFoundation.h>
#import "IntensifierDSPKernel.hpp"
#import "BufferedAudioBus.hpp"
#import "IntensifierDSPKernelAdapter.h"
@implementation IntensifierDSPKernelAdapter {
    // C++ members need to be ivars; they would be copied on access if they were properties.
    IntensifierDSPKernel  _kernel;
    BufferedInputBus _inputBus;
}

- (instancetype)init {

    if (self = [super init]) {
        AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:2];
        // Create a DSP kernel to handle the signal processing.
        _kernel.init(format.channelCount, format.sampleRate);
        _kernel.setParameter(IntensifierParamInputAmount, 0);
        _kernel.setParameter(IntensifierParamAttackAmount, 0);
        _kernel.setParameter(IntensifierParamReleaseAmount, 0);
        _kernel.setParameter(IntensifierParamAttackTime, 0);
        _kernel.setParameter(IntensifierParamReleaseTime, 0);
        _kernel.setParameter(IntensifierParamOutputAmount, 0);

        // Create the input and output busses.
        _inputBus.init(format, 2);
        _outputBus = [[AUAudioUnitBus alloc] initWithFormat:format error:nil];
    }
    return self;
}
// ... Other methods
// Parameter methods
- (void)setParameter:(AUParameter *)parameter value:(AUValue)value {
    _kernel.setParameter(parameter.address, value);
}

- (AUValue)valueForParameter:(AUParameter *)parameter {
    return _kernel.getParameter(parameter.address);
}
// ... Other methods
						
Tap to view C++ Code (Parameter Functions)

#ifndef IntensifierDSPKernel_h
#define IntensifierDSPKernel_h
#import "DSPKernel.hpp"
#import "ParameterRamper.hpp"
#import "AdjustableDelayLine.h"
#import "rmsaverage.h"
#import "slide.h"
// ... Initialization methods
// Parameter functions
void setParameter(AUParameterAddress address, AUValue value) {
        switch (address) {
            case IntensifierParamInputAmount:
                inputAmountRamper.setUIValue(clamp(value, -40.0f, 15.0f));
                break;
            case IntensifierParamAttackAmount:
                attackAmountRamper.setUIValue(clamp(value, -40.0f, 30.0f));
                break;
            case IntensifierParamReleaseAmount:
                releaseAmountRamper.setUIValue(clamp(value, -40.0f, 30.0f));
                break;
            case IntensifierParamAttackTime:
                attackTimeRamper.setUIValue(clamp(value, 0.0f, 500.0f));
                break;
            case IntensifierParamReleaseTime:
                releaseTimeRamper.setUIValue(clamp(value, 0.0f, 5.0f));
                break;
            case IntensifierParamOutputAmount:
                outputAmountRamper.setUIValue(clamp(value, -40.0f, 15.0f));
                break;
        }
    }
    AUValue getParameter(AUParameterAddress address)
    {
        switch (address) {
            case IntensifierParamInputAmount:
                // Return the goal. It is not thread safe to return the ramping value.
                //return (inputAmountRamper.getUIValue() * nyquist);
                return inputAmountRamper.getUIValue();
            case IntensifierParamAttackAmount:
                return attackAmountRamper.getUIValue();
            case IntensifierParamReleaseAmount:
                return releaseAmountRamper.getUIValue();
            case IntensifierParamAttackTime:
                return attackTimeRamper.getUIValue();
            case IntensifierParamReleaseTime:
                return releaseTimeRamper.getUIValue();
            case IntensifierParamOutputAmount:
                return outputAmountRamper.getUIValue();

            default: return 0.0;
        }
    }
    void startRamp(AUParameterAddress address, AUValue value, AUAudioFrameCount duration) override
    {
        switch (address) {
            case IntensifierParamInputAmount:
                inputAmountRamper.startRamp(clamp(value, -40.0f, 15.0f), duration);
                break;
            case IntensifierParamAttackAmount:
                attackAmountRamper.startRamp(clamp(value, -40.0f, 30.0f), duration);
                break;
            case IntensifierParamReleaseAmount:
                releaseAmountRamper.startRamp(clamp(value, -40.0f, 30.0f), duration);
                break;
            case IntensifierParamAttackTime:
                attackTimeRamper.startRamp(clamp(value, 0.0f, 500.0f), duration);
                break;
            case IntensifierParamReleaseTime:
                releaseTimeRamper.startRamp(clamp(value, 0.0f, 5.0f), duration);
                break;
            case IntensifierParamOutputAmount:
                outputAmountRamper.startRamp(clamp(value, -40.0f, 15.0f), duration);
                break;
        }
    }
// ... Other code
						

In the end, I was able to release it on the App Store for iOS and macOS: https://apps.apple.com/us/app/intensifier-auv3/id1573201268 with over 1,000 dowloads after a couple months. The plugin is open-source and available for curious tinkerers to peek at the code: https://github.com/emurray2/Intensifier-AUv3.