Files
erikbrockoandClaude Sonnet 5.5 c01032a2cc Add a sound recorder that writes several devices to one Opus file
nonemm-recorder is a separate program (src/Nonemm.Recorder.App) over a
library (src/Nonemm.Recorder). Each row in its list is one channel of one
device, and each row becomes one channel in an Ogg Opus file: 60 ms frames,
complexity 9, 32 kbps per channel by default.

- A device that is not connected keeps its row, in red, and writes silence.
  It is recorded from the moment it returns, also during a recording.
- Output devices are recorded as loopback: WASAPI on Windows, the
  "Monitor of" sources on PulseAudio and PipeWire.
- A headphone button plays one channel, in mono, on the default device.
- The rows, bitrate and folder are saved when the window closes.

SoundFlow 1.4.2 numbers its backend enum one higher than the miniaudio it
ships, so asking for PulseAudio gave ALSA and Wasapi gave DirectSound.
SoundFlowBackend lowers the value by one and checks which backend started.

Output devices and live capture are not tested on real hardware. The
tests use a fake sound system and decode the written file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 23:47:45 +00:00

75 lines
3.4 KiB
Markdown

# Sound recorder
`nonemm-recorder` is a separate program (`src/Nonemm.Recorder.App`). It records
channels of several audio devices into one Opus file. The code that does the
work is in `src/Nonemm.Recorder`, and the window is a thin layer over it.
Run it with `./build.sh run --project src/Nonemm.Recorder.App`.
## The file
- Ogg Opus, one channel in the file for each row in the list, in list order.
- Mapping family 255: each channel is its own mono stream, so a player does
not mix them as speakers. A file with one row is plain mono.
- 48 kHz, 60 ms frames, complexity 9. The bitrate is for each channel and
defaults to 32 kbps, so a file of four rows is about 128 kbps.
- A new file is named `recording-yyyyMMdd-HHmmss.opus` in the chosen folder.
- Pause leaves no gap: the audio after Resume follows directly on the audio
before Pause.
## Channels
- Each row picks one channel of one device: `Line In L`, `Line In R`, or just
`Microphone` for a mono device. The list of choices is the same in every row.
- Output devices are listed as `Speakers (output) L`. They record what that
device plays (see "Output devices").
- A device that is not there keeps its row, in red, and writes silence. The
program lists the devices every 2 seconds. When the device returns, its
channel is recorded from that moment, also during a recording.
- Rows cannot be added, removed or changed during a recording, as they would
change the channel count of the file.
- The headphone button plays that channel, in mono, on the default playback
device. If it monitors an output device that is also the default one, it
feeds back.
## Configuration
One file, written when the window closes: `nonemm-recorder/config.json` in the
user's application data folder (`%APPDATA%` on Windows, `~/.config` on Linux).
It holds the rows, the bitrate and the folder. Rows of devices that were not
connected stay in it.
## Output devices
| System | How an output is recorded | Tested |
| --- | --- | --- |
| Windows | WASAPI loopback of that playback device | no |
| Linux | the PulseAudio or PipeWire "Monitor of ..." source | no |
Neither was tested on real hardware: the machine this was written on has no
sound device. The tests in `tests/Nonemm.Recorder.Tests` use a fake sound
system and check the file by decoding it again.
On Windows, SoundFlow only loopback-records the default output through its
public API. `SoundFlowBackend.MarkLoopback` sets an internal setting by
reflection to pick any output, so it is tied to SoundFlow 1.4.2.
## Sound system
On Linux the program asks for PulseAudio (PipeWire serves it) and falls back to
ALSA only if that fails. The window shows which one is in use, and the reason
in red when it is the fallback. ALSA lists sound cards and plugins, not the
devices a PipeWire desktop shows, and has no "Monitor of ..." sources.
SoundFlow 1.4.2 numbers its backend enum one higher than the miniaudio library
it ships, so asking for `PulseAudio` gives ALSA and `Wasapi` gives DirectSound.
`SoundFlowBackend.Start` lowers the value by one and checks the result. If a
later SoundFlow fixes the numbering, that check fails with a message.
## Timing
The devices do not share a clock, so the file is cut by the system clock, not by
how much each device delivered. The encoder runs 120 ms behind the clock. A
device that delivers too little is padded with zeros. One that delivers too
much loses its oldest samples once it is 240 ms ahead.