I've recently got interested in generated audio again, but I'm having a bit of trouble. I've been following this tutorial, and converting it into Swift:
https://gist.github.com/gcatlin/0dd61f19d40804173d015c01a80461b8
However, when I play back by audio, all I get is some rather icky white noise effects rather than the pure tone I was expecting. Here's the code I'm using to create the tone unit:
private func createToneUnit() throws {
// Configure the search parameters to find the default playback output unit
var outputDesc = AudioComponentDescription()
outputDesc.componentType = kAudioUnitType_Output
outputDesc.componentSubType = kAudioUnitSubType_RemoteIO
outputDesc.componentManufacturer = kAudioUnitManufacturer_Apple
outputDesc.componentFlags = 0
outputDesc.componentFlagsMask = 0
// Get the default playback output unit
guard let output = AudioComponentFindNext(nil, &outputDesc) else {
throw AudioError.cannotFindOutput
}
// Create a new unit based on this that we'll use for output
var error = AudioComponentInstanceNew(output, &toneUnit)
guard let toneUnit = toneUnit, error == noErr else {
throw AudioError.cannotCreateComponent
}
// Set our tone rendering function on the unit
var callback = AURenderCallbackStruct()
callback.inputProcRefCon = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
callback.inputProc = {
(userData, actionFlags, timeStamp, busNumber, frameCount, data) -> OSStatus in
let _self = Unmanaged<MainViewController>.fromOpaque(userData).takeUnretainedValue()
return _self.renderTone(actionFlags: actionFlags, timeStamp: timeStamp, busNumber: busNumber, frameCount: frameCount, data: data)
}
error = AudioUnitSetProperty(
toneUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0,
&callback,
UInt32(MemoryLayout.size(ofValue: callback))
)
guard error == noErr else {
throw AudioError.cannotSetCallback
}
// Set the format to 32 bit, single channel, floating point, linear PCM
var streamFormat = AudioStreamBasicDescription()
streamFormat.mSampleRate = sampleRate
streamFormat.mFormatID = kAudioFormatLinearPCM
streamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
streamFormat.mFramesPerPacket = 1
streamFormat.mChannelsPerFrame = 1
streamFormat.mBitsPerChannel = 16
streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame * streamFormat.mBitsPerChannel / 8
streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame * streamFormat.mFramesPerPacket
error = AudioUnitSetProperty(
toneUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&streamFormat,
UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
)
guard error == noErr else {
throw AudioError.cannotSetStreamFormat
}
}
And here's the render function:
func renderTone(
actionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
timeStamp: UnsafePointer<AudioTimeStamp>,
busNumber: UInt32,
frameCount: UInt32,
data: UnsafeMutablePointer<AudioBufferList>?
) -> OSStatus {
// Get buffer
let bufferList = UnsafeMutableAudioBufferListPointer(data!)
let increment = MainViewController.fullCycle * frequency / sampleRate
// Generate samples
for buffer in bufferList {
for frame in 0 ..< frameCount {
if let audioData = buffer.mData?.assumingMemoryBound(to: Float64.self) {
audioData[Int(frame)] = sin(theta) * amplitude
}
// Note: this would NOT work for a stereo output
theta += increment
while theta > MainViewController.fullCycle {
theta -= MainViewController.fullCycle
}
}
}
return noErr;
}
Anyone see anything obviously bad about this? I'd really much rather be using Swift than Obj C but I can't find a working example of how to accomplish this, only some (admittedly useful) partial examples about how to set things up that don't actually perform any tone rendering.
You told the remote IO audio unit that you were going to give it integer data:
streamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
But then you gave it float data, which sounds bad when interpreted as integer (and also stomps memory because Float64
s are bigger than Int16
s).
if let audioData = buffer.mData?.assumingMemoryBound(to: Float64.self) {
audioData[Int(frame)] = sin(theta) * amplitude
}
So you should make the two agree. One way is to make the render callback match the stream format by producing 16 bit integer data:
if let audioData = buffer.mData?.assumingMemoryBound(to: Int16.self) {
audioData[Int(frame)] = Int16(sin(theta) * amplitude * 32767)
}
Or you could keep producing float data (maybe not 64 bit floats, not sure that's supported) and make the stream format match that. You'd probably a few swift casts to fix up.
Which brings me around to another probable issue. I'm pretty sure Swift still breaks the realtime audio rules that you can read about in this article (note: article pre-dates swift) so by using Swift in a render callback or similar API you may hear clicks/pops and dropouts.