A small ncurses FM radio controller for Linux, built for V4L2-compatible tuner
cards and USB radio sticks accessible as /dev/radio0. Includes a live audio
pipe that routes the tuner's capture output to the system's default playback
device. Tested on Fedora 44 with ADS Tech InstantFM Music RDX-155.
Runtime:
- Linux kernel with V4L2 radio support (
/dev/radio0) - PipeWire (preferred) or ALSA (
libasound) for audio output (optional; falls back to tuner control only) libudevfor automatic audio device detection (optional; falls back to sysfs)- Recording to WAV needs no library; MP3 (
libmp3lame), OGG Vorbis (libvorbisenc/libvorbis/libogg), and FLAC (libFLAC) are each optional and independent.libsamplerate(optional) lets WAV/OGG/FLAC record at a configured rate different from the capture rate; without it they always record at the native capture rate (MP3 is unaffected — lame resamples internally).
Build:
ncursesdevelopment headers (ncurses-devel/libncurses-dev)libpipewire-0.3development headers (pipewire-devel/libpipewire-0.3-dev) — preferredalsa-libdevelopment headers (alsa-lib-devel/libasound2-dev) — fallback if PipeWire not foundlibudevdevelopment headers (systemd-devel/libudev-dev) — optionallamedevelopment headers (lame-devel/libmp3lame-dev) — optional, MP3 recordinglibvorbis/vorbis-develdevelopment headers (libvorbis-dev+libvorbisenc2/vorbis-devel) — optional, OGG recordingflacdevelopment headers (flac-devel/libflac-dev) — optional, FLAC recordinglibsampleratedevelopment headers (libsamplerate-devel/libsamplerate0-dev) — optional, resampling for WAV/OGG/FLAC- GCC and GNU make
./configure
makeconfigure detects audio backends automatically: PipeWire is used when
available; ALSA (libasound) is the fallback. It writes config.mk. Run
./configure --help for all options:
| Option | Effect |
|---|---|
--disable-audio |
Build without audio support |
--enable-audio |
Require audio (fail if no backend found) |
--disable-pipewire |
Prefer ALSA even if PipeWire is available |
--disable-udev |
Use sysfs-only device autodetection; do not link libudev |
--disable-record |
Build without recording support at all (including WAV) |
--disable-lame |
Build without MP3 recording support |
--disable-vorbis |
Build without OGG Vorbis recording support |
--disable-flac |
Build without FLAC recording support |
--disable-samplerate |
Build without libsamplerate; WAV/OGG/FLAC always record at the native capture rate |
--disable-eq |
Build without the 11-band equalizer (omits eq.c and -lm) |
Optional install to /usr/local/bin:
sudo make install./ncradio # uses /dev/radio0
./ncradio /dev/radio1 # alternate device
./ncradio -v # print version and build configuration, then exit
./ncradio --version # sameThe version output shows the audio backend and optional component versions:
ncradio 0.1 built May 30 2026 18:34:51
by Constantinos Tsakiris
Audio backend: PipeWire 1.6.6
Device autodetect: libudev 259 + sysfs
Recording formats: WAV MP3(lame 3.100) OGG(vorbis Xiph.Org libVorbis 1.3.7) FLAC(1.5.0)
Resampling (non-MP3): libsamplerate
or, when built against ALSA:
ncradio 0.1 built May 30 2026 18:34:51
by Constantinos Tsakiris
Audio backend: ALSA (libasound 1.2.15.3)
Device autodetect: libudev 259 + sysfs
Recording formats: WAV MP3(lame 3.100) OGG(vorbis Xiph.Org libVorbis 1.3.7) FLAC(1.5.0)
Resampling (non-MP3): libsamplerate
Any codec or the resampler not found at build time is simply omitted from
these lines (e.g. Recording formats: WAV only, with
Resampling (non-MP3): disabled (native rate only), in a minimal build).
The tuner device must be readable and writable by the current user. On most
distributions, add yourself to the video group:
sudo usermod -aG video $USEROn startup ncradio restores the last tuned frequency and volume from
~/.ncradio.conf. On exit the tuner is muted, then the current frequency
and volume are persisted.
| Key | Action |
|---|---|
s |
Full band scan (87.50 – 108.00 MHz) |
, |
Step frequency down by the configured scan step |
. |
Step frequency up by the configured scan step |
< |
Seek backward — find previous station at or above signal threshold |
> |
Seek forward — find next station at or above signal threshold |
t |
Manual tune — type a frequency in MHz, then Enter |
+ or = |
Volume up (5% step) |
- |
Volume down (5% step) |
m |
Toggle mute |
r |
Start recording, in the configured format (requires audio enabled; prompts for filename) |
a |
Add current frequency to presets |
d |
Delete the highlighted preset |
e |
Rename the highlighted preset |
o |
Open settings panel |
E |
Open equalizer panel (11-band EQ; compiled in by default) |
↑ / ↓ |
Move selection down/up within the current preset column |
← / → |
Move selection one column left/right in the preset grid |
PgUp / PgDn |
Scroll preset list by one visible window of rows |
Enter |
Tune to the highlighted preset |
q |
Quit |
| Key | Action |
|---|---|
0–9 |
Enter digit |
. or , |
Decimal point (both accepted) |
Backspace |
Delete last character |
Enter |
Confirm and tune |
Esc |
Cancel |
| Key | Action |
|---|---|
| Printable chars | Append to name (max 32 characters) |
Backspace |
Delete last character |
Enter |
Save name |
Esc |
Cancel without saving |
| Key | Action |
|---|---|
s |
Stop scan early, save results |
Esc |
Stop scan, discard results |
| Key | Action |
|---|---|
| Any key | Cancel seek and restore previous frequency |
| Key | Action |
|---|---|
| Printable chars | Append to filename (the format's extension — .wav/.mp3/.ogg/.flac — added automatically if omitted) |
Backspace |
Delete last character |
Enter |
Start recording |
Esc |
Cancel |
All tuning, scanning, seeking, mute, and settings keys are blocked while recording.
| Key | Action |
|---|---|
s or Esc |
Stop recording and save file |
The info row shows - REC 0:00 filename with a running elapsed time.
| Key | Action |
|---|---|
↑ / ↓ |
Select setting |
← / → |
Adjust value |
Enter |
Toggle boolean settings |
Esc or o |
Close settings |
Opens a full-screen 11-band graphic equalizer covering 32 Hz – 20 kHz. A bar graph shows the current curve; the selected band is highlighted.
Band navigation and gain adjustment:
| Key | Action |
|---|---|
← / → |
Move selection left / right across bands |
↑ / ↓ |
Adjust selected band ±1 dB |
PgUp / PgDn |
Adjust selected band ±3 dB |
R |
Reset selected band to 0 dB |
0 |
Load the Flat preset (all bands 0 dB) |
Preset management:
| Key | Action |
|---|---|
[ / ] |
Cycle through presets (built-in and custom) |
S |
Save current gains as a new named custom preset |
Del Del |
Delete the active custom preset (press twice to confirm) |
Panel controls:
| Key | Action |
|---|---|
Space |
Toggle EQ on / off ([EQ] indicator on volume row) |
Esc or E |
Close EQ panel |
Built-in presets: Flat, Rock, Pop, Classical, Jazz, Electronic.
Custom presets are saved to ~/.ncradio.conf and survive restarts, as does the
name of the last active preset.
[ST] (green) is shown next to the frequency when the tuner detects a stereo
pilot tone. [MO] (dim) is shown when the signal is monophonic or too weak
for stereo decoding.
If the tuner hardware supports RDS (V4L2_TUNER_CAP_RDS), ncradio decodes
incoming RDS data automatically:
| Element | Location | Description |
|---|---|---|
| PS name | Right of volume bar | 8-character station name (e.g. Capital FM) appears in green once all 4 RDS segments arrive — typically 1–2 s |
| Radio Text | Info row | Up to 64-character "now playing" text; shown when no status message is pending |
RDS data is cleared whenever the frequency changes (tune, step, seek, or preset selection). On hardware without RDS support these areas stay blank.
[A] (green) appears on the volume row while the audio pipe is running.
[A!] (red) appears if the pipe stopped due to an error; the error text
is shown in the settings panel next to the Audio output row.
[EQ] appears when the equalizer is enabled.
Step the tuner by the configured Scan step in either direction. The same step size applies to both manual browsing and the automatic band scan.
A full band sweep from 87.50 to 108.00 MHz runs in a background thread so the UI stays responsive. A progress bar and a live list of found stations are shown during the sweep; the list auto-scrolls to always show the most recently found station.
RDS name collection — if Save RDS names is enabled (default: Yes), the
scanner dwells on each found station for up to 1.5 s to collect its RDS PS
name, exiting early once the name is received.
When the scan finishes (or is stopped), all found stations replace the
current preset list and are saved to ~/.ncradio.conf. The tuner returns to
the frequency it was on before the scan.
Seek steps through frequencies in the configured direction using the same scan step and signal threshold as the automatic scan, stopping at the first frequency that meets or exceeds the threshold. Seek runs in a background thread — the UI remains responsive and any keypress cancels it. If no qualifying station is found after a full sweep of the band, the tuner is restored to its pre-seek frequency and a "No station found" message is shown.
ncradio pipes the tuner's audio directly to the system's playback device. The
audio backend is selected at build time by configure.
When built with PipeWire, the audio pipe uses two PipeWire streams on a shared thread loop:
- A capture stream (
PW_DIRECTION_INPUT) that connects to the radio card's PipeWire source node. It proposes S16_LE with an open rate range so PipeWire selects the source's native sample rate, avoiding a resample on the capture side. - A playback stream (
PW_DIRECTION_OUTPUT) that connects to the selected sink (or PipeWire's default sink). It is connected after the capture format is negotiated, using the same rate, so at most one resample occurs (source native → sink native) rather than two.
Audio flows capture → lock-free ring buffer → playback, with PipeWire handling format negotiation and hardware scheduling on each side.
When built without PipeWire, the audio pipe uses libasound directly:
- Capture opens the configured
hw:X,Ydevice, probes the highest supported sample rate from{96000, 48000, 44100, 32000, 22050, 16000}Hz, and configures S16_LE stereo (falling back to mono). - Playback opens
"default"(which routes to PulseAudio, PipeWire, or ALSA hw as the system is configured) with the same format.
The detected rate and channel count are shown in the Audio output row of
the settings panel while the pipe is running (e.g. On (48000Hz 2ch)).
On first launch, ncradio attempts to find the audio capture device associated with the V4L2 radio device. Two strategies are tried in order:
- udev — walks up to the USB device node and matches sound cards sharing
the same USB parent. Requires
libudevat build time; disabled with./configure --disable-udev. - sysfs — resolves the radio device's sysfs path and searches sibling
directories for a
sound/card*entry. Always available.
With the PipeWire backend, the detected ALSA card name is then matched
against enumerated PipeWire Audio/Source nodes (by api.alsa.card.id
property) and the PipeWire node name is stored.
With the ALSA backend, the ALSA hw:CARD=<id>,DEV=0 string is stored
directly.
If a device is found it is saved to ~/.ncradio.conf and audio is enabled
automatically. Subsequent launches use the saved device. If no device is found
audio stays off; it can be enabled manually in the settings panel.
- Press
oto open settings. - Navigate to Audio output and press
Enteror←/→to switch to On. - If no device is configured yet, ncradio runs autodetection as described above.
- Navigate to Audio device and use
←/→to cycle through detected devices.
With the PipeWire backend, the device list shows PipeWire Audio/Source node
names and descriptions. With the ALSA backend, it shows hw:CARD=X,DEV=Y
devices.
Changes take effect immediately — switching the device or toggling audio restarts the pipe on the fly.
The Play device setting selects the output destination:
- PipeWire build: a PipeWire
Audio/Sinknode name; empty = PipeWire default sink. - ALSA build: an ALSA playback device name; empty =
"default".
ncradio can record the live audio stream to disk while continuing to play it
back, in any of four formats: WAV (uncompressed, always available, no
library needed), MP3 (libmp3lame), OGG Vorbis
(libvorbisenc/libvorbis/libogg), and FLAC (libFLAC) — each
optional and independently detected at build time. WAV is always available
whenever the audio pipe is compiled in, so recording works out of the box
even on a minimal build with none of the codec libraries installed.
- Ensure audio is enabled and running (the
[A]indicator is green). - (Optional) Press
oand set Record format to the format you want. - Press
rin normal mode. - Type a filename and press
Enter. The current format's extension (.wav/.mp3/.ogg/.flac) is appended automatically if omitted. The file is created in the current working directory; a path prefix like~/recordings/showis accepted.
During recording the info row shows:
- REC 0:03 myshow.wav
with a running elapsed time. All tuning, scanning, seeking, mute, and settings
operations are blocked. Press s or Esc to stop and save; press q to stop,
save, and quit.
Each format keeps its own independent channels/sample-rate/quality settings, so switching Record format doesn't lose the values tuned for another format. The settings panel shows a fixed set of rows whose label and value adapt to whichever format is currently selected:
| Setting | Default | Options |
|---|---|---|
| Record format | WAV | WAV / MP3 / OGG / FLAC (only formats compiled in are offered) |
| Record channels | Stereo | Stereo / Mono (independent per format) |
| Record sample rate | 44100 Hz | 22050 / 44100 / 48000 Hz (independent per format) |
| Bitrate/Quality/Compression | varies | MP3: 64–320 kbps · OGG: VBR quality −1.0…10.0 · FLAC: compression level 0–8 · WAV: not applicable |
| Apply EQ to recs | Yes | Yes / No (shared across all formats) |
WAV/OGG/FLAC record at the requested sample rate via libsamplerate when
it's available at build time; without it, they always record at the pipe's
native capture rate regardless of the configured sample-rate setting. MP3
is unaffected either way — lame resamples internally. All non-native
channel counts (e.g. recording mono from a stereo capture) are converted by
ncradio itself for WAV/OGG/FLAC; MP3 uses lame's own downmixing.
When Apply EQ to recs is Yes (default) and the equalizer is enabled, the active EQ curve is applied to the recorded audio so the file reflects what you hear. When set to No, recordings capture the unprocessed signal regardless of the EQ state. This setting is only shown when both the equalizer and recording are compiled in, and applies to whichever format is active.
Presets are displayed in a multi-column grid that fills the terminal width automatically:
- Frequencies are shown as
XX.XX(no "MHz" label). - Presets are arranged column-major: the list fills downward within a
column before spilling into the next, like
lsoutput. Preset 2 is below preset 1, not next to it. - The number of columns is derived from the terminal width and the length of the longest preset name. More columns are used when names are absent or short; fewer when names are longer.
- The currently tuned preset is marked with
<. - The selected (highlighted) preset is marked with
>. ↑/↓move within a column;←/→jump one column;PgUp/PgDnscroll by one visible window of rows.
Press o to open the settings panel. Changes take effect immediately and are
written to ~/.ncradio.conf on every adjustment.
| Setting | Default | Range / values | Description |
|---|---|---|---|
| Scan step | 0.10 MHz | 0.025 / 0.05 / 0.10 / 0.20 MHz | Frequency increment for scan, manual step, and seek |
| Signal threshold | 50% | 5% – 95% (5% steps) | Minimum signal strength to record a station during scan/seek |
| Save RDS names | Yes | Yes / No | Whether to pause on each found station to collect its RDS PS name during scan |
| Audio output | Off | Off / On | Enable or disable the audio pipe |
| Capture device | (auto) | detected capture devices | Capture device / PipeWire source node |
| Playback device | (default) | detected playback devices | Playback device / PipeWire sink node; empty = system default |
| Buffer size | 1024 frames | 512 / 1024 / 2048 / 4096 / 8192 frames | Capture period size hint (ALSA backend; ignored by PipeWire) |
| Mute while scanning | Yes | Yes / No | Stop the audio pipe during a band scan |
| Mute while seeking | Yes | Yes / No | Stop the audio pipe while seeking |
| Record format | WAV | WAV / MP3 / OGG / FLAC | Active recording format (shown only when recording is compiled in; only compiled-in formats are offered) |
| Record channels | Stereo | Stereo / Mono | Output channels for recordings, independent per format |
| Record sample rate | 44100 Hz | 22050 / 44100 / 48000 Hz | Output sample rate for recordings, independent per format |
| Bitrate/Quality/Compression | varies | MP3: kbps · OGG: −1.0…10.0 · FLAC: 0–8 · WAV: N/A | Encoding quality knob; label and range adapt to the active format |
| Apply EQ to recs | Yes | Yes / No | Apply the EQ curve to recordings when EQ is on (shown only when both recording and EQ are compiled in; shared across formats) |
The 11-band graphic equalizer applies a cascade of biquad peaking filters to the playback audio path. It has no effect when audio is disabled or when the EQ is toggled off.
| Band | Center frequency |
|---|---|
| 1 | 32 Hz |
| 2 | 64 Hz |
| 3 | 125 Hz |
| 4 | 250 Hz |
| 5 | 500 Hz |
| 6 | 1 kHz |
| 7 | 2 kHz |
| 8 | 4 kHz |
| 9 | 8 kHz |
| 10 | 12 kHz |
| 11 | 16 kHz |
Each band has a gain range of −12 dB to +12 dB.
| Preset | Character |
|---|---|
| Flat | All bands at 0 dB |
| Rock | V-curve: punchy bass, scooped 500 Hz–2 kHz, crisp highs |
| Pop | Forward mid-presence for vocal clarity |
| Classical | Warm and airy, gentle presence rolloff |
| Jazz | Rich low-mids, smooth top end |
| Electronic | Heavy sub-bass and high sparkle |
Custom presets are saved in ~/.ncradio.conf as
eq_preset_<name>=<11 comma-separated gain values>. Up to 16 custom presets
are supported. The name of the last active preset is also saved so the
displayed name is correct after a restart.
- PipeWire build: EQ is applied in the playback callback. When Apply EQ to recs is Yes, a separate EQ pass is applied to a copy of the capture buffer before it is fed to the active recording format's encoder. When No, the encoder receives raw (pre-EQ) audio.
- ALSA build: EQ is applied in-place to the shared capture buffer. When Apply EQ to recs is Yes, the EQ is applied before the recording callback so the encoder receives the equalized audio. When No, the raw buffer is recorded first, then EQ is applied for playback.
Settings and presets are stored together in ~/.ncradio.conf:
# ncradio configuration
scan_step=100000
signal_threshold=50
rds_names=1
volume=80
last_freq=98500000
audio_enabled=1
audio_device=alsa_input.hw:CARD=Si4713,DEV=0.0.analog-stereo
audio_mute_scan=1
audio_mute_seek=1
record_format=0
record_bitrate=128
record_stereo=1
record_samplerate=44100
record_eq_enabled=1
record_wav_stereo=1
record_wav_samplerate=44100
record_ogg_stereo=1
record_ogg_samplerate=44100
record_ogg_quality=3.0
record_flac_stereo=1
record_flac_samplerate=44100
record_flac_level=5
eq_enabled=1
eq_active_preset=Rock
eq_gains=4.0,3.0,2.0,0.0,-1.0,-2.0,-1.0,0.0,2.0,3.0,3.0,2.0
eq_preset_MyBass=6.0,5.0,4.0,2.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
# stations
87.90 BBC Radio 1
91.30
98.50 Capital FM
103.60 LBC
The audio_device value is a PipeWire source node name when built with
PipeWire, or an ALSA hw:CARD=<id>,DEV=0 string when built with ALSA.
Settings lines — key=value pairs written before the station list:
| Key | Value | Meaning |
|---|---|---|
scan_step |
Hz (e.g. 100000) |
Frequency step for scan, step, and seek |
signal_threshold |
percentage (e.g. 30) |
Minimum signal to record a station |
rds_names |
0 or 1 |
Whether to collect RDS names during scan |
volume |
0–100 |
Tuner volume restored on startup |
last_freq |
Hz (e.g. 98500000) |
Last tuned frequency, restored on startup |
audio_enabled |
0 or 1 |
Whether to start the audio pipe at launch |
audio_device |
device name | Capture device / PipeWire source node for the audio pipe |
audio_play_device |
device name | Playback device / PipeWire sink node; empty = default |
audio_mute_scan |
0 or 1 |
Whether to stop audio during a band scan |
audio_mute_seek |
0 or 1 |
Whether to stop audio while seeking |
record_format |
0-3 |
Active recording format: 0=WAV 1=MP3 2=OGG 3=FLAC |
record_bitrate |
kbps (e.g. 128) |
MP3 encoding bitrate (MP3-specific) |
record_stereo |
0 or 1 |
MP3 output channel count (1=stereo, 0=mono) (MP3-specific) |
record_samplerate |
Hz (e.g. 44100) |
MP3 output sample rate (MP3-specific) |
record_eq_enabled |
0 or 1 |
Apply EQ curve to recordings when EQ is on (default 1); shared across formats |
record_wav_stereo |
0 or 1 |
WAV output channel count |
record_wav_samplerate |
Hz (e.g. 44100) |
WAV output sample rate |
record_ogg_stereo |
0 or 1 |
OGG output channel count |
record_ogg_samplerate |
Hz (e.g. 44100) |
OGG output sample rate |
record_ogg_quality |
float, -1.0-10.0 |
OGG Vorbis VBR quality |
record_flac_stereo |
0 or 1 |
FLAC output channel count |
record_flac_samplerate |
Hz (e.g. 44100) |
FLAC output sample rate |
record_flac_level |
0-8 |
FLAC compression level |
eq_enabled |
0 or 1 |
Whether the EQ is active |
eq_active_preset |
name string | Name of the last active preset; empty = unsaved custom curve |
eq_gains |
11 comma-separated floats | Per-band gain values in dB (−12 … +12) |
eq_preset_<name> |
11 comma-separated floats | A saved custom EQ preset named <name> |
Station lines — frequency in MHz, optional name after a space. Lines
starting with # are comments.
The file is rewritten in full every time a setting changes, a preset is added/deleted/renamed, or a scan completes. You can also edit it by hand; ncradio reads it at startup.
Old config files (frequency lines only, no settings) are read correctly —
ncradio uses defaults for any settings not found in the file. Old ncradio
versions reading a new config silently ignore the key=value lines (the
%lf scan for a float fails on scan_step=… and the line is skipped).
| ioctl | Purpose |
|---|---|
VIDIOC_G_TUNER |
Detect frequency unit, RDS capability, tunable range, signal strength, stereo status |
VIDIOC_G_FREQUENCY / VIDIOC_S_FREQUENCY |
Get / set tuner frequency |
VIDIOC_S_HW_FREQ_SEEK |
Hardware-assisted station seek (available in radio.c, not currently bound to a key) |
VIDIOC_S_CTRL |
Volume (V4L2_CID_AUDIO_VOLUME), mute (V4L2_CID_AUDIO_MUTE), RDS reception (V4L2_CID_RDS_RECEPTION) |
RDS data is obtained by calling read() on the radio device file descriptor,
which returns a stream of struct v4l2_rds_data blocks (3 bytes each).
The tunable frequency range is read from VIDIOC_G_TUNER at startup and used
to validate manual tune input and to clamp the restored last_freq value.
When built with PipeWire, audio uses two pw_stream objects on a single
pw_thread_loop:
| Stream | Direction | Target |
|---|---|---|
| Capture | PW_DIRECTION_INPUT |
Radio card's Audio/Source node |
| Playback | PW_DIRECTION_OUTPUT |
Speaker / configured Audio/Sink node |
The capture stream proposes S16_LE with an open rate range; PipeWire selects
the source's native sample rate (no resample on the capture side). The playback
stream is connected with the same rate after capture format is negotiated, so at
most one resample occurs (source native → sink native). Data flows via a 2 MiB
lock-free ring buffer (spa_ringbuffer) between the capture and playback
process callbacks.
When built without PipeWire, the audio pipe uses these ALSA API calls:
| Call | Purpose |
|---|---|
snd_ctl_open / snd_ctl_pcm_next_device |
Enumerate physical PCM capture devices for the settings panel |
snd_pcm_open |
Open capture (hw:X,Y) and playback (default) PCMs |
snd_pcm_hw_params_test_rate |
Probe supported sample rates without modifying device state |
snd_pcm_hw_params_* |
Configure format (S16_LE), channels, rate, period size |
snd_pcm_readi / snd_pcm_writei |
Interleaved read/write of sample frames |
snd_pcm_recover |
Recover from buffer overruns and underruns |
