CommandLine StreamFormatTester

Posted on

Problem

Using Chris Adamson’s book Learning Core Audio I’m getting familiar with Core Audio for Mac/iOS. The code examples are 100% Objective-C. I’m trying to follow along using Swift 3.

This small piece of software is used to find usable AudioStreamBasicDescriptions for a given file type and format.

import Foundation
import AudioToolbox


// Create ID for specific fileType and fileFormat
var fileTypeAndFormat = AudioFileTypeAndFormatID()
fileTypeAndFormat.mFileType = kAudioFileCAFType
fileTypeAndFormat.mFormatID = kAudioFormatLinearPCM

// Target: Gather possible AudioStreamBasicDescriptions for given fileType and fileFormat
// Get size of array containing all available AudioStreamBasicDescriptions
var possibleError = noErr
var infoSize: UInt32 = 0

possibleError = AudioFileGetGlobalInfoSize(kAudioFileGlobalInfo_AvailableStreamDescriptionsForFormat,
 UInt32(MemoryLayout<AudioFileTypeAndFormatID>.stride),
 &fileTypeAndFormat,
 &infoSize)
assert(possibleError == noErr)

// Get number of AudioStreamBasicDescription stored in array
let asbdCount = Int(infoSize) / MemoryLayout<AudioStreamBasicDescription>.stride

// Allocate memory for array to store AudioStreamBasicDescriptions in
var asbds = [AudioStreamBasicDescription](repeating: AudioStreamBasicDescription(), count: asbdCount)

// Fill array with all available AudioStreamBasicDescriptions
possibleError = AudioFileGetGlobalInfo(kAudioFileGlobalInfo_AvailableStreamDescriptionsForFormat,
 UInt32(MemoryLayout<AudioFileTypeAndFormatID>.stride),
 &fileTypeAndFormat,
 &infoSize,
 &asbds)
assert(possibleError == noErr)

// Print each AudioStreamBasicDescription of array
for n in 0..<asbdCount {
    var reformattedID = asbds[n].mFormatID.bigEndian

    // TODO: This seems too ugly to be true
    withUnsafePointer(to: &reformattedID) { stringIDPtr in
        print(String(format: "%d: mFormatID: %s, mFormatFlags: %d, mBitsPerChannel: %d", n, stringIDPtr,
                     asbds[n].mFormatFlags, asbds[n].mBitsPerChannel))
    }
}

In an production environment the asserts should be replaced with better error handling, but correct error handling was not the purpose of this script.

Specific parts I’d like to get a review on:

  • The memory allocation to store available StreamDescriptions for format in. Is the allocation via array Swift-style-conform?
  • Getting the mFormatID of an AudioStreamBasicDescription via withUnsafePointer(to: &reformattedID) seems utterly unpleasant. Can this be realized in a more elegant way?

Solution

The “file and format ID” can be created in one step as

var fileTypeAndFormat = AudioFileTypeAndFormatID(mFileType: kAudioFileCAFType,
 mFormatID: kAudioFormatLinearPCM)

Using a Swift Array as storage is fine, and has the advantage that
the memory is managed automatically (contrary to memory from malloc
or UnsafeMutablePointer.alloc()). The array creation can be written
a tiny be shorter as

var audioDescriptions = Array(repeating: AudioStreamBasicDescription(), count: asbdCount)

because the element type is inferred from the first argument. I have
also chosen a different name, asbds is very short and cryptic.

For-loops are often better replaced by an enumeration:

for audioDesc in audioDescriptions {
    // ...
}

or, if you need the element index as well:

for (idx, audioDesc) in audioDescriptions.enumerated() {
    // ...
}

This saves array lookups inside the loop.

Treating the “four-character code” in mFormatID as a string is
dangerous and causes undefined behaviour, because the %s format
expects a zero-terminated sequence of chars.
You can get additional unwanted output, or the program might crash.

You could use the %4.4s format instead, which limits the output
to four characters:

for (idx, audioDesc) in audioDescriptions.enumerated() {
    var reformattedID = audioDesc.mFormatID.bigEndian

    withUnsafePointer(to: &reformattedID) { stringIDPtr in
        print(String(format: "%d: mFormatID: %4.4s, mFormatFlags: %d, mBitsPerChannel: %d",
                     idx, stringIDPtr,
                     audioDesc.mFormatFlags, audioDesc.mBitsPerChannel))
    }
}

A better solution is to convert the integer to a four-character
string, e.g. using one of the methods in
How to convert memory of Int32 as four characters, for example

func fourCCToString(_ value: FourCharCode) -> String {
    let utf16 = [
        UInt16((value >> 24) & 0xFF),
        UInt16((value >> 16) & 0xFF),
        UInt16((value >> 8) & 0xFF),
        UInt16((value & 0xFF)) ]
    return String(utf16CodeUnits: utf16, count: 4)
}

which is then used as

for (idx, audioDesc) in audioDescriptions.enumerated() {
    let reformattedID = fourCCToString(audioDesc.mFormatID)

    print(String(format: "%d: mFormatID: %@, mFormatFlags: %d, mBitsPerChannel: %d",
                 idx, reformattedID,
                 audioDesc.mFormatFlags, audioDesc.mBitsPerChannel))
}

Note that a Swift string is printed with the %@ format.

Or simply use string interpolation:

for (idx, audioDesc) in audioDescriptions.enumerated() {
    let reformattedID = fourCCToString(audioDesc.mFormatID)

    print("(idx): mFormatID: (reformattedID), mFormatFlags: (audioDesc.mFormatFlags), mBitsPerChannel: (audioDesc.mBitsPerChannel)")
}

Leave a Reply

Your email address will not be published. Required fields are marked *