Skip to content

Commit

Permalink
Merge pull request raycast#366 from rolandleth/set-audio-improvements
Browse files Browse the repository at this point in the history
Set audio improvements
  • Loading branch information
imthath-m authored Apr 21, 2021
2 parents a55cfeb + e45ccfb commit 5ee75cb
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 16 deletions.
179 changes: 179 additions & 0 deletions commands/system/audio/get-selected-audio-device.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/swift

// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Audio devices
// @raycast.mode inline
// @raycast.refreshTime 10m

// Optional parameters:
// @raycast.icon 🔈
// @raycast.packageName Audio

// Documentation:
// @raycast.description Shows the selected audio devices for input and output
// @raycast.author Roland Leth
// @raycast.authorURL https://runtimesharks.com

import Foundation
import CoreAudio

// Based on https://stackoverflow.com/a/58618034/793916

final class AudioDevice {

let audioDeviceID: AudioDeviceID

init(deviceID: AudioDeviceID) {
self.audioDeviceID = deviceID
}

var hasOutput: Bool {
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress(
mSelector: AudioObjectPropertySelector(kAudioDevicePropertyStreamConfiguration),
mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput),
mElement: AudioObjectPropertyElement(0))

var propSize: UInt32 = 0
var result = AudioObjectGetPropertyDataSize(
audioDeviceID,
&address,
UInt32(MemoryLayout<AudioObjectPropertyAddress>.size),
nil,
&propSize)

if (result != 0) {
return false
}

let bufferList = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: Int(propSize))

defer {
bufferList.deallocate()
}

result = AudioObjectGetPropertyData(audioDeviceID, &address, 0, nil, &propSize, bufferList)

if (result != 0) {
return false
}

let buffers = UnsafeMutableAudioBufferListPointer(bufferList)

return buffers.contains { $0.mNumberChannels > 0 }
}

var name: String? {
var address = AudioObjectPropertyAddress(
mSelector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceNameCFString),
mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal),
mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster))

var name: CFString? = nil
var propSize = UInt32(MemoryLayout<CFString?>.size)
let result = AudioObjectGetPropertyData(audioDeviceID, &address, 0, nil, &propSize, &name)

if (result != 0) {
return nil
}

return name as String?
}

}

func findDevices() -> [AudioDevice] {
var address = AudioObjectPropertyAddress(
mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDevices),
mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal),
mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster))

var propSize: UInt32 = 0
var result = AudioObjectGetPropertyDataSize(
AudioObjectID(kAudioObjectSystemObject),
&address,
UInt32(MemoryLayout<AudioObjectPropertyAddress>.size),
nil,
&propSize)

if (result != 0) {
print("Error \(result) from AudioObjectGetPropertyDataSize")
return []
}

let numDevices = Int(propSize / UInt32(MemoryLayout<AudioDeviceID>.size))
var devids = Array<AudioDeviceID>(repeating: AudioDeviceID(), count: numDevices)

result = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
&propSize,
&devids)

if (result != 0) {
print("Error \(result) from AudioObjectGetPropertyData")
return []
}

return (0..<numDevices).compactMap { i in
AudioDevice(deviceID: devids[i])
}

}

let devices = findDevices()
let inputs = devices.filter { !$0.hasOutput }
let outputs = devices.filter { $0.hasOutput }

func selectedDevice(output: Bool) -> String? {
var id = AudioObjectID(kAudioObjectSystemObject)
var idSize = UInt32(MemoryLayout.size(ofValue: id))
let selector = output
? kAudioHardwarePropertyDefaultOutputDevice
: kAudioHardwarePropertyDefaultInputDevice

var idPropertyAddress = AudioObjectPropertyAddress(
mSelector: AudioObjectPropertySelector(selector),
mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal),
mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster))

let result = AudioObjectGetPropertyData(
id,
&idPropertyAddress,
0,
nil,
&idSize,
&id)

if (result != 0) {
return nil
}

return (output ? outputs : inputs).first { $0.audioDeviceID == id }?.name
}

let outputDevice = selectedDevice(output: true)
let inputDevice = selectedDevice(output: false)

let inputName = inputs
.compactMap(\.name)
.filter { inputDevice == $0 }
.first
let outputName = outputs
.compactMap(\.name)
.filter { outputDevice == $0 }
.first

switch (inputName, outputName) {
case (let inputName?, let outputName?):
print("\(inputName) | \(outputName)")
case (let inputName?, nil):
print("\(inputName)")
case (nil, let outputName?):
print("\(outputName)")
case (nil, nil):
print("No devices found")
}

89 changes: 73 additions & 16 deletions commands/system/audio/set-audio-device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,55 @@
// Optional parameters:
// @raycast.icon 🎧
// @raycast.argument1 { "type": "text", "placeholder": "Name" }
// @raycast.argument2 { "type": "text", "placeholder": "Input (y/n)", "optional": true }
// @raycast.argument2 { "type": "text", "placeholder": "Type (i/o/b)", "optional": true }
// @raycast.packageName Audio

// Documentation:
// @raycast.description Sets an input or output audio device, based on name
// @raycast.description Sets the input (i), the output (o) or both (b) audio sources, based on name. If `both` is passed, but no input or output device is found with the given name, it will still try to set the other one. For example, if you're trying to set both to "External mic", which doesn't have an input source, it will still set the output to the mic; vice-versa for a speaker.
// @raycast.author Roland Leth
// @raycast.authorURL https://runtimesharks.com

// Change lines 29 and 30 if you'd like another default,
// which currently sets both when no parameter is passed.

let arguments = Array(CommandLine.arguments.dropFirst())
let query = arguments.first!
let changeType: DeviceType = arguments.count >= 2
? ["input", "i"].contains(arguments[1])
? .input
: ["output", "o"].contains(arguments[1])
? .output
: .both
: .both

import Foundation
import CoreAudio

// Based on https://stackoverflow.com/a/58618034/793916

struct DeviceType: OptionSet {

static let input = DeviceType(rawValue: 1 << 0)
static let output = DeviceType(rawValue: 1 << 1)
static let both: DeviceType = [.input, .output]

let rawValue: Int

var value: String {
switch self {
case .input:
return "input"
case .output:
return "output"
case .both:
return "both"
default:
return ""
}
}

}

final class AudioDevice {

let audioDeviceID: AudioDeviceID
Expand Down Expand Up @@ -78,10 +114,6 @@ final class AudioDevice {

}

let arguments = Array(CommandLine.arguments.dropFirst())
let query = arguments.first!
let shouldChangeInput = arguments.count >= 2 && ["yes", "y", "true"].contains(arguments[1])

func findDevices() -> [AudioDevice] {
var propSize: UInt32 = 0

Expand Down Expand Up @@ -124,24 +156,23 @@ func findDevices() -> [AudioDevice] {

}

func setDevice(to query: String) {
@discardableResult
func set(_ deviceType: DeviceType, to query: String) -> (Bool, String) {
let devices = findDevices()
let deviceType = shouldChangeInput ? "input" : "output"

guard
let device = devices.first(where: {
$0.name?.localizedCaseInsensitiveContains(query) == true
&& (shouldChangeInput ? !$0.hasOutput : $0.hasOutput)
&& (deviceType.contains(.input) ? !$0.hasOutput : $0.hasOutput)
})
else {
print("Could not find \(deviceType) device \(query)")
exit(1)
return (false, query)
}

let deviceName = device.name ?? query
var deviceId = device.audioDeviceID
let deviceIdSize = UInt32(MemoryLayout.size(ofValue: deviceId))
let selector = shouldChangeInput
let selector = deviceType.contains(.input)
? kAudioHardwarePropertyDefaultInputDevice
: kAudioHardwarePropertyDefaultOutputDevice

Expand All @@ -159,11 +190,37 @@ func setDevice(to query: String) {
&deviceId)

if (result != 0) {
print("Could not set \(deviceType) to \(deviceName)")
exit(0)
return (false, deviceName)
}

print("Set \(deviceType) to \(deviceName)")
return (true, deviceName)
}

setDevice(to: query)
switch changeType {
case .input,
.output:
let i = set(changeType, to: query)

guard i.0 else {
print("Could not set \(changeType.value) to \(i.1)")
exit(1)
}

print("Set \(changeType.value) to \(i.1)")
case .both:
let i = set(.input, to: query)
let o = set(.output, to: query)

switch (i.0, o.0) {
case (false, false):
print("Could not set any device to \(i.1)")
case (true, false):
print("Set input to \(i.1)")
case (false, true):
print("Set output to \(i.1)")
case (true, true):
print("Set both to \(i.1) & \(o.1)")
}
default:
exit(1)
}

0 comments on commit 5ee75cb

Please sign in to comment.