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

3.4 KiB

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.