etalk-ffi (Swift and Kotlin)
The UniFFI 0.31 facade over etalk (the call machine) and
etalk-codec (Opus) for iOS, macOS and Android, so a phone
places the same call as the desktop (#EXP-1441): an xcframework / an Android
.so plus the generated bindings. gpui-free, engine-free, no app crate.
Audio I/O stays native (AVAudioEngine; Oboe / AudioRecord): the facade takes
and returns PCM frames. Transport is not eTalk’s job: the packets ride eNet’s
call channel (exp/enet/call/1, exponential-net-ffi’s sendFrame).
The contract
Section titled “The contract”The spec is packages/etalk/fixtures/voice-call.json (its $comment); the
TypeScript reference is packages/etalk/src/voice-call.ts.
- The call machine, pure functions over plain records:
callIdle()→callStep(state, event)→callView(state, platform)(None = no call surface).TalkCallEvent=start {device, language},rang,ready,noSound,partial,said,speechStarted,queued {count, text},replyStarted,replyFinished,mute,unmute,speaker {on},end,hungUp {reason?},failed {code}. A finished call with a failure leavesstate.toast.callUnavailable(input)(None = the telephone is enabled) andcallSpeechOf(doctor row state).- The ring (#EXP-1423): the HOST draws
callRingRandom()once per call and stepsrangatcallRingMs(random);ready= the voice device’s warm-up done;callAnswerMs(random, readyMs)= when it answers (None = never: fail itunavailableatmaxRingMs); then the agent speaks first (the greeting,callGreeting(language)).callRingToneOn(ms)= the ring tone’s cadence (ringLowHz+ringHighHz, looping until answered). callTimer(ms since answered).- The wire form of a flow event:
callEventOfWire(json)for a caller’s incomingcall_event,callWireEvent(event)for a host’s outgoing one. callConstants()(the fixture’s numbers +wire),callSpecJson()(the fixture itself: copy, icons, test ids).
- The codec (mono,
frameMs= 20 ms frames, i16 PCM):TalkEncoder(rate, bitrate, lossPercent?)→encode(pcm)→ one packet (a caller:upRateatupKbps).lossPercentturns in-band FEC on.TalkDecoder(rate)→decode(packet)(an empty packet conceals a lost frame),decodeFec(next packet)= the lost frame before it (a caller:downRate, behind itsjitterMsbuffer).TalkUplink(rate)→push(seq, packet): the host’s receive path, one lost packet recovered from the next one’s FEC.codecBackend()=opus-rs(pure Rust, no C toolchain).
- Errors:
TalkError=Invalid | Codec, each with areason. A Rust panic never crosses the FFI: the machine is total and the codec turns an opus-rs panic intoCodecwith a fresh coder behind it.
bash apps/desktop/crates/etalk-ffi/generate-bindings.sh # bindings/swift + bindings/kotlin from the host librarybash apps/desktop/crates/etalk-ffi/build-ios.sh # out/ExponentialTalkFFI.xcframework (+ bindings)bash apps/desktop/crates/etalk-ffi/build-android.sh # out/jniLibs/<abi>/libetalk_ffi.so (+ bindings)bash apps/desktop/crates/etalk-ffi/run-binding-tests.sh # the Swift + Kotlin suites against the host dylibDevice builds use the workspace’s mobile profile (LTO, one codegen unit,
stripped), one crate type per build so LTO applies. build-ios.sh takes a
lock per target dir (a shared CARGO_TARGET_DIR is safe). The generated
binding sources under bindings/ are COMMITTED (the contract, reviewable
without a toolchain; Kotlin ships with omit_checksums, so regenerate after
every lib.rs change); out/ is ignored. Kotlin uses JNA (≥ 5.12), like
exponential-ui-ffi.
tests/facade.rs replays the fixture through the facade’s types from Rust;
tests/swift/main.swift and tests/kotlin/FacadeTest.kt replay the same
fixture through the bindings (the events built natively, and once more
through their wire form) and round-trip a 440 Hz tone through the codec with
one packet lost and recovered.
Distribution
Section titled “Distribution”Every etalk-v* tag (release-etalk.yml, docs/libraries.md) publishes:
- SwiftPM:
https://github.com/Niach/exponential-talk-swift, tagged<version>, productExponentialTalk(import ExponentialTalk; iOS 15+, macOS 12+). The xcframework zip is an asset of theetalk-v<version>release.release/swift-package.shwrites the package (--localfor a path binary you canswift build). - Maven Central:
at.exponential:etalk:<version>(an AAR: the.sofor arm64-v8a, armeabi-v7a, x86_64 + the binding, packageat.exponential.talk.ffi; JNA comes along).android/is the Gradle library on eUI Compose’s toolchain;release/central-bundle.shis the Portal bundle.
.package(url: "https://github.com/Niach/exponential-talk-swift", exact: "0.2.4")implementation("at.exponential:etalk:0.2.4")