An ex-datacenter Xeon, eleven thirty-dollar SDR dongles, and a small pile of bash scripts. The honest build log behind listennowhou.com — what worked, what didn't, and what I tried first.
This started in early 2025 with a Uniden SDS200 and an honest question: could I monitor Houston-area public safety better than what already existed? Eleven months and four hardware iterations later, the answer is "kind of, depending on what 'better' means." The system runs 24/7, captures around 75,000 calls per day, and quietly survives most things that go wrong with it. Here's how it got there.
Before "the Beast" was a server, it was a series of failed setups. Each one taught me something specific.
The SDS200 is the gold-standard consumer scanner — and the audio is genuinely beautiful. Crisp, full bandwidth, the kind of sound you wish every Broadcastify feed had. But it scans like a scanner: one channel at a time, sequentially. On a busy P25 trunked system with dozens of active talkgroups, sequential scanning means missed calls. By the time you've cycled back to HPD-3 North Patrol, two units already keyed up and nobody captured them. It sounded great and missed half the action.
Next attempt: a Pi 5 running OP25, the venerable open-source P25 decoder. Software-defined radio means you can decode multiple channels in parallel — exactly the problem the Uniden couldn't solve. OP25 is well-regarded and the audio quality is solid. But the Pi 5's CPU couldn't keep up with simultaneous multi-channel decoding on a busy trunked system. Calls dropped. The decoder fell behind during dispatch surges and stayed behind. Right architecture, wrong hardware.
I borrowed CPU horsepower from the desktop I use for video editing and ran SDRTrunk on it. SDRTrunk is the popular Java-based alternative to OP25 — easier UI, active community, great for casual scanning. It kept up with the trunked system. But the audio wasn't quite as clean as OP25, and I needed my video PC back. It worked, but it wasn't sustainable.
The fourth try is the one that stuck: a former-datacenter Xeon, eleven RTL-SDR dongles, one AirSpy, and trunk-recorder. Trunk-recorder takes a different philosophical approach than the others: record everything in parallel, sort it out downstream. Instead of "tune to channel X, decode it," it dedicates SDRs to the control channel and voice channels, captures every call as it happens, and writes each one to disk as a separate file. Filtering, routing, and playback all happen after the audio is safely on disk. Nothing gets missed because nothing is being skipped.
That paradigm shift — capture-everything-then-filter, not tune-and-listen — is the single most important thing I learned across all four attempts. Once I made that mental switch, everything else fell into place.
Most homelab writeups feature glossy rack photos. This isn't going to. The Beast lives in an open-frame case behind a couch in my house. Cable management is "embraced entropy." The reason it's open-frame is that fitting eleven USB SDR dongles into a closed case is its own kind of optimization problem and I didn't want to solve it. The cooling is better this way anyway.
Yes, an E5-2699 v4 is overkill. Yes, I know. The original plan was an i7 in the same socket family — the Xeon came up cheap and I figured the headroom would be useful. It is.
The audio pipeline runs in stages. Each stage does one job. The whole thing is held together by systemd units and a watchdog process that knows what "healthy" looks like.
trunk-recorder reads the TXWARN P25 Phase II control channel from one dedicated SDR and follows voice grants to the remaining receivers. Every captured transmission lands on disk as an .m4a file, named with its talkgroup ID and timestamp. rdio-scanner ingests these files into a SQLite database and serves them through a web UI for searchable playback.
Raw trunk-recorder output sounds like raw trunk-recorder output: variable levels, hiss, low-frequency rumble, dead air on either end of each call. Most Broadcastify feeds stream that as-is. This one doesn't. An inotify watcher fires sox on every new file with a five-stage chain: high-pass at 300 Hz to cut hum, low-pass at 3 kHz to cut hiss, peak normalization to −3 dB so loud and quiet calls land at the same volume, silence trimming on both ends, and a small pad at the end so back-to-back calls don't feel mashed together.
Three Broadcastify feeds run from this single server: HPD + Constables, Fort Bend, and Harris County Sheriff. Each feed gets a per-talkgroup whitelist. When ezstream asks for the next file to play, a script checks the talkgroup ID prefix against the whitelist and skips anything that doesn't match. Encrypted talkgroups are filtered out earlier, at the trunk-recorder layer — they have no decodable payload, so they never become files.
The slick part: when ezstream is about to play a file, the script looks up the talkgroup ID in radioreference.csv and creates a symlink with a human-readable name. Listeners on the Broadcastify mobile app see HPD-3 NORTH PATROL scroll across their screen instead of 563-1778091538. It's a small detail. It makes the feed feel professional.
ezstream serves newest-first from the whitelisted pool, keeping listeners current with active dispatch instead of replaying old calls. When there's nothing whitelisted to play, a sox-generated silence file fills the gap so the Broadcastify mount never times out. The connection is continuous; the audio only plays when there's something worth playing.
The watchdog is the part of the system I'm most proud of. Every two minutes it checks: are all SDRs detected? Is the AirSpy detected? Is trunk-recorder running? Is rdio-scanner serving? Are all three ezstream services connected? Is fresh audio being captured? And critically: is each feed actually streaming whitelisted content, or is it silently feeding silence while its source dir has busy traffic on other talkgroups?
That last check is the one most monitoring scripts miss. A feed can be running, the service can be "active," and the listener can be hearing dead air — because the talkgroups it's filtering for happen to be quiet while everything else is busy. I had to add this check the hard way; more on that below.
The Uniden, OP25, and SDRTrunk were all variations on "tune to a channel, decode it, move on." That model is fundamentally lossy on a busy trunked system. trunk-recorder's "record every call as a file, sort downstream" architecture is the right paradigm. It cost me three hardware experiments to learn that.
→ Move filtering as far downstream as you can.
The CPU and motherboard cost less than a single new SDR. A nine-year-old Xeon E5-2699 v4 has more cores than most current consumer chips and idles at half clock under steady-state load. Buying retail parts for a homelab in 2026 is a tax you don't have to pay.
→ eBay searches: "xeon e5-2680 v4 motherboard combo" and equivalents.
Early versions of the watchdog only checked systemctl is-active. Then a feed went silent during a real bomb-threat callout at a Houston-area high school — the service was running, ezstream was connected, listeners were hearing nothing because the script was filtering out every busy talkgroup that didn't match its whitelist. Coverage stayed live thanks to redundancy on other feeds, but it was a wake-up call. The watchdog now cross-references whitelist activity against feed output. If the source dir is busy and a feed is quiet on talkgroups it should be playing, it gets restarted.
→ Health checks must validate output, not just process state.
Eleven RTL-SDR dongles is a lot of USB. Statistically, one of them is going to misbehave at any given moment — flaky cables, USB power dips, thermal drift. Earlier watchdog versions would Pushover-alert me at 3am because dongle #7 had dropped, and an hour later it was back. Now: ten of eleven counts as "degraded but acceptable" with one daily summary alert. Two missing triggers escalation. Restraint took me longer to learn than escalation.
→ Build redundancy in, then trust it.
SDR software is a dependency-graph nightmare. GNU Radio versions, OOT modules, boost, OSMocom drivers, kernel-level USB tweaks — a fresh Ubuntu install takes days to get right. DragonOS ships with all of it pre-configured. Picking the right base image is sometimes the highest-leverage decision in the whole build.
→ Don't fight environment problems you don't have to.
Trunk-recorder writes each call with the talkgroup ID embedded in the filename. That single design choice means every downstream filter is a string match against the prefix — no audio decode, no metadata lookup, no database join. The whitelist filter, the per-feed router, the cleanup scripts, the wedge detector: all of them are basically grep. Cheap operations stay cheap when the upstream emits the right metadata in the right place.
→ Embed metadata in names, not just content.
The numbers below pull from the system in real time. They update every minute.
The build is never really "done." A few things on the working list:
If you build something similar and want to compare notes, the contact form is on the main page. I'm @doorman812 on X. I read everything.