From 251f5af0ff414037a8a02e8dee78b8fc7cfc2d4e Mon Sep 17 00:00:00 2001 From: Blake Burkhart Date: Tue, 19 Nov 2024 23:35:56 -0600 Subject: [PATCH] Add hs.audiodevice:thru() and hs.audiodevice:setThru(thru) Get or set the play through (low-latency/direct monitoring) state of the the audio device via `kAudioDevicePropertyPlayThru`. This is the feature of some microphones where they can play their input directly to their output (e.g. headphone jack) so you can get low-latency feedback while recording. * This only works on devices that have hardware support (often microphones with a built-in headphone jack) * This setting corresponds to the "Thru" setting in Audio MIDI Setup --- Hammerspoon Tests/HSaudiodevice.m | 4 + extensions/audiodevice/libaudiodevice.m | 90 +++++++++++++++++++++ extensions/audiodevice/test_audiodevice.lua | 15 ++++ 3 files changed, 109 insertions(+) diff --git a/Hammerspoon Tests/HSaudiodevice.m b/Hammerspoon Tests/HSaudiodevice.m index d0229c71b..4b8b9555a 100644 --- a/Hammerspoon Tests/HSaudiodevice.m +++ b/Hammerspoon Tests/HSaudiodevice.m @@ -144,6 +144,10 @@ - (void)testMute { RUN_LUA_TEST() } +- (void)testThru { + RUN_LUA_TEST() +} + - (void)testVolume { SKIP_IN_TRAVIS() RUN_LUA_TEST() diff --git a/extensions/audiodevice/libaudiodevice.m b/extensions/audiodevice/libaudiodevice.m index a6aa76d53..f2beef7a1 100644 --- a/extensions/audiodevice/libaudiodevice.m +++ b/extensions/audiodevice/libaudiodevice.m @@ -1162,6 +1162,94 @@ static int audiodevice_setbalance(lua_State* L) { } +/// hs.audiodevice:thru() -> bool or nil +/// Method +/// Get the play through (low latency/direct monitoring) state of the audio device +/// +/// Parameters: +/// * None +/// +/// Returns: +/// * True if the audio device has thru enabled, False if thru is disabled, nil if it does not support thru +/// +/// Notes: +/// * This method only works on devices that have hardware support (often microphones with a built-in headphone jack) +/// * This setting corresponds to the "Thru" setting in Audio MIDI Setup +static int audiodevice_thru(lua_State* L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; + + audioDeviceUserData *audioDevice = userdataToAudioDevice(L, 1); + AudioDeviceID deviceId = audioDevice->deviceId; + unsigned int scope; + UInt32 thru; + UInt32 thruSize = sizeof(UInt32); + + if (isOutputDevice(deviceId)) { + scope = kAudioObjectPropertyScopeOutput; + } else { + scope = kAudioObjectPropertyScopeInput; + } + + AudioObjectPropertyAddress propertyAddress = { + kAudioDevicePropertyPlayThru, + scope, + kAudioObjectPropertyElementMain + }; + + if (AudioObjectHasProperty(deviceId, &propertyAddress) && (AudioObjectGetPropertyData(deviceId, &propertyAddress, 0, NULL, &thruSize, &thru) == noErr)) { + lua_pushboolean(L, thru != 0); + } else { + lua_pushnil(L); + } + + return 1; +} + +/// hs.audiodevice:setThru(thru) -> bool +/// Method +/// Set the play through (low latency/direct monitoring) state of the audio device +/// +/// Parameters: +/// * thru - A boolean value. True to enable thru, False to disable +/// +/// Returns: +/// * True if thru was set, False if the audio device does not support thru +/// +/// Notes: +/// * This method only works on devices that have hardware support (often microphones with a built-in headphone jack) +/// * This setting corresponds to the "Thru" setting in Audio MIDI Setup +static int audiodevice_setThru(lua_State* L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBOOLEAN, LS_TBREAK]; + + audioDeviceUserData *audioDevice = userdataToAudioDevice(L, 1); + AudioDeviceID deviceId = audioDevice->deviceId; + unsigned int scope; + UInt32 thru = lua_toboolean(L, 2); + UInt32 thruSize = sizeof(UInt32); + + if (isOutputDevice(deviceId)) { + scope = kAudioObjectPropertyScopeOutput; + } else { + scope = kAudioObjectPropertyScopeInput; + } + + AudioObjectPropertyAddress propertyAddress = { + kAudioDevicePropertyPlayThru, + scope, + kAudioObjectPropertyElementMain + }; + + if (AudioObjectHasProperty(deviceId, &propertyAddress) && (AudioObjectSetPropertyData(deviceId, &propertyAddress, 0, NULL, thruSize, &thru) == noErr)) { + lua_pushboolean(L, TRUE); + } else { + lua_pushboolean(L, FALSE); + } + + return 1; +} + /// hs.audiodevice:isOutputDevice() -> boolean /// Method /// Determines if an audio device is an output device @@ -1900,6 +1988,8 @@ static int datasource_eq(lua_State* L) { {"setVolume", audiodevice_setvolume}, {"balance", audiodevice_balance}, {"setBalance", audiodevice_setbalance}, + {"thru", audiodevice_thru}, + {"setThru", audiodevice_setThru}, {"setInputVolume", audiodevice_setInputVolume}, {"setOutputVolume", audiodevice_setOutputVolume}, {"muted", audiodevice_muted}, diff --git a/extensions/audiodevice/test_audiodevice.lua b/extensions/audiodevice/test_audiodevice.lua index ecf502719..8ac019a38 100644 --- a/extensions/audiodevice/test_audiodevice.lua +++ b/extensions/audiodevice/test_audiodevice.lua @@ -158,6 +158,21 @@ function testMute() return success() end +function testThru() + local device = hs.audiodevice.defaultInputDevice() + local wasThru = device:thru() + if (type(wasThru) ~= "boolean") then + -- This device does not support thru. Not much we can do about it, so log it and move on + print("Audiodevice does not support thru, unable to test thru functionality. Skipping test due to lack of hardware") + return success() + end + device:setThru(not wasThru) + assertIsEqual(not wasThru, device:thru()) + -- Be nice to whoever is running the test and restore the original state + device:setThru(wasThru) + return success() +end + function testJackConnected() local jackConnected = hs.audiodevice.defaultOutputDevice():jackConnected() if (type(jackConnected) ~= "boolean") then