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;

Check warning on line 1189 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1189

Added line #L1189 was not covered by tests
} 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);

Check warning on line 1201 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1201

Added line #L1201 was not covered by tests
} 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];

Check warning on line 1224 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1222-L1224

Added lines #L1222 - L1224 were not covered by tests

audioDeviceUserData *audioDevice = userdataToAudioDevice(L, 1);
AudioDeviceID deviceId = audioDevice->deviceId;
unsigned int scope;
UInt32 thru = lua_toboolean(L, 2);
UInt32 thruSize = sizeof(UInt32);

Check warning on line 1230 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1226-L1230

Added lines #L1226 - L1230 were not covered by tests

if (isOutputDevice(deviceId)) {
scope = kAudioObjectPropertyScopeOutput;
} else {
scope = kAudioObjectPropertyScopeInput;
}

Check warning on line 1236 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1232-L1236

Added lines #L1232 - L1236 were not covered by tests

AudioObjectPropertyAddress propertyAddress = {
kAudioDevicePropertyPlayThru,
scope,
kAudioObjectPropertyElementMain
};

Check warning on line 1242 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1238-L1242

Added lines #L1238 - L1242 were not covered by tests

if (AudioObjectHasProperty(deviceId, &propertyAddress) && (AudioObjectSetPropertyData(deviceId, &propertyAddress, 0, NULL, thruSize, &thru) == noErr)) {
lua_pushboolean(L, TRUE);
} else {
lua_pushboolean(L, FALSE);
}

Check warning on line 1248 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1244-L1248

Added lines #L1244 - L1248 were not covered by tests

return 1;
}

Check warning on line 1251 in extensions/audiodevice/libaudiodevice.m

View check run for this annotation

Codecov / codecov/patch

extensions/audiodevice/libaudiodevice.m#L1250-L1251

Added lines #L1250 - L1251 were not covered by tests

/// 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.