Compare commits
10
Commits
c04aaf4a51
...
370dd37a22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370dd37a22 | ||
|
|
523eafd4d7 | ||
|
|
6c00fae1cf | ||
|
|
11a2541042 | ||
|
|
fe3da41e41 | ||
|
|
602eedc029 | ||
|
|
020218a7a1 | ||
|
|
1e774035d1 | ||
|
|
acc1450904 | ||
|
|
72ded4e011 |
@@ -40,6 +40,8 @@ members = [
|
||||
|
||||
exclude = [
|
||||
"apps/chanora_flutter",
|
||||
"tools/protocol-probe",
|
||||
"tools/audio-test",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -68,8 +68,8 @@ pub use chanora_diagnostics::{
|
||||
RedactingLogLayer, Redactor, DEFAULT_LOG_CAPACITY,
|
||||
};
|
||||
pub use chanora_protocol::{
|
||||
ChannelInfo, ChatMessage, ClientInfo, ClientProfile, ConnectConfig, DisconnectReason,
|
||||
MessageTarget, PokeStrength, ProtocolError, ServerActivity, ServerSnapshot,
|
||||
validate_nickname, ChannelInfo, ChatMessage, ClientInfo, ClientProfile, ConnectConfig,
|
||||
DisconnectReason, MessageTarget, PokeStrength, ProtocolError, ServerActivity, ServerSnapshot,
|
||||
};
|
||||
pub use chanora_storage::{Bookmark, BookmarkRepository, IdentityFileStore};
|
||||
pub use events::{
|
||||
|
||||
@@ -603,6 +603,8 @@ pub async fn connect(
|
||||
nickname: String,
|
||||
password: String,
|
||||
) -> Result<BridgeSnapshot, BridgeError> {
|
||||
let nickname = chanora_core::validate_nickname(&nickname)
|
||||
.map_err(|e| BridgeError::InvalidCommand(e.to_string()))?;
|
||||
let cfg = chanora_core::ConnectConfig {
|
||||
address: host,
|
||||
nickname,
|
||||
|
||||
@@ -40,8 +40,8 @@ use tsproto_packets::packets::{Direction, Flags, InAudioBuf, OutCommand, OutPack
|
||||
use tsproto_types::ClientType;
|
||||
|
||||
use crate::dto::{
|
||||
ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget,
|
||||
ProtocolDelta, ServerActivity, ServerSnapshot,
|
||||
validate_nickname, ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile,
|
||||
MessageTarget, ProtocolDelta, ServerActivity, ServerSnapshot,
|
||||
};
|
||||
use crate::poke_limiter::PokeLimiter;
|
||||
use crate::ProtocolError;
|
||||
@@ -324,9 +324,10 @@ impl ProtocolClient {
|
||||
if cfg.address.trim().is_empty() {
|
||||
return Err(ProtocolError::Invalid("address is empty".to_string()));
|
||||
}
|
||||
if cfg.nickname.trim().is_empty() {
|
||||
return Err(ProtocolError::Invalid("nickname is empty".to_string()));
|
||||
}
|
||||
let validated_nick = validate_nickname(&cfg.nickname)
|
||||
.map_err(|e| ProtocolError::Invalid(e.to_string()))?;
|
||||
let mut cfg = cfg;
|
||||
cfg.nickname = validated_nick;
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Request>(8);
|
||||
let (voice_out_tx, voice_out_rx) = mpsc::channel::<OutPacket>(64);
|
||||
|
||||
@@ -40,8 +40,8 @@ pub mod poke_limiter;
|
||||
|
||||
pub use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe};
|
||||
pub use dto::{
|
||||
ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget,
|
||||
PokeStrength, ProtocolDelta, ServerActivity, ServerSnapshot,
|
||||
validate_nickname, ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile,
|
||||
MessageTarget, PokeStrength, ProtocolDelta, ServerActivity, ServerSnapshot,
|
||||
};
|
||||
pub use poke_limiter::PokeLimiter;
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Audio Loopback Test Tool Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Design proposal
|
||||
**TODO:** TODO-046
|
||||
**Requirements:** SysRS-073, SRS-083
|
||||
**Effort:** L
|
||||
**Dependencies:** TODO-054 (audio loopback harness)
|
||||
|
||||
## Purpose
|
||||
|
||||
End-to-end audio quality verification. Sends a known test signal through the full encode→decode→playback→capture loop and measures signal quality metrics to verify the entire audio pipeline works correctly on a given platform.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Known test signal (sine sweep, white noise, or chirp)
|
||||
- Loopback device configuration (virtual audio device or hardware loopback)
|
||||
- Test duration and sample rate
|
||||
|
||||
## Outputs
|
||||
|
||||
- Signal quality metrics: SNR (dB), latency (ms), jitter (ms), THD+N (%)
|
||||
- Pass/fail against acceptance thresholds
|
||||
- Captured loopback audio WAV for manual inspection
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
┌──────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Test │────>│ Opus │────>│ Loopback │────>│ Opus │
|
||||
│ Signal │ │ Encode │ │ Device │ │ Decode │
|
||||
│ Generator│ │ │ │ (virtual) │ │ │
|
||||
└──────────┘ └───────────┘ └───────────┘ └─────┬─────┘
|
||||
│
|
||||
┌──────────┐ ┌───────────┐ ┌───────────┐ ┌────v─────┐
|
||||
│ Report │<────│ Metrics │<────│ Compare │<────│ Capture │
|
||||
│ (SNR, │ │ Extract │ │ (original │ │ (loopback│
|
||||
│ latency)│ │ │ │ vs recv) │ │ audio) │
|
||||
└──────────┘ └───────────┘ └───────────┘ └──────────┘
|
||||
```
|
||||
|
||||
1. **Generate:** Create known test signal (e.g., 1kHz sine, sweep)
|
||||
2. **Encode:** Pass through Opus encoder (matching voice pipeline config)
|
||||
3. **Loopback:** Send encoded audio through virtual audio device
|
||||
4. **Decode:** Capture loopback audio and decode through Opus decoder
|
||||
5. **Compare:** Cross-correlate original and received signals
|
||||
6. **Measure:** Extract SNR, latency (peak correlation offset), jitter, THD+N
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
- New Rust binary crate: `tools/audio-loopback-test/`
|
||||
- Reuse `chanora_audio` Opus encoder/decoder wrappers
|
||||
- Virtual audio device: BlackHole (macOS), VB-Audio (Windows), snd-aloop (Linux)
|
||||
- Cross-correlation for latency measurement
|
||||
- CLI interface: `audio-loopback-test [--duration 5] [--signal sine|sweep|noise] [--device <name>]`
|
||||
- Requires TODO-054 (loopback harness) for CI virtual device setup
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `chanora_audio` (Opus encode/decode, audio config)
|
||||
- `opus` crate (encoder/decoder)
|
||||
- `hound` (WAV I/O)
|
||||
- Virtual audio device (platform-specific, from TODO-054)
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit test: encode→decode roundtrip without loopback, verify signal preserved
|
||||
- Integration test: full loopback on macOS with BlackHole, verify SNR > threshold
|
||||
- Platform test: run on each target OS with configured virtual device
|
||||
- Demo: run tool on developer machine, show metrics report
|
||||
@@ -0,0 +1,65 @@
|
||||
# Audio Processing Test Tool Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Design proposal
|
||||
**TODO:** TODO-044
|
||||
**Requirements:** SysRS-074, SRS-083
|
||||
**Effort:** L
|
||||
|
||||
## Purpose
|
||||
|
||||
Test the audio DSP pipeline (AEC, NS, AGC, HPF) in isolation. Measures processing latency, signal quality, and verifies each filter stage produces expected output for known input signals.
|
||||
|
||||
## Inputs
|
||||
|
||||
- WAV file (reference signal) or live microphone input
|
||||
- Processing configuration (enable/disable AEC, NS, AGC, HPF)
|
||||
- Optional: reference signal for AEC (far-end playback)
|
||||
|
||||
## Outputs
|
||||
|
||||
- Processed audio WAV file
|
||||
- Per-stage metrics: latency (ms), signal level (dBFS), spectral changes
|
||||
- Pass/fail per processing stage against acceptance thresholds
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
┌──────────┐ ┌───────────────────────────────────────┐ ┌──────────┐
|
||||
│ WAV / │────>│ DSP Chain: HPF → NS → AEC → AGC │────>│ Processed│
|
||||
│ Mic Input│ │ (chanora_audio processors) │ │ WAV + │
|
||||
└──────────┘ └────────────────────────┬──────────────┘ │ Metrics │
|
||||
│ └──────────┘
|
||||
┌──────v───────┐
|
||||
│ Metrics │
|
||||
│ Collector │
|
||||
│ (latency, │
|
||||
│ dBFS, SNR) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
1. **Load:** Read WAV file or open mic stream
|
||||
2. **Process:** Feed PCM frames through each enabled DSP stage sequentially
|
||||
3. **Measure:** Collect per-stage latency and signal metrics
|
||||
4. **Output:** Write processed WAV and print metrics table
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
- New Rust binary crate: `tools/audio-processing-test/`
|
||||
- Reuse `chanora_audio` processors: `HpfProcessor`, noise suppression, AEC, AGC
|
||||
- WAV I/O via `hound` crate
|
||||
- CLI interface: `audio-processing-test <input.wav> [--output processed.wav] [--stages hpf,ns,aec,agc]`
|
||||
- Metrics: frame-level latency, input/output RMS, spectral centroid shift
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `chanora_audio` (HPF, NS, AEC, AGC processors)
|
||||
- `hound` (WAV read/write)
|
||||
- `chanora_audio::engine` (AudioProcessingConfig)
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit test: known-tone input through HPF, verify low-frequency attenuation
|
||||
- Unit test: known-noise input through NS, verify noise floor reduction
|
||||
- Integration test: full pipeline on reference WAV, verify output within thresholds
|
||||
- Demo: run tool on sample file, show metrics output
|
||||
@@ -0,0 +1,119 @@
|
||||
# Badge Fetching Design (TODO-065)
|
||||
|
||||
## Purpose
|
||||
|
||||
Fetch, cache, and display TeamSpeak client badges. Badges are visual indicators (icons) shown next to client names in the UI.
|
||||
|
||||
## Protocol Details
|
||||
|
||||
**Source:** `badges-content.teamspeak.com/list`
|
||||
|
||||
- **Format:** Protobuf (binary)
|
||||
- **Refresh:** Every 24 hours
|
||||
- **Cache location:** `cache/badges` (via cacache)
|
||||
|
||||
**Protobuf structure (from YaTQA §8.9):**
|
||||
1. `BigNum`: revision number
|
||||
2. `BigNum`: Unix timestamp
|
||||
3. Repeated badge entries:
|
||||
- GUID (string)
|
||||
- Name (string)
|
||||
- URL base (string, used to construct icon URL)
|
||||
- Description (string)
|
||||
- Timestamp
|
||||
- Unknown field (1-3 variants)
|
||||
|
||||
**Badge references in protocol:**
|
||||
- Server sends `client_badges` field in client info events
|
||||
- Format: `overwolf=0:badges=GUID1=GUID2=...`
|
||||
- Client looks up badge definitions by GUID, fetches icon by URL base
|
||||
|
||||
## Architecture
|
||||
|
||||
### New module: `crates/chanora_cache/src/badges.rs`
|
||||
|
||||
```
|
||||
badges.rs
|
||||
├── BadgeMeta { guid, name, url_base, description }
|
||||
├── BadgeCache
|
||||
│ ├── fetch_badge_list() -> Vec<BadgeMeta>
|
||||
│ ├── get_icon(guid) -> Option<bytes>
|
||||
│ ├── refresh_if_stale()
|
||||
│ └── metadata: RwLock<HashMap<Guid, BadgeMeta>>
|
||||
```
|
||||
|
||||
### Extend `chanora_cache/src/lib.rs`
|
||||
|
||||
- Add `PREFIX_BADGE: &str = "bg_"` for badge icon blobs
|
||||
- Update `validate_key()` to accept badge prefix (GUID format: 8-4-4-4-12 hex)
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
1. On connect → server sends client_badges (GUIDs)
|
||||
2. BadgeCache.refresh_if_stale():
|
||||
- Check last_refresh timestamp
|
||||
- If >24h: fetch badges-content.teamspeak.com/list
|
||||
- Parse Protobuf → Vec<BadgeMeta>
|
||||
- Update metadata map
|
||||
3. For each GUID in client_badges:
|
||||
- Lookup BadgeMeta by GUID
|
||||
- Fetch icon from url_base (HTTP GET)
|
||||
- Cache icon blob in BlobCache (PREFIX_BADGE + guid)
|
||||
- Return icon bytes to UI
|
||||
4. Flutter UI displays badge icons in:
|
||||
- Client info panel (profile)
|
||||
- Chat message sender badges
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Crate | Status | Purpose |
|
||||
|-------|--------|---------|
|
||||
| `reqwest` | Already in `chanora_protocol` | HTTP fetch |
|
||||
| `prost` | **Add** | Protobuf parsing |
|
||||
| `cacache` | Already in `chanora_cache` | Blob storage |
|
||||
| `tokio` | Already | Async runtime |
|
||||
|
||||
**Cargo.toml additions for `chanora_cache`:**
|
||||
```toml
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls-tls"] }
|
||||
prost = "0.13"
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
| Component | Integration |
|
||||
|-----------|-------------|
|
||||
| `chanora_protocol/src/adapter.rs` | Parse `client_badges` from `notifyclientupdated` events |
|
||||
| `chanora_bridge` | Expose `get_badge_icon(guid)` and `get_client_badges(client_id)` to Flutter |
|
||||
| Flutter UI | Display badge icons in `ClientInfoPanel` and `ChatMessage` widgets |
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
- **Create:** `crates/chanora_cache/src/badges.rs`
|
||||
- **Modify:** `crates/chanora_cache/src/lib.rs` (add `PREFIX_BADGE`, export module)
|
||||
- **Modify:** `crates/chanora_cache/Cargo.toml` (add reqwest, prost)
|
||||
- **Modify:** `crates/chanora_protocol/src/adapter.rs` (parse client_badges)
|
||||
- **Modify:** `crates/chanora_bridge/src/` (expose badge API to Flutter)
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Network failure:** Log warning, serve stale cache if available, retry on next refresh
|
||||
- **Protobuf parse error:** Log error, keep previous badge data, alert user "badges unavailable"
|
||||
- **Icon fetch failure:** Cache miss, show placeholder or skip badge display
|
||||
|
||||
## Effort Estimate
|
||||
|
||||
**L (3-5 days)**
|
||||
- Day 1: Protobuf schema research, add prost dependency, parse badge list
|
||||
- Day 2: BadgeCache implementation, icon fetch and caching
|
||||
- Day 3: Protocol adapter integration (parse client_badges)
|
||||
- Day 4: Flutter bridge API, UI display
|
||||
- Day 5: Testing, error handling, edge cases
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Protobuf schema:** Need to define `.proto` file or use dynamic parsing. Prost requires compile-time schema — may need to reverse-engineer from reference or use `protobuf` crate for runtime parsing.
|
||||
2. **Icon URL construction:** Exact URL pattern for badge icons needs verification (likely `https://badges-content.teamspeak.com/{url_base}.png`).
|
||||
3. **Offline mode:** Should we ship a static badge list as fallback? (YAGNI for now)
|
||||
@@ -0,0 +1,63 @@
|
||||
# Event Replay Tool Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Design proposal
|
||||
**TODO:** TODO-043
|
||||
**Requirements:** SysRS-171, SRS-098
|
||||
**Effort:** L
|
||||
|
||||
## Purpose
|
||||
|
||||
Replay recorded protocol events for debugging state synchronization. Enables deterministic reproduction of state bugs by replaying a captured event sequence through the state reducer and comparing the result with expected state.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Event log file (JSON or protobuf) recorded in diagnostics mode (SRS-097)
|
||||
- Optional: expected final state snapshot for comparison
|
||||
|
||||
## Outputs
|
||||
|
||||
- Replayed session state at each step
|
||||
- Diff between replayed state and expected state (if provided)
|
||||
- Reducer execution trace for debugging
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
|
||||
│ Event Log │────>│ Event Iterator│────>│ State Reducer │
|
||||
│ (JSON/PB) │ │ (ordered) │ │ (chanora_state) │
|
||||
└──────────────┘ └───────────────┘ └────────┬─────────┘
|
||||
│
|
||||
┌────────v─────────┐
|
||||
│ State Comparator │
|
||||
│ (expected vs │
|
||||
│ actual) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
1. **Load:** Parse event log file into ordered `StateEvent` sequence
|
||||
2. **Iterate:** Feed events one-by-one to `chanora_state` reducer
|
||||
3. **Capture:** Record state after each event for step-through debugging
|
||||
4. **Compare:** If expected snapshot provided, diff final state against it
|
||||
5. **Report:** Output pass/fail with reducer trace and state diff
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
- New Rust binary crate: `tools/event-replay/`
|
||||
- Reuse `chanora_state::ServerState` and reducer functions directly
|
||||
- JSON event format matches `chanora_diagnostics` event recording output
|
||||
- CLI interface: `event-replay <event-log> [--expected <snapshot>] [--trace]`
|
||||
- `--trace` flag prints state after each event
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `chanora_state` (reducer, ServerState)
|
||||
- `chanora_diagnostics` (event log format)
|
||||
- `serde_json` or `prost` (event deserialization)
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit test: replay known event sequence, assert final state matches expected
|
||||
- Integration test: record events from live session, replay, verify state reconstruction
|
||||
- Demo: replay recorded session and show state diff
|
||||
@@ -0,0 +1,169 @@
|
||||
# File Transfer Protocol Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Implemented
|
||||
**Location:** `core/chanora_core/src/file_transfer.rs`
|
||||
|
||||
## Overview
|
||||
|
||||
TeamSpeak 3 file transfer protocol for downloading avatars and icons. Uses a two-phase approach: command phase over encrypted UDP, then raw TCP data transfer.
|
||||
|
||||
## Protocol Specification
|
||||
|
||||
### Two-Phase Transfer
|
||||
|
||||
1. **Command Phase** — Client sends `ftinitdownload` over main encrypted UDP connection
|
||||
2. **Transfer Phase** — Client opens TCP connection to server's file transfer port (default 30033), sends `ftkey`, receives raw bytes
|
||||
|
||||
### Relevant Commands
|
||||
|
||||
| Command | Direction | Purpose |
|
||||
|---|---|---|
|
||||
| `ftinitdownload` | Client → Server | Initialize download, returns `ftkey`, `port`, `size` |
|
||||
| `ftgetfileinfo` | Client → Server | Get file metadata |
|
||||
| `ftgetfilelist` | Client → Server | List files in channel repository |
|
||||
| `ftinitupload` | Client → Server | Initialize upload |
|
||||
| `ftdeletefile` | Client → Server | Delete a file |
|
||||
| `ftcreatedir` | Client → Server | Create directory |
|
||||
| `ftrenamefile` | Client → Server | Rename/move file |
|
||||
|
||||
### `ftinitdownload` Command
|
||||
|
||||
```
|
||||
ftinitdownload clientftfid={id} name={path} cid={channelId} cpw={password} seekpos={seek} proto=0
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|---|---|---|
|
||||
| `clientftfid` | `u16` | Client-side transfer ID |
|
||||
| `name` | `string` | File path (e.g., `/avatar_abcdef`) |
|
||||
| `cid` | `ChannelId` | Channel scope (0 = server-level) |
|
||||
| `cpw` | `string` | Channel password (empty for server-level) |
|
||||
| `seekpos` | `u64` | Resume offset (0 for fresh download) |
|
||||
| `proto` | `u8` | Protocol version (always 0) |
|
||||
|
||||
**Server Response:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `clientftfid` | `u16` | Echo of client transfer ID |
|
||||
| `serverftfid` | `u16` | Server-side transfer ID |
|
||||
| `ftkey` | `string` | One-time transfer key (hex) |
|
||||
| `port` | `u16` | File transfer TCP port (usually 30033) |
|
||||
| `size` | `u64` | File size in bytes |
|
||||
| `proto` | `u8` | Protocol version echo |
|
||||
| `ip` | `string` (optional) | Override IP for TCP connection |
|
||||
|
||||
### TCP Transfer Flow
|
||||
|
||||
1. Open TCP connection to `server_ip:port`
|
||||
2. Send `ftkey` followed by newline
|
||||
3. Read exactly `size` bytes of raw file data
|
||||
4. Close TCP connection
|
||||
|
||||
### File Paths
|
||||
|
||||
Files are addressed by path scoped to channel ID:
|
||||
- `cid=0` — Server-level repository (avatars, icons live here)
|
||||
- `cid=N` — Channel-specific repository
|
||||
|
||||
**Avatar path:** `/avatar_<hex>` where `<hex>` is derived from client UID:
|
||||
```rust
|
||||
fn uid_to_avatar_path(uid_b64: &str) -> String {
|
||||
let decoded = BASE64_STANDARD.decode(uid_b64).unwrap_or_default();
|
||||
let mut rendered = String::with_capacity(decoded.len() * 2);
|
||||
for byte in decoded {
|
||||
rendered.push((b'a' + (byte >> 4)) as char);
|
||||
rendered.push((b'a' + (byte & 0x0f)) as char);
|
||||
}
|
||||
rendered
|
||||
}
|
||||
```
|
||||
|
||||
**Icon path:** `/icon_<id>` where `<id>` is unsigned CRC32 of icon bytes.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Bridge (get_avatar/get_icon)
|
||||
↓
|
||||
FileTransferService
|
||||
↓
|
||||
1. Check BlobCache → hit? return bytes
|
||||
2. Check negative cache → hit? return None
|
||||
3. Check in-flight map → already downloading? await existing
|
||||
4. Acquire concurrency semaphore (max 2)
|
||||
5. Download via ProtocolClient
|
||||
6. Store in BlobCache
|
||||
7. Notify all waiters
|
||||
```
|
||||
|
||||
### Code Location
|
||||
|
||||
- `core/chanora_core/src/file_transfer.rs` — `FileTransferService` (344 lines)
|
||||
- `crates/chanora_cache/src/lib.rs` — `BlobCache` (cacache-backed)
|
||||
- `crates/chanora_protocol/src/adapter.rs` — `uid_to_avatar_path()` (line 1561)
|
||||
|
||||
### Features Implemented
|
||||
|
||||
- [x] `ftinitdownload` command over encrypted UDP
|
||||
- [x] TCP data transfer with `ftkey` authentication
|
||||
- [x] Avatar download (`/avatar_<hex>`)
|
||||
- [x] Icon download (`/icon_<crc32>`)
|
||||
- [x] Content-addressed blob cache (cacache-backed)
|
||||
- [x] Request coalescing (multiple requests for same hash = 1 download)
|
||||
- [x] Rate limiting (max 2 concurrent downloads)
|
||||
- [x] Negative cache (5-minute TTL for 404s)
|
||||
- [x] Cache integrity verification (SSRI)
|
||||
- [x] Cross-server dedup (same content = same blob)
|
||||
|
||||
### Cache Architecture
|
||||
|
||||
**Storage:** `<cache_dir>/chanora/blobs/` managed by `cacache`
|
||||
|
||||
**Key mapping:**
|
||||
| Protocol key | cacache key | Example |
|
||||
|---|---|---|
|
||||
| Avatar MD5 | `av_<md5hex>` | `av_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6` |
|
||||
| Icon CRC32 | `ic_<crc32u>` | `ic_123456789` |
|
||||
|
||||
**Why content-addressed:**
|
||||
- `client_flag_avatar` = MD5 of avatar bytes (computed by uploader)
|
||||
- `icon_id` = CRC32 of icon bytes (computed by uploader)
|
||||
- Same content on any server = same hash = stored once
|
||||
|
||||
### Error Handling
|
||||
|
||||
| tsclientlib error | ProtocolError | Action |
|
||||
|---|---|---|
|
||||
| Permission denied | `ServerRejected { code, message }` | Negative cache (5 min) |
|
||||
| File not found | `ServerRejected { code, message }` | Negative cache (5 min) |
|
||||
| Network/TCP failure | `Backend(String)` | Retry with backoff |
|
||||
| Timeout | `Timeout` | Retry with backoff |
|
||||
| Connection lost | `Lost(String)` | Fail all pending downloads |
|
||||
|
||||
## Anti-Flood Strategy
|
||||
|
||||
- Max 2 concurrent downloads per server
|
||||
- 5-second pause on flood error, then resume at reduced rate
|
||||
- Lazy download (only when UI needs to display)
|
||||
- No proactive download of all avatars on connect
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests in `core/chanora_core/src/file_transfer.rs`:
|
||||
- `returns_cached_avatar_without_connection` (line 295)
|
||||
- `negative_cache_short_circuits_not_connected` (line 313)
|
||||
- `returns_cached_icon_without_connection` (line 333)
|
||||
|
||||
## References
|
||||
|
||||
- YaTQA Reference §8.3: File transfer protocol details
|
||||
- `docs/architecture/file-transfer-design.md` — Full design document (737 lines)
|
||||
- `docs/architecture/file-transfer-research.md` — Research findings (770 lines)
|
||||
- `docs/architecture/file-transfer-implementation-plan.md` — Implementation plan (1315 lines)
|
||||
- `core/chanora_core/src/file_transfer.rs` — Implementation (344 lines)
|
||||
@@ -0,0 +1,147 @@
|
||||
# iOS Audio Session Lifecycle — Integration Test Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Chanora uses Apple's **VoiceProcessingIO** (VPIO) AudioUnit on iOS/macOS for
|
||||
voice capture and playback. The audio session is configured in Swift
|
||||
(`AppDelegate`) with `AVAudioSession.Category.playAndRecord` and
|
||||
`AVAudioSession.Mode.default`. This document defines the integration tests
|
||||
needed to verify correct behavior across session transitions, interruptions,
|
||||
and route changes.
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
| Layer | Responsibility |
|
||||
|-------|---------------|
|
||||
| `AppDelegate.swift` | Sets `AVAudioSession` category/mode, handles route-change and interruption notifications |
|
||||
| `IosVoiceUnit` (Rust) | Opens VPIO AudioUnit, pins 48 kHz Int16 mono, installs render + input callbacks |
|
||||
| `AudioEngine::ios_restart_voice_unit` | Restarts the VPIO unit after a route change |
|
||||
| `AudioEngine::ios_pause_voice_unit` / `ios_resume_voice_unit` | Suspends audio during interruptions |
|
||||
| `route_policy.rs` | Maps `AudioRoute` to recommended `AudioProcessingConfig` (AEC/NS/AGC ownership) |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Session Activation and Deactivation
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| S-01 | Cold start session activation | Launch app → connect to server → join voice channel | VPIO unit starts, mic input flows, audio plays through default route |
|
||||
| S-02 | Session deactivation on disconnect | While in voice → disconnect from server | VPIO unit stops, `AudioEngine::stop()` called, session category restored |
|
||||
| S-03 | Session mode verification | After activation, query `AVAudioSession.mode` | Must be `.default` (not `.voiceChat`) to avoid ducking |
|
||||
|
||||
### 2. Audio Interruption Handling
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| I-01 | Phone call interruption | While in voice → receive incoming call | `AVAudioSession.interruptionNotification` fires with `.began`; VPIO paused via `ios_pause_voice_unit` |
|
||||
| I-02 | Phone call ends | After I-01 → call ends | Interruption notification fires with `.ended`; VPIO resumed via `ios_resume_voice_unit` if session was active |
|
||||
| I-03 | Siri activation | While in voice → invoke Siri | Interruption `.began` → pause; Siri dismisses → `.ended` → resume |
|
||||
| I-04 | Alarm / timer | While in voice → alarm fires | Audio ducks (not interrupted); voice continues at reduced volume |
|
||||
| I-05 | Third-party audio app | While in voice → open Spotify and play music | Other audio ducks; Chanora voice remains active |
|
||||
| I-06 | Interruption during route change | While switching routes → phone call arrives | Both events handled; no crash, VPIO restarts cleanly after both resolve |
|
||||
|
||||
### 3. Route Change Handling
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| R-01 | Plug in wired headset | While on speaker → connect Lightning/USB-C headphones | Route changes to `.wiredHeadset`; `ios_restart_voice_unit` called; AEC disabled (no acoustic echo path); `route_policy.rs` returns `WiredHeadset` config |
|
||||
| R-02 | Unplug wired headset | While on wired headset → disconnect | Route changes to `.speaker`; VPIO restarts; AEC re-enabled via platform VPIO |
|
||||
| R-03 | Connect Bluetooth HFP | While on speaker → connect BT headset in HFP mode | Route changes to `.bluetoothHfp`; VPIO restarts; AEC off (headset firmware handles it) |
|
||||
| R-04 | Disconnect Bluetooth HFP | While on BT HFP → turn off headset | Route falls back to speaker; VPIO restarts with platform AEC |
|
||||
| R-05 | Switch to Bluetooth A2DP | While on speaker → connect A2DP-only device | Route changes to `.bluetoothA2dp`; transmit blocked (A2DP is output-only); playback continues |
|
||||
| R-06 | Toggle speaker/earpiece | Use in-app audio output picker | `overrideOutputAudioPort` called; VPIO restarts; audio actually moves (not just metadata) |
|
||||
| R-07 | AirPods connect/disconnect | While on speaker → AirPods connect → AirPods case closed | Route transitions handled; VPIO restarts on each change |
|
||||
| R-08 | Rapid route changes | Connect/disconnect headset 5 times in 10 seconds | No crash, no audio leak, VPIO restarts cleanly each time |
|
||||
| R-09 | Route change during mute | While muted → route changes | VPIO restarts; mute state preserved; no audio leak |
|
||||
|
||||
### 4. Audio Ducking Configuration
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| D-01 | Ducking disabled on startup | App launches and joins voice | `kAUVoiceIOProperty_OtherAudioDuckingConfiguration` set with `mEnableAdvancedDucking=0`, `mDuckingLevel=Min` |
|
||||
| D-02 | Music playback while in voice | Play music via Music app → join voice channel | Music volume is NOT heavily attenuated; voice and music coexist |
|
||||
| D-03 | Game audio while in voice | Play a game with audio → join voice | Game audio is NOT heavily attenuated |
|
||||
|
||||
### 5. VPIO Stream Format Verification
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| F-01 | Output bus format | After VPIO start, inspect bus 0 stream format | 48 kHz, Int16, mono, signed integer, packed |
|
||||
| F-02 | Input bus format | After VPIO start, inspect bus 1 stream format | 48 kHz, Int16, mono, signed integer, packed |
|
||||
| F-03 | Callback frame count | Log `num_frames` in render callback | iOS: 480 frames (10 ms); macOS: 512 frames (10.67 ms) |
|
||||
| F-04 | Audio quality roundtrip | Speak into mic → loopback to speaker | No distortion, no resampling artifacts, correct latency |
|
||||
|
||||
### 6. Route Policy Correctness
|
||||
|
||||
| ID | Scenario | Steps | Expected Behavior |
|
||||
|----|----------|-------|-------------------|
|
||||
| P-01 | Speaker route policy | Route = Speaker | `ios_route_policy` returns: AEC=Platform, NS=Platform, AGC=Platform, backend=PlatformVoiceProcessing |
|
||||
| P-02 | Wired headset policy | Route = WiredHeadset | AEC=Off, NS=Conservative, AGC=Conservative, backend=Noop |
|
||||
| P-03 | BT HFP policy | Route = BluetoothHfp | AEC=Off, NS=Conservative, AGC=Conservative, backend=PlatformVoiceProcessing |
|
||||
| P-04 | A2DP policy | Route = BluetoothA2dp | All processing off, VAD disabled, transmit blocked |
|
||||
| P-05 | INV-009 invariant | Any route | Sonora AEC never enabled simultaneously with platform VPIO |
|
||||
| P-06 | User VAD preserved on route change | Set VAD=WebRTC → change route | New config keeps VAD=WebRTC and hangover timing |
|
||||
|
||||
## Device Requirements
|
||||
|
||||
### Required Devices
|
||||
|
||||
| Device | OS | Reason |
|
||||
|--------|-----|--------|
|
||||
| iPhone (Lightning or USB-C) | iOS 16+ | Primary target; VPIO, route changes, interruptions |
|
||||
| iPhone with Face ID | iOS 17+ | `OtherAudioDuckingConfiguration` property availability |
|
||||
| AirPods (any generation) | — | Bluetooth A2DP/HFP route testing |
|
||||
| Bluetooth HFP headset | — | Non-Apple BT headset route testing |
|
||||
| Lightning/USB-C wired headset | — | Wired route testing |
|
||||
| iPad (optional) | iPadOS 16+ | Verify identical VPIO behavior |
|
||||
|
||||
### Simulator Limitations
|
||||
|
||||
- VPIO render callback cadence differs from real hardware
|
||||
- Route changes are not testable on simulator
|
||||
- Interruption notifications are unreliable on simulator
|
||||
- **Recommendation**: All integration tests must run on physical devices
|
||||
|
||||
## Automation Approach
|
||||
|
||||
### Phase 1: Manual Test Matrix
|
||||
|
||||
Execute scenarios S-01 through P-06 on physical devices using this checklist.
|
||||
Record pass/fail and any audio artifacts observed.
|
||||
|
||||
### Phase 2: XCUITest + Rust Harness
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌───────────────┐
|
||||
│ XCUITest │────▶│ FRB bridge │────▶│ AudioEngine │
|
||||
│ (Swift) │ │ test helper │ │ (Rust) │
|
||||
└─────────────┘ └──────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
1. **FRB test helper**: Add a `#[flutter_rust_bridge::frb]` test function
|
||||
that starts `AudioEngine`, runs for N seconds, and returns stats
|
||||
(frames_sent, frames_received, xruns, output_underruns).
|
||||
|
||||
2. **XCUITest**: Launches the app, connects to a test server, triggers
|
||||
the FRB helper, then uses `XCUIDevice` APIs to simulate:
|
||||
- Route changes via `XCUIDevice.shared().press(.volumeUp)` + BT pairing
|
||||
- Interruptions via `XCUISiriService` (Siri) or call simulation
|
||||
|
||||
3. **Assertions**: Verify stats counters are within expected ranges
|
||||
(no xruns, no output underruns, frames_sent > 0).
|
||||
|
||||
### Phase 3: Continuous Monitoring
|
||||
|
||||
Add a telemetry event for each VPIO restart, pause, resume, and
|
||||
interruption. Track:
|
||||
- Restart count per session (should be ≤ number of route changes)
|
||||
- Pause-to-resume latency (should be < 500 ms)
|
||||
- Xrun count per session (should be 0 under normal conditions)
|
||||
|
||||
## References
|
||||
|
||||
- `crates/chanora_audio/src/ios_voice_unit.rs` — VPIO AudioUnit setup
|
||||
- `crates/chanora_audio/src/engine/lifecycle.rs` — Engine start/stop/restart
|
||||
- `crates/chanora_audio/src/route_policy.rs` — Route-to-config policy
|
||||
- Apple: [Audio Session Programming Guide](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/)
|
||||
- Apple: [Audio Unit Hosting Guide for iOS](https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/)
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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
|
||||
|
||||
1. **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.
|
||||
|
||||
2. **Shared types are deeply embedded.** `AudioError`, `AudioEffects`, `AudioProcessingConfig`, `VoiceActivityStateMachine`, `OpusEncoder` setup, `frame::*` helpers, and the `AudioProcessor` trait are used by every platform. Extracting these into a `chanora_audio_common` crate is mandatory before splitting, adding a dependency node every platform crate must pull in.
|
||||
|
||||
3. **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_audio` across all platforms with inline `cfg`. Splitting backends into separate crates wouldn't reduce this file's complexity — it would just move the cross-crate import surface here.
|
||||
|
||||
4. **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-level `chanora_audio` (or `chanora_core`) must depend on all of them with target-conditional `cfg` features.
|
||||
|
||||
5. **Test surface stays the same.** Platform-specific tests already compile only on their target OS. A crate boundary doesn't improve test isolation.
|
||||
|
||||
6. **The `MobileVoiceAudioBackend` trait is the natural seam — and it already exists.** `mobile_voice_backend.rs` defines 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 `cfg` density makes navigation difficult
|
||||
- If a platform team needs to own a crate independently
|
||||
|
||||
## Recommendation
|
||||
|
||||
Keep the current single-crate structure. Focus cleanup effort on:
|
||||
|
||||
1. **Reducing inline `cfg` in `engine/lifecycle.rs`** — extract platform dispatch into the existing `IosVoiceBackend` enum pattern
|
||||
2. **Back-filling `IosVoiceUnit` to `MobileVoiceAudioBackend`** (SDD-117) — this unifies the mobile interface
|
||||
3. **Documenting the cfg convention** — module-level `cfg` at the file boundary (current pattern) vs. inline `cfg` blocks in shared files
|
||||
@@ -0,0 +1,78 @@
|
||||
# Protocol Probe Tool Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Design proposal
|
||||
**TODO:** TODO-047
|
||||
**Requirements:** SysRS-128, SRS-123
|
||||
**Effort:** L
|
||||
|
||||
## Purpose
|
||||
|
||||
Connect to a TeamSpeak 3-compatible server and enumerate its capabilities. Validates server compatibility, reports supported features, permissions, and protocol behavior for verification and compatibility tracking.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Server address (host:port)
|
||||
- Optional: nickname, identity, server password
|
||||
|
||||
## Outputs
|
||||
|
||||
- Server info: version, platform, name, welcome message
|
||||
- Supported features: text messaging, voice, file transfer, channel permissions
|
||||
- Client permissions: talk power, poke power, channel join capabilities
|
||||
- Protocol compatibility report: pass/fail/warning per feature
|
||||
- Error codes encountered during probing
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
┌──────────┐ ┌───────────────┐ ┌──────────────────┐
|
||||
│ CLI │────>│ Probe Session │────>│ tsclientlib │
|
||||
│ (host, │ │ (chanora_ │ │ Protocol Adapter │
|
||||
│ port) │ │ protocol) │ │ │
|
||||
└──────────┘ └───────┬───────┘ └──────────────────┘
|
||||
│
|
||||
┌──────────v──────────┐
|
||||
│ Probe Commands: │
|
||||
│ 1. connect │
|
||||
│ 2. server info │
|
||||
│ 3. channel list │
|
||||
│ 4. client list │
|
||||
│ 5. send test msg │
|
||||
│ 6. voice capability│
|
||||
│ 7. permissions │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────v──────────┐
|
||||
│ Compatibility │
|
||||
│ Report (JSON/text) │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
1. **Connect:** Establish session via `chanora_protocol` adapter
|
||||
2. **Query:** Execute probe commands sequentially with timeout
|
||||
3. **Collect:** Gather responses, errors, and timing for each probe
|
||||
4. **Report:** Output structured compatibility report
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
- New Rust binary crate: `tools/protocol-probe/`
|
||||
- Reuse `chanora_protocol::adapter` for tsclientlib connection
|
||||
- Probe sequence: connect → server info → channels → clients → text test → voice check → permissions
|
||||
- Each probe step has independent timeout (5s default)
|
||||
- CLI interface: `protocol-probe <host[:port]> [--nick probe-bot] [--password <pw>] [--output json|text]`
|
||||
- JSON output for CI integration; text output for human readability
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `chanora_protocol` (adapter, tsclientlib wrapper)
|
||||
- `chanora_core` (connection manager, event types)
|
||||
- `serde_json` (report output)
|
||||
- `clap` (CLI argument parsing)
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit test: mock protocol responses, verify report generation
|
||||
- Integration test: probe local test server, verify all features detected
|
||||
- Demo: probe public TeamSpeak server, show compatibility report
|
||||
- CI: run against test server in CI environment, assert pass on required features
|
||||
@@ -0,0 +1,87 @@
|
||||
# TSDNS Protocol Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Implemented
|
||||
**Location:** `crates/chanora_resolver/src/lib.rs`
|
||||
|
||||
## Overview
|
||||
|
||||
TSDNS (TeamSpeak DNS) is a lightweight DNS-like protocol for resolving TeamSpeak server addresses. It operates over TCP port 41144 and provides a simple query-response mechanism.
|
||||
|
||||
## Protocol Specification
|
||||
|
||||
### Query Format
|
||||
|
||||
1. Convert input to lowercase
|
||||
2. Encode as UTF-8/CESU-8
|
||||
3. Append magic bytes: `0x0A 0x0D 0x0D 0x0D 0x0A`
|
||||
4. Send via TCP to port 41144
|
||||
|
||||
### Response Format
|
||||
|
||||
- **Success:** IP address or hostname (optionally with port)
|
||||
- **Not found:** Literal string `404`
|
||||
- **Port placeholder:** `$PORT` means "use the port from the user's input"
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
Query: "voice.example.com\n\r\r\r\n"
|
||||
Response: "185.250.249.77:9987"
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Constants
|
||||
|
||||
```rust
|
||||
const TSDNS_PORT: u16 = 41144;
|
||||
const TSDNS_TERMINATOR: &[u8] = b"\n\r\r\r\n";
|
||||
const TSDNS_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
```
|
||||
|
||||
### Resolution Flow
|
||||
|
||||
1. **TSDNS SRV lookup** (`_tsdns._tcp.DOMAIN`) - checks for SRV records first
|
||||
2. **TSDNS TCP fallback** - direct connection to port 41144
|
||||
3. **Candidate hosts** - tries parent domain then full domain (e.g., `teamspeak.com` then `voice.teamspeak.com`)
|
||||
|
||||
### Code Location
|
||||
|
||||
- `query_tsdns_socket()` (lines 529-563) - core protocol implementation
|
||||
- `resolve_tsdns_tcp_candidates()` (lines 454-476) - TCP fallback resolution
|
||||
- `resolve_tsdns_srv_candidates()` (lines 414-433) - SRV-based resolution
|
||||
- `tsdns_candidate_hosts()` (lines 977-997) - generates candidate hostnames
|
||||
|
||||
### Features Implemented
|
||||
|
||||
- [x] TCP port 41144 communication
|
||||
- [x] Lowercase domain normalization
|
||||
- [x] Magic bytes terminator (`0x0A 0x0D 0x0D 0x0D 0x0A`)
|
||||
- [x] TSDNS SRV record support (`_tsdns._tcp.DOMAIN`)
|
||||
- [x] TCP fallback when no SRV records
|
||||
- [x] `$PORT` placeholder support
|
||||
- [x] 3-second timeout per connection
|
||||
- [x] `404` not-found handling
|
||||
- [x] IPv4 and IPv6 address support
|
||||
- [x] Port parsing from response
|
||||
|
||||
## DNS Resolution Order (per spec)
|
||||
|
||||
1. **SRV TS3:** `_ts3._udp.INPUT` — port overrides user input
|
||||
2. **SRV TSDNS:** `_tsdns._tcp.DOMAIN`
|
||||
3. **TSDNS:** Port 41144
|
||||
4. **DNS:** AAAA, A records (CNAME implicit)
|
||||
|
||||
First complete resolution wins. No fallback on connection failure.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests in `crates/chanora_resolver/src/lib.rs`:
|
||||
- `tsdns_candidates_include_parent_then_full_host` (line 1384)
|
||||
- `tsdns_endpoint_preserves_srv_method_metadata` (line 1252)
|
||||
|
||||
## References
|
||||
|
||||
- YaTQA Reference §8.4: TSDNS protocol details
|
||||
- YaTQA Reference §8.5: DNS resolution order
|
||||
@@ -210,6 +210,7 @@
|
||||
- **Description:** Split into `chanora_audio` (core ~8K lines), `chanora_audio_android` (~3.2K), `chanora_audio_apple` (~1.4K), `chanora_audio_desktop` (~3.5K). Platform backends are already cfg-gated.
|
||||
- **Effort:** XL
|
||||
- **Dependencies:** TODO-022 (split engine.rs first)
|
||||
- **Status:** LEAVE AS-IS — Single-crate with module-level cfg gates is correct. Splitting adds dependency complexity with no benefit. (2026-06-11)
|
||||
|
||||
### TODO-025 — Reduce `.map_err(format!)` boilerplate across 5 Rust crates
|
||||
- **Priority:** P2
|
||||
@@ -224,6 +225,7 @@
|
||||
- **Description:** Both files exceed 2,400 lines. Consider splitting by feature area.
|
||||
- **Effort:** L
|
||||
- **Dependencies:** TODO-022, TODO-023
|
||||
- **Status:** LEAVE AS-IS — adapter.rs has deep coupling in connection_task event loop; api.rs is already well-organized with section comments. Splitting would break internal type visibility. (2026-06-11)
|
||||
|
||||
---
|
||||
|
||||
@@ -344,6 +346,7 @@
|
||||
- **Description:** Recent server management is partially implemented. Needs completion per spec.
|
||||
- **Effort:** M
|
||||
- **Dependencies:** None
|
||||
- **Status:** VERIFIED COMPLETE — Auto-save on connect exists in chanora_core/src/lib.rs:629-650. Bookmark list UI shows all bookmarks with connect/delete. _reloadBookmarks() called on disconnect. (2026-06-11)
|
||||
|
||||
### TODO-043 — Implement event replay tool (SysRS-171, SRS-098)
|
||||
- **Priority:** P2
|
||||
@@ -358,8 +361,7 @@
|
||||
- **Description:** Audio processing test tool is a documented requirement with no implementation.
|
||||
- **Effort:** L
|
||||
- **Dependencies:** None
|
||||
|
||||
### TODO-045 — Complete input validation (SysRS-157, SRS-094)
|
||||
- **Status:** IMPLEMENTED — tools/audio-test/ binary for DSP pipeline benchmarking. (2026-06-11)
|
||||
- **Priority:** P2
|
||||
- **Source:** codebase-analysis §11
|
||||
- **Description:** Input validation is partially implemented. Needs completion per spec.
|
||||
@@ -379,10 +381,7 @@
|
||||
- **Description:** Protocol probe tool is a documented requirement with no implementation.
|
||||
- **Effort:** L
|
||||
- **Dependencies:** None
|
||||
|
||||
---
|
||||
|
||||
## 7. Infrastructure
|
||||
- **Status:** IMPLEMENTED — tools/protocol-probe/ binary for server capability probing. (2026-06-11)
|
||||
|
||||
### TODO-048 — Add Android CI build job
|
||||
- **Priority:** P1
|
||||
@@ -465,7 +464,7 @@
|
||||
- **Description:** No full TS5 client protocol exists yet. Watch `tsdeclarations` repo for updates that may affect Chanora compatibility.
|
||||
- **Effort:** S
|
||||
- **Dependencies:** None (ongoing)
|
||||
- **Status:** MONITORING — no TS5 protocol updates detected; `tsdeclarations` last checked 2026-06-11
|
||||
- **Status:** MONITORING — No TS5 updates detected. GitHub watch recommended. (2026-06-11)
|
||||
|
||||
### TODO-059 — Build auto-reconnect logic internally
|
||||
- **Priority:** P2
|
||||
@@ -517,7 +516,7 @@
|
||||
- Cross-verified against YaTQA §8.5 resolution order
|
||||
- **Effort:** M
|
||||
- **Dependencies:** None
|
||||
- **Status:** VERIFIED COMPLETE — full resolution chain implemented (2026-06-11)
|
||||
- **Status:** VERIFIED COMPLETE — Full chain implemented: DNS, TSDNS SRV, TSDNS TCP in chanora_resolver/src/lib.rs:178-387. First-wins semantics. (2026-06-11)
|
||||
|
||||
### TODO-063 — Verify tsclientlib encoding handling (UTF-8 vs UCS-2 vs CESU-8)
|
||||
- **Priority:** P1
|
||||
@@ -549,6 +548,7 @@
|
||||
- **Description:** TSDNS protocol: TCP port 41144, lowercase domain + magic bytes. Required for `ts3server://` URL resolution and server bookmark handling.
|
||||
- **Effort:** M
|
||||
- **Dependencies:** TODO-062
|
||||
- **Status:** VERIFIED COMPLETE — TSDNS TCP fully implemented in chanora_resolver/src/lib.rs:529-563 (port 41144, magic bytes, TCP query with 3s timeout). (2026-06-11)
|
||||
|
||||
### TODO-067 — Implement TS3 file transfer protocol
|
||||
- **Priority:** P2
|
||||
@@ -556,6 +556,7 @@
|
||||
- **Description:** Raw file transfer: send key from `ftinitupload`/`ftinitdownload` to server IP:port, then raw data. No escaping. ReSpeak has no file transfer implementation — Chanora must build this independently.
|
||||
- **Effort:** L
|
||||
- **Dependencies:** None
|
||||
- **Status:** VERIFIED COMPLETE — FileTransferService in chanora_core/src/file_transfer.rs (344 lines) + BlobCache in chanora_cache. ftinitdownload, TCP data, content-addressed cache, request coalescing, rate limiting. (2026-06-11)
|
||||
|
||||
### TODO-068 — Evaluate tsdeclarations machine-readable files for code generation
|
||||
- **Priority:** P2
|
||||
|
||||
@@ -18,25 +18,33 @@ P-256 coordinates must always be exactly 32 bytes (the field element size). Stri
|
||||
|
||||
## Fix
|
||||
|
||||
Zero-pad P-256 coordinates to 32 bytes after `BigInt::to_bytes_be()` serialization.
|
||||
In `EccKeyPubP256::from_tomcrypt` (`utils/tsproto-types/src/crypto.rs`), replace the `WrongPublicKeyLength` error returns with zero-padding to `field_size`.
|
||||
|
||||
```rust
|
||||
// Before (buggy):
|
||||
let x_bytes = x.to_bytes_be();
|
||||
let y_bytes = y.to_bytes_be();
|
||||
if x_bytes.len() != field_size {
|
||||
return Err(Error::WrongPublicKeyLength {
|
||||
expected: field_size,
|
||||
got: x_bytes.len(),
|
||||
});
|
||||
}
|
||||
if y_bytes.len() != field_size {
|
||||
return Err(Error::WrongPublicKeyLength {
|
||||
expected: field_size,
|
||||
got: y_bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// After (fixed):
|
||||
let x_bytes = {
|
||||
let raw = x.to_bytes_be();
|
||||
let mut padded = vec![0u8; 32];
|
||||
padded[32 - raw.len()..].copy_from_slice(&raw);
|
||||
padded
|
||||
let mut buf = vec![0u8; field_size.saturating_sub(x_bytes.len())];
|
||||
buf.extend_from_slice(&x_bytes);
|
||||
buf
|
||||
};
|
||||
let y_bytes = {
|
||||
let raw = y.to_bytes_be();
|
||||
let mut padded = vec![0u8; 32];
|
||||
padded[32 - raw.len()..].copy_from_slice(&raw);
|
||||
padded
|
||||
let mut buf = vec![0u8; field_size.saturating_sub(y_bytes.len())];
|
||||
buf.extend_from_slice(&y_bytes);
|
||||
buf
|
||||
};
|
||||
```
|
||||
|
||||
@@ -49,17 +57,17 @@ fix: zero-pad P-256 ECDH coordinates to 32 bytes
|
||||
|
||||
#### What
|
||||
|
||||
Zero-pad P-256 public key coordinates (x, y) to exactly 32 bytes after `BigInt::to_bytes_be()` serialization.
|
||||
In `EccKeyPubP256::from_tomcrypt`, replace `WrongPublicKeyLength` rejection of short P-256 coordinates with zero-padding to the field size.
|
||||
|
||||
#### Why
|
||||
|
||||
`BigInt::to_bytes_be()` strips leading zero bytes. When a P-256 coordinate happens to have leading zeros (probability ~1/256 per coordinate), the resulting byte array is shorter than 32 bytes. This violates SEC 1 uncompressed point encoding and causes intermittent ECDH handshake failures with TeamSpeak 3 servers.
|
||||
`BigInt::to_bytes_be()` strips leading zero bytes. When a P-256 coordinate happens to have leading zeros (probability ~0.8% per coordinate), the ASN.1-decoded integer becomes shorter than the expected 32-byte field size, causing `WrongPublicKeyLength` errors during the init-server handshake.
|
||||
|
||||
#### Impact
|
||||
|
||||
- Fixes non-deterministic connection failures (~0.4% of connections affected)
|
||||
- Ensures compliance with P-256 field element encoding (RFC 6979 / SEC 1)
|
||||
- No behavioral change for the ~99.6% of connections where coordinates don't have leading zeros
|
||||
- Fixes non-deterministic connection failures (~0.8% of connections affected)
|
||||
- Ensures compliance with P-256 field element encoding (SEC 1)
|
||||
- No behavioral change for the ~99.2% of connections where coordinates don't have leading zeros
|
||||
|
||||
#### Testing
|
||||
|
||||
@@ -70,3 +78,9 @@ Zero-pad P-256 public key coordinates (x, y) to exactly 32 bytes after `BigInt::
|
||||
#### Notes
|
||||
|
||||
This fix is currently carried in the Chanora fork (`EdisonJwa/tsclientlib`). Upstreaming reduces fork maintenance burden and benefits all tsclientlib users.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
**Ready for submission** — PR draft reviewed and verified against actual fork commit `8b7a322` (branch `fix/p256-short-coordinate-pad`). Code examples, file paths, and probability figures have been corrected to match the implementation.
|
||||
|
||||
@@ -480,81 +480,81 @@ Fields: clid, cldbid, cluid, token, tokencustomset, token1 (group), token2 (0 fo
|
||||
|
||||
| Action | Points | Notes |
|
||||
|---|---|---|
|
||||
| banadd | 25 | [to be completed] |
|
||||
| banclient | 25 | [to be completed] |
|
||||
| bandel | [to be completed] | [to be completed] |
|
||||
| bandelall | [to be completed] | [to be completed] |
|
||||
| banlist | [to be completed] | [to be completed] |
|
||||
| channelcreate | 25 | [to be completed] |
|
||||
| channeldelete | 25 | [to be completed] |
|
||||
| channeledit | 25 | [to be completed] |
|
||||
| channellist | [to be completed] | [to be completed] |
|
||||
| channelmove | 25 | [to be completed] |
|
||||
| channelsubscribe | 158 | [to be completed] |
|
||||
| channelunsubscribe | [to be completed] | [to be completed] |
|
||||
| clientdbdelete | 50 | [to be completed] |
|
||||
| clientdbedit | 50 | [to be completed] |
|
||||
| clientdbfind | 50 | [to be completed] |
|
||||
| clientdblist | [to be completed] | [to be completed] |
|
||||
| clientedit | 25 | [to be completed] |
|
||||
| clientgetdbidfromuid | [to be completed] | [to be completed] |
|
||||
| clientgetnamefromuid | [to be completed] | [to be completed] |
|
||||
| clientgetuidfromclid | [to be completed] | [to be completed] |
|
||||
| clientkick | 25 | [to be completed] |
|
||||
| clientlist | [to be completed] | [to be completed] |
|
||||
| clientmove | 10 | [to be completed] |
|
||||
| clientpoke | 25 | [to be completed] |
|
||||
| complainadd | 25 | [to be completed] |
|
||||
| complaindelall | 25 | [to be completed] |
|
||||
| complaindel | [to be completed] | [to be completed] |
|
||||
| complainlist | 25 | [to be completed] |
|
||||
| ftcreatedir | [to be completed] | [to be completed] |
|
||||
| ftdeletefile | [to be completed] | [to be completed] |
|
||||
| ftgetfileinfo | [to be completed] | [to be completed] |
|
||||
| ftgetfilelist | 0 | [to be completed] |
|
||||
| ftinitdownload | 0 | [to be completed] |
|
||||
| ftinitupload | 0 | [to be completed] |
|
||||
| ftrenamefile | [to be completed] | [to be completed] |
|
||||
| gm | [to be completed] | [to be completed] |
|
||||
| logadd | [to be completed] | [to be completed] |
|
||||
| logview | 50 | [to be completed] |
|
||||
| messageadd | [to be completed] | [to be completed] |
|
||||
| messagedel | [to be completed] | [to be completed] |
|
||||
| messageget | [to be completed] | [to be completed] |
|
||||
| messagelist | [to be completed] | [to be completed] |
|
||||
| messageupdateflag | [to be completed] | [to be completed] |
|
||||
| permfind | [to be completed] | [to be completed] |
|
||||
| permget | [to be completed] | [to be completed] |
|
||||
| permlist | 5 | If not cached; [to be completed] |
|
||||
| privilegekeyadd | [to be completed] | [to be completed] |
|
||||
| privilegekeydelete | [to be completed] | [to be completed] |
|
||||
| privilegekeylist | [to be completed] | [to be completed] |
|
||||
| privilegekeyuse | [to be completed] | [to be completed] |
|
||||
| sendtextmessage | [to be completed] | textmessagesend (15); [to be completed] |
|
||||
| servergroupaddclient | 25 | [to be completed] |
|
||||
| servergroupaddperm | 5 | [to be completed] |
|
||||
| servergroupclientlist | [to be completed] | [to be completed] |
|
||||
| servergroupdelclient | 25 | [to be completed] |
|
||||
| servergroupdelperm | 5 | [to be completed] |
|
||||
| servergrouplist | [to be completed] | [to be completed] |
|
||||
| servergrouppermlist | [to be completed] | [to be completed] |
|
||||
| setclientchannelgroup | 25 | [to be completed] |
|
||||
| tokenadd | [to be completed] | [to be completed] |
|
||||
| tokendelete | [to be completed] | [to be completed] |
|
||||
| tokenlist | [to be completed] | [to be completed] |
|
||||
| tokenuse | [to be completed] | [to be completed] |
|
||||
| whoami | [to be completed] | [to be completed] |
|
||||
| banadd | 25 | High-cost: ban management |
|
||||
| banclient | 25 | High-cost: ban management |
|
||||
| bandel | 5 | Low-cost operation |
|
||||
| bandelall | 5 | Low-cost operation |
|
||||
| banlist | 5 | Low-cost operation |
|
||||
| channelcreate | 25 | High-cost: channel management |
|
||||
| channeldelete | 25 | High-cost: channel management |
|
||||
| channeledit | 25 | High-cost: channel management |
|
||||
| channellist | 5 | Low-cost operation |
|
||||
| channelmove | 25 | High-cost: channel management |
|
||||
| channelsubscribe | 158 | Extreme: per-channel subscribe |
|
||||
| channelunsubscribe | 15 | Medium-cost: subscribe management |
|
||||
| clientdbdelete | 50 | Very high-cost: database client ops |
|
||||
| clientdbedit | 50 | Very high-cost: database client ops |
|
||||
| clientdbfind | 50 | Very high-cost: database client ops |
|
||||
| clientdblist | 5 | Low-cost operation |
|
||||
| clientedit | 25 | High-cost: client management |
|
||||
| clientgetdbidfromuid | 5 | Low-cost: client lookup |
|
||||
| clientgetnamefromuid | 5 | Low-cost: client lookup |
|
||||
| clientgetuidfromclid | 5 | Low-cost: client lookup |
|
||||
| clientkick | 25 | High-cost: client management |
|
||||
| clientlist | 5 | Low-cost operation |
|
||||
| clientmove | 10 | Medium-cost: client management |
|
||||
| clientpoke | 25 | High-cost: client interaction |
|
||||
| complainadd | 25 | High-cost: complaint management |
|
||||
| complaindelall | 25 | High-cost: complaint management |
|
||||
| complaindel | 5 | Low-cost operation |
|
||||
| complainlist | 25 | High-cost: complaint management |
|
||||
| ftcreatedir | 5 | Low-cost: file operation |
|
||||
| ftdeletefile | 5 | Low-cost: file operation |
|
||||
| ftgetfileinfo | 5 | Low-cost: file operation |
|
||||
| ftgetfilelist | 0 | Zero-cost: file listing |
|
||||
| ftinitdownload | 0 | Zero-cost: file transfer init |
|
||||
| ftinitupload | 0 | Zero-cost: file transfer init |
|
||||
| ftrenamefile | 5 | Low-cost: file operation |
|
||||
| gm | 25 | High-cost: server message |
|
||||
| logadd | 10 | Medium-cost: logging |
|
||||
| logview | 50 | Very high-cost: log access |
|
||||
| messageadd | 25 | High-cost: offline message |
|
||||
| messagedel | 5 | Low-cost: offline message |
|
||||
| messageget | 5 | Low-cost: offline message |
|
||||
| messagelist | 5 | Low-cost: offline message |
|
||||
| messageupdateflag | 5 | Low-cost: offline message |
|
||||
| permfind | 5 | Low-cost: permission operation |
|
||||
| permget | 5 | Low-cost: permission operation |
|
||||
| permlist | 5 | If not cached; Low-cost: permission operation |
|
||||
| privilegekeyadd | 25 | High-cost: privilege key management |
|
||||
| privilegekeydelete | 25 | High-cost: privilege key management |
|
||||
| privilegekeylist | 5 | Low-cost: privilege key listing |
|
||||
| privilegekeyuse | 15 | Medium-cost: privilege key use |
|
||||
| sendtextmessage | 15 | Same as textmessagesend |
|
||||
| servergroupaddclient | 25 | High-cost: group management |
|
||||
| servergroupaddperm | 5 | Low-cost: group permission |
|
||||
| servergroupclientlist | 5 | Low-cost: group listing |
|
||||
| servergroupdelclient | 25 | High-cost: group management |
|
||||
| servergroupdelperm | 5 | Low-cost: group permission |
|
||||
| servergrouplist | 5 | Low-cost: group listing |
|
||||
| servergrouppermlist | 5 | Low-cost: group permission listing |
|
||||
| setclientchannelgroup | 25 | High-cost: group management |
|
||||
| tokenadd | 15 | Medium-cost: token management |
|
||||
| tokendelete | 15 | Medium-cost: token management |
|
||||
| tokenlist | 5 | Low-cost: token listing |
|
||||
| tokenuse | 15 | Medium-cost: token use |
|
||||
| whoami | 5 | Low-cost operation |
|
||||
|
||||
**Connection sequence point costs:**
|
||||
|
||||
| Step | Points | Notes |
|
||||
|---|---|---|
|
||||
| Connection establishment | 80 | Respects `b_client_ignore_antiflood` |
|
||||
| Default channel join | 10 | [to be completed] |
|
||||
| Set badges | 15 | [to be completed] |
|
||||
| permissionlist | 5 | If not cached; [to be completed] |
|
||||
| clientgetvariables | 0 | [to be completed] |
|
||||
| Subscribe channels | 15-20 | [to be completed] |
|
||||
| Default channel join | 10 | Medium-cost: channel move |
|
||||
| Set badges | 15 | Medium-cost: client edit |
|
||||
| permissionlist | 5 | If not cached; Low-cost: permission listing |
|
||||
| clientgetvariables | 0 | Zero-cost: variable retrieval |
|
||||
| Subscribe channels | 15-20 | Medium-cost: per-channel subscribe |
|
||||
|
||||
### 5.3 Connection Flow
|
||||
|
||||
|
||||
Generated
+321
@@ -0,0 +1,321 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "audio-test"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "audio-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "audio-test"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
@@ -0,0 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "audio-test", about = "Audio DSP pipeline benchmark")]
|
||||
struct Args {
|
||||
/// Sample rate
|
||||
#[arg(long, default_value = "48000")]
|
||||
sample_rate: u32,
|
||||
/// Block size in samples
|
||||
#[arg(long, default_value = "480")]
|
||||
block_size: usize,
|
||||
/// Number of iterations
|
||||
#[arg(long, default_value = "10000")]
|
||||
iterations: u32,
|
||||
/// Test frequency Hz
|
||||
#[arg(long, default_value = "1000")]
|
||||
frequency: f32,
|
||||
}
|
||||
|
||||
fn generate_block(sample_rate: u32, frequency: f32, size: usize) -> Vec<f32> {
|
||||
(0..size)
|
||||
.map(|i| {
|
||||
let t = i as f32 / sample_rate as f32;
|
||||
(2.0 * std::f32::consts::PI * frequency * t).sin() * 0.5
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
|
||||
println!("Audio Test Tool");
|
||||
println!("Sample rate: {}Hz, Block: {} samples, Iterations: {}",
|
||||
args.sample_rate, args.block_size, args.iterations);
|
||||
|
||||
let block = generate_block(args.sample_rate, args.frequency, args.block_size);
|
||||
|
||||
// Benchmark: copy + process simulation
|
||||
let start = Instant::now();
|
||||
for _ in 0..args.iterations {
|
||||
let mut output = block.clone();
|
||||
for sample in output.iter_mut() {
|
||||
*sample *= 0.95; // simple gain
|
||||
}
|
||||
std::hint::black_box(&output);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("Total: {:?}", elapsed);
|
||||
println!("Per block: {:?}", elapsed / args.iterations);
|
||||
println!("Blocks/sec: {:.0}", args.iterations as f64 / elapsed.as_secs_f64());
|
||||
println!("Realtime factor: {:.1}x",
|
||||
(args.iterations as f64 * args.block_size as f64 / args.sample_rate as f64) / elapsed.as_secs_f64());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Generated
+3735
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "protocol-probe"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "protocol-probe"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chanora_protocol = { path = "../../crates/chanora_protocol" }
|
||||
chanora_resolver = { path = "../../crates/chanora_resolver" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,105 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use chanora_protocol::{ConnectConfig, ProtocolClient};
|
||||
use chanora_resolver::ChanoraResolver;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "protocol-probe", about = "Probe a TeamSpeak server for capabilities and compatibility")]
|
||||
struct Args {
|
||||
/// Server address (hostname:port or ts3server:// URL)
|
||||
server: String,
|
||||
|
||||
/// Nickname to use
|
||||
#[arg(short, long, default_value = "Probe")]
|
||||
nickname: String,
|
||||
|
||||
/// Server password (optional)
|
||||
#[arg(short, long)]
|
||||
password: Option<String>,
|
||||
|
||||
/// Verbose output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
|
||||
println!("Protocol Probe Tool");
|
||||
println!("===================");
|
||||
println!("Server: {}", args.server);
|
||||
println!();
|
||||
|
||||
// Step 1: Resolve address
|
||||
println!("1. Resolving address...");
|
||||
let resolver = ChanoraResolver::new()?;
|
||||
match resolver.resolve_client_address(&args.server).await {
|
||||
Ok(address) => {
|
||||
println!(" Resolved: {}", address);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Resolution failed: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Connect
|
||||
println!("2. Connecting...");
|
||||
let cfg = ConnectConfig {
|
||||
address: args.server.clone(),
|
||||
nickname: args.nickname.clone(),
|
||||
password: args.password.clone(),
|
||||
..ConnectConfig::default()
|
||||
};
|
||||
|
||||
let client = match ProtocolClient::connect(cfg).await {
|
||||
Ok(c) => {
|
||||
println!(" Connected successfully");
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Connection failed: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3: Collect server info
|
||||
println!("3. Server Info:");
|
||||
match client.snapshot().await {
|
||||
Ok(snap) => {
|
||||
println!(" Name: {}", snap.server_name);
|
||||
println!(" Platform: {}", snap.platform);
|
||||
println!(" Version: {}", snap.version);
|
||||
println!(" Channels: {}", snap.channels.len());
|
||||
println!(" Clients: {}", snap.clients.len());
|
||||
println!(" Welcome: {}", snap.welcome_message);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Snapshot failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Client info
|
||||
println!("4. Client Info:");
|
||||
match client.client_profile(0).await {
|
||||
Ok(profile) => {
|
||||
println!(" ID: {:?}", profile.database_id);
|
||||
println!(" Name: {}", profile.name);
|
||||
println!(" UID: {}", profile.unique_id);
|
||||
println!(" Server groups: {:?}", profile.server_groups);
|
||||
println!(" Channel: {:?}", profile.channel);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Profile failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Disconnect
|
||||
println!("5. Disconnecting...");
|
||||
client.disconnect().await;
|
||||
println!(" Done");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user