diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 478c6c706..29953e6c3 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -818,6 +818,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { lossless = lossless, oscMode = oscMode, onScreenButtonsVisible = onScreenButtonsVisible, suspendPolicy = suspendPolicy, steamDownloadsInBackground = steamDownloadsInBackground, touchMode = touchMode, + micAvailable = SessionState.micAvailable && !SessionState.suspended, + micEnabled = SessionState.micEnabled, + onMicEnabled = { SessionService.setMicEnabled(this@SessionActivity, it) }, touchAuto = getString(if (usingTouchpad()) R.string.session_touch_auto_touchpad else R.string.session_touch_auto_direct), fexPreset = fexPreset, secondScreenMode = secondScreenMode, diff --git a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java index f1fa53492..f8f1b2a3e 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java @@ -3,6 +3,7 @@ package com.droiddeck.launcher.audio; import android.os.Process; import android.util.Log; +import com.droiddeck.launcher.core.FileUtils; import com.droiddeck.launcher.core.SessionPart; import com.droiddeck.launcher.core.HostProcess; @@ -19,10 +20,13 @@ public class DirectAudioRelayComponent extends SessionPart { private volatile int pid = -1; private final Object micLock = new Object(); private boolean micHot; + private final File micControl; + public java.util.function.Consumer onMicEnabled; public DirectAudioRelayComponent(File socketPath, File micFifoPath) { this.socketPath = socketPath; this.micFifoPath = micFifoPath; + this.micControl = micFifoPath == null ? null : new File(micFifoPath.getParentFile(), "mic-enabled"); } public void setLogFile(File file) { @@ -63,8 +67,13 @@ public class DirectAudioRelayComponent extends SessionPart { StringBuilder command = new StringBuilder(binary.getAbsolutePath()); command.append(" --socket ").append(socketPath.getAbsolutePath()); command.append(" --log"); - if (micFifoPath != null) { + final boolean withMic = micFifoPath != null && setMicEnabled(true); + if (withMic) { command.append(" --mic-fifo ").append(micFifoPath.getAbsolutePath()); + command.append(" --mic-control ").append(micControl.getAbsolutePath()); + } else if (micFifoPath != null) { + Log.w(TAG, "microphone control unavailable; starting with capture disabled"); + command.append(" --mic-control /dev/null"); } ArrayList env = new ArrayList<>(); env.add("HOME=" + app().getFilesDir()); @@ -73,7 +82,7 @@ public class DirectAudioRelayComponent extends SessionPart { try { out = new java.io.PrintWriter(new java.io.FileWriter(logFile, true)); out.println("== DirectAudio relay helper starting" - + (micFifoPath != null ? " with the microphone" : " (no microphone)")); + + (withMic ? " with the microphone" : " (no microphone)")); out.flush(); } catch (Exception e) { Log.w(TAG, "could not open " + logFile, e); @@ -84,14 +93,23 @@ public class DirectAudioRelayComponent extends SessionPart { app().getFilesDir(), null, line -> { Log.i(TAG, line); - trackMic(line); + if (withMic) trackMic(line); if (log != null) synchronized (log) { log.println(line); log.flush(); } }); Log.i(TAG, "started pid=" + pid + " socket=" + socketPath - + (micFifoPath != null ? " mic=" + micFifoPath : " (no microphone)")); + + (withMic ? " mic=" + micFifoPath : " (no microphone)")); + } + + public synchronized boolean setMicEnabled(boolean enabled) { + if (micControl == null) return false; + File next = new File(micControl.getPath() + ".new"); + return FileUtils.writeString(next, enabled ? "1" : "0") && next.renameTo(micControl); } private void trackMic(String line) { + if (line.startsWith("DA-Relay: mic enabled=") && onMicEnabled != null) { + onMicEnabled.accept(line.equals("DA-Relay: mic enabled=1")); + } boolean start = line.startsWith("DA-Relay: mic start "); if (!start && !line.startsWith("DA-Relay: mic stop ")) return; synchronized (micLock) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 1293d1d60..fd8d8d373 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -278,6 +278,16 @@ class SessionService : Service() { updateSuspendPolicy() return START_NOT_STICKY } + ACTION_MIC_CHANGED -> { + if (SessionState.running && SessionState.micAvailable && !SessionState.suspended) { + val enabled = intent.getBooleanExtra("enabled", false) + val relay = components.firstNotNullOfOrNull { it as? DirectAudioRelayComponent } + if (relay?.setMicEnabled(enabled) != true) { + android.widget.Toast.makeText(this, R.string.drawer_mic_failed, android.widget.Toast.LENGTH_LONG).show() + } + } + return START_NOT_STICKY + } ACTION_SUSPEND_POLICY_CHANGED -> { if (!SessionState.running) return START_NOT_STICKY suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode) @@ -329,6 +339,8 @@ class SessionService : Service() { freezePending = false suspendAttemptFailed = false suspendController = null + SessionState.micAvailable = false + SessionState.micEnabled = false SessionState.suspended = false SessionState.resumeRequested = false SessionState.resuming = false @@ -761,6 +773,15 @@ class SessionService : Service() { components.add(pulse) if (wantsDirectAudio || wantsMic) { val relay = DirectAudioRelayComponent(relaySocket, micFifo) + val gen = sessionGen + relay.onMicEnabled = java.util.function.Consumer { enabled -> + mainHandler.post { + if (gen == sessionGen && SessionState.running && wantsMic) { + SessionState.micAvailable = true + SessionState.micEnabled = enabled + } + } + } relay.setLogFile(audioLog) relay.attach(this) components.add(relay) @@ -1656,6 +1677,8 @@ class SessionService : Service() { SessionState.running = false SessionPaths.take() to SessionState.steamClientArch } + SessionState.micAvailable = false + SessionState.micEnabled = false xrActivityVisible = false updateDownloadMonitoring() val stoppedGen = sessionGen @@ -1948,6 +1971,7 @@ class SessionService : Service() { const val ACTION_LAUNCH_GAME = "com.droiddeck.launcher.LAUNCH_STEAM_GAME" const val ACTION_HOME_GUIDE = "com.droiddeck.launcher.HOME_GUIDE" const val ACTION_AGENT_START = "com.droiddeck.launcher.AGENT_START" + private const val ACTION_MIC_CHANGED = "com.droiddeck.launcher.MIC_CHANGED" private const val ACTION_SUSPEND_POLICY_CHANGED = "com.droiddeck.launcher.SUSPEND_POLICY_CHANGED" private const val ACTION_ACTIVITY_VISIBLE = "com.droiddeck.launcher.ACTIVITY_VISIBLE" private const val ACTION_ACTIVITY_HIDDEN = "com.droiddeck.launcher.ACTIVITY_HIDDEN" @@ -2078,6 +2102,11 @@ class SessionService : Service() { }.getOrDefault(false) } + fun setMicEnabled(context: Context, enabled: Boolean) { + context.startService(Intent(context, SessionService::class.java).setAction(ACTION_MIC_CHANGED) + .putExtra("enabled", enabled)) + } + fun suspendPolicyChanged(context: Context) { if (!SessionState.running) return context.startService(Intent(context, SessionService::class.java).setAction(ACTION_SUSPEND_POLICY_CHANGED)) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt index 5ff739e47..6bdece304 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt @@ -46,6 +46,8 @@ object SessionState { var activityDisplayId = 0 @Volatile var runtimeDriver: String? = null + var micAvailable by mutableStateOf(false) + var micEnabled by mutableStateOf(false) var suspended by mutableStateOf(false) var resumeRequested by mutableStateOf(false) var resuming by mutableStateOf(false) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 58b4c11ee..ef90316ea 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -208,6 +208,9 @@ class DrawerActions( val onScreenButtonsVisible: Boolean, val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, + val micAvailable: Boolean = false, + val micEnabled: Boolean = false, + val onMicEnabled: (Boolean) -> Unit = {}, val backActionsInverted: Boolean, val padType: String? = null, val onPadType: (String) -> Unit = {}, @@ -669,6 +672,9 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont } } SettingsGroup(stringResource(R.string.drawer_session_behavior)) { + if (a.micAvailable) ToggleRow(host, "microphone", stringResource(R.string.mode_mic), + stringResource(R.string.drawer_mic_hint), a.micEnabled, + chipModifier = focus.track(page, "microphone"), onChange = a.onMicEnabled) ChoiceRow( host, "suspend", stringResource(R.string.mode_suspend), null, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f42580fff..d0d524647 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -389,6 +389,8 @@ Bypasses PulseAudio for lower latency. Microphone For voice chat. + Turn microphone capture on or off for this session. + Could not change the microphone. Please try again. Network Wi-Fi discovery Show Wi-Fi names and nearby networks in Steam. diff --git a/app/src/test/java/com/droiddeck/launcher/audio/DirectAudioMicrophoneTest.kt b/app/src/test/java/com/droiddeck/launcher/audio/DirectAudioMicrophoneTest.kt new file mode 100644 index 000000000..88a211b85 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/audio/DirectAudioMicrophoneTest.kt @@ -0,0 +1,86 @@ +package com.droiddeck.launcher.audio + +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.rules.TemporaryFolder +import org.robolectric.RuntimeEnvironment +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.util.ReflectionHelpers +import java.io.File +import java.util.function.Consumer + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28]) +class DirectAudioMicrophoneTest { + @get:Rule val folder = TemporaryFolder() + + @Test fun toggleOnlyChangesTheSessionCaptureControl() { + val fifo = File(folder.root, "mic.fifo") + val relay = DirectAudioRelayComponent(File(folder.root, "relay.sock"), fifo) + val changes = mutableListOf() + relay.onMicEnabled = Consumer { changes.add(it) } + val control = File(folder.root, "mic-enabled") + assertTrue(relay.setMicEnabled(false)) + assertEquals("0", control.readText()) + assertTrue(changes.isEmpty()) + log(relay, "DA-Relay: mic enabled=0") + assertEquals(listOf(false), changes) + assertTrue(relay.setMicEnabled(true)) + assertEquals("1", control.readText()) + log(relay, "DA-Relay: mic enabled=1") + assertEquals(listOf(false, true), changes) + assertFalse(fifo.exists()) + } + + @Test fun sessionWithoutCaptureCannotEnableIt() { + val relay = DirectAudioRelayComponent(File(folder.root, "relay.sock"), null) + assertFalse(relay.setMicEnabled(true)) + assertFalse(File(folder.root, "mic-enabled").exists()) + } + + @Test fun captureDemandDoesNotOverrideTheAcknowledgedToggle() { + val relay = DirectAudioRelayComponent(File(folder.root, "relay.sock"), File(folder.root, "mic.fifo")) + val changes = mutableListOf() + relay.onMicEnabled = Consumer { changes.add(it) } + log(relay, "DA-Relay: mic enabled=0") + log(relay, "DA-Relay: mic start (game wants it, 1 consumer(s))") + log(relay, "DA-Relay: mic stop (game)") + assertEquals(listOf(false), changes) + assertTrue(relay.awaitMicReleased(0)) + } + + @Test fun controlWriteFailureStillStartsPlaybackWithCaptureDisabled() { + val context = RuntimeEnvironment.getApplication() + val info = context.applicationInfo + val previous = info.nativeLibraryDir + val nativeDir = folder.newFolder("native") + val args = File(folder.root, "arguments") + File(nativeDir, DirectAudioRelayComponent.BINARY).apply { + writeText("#!/bin/sh\nprintf '%s\\n' \"\$@\" > '${args.path}.new'\nmv '${args.path}.new' '${args.path}'\n") + setExecutable(true) + } + val fifo = folder.newFile("mic.fifo") + File(folder.root, "mic-enabled.new").mkdir() + val relay = DirectAudioRelayComponent(File(folder.root, "relay.sock"), fifo) + relay.attach(context) + try { + info.nativeLibraryDir = nativeDir.path + relay.start() + val deadline = System.nanoTime() + 3_000_000_000L + while (!args.isFile && System.nanoTime() < deadline) Thread.sleep(10) + assertTrue("Playback helper did not start", args.isFile) + val command = args.readLines() + assertTrue(command.contains("--socket")) + assertFalse(command.contains("--mic-fifo")) + assertEquals(listOf("--mic-control", "/dev/null"), command.takeLast(2)) + } finally { + info.nativeLibraryDir = previous + } + } + + private fun log(relay: DirectAudioRelayComponent, line: String) = + ReflectionHelpers.callInstanceMethod(relay, "trackMic", ReflectionHelpers.ClassParameter.from(String::class.java, line)) +} diff --git a/app/src/test/java/com/droiddeck/launcher/ui/SessionSecondScreenTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/SessionSecondScreenTest.kt index 04d3f5632..735632e61 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/SessionSecondScreenTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/SessionSecondScreenTest.kt @@ -9,6 +9,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.compose.ui.test.* +import androidx.compose.ui.semantics.SemanticsProperties.StateDescription import androidx.compose.runtime.MutableState import com.droiddeck.launcher.R import com.droiddeck.launcher.SessionActivity @@ -97,6 +98,42 @@ class SessionSecondScreenTest { } } + @Test fun sessionMicrophoneCanBeTurnedOff() { + val changes = mutableListOf() + compose.setContent { + DroidDeckTheme { + SessionDrawer(true, SessionDrawerPage.SESSION, false, {}, + actions(true, emptyList(), micAvailable = true, micEnabled = true, onMic = changes::add)) + } + } + compose.onNodeWithTag("setting-microphone").assertIsEnabled() + .assert(SemanticsMatcher.expectValue(StateDescription, context.getString(R.string.widgets_on))).performClick() + assertEquals(listOf(false), changes) + } + + @Test fun sessionMicrophoneCanBeTurnedBackOn() { + val changes = mutableListOf() + compose.setContent { + DroidDeckTheme { + SessionDrawer(true, SessionDrawerPage.SESSION, false, {}, + actions(true, emptyList(), micAvailable = true, onMic = changes::add)) + } + } + compose.onNodeWithTag("setting-microphone").assertIsEnabled() + .assert(SemanticsMatcher.expectValue(StateDescription, context.getString(R.string.widgets_off))).performClick() + assertEquals(listOf(true), changes) + } + + @Test fun sessionWithoutMicrophoneOmitsCaptureControl() { + compose.setContent { + DroidDeckTheme { + SessionDrawer(true, SessionDrawerPage.SESSION, false, {}, actions(false, emptyList())) + } + } + compose.onNodeWithTag("setting-microphone").assertDoesNotExist() + compose.onNodeWithText(context.getString(R.string.mode_mic)).assertDoesNotExist() + } + private fun checkModes(steam: Boolean) { SessionState.mode = if (steam) SessionService.MODE_STEAM else SessionService.MODE_DESKTOP val selected = mutableListOf() @@ -145,8 +182,11 @@ class SessionSecondScreenTest { if (view is ViewGroup) for (index in 0 until view.childCount) yieldAll(descendants(view.getChildAt(index))) } - private fun actions(steam: Boolean, displays: List, select: (SecondScreenMode) -> Unit = {}) = + private fun actions(steam: Boolean, displays: List, + micAvailable: Boolean = false, micEnabled: Boolean = false, onMic: (Boolean) -> Unit = {}, + select: (SecondScreenMode) -> Unit = {}) = DrawerActions(steam = steam, isHomeApp = false, androidApps = emptyList(), hudOn = false, + micAvailable = micAvailable, micEnabled = micEnabled, onMicEnabled = onMic, frameGen = FrameGen.Mode.OFF, lossless = Lossless.State.NONE, oscMode = "never", onScreenButtonsVisible = false, suspendPolicy = "auto", backActionsInverted = false, touchMode = "auto", touchAuto = "trackpad", fexPreset = "default", diff --git a/tools/components/recipes.json b/tools/components/recipes.json index 19144018f..602fb379e 100644 --- a/tools/components/recipes.json +++ b/tools/components/recipes.json @@ -110,7 +110,8 @@ ], "outputs": [ "app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so" - ] + ], + "ndk": true }, "audio": { "script": "tools/components/recipes/audio.sh", diff --git a/tools/directaudio/build-relay.sh b/tools/directaudio/build-relay.sh new file mode 100755 index 000000000..59c071616 --- /dev/null +++ b/tools/directaudio/build-relay.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root=$1 +source_dir=$2 +: "${NDK:=${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}}}" +: "${NDK:?set NDK to the Android NDK root}" +case "$(uname -s)" in + Darwin) host=darwin-x86_64 ;; + Linux) host=linux-x86_64 ;; + *) echo 'Unsupported NDK host' >&2; exit 1 ;; +esac +toolchain="$NDK/toolchains/llvm/prebuilt/$host/bin" +patch -d "$source_dir" -p1 < "$repo_root/tools/directaudio/microphone-toggle.patch" +"$toolchain/aarch64-linux-android28-clang" -O2 -Wl,-z,max-page-size=16384 \ + "$source_dir/directaudio-relay.c" -o "$repo_root/app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so" \ + -laaudio -llog -pthread +"$toolchain/llvm-strip" --strip-unneeded "$repo_root/app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so" diff --git a/tools/directaudio/fetch.sh b/tools/directaudio/fetch.sh index af886304f..35e327382 100755 --- a/tools/directaudio/fetch.sh +++ b/tools/directaudio/fetch.sh @@ -3,7 +3,7 @@ set -euo pipefail repo_root=$1 sink_out=$2 . "${repo_root}/tools/directaudio/release.env" -for v in DIRECTAUDIO_REPO DIRECTAUDIO_TAG DIRECTAUDIO_WINE11_SHA256 DIRECTAUDIO_SYSTHREAD_SHA256 DIRECTAUDIO_RELAY_SHA256 DIRECTAUDIO_SINK_SHA256; do +for v in DIRECTAUDIO_REPO DIRECTAUDIO_TAG DIRECTAUDIO_WINE11_SHA256 DIRECTAUDIO_SYSTHREAD_SHA256 DIRECTAUDIO_SINK_SHA256 DIRECTAUDIO_SOURCE_COMMIT DIRECTAUDIO_SOURCE_SHA256; do [[ -n "${!v:-}" && "${!v}" != TBD ]] || { echo "tools/directaudio/release.env: ${v} is not set" >&2; exit 1; } done work=$(mktemp -d) @@ -24,7 +24,15 @@ fetch() { } fetch directaudio-linux-wine11.zip "${DIRECTAUDIO_WINE11_SHA256}" fetch directaudio-linux-wine11-systhread.zip "${DIRECTAUDIO_SYSTHREAD_SHA256}" -fetch directaudio-linux-relay.zip "${DIRECTAUDIO_RELAY_SHA256}" +source_archive="${cache}/${DIRECTAUDIO_SOURCE_SHA256}-directaudio-source.tar.gz" +if ! echo "${DIRECTAUDIO_SOURCE_SHA256} ${source_archive}" | sha256sum -c - >/dev/null 2>&1; then + curl -fsSL --retry 3 -o "${work}/source.tar.gz" \ + "https://github.com/${DIRECTAUDIO_REPO}/archive/${DIRECTAUDIO_SOURCE_COMMIT}.tar.gz" + echo "${DIRECTAUDIO_SOURCE_SHA256} ${work}/source.tar.gz" | sha256sum -c - >/dev/null + mv "${work}/source.tar.gz" "${source_archive}" +fi +mkdir "${work}/source" +tar -xzf "${source_archive}" --strip-components=1 -C "${work}/source" fetch directaudio-linux-sink.zip "${DIRECTAUDIO_SINK_SHA256}" assets="${repo_root}/app/src/main/assets/directaudio" @@ -40,9 +48,8 @@ done grep -q '^abi wine11$' "${assets}/linux-wine11/version.txt" grep -q '^abi systhread$' "${assets}/linux-wine11-systhread/version.txt" -mkdir -p "${work}/relay" "${work}/sink" "${sink_out}" "${repo_root}/app/src/main/jniLibs/arm64-v8a" -unzip -q "${work}/directaudio-linux-relay.zip" -d "${work}/relay" -install -m755 "${work}/relay/libdirectaudiorelay.so" "${repo_root}/app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so" +mkdir -p "${work}/sink" "${sink_out}" "${repo_root}/app/src/main/jniLibs/arm64-v8a" +"${repo_root}/tools/directaudio/build-relay.sh" "${repo_root}" "${work}/source" unzip -q "${work}/directaudio-linux-sink.zip" -d "${work}/sink" install -m755 "${work}/sink/module-directaudio-native-sink.so" "${sink_out}/" echo "DirectAudio ${DIRECTAUDIO_TAG}: two driver sets, the relay helper and the client sink in place" diff --git a/tools/directaudio/microphone-toggle.patch b/tools/directaudio/microphone-toggle.patch new file mode 100644 index 000000000..22af228c0 --- /dev/null +++ b/tools/directaudio/microphone-toggle.patch @@ -0,0 +1,123 @@ +--- a/directaudio-relay.c ++++ b/directaudio-relay.c +@@ -70,6 +70,9 @@ + * session log captures it too. Event-level lines are always on: they fire a + * handful of times per session and are what makes a field report actionable. */ + static int g_verbose; ++static const char *g_mic_control; ++static int g_mic_enabled = 1; ++static pthread_mutex_t g_mic_operations = PTHREAD_MUTEX_INITIALIZER; + + static void rlog(const char *fmt, ...) + { +@@ -735,6 +738,9 @@ + (void)user; + if (aq != __atomic_load_n(&g_mic.cq, __ATOMIC_SEQ_CST) && __atomic_load_n(&g_mic.cq, __ATOMIC_SEQ_CST)) + return AAUDIO_CALLBACK_RESULT_CONTINUE; ++ if (!__atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE)) ++ memset(audioData, 0, numFrames * g_mic.in_channels * ++ (g_mic.in_format == AAUDIO_FORMAT_PCM_I16 ? sizeof(int16_t) : sizeof(float))); + __atomic_add_fetch(&g_mic.cb_count, 1, __ATOMIC_RELAXED); + + pthread_mutex_lock(&g_clients_lock); +@@ -832,13 +838,14 @@ + AAudioStream *aq = NULL; + int start = 0, stop = 0; + ++ pthread_mutex_lock(&g_mic_operations); + pthread_mutex_lock(&g_mic.lock); + g_mic.want += delta; + if (g_mic.want < 0) g_mic.want = 0; + if (g_mic.cq) + { +- if (g_mic.want > 0 && !g_mic.started) { g_mic.started = 1; start = 1; aq = g_mic.cq; } +- if (g_mic.want == 0 && g_mic.started) { g_mic.started = 0; stop = 1; aq = g_mic.cq; } ++ if (g_mic.want > 0 && __atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE) && !g_mic.started) { g_mic.started = 1; start = 1; aq = g_mic.cq; } ++ if ((g_mic.want == 0 || !__atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE)) && g_mic.started) { g_mic.started = 0; stop = 1; aq = g_mic.cq; } + } + pthread_mutex_unlock(&g_mic.lock); + +@@ -856,8 +863,23 @@ + else if (stop) + { + AAudioStream_requestStop(aq); +- REVENT("mic stop (%s was the last consumer)", who); +- } ++ REVENT("mic stop (%s)", who); ++ } ++ pthread_mutex_unlock(&g_mic_operations); ++} ++ ++static void mic_control_update(void) ++{ ++ FILE *f; ++ int enabled; ++ if (!g_mic_control) return; ++ f = fopen(g_mic_control, "r"); ++ enabled = f && fgetc(f) == '1'; ++ if (f) fclose(f); ++ if (enabled == __atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE)) return; ++ __atomic_store_n(&g_mic_enabled, enabled, __ATOMIC_RELEASE); ++ mic_want(0, "session toggle"); ++ REVENT("mic enabled=%d", enabled); + } + + static void *mic_reopen_thread(void *unused) +@@ -870,8 +892,9 @@ + + __atomic_store_n(&g_mic.reopen_redo, 0, __ATOMIC_SEQ_CST); + ++ pthread_mutex_lock(&g_mic_operations); + pthread_mutex_lock(&g_mic.lock); +- want_start = g_mic.want > 0; ++ want_start = g_mic.want > 0 && __atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE); + pthread_mutex_unlock(&g_mic.lock); + + if (open_input(&neu) == AAUDIO_OK) +@@ -902,6 +925,7 @@ + AAudioStream_close(old); + } + ++ pthread_mutex_unlock(&g_mic_operations); + if (__atomic_load_n(&g_mic.reopen_redo, __ATOMIC_SEQ_CST)) { usleep(20000); continue; } + __atomic_store_n(&g_mic.reopen_running, 0, __ATOMIC_SEQ_CST); + if (!__atomic_load_n(&g_mic.reopen_redo, __ATOMIC_SEQ_CST)) break; +@@ -1348,7 +1372,8 @@ + /* ---- housekeeping: watchdog tick + mailbox watch, once a second ------------- */ + static void *housekeeping_thread(void *unused) + { +- struct timespec iv = { 1, 0 }; ++ struct timespec iv = { 0, 100000000 }; ++ unsigned tick = 0; + + (void)unused; + for (;;) +@@ -1357,6 +1382,8 @@ + int reload = 0; + + nanosleep(&iv, NULL); ++ mic_control_update(); ++ if (++tick % 10) continue; + + if (g_rt_path[0]) + { +@@ -1402,8 +1429,9 @@ + { + if (!strcmp(argv[i], "--socket") && i + 1 < argc) path = argv[++i]; + else if (!strcmp(argv[i], "--mic-fifo") && i + 1 < argc) fifo_path = argv[++i]; ++ else if (!strcmp(argv[i], "--mic-control") && i + 1 < argc) g_mic_control = argv[++i]; + else if (!strcmp(argv[i], "--log")) g_verbose = 1; +- else { fprintf(stderr, "usage: directaudio-relay --socket [--mic-fifo ] [--log]\n"); return 2; } ++ else { fprintf(stderr, "usage: directaudio-relay --socket [--mic-fifo ] [--mic-control ] [--log]\n"); return 2; } + } + if (!path) + { +@@ -1429,6 +1457,8 @@ + chmod(path, 0666); + if (listen(srv, 8) != 0) { REVENT("listen: %d", errno); return 1; } + ++ mic_control_update(); ++ REVENT("mic enabled=%d", __atomic_load_n(&g_mic_enabled, __ATOMIC_ACQUIRE)); + if (pthread_create(&th, NULL, housekeeping_thread, NULL) == 0) pthread_detach(th); + if (fifo_path && fifo_setup(fifo_path) != 0) + REVENT("mic-fifo: disabled"); diff --git a/tools/directaudio/release.env b/tools/directaudio/release.env index 5e052f0bb..95359b3ae 100644 --- a/tools/directaudio/release.env +++ b/tools/directaudio/release.env @@ -2,5 +2,6 @@ DIRECTAUDIO_REPO=The412Banner/directaudio DIRECTAUDIO_TAG=directaudio-linux-v1.1.0 DIRECTAUDIO_WINE11_SHA256=835e6cab072f5148893c43379e60506483aeb3f1444e777b430c295cbeb472c9 DIRECTAUDIO_SYSTHREAD_SHA256=2ddc47f446142d5940540ad40a423ca8d47fd536e3225b888df1077c6be7329e -DIRECTAUDIO_RELAY_SHA256=b68eb6d7a892fd92a1cd73fac36dc05cabb77cc73792e2c582d3eeccad355942 DIRECTAUDIO_SINK_SHA256=1d5d3ed709e5389ef7603f019c62b07e4e8428bb42882193d4a144a5c15425b6 +DIRECTAUDIO_SOURCE_COMMIT=52aedbf8ddd42874a5bbd21df20e1046367f442e +DIRECTAUDIO_SOURCE_SHA256=a20c9a0684ae3b1e60a6b9c17b7f1d5ec2501627ae9683aa9f391576a6c1974d