feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
// Plain Swift value types — what survives past the native struct/free-list lifetime
|
|
|
|
|
|
// (Marshaling.swift converts the C structs into these and immediately frees the native
|
|
|
|
|
|
// list). Nothing here holds a raw pointer. This is the Swift analog of the C# client's
|
|
|
|
|
|
// Models.cs. Field naming follows Swift camelCase (the C structs use snake_case).
|
|
|
|
|
|
|
|
|
|
|
|
import VoiceCatC
|
|
|
|
|
|
|
|
|
|
|
|
/// Channel snapshot — mirrors `vc_channel` (the pull-based view; re-call `listChannels()`
|
|
|
|
|
|
/// after `.channelList` / `.userJoined` / `.userLeft` / `.userUpdated` events).
|
|
|
|
|
|
public struct Channel: Sendable, Equatable, Identifiable {
|
|
|
|
|
|
public let id: UInt32
|
|
|
|
|
|
/// 0 = root.
|
|
|
|
|
|
public let parentId: UInt32
|
|
|
|
|
|
public let name: String
|
|
|
|
|
|
public let topic: String
|
|
|
|
|
|
public let passwordProtected: Bool
|
|
|
|
|
|
/// 0 = unlimited.
|
|
|
|
|
|
public let maxUsers: UInt32
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: UInt32, parentId: UInt32, name: String, topic: String,
|
|
|
|
|
|
passwordProtected: Bool, maxUsers: UInt32) {
|
|
|
|
|
|
self.id = id; self.parentId = parentId; self.name = name; self.topic = topic
|
|
|
|
|
|
self.passwordProtected = passwordProtected; self.maxUsers = maxUsers
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Channel creation/edition descriptor — mirrors `vc_channel_info`. Used by
|
|
|
|
|
|
/// `createChannel(_:)` and `editChannel(_:)`. `id == 0` means new channel (for create).
|
|
|
|
|
|
public struct ChannelEdit: Sendable, Equatable {
|
|
|
|
|
|
public let id: UInt32 // 0 = new channel for create
|
|
|
|
|
|
public let parentId: UInt32 // 0 = root
|
|
|
|
|
|
public let name: String
|
|
|
|
|
|
public let topic: String
|
|
|
|
|
|
public let passwordProtected: Bool
|
|
|
|
|
|
public let password: String? // nil/empty ignored if passwordProtected == false
|
|
|
|
|
|
public let maxUsers: UInt32 // 0 = unlimited
|
|
|
|
|
|
public let sortOrder: UInt32
|
|
|
|
|
|
/// 0/nil fields use server defaults.
|
|
|
|
|
|
public let audio: AudioConfig
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: UInt32, parentId: UInt32, name: String, topic: String,
|
|
|
|
|
|
passwordProtected: Bool, password: String?, maxUsers: UInt32,
|
|
|
|
|
|
sortOrder: UInt32, audio: AudioConfig) {
|
|
|
|
|
|
self.id = id; self.parentId = parentId; self.name = name; self.topic = topic
|
|
|
|
|
|
self.passwordProtected = passwordProtected; self.password = password
|
|
|
|
|
|
self.maxUsers = maxUsers; self.sortOrder = sortOrder; self.audio = audio
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// User snapshot — mirrors `vc_user`.
|
|
|
|
|
|
public struct User: Sendable, Equatable, Identifiable {
|
|
|
|
|
|
public let id: UInt32
|
|
|
|
|
|
public let nickname: String
|
|
|
|
|
|
public let isGuest: Bool
|
|
|
|
|
|
public let channelId: UInt32
|
|
|
|
|
|
public let selfMicMuted: Bool
|
|
|
|
|
|
public let selfDeafened: Bool
|
|
|
|
|
|
public let serverMuted: Bool
|
|
|
|
|
|
public let serverDeafened: Bool
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: UInt32, nickname: String, isGuest: Bool, channelId: UInt32,
|
|
|
|
|
|
selfMicMuted: Bool, selfDeafened: Bool, serverMuted: Bool,
|
|
|
|
|
|
serverDeafened: Bool) {
|
|
|
|
|
|
self.id = id; self.nickname = nickname; self.isGuest = isGuest; self.channelId = channelId
|
|
|
|
|
|
self.selfMicMuted = selfMicMuted; self.selfDeafened = selfDeafened
|
|
|
|
|
|
self.serverMuted = serverMuted; self.serverDeafened = serverDeafened
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Permission bitset — mirrors `vc_permissions` (M5).
|
|
|
|
|
|
public struct Permissions: Sendable, Equatable {
|
|
|
|
|
|
public let canCreateTempChannel: Bool
|
|
|
|
|
|
public let canKick: Bool
|
|
|
|
|
|
public let canBan: Bool
|
|
|
|
|
|
public let canMoveUsers: Bool
|
|
|
|
|
|
public let canAdminAccounts: Bool
|
|
|
|
|
|
public let isAdmin: Bool
|
|
|
|
|
|
|
|
|
|
|
|
public init(canCreateTempChannel: Bool, canKick: Bool, canBan: Bool,
|
|
|
|
|
|
canMoveUsers: Bool, canAdminAccounts: Bool, isAdmin: Bool) {
|
|
|
|
|
|
self.canCreateTempChannel = canCreateTempChannel; self.canKick = canKick; self.canBan = canBan
|
|
|
|
|
|
self.canMoveUsers = canMoveUsers; self.canAdminAccounts = canAdminAccounts; self.isAdmin = isAdmin
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Account entry — mirrors `vc_account` (M5, reply to `listAccounts()`).
|
|
|
|
|
|
public struct Account: Sendable, Equatable {
|
|
|
|
|
|
public let username: String
|
|
|
|
|
|
public let isAdmin: Bool
|
|
|
|
|
|
public let createdAtUnixMs: UInt64
|
|
|
|
|
|
public let lastLoginUnixMs: UInt64
|
|
|
|
|
|
|
|
|
|
|
|
public init(username: String, isAdmin: Bool, createdAtUnixMs: UInt64,
|
|
|
|
|
|
lastLoginUnixMs: UInt64) {
|
|
|
|
|
|
self.username = username; self.isAdmin = isAdmin
|
|
|
|
|
|
self.createdAtUnixMs = createdAtUnixMs; self.lastLoginUnixMs = lastLoginUnixMs
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Per-user stream summary — mirrors `vc_stream_summary`. For the full effective Opus
|
|
|
|
|
|
/// config of a specific (user_id, stream_id), use `VoiceCatClient.getStreamAudioConfig`.
|
|
|
|
|
|
public struct StreamSummary: Sendable, Equatable, Identifiable {
|
|
|
|
|
|
public let id: UInt32 // stream_id
|
|
|
|
|
|
public let kind: VoiceCatStreamKind
|
|
|
|
|
|
public let label: String
|
|
|
|
|
|
|
|
|
|
|
|
public init(streamId: UInt32, kind: VoiceCatStreamKind, label: String) {
|
|
|
|
|
|
self.id = streamId; self.kind = kind; self.label = label
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Receive-side state the local listener chose for a specific remote stream — mirrors
|
|
|
|
|
|
/// `vc_remote_stream_state`. All LOCAL (no protocol traffic) — docs/voice.md §10.
|
|
|
|
|
|
/// Defaults (if `setRemoteStream` was never called): gain 1.0, unmuted, NR off.
|
|
|
|
|
|
public struct RemoteStreamState: Sendable, Equatable {
|
|
|
|
|
|
public let gain: Float // 0.0–… ; default 1.0
|
|
|
|
|
|
public let muted: Bool
|
|
|
|
|
|
public let noiseReduction: Bool
|
|
|
|
|
|
|
|
|
|
|
|
public init(gain: Float, muted: Bool, noiseReduction: Bool) {
|
|
|
|
|
|
self.gain = gain; self.muted = muted; self.noiseReduction = noiseReduction
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Audio device — mirrors `vc_device`. `id` is an opaque, internally-encoded handle
|
|
|
|
|
|
/// (currently hex-encoded `ma_device_id`) — always round-trip an id from `listDevices`;
|
|
|
|
|
|
/// never construct one by hand (docs/architecture.md §4).
|
|
|
|
|
|
public struct Device: Sendable, Equatable, Identifiable {
|
|
|
|
|
|
public let id: String
|
|
|
|
|
|
public let name: String
|
|
|
|
|
|
public let isDefault: Bool
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: String, name: String, isDefault: Bool) {
|
|
|
|
|
|
self.id = id; self.name = name; self.isDefault = isDefault
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/// iOS audio input port — derived from `AVAudioSession.availableInputs`. Unlike the
|
|
|
|
|
|
/// miniaudio-based `Device` (which returns ~2 entries on iOS), this exposes the real
|
|
|
|
|
|
/// AVAudioSession input ports (builtInMic, bluetoothHFP, headsetMic, usbAudio, airPlay)
|
|
|
|
|
|
/// with their data sources (orientation: front/back/top/bottom) and polar patterns
|
|
|
|
|
|
/// (omni/cardioid/subcardioid/bidirectional). Used by `IOSAudioRouter` + `SettingsView`.
|
|
|
|
|
|
public struct IOSAudioInputPort: Identifiable, Hashable {
|
|
|
|
|
|
public let id: String // port UID (stable across route changes)
|
|
|
|
|
|
public let name: String // human-readable port name
|
|
|
|
|
|
public let portType: String // AVAudioSession.Port raw value as string
|
|
|
|
|
|
public let dataSources: [IOSAudioDataSource]?
|
|
|
|
|
|
public let isSelected: Bool // true if this is the current preferredInput
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: String, name: String, portType: String,
|
|
|
|
|
|
dataSources: [IOSAudioDataSource]?, isSelected: Bool) {
|
|
|
|
|
|
self.id = id; self.name = name; self.portType = portType
|
|
|
|
|
|
self.dataSources = dataSources; self.isSelected = isSelected
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// iOS audio data source — a sub-selection of an input port (e.g. built-in mic
|
|
|
|
|
|
/// orientation: front/back/top/bottom). May have polar pattern options.
|
|
|
|
|
|
public struct IOSAudioDataSource: Identifiable, Hashable {
|
|
|
|
|
|
public let id: String // dataSource UID
|
|
|
|
|
|
public let name: String // "Front", "Back", "Top", "Bottom"
|
|
|
|
|
|
public let polarPatterns: [String]? // AVAudioSession.PolarPattern raw values
|
|
|
|
|
|
public let isSelected: Bool // true if this is the current preferredDataSource
|
|
|
|
|
|
public let selectedPolarPattern: String?
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: String, name: String, polarPatterns: [String]?,
|
|
|
|
|
|
isSelected: Bool, selectedPolarPattern: String?) {
|
|
|
|
|
|
self.id = id; self.name = name; self.polarPatterns = polarPatterns
|
|
|
|
|
|
self.isSelected = isSelected; self.selectedPolarPattern = selectedPolarPattern
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// iOS audio output route — read-only display of `AVAudioSession.currentRoute.outputs`.
|
|
|
|
|
|
public struct IOSAudioOutputRoute: Identifiable, Hashable {
|
|
|
|
|
|
public let id: String // port UID
|
|
|
|
|
|
public let name: String // human-readable route name
|
|
|
|
|
|
public let portType: String // AVAudioSession.Port raw value as string
|
|
|
|
|
|
|
|
|
|
|
|
public init(id: String, name: String, portType: String) {
|
|
|
|
|
|
self.id = id; self.name = name; self.portType = portType
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
/// Effective Opus configuration — mirrors `vc_audio_config`.
|
|
|
|
|
|
public struct AudioConfig: Sendable, Equatable {
|
|
|
|
|
|
public let codec: UInt32 // 0 = OPUS
|
|
|
|
|
|
public let stereo: Bool // mode: 0 = mono, 1 = stereo
|
|
|
|
|
|
public let sampleRate: UInt32
|
|
|
|
|
|
public let bitrateBps: UInt32
|
|
|
|
|
|
public let frameMs: UInt32
|
|
|
|
|
|
public let application: UInt32 // 0 = VOIP, 1 = AUDIO, 2 = LOWDELAY
|
|
|
|
|
|
public let fec: Bool
|
|
|
|
|
|
public let expectedPacketLoss: UInt32 // % 0..100
|
|
|
|
|
|
public let dtx: Bool
|
|
|
|
|
|
public let complexity: UInt32 // 0..10
|
feat(clients): expose all channel codec params + guest nickname everywhere
Channel create/edit UIs only surfaced a subset of the core's vc_audio_config,
and DRED was exposed nowhere. While adding it, found a latent ABI mismatch:
both Swift AudioConfig and the C# VcAudioConfigNative blittable struct were one
int short of the native vc_audio_config (missing the trailing `dred`), so native
read past the managed struct in vc_create_channel/vc_edit_channel.
- core marshaling: thread `dred` through Swift (Models/Marshaling/toNative) and
C# (Structs/Models/Marshaling/VoiceCatClient) -- fixes the ABI gap + enables it
- windows: add the one missing DRED checkbox to ChannelEditDialog
- macos: ChannelEditSheet now exposes application, sample rate, packet loss,
complexity, and DRED (was stereo/bitrate/frame/FEC/DTX only)
- ios: rebuild ChannelEditView into a full create+edit form (all params); add
SessionState.editChannel + an admin Edit swipe action (iOS had no edit UI)
- guest nickname: add a dedicated `nickname` to SavedServer on macOS+iOS
(backward-compatible Codable), shown in Guest mode, wired into the guest auth
path -- guests could not set a display name on either before (only Windows)
Verified: macOS + iOS (sim, arm64) xcodebuild BUILD SUCCEEDED; core ctest 22/23
(only external_pcm aborts on a pre-existing shutdown mutex race; no C++ changed).
2026-06-21 04:03:50 +02:00
|
|
|
|
public let dred: Bool // Deep REDundancy (Opus 1.6), off by default
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
|
|
|
|
|
|
public init(codec: UInt32 = 0, stereo: Bool = false, sampleRate: UInt32 = 48000,
|
|
|
|
|
|
bitrateBps: UInt32 = 64000, frameMs: UInt32 = 20, application: UInt32 = 0,
|
|
|
|
|
|
fec: Bool = true, expectedPacketLoss: UInt32 = 5, dtx: Bool = false,
|
feat(clients): expose all channel codec params + guest nickname everywhere
Channel create/edit UIs only surfaced a subset of the core's vc_audio_config,
and DRED was exposed nowhere. While adding it, found a latent ABI mismatch:
both Swift AudioConfig and the C# VcAudioConfigNative blittable struct were one
int short of the native vc_audio_config (missing the trailing `dred`), so native
read past the managed struct in vc_create_channel/vc_edit_channel.
- core marshaling: thread `dred` through Swift (Models/Marshaling/toNative) and
C# (Structs/Models/Marshaling/VoiceCatClient) -- fixes the ABI gap + enables it
- windows: add the one missing DRED checkbox to ChannelEditDialog
- macos: ChannelEditSheet now exposes application, sample rate, packet loss,
complexity, and DRED (was stereo/bitrate/frame/FEC/DTX only)
- ios: rebuild ChannelEditView into a full create+edit form (all params); add
SessionState.editChannel + an admin Edit swipe action (iOS had no edit UI)
- guest nickname: add a dedicated `nickname` to SavedServer on macOS+iOS
(backward-compatible Codable), shown in Guest mode, wired into the guest auth
path -- guests could not set a display name on either before (only Windows)
Verified: macOS + iOS (sim, arm64) xcodebuild BUILD SUCCEEDED; core ctest 22/23
(only external_pcm aborts on a pre-existing shutdown mutex race; no C++ changed).
2026-06-21 04:03:50 +02:00
|
|
|
|
complexity: UInt32 = 10, dred: Bool = false) {
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
self.codec = codec; self.stereo = stereo; self.sampleRate = sampleRate
|
|
|
|
|
|
self.bitrateBps = bitrateBps; self.frameMs = frameMs; self.application = application
|
|
|
|
|
|
self.fec = fec; self.expectedPacketLoss = expectedPacketLoss; self.dtx = dtx
|
feat(clients): expose all channel codec params + guest nickname everywhere
Channel create/edit UIs only surfaced a subset of the core's vc_audio_config,
and DRED was exposed nowhere. While adding it, found a latent ABI mismatch:
both Swift AudioConfig and the C# VcAudioConfigNative blittable struct were one
int short of the native vc_audio_config (missing the trailing `dred`), so native
read past the managed struct in vc_create_channel/vc_edit_channel.
- core marshaling: thread `dred` through Swift (Models/Marshaling/toNative) and
C# (Structs/Models/Marshaling/VoiceCatClient) -- fixes the ABI gap + enables it
- windows: add the one missing DRED checkbox to ChannelEditDialog
- macos: ChannelEditSheet now exposes application, sample rate, packet loss,
complexity, and DRED (was stereo/bitrate/frame/FEC/DTX only)
- ios: rebuild ChannelEditView into a full create+edit form (all params); add
SessionState.editChannel + an admin Edit swipe action (iOS had no edit UI)
- guest nickname: add a dedicated `nickname` to SavedServer on macOS+iOS
(backward-compatible Codable), shown in Guest mode, wired into the guest auth
path -- guests could not set a display name on either before (only Windows)
Verified: macOS + iOS (sim, arm64) xcodebuild BUILD SUCCEEDED; core ctest 22/23
(only external_pcm aborts on a pre-existing shutdown mutex race; no C++ changed).
2026-06-21 04:03:50 +02:00
|
|
|
|
self.complexity = complexity; self.dred = dred
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Stream descriptor — mirrors `vc_stream_desc`. Used by `startStream(kind:deviceId:label:)`.
|
|
|
|
|
|
public struct StreamDescriptor: Sendable, Equatable {
|
|
|
|
|
|
public let kind: VoiceCatStreamKind
|
|
|
|
|
|
/// nil = default device for this kind.
|
|
|
|
|
|
public let deviceId: String?
|
|
|
|
|
|
public let label: String
|
2026-06-22 02:38:01 +02:00
|
|
|
|
/// When true the caller feeds PCM via `feedPcm` (e.g. the iOS VPIO mic path) and the core
|
|
|
|
|
|
/// skips opening a hardware capture device for this stream. Mirrors `vc_stream_desc.external_feed`.
|
|
|
|
|
|
public let externalFeed: Bool
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
|
2026-06-22 02:38:01 +02:00
|
|
|
|
public init(kind: VoiceCatStreamKind, deviceId: String? = nil, label: String,
|
|
|
|
|
|
externalFeed: Bool = false) {
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
self.kind = kind; self.deviceId = deviceId; self.label = label
|
2026-06-22 02:38:01 +02:00
|
|
|
|
self.externalFeed = externalFeed
|
feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients
Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.
Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.
Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
(libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.
VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
(10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
[UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
delivery on main queue via coalesced DispatchQueue.main drain).
Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.
Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Server identity info — parsed from a `.serverIdentity` event + `getServerIdentityDisplay()`.
|
|
|
|
|
|
/// The `tlsCertFingerprint` (SHA-256 hex of the TLS leaf cert) is the value the TOFU gate
|
|
|
|
|
|
/// actually pins on; `ed25519Fingerprint` is display-only (docs/security.md §1.1).
|
|
|
|
|
|
public struct ServerIdentity: Sendable, Equatable {
|
|
|
|
|
|
public let tofuStatus: VoiceCatTofuStatus
|
|
|
|
|
|
/// SHA-256 hex of the TLS leaf certificate — the pinned value. No separators (64 chars).
|
|
|
|
|
|
public let tlsCertFingerprint: String
|
|
|
|
|
|
/// Ed25519 identity fingerprint from ServerHello, hex-formatted — display only.
|
|
|
|
|
|
/// Empty if not yet available.
|
|
|
|
|
|
public let ed25519Fingerprint: String
|
|
|
|
|
|
|
|
|
|
|
|
public init(tofuStatus: VoiceCatTofuStatus, tlsCertFingerprint: String,
|
|
|
|
|
|
ed25519Fingerprint: String) {
|
|
|
|
|
|
self.tofuStatus = tofuStatus; self.tlsCertFingerprint = tlsCertFingerprint
|
|
|
|
|
|
self.ed25519Fingerprint = ed25519Fingerprint
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|