Single-crate with module-level cfg gates is correct architecture. Splitting would add dependency complexity with no compilation benefit.
4.1 KiB
Platform Backend Extraction Evaluation — chanora_audio
Date: 2026-06-11
Status: Evaluation (not implementation)
Crate: chanora_audio (~18,500 lines total)
Current Structure
Platform-specific code is isolated into dedicated files with #[cfg] gates at the module boundary. The engine module (engine/mod.rs, engine/lifecycle.rs) uses pervasive inline cfg attributes to dispatch across platforms.
Lines Per Platform (exclusive files only)
| Platform | Files | Lines | Key Dependencies |
|---|---|---|---|
| iOS/macOS | ios_voice_unit.rs, vad/apple_coreml.rs, ptt_backends/macos.rs, voice_render.rs |
~2,600 | coreaudio-rs, dispatch2 |
| Android | android_voice_unit.rs, android_render_ring.rs, audio_event_queue.rs |
~2,130 | oboe, jni, ndk-context, bytemuck |
| Desktop | engine/capture.rs, engine/render.rs, sdl_output.rs, ptt_backends/windows*.rs, ptt_backends/linux.rs, vad/silero_onnx.rs |
~4,400 | cpal, sdl2, ort, windows, zbus |
Shared Code (cross-platform)
| Module | Lines | Notes |
|---|---|---|
mobile_voice_backend.rs |
813 | Trait + types for iOS/Android backends |
engine/mod.rs + engine/lifecycle.rs |
2,255 | Heavy inline cfg dispatch |
processor/ |
~500 | AudioProcessor trait + backends |
vad/mod.rs + vad/resampler.rs |
393 | VAD trait + WebRTC fallback |
Other shared (frame, opus_voice, ptt, voice_activity, etc.) |
~3,200 | Platform-neutral |
Assessment
Would splitting help?
No — not recommended at this time.
Reasons Against Splitting
-
cfg gating already works. Platform files are cleanly isolated at the module boundary. The compiler strips unused code per-target; a separate crate doesn't add compilation speed for the active target.
-
Shared types are deeply embedded.
AudioError,AudioEffects,AudioProcessingConfig,VoiceActivityStateMachine,OpusEncodersetup,frame::*helpers, and theAudioProcessortrait are used by every platform. Extracting these into achanora_audio_commoncrate is mandatory before splitting, adding a dependency node every platform crate must pull in. -
engine/lifecycle.rs is the real problem — but it's an integration point, not a platform backend. This 1,149-line file dispatches
start_audio/stop_audioacross all platforms with inlinecfg. Splitting backends into separate crates wouldn't reduce this file's complexity — it would just move the cross-crate import surface here. -
Dependency graph complexity. The current single crate has 5
cfg-gated dependency blocks in Cargo.toml. Splitting into 3+ crates means each platform crate needs its own Cargo.toml with the shared types dependency, and the top-levelchanora_audio(orchanora_core) must depend on all of them with target-conditionalcfgfeatures. -
Test surface stays the same. Platform-specific tests already compile only on their target OS. A crate boundary doesn't improve test isolation.
-
The
MobileVoiceAudioBackendtrait is the natural seam — and it already exists.mobile_voice_backend.rsdefines the cross-platform interface. The iOS backend will implement it under SDD-117. This is the correct abstraction boundary without adding a crate boundary.
When Splitting Would Make Sense
- If build times for cross-compilation become painful (building all 3 platform variants from CI)
- If platform-specific dependencies cause feature flag conflicts (not observed today)
- If the crate exceeds ~30k lines and the
cfgdensity makes navigation difficult - If a platform team needs to own a crate independently
Recommendation
Keep the current single-crate structure. Focus cleanup effort on:
- Reducing inline
cfginengine/lifecycle.rs— extract platform dispatch into the existingIosVoiceBackendenum pattern - Back-filling
IosVoiceUnittoMobileVoiceAudioBackend(SDD-117) — this unifies the mobile interface - Documenting the cfg convention — module-level
cfgat the file boundary (current pattern) vs. inlinecfgblocks in shared files