Skip to content

Commit

Permalink
Add hs.audiodevice:thru() and hs.audiodevice:setThru(thru)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bburky committed Nov 20, 2024
1 parent 37057a0 commit 6a75b8a
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Hammerspoon Tests/HSaudiodevice.m
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ - (void)testMute {
RUN_LUA_TEST()
}

- (void)testThru {
RUN_LUA_TEST()
}

- (void)testVolume {
SKIP_IN_TRAVIS()
RUN_LUA_TEST()
Expand Down
90 changes: 90 additions & 0 deletions extensions/audiodevice/libaudiodevice.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
Expand Down
15 changes: 15 additions & 0 deletions extensions/audiodevice/test_audiodevice.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6a75b8a

Please sign in to comment.