Release v3.9: silent-mic fix, buffer drain, idle-send guard, start-up sound, command-line
- Receiver: a plain (Mixed) stream now renders on an active lane when the
receiver is in two-lane (ASIO) mode, instead of being decoded into a ring
nothing reads. Fixes one-way silence ("my mic works for me but not for them").
- Receiver: drift resampler gains a buffer-depth correction term so a bloated
standing buffer eases back to the latency target over a long session.
- App: don't send audio until a peer is genuinely reachable (issue #8); status
no longer shows phantom send traffic with nobody connected.
- App: start-up cue sound (machine-wide toggle + custom path in Preferences).
- App: command-line options (CommandLine.cs) -- --devices, --selftest,
--diagnostics, --log, --close, --profile, --connect, --minimized, --version,
--help. New "Command-line options" manual section (readme.html + MANUAL.md).
- Version 3.9; About-dialog and RELEASE_NOTES updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
84c4a47411
commit
05b218825d
@@ -29,8 +29,9 @@ It was built for playing music together over the internet — a guitarist on one
|
||||
1. [Updating RemSound](#19-updating-remsound)
|
||||
1. [Recording to a file](#20-recording-to-a-file)
|
||||
1. [Logs and diagnostics](#21-logs-and-diagnostics)
|
||||
1. [Troubleshooting](#22-troubleshooting)
|
||||
1. [Glossary](#23-glossary)
|
||||
1. [Command-line options](#22-command-line-options)
|
||||
1. [Troubleshooting](#23-troubleshooting)
|
||||
1. [Glossary](#24-glossary)
|
||||
|
||||
|
||||
## 1. What RemSound does
|
||||
@@ -1077,7 +1078,58 @@ Logs are plain text and can be opened in any text editor, or in a spreadsheet. T
|
||||
|
||||
|
||||
|
||||
## 22. Troubleshooting
|
||||
## 22. Command-line options
|
||||
|
||||
RemSound is normally a windowed program you click to open. But it can also take **command-line options** — short text instructions you type after the program name. They are handy for three things: checking a machine quickly (what devices are present, does the audio path work at all), getting a support report to send to whoever helps you, and starting RemSound a particular way from a shortcut or a script.
|
||||
|
||||
To use them, open a command prompt (press the Windows key, type `cmd`, press Enter), then run RemSound with the option after it. If RemSound is on your desktop you can type the whole path in quotes, for example:
|
||||
|
||||
|
||||
"C:\Users\you\Desktop\RemSound\RemSound.exe" --devices
|
||||
|
||||
|
||||
The options that just report something print their answer straight into the same command window as plain text — a screen reader reads it normally — and then RemSound exits without opening a window. The start-up options open RemSound as usual, just set up the way you asked.
|
||||
|
||||
### Options that print something and then exit
|
||||
|
||||
Option| What it does
|
||||
---|---
|
||||
`--help` or `-h`| Lists every option, the same as this section in short form.
|
||||
`--version`| Prints which version of RemSound is installed, for example “RemSound 3.9”.
|
||||
`--devices`| Lists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine — each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows.
|
||||
`--selftest`| Runs a complete round-trip on the machine on its own: it captures sound, encodes it, sends it across the network layer to itself, receives it and decodes it, then reports **PASS** or **FAIL**. No sound is played out, so it is safe to run silently. A PASS proves capture, encoding, the network code and decoding are all working on that computer. Add `--opus` to test the Opus codec path, or `--seconds N` to run it for longer than the default.
|
||||
`--diagnostics`| Writes a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the **user settings and logs** folder and prints where it put it; you can also give a path, for example `--diagnostics C:\Users\you\Desktop\report.txt`. This is the file to send when asking for help — it answers most questions in one go.
|
||||
|
||||
### Options that change a setting or control a running copy, then exit
|
||||
|
||||
Option| What it does
|
||||
---|---
|
||||
`--log on` or `--log off`| Turns the diagnostic log on or off. The change takes effect the next time RemSound starts. The same setting lives in the Preferences dialog; this is just a way to set it without opening the window.
|
||||
`--close`| Closes a copy of RemSound that is already running. Useful in a script that needs to restart it.
|
||||
|
||||
### Options that change how RemSound starts
|
||||
|
||||
These open RemSound as normal, set up the way you ask, and are meant for shortcuts and scripts.
|
||||
|
||||
Option| What it does
|
||||
---|---
|
||||
`--profile "<name>"`| Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example `--profile "Studio link"`.
|
||||
`--connect <ip>`| Starts and connects to a peer at that address. You can give just an address (`--connect 192.168.1.42`) or an address and port (`--connect 192.168.1.42:47830`); with no port it uses RemSound's normal port, 47830. If you don't also give a `--profile`, it starts on a fresh blank profile already pointed at that peer.
|
||||
`--minimized` or `--tray`| Starts minimized to the notification area, with no window popping up. Pair it with `--profile` or `--connect` so it has something to do without waiting at the picker.
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
RemSound.exe --devices
|
||||
RemSound.exe --selftest --opus
|
||||
RemSound.exe --diagnostics
|
||||
RemSound.exe --profile "Studio" --minimized
|
||||
RemSound.exe --connect 192.168.1.42
|
||||
|
||||
|
||||
A common support sequence: ask the person to run `--diagnostics` and send you the file, then have them run `--selftest` — if that says PASS, capture, encoding and the audio path are all sound on their machine and the problem is somewhere in the connection between you.
|
||||
|
||||
## 23. Troubleshooting
|
||||
|
||||
### I don't hear my friend
|
||||
|
||||
@@ -1164,7 +1216,7 @@ The most common reasons:
|
||||
|
||||
If none of those apply, just fall back to Tailscale — it works without involving the router at all.
|
||||
|
||||
## 23. Glossary
|
||||
## 24. Glossary
|
||||
|
||||
Term| Meaning
|
||||
---|---
|
||||
|
||||
@@ -24,6 +24,7 @@ It works with a simple shared password. You and the person you're connecting to
|
||||
* **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.
|
||||
* **Drive it from the command line.** As well as its normal window, RemSound takes command-line options — list your audio devices, run a self-test of the whole capture-to-playback path, write a diagnostics report to send for help, or start straight into a profile from a shortcut or script. See [Command-line options](MANUAL.md#22-command-line-options) in the manual.
|
||||
|
||||
## Three quality settings, simple choice
|
||||
|
||||
|
||||
+27
-12
@@ -1,35 +1,50 @@
|
||||
# RemSound v3.8
|
||||
# RemSound v3.9
|
||||
|
||||
A usability release from user reports: you can now create a new profile at any time, the "different passwords" warning stays put, and the manual explains how to pin a connection to a specific IP.
|
||||
A reliability release built from real reports: a long-standing cause of one-way silence is fixed, long sessions no longer drift slower, RemSound stops "sending" when nobody is connected, there's a start-up sound, and you can now drive RemSound from the command line.
|
||||
|
||||
## New profile, any time
|
||||
## One-way silence is fixed
|
||||
|
||||
There's a new **New profile** item at the top of the **File** menu (and **Ctrl+N**). It opens a fresh, unsaved session, so you can build a profile for a different setup from scratch.
|
||||
If you sent plain (non-ASIO) audio to someone who had an **ASIO device selected** at their end, your sound — your microphone included — could arrive at their machine and then never be played. They heard nothing, even though everything looked connected and their logs showed audio coming in.
|
||||
|
||||
This closes a real gap: if you'd set RemSound to **start in a specific profile**, you booted straight past the profile picker and had **no way to reach a clean slate** to start something new — your only escape was opening an old profile by luck. Now you always can, without unchecking your startup setting. If your current profile has unsaved changes, New profile offers to save them first. The startup picker and the window title now read **"New profile"** as well, where they used to say "blank template".
|
||||
This was a routing fault inside the receiver: a plain stream arriving at a receiver running in two-lane (ASIO) mode was being decoded into a buffer that nothing ever played out, so it silently piled up and was dropped. A plain stream now always reaches the speakers, whatever mix of ASIO and standard audio the two of you happen to be using. This is the fix behind "my mic works for me but they can't hear it."
|
||||
|
||||
## The "different passwords" warning stays on screen
|
||||
## Long sessions stay as tight as they started
|
||||
|
||||
When you connect to someone whose password doesn't match yours, RemSound warns you. That warning was being raised from the once-a-second status update, which kept rebuilding the peer list underneath it and knocked it out of the foreground — so it could **flash away before you reached OK**. It now holds still and stays in front until you dismiss it.
|
||||
On the standard (non-ASIO) path the incoming audio buffer used to creep deeper after a network hiccup and never settle back, so a connection that started tight could feel laggy hours later. It now eases itself back to your chosen latency, gently and silently, so a long session stays as tight as it began.
|
||||
|
||||
## Pin a connection to a specific IP (documentation)
|
||||
## RemSound no longer "sends into the void"
|
||||
|
||||
If a computer has more than one IP address and you want to reach **only one** of them, you can — and you always could: use **Add peer by IP** and type the exact address. RemSound then talks to that address and nothing else (no name lookup, so it can't drift to a different IP). The manual now has a section, *"Connecting to one specific IP address (and only that one)"*, explaining the difference between connecting by name and by a fixed IP, and confirming that a profile remembers exactly what you ticked.
|
||||
If you left a profile open with nobody connected, the status line could still report that it was sending data — and a large running total — which was confusing and looked wrong. RemSound now only sends audio once a peer is genuinely reachable. With no one connected it sits quietly, and the status line reflects that. (Reported on the issue tracker.)
|
||||
|
||||
## A start-up sound
|
||||
|
||||
RemSound now plays a short sound as it starts, so you know it's running even when it opens straight to the notification area. You can turn it off, or choose your own sound, under **Options → Preferences**.
|
||||
|
||||
## Run RemSound from the command line
|
||||
|
||||
As well as its normal window, RemSound now takes **command-line options** — handy for quick checks, for getting a support report to send, and for starting RemSound a particular way from a shortcut or a script:
|
||||
|
||||
* `--devices` lists every microphone, output and ASIO driver, with formats and ids.
|
||||
* `--selftest` runs the whole audio path on the machine on its own and reports PASS or FAIL.
|
||||
* `--diagnostics` writes one report file (version, system, settings, profiles, devices, mic-privacy check, recent log) to send for help.
|
||||
* `--profile`, `--connect` and `--minimized` start RemSound straight into a profile, connected to an address, or down in the tray.
|
||||
|
||||
Run `RemSound.exe --help` for the full list, or see the new **Command-line options** section in the manual (press F1).
|
||||
|
||||
## Compatibility
|
||||
|
||||
**v3.8 talks to v3.3 through v3.7 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
||||
**v3.9 talks to v3.3 through v3.8 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `RemSound-v3.8.zip` from this release.
|
||||
1. Download `RemSound-v3.9.zip` from this release.
|
||||
2. Close RemSound.
|
||||
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your settings, profiles, logs, recordings, or sounds.
|
||||
4. Run `RemSound.exe`.
|
||||
|
||||
## Upgrading
|
||||
|
||||
**From v3.6 or v3.7:** Help → Check for updates installs v3.8 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
||||
**From v3.6, v3.7 or v3.8:** Help → Check for updates installs v3.9 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
||||
|
||||
**From v1.9–v3.5:** Check for updates works, but it uses your current version's older updater for this one hop. If auto-update has been failing on your machine, install by hand using the steps above.
|
||||
|
||||
|
||||
+52
-2
@@ -48,6 +48,7 @@ ul, ol { padding-left: 1.4em; }
|
||||
<li><a href="#updates">Updating RemSound</a></li>
|
||||
<li><a href="#recording">Recording to a file</a></li>
|
||||
<li><a href="#logs">Logs and diagnostics</a></li>
|
||||
<li><a href="#command-line">Command-line options</a></li>
|
||||
<li><a href="#troubleshooting">Troubleshooting</a></li>
|
||||
<li><a href="#glossary">Glossary</a></li>
|
||||
</ol>
|
||||
@@ -1135,7 +1136,56 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
<li><strong>OpusUnrecoveredGaps</strong> — a running total of multi-packet losses that Opus couldn't repair. Each one is an audible click. It stays at 0 on a clean connection; small numbers are normal over the internet.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="troubleshooting">22. Troubleshooting</h2>
|
||||
<h2 id="command-line">22. Command-line options</h2>
|
||||
|
||||
<p>RemSound is normally a windowed program you click to open. But it can also take <strong>command-line options</strong> — short text instructions you type after the program name. They are handy for three things: checking a machine quickly (what devices are present, does the audio path work at all), getting a support report to send to whoever helps you, and starting RemSound a particular way from a shortcut or a script.</p>
|
||||
|
||||
<p>To use them, open a command prompt (press the Windows key, type <code>cmd</code>, press Enter), then run RemSound with the option after it. If RemSound is on your desktop you can type the whole path in quotes, for example:</p>
|
||||
|
||||
<pre>
|
||||
"C:\Users\you\Desktop\RemSound\RemSound.exe" --devices
|
||||
</pre>
|
||||
|
||||
<p>The options that just report something print their answer straight into the same command window as plain text — a screen reader reads it normally — and then RemSound exits without opening a window. The start-up options open RemSound as usual, just set up the way you asked.</p>
|
||||
|
||||
<h3>Options that print something and then exit</h3>
|
||||
<table>
|
||||
<tr><th>Option</th><th>What it does</th></tr>
|
||||
<tr><td><code>--help</code> or <code>-h</code></td><td>Lists every option, the same as this section in short form.</td></tr>
|
||||
<tr><td><code>--version</code></td><td>Prints which version of RemSound is installed, for example “RemSound 3.9”.</td></tr>
|
||||
<tr><td><code>--devices</code></td><td>Lists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine — each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows.</td></tr>
|
||||
<tr><td><code>--selftest</code></td><td>Runs a complete round-trip on the machine on its own: it captures sound, encodes it, sends it across the network layer to itself, receives it and decodes it, then reports <strong>PASS</strong> or <strong>FAIL</strong>. No sound is played out, so it is safe to run silently. A PASS proves capture, encoding, the network code and decoding are all working on that computer. Add <code>--opus</code> to test the Opus codec path, or <code>--seconds N</code> to run it for longer than the default.</td></tr>
|
||||
<tr><td><code>--diagnostics</code></td><td>Writes a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the <strong>user settings and logs</strong> folder and prints where it put it; you can also give a path, for example <code>--diagnostics C:\Users\you\Desktop\report.txt</code>. This is the file to send when asking for help — it answers most questions in one go.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Options that change a setting or control a running copy, then exit</h3>
|
||||
<table>
|
||||
<tr><th>Option</th><th>What it does</th></tr>
|
||||
<tr><td><code>--log on</code> or <code>--log off</code></td><td>Turns the diagnostic log on or off. The change takes effect the next time RemSound starts. The same setting lives in the Preferences dialog; this is just a way to set it without opening the window.</td></tr>
|
||||
<tr><td><code>--close</code></td><td>Closes a copy of RemSound that is already running. Useful in a script that needs to restart it.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Options that change how RemSound starts</h3>
|
||||
<p>These open RemSound as normal, set up the way you ask, and are meant for shortcuts and scripts.</p>
|
||||
<table>
|
||||
<tr><th>Option</th><th>What it does</th></tr>
|
||||
<tr><td><code>--profile "<name>"</code></td><td>Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example <code>--profile "Studio link"</code>.</td></tr>
|
||||
<tr><td><code>--connect <ip></code></td><td>Starts and connects to a peer at that address. You can give just an address (<code>--connect 192.168.1.42</code>) or an address and port (<code>--connect 192.168.1.42:47830</code>); with no port it uses RemSound's normal port, 47830. If you don't also give a <code>--profile</code>, it starts on a fresh blank profile already pointed at that peer.</td></tr>
|
||||
<tr><td><code>--minimized</code> or <code>--tray</code></td><td>Starts minimized to the notification area, with no window popping up. Pair it with <code>--profile</code> or <code>--connect</code> so it has something to do without waiting at the picker.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Examples</h3>
|
||||
<pre>
|
||||
RemSound.exe --devices
|
||||
RemSound.exe --selftest --opus
|
||||
RemSound.exe --diagnostics
|
||||
RemSound.exe --profile "Studio" --minimized
|
||||
RemSound.exe --connect 192.168.1.42
|
||||
</pre>
|
||||
|
||||
<p>A common support sequence: ask the person to run <code>--diagnostics</code> and send you the file, then have them run <code>--selftest</code> — if that says PASS, capture, encoding and the audio path are all sound on their machine and the problem is somewhere in the connection between you.</p>
|
||||
|
||||
<h2 id="troubleshooting">23. Troubleshooting</h2>
|
||||
|
||||
<h3>I don't hear my friend</h3>
|
||||
<ol>
|
||||
@@ -1207,7 +1257,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
</ul>
|
||||
<p>If none of those apply, just fall back to Tailscale — it works without involving the router at all.</p>
|
||||
|
||||
<h2 id="glossary">23. Glossary</h2>
|
||||
<h2 id="glossary">24. Glossary</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Term</th><th>Meaning</th></tr>
|
||||
|
||||
@@ -30,11 +30,35 @@ internal sealed class AboutDialog : Form
|
||||
your chosen latency, gently and silently, so a long
|
||||
session stays as tight as it began.
|
||||
|
||||
Fixed a long-standing cause of one-way silence: if you
|
||||
sent plain (non-ASIO) audio to someone who had an ASIO
|
||||
device selected, your sound — your microphone included —
|
||||
could arrive at their machine and then never be played,
|
||||
leaving them in silence. It now always reaches their
|
||||
speakers, whatever mix of ASIO and standard audio the two
|
||||
of you happen to be using.
|
||||
|
||||
RemSound's log now also records whether your microphone
|
||||
audio is actually leaving the machine, alongside how loud
|
||||
it is — so a "my mic isn't getting through" report can be
|
||||
pinned down from the log instead of guessed at.
|
||||
|
||||
RemSound no longer sends audio out when no one is actually
|
||||
connected. It waits until a peer is genuinely reachable, so
|
||||
a profile left open on its own sits quietly instead of
|
||||
streaming into nothing.
|
||||
|
||||
RemSound now plays a short sound as it starts up, so you
|
||||
know it's running even when it opens straight to the
|
||||
notification area. You can turn it off, or choose your own
|
||||
sound, under Options → Preferences.
|
||||
|
||||
And RemSound can now be driven from the command line for
|
||||
quick checks and support — list the audio devices, run a
|
||||
self-test of the whole audio path, or write a diagnostics
|
||||
report to send for help. Press F1 and see "Command-line
|
||||
options" in the manual for the full list.
|
||||
|
||||
RemSound v3.8
|
||||
|
||||
You can now start a brand-new profile at any time. A
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32;
|
||||
using NAudio.CoreAudioApi;
|
||||
using NAudio.Wave;
|
||||
using RemSound.Core;
|
||||
using RemSound.Receiver;
|
||||
using RemSound.Sender;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Command-line interface for RemSound. RemSound is primarily a GUI app, but - inspired by Andre's
|
||||
/// Sensor Readout - it also accepts command-line options so a screen-reader user (or a script) can
|
||||
/// list devices, run an audio self-test, dump a diagnostics bundle, toggle logging, close a running
|
||||
/// copy, or launch straight into a profile / connection without touching the UI.
|
||||
///
|
||||
/// Two kinds of option:
|
||||
/// * "do-and-exit" commands (--help, --version, --devices, --selftest, --diagnostics, --log,
|
||||
/// --close) print to the calling terminal (and/or a file) and terminate the process.
|
||||
/// * "launch options" (--profile, --connect, --minimized) modify a normal GUI start.
|
||||
///
|
||||
/// Wired into <see cref="Program"/> right after the legacy-layout migration and before the
|
||||
/// single-instance guard. The do-and-exit commands need no window and no instance lock.
|
||||
/// </summary>
|
||||
internal static class CommandLine
|
||||
{
|
||||
/// <summary>Overrides applied to a normal GUI launch when no do-and-exit command ran.</summary>
|
||||
internal sealed class LaunchOverrides
|
||||
{
|
||||
public bool StartMinimized;
|
||||
public string? ProfileName;
|
||||
public readonly List<IPEndPoint> ConnectPeers = new();
|
||||
/// <summary>--connect given with no --profile → start on a blank profile and connect.</summary>
|
||||
public bool ForceBlankProfile;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll")] private static extern bool AttachConsole(int dwProcessId);
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Process the command line. Returns a non-null exit code when a do-and-exit command ran (the
|
||||
/// caller should <c>Environment.Exit</c> it); returns null to continue into the GUI launch with
|
||||
/// <paramref name="overrides"/> populated.
|
||||
/// </summary>
|
||||
public static int? Process(string[] args, out LaunchOverrides overrides)
|
||||
{
|
||||
overrides = new LaunchOverrides();
|
||||
if (args.Length == 0) return null;
|
||||
|
||||
// --- do-and-exit commands (first match wins) ---
|
||||
foreach (var raw in args)
|
||||
{
|
||||
switch (raw.ToLowerInvariant())
|
||||
{
|
||||
case "--help": case "-h": case "/?": case "-?": case "--?":
|
||||
return WithConsole(PrintHelp);
|
||||
case "--version": case "-v": case "--ver":
|
||||
return WithConsole(PrintVersion);
|
||||
case "--devices": case "--list-devices":
|
||||
return WithConsole(() => { WriteDevices(Console.Out); return 0; });
|
||||
case "--selftest": case "--self-test":
|
||||
return WithConsole(() => RunSelfTest(args));
|
||||
case "--diagnostics": case "--diag":
|
||||
return WithConsole(() => RunDiagnostics(ValueAfter(args, raw)));
|
||||
case "--log":
|
||||
return WithConsole(() => SetLogging(ValueAfter(args, raw)));
|
||||
case "--close": case "--quit":
|
||||
return WithConsole(CloseRunning);
|
||||
}
|
||||
}
|
||||
|
||||
// --- launch options (applied to the normal GUI start) ---
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i].ToLowerInvariant())
|
||||
{
|
||||
case "--minimized": case "--minimised": case "--tray":
|
||||
overrides.StartMinimized = true;
|
||||
break;
|
||||
case "--profile":
|
||||
overrides.ProfileName = ValueAt(args, i + 1);
|
||||
break;
|
||||
case "--connect":
|
||||
if (TryParsePeer(ValueAt(args, i + 1), out var ep)) overrides.ConnectPeers.Add(ep);
|
||||
break;
|
||||
}
|
||||
}
|
||||
overrides.ForceBlankProfile = overrides.ConnectPeers.Count > 0 && overrides.ProfileName is null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------- console plumbing ----------------
|
||||
|
||||
/// <summary>Attach to the calling terminal (when launched from one), point Console.Out at the
|
||||
/// real stdout handle (works for an interactive console AND a redirected pipe), run the command,
|
||||
/// and return its exit code. A WinExe has no console of its own, hence the attach dance.</summary>
|
||||
private static int WithConsole(Func<int> body)
|
||||
{
|
||||
try { AttachConsole(ATTACH_PARENT_PROCESS); } catch { /* no parent console - fine */ }
|
||||
try { Console.OutputEncoding = Encoding.UTF8; } catch { /* redirected / no console */ }
|
||||
try { Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true }); }
|
||||
catch { /* no usable stdout - file-writing commands still work */ }
|
||||
try { return body(); }
|
||||
catch (Exception ex) { Console.WriteLine($"RemSound: error - {ex.GetType().Name}: {ex.Message}"); return 1; }
|
||||
}
|
||||
|
||||
// ---------------- commands ----------------
|
||||
|
||||
private static string AppVersion =>
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||
|
||||
private static int PrintVersion()
|
||||
{
|
||||
Console.WriteLine($"RemSound {AppVersion}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int PrintHelp()
|
||||
{
|
||||
Console.WriteLine($"RemSound {AppVersion} - command-line options");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Run RemSound.exe with no options to start normally.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Information and tests (these print, then exit):");
|
||||
Console.WriteLine(" --help, -h Show this help.");
|
||||
Console.WriteLine(" --version Show the installed version.");
|
||||
Console.WriteLine(" --devices List all microphones, outputs and ASIO drivers,");
|
||||
Console.WriteLine(" with their formats and device ids.");
|
||||
Console.WriteLine(" --selftest [--opus] Run a localhost audio round-trip (capture -> encode ->");
|
||||
Console.WriteLine(" [--seconds N] network -> decode) and report PASS or FAIL.");
|
||||
Console.WriteLine(" --diagnostics [path] Write a diagnostics report (version, config, profiles,");
|
||||
Console.WriteLine(" devices, mic-privacy check, recent log) and exit. With");
|
||||
Console.WriteLine(" no path, it is saved in the user settings and logs folder.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Settings and control (these act, then exit):");
|
||||
Console.WriteLine(" --log on|off Turn the diagnostic log on or off.");
|
||||
Console.WriteLine(" --close Close a running copy of RemSound.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Start-up options (these change how RemSound launches):");
|
||||
Console.WriteLine(" --profile \"<name>\" Start straight into the named profile (skip the picker).");
|
||||
Console.WriteLine(" --connect <ip[:port]> Start and connect to a peer at this address. With no");
|
||||
Console.WriteLine(" --profile, starts on a fresh profile connected to it.");
|
||||
Console.WriteLine(" --minimized, --tray Start minimized to the notification area.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" RemSound.exe --devices");
|
||||
Console.WriteLine(" RemSound.exe --selftest --opus");
|
||||
Console.WriteLine(" RemSound.exe --diagnostics");
|
||||
Console.WriteLine(" RemSound.exe --profile \"Studio\" --minimized");
|
||||
Console.WriteLine(" RemSound.exe --connect 192.168.1.42");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Write the full device inventory (capture inputs, render outputs, ASIO drivers) to a
|
||||
/// writer - shared by <c>--devices</c> and the diagnostics report. Formats come straight from
|
||||
/// each endpoint's mix format, the same value the audio engine negotiates.</summary>
|
||||
private static void WriteDevices(TextWriter w)
|
||||
{
|
||||
using var en = new MMDeviceEnumerator();
|
||||
|
||||
void ListEndpoints(DataFlow flow, string header)
|
||||
{
|
||||
w.WriteLine(header);
|
||||
string? defaultId = null;
|
||||
try { using var def = en.GetDefaultAudioEndpoint(flow, Role.Multimedia); defaultId = def.ID; }
|
||||
catch { /* no default of this kind */ }
|
||||
var any = false;
|
||||
foreach (var d in en.EnumerateAudioEndPoints(flow, DeviceState.Active))
|
||||
{
|
||||
any = true;
|
||||
var isDefault = defaultId is not null && d.ID == defaultId ? " [default]" : "";
|
||||
string fmt;
|
||||
try
|
||||
{
|
||||
var f = d.AudioClient.MixFormat;
|
||||
var enc = f.Encoding is WaveFormatEncoding.IeeeFloat or WaveFormatEncoding.Extensible
|
||||
? "float" : f.Encoding.ToString();
|
||||
fmt = $"{f.SampleRate} Hz, {f.Channels} ch, {f.BitsPerSample}-bit {enc}";
|
||||
}
|
||||
catch { fmt = "format unavailable"; }
|
||||
w.WriteLine($" {d.FriendlyName}{isDefault}");
|
||||
w.WriteLine($" {fmt}");
|
||||
w.WriteLine($" id: {d.ID}");
|
||||
d.Dispose();
|
||||
}
|
||||
if (!any) w.WriteLine(" (none)");
|
||||
w.WriteLine();
|
||||
}
|
||||
|
||||
ListEndpoints(DataFlow.Capture, "Microphones / line-in (WASAPI capture inputs):");
|
||||
ListEndpoints(DataFlow.Render, "Speakers / headphones (WASAPI outputs; also capturable as system-audio loopback):");
|
||||
|
||||
w.WriteLine("ASIO drivers:");
|
||||
IReadOnlyList<string> asio;
|
||||
try { asio = AsioDeviceProbe.EnumerateDriverNames(); } catch { asio = Array.Empty<string>(); }
|
||||
if (asio.Count == 0) w.WriteLine(" (none installed)");
|
||||
else foreach (var n in asio) w.WriteLine($" {n}");
|
||||
w.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>Localhost audio round-trip: capture the default output (as loopback) → encode →
|
||||
/// send to 127.0.0.1 → receive → decode. The receiver renders to nothing (no sound), so this is
|
||||
/// safe to run any time. PASS when packets flow end-to-end; exit code 0 = PASS, 1 = FAIL.</summary>
|
||||
private static int RunSelfTest(string[] args)
|
||||
{
|
||||
var opus = args.Any(a => a.Equals("--opus", StringComparison.OrdinalIgnoreCase));
|
||||
var seconds = int.TryParse(ValueAfter(args, "--seconds"), out var s) && s is > 0 and <= 60 ? s : 5;
|
||||
|
||||
Console.WriteLine($"RemSound self-test: localhost {(opus ? "Opus" : "PCM")} round-trip for {seconds}s...");
|
||||
|
||||
IReadOnlyList<AudioDeviceChoice> outputs;
|
||||
try { outputs = AudioDeviceCatalog.LoadOutputs(); }
|
||||
catch (Exception ex) { Console.WriteLine($" could not enumerate outputs: {ex.Message}"); return 1; }
|
||||
var dev = outputs.FirstOrDefault(o => o.DeviceId is not null);
|
||||
var deviceId = dev?.DeviceId;
|
||||
if (dev is null || deviceId is null)
|
||||
{
|
||||
Console.WriteLine(" RESULT: SKIP - no usable output device to capture from.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var receiver = new AudioReceiver();
|
||||
using var sender = new AudioSender();
|
||||
try
|
||||
{
|
||||
receiver.Start();
|
||||
receiver.SetOutputDevices(Array.Empty<string>()); // decode only - never make sound during a test
|
||||
sender.ConfigureCodec(opus ? AudioTransportCodec.Opus : AudioTransportCodec.Pcm);
|
||||
sender.Configure(new[] { new CaptureSourceSpec(deviceId, CaptureKind.Loopback, dev.Name) });
|
||||
sender.SetReceivers(new[] { new IPEndPoint(IPAddress.Loopback, RemPacket.DefaultPort) });
|
||||
sender.Start();
|
||||
Console.WriteLine($" capturing \"{dev.Name}\" -> 127.0.0.1:{RemPacket.DefaultPort}");
|
||||
Thread.Sleep(seconds * 1000);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { sender.Stop(); } catch { /* ignore */ }
|
||||
try { receiver.Stop(); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
var sent = sender.PacketsSent;
|
||||
var got = receiver.PacketsReceived;
|
||||
Console.WriteLine($" packets sent={sent} received={got} bytes received={receiver.BytesReceived}");
|
||||
var pass = sent > 0 && got > 0;
|
||||
Console.WriteLine(pass
|
||||
? " RESULT: PASS - capture, encode, network and decode are all working."
|
||||
: " RESULT: FAIL - audio did not flow end-to-end (sent or received was zero).");
|
||||
return pass ? 0 : 1;
|
||||
}
|
||||
|
||||
private static int SetLogging(string? value)
|
||||
{
|
||||
var on = value is not null && value.ToLowerInvariant() is "on" or "true" or "1" or "enable" or "enabled" or "yes";
|
||||
var off = value is not null && value.ToLowerInvariant() is "off" or "false" or "0" or "disable" or "disabled" or "no";
|
||||
if (!on && !off)
|
||||
{
|
||||
Console.WriteLine("Usage: --log on (or) --log off");
|
||||
return 1;
|
||||
}
|
||||
var cfg = AppConfig.Load();
|
||||
cfg.LoggingEnabled = on;
|
||||
try { cfg.Save(); }
|
||||
catch (Exception ex) { Console.WriteLine($"Could not save the setting: {ex.Message}"); return 1; }
|
||||
Console.WriteLine($"RemSound logging is now {(on ? "ON" : "OFF")}. The change takes effect next time RemSound starts.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int CloseRunning()
|
||||
{
|
||||
bool closed;
|
||||
try { closed = SingleInstanceCoordinator.ForceCloseOtherInstances(); }
|
||||
catch (Exception ex) { Console.WriteLine($"Could not close RemSound: {ex.Message}"); return 1; }
|
||||
Console.WriteLine(closed
|
||||
? "Closed the running copy of RemSound."
|
||||
: "No running copy of RemSound was found (or it could not be closed - it may be running as administrator).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Write a support-friendly diagnostics report and exit. Always writes a file so it
|
||||
/// works even when launched without a terminal; prints the path if a console is attached.</summary>
|
||||
private static int RunDiagnostics(string? pathArg)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"RemSound diagnostics");
|
||||
sb.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
sb.AppendLine($"Version: {AppVersion}");
|
||||
sb.AppendLine($"Machine: {Environment.MachineName}");
|
||||
sb.AppendLine($"OS: {Environment.OSVersion}");
|
||||
sb.AppendLine($".NET: {Environment.Version}");
|
||||
sb.AppendLine($"Exe: {Environment.ProcessPath}");
|
||||
sb.AppendLine();
|
||||
|
||||
AppConfig cfg;
|
||||
try { cfg = AppConfig.Load(); } catch { cfg = new AppConfig(); }
|
||||
sb.AppendLine("Settings:");
|
||||
sb.AppendLine($" Logging enabled: {cfg.LoggingEnabled}");
|
||||
sb.AppendLine($" Start minimised: {cfg.StartMinimised}");
|
||||
sb.AppendLine($" Start with profile: {cfg.StartWithProfileTitle ?? "(picker)"}");
|
||||
sb.AppendLine($" Profiles folder: {cfg.ProfilesDirectory ?? AppConfig.ProfilesBaseDirectory}");
|
||||
sb.AppendLine($" Update check frequency: {cfg.UpdateCheckFrequency}");
|
||||
sb.AppendLine($" Startup cue enabled: {cfg.EnableStartupCue}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Profiles:");
|
||||
try
|
||||
{
|
||||
var store = cfg.CreateStore();
|
||||
var titles = store.ListProfileTitles();
|
||||
if (titles.Count == 0) sb.AppendLine(" (none)");
|
||||
else foreach (var t in titles) sb.AppendLine($" {t}{(store.IsProfileReadOnly(t) ? " (read-only)" : "")}");
|
||||
}
|
||||
catch (Exception ex) { sb.AppendLine($" (could not list profiles: {ex.Message})"); }
|
||||
sb.AppendLine();
|
||||
|
||||
try { using var sw = new StringWriter(sb); WriteDevices(sw); }
|
||||
catch (Exception ex) { sb.AppendLine($"Devices: (could not enumerate: {ex.Message})"); sb.AppendLine(); }
|
||||
|
||||
sb.AppendLine("Microphone privacy (Windows):");
|
||||
sb.AppendLine($" {DescribeMicPrivacy()}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Most recent log (tail):");
|
||||
sb.AppendLine(TailNewestLog(40));
|
||||
sb.AppendLine();
|
||||
|
||||
var path = !string.IsNullOrWhiteSpace(pathArg)
|
||||
? pathArg!
|
||||
: Path.Combine(AppConfig.UserDataDirectory,
|
||||
$"RemSound-diagnostics-{Environment.MachineName}-{DateTime.Now:yyyyMMdd-HHmmss}.txt");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
||||
File.WriteAllText(path, sb.ToString());
|
||||
Console.WriteLine($"Diagnostics written to:");
|
||||
Console.WriteLine($" {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Could not write the diagnostics file: {ex.Message}");
|
||||
Console.WriteLine();
|
||||
Console.Write(sb.ToString()); // last resort - dump to the terminal
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string DescribeMicPrivacy()
|
||||
{
|
||||
try
|
||||
{
|
||||
const string consent = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone";
|
||||
var perUser = Registry.GetValue(consent, "Value", null) as string;
|
||||
var policy = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy", "LetAppsAccessMicrophone", null);
|
||||
var bits = new List<string> { $"user mic access = {perUser ?? "Allow (default)"}" };
|
||||
if (policy is int p) bits.Add($"group-policy LetAppsAccessMicrophone = {p}{(p == 2 ? " (FORCE DENY)" : "")}");
|
||||
return string.Join("; ", bits);
|
||||
}
|
||||
catch (Exception ex) { return $"(could not read: {ex.Message})"; }
|
||||
}
|
||||
|
||||
private static string TailNewestLog(int lines)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = AppConfig.LogsDirectory;
|
||||
if (!Directory.Exists(dir)) return " (no logs folder - logging may be off)";
|
||||
var newest = new DirectoryInfo(dir).GetFiles("*.log")
|
||||
.OrderByDescending(f => f.LastWriteTimeUtc).FirstOrDefault();
|
||||
if (newest is null) return " (no log files - logging may be off)";
|
||||
var all = File.ReadAllLines(newest.FullName);
|
||||
var tail = all.Length <= lines ? all : all[^lines..];
|
||||
return $" file: {newest.Name}" + Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, tail.Select(l => " " + l));
|
||||
}
|
||||
catch (Exception ex) { return $" (could not read log: {ex.Message})"; }
|
||||
}
|
||||
|
||||
// ---------------- arg helpers ----------------
|
||||
|
||||
/// <summary>The token after the first occurrence of <paramref name="flag"/>, unless that token
|
||||
/// is itself a flag (starts with '-'); null when absent. Used for optional values like a path.</summary>
|
||||
private static string? ValueAfter(string[] args, string flag)
|
||||
{
|
||||
for (var i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (args[i].Equals(flag, StringComparison.OrdinalIgnoreCase))
|
||||
return ValueAt(args, i + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ValueAt(string[] args, int index) =>
|
||||
index >= 0 && index < args.Length && !args[index].StartsWith('-') ? args[index] : null;
|
||||
|
||||
private static bool TryParsePeer(string? text, out IPEndPoint endpoint)
|
||||
{
|
||||
endpoint = new IPEndPoint(IPAddress.Loopback, RemPacket.DefaultPort);
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
var parts = text.Split(':');
|
||||
if (parts.Length == 1 && IPAddress.TryParse(parts[0], out var ip1))
|
||||
{
|
||||
endpoint = new IPEndPoint(ip1, RemPacket.DefaultPort);
|
||||
return true;
|
||||
}
|
||||
if (parts.Length == 2 && IPAddress.TryParse(parts[0], out var ip2)
|
||||
&& int.TryParse(parts[1], out var port) && port is > 0 and <= 65535)
|
||||
{
|
||||
endpoint = new IPEndPoint(ip2, port);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,15 @@ internal static class Program
|
||||
// a no-op once they're gone. 2026-06-08.
|
||||
CleanUpLegacyRootSounds();
|
||||
|
||||
// Command-line interface (Sensor-Readout-style). "Do-and-exit" commands (--devices,
|
||||
// --selftest, --diagnostics, --version, --log, --close, --help) run here — before the
|
||||
// single-instance lock and before any window — and terminate the process. Otherwise we
|
||||
// collect launch overrides (--profile / --connect / --minimized) and continue the normal
|
||||
// GUI start below, applying them as we resolve the profile.
|
||||
var cliExit = CommandLine.Process(args, out var cli);
|
||||
if (cliExit is { } cliCode) Environment.Exit(cliCode);
|
||||
if (cli.StartMinimized) MainForm.startNextInstanceMinimized = true;
|
||||
|
||||
// Single-instance guard. RemSound must never run as two copies at once: with the
|
||||
// auto-updater relaunching the app, a copy that didn't exit cleanly used to leave two
|
||||
// (then more) copies running, each playing received audio — Andre's "stacked and
|
||||
@@ -155,6 +164,20 @@ internal static class Program
|
||||
Profile? autoLoaded = null;
|
||||
string? autoLoadedTitle = null;
|
||||
|
||||
// CLI overrides take precedence over the resume sentinel and the "start with profile"
|
||||
// setting. --profile loads a named profile; --connect with no --profile starts blank
|
||||
// (the requested peer is added to whatever profile loads, further below).
|
||||
if (cli.ProfileName is not null)
|
||||
{
|
||||
try { autoLoaded = store.Load(cli.ProfileName); if (autoLoaded is not null) autoLoadedTitle = cli.ProfileName; }
|
||||
catch { /* fall through to the normal resolution */ }
|
||||
}
|
||||
else if (cli.ForceBlankProfile)
|
||||
{
|
||||
autoLoaded = Profile.NewBlank();
|
||||
autoLoadedTitle = null;
|
||||
}
|
||||
|
||||
var resumeSentinelPath = Path.Combine(AppContext.BaseDirectory, RemSoundUpdater.ResumeProfileSentinelName);
|
||||
string? resumeTitle = null;
|
||||
if (File.Exists(resumeSentinelPath))
|
||||
@@ -167,7 +190,7 @@ internal static class Program
|
||||
try { File.Delete(resumeSentinelPath); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resumeTitle))
|
||||
if (autoLoaded is null && !string.IsNullOrWhiteSpace(resumeTitle))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -204,6 +227,19 @@ internal static class Program
|
||||
title = dialog.SelectedTitle;
|
||||
}
|
||||
|
||||
// Apply --connect: add the requested peer address(es) to the loaded profile so MainForm
|
||||
// selects and connects to them on startup. Stored as plain address strings, the same
|
||||
// shape "Add peer by IP" produces (the audio port is the default unless one is given).
|
||||
if (cli.ConnectPeers.Count > 0 && profile is not null)
|
||||
{
|
||||
foreach (var ep in cli.ConnectPeers)
|
||||
{
|
||||
var addr = ep.Port == RemPacket.DefaultPort ? ep.Address.ToString() : $"{ep.Address}:{ep.Port}";
|
||||
if (!profile.RememberedPeers.Contains(addr)) profile.RememberedPeers.Add(addr);
|
||||
if (!profile.SelectedConnectedPeers.Contains(addr)) profile.SelectedConnectedPeers.Add(addr);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch-profile loop: when the user clicks "Switch to profile" in the Manage
|
||||
// Profiles dialog, the form sets NextProfileTitleToLoad and closes; we re-open
|
||||
// MainForm under the newly chosen profile. Null = user closed the form normally
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>3.9.1</Version>
|
||||
<Version>3.9</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user