Skip to content

Commit a0d1ca3

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents b8f0cd6 + 931d38a commit a0d1ca3

File tree

4 files changed

+148
-4
lines changed

4 files changed

+148
-4
lines changed

Hammerspoon Tests/HSaudiodevice.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ - (void)testMute {
144144
RUN_LUA_TEST()
145145
}
146146

147+
- (void)testThru {
148+
RUN_LUA_TEST()
149+
}
150+
147151
- (void)testVolume {
148152
SKIP_IN_TRAVIS()
149153
RUN_LUA_TEST()

extensions/audiodevice/libaudiodevice.m

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,94 @@ static int audiodevice_setbalance(lua_State* L) {
11621162

11631163
}
11641164

1165+
/// hs.audiodevice:thru() -> bool or nil
1166+
/// Method
1167+
/// Get the play through (low latency/direct monitoring) state of the audio device
1168+
///
1169+
/// Parameters:
1170+
/// * None
1171+
///
1172+
/// Returns:
1173+
/// * True if the audio device has thru enabled, False if thru is disabled, nil if it does not support thru
1174+
///
1175+
/// Notes:
1176+
/// * This method only works on devices that have hardware support (often microphones with a built-in headphone jack)
1177+
/// * This setting corresponds to the "Thru" setting in Audio MIDI Setup
1178+
static int audiodevice_thru(lua_State* L) {
1179+
LuaSkin *skin = [LuaSkin sharedWithState:L];
1180+
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK];
1181+
1182+
audioDeviceUserData *audioDevice = userdataToAudioDevice(L, 1);
1183+
AudioDeviceID deviceId = audioDevice->deviceId;
1184+
unsigned int scope;
1185+
UInt32 thru;
1186+
UInt32 thruSize = sizeof(UInt32);
1187+
1188+
if (isOutputDevice(deviceId)) {
1189+
scope = kAudioObjectPropertyScopeOutput;
1190+
} else {
1191+
scope = kAudioObjectPropertyScopeInput;
1192+
}
1193+
1194+
AudioObjectPropertyAddress propertyAddress = {
1195+
kAudioDevicePropertyPlayThru,
1196+
scope,
1197+
kAudioObjectPropertyElementMain
1198+
};
1199+
1200+
if (AudioObjectHasProperty(deviceId, &propertyAddress) && (AudioObjectGetPropertyData(deviceId, &propertyAddress, 0, NULL, &thruSize, &thru) == noErr)) {
1201+
lua_pushboolean(L, thru != 0);
1202+
} else {
1203+
lua_pushnil(L);
1204+
}
1205+
1206+
return 1;
1207+
}
1208+
1209+
/// hs.audiodevice:setThru(thru) -> bool
1210+
/// Method
1211+
/// Set the play through (low latency/direct monitoring) state of the audio device
1212+
///
1213+
/// Parameters:
1214+
/// * thru - A boolean value. True to enable thru, False to disable
1215+
///
1216+
/// Returns:
1217+
/// * True if thru was set, False if the audio device does not support thru
1218+
///
1219+
/// Notes:
1220+
/// * This method only works on devices that have hardware support (often microphones with a built-in headphone jack)
1221+
/// * This setting corresponds to the "Thru" setting in Audio MIDI Setup
1222+
static int audiodevice_setThru(lua_State* L) {
1223+
LuaSkin *skin = [LuaSkin sharedWithState:L];
1224+
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBOOLEAN, LS_TBREAK];
1225+
1226+
audioDeviceUserData *audioDevice = userdataToAudioDevice(L, 1);
1227+
AudioDeviceID deviceId = audioDevice->deviceId;
1228+
unsigned int scope;
1229+
UInt32 thru = lua_toboolean(L, 2);
1230+
UInt32 thruSize = sizeof(UInt32);
1231+
1232+
if (isOutputDevice(deviceId)) {
1233+
scope = kAudioObjectPropertyScopeOutput;
1234+
} else {
1235+
scope = kAudioObjectPropertyScopeInput;
1236+
}
1237+
1238+
AudioObjectPropertyAddress propertyAddress = {
1239+
kAudioDevicePropertyPlayThru,
1240+
scope,
1241+
kAudioObjectPropertyElementMain
1242+
};
1243+
1244+
if (AudioObjectHasProperty(deviceId, &propertyAddress) && (AudioObjectSetPropertyData(deviceId, &propertyAddress, 0, NULL, thruSize, &thru) == noErr)) {
1245+
lua_pushboolean(L, TRUE);
1246+
} else {
1247+
lua_pushboolean(L, FALSE);
1248+
}
1249+
1250+
return 1;
1251+
}
1252+
11651253
/// hs.audiodevice:isOutputDevice() -> boolean
11661254
/// Method
11671255
/// Determines if an audio device is an output device
@@ -1900,6 +1988,8 @@ static int datasource_eq(lua_State* L) {
19001988
{"setVolume", audiodevice_setvolume},
19011989
{"balance", audiodevice_balance},
19021990
{"setBalance", audiodevice_setbalance},
1991+
{"thru", audiodevice_thru},
1992+
{"setThru", audiodevice_setThru},
19031993
{"setInputVolume", audiodevice_setInputVolume},
19041994
{"setOutputVolume", audiodevice_setOutputVolume},
19051995
{"muted", audiodevice_muted},

extensions/audiodevice/test_audiodevice.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ function testMute()
158158
return success()
159159
end
160160

161+
function testThru()
162+
local device = hs.audiodevice.defaultInputDevice()
163+
local wasThru = device:thru()
164+
if (type(wasThru) ~= "boolean") then
165+
-- This device does not support thru. Not much we can do about it, so log it and move on
166+
print("Audiodevice does not support thru, unable to test thru functionality. Skipping test due to lack of hardware")
167+
return success()
168+
end
169+
device:setThru(not wasThru)
170+
assertIsEqual(not wasThru, device:thru())
171+
-- Be nice to whoever is running the test and restore the original state
172+
device:setThru(wasThru)
173+
return success()
174+
end
175+
161176
function testJackConnected()
162177
local jackConnected = hs.audiodevice.defaultOutputDevice():jackConnected()
163178
if (type(jackConnected) ~= "boolean") then

extensions/ipc/ipc.lua

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,54 @@ local MSG_ID = {
3333
CONSOLE = 3, -- cloned console output
3434
}
3535

36+
-- stop printReplacement from being reentrant
37+
-- otherwise errors might cascade into lots of recursive prints
38+
-- hammerspoon is single threaded, thus this does not need a semaphore
39+
-- otherwise we'll have to deal with a potential race condition
40+
module.insidePrintInstances = {}
41+
42+
module.print_enter = function(instance)
43+
val = module.insidePrintInstances[instance] or 0
44+
module.insidePrintInstances[instance] = val + 1
45+
end
46+
47+
module.print_exit = function(instance)
48+
-- make sure instance exists
49+
if module.insidePrintInstances[instance] then
50+
module.insidePrintInstances[instance] = module.insidePrintInstances[instance] - 1
51+
-- make sure to delete the entry from the table to avoid
52+
-- growing forever
53+
if module.insidePrintInstances[instance] == 0 then
54+
module.insidePrintInstances[instance] = nil
55+
end
56+
end
57+
end
58+
59+
module.print_inside = function(instance)
60+
-- return true if we are already inside printReplacement
61+
val = module.insidePrintInstances[instance]
62+
return val and val > 0
63+
end
64+
3665
local originalPrint = print
3766
local printReplacement = function(...)
3867
originalPrint(...)
39-
for _,v in pairs(module.__registeredCLIInstances) do
68+
for id,v in pairs(module.__registeredCLIInstances) do
4069
if v._cli.console and v.print and not v._cli.quietMode then
41-
-- v.print(...)
42-
-- make it more obvious what is console output versus the command line's
70+
if module.print_inside(id) then
71+
log.w(string.format("Instance of [%s] already recursing, refusing request.", id))
72+
else
73+
module.print_enter(id)
74+
-- v.print(...)
75+
-- make it more obvious what is console output versus the command line's
4376
local things = table.pack(...)
4477
local stdout = (things.n > 0) and tostring(things[1]) or ""
4578
for i = 2, things.n do
46-
stdout = stdout .. "\t" .. tostring(things[i])
79+
stdout = stdout .. "\t" .. tostring(things[i])
4780
end
4881
v._cli.remote:sendMessage(stdout .. "\n", MSG_ID.CONSOLE)
82+
module.print_exit(id)
83+
end
4984
end
5085
end
5186
end

0 commit comments

Comments
 (0)