Commit Graph

13 Commits

Author SHA1 Message Date
b07362e525 feat(ios): iPhone channel drill-down, fix chat compose box, unify chat+activity
- Channels tab is now a drill-down on iPhone: ChannelBrowserView lists top-level
  channels; ChannelDetailView shows the people in a channel, its sub-channels, and
  an explicit Join button (with password prompt). iPad split view unchanged.
- Extract self-contained UserRow (context menu + sheets) from UserListView so admin
  actions are reused in the drill-down.
- Fix off-screen chat compose box: pin VoiceControlsView via per-tab
  .safeAreaInset(edge: .bottom) instead of a floating overlay, so it reserves layout
  space above the tab bar (keeping the compose box visible, cooperating with keyboard
  avoidance) without covering the tab bar buttons.
- Collapse Activity into Chat like macOS/Windows: ChatView renders a merged,
  time-sorted timeline of messages + activity (activity rows in gray); remove the
  Activity tab and ActivityLogView.
- Label the RPSystemBroadcastPickerView inner UIButton for VoiceOver
  ("Share/Stop sharing screen audio") instead of relying on an outer SwiftUI label.
2026-06-21 03:02:58 +02:00
ef88af14c2 fix(ios): show screen-audio broadcast extension in picker
The broadcast upload extension never appeared in the iOS broadcast picker
(or Control Center screen-recording list) because its Info.plist set
RPBroadcastProcessMode to the invalid string "BroadcastUpload". iOS only
recognises "RPBroadcastProcessModeSampleBuffer" for a sample-buffer upload
extension and silently rejects any other value, hiding the extension.

Also sign VoiceCatCore.xcframework in build-xcframework.sh: Xcode 15+ rejects
unsigned binary XCFrameworks consumed as SwiftPM binary targets. Signs with
CODESIGN_IDENTITY, else the first Apple Development identity, else ad-hoc.
2026-06-21 02:08:57 +02:00
8c90e250f0 feat(apple): screen-audio sharing -- macOS ScreenCaptureKit, iOS ReplayKit
Implement system/desktop audio sharing on the Apple clients, feeding the
existing SCREEN_AUDIO Opus -> AEAD -> UDP path via vc_stream_feed_pcm. No
C++/protocol/codec changes -- the core was already ready (the Windows-only
loopback is #ifdef VOICECAT_HAS_LOOPBACK; off Windows the stream just waits
for fed PCM). Audio only; video is dropped.

macOS (in-process):
- ScreenAudioCapture.swift drives an audio-only SCStream
  (excludesCurrentProcessAudio), converts Float32 -> int16 in the channel's
  mono/stereo mode, and calls feedPcm. Capture starts on the self
  .streamStarted event (effective config known then). Wired into
  MainWindowController.screenAudioClicked().

iOS (forward-to-host, single session):
- VoiceCatBroadcast: a ReplayKit Broadcast Upload Extension consumes
  .audioApp only, resamples to 48kHz int16 stereo (AVAudioConverter), and
  writes a shared App Group SPSC ring (BroadcastAudioRing.swift). It does
  not link libvoicecat.
- Host BroadcastAudioPump drains the ring (reacting to the extension's
  Darwin notifications) and feeds the SCREEN_AUDIO stream it owns, downmixing
  to mono when the channel is mono. Screen audio appears as a second stream
  of the same user; no credentials persisted. UI is RPSystemBroadcastPicker
  View in VoiceControlsView. Removes the speculative BroadcastCredentials.

Docs: voice.md s9, CLAUDE.md status, PROGRESS.md.
2026-06-21 00:14:31 +02:00
f3c1172a3e 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.
2026-06-20 03:03:34 +02:00
f1e1ef59ed fix(ios): stop miniaudio from clobbering AVAudioSession (stereo->A2DP output death)
The real root cause of "selecting Stereo Mic kills headphone/A2DP output on Join
Voice." Every prior fix worked on the Swift IOSAudioRouter under the false premise
that "miniaudio does NOT touch AVAudioSession on iOS." It does: the core opened
devices via ma_device_init(nullptr, ...), and with a NULL context miniaudio 0.11.25
runs an iOS "hack" that sets the session category by device type, then
ma_context_init__coreaudio calls setCategory()+setActive() on every device open --
capture -> AVAudioSessionCategoryRecord with zero options. That wipes the
.playAndRecord category, the mode, and .allowBluetoothA2DP / .mixWithOthers /
.allowAirPlay that IOSAudioRouter had just configured, killing headphone/A2DP (and
even wired) output. Stereo presets break worst because they rely on the A2DP output
route the wipe removes. TeamTalk avoids this by opening RemoteIO/VPIO AudioUnits
directly and leaving the session entirely to the app.

Fix (core, cross-platform safe): AudioEngine now owns a ma_context built by
make_context_config() with coreaudio.sessionCategory = ma_ios_session_category_none
and noAudioSessionActivate/Deactivate = MA_TRUE, and routes all ma_device_init calls
(playback, capture, loopback) plus enumerate_devices through it. miniaudio no longer
touches AVAudioSession; IOSAudioRouter is the sole owner (the session is already
activated on connect in AppState before any device opens). Context is lazily inited
in start(), reused across restarts, uninited in ~AudioEngine.

Adds TEMP AudioSessionManager.logSessionState() diagnostics (after activate, on route
change, on .streamStarted) to verify on-device that the category stays
PlayAndRecord+allowBluetoothA2DP instead of flipping to Record. Remove once confirmed.

Windows: cmake --build --preset dev clean; ctest --preset dev 21/21.
iOS build + on-device verification pending on Mac.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:34:07 +02:00
dcb7e6eeca fix(ios): fix stereo mic + A2DP output silence
Three coordinated fixes for the bug where enabling stereo mic capture
caused all audio output (A2DP, speaker, wired) to go silent:

1. audio_engine.cpp — open playback before capture
   On iOS, starting the stereo capture AudioUnit can trigger an audio
   route reconfiguration that drops A2DP before the playback device has
   a chance to claim the route. Opening and starting the playback device
   first commits the output route (A2DP), so iOS is less likely to drop
   it when stereo capture activates afterward.

2. client.cpp — decouple set_capture_channels from engine restart
   Previously vc_set_capture_channels() stopped and restarted the engine
   immediately, which opened capture first (old ordering) and raced
   against the settling AVAudioSession route. Now it only stores the
   channel count; the caller (Swift via vc_audio_restart) controls when
   the engine restarts, after the route has settled.

3. IOSAudioRouter.swift — call audioRestart() after channel config
   selectCaptureChannels() and applyPreset() now call audioRestart()
   after applyConfiguration() + setCaptureChannels(). This is the
   vc_audio_restart() path that was added to the ABI in fdcc84f but
   never wired up in the Swift layer. The restart sees the stored
   channel count and reopens devices in the correct order (playback
   first, capture second).

The doStartMicStream path is unaffected: setCaptureChannels is called
before the server acknowledges the stream (engine not yet running), so
ensure_audio_running() picks up capture_channels=2 directly when the
stream is confirmed and opens with the right count from the start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:39:23 +02:00
fdcc84fb42 fix(ios): stereo mic + A2DP output, add vc_audio_restart ABI
Diagnosed by comparing against TeamTalk5 (Client/iTeamTalk), which
achieves stereo mic + A2DP output. Five fixes:

1. configureStereoCapture now calls setPreferredInput +
   setInputDataSource (mirroring TeamTalk5's SoundDevicesModel).
   Previously omitted based on incorrect diagnosis that
   setPreferredInput collapsed A2DP — the real culprit was
   setPreferredInputNumberOfChannels(2), which neither project uses.

2. New C ABI: vc_audio_restart (full stop + re-init, unlike
   suspend/resume which only stop/start). Swift wrapper added.
   The withAudioSuspend wrapper that used it was removed after
   on-device testing showed it killed all audio (including
   VoiceOver) when switching presets — the core's
   set_capture_channels handles engine restart internally.

3. Bluetooth options: Voice Chat preset now includes BOTH
   .allowBluetoothHFP AND .allowBluetoothA2DP (matching TeamTalk5's
   UtilSound.swift:228). Previously HFP-only blocked A2DP headphones.

4. Capture channels now reset when switching stereo→mono via
   selectCaptureChannels/applyPreset. AudioSessionManager tracks
   activeMicStreamId (set by SessionState on join/leave voice).

5. Docs synced: voice.md, tech-stack.md, architecture.md,
   PROGRESS.md. Removed stale setPreferredInputNumberOfChannels(2)
   references.

Verified: ctest --preset dev 21/21 green, iOS client builds.
Stereo mic + A2DP output still needs on-device debugging — the
core recipe is correct but iOS 26 route behavior requires
hands-on testing with a debugger.
2026-06-19 16:58:21 +02:00
1a1c8a1dfe feat(ios): rework audio presets — always-available + device-specific
Presets reorganized to give users choice at every level:

Always available (work with any output route):
- Voice Chat: AEC/AGC/HPF on, mono, system picks best route (BT HFP,
  wired, or speaker). The standard iOS VoIP experience.
- Stereo Mic: Stereo built-in mic (front+back capsules), A2DP output
  if BT connected else speaker/wired. Standard processing.
- Studio (No Processing): Stereo mic, no AEC/AGC/HPF (raw mode).
  Maximum fidelity. Echo risk on speaker.

When Bluetooth connected:
- Bluetooth Headset (HFP): BT mic + BT output, AEC on, mono.
- BT Headphones + Mono Mic: A2DP output + built-in mic, mono, no AEC.
- BT Headphones + Stereo Mic: A2DP output + stereo built-in mic.

When wired headset/earpods connected:
- Wired Headset: Wired output + wired/built-in mic, AEC on, mono.

Always:
- Custom: shown when advanced settings don't match any preset.

Key changes from previous version:
- Stereo Mic is no longer gated behind Bluetooth — it's always available
  and uses A2DP output if BT is connected, else speaker/wired.
- Wired headset detection (headphones/headsetMic/usbAudio port types)
  with a dedicated preset.
- Voice Chat preset always available with AEC — the safe default.
- activePreset checks device-specific presets first so e.g. when BT is
  connected and settings match 'Bluetooth Headset', it returns that
  instead of the equivalent 'Voice Chat'.
- detectAudioDevices() replaces detectBluetooth(), detects both BT and
  wired devices from currentRoute + availableInputs.
2026-06-19 14:09:08 +02:00
d6352627e9 feat(ios): audio presets + advanced settings disclosure
Replaces the flat list of audio settings with a preset picker that shows
context-appropriate options based on whether a Bluetooth device is connected.

Presets:
- Default (Phone Speaker): built-in mic + speaker, standard, mono
- Bluetooth Headset (HFP): BT mic + BT output, standard, mono — only shown
  when a BT device is connected
- BT Headphones + Phone Mic: A2DP stereo output + built-in mic, standard,
  mono — only shown when BT connected
- BT Headphones + Stereo Mic: A2DP stereo output + built-in mic stereo
  (front+back capsules), standard, stereo — only shown when BT connected
- Custom: shown when advanced settings don't match any preset

When no Bluetooth device is connected, only 'Default' and 'Custom' appear,
with a hint to connect Bluetooth headphones for more options.

All granular controls (input port, orientation, polar pattern, mic mode,
channels, bluetooth mode, output route, AirPlay) are now under an
'Advanced Audio' disclosure group, collapsed by default.

IOSAudioRouter gains:
- AudioPreset enum with bluetoothMode/captureChannels/micMode/usesBuiltInMic
- hasBluetoothDevice detection (checks currentRoute + availableInputs for
  bluetoothA2DP/bluetoothHFP port types)
- availablePresets (filtered by BT connection state)
- activePreset (computed from current settings)
- applyPreset() (sets all individual settings + finds built-in mic port UID)
2026-06-19 14:05:19 +02:00
3e80af2f3f fix(ios): output muted with A2DP, session lifecycle, mic input issues
Three bugs causing no audio output and no mic input:

1. .voiceChat mode + A2DP = output muted. The .voiceChat mode uses hardware
   AEC/AGC/HPF but requires HFP-compatible routes. A2DP is NOT HFP — iOS
   mutes the output because it can't set up the voice processing pipeline on
   an A2DP route. Fix: use .default mode for Standard+A2DP (no hardware AEC,
   but audio routes correctly). .voiceChat kept for HFP and speaker modes.
   Added info warning in Settings UI for A2DP no-AEC.

2. Session lifecycle broken. stopMicStream() called deactivateAfterStreaming()
   which deactivated the AVAudioSession — but the AudioEngine keeps running for
   remote audio playback, so leaving voice killed all remote audio. And the
   session was never activated when a remote user started talking (only on
   Join Voice), so you couldn't hear anyone before joining voice. Fix:
   - ensureSessionActive() replaces activateForStreaming() — idempotent, called
     on Join Voice AND on .streamStarted (remote user starts talking).
   - stopMicStream() no longer deactivates the session.
   - deactivateSession() called only on disconnect from server.
   - isSessionActive flag tracks state, updated by interruption handler.

3. setPreferredInputNumberOfChannels(1) called for mono — unnecessary (1 is
   the default) and may put the session in a bad state on some devices. Fix:
   only call it when stereo is explicitly selected. Also handle empty input
   port ID (selecting 'Default' in the picker) correctly.

Added comprehensive route logging — after activation, logs the current output
and input route names so issues can be diagnosed from Console.app.
2026-06-19 13:46:20 +02:00
ab973940df fix(ios): break audio session route-change feedback loop
handleRouteChange called applyConfiguration() unconditionally, which called
setCategory/setPreferredInput/etc., which triggered another route-change
notification, which called applyConfiguration() again — an infinite loop that
burned CPU (phone slowdown) and repeatedly tore down/rebuilt the audio session
(audio cycling on/off, VoiceOver glitching).

Two fixes:
1. handleRouteChange now only re-applies config on external device changes
   (.oldDeviceUnavailable / .newDeviceAvailable), not on .categoryChange /
   .routeConfigurationChange which are triggered by our own setCategory calls.
2. IOSAudioRouter.applyConfiguration() gained a re-entrancy guard
   (isApplyingConfiguration) for synchronous route-change notifications.

Also added os.Logger logging to both files (subsystem cat.voice.VoiceCatiOS)
so future issues can be debugged from Console.app on the Mac.
2026-06-19 13:26:23 +02:00
9fc51cffc4 feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:

1. Channel-id sync bug (mic button permanently dimmed): SessionState never
   synced currentChannelId from the self user's channelId on connect, so the
   mic button (gated on currentChannelId == 0) stayed dimmed. Added
   syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
   called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
   Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.

2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
   text button (parity with macOS). Mute/deafen disable when not in voice.

3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
   port selection, built-in mic orientation/polar patterns, Bluetooth
   HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
   UserDefaults persistence. AudioSessionManager delegates to it.

4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
   lets the core open the mic device in stereo (2-ch interleaved). LocalStream
   gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
   capture_accum_ + on_capture updated to channel-aware accumulation. Test
   test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
   VoiceCatClient.setCaptureChannels.

5. Settings UI rework: AVAudioSession-derived input/output tree replaces
   miniaudio device picker.

6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
   swift-tools-version 6.0 with swiftLanguageModes .v5.

Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.

Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
e26e7db5b1 feat(ios): ship iOS SwiftUI client (VoiceCatiOS)
Full SwiftUI app at clients/apple/iOS/VoiceCatiOS.xcodeproj:
- 24 Swift source files: AppState + SessionState (@Observable @MainActor),
  AudioSessionManager (AVAudioSession owner + interruption/route handling),
  ServerListStore/SavedServer (App Group container + Keychain sharing),
  and 14 SwiftUI views covering the full feature set
- NavigationSplitView on iPad, TabView on iPhone (horizontalSizeClass)
- Channel tree via OutlineGroup, user list with context menu admin actions
- PTT via DragGesture(minimumDistance: 0) + @GestureState
- onEvent closures hop to MainActor via Task { @MainActor in ... }
- App Group: group.cat.voice.VoiceCat (shared with future ReplayKit extension)

C ABI: add vc_audio_suspend / vc_audio_resume (AudioEngine::suspend/resume)
called by AudioSessionManager on AVAudioSession interruption events.

XCFramework: add ios-arm64 and ios-arm64-simulator slices to build-xcframework.sh;
Package.swift gains .iOS(.v17) platform; CMakePresets.json adds apple-ios /
apple-ios-sim presets with arm64-ios / arm64-ios-simulator vcpkg triplets.

Verified: xcodebuild -target VoiceCatiOS -sdk iphonesimulator26.5 BUILD SUCCEEDED.
2026-06-19 02:10:25 +02:00