An AI-native radio — FM broadcast and AM airband. RTL-SDR hardware, an Elixir/OTP core, and no AI inside, because the AI is the one holding the remote. Aether is built to be operated by AI agents (Claude Code, Codex, cron'd scripts, you name it): they tune it, they pull the last minute of broadcast out of it as WAV, and they decide what it means.
Aether: the fifth element of classical alchemy, and the medium radio waves were once believed to travel through. An elixir among elements.
RF PCM (48kHz s16le)
┌───────────┐ ┌──────────────┐ ┌────────────────────┐ ┌──────┐
│ RTL-SDR │──▶│ demodulator │──▶│ Aether.Player │──▶│ play │──▶ 🔊
│ dongle │ │ (airspy- │ │ (GenServer) │ │ (sox)│
└───────────┘ │ fmradion or │ │ │ │ └──────┘
│ rtl_fm) │ │ ▼ │
└──────────────┘ │ RingBuffer (60s) │──▶ capture → .wav → 🤖
└────────────────────┘
▲
Unix socket ───────┘
(tune / status / capture / ... — you)
A classic radio assumes a human: knobs, presets, frequencies to memorize. Aether assumes an agent:
- A control socket, not a UI. Every operation is one line over a
Unix domain socket, with a
jsonmode for parsing. One binary acts as both daemon and client. - A live audio tap, not a speaker-only path. The last 60 seconds of
broadcast are always in memory;
capturehands them to the agent as WAV, while playback keeps going. The agent transcribes and reasons with its own tools. - A station knowledge base, not frequency knobs.
tune nhkworks; so dotune 82.5andtune atis. The roster (Tokyo & Fukuoka FM, Tokyo airband built in) is data an agent can read and choose from; airband entries are AM, the intermittent ones with squelch defaults. Raw frequencies are first-class too: known ones resolve back to their roster entry, unknown airband frequencies (108-137 MHz) default to AM with squelch, and anything outside the tuner's 24-1766 MHz is rejected. - AGENTS.md ships in the repo — ground rules telling the agent: you pick the station, you verify what's on air by listening, you own reception quality.
- An RTL-SDR USB dongle (developed against RTL-SDR Blog V4 / R828D)
- macOS or Linux with:
airspy-fmradion— the preferred demodulator (SDR++-grade DSP, AGC, FM stereo, AM airband, Japanese 50 µs de-emphasis); the formula in this tap builds it from sourcertl_fm— FM-only fallback;brew install rtl-sdr(librtlsdr ≥ 2.0.1 for the V4)play—brew install sox
- Optional: a speech-to-text tool for the "what's playing?" loop —
the agent transcribes captured WAVs itself (e.g.
brew install openai-whisper) - For development: Elixir ≥ 1.18 (JSON module), OTP ≥ 27
Via Homebrew — the repository doubles as a tap; the formula pulls the
release binary plus every runtime dependency (airspy-fmradion built
from source, librtlsdr, sox):
brew tap kentaro/aether https://github.com/kentaro/aether
brew install aether # short name works once the tap is addedOr grab a single-file binary from GitHub Releases (built by Burrito for macOS/Linux × arm64/x86_64), or build from source:
mix deps.get
mix check # format + credo + tests
MIX_ENV=prod mix release # needs zig 0.16 → burrito_out/aether_*One executable, two roles:
aether serve & # the radio daemon (holds the SDR)
aether stations # what can I tune?
aether tune nhk # fuzzy: name, alias, or MHz
aether tune atis # airband (AM): Haneda ATIS/Tower, Tokyo Approach
aether status
aether volume 40 # output volume (1-100)
aether capture 30 /tmp/onair.wav # the last 30s, as WAV
aether json status # every command, machine-readable
aether stop # stop playback
aether shutdown # stop the daemonThe same protocol is available over the socket directly
(echo "tune nhk" | nc -U /tmp/aether.sock), interactively
(mix aether), and as an Elixir API:
iex> Aether.listen("j-wave")
{:ok, %Aether.Stations.Station{name: "J-WAVE", mhz: 81.3, ...}}
iex> Aether.capture(30, "/tmp/onair.wav")
{:ok, "/tmp/onair.wav"}
iex> Aether.stop()
:okTuning options: aether tune 82.5 accepts whatever Aether.listen/2
does — gain=40.2 for manual tuner gain, stereo=true for FM stereo,
mode=am squelch=24 for airband (ppm= frequency correction applies
to the rtl_fm fallback only). Region defaults to :tokyo
(config :aether, region: :fukuoka to change).
Inside this repository, agents learn the radio from AGENTS.md
automatically. For every other directory, install the bundled skill
(embedded in the binary — no checkout needed):
aether install-skill # or from the repo: mix aether.install_skillThis writes ~/.claude/skills/aether/SKILL.md for Claude Code, and —
when ~/.codex exists — appends the same instructions to
~/.codex/AGENTS.md between marker comments (idempotent; re-run to
update). After that, any session responds to "play the radio",
"what's playing?", "listen to ATC" — in English or Japanese — by
driving the daemon. Other agents can read the same document from
skills/aether/SKILL.md.
- Demodulation runs in an external backend —
airspy-fmradionwhen present (AGC, FM stereo, AM airband with squelch), elsertl_fm(FM mono, tuned flags: integer-ratio 240k capture,-F 9FIR, 15 kHz sinc low-pass in the sink); Elixir orchestrates processes and owns the stream. The PCM rate (96 KB/s mono, 192 KB/s stereo) is trivial for the BEAM, so the audio literally flows through theAether.PlayerGenServer, which tees it into the ring buffer on the way to sox. - External programs are ports with explicit SIGTERM cleanup — a dead pipeline can't wedge the SDR device.
- Everything is swappable via application env, which is how the test suite (no hardware, no audible output) runs the full pipeline against shell-script fakes.
- Protocol logic (
Aether.Control.Command) is pure command-line → response rendering, independent of the socket transport.
MIT