2
\$\begingroup\$

Due to some bug somewhere, my speaker balance kept moving off-center and it was getting annoying. So I cobbled together the code below (based on this question) which—to my surprise—does compile and run on macOS 11.1 / Swift 5.3.2.

I am a beginner and am trying to learn, trying to avoid errors and adhere to the DRY principle. I would also like to know if there are any glaring mistakes.

For example, I believe there is a way to make a single function that can return the default Audio Device as well as fetch properties from it, since there are a lot of similarities between getDefaultOutputDevice() and aPropGet()

I also am not at all sure about the use of the struct {...} to declare "global" variables.

Can anyone please pass along any pointers?

To compile save the code below as bal-reset.swift and build with swiftc -O bal_reset.swift

import AudioToolbox
import CoreAudio
import Foundation

// global variables
struct g {
    static var deviceId: AudioDeviceID = 0
    static var deviceName = "" as CFString
    static var eq_balance: Float32 = 0.50 // 0.0 (left) through 1.0 (right)
}

func getDefaultOutputDevice() {
    var aSize = UInt32(MemoryLayout.size(ofValue: g.deviceId))
    var address = AudioObjectPropertyAddress(
        mSelector: kAudioHardwarePropertyDefaultOutputDevice,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMaster)
    var err = AudioObjectGetPropertyData(
        AudioObjectID(kAudioObjectSystemObject),
        &address,
        0,
        nil,
        &aSize,
        &g.deviceId)
    if (err == 0) {
        address.mSelector = kAudioDevicePropertyDeviceNameCFString
        aSize = UInt32(MemoryLayout.size(ofValue: g.deviceName))
        err = AudioObjectGetPropertyData(
            g.deviceId,
            &address,
            0,
            nil,
            &aSize,
            &g.deviceName)
        if (err == 0) {
            print("dev:", String(g.deviceName) + " [" + String(g.deviceId) + "]")
        } else {
            print("dev:", g.deviceId)
        }
    } else {
        print("error [" + String(err) + "] could not determine output device") 
        exit(1)
    }
}
getDefaultOutputDevice()

func aPropGet(selector: UInt32) -> (err: OSStatus, val: Float32) {
    var val: Float32 = 0.0
    var aSize = UInt32(MemoryLayout.size(ofValue: val))
    var address = AudioObjectPropertyAddress(
        mSelector: selector,
        mScope: kAudioObjectPropertyScopeOutput,
        mElement: kAudioObjectPropertyElementMaster
    )
    let err = AudioObjectGetPropertyData(
        g.deviceId,
        &address,
        0,
        nil,
        &aSize,
        &val)
    return (err, val)
}

func setBalance(selector: UInt32) -> OSStatus {
    let aSize = UInt32(MemoryLayout.size(ofValue: g.eq_balance))
    var address = AudioObjectPropertyAddress(
        mSelector: selector,
        mScope: kAudioObjectPropertyScopeOutput,
        mElement: kAudioObjectPropertyElementMaster
    )
    let err = AudioObjectSetPropertyData(
        g.deviceId,
        &address,
        0,
        nil,
        aSize,
        &g.eq_balance)
    return err
}

func getBalance() -> (err: OSStatus, val: Float32) {
    var res = aPropGet(selector: kAudioDevicePropertyStereoPan)
    if (res.err != 0) {
        res = aPropGet(selector: kAudioHardwareServiceDeviceProperty_VirtualMasterBalance)
    }
    return (res.err, res.val)
}

var res = aPropGet(selector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume)
if (res.err == 0) {
    print("vol:", String(format: "%.0f", res.val*100))
}

res = getBalance()
switch res.err {
    case 0:
        print("old:", String(format: "%.2f", res.val))
    case kAudioCodecUnknownPropertyError:
        print("device does not support balance adjustment")
        exit(0)
    default:
        print("unknown error [" + String(res.err) + "] while trying to query balance")
        exit(1)
}

switch res.val {
    case 0.5:
        exit(0)
    default:
        var bal = setBalance(selector: kAudioDevicePropertyStereoPan)
        if (bal != 0) {
            bal = setBalance(selector: kAudioHardwareServiceDeviceProperty_VirtualMasterBalance)
        }
        switch bal {
            case 0:
                print("new:", String(format: "%.2f", g.eq_balance))
                exit(0)
            default:
                print("error [" + String(bal) + "] while trying to set balance")
                exit(1)
        }
}
\$\endgroup\$
1
  • \$\begingroup\$ Just want to note that there's another utility (undoubtedly better-written) that does this as well: see Wevah/sndctl \$\endgroup\$
    – luckman212
    Commented Jun 16, 2023 at 21:39

1 Answer 1

1
\$\begingroup\$

Use string interpolation:

"unknown error [" + String(res.err) + "] while trying to query balance"

is normally written like this:

"unknown error [\(String(res.err))] while trying to query balance"

Split out functions into pieces dealing with different issues

For example, your getDefaultOutputDevice function has lines to do with the mechanics of calling the API (MemoryLayout.size(ofValue... etc), and lines to do with dealing with errors (print error... etc)

Splitting those things up into separate pieces unlocks the parts

Use a suitable existing type or make new ones, to avoid global scope. Your struct "g" can be avoided completely by sticking to functions that return values. I would be wary about storing any of the values you read back from the system, like the name or the value of the balance etc because they are volatile and can change without you changing them, so for example, if name is always read afresh each time you need it then it's never stale data.

Here is how you might use generics to wrap the get and set calls:

import AudioToolbox
import CoreAudio
import Foundation

// The API uses AudioObjectPropertyAddress structs
// Let's make some helpers to make working with these struts easier.
extension AudioObjectPropertyAddress {

    /// We have to use backticks here because we want to use a swift keyword
    /// for our variable name, or we could call it something else like defaultOutput
    static var `default`: Self {
        .init(
            mSelector: kAudioHardwarePropertyDefaultOutputDevice,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMaster)
    }
    
    static var masterOutput: Self {
        .init(
            mSelector: kAudioHardwarePropertyDefaultOutputDevice,
            mScope: kAudioObjectPropertyScopeOutput,
            mElement: kAudioObjectPropertyElementMaster)
    }
    
    /// Take our existing self, and return a new struct with just the scope
    /// different
    func scoped(_ s: AudioObjectPropertyScope) -> Self {
        .init(mSelector: mSelector, mScope: s, mElement: mElement)
    }
    
    /// same for element etc
    func elemented(_ e: AudioObjectPropertyScope) -> Self {
        .init(mSelector: mSelector, mScope: mScope, mElement: e)
    }
    
    func selectored(_ s: AudioObjectPropertySelector) -> Self {
        .init(mSelector: s, mScope: mScope, mElement: mElement)
    }
}

/// With these helpers in place we can be more consise at the call site,
/// transforming these values to say something like default master output, but selecting
/// the balance property
//let example = AudioObjectPropertyAddress
//    .default
//    .selectored(kAudioHardwareServiceDeviceProperty_VirtualMasterBalance)


/// The API emits errors, so wrapping it to make it more Swifty, I wrap those OSStatus errors
/// in swift's Error
struct OSStatusError: Swift.Error {
    var value: Int32
}


/// The audio API likes to use inout arguments. These functions are of a type
/// f(address, inout Something)
/// these can always be converted into equivalent functions but of type f(address, something) -> something
/// and that can make them easier to work with
/// the pattern to use it is:
/// make a new Float or something, pass that into GetPropertyData, check for errors
/// let's wrap the noise involved in making one of these calls:
extension AudioObjectID {
    /// We have to use a generic here because the function can deal with any type of
    /// data from AudioObject
    func get<T>(_ address: AudioObjectPropertyAddress,
                v: inout T) throws {
        var addr = address
        var size = UInt32(MemoryLayout.size(ofValue: v))
        let err = AudioObjectGetPropertyData(self, &addr, 0, nil, &size, &v)
        
        if err != 0 {
            throw OSStatusError(value: err)
        }
    }
    
    /// And we prefer using the f(A, V) -> V style rather than the f(A, inout V) style
    /// so let's make that
    func get<T>(_ address: AudioObjectPropertyAddress,
                v: T) throws -> T {
        var intoV = v
        
        try get(address, v: &intoV)
        
        return intoV
    }

    /// Set is similar, you can make this be like f(A, X) -> X if you want
    /// typically a setter would return void but there is the chance that the value is
    /// set to something other than the value you specified
    func set<T>(_ address: AudioObjectPropertyAddress,
                v: inout T) throws {
        var address = address
        let size = UInt32(MemoryLayout.size(ofValue: v))
        
        let err = AudioObjectSetPropertyData(self, &address, 0, nil, size, &v)
        
        if err != 0 {
            throw OSStatusError(value: err)
        }
    }
}


/// With that in place we can extend an existing type, to make it more ergonomic to use
extension AudioDeviceID {
    static func defaultOutput() throws -> Self {
        try AudioObjectID(kAudioObjectSystemObject)
            .get(.default,
                 v: AudioObjectID())  // Here we have to instantiate an instance of the type
        // that's then used in the inout getting dance
    }

    func name() throws -> String {
        try get(AudioObjectPropertyAddress
                    .default
                    .selectored(kAudioDevicePropertyDeviceNameCFString),
                v: "" as CFString) as String
    }
    
    func balance() throws -> Float {
        try get(AudioObjectPropertyAddress
                    .masterOutput
                    .selectored(kAudioHardwareServiceDeviceProperty_VirtualMasterBalance),
                 v: Float(0))
    }
    
    func setBalance(_ target: Float) throws -> Float {
        let existing = try balance()
        guard existing != target else {
            return existing
        }
        
        var result = target
        try set(AudioObjectPropertyAddress
                    .masterOutput
                    .selectored(kAudioHardwareServiceDeviceProperty_VirtualMasterBalance),
                v: &result)
        return result
    }
}

/// With all these helpers in place, and with the throwing and catching,
/// our program now reads like what it does
do {
    try AudioDeviceID.defaultOutput().setBalance(0.5)
}
catch {
    print(error)
    exit(1)
}

Using throwing functions cleans up the code, there is one place where we deal with any errors.

Overall, we split a function like getDefaultOutputDevice(andPrintErrors...) into pieces that deal with getting stuff from the audio api, the default device, and handling errors, and that breaking stuff up is normally a good idea. Avoid side effects in functions too, like your print(error)/exit in getDefaultOutputDevice, these side effects make the functions harder to test and harder to combine together.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.