Reputation: 801
I am trying to learn some Core Audio and to start with I've tried to make a basic audio unit that just generates a sine wave with an adjustable frequency. The problem I have now is that I have severe crackle and pops whenever I adjust the frequency and I am not sure where to look to find the cause of this.
I can't even tell if it's just because I need to do some smoothing when moving between frequencies, although I haven't seen that in any examples so far.
Anyway, here's the implementation of my audio unit:
#import "VCOUnit.h"
@interface VCOUnit () {
AUParameterTree *_parameterTree;
AUAudioUnitBusArray *_outputBusses;
AVAudioPCMBuffer *_outputBuffer;
}
@end
@implementation VCOUnit
- (instancetype)initWithComponentDescription:(AudioComponentDescription)componentDescription options:(AudioComponentInstantiationOptions)options error:(NSError *__autoreleasing _Nullable *)outError {
self = [super initWithComponentDescription:componentDescription options:options error:outError];
return self;
}
- (AUParameterTree *)parameterTree {
if (!_parameterTree) {
AUParameter *frequency = [AUParameterTree createParameterWithIdentifier:@"frequency"
name:@"Frequency"
address:0
min:10
max:500
unit:kAudioUnitParameterUnit_Hertz
unitName:nil
flags:0
valueStrings:nil
dependentParameters:nil];
frequency.value = 220.0;
_parameterTree = [AUParameterTree createTreeWithChildren:@[frequency]];
}
return _parameterTree;
}
- (AUAudioUnitBusArray *)outputBusses {
if (!_outputBusses) {
AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:1];
AUAudioUnitBus *bus = [[AUAudioUnitBus alloc] initWithFormat:format error:nil];
_outputBusses = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeOutput busses:@[bus]];
}
return _outputBusses;
}
- (BOOL)allocateRenderResourcesAndReturnError:(NSError *__autoreleasing _Nullable *)outError {
if (![super allocateRenderResourcesAndReturnError:outError]) {
return NO;
}
AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:1];
_outputBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:format frameCapacity:1024];
return YES;
}
- (AUInternalRenderBlock)internalRenderBlock {
AUParameter *frequencyParam = [self.parameterTree parameterWithAddress:0];
float deltaTime = 1.0 / 44100.0;
__block float time = 0;
return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, NSInteger outputBusNumber, AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock pullInputBlock) {
AudioBufferList *abl = outputData;
for (int frame = 0; frame < frameCount; frame++) {
float value = sin(2.0 * M_PI * frequencyParam.value * time);
for (int i = 0; i < abl->mNumberBuffers; i++) {
float *output = abl->mBuffers[i].mData;
output[frame] = value;
}
time += deltaTime;
}
return noErr;
};
}
@end
I have a Swift class that wraps it and that I use to bind the frequency to a SwiftUI slider:
class VCO: ObservableObject {
@Published var frequency = 220.0 {
didSet {
guard
let parameterTree = unit.auAudioUnit.parameterTree,
let freqParam = parameterTree.parameter(withAddress: 0) else {
return
}
freqParam.setValue(AUValue(frequency), originator: nil)
}
}
var unit: AVAudioUnit!
private let componentDescription = AudioComponentDescription(componentType: kAudioUnitType_Generator,
componentSubType: OSType(1),
componentManufacturer: 0x666f6f20,
componentFlags: 0,
componentFlagsMask: 0
)
init() {
AUAudioUnit.registerSubclass(VCOUnit.self, as: componentDescription, name: "VCO", version: 1)
}
func setup(sampleRate: Double) {
let opts = AudioComponentInstantiationOptions.init(rawValue: 0)
AVAudioUnit.instantiate(with: componentDescription, options: opts) { [weak self] (unit, error) in
guard let unit = unit else {
print(error!)
return
}
self?.unit = unit
}
}
}
Of course this isn't production code at this stage, but I suspect the reason for the cracks and pops is something quite specific. I was even starting to think if it has to do with me using SwiftUI bindings to update or something, but it does sound a bit far fetched to me. It's more likely the problem is in the render block, but I can't see it at the moment. :)
Anyway, any input is appreciated.
Upvotes: 0
Views: 340
Reputation: 70703
This is a typical bug caused by incrementing time and calculating a new phase (for the sine function parameter) every loop iteration, which produces phase discontinuities when changing frequencies.
One simple solution is to increment phase instead of time. It's easy to compute the phase delta per unit time for a given frequency, and do that computation outside the inner loop. Since this phase delta is small, there will be no big phase discontinuities to produce crackles and pops.
Calculating a new phase this way each iteration only requires addition, not multiplication (by 2.0 * M_PI, etc.).
You might also want to bound the range of the phase inside +- M_PI or +-2pi, instead of letting it grow arbitrarily large, which can cause range reduction errors inside the sine function.
Upvotes: 1