2
\$\begingroup\$

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?
\$\endgroup\$
1
\$\begingroup\$

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)")
}
\$\endgroup\$
  • \$\begingroup\$ Wow, thank you! The only thing I'm asking myself is why we don't need the .bigEndian in the argument of fourCCToString. Could you elaborate? \$\endgroup\$ – user101048 Mar 3 '17 at 21:32
  • 1
    \$\begingroup\$ @Herickson: 'HELO' as a four character code is 0x48454C4F, which on a little-endian platform is stored as 4F 4C 45 48 in memory. Therefore you have to swap the byte order to get the bytes into the right order (when interpreted as "string"). – On the other hand, the fourCCToString function puts the bytes into the right order when creating the array. \$\endgroup\$ – Martin R Mar 3 '17 at 21:45
  • 1
    \$\begingroup\$ @Herickson: There are currently no big-endian platforms running iOS or macOS :) But as far as I remember from when there was a PowerPC Mac, 'HELO' was 0x48454C4F as well (and 48 45 4C 4F in memory). The above code would then work correctly. \$\endgroup\$ – Martin R Mar 5 '17 at 16:31
  • 1
    \$\begingroup\$ @Herickson: You are thinking too complicated now. If value = 0x48454C4F then value >> 24 = 0x48 and value >> 16 = 0x4845 independent of endianness. \$\endgroup\$ – Martin R Mar 5 '17 at 16:44
  • 1
    \$\begingroup\$ Oh, bit shifting is endianness independent. I only need to consider endianness, when I was about to interpret the whole chunk of memory (the UInt32) as String. You helped me so much the last few days. Thank you so much! \$\endgroup\$ – user101048 Mar 5 '17 at 16:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy