- ios/Runner.xcodeproj/project.pbxproj: Update RunnerTests TEST_HOST
paths from Runner.app/Runner to Chanora.app/Chanora (target was
renamed in prior commit but test config still pointed at old paths,
breaking xcodebuild test).
- Cargo.toml: Move release DWARF flags from workspace [profile.release]
into Apple-only podspec CARGO_PROFILE_RELEASE_* env vars so Android,
Linux, Windows release builds stay lean (~10MB DWARF avoided).
- ios/Runner/Info.plist + macos/Runner/Info.plist: Flip
ITSAppUsesNonExemptEncryption from false to true (Chanora ships
ChaCha20-Poly1305 local storage + tsclientlib ECDH/AES-EAX voice
channel encryption, not exempt under Apple export-compliance rules).
- scripts/verify_silero_exports.sh: Make slice-aware via lipo -archs
loop + per-arch nm -arch invocation so universal macOS builds
verify every architecture slice, not just whichever slice nm picks.
- .gitignore: Drop .omo/ and .playwright-mcp/ entries (scope leak;
unrelated tooling state, not part of PR #26 archive-symbol concern).
The Apple CoreML Silero VAD backend resolves six @_cdecl Swift symbols
via dlsym(RTLD_DEFAULT) at runtime in the Rust audio crate. Local
flutter build paths preserved those symbols, but Xcode Archive (the
path used for TestFlight and App Store uploads) silently stripped them
through two independent mechanisms, causing Rust to fall back to
WebRTC VAD on every shipped build.
Both stripping mechanisms are now neutralised:
* ld dead-strip: OTHER_LDFLAGS now whitelists each of the six
chanora_silero_vad_* symbols via repeated `-Xlinker -exported_symbol`
pairs in ios/Flutter/{Release,Debug}.xcconfig and
macos/Flutter/Flutter-{Release,Debug}.xcconfig.
* install-time strip: STRIP_STYLE is set to `non-global` in the same
four xcconfigs so the post-link strip phase no longer drops exported
global text symbols from the Archive product. Cost: ~264 bytes per
binary; verified `xcrun strip` vs `xcrun strip -x` behaviour.
Self-test wired into both AppDelegates: at launch on a utility queue,
ChanoraSileroSelfTest resolves all six symbols through dlsym (the same
path the Rust runtime uses, not a direct call that would mask the bug
class) and exercises create → reset → process → destroy. Result is
logged via NSLog and surfaces in Console.app / idevicesyslog.
A post-link verify_silero_exports.sh build phase runs nm -gU on the
final Archive binary and fails the build if any of the six symbols are
missing. Empirically caught the original Archive regression that
flutter build --no-codesign did not.
CocoaPods bridge podspecs now emit a proper .dSYM via dsymutil so
TestFlight crash reports are symbolicated; Cargo.toml release profile
sets `debug = true` because dsymutil needs DWARF in the input dylib.
macOS chanora_bridge.podspec PATH inserts /opt/homebrew/opt/rustup/bin
ahead of /opt/homebrew/bin so rustup's cargo (which has the
x86_64-apple-darwin target installed) wins over the homebrew rust
formula that is aarch64-only.
iOS Podfile target renamed from `Runner` to `Chanora` to match the
Xcode target name shipped in the project (the workspace and scheme
already referenced Chanora; the Podfile mismatch produced lint
warnings during `pod install`).
ITSAppUsesNonExemptEncryption=false declared in both Info.plist files
so TestFlight and App Store Connect uploads skip the export-compliance
prompt; Chanora uses only platform-provided TLS.
.gitignore now covers Xcode archive bundles, IPA exports, dSYM
directories, the local macOS release zip, and agent/tooling state
directories so generated TestFlight artifacts no longer appear in
git status.
End-to-end verified by headless archive:
xcodebuild -workspace Runner.xcworkspace -scheme Runner \
-configuration Release -destination 'generic/platform=iOS' \
-archivePath /tmp/chanora.xcarchive archive CODE_SIGNING_ALLOWED=NO
nm -gU on the resulting .app/Chanora binary shows all six
chanora_silero_vad_* symbols present.
* feat(voice): add real-time mic input level metering at 30 Hz
Expose input RMS from the audio engine through the bridge as a
dedicated Rust→Dart Stream<double>, replacing the binary on/off
indicator with a proportional dBFS level meter.
Rust side:
- chanora_audio: add set_input_dbfs/input_dbfs accessors to
SharedAudioProcessingStats; restructure CaptureState::ingest()
to compute dBFS from mono buffer before the PTT guard so the
meter shows mic activity even when not transmitting.
- chanora_core: widen audio_stats() return to include f32 input
level.
- chanora_bridge: add input_level: f32 to BridgeAudioStats and
new input_level_stream(sink: StreamSink<f32>) that pushes at
~30 Hz via tokio interval task.
- Update frb_generated.rs serialization for the new field.
Flutter side:
- VoiceLevelMeter: accept optional double level (dBFS), map
-60..0 dBFS to 0..1 fill fraction, animate with
TweenAnimationBuilder for smooth transitions.
- voice_compact.dart: subscribe to inputLevelStream in the voice
details sheet for 30 Hz meter updates, keeping 250 ms poll for
TX/RX counters.
- voice_bar.dart: accept optional inputLevel from the stream.
- main.dart: subscribe to inputLevelStream, pass to VoiceBar.
* chore: sync Flutter build config and dependency updates
- Add Flutter migrator flags to gradle.properties (builtInKotlin, newDsl)
- Add FlutterGeneratedPluginSwiftPackage to iOS/macOS Xcode projects
- Update meta 1.17→1.18, test_api 0.7.10→0.7.11
- Rebuild chanora_bridge framework for macOS
- Update Podfile.lock for iOS and macOS
* fix(voice): correct meter animation, pre-gain dBFS, stream lifecycle, and protocol warnings
B1: Convert VoiceLevelMeter to StatefulWidget tracking previous fill
as Tween begin so the meter animates smoothly instead of resetting
to zero on every frame.
B2: Compute dBFS from pre-gain mono samples in CaptureState::ingest()
so the level meter reflects raw mic input, matching mobile paths.
B4: End input_level_stream after 10 consecutive session errors instead
of emitting -120 dBFS forever when the session is gone.
Also fixes all 13 clippy warnings in chanora_protocol: collapsed
nested if-let patterns, replaced .ok() + Some matching with Ok, used
? operator, and introduced EventChannels struct to reduce the four
helper functions below the 7-argument threshold.
* fix(voice): use MissedTickBehavior::Skip for level meter stream and align dBFS doc
Set MissedTickBehavior::Skip on the input_level_stream tokio interval
so slow audio_stats() calls skip missed ticks instead of bursting,
preventing CPU spikes on the UI meter thread.
Align VoiceLevelMeter class doc: the mapping floors at -60 dBFS
(via dbfsToFraction), not the full -120 range.
* build: add silero-coreml as git submodule
Replaces sibling-directory local package with in-repo submodule.
Updates Xcode relative paths and CI checkout to fetch submodules.
* build: add silero-coreml submodule
Apple has enforced a `PrivacyInfo.xcprivacy` privacy manifest at App
Store submission since May 2024 for iOS / iPadOS / visionOS /
watchOS, and rolled the requirement out to macOS in late 2024.
Without the file, App Store Connect rejects archive uploads with
"missing required privacy manifest". This commit adds the manifest
for both iOS and macOS Runner targets.
apps/chanora_flutter/ios/Runner/PrivacyInfo.xcprivacy
apps/chanora_flutter/macos/Runner/PrivacyInfo.xcprivacy
Identical content. Declarations:
NSPrivacyCollectedDataTypes:
NSPrivacyCollectedDataTypeAudioData
Microphone audio transmitted to the user's chosen voice
server while connected and unmuted. Not linked to user
identity (no Apple ID / IDFA tied), not used for tracking.
Purpose: AppFunctionality (communications).
NSPrivacyTracking: false
NSPrivacyTrackingDomains: []
Chanora performs no cross-app / cross-website tracking.
NSPrivacyAccessedAPITypes:
FileTimestamp (C617.1)
tokio + rusqlite file I/O for identity.tskey, chanora.db,
audio_meta.json, chanora.log inside the app container.
UserDefaults (CA92.1)
Indirect via path_provider Flutter plugin querying for
Application Support / Documents directories.
SystemBootTime (35F9.1)
tracing-subscriber timestamps log records relative to boot.
DiskSpace (85F4.1)
rusqlite checks before sqlite page writes.
All four "required reason" API categories use Apple's published
allow-list reason codes; no fingerprinting / analytics usage.
apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj
Added PrivacyInfo.xcprivacy to the Runner group and to the
Runner target's "Copy Bundle Resources" build phase via the
xcodeproj Ruby gem (via a one-shot script). With this, the file
is placed at Runner.app/PrivacyInfo.xcprivacy where Apple's
validator looks for it — `find Runner.app -name
PrivacyInfo.xcprivacy` shows our manifest at the bundle root
alongside Flutter's and connectivity_plus's.
Verified on the M1 Mac (coder@100.118.130.73):
flutter build ios --release --no-codesign 4.0 s
-> Runner.app/PrivacyInfo.xcprivacy present
flutter build ipa --release --no-codesign 28.4 s
-> Runner.xcarchive built (171.4 MB)
-> archive's Runner.app/PrivacyInfo.xcprivacy present
-> archive's Runner.app/Frameworks/chanora_bridge.framework
built fresh via the chanora_bridge.podspec prepare_command
under xcodebuild's sandbox (no PATH / env weirdness).
P1 follow-ups noted by xcodebuild's validator (not blockers for
this commit but for App Store submission):
* Real app icon (currently default placeholder)
* Real launch image (currently default placeholder)
* Paid Apple Developer Program account, registered App ID, and
Distribution provisioning profile (Personal Team sideloads
still work as today).
Building the bridge for `aarch64-apple-ios` failed in two ways with
the previous TLS stack:
1. `aws-lc-sys` (transitive: rustls -> aws-lc-rs -> aws-lc-sys)
does not cross-compile cleanly to iOS — the build produced
undefined symbols for architecture arm64 (mldsa44, ec_GFp_mont,
etc).
2. `audiopus_sys` linked against the wrong iOS runtime version,
missing `___chkstk_darwin`.
Following the rustls-platform-verifier docs and the standard Rust+
iOS+TLS pattern used by 1Password / Signal / rustup / Bitwarden,
this commit swaps the TLS provider to **native-tls** so each
platform picks its own:
* macOS + iOS -> Security.framework (no external C deps)
* Windows -> SChannel
* Linux/BSD -> system OpenSSL
Changes:
crates/chanora_protocol/Cargo.toml
crates/chanora_audio/Cargo.toml
* Drop `default-tls` from tsclientlib's features. The remaining
`audio` feature is what we actually use; default-tls was a
reqwest convenience that picked rustls+aws-lc-rs.
* Add a direct `reqwest` dep with `default-features = false,
features = ["charset", "http2", "native-tls"]`. Cargo's
workspace feature unification carries this through the
transitive `tsclientlib -> reqwest` chain.
apps/chanora_flutter/ios/Podfile
* Uncomment `platform :ios, '13.0'` so CocoaPods stops emitting
the implicit-platform warning and Xcode's iOS deployment-
target check is honored.
apps/chanora_flutter/ios/Podfile.lock
* Generated by `pod install` after the platform pin. Committed so
iOS builds on other developer machines pull the exact same Pod
versions.
apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
* CocoaPods auto-integration: adds Pods_Runner.framework +
Pods_RunnerTests.framework references and the Pods xcconfig
file references. Standard `pod install` output; reviewing the
diff shows only Pod-bookkeeping additions, no signing or
target-config drift.
Verified end-to-end on the M1 Mac (coder@100.118.130.73):
cargo build --release -p chanora_bridge 29.09 s
cargo build --release --target aarch64-apple-ios 24.32 s
(with IPHONEOS_DEPLOYMENT_TARGET=13.0 and
CMAKE_POLICY_VERSION_MINIMUM=3.5 in the env to satisfy the
audiopus_sys cmake invocation; documented as a P1 build-glue
follow-up.)
flutter build ios --release --no-codesign ok
Built build/ios/iphoneos/Runner.app (16.9 MB)
Xcode GUI build of Runner.xcworkspace ok
(after the user opened Runner.xcworkspace, NOT
Runner.xcodeproj, and Clean Build Folder.)
Tests on macOS unchanged: chanora_audio 34 / 0 / 0.
DEC-025: iOS + macOS officially in scope for P0.
Ran `flutter create --platforms=macos,ios --project-name=chanora_flutter
--org=app.chanora .` on the M1 Mac to generate the standard Flutter
platform-specific scaffolding (Runner.xcodeproj, Podfile, AppDelegate,
entitlements, etc.) for both macOS and iOS.
The cross-platform Dart source (lib/) and Rust workspace (crates/,
core/) carry the actual application logic; these scaffolds are
required only so flutter build macos / ios can resolve their Xcode
projects. No application code added.
macOS smoke-launch from the M1 Mac verified the bridge dylib load
path: after manually wrapping libchanora_bridge.dylib into a proper
chanora_bridge.framework bundle (FRB on macOS expects a framework,
not a plain dylib) and codesigning ad-hoc, the runner starts cleanly
through 'bridge initialised', 'identity store initialised', 'bookmark
store initialised' just like the Linux runner.
Following commits will:
* Wire the framework-bundling step into build glue (currently manual
install_name_tool + codesign).
* Replace the macOS PTT backend stub (crates/chanora_audio/src/
ptt_backends/macos.rs) with live IOHIDCheckAccess +
CGEventTapCreate so the descriptor advertises real L2/L3 capability
on a permission-granted box (SDD-085).
* Add the iOS AVAudioSession PlayAndRecord+voiceChat wiring.
* Add docs/verification/macos-p0-acceptance.md and ios-p0-acceptance.md.