52 lines
2.9 KiB
Swift
52 lines
2.9 KiB
Swift
|
|
// Callbacks — the C function pointers passed to `vc_callbacks`. These are the Swift
|
||
|
|
// equivalent of the C# client's `[UnmanagedCallersOnly]` static methods (NativeCallbacks.cs).
|
||
|
|
//
|
||
|
|
// The critical patterns (carried over from the proven C# implementation):
|
||
|
|
// 1. `@convention(c)` closures — plain C function pointers, NOT GC/ARC-managed closures.
|
||
|
|
// A @convention(c) closure cannot capture context, which is why the `user` pointer is
|
||
|
|
// used to resolve back to the VoiceCatClient instance (the C# version uses GCHandle for
|
||
|
|
// the same thing; Swift uses Unmanaged).
|
||
|
|
// 2. `Unmanaged.passUnretained(self).toOpaque()` as the `user` context — a stable raw
|
||
|
|
// pointer to the Swift object WITHOUT incrementing the retain count. This is safe
|
||
|
|
// because `deinit` calls `vc_client_destroy` (which synchronously joins every internal
|
||
|
|
// thread) BEFORE the object's memory is freed — so no callback can fire after the object
|
||
|
|
// is gone. (The C# equivalent: GCHandle.Alloc + GCHandle.Free in Dispose.)
|
||
|
|
// 3. Copy `ev.text` to a Swift `String` INSIDE `onEvent` (via `VoiceCatEvent.from(_:)`)
|
||
|
|
// before returning — the raw pointer is dangling after the callback returns. This is
|
||
|
|
// the #1 lifetime rule from voicecat.h's vc_event doc comment.
|
||
|
|
|
||
|
|
import VoiceCatC
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
/// Internal: builds the `vc_callbacks` struct wired to VoiceCatClient's C function pointers.
|
||
|
|
/// The `user` context is an Unmanaged-passUnretained pointer to the client — resolved back
|
||
|
|
/// to the client inside `onEvent`/`onLevel` below.
|
||
|
|
internal enum Callbacks {
|
||
|
|
/// The `on_event` C function pointer. Non-capturing @convention(c) closure — resolves
|
||
|
|
/// the VoiceCatClient from `user` and enqueues a safe copy of the event.
|
||
|
|
static let onEvent: @convention(c) (
|
||
|
|
UnsafeMutableRawPointer?, UnsafePointer<vc_event>?
|
||
|
|
) -> Void = { user, ev in
|
||
|
|
guard let user, let ev else { return }
|
||
|
|
let client = Unmanaged<VoiceCatClient>.fromOpaque(user).takeUnretainedValue()
|
||
|
|
// Copy the event (including text) to a Swift value NOW — the raw vc_event is
|
||
|
|
// invalid after this callback returns.
|
||
|
|
client.enqueueEvent(VoiceCatEvent.from(ev.pointee))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The `on_level` C function pointer. Coalesces to "latest sample per stream_id"
|
||
|
|
/// (intermediate values are visually irrelevant — same as C#'s ConcurrentDictionary).
|
||
|
|
static let onLevel: @convention(c) (
|
||
|
|
UnsafeMutableRawPointer?, UInt32, Float
|
||
|
|
) -> Void = { user, streamId, rms in
|
||
|
|
guard let user else { return }
|
||
|
|
let client = Unmanaged<VoiceCatClient>.fromOpaque(user).takeUnretainedValue()
|
||
|
|
client.enqueueLevel(streamId, rms)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Construct the vc_callbacks struct for a given client.
|
||
|
|
static func make(user: UnsafeMutableRawPointer) -> vc_callbacks {
|
||
|
|
vc_callbacks(on_event: onEvent, on_level: onLevel, user: user)
|
||
|
|
}
|
||
|
|
}
|