Reaches the Internal Beta milestone of DEC-001's release sequence the
same day as Alpha. Adds voice capture and playback through the full
Flutter UI → FRB → Rust core → tsclientlib → server path.
Promotions from PoC:
poc/audio-capture-playback-spike → crates/chanora_audio/
New product code:
crates/chanora_audio/src/engine.rs — cpal capture and playback,
audiopus Opus VoIP encoder (48 kHz mono 20 ms frames), tsclientlib
AudioHandler for decode + jitter buffer + mix on playback,
push-to-talk gate, graceful playback-only fallback when capture
is unavailable.
crates/chanora_protocol/src/adapter.rs — extended with
voice_out_tx (clonable mpsc::Sender<OutPacket>) and
take_voice_in() (one-shot mpsc::Receiver<InboundVoice>); main
loop now interleaves outbound voice drain, event pumping, and
control-request handling.
crates/chanora_protocol/src/lib.rs — re-exports the few
tsproto_packets types (OutAudio, OutPacket, InAudioBuf,
AudioData, CodecType, Direction) that chanora_audio
legitimately needs. Documented as the single deliberate
cross-crate type re-export per SAD-067, justified by the
performance cost of a parallel type hierarchy on the 20 ms
voice frame.
core/chanora_core/src/lib.rs — ChanoraSession::start_audio,
set_ptt, audio_stats; disconnect now stops the engine first.
crates/chanora_bridge/src/api.rs — startAudio, setPtt,
audioStats commands and BridgeAudioStats DTO.
apps/chanora_flutter/lib/main.dart — "Start audio" button +
hold-to-talk PTT button with pressed/released visual state +
live stats line (TX/RX/PTT). Stats polled every 500 ms.
ARB:
Both en and zh-Hans gain startAudioAction, pttHoldToTalk,
pttTransmitting, audioStatsLine. Banner updated to
"Beta build — voice in/out wired; not production ready."
FRB config:
flutter_rust_bridge.yaml gains local: true so codegen resolves
the workspace member's library stem to "chanora_bridge" instead
of falling back to "UNKNOWN".
Empirical verification (2026-05-14, against cn.teamspeak.app):
cargo check + cargo test --workspace: all green.
flutter analyze: 0 issues.
flutter test: 4/4 passing including:
- test/alpha_e2e_test.dart (regression: Alpha still works)
- test/beta_e2e_test.dart (Beta: connect → startAudio →
PTT cycle → disconnect against cn.teamspeak.app).
Live smoke (cargo test alpha_smoke -- --ignored): 49 channels,
37 clients retrieved.
Capture stream open against the host PipeWire auto_null source
refused (snd_pcm_hw_params); engine correctly logged the warning
and continued in playback-only mode. TX=0 frames, RX=0 frames
reflects the headless null-source environment; on a real mic
host the encoder produces ~50 frames/second while PTT is held.
Honest Beta scope (NOT in this release):
- AEC / AGC / NS / HPF DSP (DEC-007..010): AudioEffects exists
as a struct but the filters are no-ops. Beta+ work.
- Production-quality resampler: current code is linear
interpolation. Beta+ work.
- Identity persistence via chanora_storage: still ephemeral.
- Push-to-Dart event stream: UI polls instead.
- chanora_diagnostics tracing-layer wiring: still scaffold.
- Mobile (Android) cdylib + UI: PoC-proven, not yet in product.
- Reconnect / network-loss recovery for the voice path.
Docs updates:
- docs/governance/product-decision-register.md bumped to v0.9.7
(Beta-milestone change-history entry; no row changes).
- docs/governance/poc-results-summary.md bumped to v0.6.0
(RISK-PoC-005 updated with Beta progress).
16 KiB
Changelog
All notable changes to Chanora will be documented in this file.
This project is expected to follow a Conventional Commits style workflow.
[Unreleased]
Added — Beta build (v0.2.0-beta.1)
- Voice in/out wired end-to-end through the Flutter UI. Per DEC-001
this reaches the Internal Beta milestone. Build hash: see the
v0.2.0-beta.1git tag. crates/chanora_audio/promoted from scaffold to a working engine:- cpal-based capture (mic gain, linear resampling to 48 kHz, mono down-mix) and playback (48 kHz stereo, requested config).
audiopus::Encoderfor Opus VoIP encoding (20 ms / 960-sample mono frames).tsclientlib::audio::AudioHandlerfor the decode + per-client jitter buffer + mix on the playback side.- Push-to-talk gate: encoder is bypassed entirely when PTT is off, so no spurious silence frames leak out.
- Graceful playback-only fallback: if the host has no usable mic
(typical for headless CI / users who deny the mic permission),
capture logs a warning and the engine continues with output only.
AudioEngine::capture_active()exposes this for the UI. - Live counters:
frames_sent/frames_received/ptt().
crates/chanora_protocol/extended with voice channels:ProtocolClient::voice_out()returns a clonablempsc::Sender<OutPacket>for outbound frames.ProtocolClient::take_voice_in()returns a one-shotmpsc::Receiver<InboundVoice>of decodedS2C/S2CWhisperpackets, with the originatingfrom_clientID extracted.- Re-exports the few
tsproto_packets::packetstypes (OutAudio,OutPacket,InAudioBuf,AudioData,CodecType,Direction) thatchanora_audiolegitimately needs. This is the only deliberate cross-crate type re-export; per SAD-067 the audio path is performance-sensitive and a parallel type hierarchy would force a copy per 20 ms frame. - Connection task interleaves outbound voice (drained first per loop iteration), event pumping, and control-request handling.
core/chanora_core::ChanoraSessionaudio API:start_audio(AudioEngineConfig)— starts the engine attached to the active connection. Idempotent.set_ptt(bool)— toggles transmission. No-op without an engine.audio_stats()→(frames_sent, frames_received, ptt_active).disconnect()now stops the engine before disconnecting the protocol task.
crates/chanora_bridge/audio surface:start_audio(),set_ptt(active),audio_stats()Dart-callable commands.BridgeAudioStats { frames_sent, frames_received, ptt_active }DTO.- Mapped
CoreError::AudioNotStartedandCoreError::Audio(_)arms inBridgeError::From<CoreError>.
apps/chanora_flutter/:- Beta UI rewrite of
main.dart: "Start audio" button after connect; hold-to-talk button with pressed/released visual state; live audio-stats line below the PTT (TX … frames • RX … frames • PTT on/off). - ARB key set expanded with
startAudioAction,pttHoldToTalk,pttTransmitting,audioStatsLinein bothenandzh-Hans. test/beta_e2e_test.dartexercises the full Dart → FRB → chanora_bridge → chanora_core → chanora_audio path againstcn.teamspeak.app. Verifies connect, audio start, PTT toggle, disconnect.
- Beta UI rewrite of
flutter_rust_bridge.yamlnow setslocal: trueso the codegen resolves the workspace member's library name correctly. Without this, the generated Dart side fell back tolibUNKNOWN.soand failed to load the cdylib.
Changed
flutter_rust_bridge.yaml: addedlocal: true.chanora_bridge::api:BridgeError::From<CoreError>now mapsCoreError::AudioNotStartedtoBridgeError::InvalidCommandandCoreError::Audio(_)toBridgeError::Connection.apps/chanora_flutter/test/widget_test.dart: banner-string expectations updated from "Alpha build" to "Beta build" and from "Alpha 版本" to "Beta 版本".docs/governance/product-decision-register.mdbumped to v0.9.7 with a Beta-milestone change-history entry. No decision rows change.docs/governance/poc-results-summary.mdbumped to v0.6.0 with a Beta-milestone change-history entry; RISK-PoC-005 updated to reflect Beta progress.
Notes (Beta scope honesty)
- DSP chain (AEC / AGC / NS / HPF per DEC-007..010) is not yet
implemented.
AudioEffectsexists as a struct but its filters are no-ops in v0.2.0-beta.1. Real DSP is queued for Beta+ work. - The capture resampler is a simple linear interpolator. Production quality requires a proper resampler in Beta+.
- Identity is still ephemeral per connect; persistence via
chanora_storageis queued. - No live event stream into Dart yet — the UI fetches snapshots and audio stats on a timer instead of subscribing to push events.
chanora_diagnosticsis still a scaffold; no redaction wired intotracingyet.- Audio engine is desktop-only in this Beta. Mobile bundle of the bridge cdylib + UI verification was proven by the PoC but is not re-built into product code in this milestone.
Carry-over from Alpha (v0.1.0-alpha.1)
- First Alpha build wires the connect → snapshot → disconnect cycle end-to-end from the Flutter UI to a live TeamSpeak-compatible server via the typed Flutter/Rust bridge. Per DEC-001 this is the Internal Alpha milestone; Audio (voice in/out) is deferred to Beta.
crates/chanora_protocol/promoted from a scaffold to a working adapter. Public surface:ConnectConfig,ProtocolClient,ProtocolError.- DTO module exposing
ChannelId,ClientId,ChannelInfo,ClientInfo,ServerSnapshot— all owned primitives andStrings; notsclientlib::*types leak (SAD-067). - Tokio task owns the
tsclientlib::Connection; public handle communicates viampscrequests +oneshotreplies. - Connect waits for the initial
BookEventssnapshot, then pumps events for ~2 s so the subscribed channel tree settles before the first snapshot is served. - Promoted from
poc/tsclientlib-connect-spike.
core/chanora_core::ChanoraSessionnow drives the protocol crate with a typedconnect/snapshot/is_connected/disconnectAPI. Enforces the DEC-006 single-connection invariant via an internaltokio::sync::Mutex<Option<ProtocolClient>>.crates/chanora_bridge/wired againstflutter_rust_bridge2.12.0 (DEC-014). Compiled ascdylib + staticlib + rlib. Exposes:bridge_init()(FRB lifecycle),connect(),snapshot(),disconnect(),is_connected().- Typed
BridgeChannel,BridgeClient,BridgeSnapshotDTOs;BridgeErrorwithFrom<chanora_core::CoreError>. - A process-wide
tokio::Runtime+ChanoraSessionviaOnceLock, used by every async command. - The crate's
#![forbid(unsafe_code)]lint was lifted to#![warn(missing_docs)]only, with a doc-comment explanation that the FRB-generated glue legitimately uses unsafe at the FFI boundary; hand-written code in the crate is still expected to avoidunsafe.
flutter_rust_bridge.yamlat the repo root drives codegen for the bridge.- Generated Dart bindings under
apps/chanora_flutter/lib/src/rust/{api.dart,frb_generated*.dart,lib*.dart}. - Generated Rust glue under
crates/chanora_bridge/src/frb_generated.rs. apps/chanora_flutter/lib/main.dartrewritten as the Alpha UI:- Form: server address + nickname, both pre-populated for convenience.
- Connect button → calls FRB → enters connecting state → shows snapshot.
- Snapshot view: server welcome banner (preserved verbatim per
ADR-008),
N channels • M onlinecount, ordered channel list with clients indented under their channel. - Refresh and Disconnect actions in the app bar.
apps/chanora_flutter/lib/l10n/app_{en,zh}.arbexpanded with the Alpha key set:homeNotProductionReadyBanner(now says "Alpha build"),fieldServerHost,fieldNickname,connectAction,disconnectAction,refreshAction,statusIdle,statusConnecting,statusConnected,statusError,channelsHeading,clientsHeading,countChannelsAndClients.flutter_localizations,intl,flutter_rust_bridge,freezed_annotationadded to dependencies;freezedandbuild_runneradded to dev_dependencies.apps/chanora_flutter/test/alpha_e2e_test.dartruns the full Dart → FRB → Rust → tsclientlib → network → server path againstcn.teamspeak.app. Verifies the snapshot contains a non-empty server name and a non-empty channel list, thatisConnected()flips true → false across the disconnect, and that a re-fetched snapshot agrees on the server name. Passes in ~2.5 s.core/chanora_core/tests/alpha_smoke.rsruns the same path from the Rust side; tagged#[ignore]socargo test --workspacedoesn't hit the network by default. Run with--ignored alpha_smoke.
Changed
chanora_core::CoreErrorno longer wrapschanora_bridge::BridgeError; the relationship is the other way around (bridge maps from core). This removes a cyclicchanora_core↔chanora_bridgedependency introduced when the bridge crate gainedchanora_coreas a dep.chanora_bridgelints relaxed from#![forbid(unsafe_code)]to#.
LICENSE files (carry-over from earlier in this branch)
LICENSE-APACHE— Apache License Version 2.0 text (DEC-020).LICENSE-MIT— MIT License text (DEC-020).- Initial repository foundation files.
- Documentation-first project structure.
justfilewithformat,lint,test,verify-docs, andsecurity-scantargets, completingrepository-bootstrap-planv0.1.0 §3.poc/tsclientlib-connect-spike/— PoC proving protocol feasibility viatsclientlib. Verified againstcn.teamspeak.appon 2026-05-13.poc/flutter_rust_bridge_hello/— PoC proving the Flutter↔Rust command and event-stream boundary viaflutter_rust_bridge2.12.0. Verified on Linux desktop on 2026-05-13.poc/secure-storage-spike/— PoC proving platform secure storage via a typedSecretStorageRepositorytrait and a Linux adapter selecting between Secret Service (libsecret) and kernel keyutils. Audit checks SS-AUD-001/002/003/005/006 and SS-TC-003 verified on 2026-05-13.poc/sqlite-storage-spike/— PoC proving SRS-089's "embedded data store + migration mechanism" acceptance criteria: forward-only schema migrator tracked viaPRAGMA user_version, repository pattern withBookmarkRepository/SettingsRepositorytraits overLocalDatabaseRepository. 11/11 tests verified on 2026-05-13.poc/diagnostics-redaction-spike/— PoC proving the diagnostic redaction policy fromdiagnostic-redaction-audit-report.md: typed policy + regex rules + literal known-secret registry + bundle redaction. Audit cases REDACT-TC-001..010 verified on 2026-05-13.poc/audio-capture-playback-spike/— PoC proving platform audio capture/playback via cpal. Desktop half (Linux + PipeWire) empirically verified end-to-end on 2026-05-13; mobile half closed separately bypoc/audio-capture-playback-android-spike.poc/audio-capture-playback-android-spike/— PoC closing the mobile half of the audio capture/playback PoC plan entry. Rust cdylib + JNI + Kotlin Android app; cpal targets Android's Oboe backend (AAudio). Verified end-to-end on a physical Motorola Moto G Stylus 5G (2023) running Android 14 arm64-v8a on 2026-05-13: 500 ms 440 Hz sine wave driven out the device speaker (22,050 frames at 44.1 kHz) and 1 s captured from the microphone into a valid 85,292-byte RIFF/WAVE mono 16-bit PCM file pulled viaadb exec-out run-as.poc/README.mdsummarising PoC status againstdocs/architecture/proof-of-concept-plan.md.
Changed
-
DEC-020 license resolved. Chanora is now dual-licensed under Apache-2.0 OR MIT (recipient's choice), the standard Rust-ecosystem permissive model. Compatible with every direct dependency (
tsclientlib,flutter_rust_bridge,cpal,rusqlite,keyring, etc.) and with the Flutter framework's BSD-3-Clause.LICENSErewritten as a dual-license aggregator pointing atLICENSE-APACHEandLICENSE-MIT.NOTICErewritten with current direct-dependency attributions.README.md§License updated. -
docs/governance/product-decision-register.mdbumped to v0.9.6: DEC-020 status promoted from Open to Accepted. §4 license row updated. §6 collapsed: there is no longer any open decision — DEC-012 legal review remains as a work item, not a pending decision. Change-history entry added. -
docs/governance/poc-results-summary.mdbumped to v0.4.0: RISK-PoC-003 closed. DEC-020 row moved from the "Still open" section into the closed table. -
docs/governance/product-decision-register.mdbumped to v0.9.5: owner confirmation on all 17 previously-Proposed decisions (DEC-001..010, 012, 015..019, 021). Sixteen were Accepted as recommended; two were modified by the owner — DEC-004 Android minimum raised from API 24 to API 28, and DEC-015 product language for MVP expanded from English-only to English + Chinese (Simplified). DEC-020 license remains Open / Deferred and is now the only public-release-gating decision outstanding. §4 renamed "Recommended" → "Accepted MVP Defaults" with MODIFIED rows annotated. §6 collapsed to the single remaining DEC-020 item. §7 dated and statused for every decision. -
docs/governance/poc-results-summary.mdbumped to v0.3.0: RISK-PoC-004 closed by the owner-confirmation pass; new RISK-PoC-006 (AndroidminSdkmove 24 → 28 for product code) and RISK-PoC-007 (MVP language expansion to en + zh-Hans) added. -
docs/architecture/proof-of-concept-plan.mdbumped to v0.3.0 to promote the audio PoC from PARTIAL PASS to PASS after the Android mobile half was closed; all six PoC plan entries are now PASS. -
docs/governance/product-decision-register.mdbumped to v0.9.4 to promote DEC-011.1 mobile half from Deferred to Accepted (Android), keeping iOS Deferred. -
docs/governance/poc-results-summary.mdbumped to v0.2.0: audio row promoted to PASS, RISK-PoC-001 narrowed from "mobile audio" to "iOS audio only", Android toolchain added to the toolchain table. -
poc/audio-capture-playback-spike/VERIFICATION.mdupdated to point at the Android spike for the mobile half. -
poc/README.mdupdated to list both audio spike directories. -
docs/architecture/proof-of-concept-plan.mdbumped to v0.2.0 to record PoC outcomes (5 PASS, 1 PARTIAL) and add a Status column. -
docs/security/secure-storage-audit-report.mdbumped to v0.9.3: SS-AUD-001/002/003/005/006 status set to PoC Pass with evidence pointers; SS-TC-003 (Linux) Actual Result populated and Status set to PoC Pass; findings SS-FIND-001..003 added; non-Linux test cases marked Deferred. -
docs/security/diagnostic-redaction-audit-report.mdbumped to v0.9.3: REDACT-TC-001..010 status set to PoC Pass with evidence pointers; export bundle policy §5 populated; findings REDACT-FIND-001..003 added. -
docs/governance/product-decision-register.mdbumped to v0.9.3: owner-confirmed decisions recorded — DEC-014 Accepted (flutter_rust_bridge2.x pinned), DEC-013.1 Accepted (rusqlitebundled), DEC-013.2 Accepted (Linux Secret Service preferred, keyutils fallback), DEC-011.1 Accepted (desktopcpal) / Deferred (mobile), DEC-022 Accepted (canonical implementation directory layout per README sketch + SAD §7.2), DEC-020 explicitly Deferred and remains a public-release blocker.
Added (governance)
docs/governance/poc-results-summary.mdv0.1.0 — single-page reviewer-facing summary of the PoC phase, the toolchain exercised, the owner decisions taken, the audit coverage, and the open risks RISK-PoC-001..005.
Fixed
- N/A
Security
- N/A
Versioning note
The project implementation has not reached a public release version yet.