Docs: rewrite README.md for users, add MANUAL.md, sync from readme.html on every release
People landing on the repo page were having to install RemSound just to read what it does and how to use it. Two doc changes fix that: 1) README.md rewritten as a plain-English landing page. Drops the developer-focused highlights / build-from-source / project-layout sections in favour of what RemSound is, who it's for, how to install it, and a prominent link to the manual. No jargon, no command lines, no NuGet / SDK / ASIO-protocol talk. The dev-side information that used to live here (build commands, source layout, relay setup) is still discoverable for anyone who wants it — the source itself is on the same page, and the relay docs are under server/README.md. 2) MANUAL.md added at the repo root as the GitHub-rendered version of the F1 help. Markdown derived directly from readme.html via sync-manual.py (new), so visitors can read the manual inline on the repo page with no download. readme.html stays exactly where it was (bundled inside RemSound, opened by F1) — it remains the canonical source of the manual content; MANUAL.md is auto-generated from it. The sync-manual.py script is invoked automatically from build-release.ps1 as step 0, before any other release work. It regenerates MANUAL.md from readme.html and then checks `git diff` on MANUAL.md — if the file changed, the release is paused with a message asking the user to commit the updated MANUAL.md alongside the release commit. That makes it structurally impossible to ship a release with a stale GitHub-facing manual: forgetting to commit MANUAL.md after editing the bundled help triggers a deliberate release-time stop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8aa8d0c3bd
commit
0189b12668
@@ -1,64 +1,60 @@
|
||||
# RemSound
|
||||
|
||||
Low-latency peer-to-peer audio between two or more Windows PCs over UDP. Pick what each machine captures and what it plays back; audio flows directly between them, no central server.
|
||||
**Free Windows app for sending live audio between two computers — across a house, across a city, or anywhere your internet reaches. Low delay, great quality, fully accessible to screen-reader users.**
|
||||
|
||||
Built for music collaboration over the internet, live monitoring across rooms in a house, podcast co-hosting, NVDA-Remote audio workflows, and anything else that wants "send the sound from this PC to that PC, fast".
|
||||
[**Download the latest version**](https://github.com/Ednunp/RemSound/releases/latest) · [**Read the user manual**](MANUAL.md)
|
||||
|
||||
## Highlights
|
||||
---
|
||||
|
||||
- **WASAPI and ASIO side by side.** Run them as two independent UDP streams at their own native latencies, or use WASAPI alone. ASIO drivers get hardware-clocked timing; WASAPI gets push-mode timing on single-source captures.
|
||||
- **Profiles.** Save your full setup (device ticks, peers, codec, latency targets, hotkeys, ASIO driver) into one JSON file. Pick which profile to load at launch.
|
||||
- **Continuous auto-tune.** Watches receive jitter and nudges the latency target to stay click-free without forcing you to overshoot. Independent per-lane in WASAPI+ASIO mode.
|
||||
- **Opus with inband FEC.** Single-packet losses recover transparently — no click. PCM 24-bit 48 kHz is also available for clean LAN.
|
||||
- **Remote control hotkeys.** Configurable global hotkeys can nudge a peer's RemSound volume or their Windows system master volume, opt-in on the receiver.
|
||||
- **Built-in self-updater.** Optional GitHub-driven update check on a schedule you set.
|
||||
- **Designed for screen readers.** Each control has a paired Alt+letter mnemonic. State changes raise the right UIA notifications. F1 anywhere opens the user manual.
|
||||
RemSound is for musicians, sound designers, podcasters, and anyone else who wants to share audio between two Windows machines with as little delay as possible.
|
||||
|
||||
## Install
|
||||
You sit at one computer, RemSound captures whatever is playing — a track in your music software, a video call, system sound from anything else running — and sends it cleanly to another computer where it plays through speakers or headphones in real time. The person at the other end hears what you're hearing, with a delay measured in milliseconds rather than seconds.
|
||||
|
||||
1. Download the latest `RemSound-vX.Y.zip` from [Releases](https://github.com/Ednunp/RemSound/releases).
|
||||
2. Extract somewhere it can write — e.g. `C:\RemSound\`, your `Documents`, or a folder in your user profile. Avoid `Program Files` unless you grant write permission to the install folder (the self-updater needs to overwrite files in place).
|
||||
3. Run `RemSound.exe`. On first launch Windows Firewall will prompt — allow on private networks.
|
||||
4. Open the user manual from the **Help** menu (or press F1) for the full walkthrough.
|
||||
It's also fully accessible. The interface was designed with screen readers (NVDA in particular) in mind from day one. Every button has a keyboard shortcut, every status line is read out clearly, and there are no menus or controls that need a mouse to reach.
|
||||
|
||||
RemSound requires the .NET 10 Desktop Runtime. If it's not installed, Windows offers to fetch it on first launch. You can also install it from <https://dotnet.microsoft.com/download/dotnet/10.0> (pick the "Windows x64 Desktop Runtime").
|
||||
## What you can do with it
|
||||
|
||||
## Updates
|
||||
* **Listen to one of your computers from another room.** Sit at your laptop and hear what's playing on your desktop. Walk around the house — the sound follows you.
|
||||
* **Play music together over the internet.** Two musicians at different houses can play along together with very low delay. Much faster than a video call, fast enough that timing-sensitive playing works.
|
||||
* **Send a finished mix to a producer or client** in real time, without uploading a file and waiting.
|
||||
* **Record what comes through the connection** to WAV, MP3, OGG-Opus, or FLAC. Save sessions for review later.
|
||||
|
||||
RemSound can check this repository's Releases page on a schedule (never, hourly, every 6 hours, every 24 hours) and either prompt you to install or do it silently. Configure via File → Preferences. You can also trigger a manual check from the Help menu or the same Preferences dialog.
|
||||
## Three quality settings, simple choice
|
||||
|
||||
## Build from source
|
||||
Inside RemSound there's just one main decision: which quality and delay you want.
|
||||
|
||||
You need the .NET 10 SDK. The solution lives at `RemSound.slnx`.
|
||||
* **PCM 48K 24 bit — uncompressed.** The best possible sound. Uses about 2.3 megabits a second. Use it when both computers are on the same local network.
|
||||
* **Opus, broadcast quality — loss tolerant.** Compressed, very good sound, only 200 kilobits a second. Robust against patchy connections. Use it across the internet.
|
||||
* **Opus, live latency — for jamming and monitoring.** Compressed, ultra-low-latency mode. About 5 milliseconds of delay added by the codec itself, very close to PCM. Best when you and the person on the other end are playing along together over a clean network.
|
||||
|
||||
```powershell
|
||||
cd D:\proj\RemSound
|
||||
dotnet build -c Release
|
||||
dotnet publish src\RemSound.App\RemSound.App.csproj -c Release
|
||||
```
|
||||
## How to install it
|
||||
|
||||
The publish output lands at `src\RemSound.App\bin\Release\net10.0-windows\publish\`. Copy its contents into a folder of your choice — or zip it for distribution. Don't enable `PublishSingleFile` or `SelfContained=true`; RemSound ships framework-dependent on purpose so the publish folder stays under 2 MB.
|
||||
1. Go to the [latest release](https://github.com/Ednunp/RemSound/releases/latest).
|
||||
2. Download the file called `RemSound-v3.0.2.zip` (the version number changes over time — pick whichever is newest).
|
||||
3. Extract the zip into a folder of your choice.
|
||||
4. Double-click `RemSound.exe` and away you go.
|
||||
|
||||
## Project layout
|
||||
The first time you launch, RemSound will offer to install Microsoft's .NET 10 Desktop Runtime if you don't already have it. Free, just say yes.
|
||||
|
||||
```
|
||||
src/RemSound.Core packet protocol, peer discovery, hotkeys, MMCSS, heartbeat, settings, AppConfig
|
||||
src/RemSound.Sender capture → mix → encode → UDP send
|
||||
src/RemSound.Receiver UDP receive → ring buffer → drift-corrected playout → render
|
||||
src/RemSound.Harness console test program (1 sender → 1 receiver, no UI)
|
||||
src/RemSound.App WinForms UI (sender + receiver + heartbeat + discovery + updater)
|
||||
server/ optional Raspberry Pi / systemd-Linux relay bundle (see below)
|
||||
```
|
||||
After that, RemSound updates itself. Help → Check for updates pulls the next version, or you can tick a box in Preferences and let it install updates quietly in the background.
|
||||
|
||||
## Optional: running your own relay server
|
||||
## What you'll need
|
||||
|
||||
Two RemSound peers normally reach each other directly over your LAN, or via Tailscale across the internet. If neither of those work for your situation — for example one peer is behind a router that won't forward inbound UDP and you'd prefer not to use Tailscale — you can run a small Python relay on a publicly-reachable host (a Raspberry Pi at home with one UDP port forwarded works fine) and have both peers dial that.
|
||||
* **Windows 10 or 11.** Some users run it successfully on Windows 7, but it's not officially supported there.
|
||||
* **Another person running RemSound** on their own Windows machine.
|
||||
* **A way for the two machines to reach each other on the network.** Both on the same Wi-Fi works. Both on the same [Tailscale](https://tailscale.com) network works (free and easy to set up). Or both pointed at the public RemSound relay (also free, no setup).
|
||||
|
||||
The `server/` folder in this repo is a self-contained bundle: relay script, systemd unit, install / uninstall / smoke-test scripts, and a step-by-step README. See [`server/README.md`](server/README.md) for the setup walkthrough.
|
||||
## Learn how to use it
|
||||
|
||||
## Issues and feedback
|
||||
The full user manual is right here on GitHub: **[Read the user manual](MANUAL.md)**. It covers getting connected for the first time, every setting and what it does, troubleshooting tips, and a glossary at the end. It's the same manual you can press F1 to read from inside RemSound, so you can read it before installing if you want to see what you're getting.
|
||||
|
||||
Open an issue on the [GitHub issues page](https://github.com/Ednunp/RemSound/issues). If reporting an audio problem, please tick **File → Preferences → Enable logs**, reproduce the issue, then attach the latest log file from `logs\` next to `RemSound.exe`.
|
||||
## Questions or problems?
|
||||
|
||||
[File an issue](https://github.com/Ednunp/RemSound/issues/new). Bugs and questions are welcome and someone will get back to you.
|
||||
|
||||
## Who made this
|
||||
|
||||
RemSound was built by a sound designer who wanted to listen to one of his computers while sitting at another, and couldn't find anything else that fit the bill. It's free, open-source, and yours to use however you like.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@@ -35,6 +35,44 @@ $distDir = Join-Path $repo 'dist'
|
||||
$zipPath = Join-Path $distDir "RemSound-$Tag.zip"
|
||||
$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("remsound-release-" + [guid]::NewGuid().ToString('N'))
|
||||
|
||||
# 0. Keep the GitHub-facing MANUAL.md in sync with the bundled readme.html.
|
||||
# Two copies of the manual exist on purpose: readme.html ships inside RemSound (F1
|
||||
# inside the app opens it), MANUAL.md is the Markdown rendition rendered on the
|
||||
# repo's main page. The Python sync-manual.py script regenerates MANUAL.md from
|
||||
# readme.html every time we package a release, so the GitHub page can never go out
|
||||
# of sync with the bundled help. After the regeneration we check whether MANUAL.md
|
||||
# differs from what git has committed — if so, the release is paused so the user
|
||||
# can commit the updated MANUAL.md alongside the release commit.
|
||||
$syncScript = Join-Path $repo 'sync-manual.py'
|
||||
if (Test-Path $syncScript) {
|
||||
Write-Host "Syncing MANUAL.md from readme.html..." -ForegroundColor Cyan
|
||||
# Prefer the user-local Python 3.11 install; fall back to whichever 'python' resolves
|
||||
# on PATH if that's not present. py.exe is the official Windows launcher and is the
|
||||
# most reliable single command, so try it first.
|
||||
$pythonCmd = $null
|
||||
foreach ($candidate in @('py', 'python', 'python3')) {
|
||||
if (Get-Command $candidate -ErrorAction SilentlyContinue) { $pythonCmd = $candidate; break }
|
||||
}
|
||||
if (-not $pythonCmd) { throw "Python not found on PATH - cannot sync MANUAL.md. Install Python 3.x and re-run." }
|
||||
& $pythonCmd $syncScript
|
||||
if ($LASTEXITCODE -ne 0) { throw "sync-manual.py failed (exit $LASTEXITCODE)" }
|
||||
|
||||
# Refuse to ship a release when MANUAL.md is uncommitted relative to readme.html.
|
||||
# 'git diff --quiet -- MANUAL.md' exits 0 if no change, 1 if there is one.
|
||||
& git -C $repo diff --quiet -- MANUAL.md
|
||||
if ($LASTEXITCODE -eq 1) {
|
||||
Write-Host ""
|
||||
Write-Host "RELEASE PAUSED - MANUAL.md was regenerated and now differs from the committed copy." -ForegroundColor Yellow
|
||||
Write-Host "Commit the updated MANUAL.md alongside this release before re-running build-release.ps1:" -ForegroundColor Yellow
|
||||
Write-Host " git add MANUAL.md" -ForegroundColor Yellow
|
||||
Write-Host " git commit -m 'Refresh MANUAL.md from readme.html'" -ForegroundColor Yellow
|
||||
Write-Host "Then re-run: powershell -ExecutionPolicy Bypass -File build-release.ps1 -Tag $Tag" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "Note: sync-manual.py not found - skipping MANUAL.md sync." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# Anything matching these must NEVER appear in a release. Folders by name; files by
|
||||
# extension / exact name. RemSound.deps.json and RemSound.runtimeconfig.json are
|
||||
# legitimate app files and are deliberately NOT matched (different names).
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync MANUAL.md from readme.html.
|
||||
|
||||
readme.html is the canonical user manual — bundled inside RemSound and opened by F1.
|
||||
MANUAL.md is the GitHub-facing Markdown rendition shown on the repository page.
|
||||
|
||||
This script reads readme.html, converts it to Markdown via html2text, and post-processes
|
||||
the output to fix two things html2text doesn't do well on this document:
|
||||
|
||||
* Table of contents links. The HTML uses custom anchors like `#what-it-does` which
|
||||
html2text strips. We rebuild the TOC after conversion using GitHub's auto-generated
|
||||
heading anchors (lowercased, punctuation stripped, spaces → hyphens).
|
||||
* Cosmetic backslash escapes before periods after numbers in headings (`## 1\. ...`
|
||||
instead of `## 1. ...`). GitHub renders both the same but the un-escaped version
|
||||
looks cleaner in raw source.
|
||||
|
||||
The script is invoked automatically by build-release.ps1 as the first step before
|
||||
packaging a release, so MANUAL.md can never get out of sync with the bundled help.
|
||||
It can also be run by hand from the repo root: `python sync-manual.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html2text
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parent
|
||||
README_HTML = REPO_ROOT / "readme.html"
|
||||
MANUAL_MD = REPO_ROOT / "MANUAL.md"
|
||||
|
||||
|
||||
def github_anchor(heading_text: str) -> str:
|
||||
"""Replicate GitHub's auto-anchor rule for a Markdown heading.
|
||||
|
||||
Rules (per https://gist.github.com/asabaylus/3071099):
|
||||
* Lowercase everything.
|
||||
* Replace spaces with hyphens.
|
||||
* Strip all punctuation EXCEPT hyphens and underscores.
|
||||
* Collapse runs of hyphens (though GitHub does NOT — it keeps them).
|
||||
"""
|
||||
s = heading_text.strip().lower()
|
||||
# Replace whitespace with hyphens
|
||||
s = re.sub(r"\s+", "-", s)
|
||||
# Strip punctuation except hyphens and underscores
|
||||
s = re.sub(r"[^\w\-]", "", s)
|
||||
return s
|
||||
|
||||
|
||||
def convert_html_to_markdown(html: str) -> str:
|
||||
# Strip <head> entirely — none of its content belongs in the Markdown
|
||||
html = re.sub(r"<head>.*?</head>", "", html, flags=re.DOTALL)
|
||||
# Render every <div class="..."> callout (note / warn / etc.) as a blockquote —
|
||||
# Markdown's native callout. The wildcard catches all classes in one rule so a new
|
||||
# callout class added later doesn't silently break the structure (which it did for
|
||||
# the original `<div class="note">`-only rule when `class="warn"` was introduced).
|
||||
html = re.sub(r'<div\s+class="[^"]*">', "<blockquote>", html)
|
||||
html = re.sub(r"</div>", "</blockquote>", html)
|
||||
|
||||
h = html2text.HTML2Text()
|
||||
h.body_width = 0 # never hard-wrap; let the rendering layer reflow
|
||||
h.ignore_links = False
|
||||
h.unicode_snob = True
|
||||
h.use_automatic_links = True
|
||||
h.bypass_tables = False
|
||||
h.escape_snob = False
|
||||
md = h.handle(html)
|
||||
|
||||
# Replace any leftover U+FFFD with the em-dash that almost always belongs there
|
||||
md = md.replace("�", "—")
|
||||
# Strip cosmetic backslash escapes before periods after numbers in headings
|
||||
md = re.sub(
|
||||
r"^(##+ \d+)\\\.",
|
||||
lambda m: m.group(1) + ".",
|
||||
md,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
# Trim trailing whitespace on every line
|
||||
md = "\n".join(line.rstrip() for line in md.splitlines()) + "\n"
|
||||
return md
|
||||
|
||||
|
||||
def rebuild_toc(md: str) -> str:
|
||||
"""Rebuild the Table of contents section using GitHub-style anchor links.
|
||||
|
||||
The HTML version of readme.html uses a hand-written `<ol>` with `<a href="#id">` items
|
||||
where `id` matches the `id=""` attribute on later `<h2>` elements. html2text drops those
|
||||
links entirely on conversion. Here we regenerate the list from the actual `## ` headings
|
||||
in the post-conversion Markdown, so the TOC always reflects the real document.
|
||||
"""
|
||||
# Collect all top-level (##) headings in document order, skipping the TOC heading itself
|
||||
headings: list[str] = []
|
||||
for line in md.splitlines():
|
||||
m = re.match(r"^##\s+(.+?)\s*$", line)
|
||||
if not m:
|
||||
continue
|
||||
text = m.group(1).strip()
|
||||
if text.lower() == "table of contents":
|
||||
continue
|
||||
headings.append(text)
|
||||
|
||||
if not headings:
|
||||
return md
|
||||
|
||||
# Build the new TOC: numbered list with anchor links to each heading
|
||||
toc_lines = []
|
||||
for h_text in headings:
|
||||
# Strip the leading "N. " from headings like "1. What RemSound does" so the displayed
|
||||
# link text in the TOC reads naturally. The anchor still references the FULL heading.
|
||||
display = re.sub(r"^\d+\.\s*", "", h_text)
|
||||
anchor = github_anchor(h_text)
|
||||
toc_lines.append(f" 1. [{display}](#{anchor})")
|
||||
new_toc_block = "\n".join(toc_lines) + "\n"
|
||||
|
||||
# Replace the existing TOC block (everything between "## Table of contents" and the next "## ")
|
||||
# with the regenerated one. Using a callback to keep the surrounding markers intact.
|
||||
def replace_toc(match: re.Match[str]) -> str:
|
||||
return match.group(1) + "\n\n" + new_toc_block + "\n"
|
||||
|
||||
pattern = re.compile(
|
||||
r"(##\s+Table of contents\s*\n)(?:.*?)(?=\n##\s)",
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if not pattern.search(md):
|
||||
# Defensive: TOC section wasn't found in the expected shape — leave the doc alone.
|
||||
return md
|
||||
return pattern.sub(replace_toc, md, count=1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not README_HTML.exists():
|
||||
print(f"ERROR: {README_HTML} not found", file=sys.stderr)
|
||||
return 1
|
||||
html = README_HTML.read_text(encoding="utf-8")
|
||||
md = convert_html_to_markdown(html)
|
||||
md = rebuild_toc(md)
|
||||
MANUAL_MD.write_text(md, encoding="utf-8", newline="\n")
|
||||
print(f"OK - MANUAL.md regenerated from readme.html ({len(md):,} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user