Commit Graph
17 Commits
Author SHA1 Message Date
mollusk bcb597a0ea Add inline chat audio player 2026-06-21 01:05:24 -04:00
molluskandClaude Opus 4.8 79b24fd567 deps: add rodio for inline chat audio playback (senior-vetted)
Pre-stages the playback dependency for the inline chat audio player task so
Codex can build it in its network-off sandbox.

rodio 0.22.2 decodes wav/mp3/ogg(vorbis)/flac (via bundled symphonia) and
handles output + play/pause/seek + resampling. It brings its own cpal 0.17
(the project's PipeWire/cpal-0.15 call path is untouched; rodio's output is a
separate stream on the system default device) and alsa on Linux.

Supply chain: cargo audit reports NO new advisories from this subtree -- the
only 2 warnings (audiopus_sys, paste) are pre-existing, unmaintained-only, and
already on the allow-list. Builds clean (release).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:52:55 -04:00
molluskandClaude Opus 4.8 02cb46550e Chat file attachments, stage 1: protocol + data model + pure seams
First slice of in-chat file/photo sharing (dedicated file plane, images
inline + file chips, session-only). This stage adds the wire types and
the pure, unit-tested logic; no transport or UI yet.

- protocol: new FILES_ALPN / FILES_PROTO (peerspeak/files/1) for the
  dedicated file-transfer plane. Bump GOSSIP_PROTO 1->2 + sig domain v2
  (Chat gained an attachment field, so cross-version peers fail fast
  rather than half-work) and Cargo 0.2.0 -> 0.3.0 per VERSIONING.md.
  BREAKING wire change: all peers must run >= 0.3.0.
- new src/files.rs: ChatAttachment descriptor (name/size/kind/id; bytes
  travel off-gossip), AttachmentKind, plus pure seams — sanitize_filename
  (path-traversal/control-char/length-safe), size_within_cap, image
  magic-byte sniffing + defensive limited decode (decode-bomb guard),
  32-byte request parsing, human_size. 13 unit tests.
- GossipMessage::Chat and RoomEvent::ChatMessage carry an optional
  ChatAttachment; send_chat takes Option<ChatAttachment>. Untrusted
  inbound descriptors are filename-sanitized + size-validated on ingest.
  serde(default) keeps the field forward-compatible at the JSON layer;
  +round-trip and pre-v2 back-compat tests.

The attachment id is a random 32-byte handle (rand, already a dep), not
a content hash — the fetch is authenticated + encrypted + member-gated,
so no crypto-hash dep is needed.

349 lib tests pass, clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:00:51 -04:00
molluskandClaude Opus 4.8 ddb3d2aabc Windows compat quick wins: echo-cancel UI gate, pixelpass .exe, cfg tighten (W5/W6/W9)
Three Windows-compatibility fixes from the Codex review. Implemented by Codex
(gpt-5.5); reviewed and committed by Claude.

W5 — echo cancellation is a Linux/PipeWire feature, but the toggle was shown and
live on Windows, so a Windows join tried `pactl` and errored before falling back.
Now `#[cfg(target_os = "linux")]` gates the core enable path (and the
ActiveSession guard field); on other targets the Settings + in-call controls
render as a disabled checkbox with a "not available on Windows yet" note.

W6 — pixelpass PATH lookup only tried `pixelpass`; on Windows it now also tries
`pixelpass.exe` via a cfg-selected candidate list (+ unit test).

W9 — the Linux audio stack (pipewire/pw_cli/echo_cancel/audio_probe + the
`PlatformAudioBackend` alias and device-enum re-export) was gated `cfg(unix)`;
tightened to `cfg(target_os = "linux")` so a hypothetical macOS build won't try
to compile PipeWire. cpal stays `cfg(windows)`. Genuinely-Unix file/key
permission code in lib.rs/identity.rs left as `cfg(unix)`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 04:03:49 -04:00
molluskandClaude Opus 4.8 47c58047ce Windows port Phase 1: real cpal/WASAPI audio backend
windows-build / windows-build (push) Has been cancelled
cargo-deny / cargo-deny (pull_request) Has been cancelled
windows-build / windows-build (pull_request) Has been cancelled
Replace the Phase 0 no-op CpalBackend stub with a working cpal backend
(WASAPI on Windows), preserving the exact PipeWire AudioBackend contract
so the mixer/encoder/jitter pipeline is unchanged.

- Capture: input stream -> downmix to mono -> 960-sample (20ms) i16 frames
  -> tx, matching the encoder/jitter frame size.
- Playback: 200ms stereo ring prefilled to PLAYBACK_TARGET_SAMPLES; the
  output callback drains it (silence on underrun) while the owning thread
  feeds it from rx. ring_fill is the exact delta-maintained occupancy
  counter (fetch_add on push, fetch_sub on pop), preserving the clock-paced
  production design (not ringbuf's stale occupied_len).
- cpal::Stream is !Send, but AudioBackend is Send+Sync and shared via Arc,
  so each stream lives on its own owning thread (built/played/dropped
  there); the struct holds only the running flag + JoinHandle. stop()
  flips the flag and joins.
- Generic over F32/I16/U16 sample formats; device selected by name else
  default; requires a native 48kHz config (clear error otherwise, no
  resampling yet). Mirrors the PipeWire drain_loop and playout-health line.
- Cargo.toml: add cpal 0.15 under cfg(windows).

Verified by temporarily compiling cpal_impl against real cpal on Linux/ALSA:
build + clippy clean, 6/6 cpal_impl unit tests pass. Reverted to windows-only
gating; shipped Linux state green (316/316). Runtime/WASAPI end-to-end is
unverified and pending a Windows host (plan M2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:15:56 -04:00
molluskandClaude Opus 4.8 85b12a26c9 Windows port Phase 0: platform-select the audio backend
windows-build / windows-build (push) Has been cancelled
cargo-deny / cargo-deny (pull_request) Has been cancelled
windows-build / windows-build (pull_request) Has been cancelled
Make the tree compile for Windows without touching core logic, by
confining all Linux/PipeWire assumptions behind cfg gates and a single
platform-selected backend alias. No new dependencies — the cpal/WASAPI
backend lands in Phase 1; this ships a no-op stub.

- Cargo.toml: move pipewire + rfd(xdg-portal) under cfg(unix); add a
  cfg(windows) rfd using the Win32 dialog backend.
- audio: gate pipewire_impl to unix, add a cpal_impl stub for windows,
  and select between them via the new PlatformAudioBackend alias.
- core: use PlatformAudioBackend instead of the concrete PipeWireBackend.
- lib: gate the unix-only 0o600 log-file mode code (+ its test); Windows
  logs inherit the directory ACL.
- audio_probe: gate this PipeWire diagnostic to unix with a stub main.
- app: open URLs via rundll32 on windows, xdg-open on unix (shell-free).
- ci: add .gitea/workflows/windows-build.yml (M1) — build + lib tests for
  x86_64-pc-windows-msvc, with CMAKE_POLICY_VERSION_MINIMUM=3.5 for the
  vendored libopus build. Needs a windows act_runner to actually run.

Linux build/clippy/tests green (316/316). The Windows path is verified by
inspection only (no local Windows toolchain); CI is the real gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:47:13 -04:00
molluskandClaude Opus 4.8 465c7ba2b0 Add cargo-deny supply-chain policy (deny.toml)
Supersede bare cargo-audit with an enforceable four-part policy, validated
against the current tree with cargo-deny 0.19.9 (advisories/bans/licenses/
sources all pass):

- advisories: deny vulnerabilities + yanked; ignore the two *unmaintained*
  warnings (paste RUSTSEC-2024-0436, audiopus_sys RUSTSEC-2026-0150) with
  rationale. Both are transitive and pinned via Cargo.lock, so a future
  malicious release can't reach us until a deliberate cargo update.
- sources: trust only crates.io; deny unknown registries and git sources
  (core anti-hijack control).
- bans: deny wildcard version reqs; warn on duplicate versions.
- licenses: permissive allow-list covering the current graph.

Mark peerspeak publish = false (it's an application, not a published
library): blocks accidental cargo publish and lets [licenses.private]
skip the missing-license check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:46:52 -04:00
molluskandClaude Opus 4.8 3ec09de87e Adopt versioning standard + migrate to versioned protocol planes (0.2.0)
Establishes VERSIONING.md: SemVer 0.x (MINOR = breaking wire change) for the
release version, and per-plane protocol versions enforced on the wire so
incompatible peers fail fast and legibly instead of via silent decode/signature
errors.

⚠️ BREAKING WIRE CHANGE — all peers must run >= 0.2.0 to interoperate (ALPNs and
gossip subscription topics changed). A pre-0.2.0 peer (e.g. an un-resynced
dopedart) can no longer connect, by design, and now fails at the handshake.

- New src/protocol.rs: single source of truth for AUDIO/FRIENDS/GOSSIP_PROTO,
  the derived ALPNs (peerspeak/audio/1, peerspeak/friends/1), GOSSIP_SIG_DOMAIN,
  and versioned_topic(). Unit tests assert ALPN/domain strings match their
  integer versions (no silent drift) + that topic namespacing is deterministic.
- Unified ALPNs: audio was b"peerspeak-audio" (unversioned, and duplicated in
  iroh_impl.rs + core/mod.rs) -> peerspeak/audio/1 from protocol.rs; friends
  re-exports protocol::FRIENDS_ALPN (was peerspeak/friends/0 -> /1).
- Gossip: subscribe to versioned_topic(ticket.topic_id) so different gossip
  versions never share a swarm; the raw topic_id stays the room identity and
  what signatures bind. GOSSIP_SIG_DOMAIN centralized into protocol.rs.
- Cargo.toml 0.1.0 -> 0.2.0.

316 lib tests / clippy --all-targets clean. VERSIONING.md documents the bump
rules, the "I changed X -> what do I bump" table, and a release checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 05:20:27 -04:00
molluskandClaude Opus 4.8 f029a30ea7 feat(avatars): custom image upload — W4 Phase 3
Completes W4: users can upload a custom avatar image.

- `Avatar::Custom(String)` carries a base64 PNG. `process_upload` decodes an
  arbitrary png/jpeg, downscales so the longest side is 128px (aspect kept),
  re-encodes PNG, base64s, and rejects anything over a hard cap.
- Settings "Avatar" gains an "Upload image…" button (native picker via rfd's
  xdg-portal backend, off-thread through Task::perform) and shows the current
  custom avatar as a selected tile.
- Untrusted peer avatars are validated at gossip ingest (`sanitize_incoming`):
  a custom image must be within the byte cap and decode as a PNG within bounds
  (image-crate decode limits guard against decompression bombs) or it's
  downgraded to a monogram.
- Raised the gossip max message size to 64 KB so a capped custom avatar fits
  inline on the presence plane (all peers already need a matching build).
- Deps: image (png/jpeg only), rfd (xdg-portal, no GTK); only `rfd`+`pollster`
  are actually new in the lockfile (rest were already transitive). cargo audit
  clean (0 vulns; the 2 unmaintained warnings are pre-existing S7).
- `Controller::send` now returns bool (was a Result carrying the now-larger
  CoreCommand by value, which tripped result_large_err).
- +5 avatar unit tests (upload resize/round-trip, reject non-image, ingest
  accept/reject). 227 lib tests green, clippy clean.

Manual check: Settings → Avatar → Upload; confirm the picker opens and the
image shows for you and (after redeploy) for a peer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:00:18 -04:00
molluskandClaude Opus 4.8 3007002b1b feat(avatars): 6 selectable preset avatars over presence — W4 Phase 2
Adds bundled preset avatars on top of the monogram foundation.

- New `Avatar` enum (Monogram | Preset(u8)) in the avatar module, serde-encoded;
  6 preset PNGs embedded via include_bytes! (placeholder art in assets/avatars/
  — swap for real designs later). +3 unit tests.
- `PeerState` gains `avatar` (rides the gossip presence plane like `sharing`);
  `AppConfig` gains `avatar` (persisted). Avatar choice flows app → core (Join +
  new SetAvatar command) → every self-state announce, so it reaches the room incl.
  late joiners, and changing it mid-call re-announces live.
- Settings "Avatar" section: monogram + 6 preset tiles, applied live + persisted.
- Rendering: `avatar_view` draws the chosen preset image (iced `image` feature,
  now enabled) else the monogram, in the self card, peer rows, and chat (chat
  looks up the sender's avatar from presence by id).

⚠️ BREAKING gossip wire change: presence Announce is signed (S2) and the
signature is recomputed by re-serializing the parsed struct, so a new PeerState
field means old and new builds can't verify each other's presence — ALL peers
must run a build >= this one (same as the S2 change). Redeploy dopedart before
2-machine testing.

Build + clippy clean, 222 lib + integration tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:43:24 -04:00
molluskandClaude Opus 4.8 aa94e861b1 feat(dsp): spectrogram + AEC measurement toolkit (specview)
Add a dependency-free signal-analysis toolkit and a `specview` dev CLI to
evaluate audio (especially echo cancellation) with objective numbers and a
terminal spectrogram instead of ear alone.

- src/dsp/: hand-written radix-2 FFT, Hann window, STFT, seeded test-signal
  generators (sine/log-sweep/white/pink/impulse), metrics (RMS/dBFS/peak/ERLE/
  per-band energy), minimal WAV read+write, and a 24-bit-ANSI half-block
  spectrogram renderer (magma colormap, freq/time axes, dB legend, ASCII
  fallback). Pure layers have no I/O; only `wav` touches the filesystem.
- src/bin/specview.rs: `gen` (conjure a test signal -> WAV), `show` (spectrogram
  + per-band energy summary), `erle` (broadband + per-band echo-return-loss
  between a before/after pair).
- 27 unit tests (FFT correctness, ERLE landmarks, WAV round-trip, render shape).
  Verified end-to-end: log sweep renders as the expected exponential curve;
  a 20 dB-quieter copy reads +20.0 dB ERLE broadband and per band.

Measurement substrate for upcoming AEC refinement (no shipped-path changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 02:25:58 -04:00
molluskandClaude Opus 4.8 5bd32250a5 fix(audio): honor per-cycle quantum in playback (A1, crackle on non-1024 hw)
The playback RT callback pinned the PipeWire buffer to exactly one 1024-frame
quantum (the 2026-05-31 crackle fix). That is only correct when the machine's
clock.quantum is 1024 — on hardware running quantum 512 or 2048 the pinned
slice mismatches the device's per-cycle demand and the crackle returns. We just
shipped a release to a friend whose quantum is unknown, so this was P1.

Fix: enable the pipewire `v0_3_49` feature (exposes Buffer::requested(), the
graph's per-cycle quantum) and fill exactly that many frames each callback via a
new pure `frames_to_produce()` seam, with a safe ≤1024 fallback when the graph
reports 0 (never the whole slice — over-pulling past the ring depth is the
original crackle). Relax the Buffers size pin from a hard 1024 to a generous
8192-frame max so the mapped slice fits any plausible quantum; requested(), not
the buffer size, now governs per-cycle output.

Verified locally with `pw-metadata clock.force-quantum` + audio_probe at forced
quanta 512/1024/2048: each shows `underrun +0` steady, `quantum=` matching the
forced value, and callbacks/s ≈ rate/quantum — proving requested() is live (the
health line would otherwise read the 1024 fallback). +4 unit tests on
frames_to_produce (148 lib tests, clippy --all-targets clean).

Still pending (field test): one real desktop<->dopedart call through the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:49:53 -04:00
molluskandClaude Opus 4.8 b4d0f2db7b feat(audio): live mic meter with draggable noise gate
Adds a mic input meter to Settings for gate calibration, replacing the
blind noise-gate slider with a unified Discord/OBS-style control: the bar
shows the live mic level and a draggable handle sets the gate threshold on
the same axis. Fill is green above the gate (transmitting), dim below it
(muted), with a live status word; the handle is bright red with a dark
edge so it stays legible when the green level sweeps past it.

Two level sources:
- In-call: the capture thread peak-holds the raw (pre-gate, pre-mute)
  frame level and emits UiEvent::MicLevel ~10/sec.
- Off-call: a "Test mic" toggle runs CoreCommand::SetMicMonitor, spinning
  up a standalone capture-only stream feeding run_mic_monitor. It shares
  the backend's single capture stream, so Join tears it down first and
  leaving Settings releases it; ignored while a session is active.

The gate handle drags live via NoiseGateDragging (no disk write per pixel)
and persists once on release via NoiseGateChanged. Meter axis is 0..0.3 so
a normal voice doesn't peg. Enables the iced "canvas" feature for the
custom GateMeter widget.

Build + clippy clean, tests pass. Field-verified on desktop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:46:38 -04:00
molluskandClaude Opus 4.8 7af0235736 chore: senior-review cleanup pass
- Remove Gemini's committed update_*.py regex-surgery scripts
- Drop unused iroh-tickets dependency (hand-rolled ticket is used instead)
- Replace ToString antipattern with Display impl on PeerSpeakTicket
- Route debug log to XDG state/cache dir instead of hardcoded /home path
- Clear all compiler + clippy warnings (unused imports, collapsible ifs,
  redundant pattern matching, missing Default)

Builds clean with zero warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:36:55 -04:00
mollusk dac53fc2ad feat: Add persistent configuration and enumerate PipeWire audio devices 2026-05-27 16:03:07 -04:00
mollusk 5ddb792f0f Fix core Tokio runtime panic and add network logging
- Fix tokio runtime panic by spawning a dedicated Tokio runtime thread in CoreController.
- Add central log_msg utility in src/lib.rs for debugging.
- Add instrumentation/logs to join, leave, and gossip events in src/network/gossip.rs.
- Add test_net.rs bin for testing gossip loopback sync.
- Use std::sync::Mutex in IrohGossipState to resolve Tokio block-in-async panics.
2026-05-27 05:48:04 -04:00
mollusk 1220d94e91 Initialize project and implement decentralized voice chat client (PipeWire, Opus, Iroh, Iced) 2026-05-27 05:18:56 -04:00