Retire legacy implementations and flatten managed layout
This commit is contained in:
+17
-152
@@ -1,160 +1,25 @@
|
||||
# Apple client (macOS + iOS)
|
||||
# Apple clients
|
||||
|
||||
Built in **M4** (see [`docs/roadmap.md`](../../docs/roadmap.md)). One shared **Swift core**
|
||||
(`VoiceCatCore` package) wrapping the C ABI ([`core/include/voicecat.h`](../../core/include/voicecat.h)),
|
||||
with platform-specific UIs: **AppKit** for macOS (best VoiceOver accessibility), **SwiftUI**
|
||||
for iOS. See [`docs/architecture.md`](../../docs/architecture.md) §4 and
|
||||
[`docs/tech-stack.md`](../../docs/tech-stack.md) §2.
|
||||
`VoiceCat.Mac` and `VoiceCat.iOS` are .NET 10 AppKit and UIKit clients over the shared managed
|
||||
core. The ReplayKit upload extension under `native/apple/broadcast` remains Swift because it
|
||||
runs under the extension memory limit and writes the versioned shared audio ring.
|
||||
|
||||
## What's here now
|
||||
|
||||
### `VoiceCatCore` Swift Package — ✓ complete (2026-06-18)
|
||||
|
||||
The shared Swift core that both the macOS AppKit app and the iOS SwiftUI app will consume.
|
||||
Mirrors the Windows client's `VoiceCat.Interop` layer ([`clients/windows/`](../windows/))
|
||||
using Swift-native C interop instead of P/Invoke.
|
||||
|
||||
```
|
||||
clients/apple/
|
||||
├── Package.swift # SPM: binary target (XCFramework) + VoiceCatCore library + tests
|
||||
├── VoiceCatCore.xcframework/ # BUILT ARTIFACT — produced by scripts/build-xcframework.sh (gitignored)
|
||||
├── scripts/
|
||||
│ └── build-xcframework.sh # builds libvoicecat + vcpkg deps → fat .a → XCFramework + module map
|
||||
├── Sources/VoiceCatCore/
|
||||
│ ├── Enums.swift # Swift-idiomatic mirrors of the 9 voicecat.h C enums
|
||||
│ ├── Config.swift # VoiceCatConfig (wraps vc_config)
|
||||
│ ├── Event.swift # VoiceCatEvent — copies ev.text inside the callback (the #1 lifetime rule)
|
||||
│ ├── Models.swift # Channel, User, Stream, Device, Permissions, Account, AudioConfig, …
|
||||
│ ├── Marshaling.swift # C arrays → Swift arrays + immediate vc_free_* (callers never manage native lifetime)
|
||||
│ ├── Callbacks.swift # @convention(c) on_event/on_level + Unmanaged.passUnretained context bridging
|
||||
│ └── VoiceCatClient.swift # the public Swift surface — owns vc_client*, all 38 C functions, event delivery on @MainActor
|
||||
└── Tests/VoiceCatCoreTests/
|
||||
└── VoiceCatClientSmokeTests.swift # 6 XCTest smoke tests against a real voicecat-server (6/6 green)
|
||||
```
|
||||
|
||||
**Key patterns** (carried over from the proven C# `VoiceCat.Interop` — see
|
||||
[`docs/architecture.md`](../../docs/architecture.md) §4 per-platform binding notes):
|
||||
|
||||
- **C interop via module map:** `import VoiceCatC` — Swift sees all C enums/structs/functions
|
||||
directly. No manual struct/function redeclaration (unlike C# P/Invoke). The module map
|
||||
(`module VoiceCatC { header "voicecat.h" }`) is staged into the XCFramework headers by
|
||||
`build-xcframework.sh`.
|
||||
- **`@convention(c)` callbacks:** plain C function pointers (not ARC-managed closures) +
|
||||
`Unmanaged.passUnretained(self)` as the `user` context — the Swift analog of C#'s
|
||||
`[UnmanagedCallersOnly]` + `GCHandle`. `deinit` calls `vc_client_destroy` (joins all
|
||||
threads) before the object's memory is freed, so no callback can fire with a dangling
|
||||
pointer.
|
||||
- **Config string lifetimes:** the core stores raw pointers from `vc_config` (doesn't copy).
|
||||
Native CString storage (`strdup`) is held for the client's entire lifetime, freed in
|
||||
`deinit` after `vc_client_destroy`.
|
||||
- **Event delivery:** events buffered in a lock-protected array + coalesced
|
||||
`DispatchQueue.main` drain (one async block at a time) — the Swift analog of C#'s
|
||||
`Channel<VoiceCatEvent>` + 30ms WinForms Timer pump. `ev.text` is copied to `String`
|
||||
inside the callback before enqueueing (dangling-pointer rule).
|
||||
- **Level meters:** coalesced to latest-per-stream-id (intermediate values are visually
|
||||
irrelevant, same as C#'s `ConcurrentDictionary<uint,float>`).
|
||||
- **Immediate `vc_free_*`** on list reads — callers never manage native list lifetime.
|
||||
|
||||
### Tests — 6/6 green
|
||||
|
||||
```
|
||||
swift test
|
||||
# ✓ testVersionStringIsNonEmpty
|
||||
# ✓ testResultStringRoundTrips
|
||||
# ✓ testConnectTofuAuthListChannelsRoundTrips (connect → TOFU → confirm → guest auth → channels → permissions → guest ListAccounts rejected)
|
||||
# ✓ testAdminChannelCrudAccountCrudRoundTrips (admin auth → channel create/edit/delete → account create/list/reset/delete)
|
||||
# ✓ testScreenAudioStreamStartsAndStops (screen-audio stream start/stop through Swift interop)
|
||||
# ✓ testPerStreamRecvControlsRoundTrip (two clients, per-stream gain/mute/NR round-trip)
|
||||
```
|
||||
|
||||
Prerequisites for tests: `cmake --preset dev && cmake --build --preset dev` (builds
|
||||
`voicecat-server` + `voicecat-admin` into `build/dev/bin/`).
|
||||
|
||||
## What's NOT here yet (next steps)
|
||||
|
||||
- **macOS AppKit app** (`clients/apple/macOS/`) — the M4 UI: connect dialog, saved-server
|
||||
list (Keychain for passwords), TOFU identity dialog, main window (NSOutlineView channel
|
||||
tree, NSTableView user list, NSTextView chat, activity log), voice controls, per-user
|
||||
tuning, full VoiceOver accessibility. Mirrors the Windows `VoiceCat.App` feature set.
|
||||
- **iOS SwiftUI app** — AVAudioSession, mic permission, foreground voice.
|
||||
- **`vc_audio_suspend`/`vc_audio_resume` ABI hooks** — deferred until the iOS client
|
||||
milestone (keep ABI stable).
|
||||
- **ReplayKit Broadcast Upload Extension** for iOS `SCREEN_AUDIO` ([`docs/voice.md`](../../docs/voice.md) §9).
|
||||
- **macOS `SCREEN_AUDIO`** via ScreenCaptureKit (currently stub returns `false`).
|
||||
- **iOS XCFramework slices** — `apple-ios` / `apple-ios-sim` presets are scaffolding; run
|
||||
`scripts/build-xcframework.sh --all` once the iOS vcpkg triplets are validated.
|
||||
|
||||
## Building the XCFramework
|
||||
|
||||
The XCFramework is a **local build artifact** (gitignored, like the Windows client's
|
||||
`build/windows-client/bin/voicecat.dll`). Run the build script before `swift build` /
|
||||
`swift test`:
|
||||
Build on macOS with Xcode and the pinned .NET workloads:
|
||||
|
||||
```bash
|
||||
# Prerequisites: VCPKG_ROOT set, Xcode installed
|
||||
export VCPKG_ROOT=/path/to/vcpkg
|
||||
|
||||
# Build the macOS slice + fat static lib + XCFramework (validated)
|
||||
scripts/build-xcframework.sh
|
||||
# → clients/apple/VoiceCatCore.xcframework/ (macOS-arm64 slice)
|
||||
|
||||
# Build all 3 slices (macOS + iOS device + iOS sim) — iOS still scaffolding
|
||||
scripts/build-xcframework.sh --all
|
||||
./scripts/build-native.ps1
|
||||
./scripts/build-native-ios.sh
|
||||
dotnet restore clients/apple/VoiceCat.Apple.slnx
|
||||
dotnet build clients/apple/VoiceCat.Apple.slnx -c Debug --no-restore
|
||||
```
|
||||
|
||||
### Fat static library
|
||||
Use `publish-macos.sh --dry-run` to validate an ad-hoc macOS bundle. For distribution, set
|
||||
`VOICECAT_CODESIGN_IDENTITY`; optional notarization uses `APPLE_ID`, `APPLE_TEAM_ID`, and
|
||||
`APPLE_APP_PASSWORD`.
|
||||
|
||||
The `apple-dev` CMake preset produces a 1.9 MB `libvoicecat.a` containing only voicecat's
|
||||
own object files — vcpkg's static dependencies (protobuf, mbedtls, libsodium, opus, sqlite3,
|
||||
spdlog, asio, abseil, …) are 107 separate `.a` files under `vcpkg_installed/arm64-osx/lib/`,
|
||||
and the vendored RNNoise noise-suppression lib (`native/rnnoise/`, built as a CMake
|
||||
target → `build/<preset>/lib/librnnoise.a`) is another. A Swift Package binary target can
|
||||
only link ONE `.a` per XCFramework slice, so `build-xcframework.sh` merges them all — vcpkg
|
||||
deps plus the locally-built vendored libs — into a single self-contained `libvoicecat-fat.a`
|
||||
(~33 MB) using `libtool -static`. This is the Apple equivalent of how the Windows client
|
||||
ships a single `voicecat.dll` with all deps statically linked (via MinGW's `-static` flags
|
||||
in [`core/CMakeLists.txt`](../../core/CMakeLists.txt)). If you add another vendored (non-vcpkg)
|
||||
static-lib target to the core, it's picked up automatically as long as it lands in
|
||||
`build/<preset>/lib/` and isn't named `libvoicecat*`.
|
||||
For a physical iOS device, use `build-ios-device.sh` and `deploy-ios-device.sh`. The host and
|
||||
ReplayKit extension require signing profiles with App Group `group.me.iamtalon.voicecat`.
|
||||
Hardware validation must cover VoiceOver, background and lock behavior, interruptions, route
|
||||
changes, Bluetooth, ReplayKit, and iOS 27 ScreenCaptureKit audio.
|
||||
|
||||
### Swift Package
|
||||
|
||||
```bash
|
||||
swift build # builds VoiceCatCore library
|
||||
swift test # runs 6 smoke tests against a real voicecat-server
|
||||
```
|
||||
|
||||
The `Package.swift` declares:
|
||||
- A **binary target** (`VoiceCatCoreXCF`) pointing at the local `VoiceCatCore.xcframework`.
|
||||
- A **library target** (`VoiceCatCore`) that depends on the binary target and provides the
|
||||
Swift wrapper.
|
||||
- A **test target** (`VoiceCatCoreTests`) with `linkerSettings: [.linkedLibrary("c++")]` —
|
||||
the fat static lib is C++20, so the final executable must link libc++ (the LLVM C++ standard
|
||||
library on macOS). vcpkg's static deps are already in the `.a`; macOS system frameworks
|
||||
(CoreAudio/CoreFoundation) are auto-discovered by the linker.
|
||||
|
||||
## Ad-hoc distribution (iOS, pre-TestFlight)
|
||||
|
||||
To hand the iOS app to a handful of friends before TestFlight, use
|
||||
[`scripts/dist-ios-adhoc.sh`](../../scripts/dist-ios-adhoc.sh). It registers each device's
|
||||
UDID, builds an ad-hoc-signed `VoiceCatiOS.ipa`, and generates the `manifest.plist` +
|
||||
`index.html` for an over-the-air (`itms-services://`) web install. Ad-hoc builds only run on
|
||||
devices whose UDID is registered *before* signing, and stock iOS won't install a bare `.ipa`
|
||||
without a sideloading tool — so the web-install page is the friend-friendly path.
|
||||
|
||||
```bash
|
||||
# One-time: create an App Store Connect API "Team Key" (.p8, Admin/App Manager access) at
|
||||
# App Store Connect → Users and Access → Integrations → App Store Connect API
|
||||
export ASC_KEY_ID=ABC123 ASC_ISSUER_ID=1111-... ASC_KEY_PATH=~/.appstoreconnect/AuthKey_ABC123.p8
|
||||
|
||||
# Register a device + build + stage everything into dist/ios-adhoc/
|
||||
scripts/dist-ios-adhoc.sh --udid <UDID> --name "Friend iPhone" \
|
||||
--base-url https://example.com/voicecat
|
||||
```
|
||||
|
||||
Then upload the three staged files (`VoiceCatiOS.ipa`, `manifest.plist`, `index.html`) to
|
||||
that **HTTPS** folder and open `index.html` in Safari on a registered iPhone (iOS 18+).
|
||||
Device UDID registration is automated via [`scripts/asc_api.py`](../../scripts/asc_api.py)
|
||||
(`asc_api.py list` shows the registered devices against the 100-iOS-devices/year cap).
|
||||
Requires a paid Apple Developer Program membership. Run `scripts/dist-ios-adhoc.sh --help`
|
||||
for all flags.
|
||||
The shared ring contract is documented in `docs/broadcast-ring-format.md`.
|
||||
|
||||
Reference in New Issue
Block a user