docs(ios): drop stale debug comments + TeamTalk refs from audio router
After the stereo-mic/A2DP debugging settled, the iOS audio code carried leftover TeamTalk5 comparison notes, source-line citations, TEMP DIAGNOSTIC markers, and "this was the bug" narratives that no longer help. Reworded those to state the current rules; kept the comments that document real constraints (the setPreferredInputNumberOfChannels(2) trap, the re-entrancy guard, the ma_context no-session-management config). Comment-only — no behavior change. core builds, ctest --preset dev 21/21.
This commit is contained in:
@@ -349,9 +349,8 @@ public final class VoiceCatClient {
|
|||||||
/// AVAudioSession (setCategory, setPreferredInput, setPreferredPolarPattern, etc.) so the
|
/// AVAudioSession (setCategory, setPreferredInput, setPreferredPolarPattern, etc.) so the
|
||||||
/// core's devices reopen against the new route. Unlike `audioSuspend()`/`audioResume()`
|
/// core's devices reopen against the new route. Unlike `audioSuspend()`/`audioResume()`
|
||||||
/// (which only stop/start the existing devices, leaving them bound to the route that was
|
/// (which only stop/start the existing devices, leaving them bound to the route that was
|
||||||
/// active when they were opened), this fully re-initializes them. Mirrors TeamTalk5's
|
/// active when they were opened), this fully re-initializes them. Safe to call when the
|
||||||
/// closeSoundDevices()/initSoundInputDevice()/initSoundOutputDevice() pattern. Safe to
|
/// engine is not running (it will just start it).
|
||||||
/// call when the engine is not running (it will just start it).
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func audioRestart() -> VoiceCatResult {
|
public func audioRestart() -> VoiceCatResult {
|
||||||
VoiceCatResult(vc_audio_restart(handle))
|
VoiceCatResult(vc_audio_restart(handle))
|
||||||
|
|||||||
@@ -16,12 +16,10 @@ final class AudioSessionManager {
|
|||||||
var activeMicStreamId: UInt32?
|
var activeMicStreamId: UInt32?
|
||||||
|
|
||||||
/// Tracks whether WE activated the session. The session must be active whenever the
|
/// Tracks whether WE activated the session. The session must be active whenever the
|
||||||
/// AudioEngine is running (for capture OR playback). Previously, the session was only
|
/// AudioEngine is running (for capture OR playback), so it is activated when any audio
|
||||||
/// activated when the user joined voice (startMicStream), which meant:
|
/// needs to play (a remote stream started OR the user joins voice) and only deactivated
|
||||||
/// - Remote audio was silent if the user hadn't joined voice yet.
|
/// when disconnecting from the server — not when leaving voice, since the user may still
|
||||||
/// - Leaving voice (stopMicStream) deactivated the session, killing remote audio.
|
/// want to hear remote audio.
|
||||||
/// Now the session is activated when any audio needs to play (remote stream started OR
|
|
||||||
/// user joins voice) and only deactivated when disconnecting from the server.
|
|
||||||
private var isSessionActive = false
|
private var isSessionActive = false
|
||||||
|
|
||||||
func configure() {
|
func configure() {
|
||||||
@@ -78,12 +76,10 @@ final class AudioSessionManager {
|
|||||||
logger.info("session deactivated")
|
logger.info("session deactivated")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TEMP DIAGNOSTIC (stereo-A2DP fix verification): log the full AVAudioSession state —
|
/// Log the full AVAudioSession state — category, mode, options, and active route.
|
||||||
/// category, mode, options, and active route. The bug was miniaudio resetting the category
|
/// Useful for diagnosing routing issues, e.g. confirming the session stays
|
||||||
/// to `Record` (no output) on device open; with the core's ma_context now configured to
|
/// `PlayAndRecord` with `allowBluetoothA2DP` and keeps the A2DP output route even
|
||||||
/// leave the session alone, this should report `AVAudioSessionCategoryPlayAndRecord` with
|
/// after the mic engine starts.
|
||||||
/// `allowBluetoothA2DP` set, and the output route should be the headphones/A2DP device —
|
|
||||||
/// even after the mic engine starts. Remove once verified on-device.
|
|
||||||
func logSessionState(_ when: String) {
|
func logSessionState(_ when: String) {
|
||||||
let s = AVAudioSession.sharedInstance()
|
let s = AVAudioSession.sharedInstance()
|
||||||
var opts: [String] = []
|
var opts: [String] = []
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import VoiceCatCore
|
|||||||
private let logger = Logger(subsystem: "cat.voice.VoiceCatiOS", category: "IOSAudioRouter")
|
private let logger = Logger(subsystem: "cat.voice.VoiceCatiOS", category: "IOSAudioRouter")
|
||||||
|
|
||||||
/// iOS audio routing layer — drives all iOS audio route selection via `AVAudioSession`
|
/// iOS audio routing layer — drives all iOS audio route selection via `AVAudioSession`
|
||||||
/// *before* the core (miniaudio) opens its device. miniaudio does NOT touch
|
/// *before* the core (miniaudio) opens its device. This class is the sole owner of the
|
||||||
/// `AVAudioSession` on iOS — but ONLY because the core deliberately opens its devices
|
/// session: miniaudio does NOT touch `AVAudioSession` on iOS, because the core opens its
|
||||||
/// through a `ma_context` configured with `sessionCategory = none` +
|
/// devices through a `ma_context` configured with `sessionCategory = none` +
|
||||||
/// `noAudioSessionActivate/Deactivate` (see `AudioEngine::make_context_config` in
|
/// `noAudioSessionActivate/Deactivate` (see `AudioEngine::make_context_config` in
|
||||||
/// `core/src/audio/audio_engine.cpp`). With miniaudio's default NULL-context path it WOULD
|
/// `core/src/audio/audio_engine.cpp`). Without that, miniaudio's default path resets the
|
||||||
/// reset the category to `Record`/`Playback` with no options on every device open, wiping
|
/// category to `Record`/`Playback` with no options on every device open, wiping
|
||||||
/// `.allowBluetoothA2DP`/`.playAndRecord` and killing headphone/A2DP output (the long-standing
|
/// `.allowBluetoothA2DP`/`.playAndRecord` and killing headphone/A2DP output — so that
|
||||||
/// "stereo mic kills output" bug). With that disabled, this class is the sole owner of the
|
/// config must stay in place. All iOS audio routing (input port selection, mic
|
||||||
/// session. All iOS audio routing (input port selection, mic orientation/polar patterns,
|
/// orientation/polar patterns, HFP vs A2DP, measurement/raw mode, stereo capture) must be
|
||||||
/// HFP vs A2DP, measurement/raw mode, stereo capture) must be driven from here.
|
/// driven from here.
|
||||||
///
|
///
|
||||||
/// The three user-facing choices:
|
/// The three user-facing choices:
|
||||||
/// 1. **Input port** — which physical input (built-in mic, Bluetooth HFP, headset,
|
/// 1. **Input port** — which physical input (built-in mic, Bluetooth HFP, headset,
|
||||||
@@ -32,16 +32,14 @@ private let logger = Logger(subsystem: "cat.voice.VoiceCatiOS", category: "IOSAu
|
|||||||
/// but shows a warning when the output route is the speaker (echo risk, no AEC).
|
/// but shows a warning when the output route is the speaker (echo risk, no AEC).
|
||||||
///
|
///
|
||||||
/// Additionally, **stereo capture** (2-channel built-in mic) is enabled by switching the
|
/// Additionally, **stereo capture** (2-channel built-in mic) is enabled by switching the
|
||||||
/// built-in mic's data source to the `.stereo` polar pattern. The recipe (mirroring
|
/// built-in mic's data source to the `.stereo` polar pattern. The recipe is:
|
||||||
/// TeamTalk5's `SoundDevicesModel.selectDataSource` + `UtilSound.setupSoundDevices`, which
|
/// `setPreferredDataSource(.stereo source)` + `setPreferredPolarPattern(.stereo)` +
|
||||||
/// achieves stereo mic + A2DP output simultaneously) is: `setPreferredDataSource(.stereo
|
/// `setPreferredInput(built-in mic)` + `setInputDataSource(stereo source)`. The channel
|
||||||
/// source)` + `setPreferredPolarPattern(.stereo)` + `setPreferredInput(built-in mic)` +
|
/// count itself must NOT be requested via `setPreferredInputNumberOfChannels(2)` — that
|
||||||
/// `setInputDataSource(stereo source)`. The channel count itself is NOT requested via
|
/// session-level call collapses the A2DP output route. Instead the core is told to open the
|
||||||
/// `setPreferredInputNumberOfChannels(2)` — that session-level call is what collapses the
|
/// device with 2 channels via `vc_set_capture_channels(streamId, 2)`, and the AVAudioSession
|
||||||
/// A2DP output route (the original "stereo kills output" bug). Instead the core is told to
|
/// input anchor (`setPreferredInput` + `setInputDataSource`) keeps the route stable during
|
||||||
/// open the device with 2 channels via `vc_set_capture_channels(streamId, 2)`, and the
|
/// the HFP→A2DP and mono→stereo reconfigurations.
|
||||||
/// AVAudioSession input anchor (`setPreferredInput` + `setInputDataSource`) keeps the route
|
|
||||||
/// stable during the HFP→A2DP and mono→stereo reconfigurations.
|
|
||||||
///
|
///
|
||||||
/// Voice Isolation / Wide Spectrum (iOS 17+/18+) are user-toggleable in Control Center
|
/// Voice Isolation / Wide Spectrum (iOS 17+/18+) are user-toggleable in Control Center
|
||||||
/// for `.voiceChat` apps — surfaced as a hint, not a programmatic toggle.
|
/// for `.voiceChat` apps — surfaced as a hint, not a programmatic toggle.
|
||||||
@@ -332,12 +330,9 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
switch bluetoothMode {
|
switch bluetoothMode {
|
||||||
case .btHfpVoice:
|
case .btHfpVoice:
|
||||||
// Voice Chat: allow BOTH HFP and A2DP, let iOS pick the right profile for the
|
// Voice Chat: allow BOTH HFP and A2DP, let iOS pick the right profile for the
|
||||||
// connected device. This matches TeamTalk5's default (UtilSound.swift:228):
|
// connected device. HFP and A2DP must NOT be made mutually exclusive (HFP-only)
|
||||||
// [.allowBluetoothHFP, .allowAirPlay, .allowBluetoothA2DP]
|
// — that blocks A2DP headphones from receiving audio. HFP is preferred (the system
|
||||||
// Making HFP and A2DP mutually exclusive (HFP-only here) blocks A2DP headphones
|
// uses it when a two-way mic path is needed); A2DP stays available for output-only.
|
||||||
// from receiving audio — the "Voice Chat kills Bluetooth output" regression.
|
|
||||||
// HFP is *preferred* (the system uses HFP when a two-way mic path is needed),
|
|
||||||
// but A2DP is still available for output-only scenarios.
|
|
||||||
options.insert(.allowBluetoothHFP)
|
options.insert(.allowBluetoothHFP)
|
||||||
options.insert(.allowBluetoothA2DP)
|
options.insert(.allowBluetoothA2DP)
|
||||||
options.insert(.allowAirPlay)
|
options.insert(.allowAirPlay)
|
||||||
@@ -345,8 +340,6 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
// A2DP output only (no HFP). With HFP disabled the Bluetooth device can only be
|
// A2DP output only (no HFP). With HFP disabled the Bluetooth device can only be
|
||||||
// an OUTPUT (A2DP), so the system routes the mic to the built-in mic — exactly
|
// an OUTPUT (A2DP), so the system routes the mic to the built-in mic — exactly
|
||||||
// what we want for "built-in mic + A2DP output", in either mono OR stereo.
|
// what we want for "built-in mic + A2DP output", in either mono OR stereo.
|
||||||
// This matches TeamTalk5's A2DP mode (UtilSound.swift:232-233): remove HFP from
|
|
||||||
// the default set, leaving only A2DP.
|
|
||||||
options.insert(.allowBluetoothA2DP)
|
options.insert(.allowBluetoothA2DP)
|
||||||
options.insert(.allowAirPlay)
|
options.insert(.allowAirPlay)
|
||||||
case .builtInMicSpeaker:
|
case .builtInMicSpeaker:
|
||||||
@@ -354,12 +347,10 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
options.insert(.defaultToSpeaker)
|
options.insert(.defaultToSpeaker)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Set category + mode. Recipe validated against TeamTalk5 / Ferrite, which both do
|
// 2. Set category + mode, chosen per scenario:
|
||||||
// built-in stereo mic + A2DP Bluetooth output simultaneously:
|
|
||||||
// - Stereo capture: .default — .voiceChat (the AEC/VPIO path) forces MONO, so stereo
|
// - Stereo capture: .default — .voiceChat (the AEC/VPIO path) forces MONO, so stereo
|
||||||
// is only possible in a non-VPIO mode. .default supports multi-capsule stereo AND
|
// is only possible in a non-VPIO mode. .default supports multi-capsule stereo AND
|
||||||
// keeps the A2DP output route alive. (Earlier .videoRecording + a session-level
|
// keeps the A2DP output route alive.
|
||||||
// channel-count request collapsed A2DP output — the "stereo kills output" bug.)
|
|
||||||
// - Mono raw/studio: .measurement — all system processing off.
|
// - Mono raw/studio: .measurement — all system processing off.
|
||||||
// - Mono + A2DP output: .videoRecording — keeps A2DP output without VPIO (no AEC).
|
// - Mono + A2DP output: .videoRecording — keeps A2DP output without VPIO (no AEC).
|
||||||
// - Mono standard (HFP or speaker): .voiceChat — hardware AEC/AGC/HPF.
|
// - Mono standard (HFP or speaker): .voiceChat — hardware AEC/AGC/HPF.
|
||||||
@@ -384,13 +375,12 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
// 3. Input & mic-capsule configuration.
|
// 3. Input & mic-capsule configuration.
|
||||||
if captureChannels == .stereo {
|
if captureChannels == .stereo {
|
||||||
// Stereo: enable the built-in mic's .stereo polar pattern AND anchor the input
|
// Stereo: enable the built-in mic's .stereo polar pattern AND anchor the input
|
||||||
// route explicitly via setPreferredInput + setInputDataSource. The session-level
|
// route explicitly via setPreferredInput + setInputDataSource. With HFP disabled
|
||||||
// channel-count call (setPreferredInputNumberOfChannels(2)) is what collapses the
|
// the system routes input to the built-in mic, but without the explicit
|
||||||
// A2DP output route — NOT setPreferredInput (TeamTalk5 uses setPreferredInput and
|
// preferred-input anchor the route can collapse during the mode switch
|
||||||
// gets stereo + A2DP). With HFP disabled the system routes input to the built-in
|
// (.voiceChat → .default) and the output dies. The channel count is requested by
|
||||||
// mic, but without the explicit preferred-input anchor the route can collapse
|
// miniaudio at the audio-unit level (vc_set_capture_channels), NOT via
|
||||||
// during the mode switch (.voiceChat → .default) and the output dies. The channel
|
// setPreferredInputNumberOfChannels(2) — that call collapses the A2DP output route.
|
||||||
// count is requested by miniaudio at the audio-unit level (vc_set_capture_channels).
|
|
||||||
configureStereoCapture(session: session)
|
configureStereoCapture(session: session)
|
||||||
} else if let portId = selectedInputPortId, !portId.isEmpty,
|
} else if let portId = selectedInputPortId, !portId.isEmpty,
|
||||||
let port = session.availableInputs?.first(where: { $0.uid == portId }) {
|
let port = session.availableInputs?.first(where: { $0.uid == portId }) {
|
||||||
@@ -411,18 +401,16 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
updateWarnings()
|
updateWarnings()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable 2-channel capture on the built-in mic. Mirrors TeamTalk5's recipe
|
/// Enable 2-channel capture on the built-in mic. The recipe that achieves stereo mic +
|
||||||
/// (`SoundDevicesModel.selectDataSource` + `UtilSound.setupSoundDevices`), which
|
/// A2DP Bluetooth output simultaneously:
|
||||||
/// achieves stereo mic + A2DP Bluetooth output simultaneously:
|
|
||||||
/// 1. `setPreferredDataSource(stereoSource)` on the built-in mic port
|
/// 1. `setPreferredDataSource(stereoSource)` on the built-in mic port
|
||||||
/// 2. `setPreferredPolarPattern(.stereo)` on that data source
|
/// 2. `setPreferredPolarPattern(.stereo)` on that data source
|
||||||
/// 3. `setPreferredInput(builtIn)` — anchor the input route explicitly (this is NOT
|
/// 3. `setPreferredInput(builtIn)` — anchor the input route explicitly. Without this
|
||||||
/// what collapses A2DP — the session-level `setPreferredInputNumberOfChannels(2)`
|
/// anchor the route can collapse during the mode switch (.voiceChat → .default).
|
||||||
/// is. Without this anchor the route can collapse during the mode switch.)
|
|
||||||
/// 4. `setInputDataSource(stereoSource)` — commit the data source at the session level
|
/// 4. `setInputDataSource(stereoSource)` — commit the data source at the session level
|
||||||
/// The channel count itself is requested by miniaudio at the audio-unit level via
|
/// The channel count itself is requested by miniaudio at the audio-unit level via
|
||||||
/// `vc_set_capture_channels(2)`. We do NOT call `setPreferredInputNumberOfChannels(2)`
|
/// `vc_set_capture_channels(2)`. We must NOT call `setPreferredInputNumberOfChannels(2)`
|
||||||
/// — that session-level call is the one that collapses the A2DP output route.
|
/// — that session-level call collapses the A2DP output route.
|
||||||
private func configureStereoCapture(session: AVAudioSession) {
|
private func configureStereoCapture(session: AVAudioSession) {
|
||||||
guard let builtIn = session.availableInputs?.first(where: { $0.portType == .builtInMic })
|
guard let builtIn = session.availableInputs?.first(where: { $0.portType == .builtInMic })
|
||||||
else {
|
else {
|
||||||
@@ -438,12 +426,10 @@ final class IOSAudioRouter: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
try builtIn.setPreferredDataSource(stereoSource)
|
try builtIn.setPreferredDataSource(stereoSource)
|
||||||
try stereoSource.setPreferredPolarPattern(.stereo)
|
try stereoSource.setPreferredPolarPattern(.stereo)
|
||||||
// Anchor the input route explicitly. TeamTalk5 does this (SoundDevicesModel
|
// Anchor the input route explicitly; without it the route can collapse during
|
||||||
// .selectDataSource:147); without it the route can collapse during the mode
|
// the mode switch (.voiceChat → .default) and the A2DP output dies.
|
||||||
// switch (.voiceChat → .default) and the A2DP output dies.
|
|
||||||
try session.setPreferredInput(builtIn)
|
try session.setPreferredInput(builtIn)
|
||||||
// Commit the data source at the session level (TeamTalk does this at
|
// Commit the data source at the session level. setPreferredDataSource alone only
|
||||||
// SoundDevicesModel.selectDataSource:148). setPreferredDataSource alone only
|
|
||||||
// sets the port-level preference; setInputDataSource makes it the active source.
|
// sets the port-level preference; setInputDataSource makes it the active source.
|
||||||
try session.setInputDataSource(stereoSource)
|
try session.setInputDataSource(stereoSource)
|
||||||
logger.info("stereo capsule enabled — source=\(stereoSource.dataSourceName), pattern=.stereo, input anchored")
|
logger.info("stereo capsule enabled — source=\(stereoSource.dataSourceName), pattern=.stereo, input anchored")
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ final class SessionState {
|
|||||||
addActivity(talking ? "\(who) started talking" : "\(who) stopped talking")
|
addActivity(talking ? "\(who) started talking" : "\(who) stopped talking")
|
||||||
case .streamStarted:
|
case .streamStarted:
|
||||||
// A remote user started a stream — ensure the audio session is active so we can
|
// A remote user started a stream — ensure the audio session is active so we can
|
||||||
// hear them even if we haven't joined voice ourselves. Previously the session was
|
// hear them even if we haven't joined voice ourselves.
|
||||||
// only activated when the user pressed Join Voice, so remote audio was silent.
|
|
||||||
if ev.userId != selfUserId {
|
if ev.userId != selfUserId {
|
||||||
do {
|
do {
|
||||||
try AudioSessionManager.shared.ensureSessionActive()
|
try AudioSessionManager.shared.ensureSessionActive()
|
||||||
@@ -110,8 +109,6 @@ final class SessionState {
|
|||||||
addActivity("Audio session activate failed: \(error)")
|
addActivity("Audio session activate failed: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TEMP DIAGNOSTIC: the core opens its miniaudio devices around now — log the
|
|
||||||
// session state to confirm miniaudio is no longer resetting the category to Record.
|
|
||||||
AudioSessionManager.shared.logSessionState("stream started (user \(ev.userId))")
|
AudioSessionManager.shared.logSessionState("stream started (user \(ev.userId))")
|
||||||
addActivity("Stream started (user \(ev.userId))")
|
addActivity("Stream started (user \(ev.userId))")
|
||||||
case .streamStopped:
|
case .streamStopped:
|
||||||
|
|||||||
@@ -491,8 +491,7 @@ VC_API vc_result vc_audio_resume(vc_client* c);
|
|||||||
* were opened), vc_audio_restart() uninitializes and re-initializes the capture and playback
|
* were opened), vc_audio_restart() uninitializes and re-initializes the capture and playback
|
||||||
* devices so they pick up a new AVAudioSession route. Call this from the Swift layer AFTER
|
* devices so they pick up a new AVAudioSession route. Call this from the Swift layer AFTER
|
||||||
* reconfiguring AVAudioSession (setCategory, setPreferredInput, setPreferredPolarPattern, etc.)
|
* reconfiguring AVAudioSession (setCategory, setPreferredInput, setPreferredPolarPattern, etc.)
|
||||||
* so the core's devices reopen against the new route. Mirrors TeamTalk5's
|
* so the core's devices reopen against the new route.
|
||||||
* closeSoundDevices()/initSoundInputDevice()/initSoundOutputDevice() reconfiguration pattern.
|
|
||||||
* Safe to call when the engine is not running (it will just start it). */
|
* Safe to call when the engine is not running (it will just start it). */
|
||||||
VC_API vc_result vc_audio_restart(vc_client* c);
|
VC_API vc_result vc_audio_restart(vc_client* c);
|
||||||
|
|
||||||
|
|||||||
@@ -301,8 +301,7 @@ class AudioEngine {
|
|||||||
// selected. The iOS Swift layer is the SOLE owner of the audio session (activated in
|
// selected. The iOS Swift layer is the SOLE owner of the audio session (activated in
|
||||||
// AppState on connect, configured by IOSAudioRouter); miniaudio must only open the
|
// AppState on connect, configured by IOSAudioRouter); miniaudio must only open the
|
||||||
// AudioUnit against the already-configured, already-active route. These coreaudio fields
|
// AudioUnit against the already-configured, already-active route. These coreaudio fields
|
||||||
// are no-ops on non-Apple backends. See PROGRESS.md / the "stereo mic kills output"
|
// are no-ops on non-Apple backends.
|
||||||
// investigation.
|
|
||||||
static ma_context_config make_context_config();
|
static ma_context_config make_context_config();
|
||||||
|
|
||||||
// Owned context, shared by the playback, capture, and loopback devices so they all honor
|
// Owned context, shared by the playback, capture, and loopback devices so they all honor
|
||||||
|
|||||||
@@ -1426,8 +1426,7 @@ vc_result vc_client::audio_resume() {
|
|||||||
|
|
||||||
vc_result vc_client::audio_restart() {
|
vc_result vc_client::audio_restart() {
|
||||||
// Full uninit + re-init (not just stop/start like suspend/resume) so the miniaudio
|
// Full uninit + re-init (not just stop/start like suspend/resume) so the miniaudio
|
||||||
// devices reopen against the current AVAudioSession route. Mirrors TeamTalk5's
|
// devices reopen against the current AVAudioSession route.
|
||||||
// closeSoundDevices()/initSoundInputDevice()/initSoundOutputDevice() pattern.
|
|
||||||
// IMPORTANT: only restart if the engine was already running — calling
|
// IMPORTANT: only restart if the engine was already running — calling
|
||||||
// ensure_audio_running() when the engine isn't running would start it prematurely
|
// ensure_audio_running() when the engine isn't running would start it prematurely
|
||||||
// (opening the capture device / mic without an active mic stream), which on iOS
|
// (opening the capture device / mic without an active mic stream), which on iOS
|
||||||
|
|||||||
Reference in New Issue
Block a user