4 Commits
Author SHA1 Message Date
molluskandClaude Opus 4.8 8014edf91c Release 0.6.2 + AppImage packaging
CI / check (push) Failing after 3m41s
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
Bump to 0.6.2 (Cargo + Inno .iss), promote CHANGELOG [Unreleased] -> [0.6.2].
0.6.2 rolls up the licensing (MIT + THIRD_PARTY_LICENSES) and the friends-list
liveness fixes (active offline marking, 15s refresh, manual Rescan) on the
0.6.x wire format (gossip v5, compatible with 0.6.0/0.6.1).

Adds packaging/appimage: a thin AppImage recipe (linuxdeploy) that bundles the
pixelpass screen-share helper in usr/bin so peerspeak's $PATH lookup finds it
with no code change. Assets are include_bytes!-embedded; the graphics stack and
pixelpass's gstreamer/mpv tools are left to the host. Built on Ubuntu 24.04
(glibc 2.39) for reach across Debian 13+/Fedora 40+/rolling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:07:41 -04:00
molluskandClaude Opus 4.8 89d5218d25 Housekeeping: Windows doc refresh + scan.rs test-import cleanup
Codex-authored refresh of docs/WINDOWS.md and packaging/windows/{README,INSTALL}.md
from the 2026-07-01 Windows session; scan.rs qualifies super::running_executables()
to drop an unused glob import. PKGBUILD pkgver reflects the last Arch build
(auto-regenerated by makepkg's pkgver()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:59:26 -04:00
mollusk f3c7aa7050 Merge A6 playback handoff fix 2026-07-01 13:50:44 -04:00
mollusk 39b5dafd57 fix(audio): bound playback handoff queue 2026-07-01 13:39:10 -04:00
15 changed files with 319 additions and 59 deletions
+3 -1
View File
@@ -2,7 +2,7 @@
All notable changes to PeerSpeak are documented here. All notable changes to PeerSpeak are documented here.
## [Unreleased] ## [0.6.2] — 2026-07-03
### Fixed ### Fixed
- **Friends list now reflects status changes without a restart.** A presence probe that fails now actively marks the friend **offline**, so a friend who goes offline, leaves a room, or turns invisible no longer lingers showing a stale "online" / "in a room" status until PeerSpeak is relaunched. Previously only successful probes updated the list, so it could ratchet a friend's status up but never down. The auto-refresh interval was also shortened from 60s to **15s** so the list tracks changes more closely. - **Friends list now reflects status changes without a restart.** A presence probe that fails now actively marks the friend **offline**, so a friend who goes offline, leaves a room, or turns invisible no longer lingers showing a stale "online" / "in a room" status until PeerSpeak is relaunched. Previously only successful probes updated the list, so it could ratchet a friend's status up but never down. The auto-refresh interval was also shortened from 60s to **15s** so the list tracks changes more closely.
@@ -13,6 +13,8 @@ All notable changes to PeerSpeak are documented here.
### Licensing ### Licensing
- **PeerSpeak is now released under the MIT License** (previously an unlicensed private build). Added a `LICENSE` file and a `THIRD_PARTY_LICENSES` file enumerating the full dependency manifest plus the canonical text of every referenced license, with notices for the statically bundled Opus codec and the embedded fonts (Iced-Icons, Cantarell/OFL-1.1). Both files ship in the Arch and Debian packages. - **PeerSpeak is now released under the MIT License** (previously an unlicensed private build). Added a `LICENSE` file and a `THIRD_PARTY_LICENSES` file enumerating the full dependency manifest plus the canonical text of every referenced license, with notices for the statically bundled Opus codec and the embedded fonts (Iced-Icons, Cantarell/OFL-1.1). Both files ship in the Arch and Debian packages.
[0.6.2]: https://gitbutter.xyz/mollusk/peerspeak/releases/tag/v0.6.2
## [0.6.0] — 2026-06-28 ## [0.6.0] — 2026-06-28
### Added ### Added
Generated
+1 -1
View File
@@ -4871,7 +4871,7 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]] [[package]]
name = "peerspeak" name = "peerspeak"
version = "0.6.1" version = "0.6.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "peerspeak" name = "peerspeak"
version = "0.6.1" version = "0.6.2"
edition = "2024" edition = "2024"
description = "Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)" description = "Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
license = "MIT" license = "MIT"
+3 -3
View File
@@ -2,11 +2,11 @@ use std::process::Command;
fn main() { fn main() {
println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/HEAD");
if let Ok(head) = std::fs::read_to_string(".git/HEAD") { if let Ok(head) = std::fs::read_to_string(".git/HEAD")
if let Some(reference) = head.strip_prefix("ref: ") { && let Some(reference) = head.strip_prefix("ref: ")
{
println!("cargo:rerun-if-changed=.git/{}", reference.trim()); println!("cargo:rerun-if-changed=.git/{}", reference.trim());
} }
}
let short = Command::new("git") let short = Command::new("git")
.args(["rev-parse", "--short=8", "HEAD"]) .args(["rev-parse", "--short=8", "HEAD"])
+65 -39
View File
@@ -1,19 +1,28 @@
# PeerSpeak on Windows # PeerSpeak on Windows
Current status: the Windows port cross-compiles to `x86_64-pc-windows-gnu` and the `.exe` Current status: PeerSpeak cross-compiles to `x86_64-pc-windows-gnu` from Linux and
launches under Wine. A real Windows/WASAPI host is still needed for the final audio-device has passed an older native Windows 11 VM smoke test for launch, GUI render, call
checks listed below. join, and audio flow. The build environment is **not** the Windows VM; current
Windows binaries are built from Linux, normally inside the `peerspeak-win`
distrobox or with the same GNU target environment.
The Windows runtime still trails Linux in a few important areas. See the Claude
handoff file `windows-parity-audit.md` for the full audit and task breakdown.
## What works today ## What works today
| Area | Status | | Area | Status |
|---|---| |---|---|
| GUI | Iced/wgpu builds and renders under Wine. | | GUI | Iced/wgpu builds for Windows and rendered in the Windows 11 VM. |
| Networking | Iroh QUIC transport and gossip compile on Windows. | | Networking | Iroh QUIC transport and gossip compile on Windows; VM call reached two peers. |
| Audio backend | `cpal` drives WASAPI capture/playback behind `AudioBackend`. | | Audio backend | `cpal` drives WASAPI capture/playback behind `AudioBackend`. |
| Device selection | cpal enumerates input/output devices; see caveat below about stable IDs. |
| Resampling/remap | WASAPI devices can run non-48 kHz formats; PeerSpeak converts at the backend boundary. |
| Codec | Opus remains 48 kHz mono, 20 ms frames. | | Codec | Opus remains 48 kHz mono, 20 ms frames. |
| Identity | `ring` identity generation/load is platform-neutral. | | Identity/config | Stored through `dirs` under the Windows profile. |
| Chimes | Windows uses PowerShell `System.Media.SoundPlayer` for WAV playback. | | Chimes | Windows uses PowerShell `System.Media.SoundPlayer` for WAV playback. |
| Game detection | Steam registry `RunningAppID` plus Toolhelp process-scan fallback compile on Windows. |
| File dialogs | `rfd` uses the native Win32 dialog backend. |
Windows paths are resolved through `dirs`: Windows paths are resolved through `dirs`:
@@ -23,55 +32,72 @@ Windows paths are resolved through `dirs`:
## Building ## Building
### Native Windows ### Cross-compile from Linux
Install MSVC Build Tools and CMake, then build normally: Preferred local path:
```powershell ```sh
cargo build --release distrobox enter peerspeak-win -- bash -lc '
cd ~/git/butter/peerspeak &&
RUSTC_BOOTSTRAP=1 ./win-cross-build.sh -Z build-std=std,panic_abort
'
``` ```
If CMake is 4.x or newer, the vendored `opus`/`libopus` build may need: Equivalent direct command when the host has the GNU target, MinGW, `rust-src`, and
CMake available:
```sh
CMAKE_POLICY_VERSION_MINIMUM=3.5 RUSTC_BOOTSTRAP=1 \
cargo build --release --target x86_64-pc-windows-gnu --bin peerspeak \
-Z build-std=std,panic_abort
```
`CMAKE_POLICY_VERSION_MINIMUM=3.5` is required with host CMake 4.x because the
vendored Opus build used by `audiopus_sys` still declares an old minimum CMake
version. Without that env var, the Windows build/check fails during Opus configure.
### Native Windows
A native MSVC build is not the active development path. If used, install MSVC Build
Tools and CMake, then build normally:
```powershell ```powershell
$env:CMAKE_POLICY_VERSION_MINIMUM = "3.5" $env:CMAKE_POLICY_VERSION_MINIMUM = "3.5"
cargo build --release cargo build --release
``` ```
### Cross-compile from Linux
The current dev path cross-compiles from an Arch environment to the GNU Windows target:
```sh
rustup target add x86_64-pc-windows-gnu
sudo pacman -S mingw-w64-gcc cmake
CMAKE_POLICY_VERSION_MINIMUM=3.5 cargo build --release --target x86_64-pc-windows-gnu --bin peerspeak
```
Wine is useful for launch/render smoke tests, but it is not a substitute for a real
Windows audio-device pass. The deeper migration plan (phases, decisions, the opus build
spike) lives in the maintainer's handoff docs, outside the repo.
## First run and networking ## First run and networking
Expect a Windows Firewall prompt the first time the app opens network sockets. Allow it: Expect a Windows Firewall prompt the first time the app opens network sockets, or
PeerSpeak uses UDP for QUIC, plus relay traffic when direct NAT traversal is not available. use the Inno installer option that pre-adds a firewall allow rule. PeerSpeak uses
UDP for QUIC plus relay traffic when direct NAT traversal is unavailable.
The default network mode keeps the n0 relay available for NAT traversal without publishing The default network mode keeps the n0 relay available for NAT traversal without
presence to n0 DNS. Direct peer-to-peer paths may work when both networks allow them; relayed publishing presence to n0 DNS. Relayed connections are expected and valid.
connections are expected and valid.
## Known gaps ## Known gaps
| Item | Status | | Item | Status |
|---|---| |---|---|
| Echo cancellation | Linux-only PipeWire feature. The Windows UI shows it disabled as unavailable. | | Echo cancellation | Linux-only today. The Windows UI shows it disabled as unavailable. |
| Screen share | Requires a Windows `pixelpass.exe` on `PATH` or a configured override. | | Screen share | Blocked by PixelPass, which is currently Linux-only in practice. PeerSpeak can spawn `pixelpass.exe`, but there is no Windows PixelPass host/viewer parity yet. |
| Chimes | Now routed through Windows `SoundPlayer`; needs a real Windows host to audibly verify. | | Device persistence | Uses cpal friendly names as keys. These can duplicate or change across Windows driver/profile changes; stable WASAPI endpoint IDs are still needed. |
| Resampling/device format | Cross-compiled. cpal/WASAPI now chooses native 48 kHz when available and otherwise resamples/remaps at the device boundary; needs real Windows hardware audio verification. | | Release hygiene | Keep `.iss` and installer output in sync with `Cargo.toml`; rebuild Windows artifacts during each release. |
| Device persistence | Open. WASAPI friendly names may duplicate or change across driver/profile changes. | | Runtime coverage | The Windows VM smoke test proved an older tester build. Current `main` needs a fresh VM smoke matrix before calling parity current. |
| Playback pacing | Cross-compiled. The fixed playback target under WASAPI shared mode still needs real-hardware verification with `audio_probe`. |
Before calling Windows support done, verify a real Windows machine can create/join a room, ## Current smoke checklist
capture mic audio, hear remote audio, select devices, restart with selections preserved, and
play notification chimes. Before calling a Windows build current, verify on the Windows VM or real Windows
hardware:
- Launch current `peerspeak.exe`; GUI renders and settings open.
- Run `audio_probe.exe 440 30`; listen for glitches and inspect `playout-health`.
- Create/join a Linux <-> Windows room; confirm mic and playback both directions.
- Select input/output devices, restart, and confirm selections persist or fall back clearly.
- Play chimes and custom chime paths.
- Send chat, image/file attachments, and save an attachment through the native dialog.
- Import/play/share/listen to music from Windows file paths.
- Record mixed/stems/both and inspect the WAV output path.
- Exercise friends/presence/recents and the clock-skew banner.
- Test Steam and non-Steam game detection on a real Windows Steam install.
- Install/upgrade/uninstall through the Inno installer, including firewall rule cleanup.
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: mollusk <jitty+lc1iz0dc@protonmail.com> # Maintainer: mollusk <jitty+lc1iz0dc@protonmail.com>
pkgname=peerspeak-git pkgname=peerspeak-git
_pkgname=peerspeak _pkgname=peerspeak
pkgver=0.6.1.r310.g660261a pkgver=0.6.1.r315.ga78860d
pkgrel=1 pkgrel=1
pkgdesc="Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)" pkgdesc="Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
arch=('x86_64') arch=('x86_64')
+4
View File
@@ -0,0 +1,4 @@
.tools/
AppDir/
*.AppImage
squashfs-root/
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# AppRun for the PeerSpeak AppImage.
#
# PeerSpeak bundles the pixelpass screen-share helper in usr/bin. We prepend our
# own usr/bin to PATH so peerspeak's $PATH lookup for `pixelpass` finds the
# bundled copy, while the host's tools (gst-launch-1.0, pactl, mpv — which
# pixelpass in turn shells out to) remain reachable via the appended host PATH.
# That no-sandbox spawning is exactly why this app suits AppImage over Flatpak.
HERE="$(dirname "$(readlink -f "$0")")"
export PATH="$HERE/usr/bin:$PATH"
exec "$HERE/usr/bin/peerspeak" "$@"
+76
View File
@@ -0,0 +1,76 @@
# PeerSpeak AppImage
A "thin" AppImage: the `peerspeak` binary, the bundled `pixelpass` screen-share
helper, a launcher (`AppRun`), and the desktop entry + icon. Run
`./build-appimage.sh` to produce `peerspeak-<version>-x86_64.AppImage`.
## Why thin, and why pixelpass is bundled
PeerSpeak owns voice; **pixelpass** owns pixels. They are never Cargo
dependencies of each other — peerspeak shells out to the `pixelpass` binary over
its CLI. The AppImage co-locates `pixelpass` in `usr/bin`, and `AppRun` prepends
`usr/bin` to `PATH`, so peerspeak's normal `$PATH` lookup finds it with no code
change. Joe gets one file, and screen-share works out of the box.
Almost nothing is bundled: peerspeak's own assets (notification WAVs, avatar
presets, window icon, fonts) are `include_bytes!`-embedded, and the graphics
stack (`libGL`, `libvulkan`, `libwayland-*`, `libxkbcommon`, X11) is dlopen'd at
runtime and on the AppImage excludelist because it must match the host driver.
So the image carries just the two binaries plus a handful of small libs.
## Host requirements
The AppImage runs on any reasonably current glibc-based distro that has:
- **A Vulkan-capable GPU + driver** (peerspeak's iced/wgpu renderer). Mesa/RADV
on AMD/Intel or the NVIDIA driver all work.
- **PipeWire** (with the PulseAudio shim, for `pactl`).
- For **screen-share only** — pixelpass shells out to these on the host `PATH`;
it prints the exact package names for your distro if any are missing:
- **GStreamer + plugins** (`gst-launch-1.0`/`gst-inspect-1.0`, base,
good/bad/ugly, libav, and the PipeWire plugin),
- **mpv** (or vlc) for the viewer side,
- on X11, `xwininfo` for single-window capture.
On Arch/Artix that is one pacman line, e.g.:
```sh
sudo pacman -S gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad \
gst-plugins-ugly gst-libav gst-plugin-pipewire mpv xorg-xwininfo libpulse
```
(Add `gstreamer-vaapi` for hardware H.264 encode on AMD/Intel; the software
x264 path always works. On XLibre / X11 the capture path uses `ximagesrc` and
needs no XDG portal — no systemd required.)
## Building for broad compatibility (glibc baseline)
An AppImage requires a host glibc **at least as new** as the build host's. Built
on a rolling distro (glibc 2.43) it only runs on equally-new systems. Build
inside **Ubuntu 24.04** (glibc 2.39, PipeWire 1.0.5) for wide reach — pixelpass's
`pipewire` crate binds the system PipeWire headers and needs PipeWire >= 1.0, so
the older Debian 12 `peerspeak-bookworm` box (PW 0.3.65) cannot build it. 2.39
covers Debian 13+, Fedora 40+, and current rolling distros.
```sh
# One-time: an Ubuntu 24.04 distrobox that reuses the host rustup toolchain.
distrobox create --yes --image ubuntu:24.04 --name peerspeak-appimage
distrobox enter peerspeak-appimage -- sudo apt-get update
distrobox enter peerspeak-appimage -- sudo apt-get install -y \
build-essential cmake clang libclang-dev pkg-config \
libpipewire-0.3-dev libspa-0.2-dev libasound2-dev libxcb1-dev \
curl ca-certificates file patchelf git
# Build (the host's ~/.rustup toolchain is glibc-2.17-baseline, so it runs in the
# box; isolated CARGO_TARGET_DIRs keep it off the host target/):
distrobox enter peerspeak-appimage -- env \
PATH="$HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH" \
./packaging/appimage/build-appimage.sh
```
## Caveats
- **Hardware encode (VAAPI)** uses the host GPU driver and can't be bundled; the
software x264 path always works.
- The bundled `pixelpass` is built headless (no `gui` feature) — it is only ever
driven by peerspeak, never launched standalone from this image.
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Build a "thin" PeerSpeak AppImage that also bundles the pixelpass screen-share
# helper.
#
# PeerSpeak is an iced/wgpu GUI app; pixelpass is the separate screen-share
# orchestrator peerspeak shells out to (never a Cargo dependency). Both link
# almost nothing — the graphics stack (libGL, libvulkan, wayland, xkbcommon,
# X11) is dlopen'd at runtime and is on the AppImage excludelist because it must
# match the host driver, and pixelpass's capture/encode tools (gst-launch-1.0,
# pactl, mpv) are expected on the host PATH. So the AppImage carries just the two
# binaries plus their handful of non-excludelisted libs. The custom AppRun
# prepends usr/bin to PATH so peerspeak's own $PATH lookup finds the bundled
# pixelpass, while the host's tools stay reachable.
#
# All runtime assets (notification WAVs, avatar presets, window icon, fonts) are
# include_bytes!-embedded in the peerspeak binary, so nothing else is bundled.
#
# Usage: packaging/appimage/build-appimage.sh
# Output: packaging/appimage/peerspeak-<version>-x86_64.AppImage
#
# Build inside an Ubuntu 24.04 distrobox (glibc 2.39, PipeWire 1.0.5) for broad
# reach — pixelpass's `pipewire` crate needs PipeWire >= 1.0 headers, so the
# older peerspeak-bookworm box (PW 0.3.65) cannot build it. The 2.39 baseline
# covers Debian 13+, Fedora 40+, and all current rolling distros. See README.md.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "$here/../.." && pwd)"
tools="$here/.tools"
appdir="$here/AppDir"
mkdir -p "$tools"
# linuxdeploy is itself an AppImage; run it without FUSE so this works in a
# container / on CI without libfuse2.
export APPIMAGE_EXTRACT_AND_RUN=1
VERSION="$(grep -m1 '^version' "$repo/Cargo.toml" | sed -E 's/.*"(.*)".*/\1/')"
export VERSION
# Isolated target dirs so an old-glibc box build never clobbers the host target/.
cache="${PEERSPEAK_APPIMAGE_CACHE:-$HOME/.cache/peerspeak-appimage}"
ps_target="$cache/peerspeak-target"
pp_target="$cache/pixelpass-target"
# The pixelpass screen-share helper we bundle. Sibling checkout by default.
pixelpass_repo="${PIXELPASS_REPO:-$repo/../pixelpass}"
if [ ! -d "$pixelpass_repo" ]; then
echo "!! pixelpass repo not found at $pixelpass_repo (set PIXELPASS_REPO)" >&2
exit 1
fi
echo ">> building peerspeak (release)"
( cd "$repo" && CARGO_TARGET_DIR="$ps_target" cargo build --release )
ps_bin="$ps_target/release/peerspeak"
# Headless pixelpass: peerspeak drives it via `--host`/viewer + `--output json`,
# never its GUI, so the default (no `gui` feature) keeps the GL toolkit out.
echo ">> building pixelpass (release, headless) from $pixelpass_repo"
( cd "$pixelpass_repo" && CARGO_TARGET_DIR="$pp_target" cargo build --release )
pp_bin="$pp_target/release/pixelpass"
echo ">> fetching linuxdeploy"
ld="$tools/linuxdeploy-x86_64.AppImage"
if [ ! -x "$ld" ]; then
curl -fL --retry 3 -o "$ld" \
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod +x "$ld"
fi
echo ">> assembling AppDir"
rm -rf "$appdir"
mkdir -p "$appdir/usr/bin"
install -m755 "$ps_bin" "$appdir/usr/bin/peerspeak"
install -m755 "$pp_bin" "$appdir/usr/bin/pixelpass"
echo ">> running linuxdeploy (bundles libs, builds the AppImage)"
# -e (repeated): analyse both binaries for libraries to bundle; excludelisted
# graphics/glibc libs are skipped. -d/-i: desktop entry + icon.
# --custom-apprun: our launcher that puts the bundled pixelpass on PATH.
( cd "$here" && OUTPUT="peerspeak-${VERSION}-x86_64.AppImage" "$ld" \
--appdir "$appdir" \
-e "$appdir/usr/bin/peerspeak" \
-e "$appdir/usr/bin/pixelpass" \
-d "$repo/packaging/peerspeak.desktop" \
-i "$repo/assets/icons/peerspeak-256.png" \
--icon-filename peerspeak \
--custom-apprun "$here/AppRun" \
--output appimage )
echo ">> done: $here/peerspeak-${VERSION}-x86_64.AppImage"
+1 -1
View File
@@ -8,7 +8,7 @@ it once, then you and I connect directly to each other.
## 1. Install it ## 1. Install it
1. Double-click **`peerspeak-0.4.0-setup.exe`** (the file I sent you). 1. Double-click **`peerspeak-<version>-setup.exe`** (the file I sent you).
2. **Windows will probably show a blue "Windows protected your PC" warning.** 2. **Windows will probably show a blue "Windows protected your PC" warning.**
This is normal — it shows up for any app that isn't from a big company with a This is normal — it shows up for any app that isn't from a big company with a
+3 -2
View File
@@ -11,8 +11,9 @@ runtime, so there are no extra DLLs to bundle. The installer payload is just the
## Version compatibility ## Version compatibility
The installer version tracks the crate version in `Cargo.toml` (currently The installer version tracks the release version in `Cargo.toml` — keep
**0.4.0**) — keep `MyAppVersion` in `peerspeak.iss` in sync when it changes. `MyAppVersion` in `peerspeak.iss` in sync when cutting a release. Do not reuse an
old installer filename after a crate-version bump.
Per `VERSIONING.md`, a **MINOR** bump in `0.x` is a **breaking wire change**: Per `VERSIONING.md`, a **MINOR** bump in `0.x` is a **breaking wire change**:
peers on different MINOR versions can't connect (they fail fast at the peers on different MINOR versions can't connect (they fail fast at the
+1 -1
View File
@@ -12,7 +12,7 @@
; (x86_64-pc-windows-gnu, statically linked -- no extra DLLs needed). ; (x86_64-pc-windows-gnu, statically linked -- no extra DLLs needed).
#define MyAppName "PeerSpeak" #define MyAppName "PeerSpeak"
#define MyAppVersion "0.6.0" #define MyAppVersion "0.6.2"
#define MyAppPublisher "mollusk" #define MyAppPublisher "mollusk"
#define MyAppExeName "peerspeak.exe" #define MyAppExeName "peerspeak.exe"
+58 -5
View File
@@ -31,6 +31,12 @@ use tokio::sync::{Mutex, mpsc};
type CoalesceStore = Arc<StdMutex<HashMap<CoalesceKey, CoreCommand>>>; type CoalesceStore = Arc<StdMutex<HashMap<CoalesceKey, CoreCommand>>>;
// Mixer -> playback-worker handoff. The playback ring itself targets three
// 20ms frames; allow at most two more in flight so worker lag applies
// backpressure before the ring can overshoot to its 200ms cap (A6).
const PLAYBACK_HANDOFF_QUEUE_FRAMES: usize = 2;
const PLAYBACK_HANDOFF_RETRY: Duration = Duration::from_millis(1);
pub struct CoreController { pub struct CoreController {
reliable_tx: mpsc::UnboundedSender<CoreCommand>, reliable_tx: mpsc::UnboundedSender<CoreCommand>,
coalesce: CoalesceStore, coalesce: CoalesceStore,
@@ -156,6 +162,22 @@ fn audio_datagram_len_ok(len: usize) -> bool {
(4..=4 + MAX_OPUS_PAYLOAD).contains(&len) (4..=4 + MAX_OPUS_PAYLOAD).contains(&len)
} }
async fn send_playback_frame(
tx: &std::sync::mpsc::SyncSender<Vec<i16>>,
mut frame: Vec<i16>,
) -> bool {
loop {
match tx.try_send(frame) {
Ok(()) => return true,
Err(std::sync::mpsc::TrySendError::Full(returned)) => {
frame = returned;
tokio::time::sleep(PLAYBACK_HANDOFF_RETRY).await;
}
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => return false,
}
}
}
/// The presence label to broadcast for a detected game: its display name, /// The presence label to broadcast for a detected game: its display name,
/// sanitized + length-capped, or `None` when there's no game or no broadcastable /// sanitized + length-capped, or `None` when there's no game or no broadcastable
/// name (a Steam appid without a manifest name, or a label that sanitizes empty). /// name (a Steam appid without a manifest name, or a label that sanitizes empty).
@@ -1654,7 +1676,8 @@ async fn run_core_loop(
// Setup raw audio channels // Setup raw audio channels
let (capture_tx, capture_rx) = std::sync::mpsc::channel(); let (capture_tx, capture_rx) = std::sync::mpsc::channel();
let (playback_tx, playback_rx) = std::sync::mpsc::channel(); let (playback_tx, playback_rx) =
std::sync::mpsc::sync_channel(PLAYBACK_HANDOFF_QUEUE_FRAMES);
// Echo cancellation: if enabled, load PipeWire's echo-cancel module // Echo cancellation: if enabled, load PipeWire's echo-cancel module
// bound to the chosen real devices and route capture/playback // bound to the chosen real devices and route capture/playback
@@ -2114,7 +2137,7 @@ async fn run_core_loop(
mixed mixed
}; };
if playback_tx.send(frame_to_send).is_err() { if !send_playback_frame(&playback_tx, frame_to_send).await {
break; break;
} }
@@ -3222,12 +3245,15 @@ async fn run_core_loop(
mod tests { mod tests {
use super::{ use super::{
KnownPeers, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS, MIC_LEVEL_REPORT_SAMPLES, MicLevelMeter, KnownPeers, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS, MIC_LEVEL_REPORT_SAMPLES, MicLevelMeter,
PeerSpeakTicket, admit_retained, apply_peer_volume, apply_volume, audio_datagram_len_ok, PLAYBACK_HANDOFF_QUEUE_FRAMES, PeerSpeakTicket, admit_retained, apply_peer_volume,
coalesce_insert, coalesce_pop, frame_level, mix_frames, mix_stereo_frames, apply_volume, audio_datagram_len_ok, coalesce_insert, coalesce_pop, frame_level,
next_game_change, should_auto_fetch, stereo_to_mono, mix_frames, mix_stereo_frames, next_game_change, send_playback_frame, should_auto_fetch,
stereo_to_mono,
}; };
use crate::core::messages::{CoalesceKey, CoreCommand, coalesce_key}; use crate::core::messages::{CoalesceKey, CoreCommand, coalesce_key};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::mpsc::sync_channel;
use std::time::Duration;
fn endpoint_id() -> iroh::EndpointId { fn endpoint_id() -> iroh::EndpointId {
iroh::SecretKey::generate().public() iroh::SecretKey::generate().public()
@@ -3461,6 +3487,33 @@ mod tests {
assert!(!audio_datagram_len_ok(5 + MAX_OPUS_PAYLOAD)); assert!(!audio_datagram_len_ok(5 + MAX_OPUS_PAYLOAD));
} }
#[tokio::test]
async fn playback_handoff_waits_for_bounded_queue_space() {
let (tx, rx) = sync_channel::<Vec<i16>>(PLAYBACK_HANDOFF_QUEUE_FRAMES);
for n in 0..PLAYBACK_HANDOFF_QUEUE_FRAMES {
tx.try_send(vec![n as i16]).unwrap();
}
let worker = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(20));
for n in 0..PLAYBACK_HANDOFF_QUEUE_FRAMES {
assert_eq!(rx.recv().unwrap(), vec![n as i16]);
}
assert_eq!(rx.recv().unwrap(), vec![99, 100]);
});
let sent = tokio::time::timeout(
Duration::from_secs(1),
send_playback_frame(&tx, vec![99, 100]),
)
.await
.expect("bounded handoff should unblock after the worker drains a frame");
assert!(sent);
drop(tx);
worker.join().unwrap();
}
#[test] #[test]
fn mic_meter_holds_the_peak_across_the_window() { fn mic_meter_holds_the_peak_across_the_window() {
let mut m = MicLevelMeter::new(); let mut m = MicLevelMeter::new();
+1 -3
View File
@@ -97,14 +97,12 @@ fn windows_toolhelp_executables() -> Vec<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn enumerates_at_least_this_process() { fn enumerates_at_least_this_process() {
// The test runner itself is a process, so /proc enumeration must be // The test runner itself is a process, so /proc enumeration must be
// non-empty and include something that normalizes to our own exe basename. // non-empty and include something that normalizes to our own exe basename.
let exes = running_executables(); let exes = super::running_executables();
assert!( assert!(
!exes.is_empty(), !exes.is_empty(),
"expected to see running processes via /proc" "expected to see running processes via /proc"