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).
311 lines
16 KiB
Markdown
311 lines
16 KiB
Markdown
# 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.1` git 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::Encoder` for Opus VoIP encoding (20 ms / 960-sample
|
|
mono frames).
|
|
- `tsclientlib::audio::AudioHandler` for 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 clonable
|
|
`mpsc::Sender<OutPacket>` for outbound frames.
|
|
- `ProtocolClient::take_voice_in()` returns a one-shot
|
|
`mpsc::Receiver<InboundVoice>` of decoded `S2C` / `S2CWhisper`
|
|
packets, with the originating `from_client` ID extracted.
|
|
- Re-exports the few `tsproto_packets::packets` types (`OutAudio`,
|
|
`OutPacket`, `InAudioBuf`, `AudioData`, `CodecType`, `Direction`)
|
|
that `chanora_audio` legitimately 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::ChanoraSession` audio 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::AudioNotStarted` and `CoreError::Audio(_)`
|
|
arms in `BridgeError::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`, `audioStatsLine` in both `en` and `zh-Hans`.
|
|
- `test/beta_e2e_test.dart` exercises the full
|
|
Dart → FRB → chanora_bridge → chanora_core → chanora_audio
|
|
path against `cn.teamspeak.app`. Verifies connect, audio start,
|
|
PTT toggle, disconnect.
|
|
- `flutter_rust_bridge.yaml` now sets `local: true` so the codegen
|
|
resolves the workspace member's library name correctly. Without
|
|
this, the generated Dart side fell back to `libUNKNOWN.so` and
|
|
failed to load the cdylib.
|
|
|
|
### Changed
|
|
|
|
- `flutter_rust_bridge.yaml`: added `local: true`.
|
|
- `chanora_bridge::api`: `BridgeError::From<CoreError>` now maps
|
|
`CoreError::AudioNotStarted` to `BridgeError::InvalidCommand` and
|
|
`CoreError::Audio(_)` to `BridgeError::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.md` bumped to v0.9.7
|
|
with a Beta-milestone change-history entry. No decision rows
|
|
change.
|
|
- `docs/governance/poc-results-summary.md` bumped 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**. `AudioEffects` exists 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_storage` is 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_diagnostics` is still a scaffold; no redaction wired into
|
|
`tracing` yet.
|
|
- 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 and
|
|
`String`s; no `tsclientlib::*` types leak (SAD-067).
|
|
- Tokio task owns the `tsclientlib::Connection`; public handle
|
|
communicates via `mpsc` requests + `oneshot` replies.
|
|
- Connect waits for the initial `BookEvents` snapshot, 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::ChanoraSession` now drives the protocol crate
|
|
with a typed `connect`/`snapshot`/`is_connected`/`disconnect` API.
|
|
Enforces the DEC-006 single-connection invariant via an internal
|
|
`tokio::sync::Mutex<Option<ProtocolClient>>`.
|
|
- `crates/chanora_bridge/` wired against `flutter_rust_bridge` 2.12.0
|
|
(DEC-014). Compiled as `cdylib + staticlib + rlib`. Exposes:
|
|
- `bridge_init()` (FRB lifecycle), `connect()`, `snapshot()`,
|
|
`disconnect()`, `is_connected()`.
|
|
- Typed `BridgeChannel`, `BridgeClient`, `BridgeSnapshot` DTOs;
|
|
`BridgeError` with `From<chanora_core::CoreError>`.
|
|
- A process-wide `tokio::Runtime` + `ChanoraSession` via
|
|
`OnceLock`, 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
|
|
avoid `unsafe`.
|
|
- `flutter_rust_bridge.yaml` at 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.dart` rewritten 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 online` count, 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}.arb` expanded 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_annotation` added to dependencies;
|
|
`freezed` and `build_runner` added to dev_dependencies.
|
|
- `apps/chanora_flutter/test/alpha_e2e_test.dart` runs the full
|
|
Dart → FRB → Rust → tsclientlib → network → server path against
|
|
`cn.teamspeak.app`. Verifies the snapshot contains a non-empty
|
|
server name and a non-empty channel list, that `isConnected()`
|
|
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.rs` runs the same path from the
|
|
Rust side; tagged `#[ignore]` so `cargo test --workspace` doesn't
|
|
hit the network by default. Run with `--ignored alpha_smoke`.
|
|
|
|
### Changed
|
|
|
|
- `chanora_core::CoreError` no longer wraps `chanora_bridge::BridgeError`;
|
|
the relationship is the other way around (bridge maps from core).
|
|
This removes a cyclic `chanora_core` ↔ `chanora_bridge` dependency
|
|
introduced when the bridge crate gained `chanora_core` as a dep.
|
|
- `chanora_bridge` lints relaxed from `#![forbid(unsafe_code)]` to
|
|
`#![warn(missing_docs)]` (documented above).
|
|
|
|
### 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.
|
|
- `justfile` with `format`, `lint`, `test`, `verify-docs`, and
|
|
`security-scan` targets, completing `repository-bootstrap-plan` v0.1.0 §3.
|
|
- `poc/tsclientlib-connect-spike/` — PoC proving protocol feasibility via
|
|
`tsclientlib`. Verified against `cn.teamspeak.app` on 2026-05-13.
|
|
- `poc/flutter_rust_bridge_hello/` — PoC proving the Flutter↔Rust command
|
|
and event-stream boundary via `flutter_rust_bridge` 2.12.0. Verified on
|
|
Linux desktop on 2026-05-13.
|
|
- `poc/secure-storage-spike/` — PoC proving platform secure storage via
|
|
a typed `SecretStorageRepository` trait 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 via `PRAGMA user_version`, repository pattern with
|
|
`BookmarkRepository` / `SettingsRepository` traits over
|
|
`LocalDatabaseRepository`. 11/11 tests verified on 2026-05-13.
|
|
- `poc/diagnostics-redaction-spike/` — PoC proving the diagnostic
|
|
redaction policy from `diagnostic-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 by `poc/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 via
|
|
`adb exec-out run-as`.
|
|
- `poc/README.md` summarising PoC status against
|
|
`docs/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.
|
|
`LICENSE` rewritten as a dual-license aggregator pointing at
|
|
`LICENSE-APACHE` and `LICENSE-MIT`. `NOTICE` rewritten with current
|
|
direct-dependency attributions. `README.md` §License updated.
|
|
- `docs/governance/product-decision-register.md` bumped 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.md` bumped 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.md` bumped 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.md` bumped to v0.3.0:
|
|
RISK-PoC-004 closed by the owner-confirmation pass; new
|
|
RISK-PoC-006 (Android `minSdk` move 24 → 28 for product code)
|
|
and RISK-PoC-007 (MVP language expansion to en + zh-Hans) added.
|
|
- `docs/architecture/proof-of-concept-plan.md` bumped 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.md` bumped to v0.9.4 to
|
|
promote DEC-011.1 mobile half from Deferred to Accepted (Android),
|
|
keeping iOS Deferred.
|
|
- `docs/governance/poc-results-summary.md` bumped 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.md` updated to point
|
|
at the Android spike for the mobile half.
|
|
- `poc/README.md` updated to list both audio spike directories.
|
|
- `docs/architecture/proof-of-concept-plan.md` bumped to v0.2.0 to
|
|
record PoC outcomes (5 PASS, 1 PARTIAL) and add a Status column.
|
|
- `docs/security/secure-storage-audit-report.md` bumped 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.md` bumped 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.md` bumped to v0.9.3:
|
|
owner-confirmed decisions recorded — DEC-014 Accepted
|
|
(`flutter_rust_bridge` 2.x pinned), DEC-013.1 Accepted (`rusqlite`
|
|
bundled), DEC-013.2 Accepted (Linux Secret Service preferred,
|
|
keyutils fallback), DEC-011.1 Accepted (desktop `cpal`) / 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.md` v0.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.
|