Installing & running TrunkRecorder

Pretense before installation… please do read this first section.

TrunkRecorder, an amazing, or nifty, or stalkerish tool… all depending on your outlook with the compiled final product. TrunkRecorder, as it name says, records digital trunked radio. It does not record encrypted audio (well, it can, if you use a forked build – but honestly, it’s a waste of drive space and possibly illegal to store such encrypted audio), just clear-air audio. So if you’ve an active radio system, that doesn’t slap AES256 encryption on everything under the sun (Wyandotte KS, Cass MO and Jackson MO – I’m looking at you), this tool (and TrunkPlayer) might be right up your alley!

For my TrunkRecorder installation, as of this writing, it all runs on a Virtual Machine. I tried running it on a generic dedicated machine, but quickly found out that the machines hardware in use was sub-par. It was causing excessive audio popping, incomplete audio, and other P25 decoding issues. The machines CPU was an Intel Core i5-4460, rocking some G.SKILL Ares Series 16GB (2 x 8GB) RAM. For the Johnson County KS P25 radio system, the CPU was simply overloaded throughout most of the day hours.

Virtual Machine details…

Currently, the machine runs on a VMWare Workstation instance with the following specifications…

  • 8GB RAM
  • 12 vCPU’s
    • Virtualize Intel VT-x is checked.
    • Virtualize IOMMU is checked.
  • Hard Disk is preallocated, SATA, at 25GB (more on this later).
    • The Virtual Hard Disk is stored on a NVMe m2 port. Which makes just about every i/o operation ungodly fast – too fast.
  • Network Adapter is Bridged.
    • It does not replicate the physical network connection (unchecked).
    • Click advanced, generate or copy the MAC address.
    • Grant that MAC address a static LAN IP. (Easier to administer down the road with a static LAN IP)
  • USB Controller compatibility: USB 2.0
  • Sound Card: None needed.
  • Display: Host Settings, no need to accelerate 3D GFX.
  • Operating System: Debian 9.x series.
root@TrunkRecorder:~# lsb_release -da
 No LSB modules are available.
 Distributor ID: Debian
 Description:    Debian GNU/Linux 9.8 (stretch)
 Release:        9.8
 Codename:       stretch

It may appear that I am throwing everything and the kitchen sink at this virtual machine….. because I am. TrunkRecorder, when really going at it with frequency-audio-slicing from the SDR USB device to the machine, uses a TON of CPU, RAM isn’t as much. I am not sure if this is a long-time bug or 100% intended, but it’s been like this for many years now.

Hardware in use…

The following is the backing hardware in use that powers the virtual machine. I’ve attached a screenshot of the host machines CPU utilization below. If i were to kill TrunkRecorder’s VM or just the recorder process in the VM, the CPU utilization would go from an average of 30-50% down to 4% overall. The first two images are from dead-hour, when there’s just about no activity in the middle of the night. The second set of images are from around 3PM. Night and day difference in utilization – literally!

Daytime utilization….

Currently, the machine running the VMWare instances is Windows 10 Pro. I know, I know, I should use VMWare ESXi or something that isn’t Windows 10/8/7/XP/whatever. This gets the job done, and I’m happy with it.

  • CPU: Intel i7-6800k @ 3.4GHz, clocks up to 3.6GHz – as is by design.
    • The “Step-up” CPU is the “Intel Xeon E5-2643 v3”
  • RAM: 64GB DDR4 RAM (non-ECC) (CMK64GX4M4A2666C16)
  • VM Storage Drive: Samsung SSD 970 EVO 1TB m2/2280 (MZ-V7E1T0BW)
  • RTL SDR in use: HackRF One.

    In the coming years, if JoCo doesn’t go full tinfoil and encrypt everything, will likely move the installation and VM’s over to an AMD ThreadRipper 1950x CPU with 128GB RAM.

Getting things to “work”…

Assuming you’ve followed the steps at and the rest should be rather simple to understand. But in the event something goes haywire with your installation, here’s my method below… assuming you’re running as root, because well, sudo drove me nuts and i’m old school.

  1. apt install gnuradio-dev gr-osmosdr libhackrf-dev libuhd-dev
  2. apt install git cmake build-essential libboost-all-dev libusb-1.0-0-dev libssl-dev
  3. Ensure you’re in the /root/ directory as your current working directory.
  4. mkdir trunk-build
  5. git clone
  6. cd trunk-build/
  7. cmake -j ../trunk-recorder
  8. make -j

Why the -j on cmake and make? -j will force the compiler to use ALL CPU cores available to the VM or machine to compile.

Do note that the machine WILL REQUIRE about 8+GB of RAM for the TrunkRecorder compile due to the -j flag mass compiling files in to the recorder file. If you want it to not use as many CPU cores, you can always limit it to 4, 3, or 2 cores rather just the single threaded compiler. You can do that by adding a value after the j without the space, it’ll look something like this: -j4.

You could try and use some spiffy environment flags and set it passively and dynamically, but i’m personally against doing that. You can likely pull that off with using something like this: export MAKEFLAGS=-j$(($(grep -c ^processor /proc/cpuinfo) - 0)) as an edit to ~/.bashrc. It’ll use all cores except one core. Alternatively, you can just type in cat /proc/cpuinfo | grep ^processor | wc -l on your terminal window to get a total amount of CPU cores, then reduce the value to something that you see fit, if required/needed.

Things have compiled, time to configure!

Some things to note here with config.json on my install…

  1. I do not use an array of RTL-SDR’s.
  2. I only use a single HackRF One device.
  3. It does have a PPM auto-adjust setting, that can be viewed on GitHub.
  4. PPM Auto-adjust does not play will with two or more systems on a HackRF One, it ends up fighting each Control Channel non-stop once one makes a PPM change.
  5. The PPM can wildly change due to room temperature of where the HackRF One resides. For me, a room temp of 81F (or about 27C) causes the HackRF to have a PPM offset/drift of 15.5 to 17.9. Currently, it is sitting at 16.9.
    • Regardless of this, and the autoPPM setting, you will be tinkering with the PM value… A LOT with a HackRF One. It’s extremely sensitive.
  6. The HackRF One is configured to one, sole, single P25 system.
  7. It sends audio to TrunkPlayer – another Virtual Machine and an another article to talk about later on!

The config.json file.

Below is my working config.json file that resides in the trunk-build folder.

	"sources": [{
			"center": "855612500",
			"rate": "10000000",
			"squelch": "0",
			"error": "0",
			"ppm": "16.9",
			"ppm_adjust_interval": "0.05",
			"ppm_max_adjust": "15",
			"gain": "0",
			"ifGain": "24",
			"bbGain": "36",
			"digitalRecorders": 15,
			"debugRecorders": 0,
			"analogRecorders": 0,
			"digitalLevels": 9,
			"driver": "osmosdr",
			"device": "hackrf=56b35f",
			"modulation": "qpsk"
	"systems": [{
			"control_channels": [853775000],
			"type": "p25",
			"talkgroupsFile": "joco.csv",
			"recordUnknown": true,
			"shortName": "JoCoMARRS",
			"callLog": true,
			"hideEncrypted": false,
			"hideUnknownTalkgroups": false,
			"uploadScript": ""
	"defaultMode": "digital",
	"captureDir": "/root/trunk-build/audio_files",
	"callTimeout": "4",
	"logFile": "1",
	"frequencyFormat": "mhz",
	"controlWarnRate": "5",
	"statusAsString": true

rate Value

On a hackRF One, it has the native ability to scan in 10, 12.5 and 20MHz spectrum swaths, among a few others. In this install, i’ve capped it to 10000000 within the rate config option, or 10MHz. The JoCo P25 trunking system comes in just under 9MHz of pure spectrum used (it’s right around 8.7MHz total), so having this at 10MHz is a safe bet (until they poke about with P25 Phase 2 stuff).

center Value

The center clause is where I set HackRF One’s central frequency to. This settings value should NEVER be near or within or exact-to a P25 frequency used by the system that you’re going to be monitoring. For the JoCoKS site, I would be taking a peek at just to see the frequencies used, the control channel frequency and it’s actual frequency range. The range looks something like this…


Italics in chart: Alt Control Channel. Bold+Italics: Primary Control Channel.

As we see above, the control channel is 853.7750MHz, the lowest frequency is 851.2625MHz, and the highest frequency is 859.9625. The lazy way to find a median frequency to set the center to is just by adding the lowest and highest frequency in Hz.

851.2625 is 851262500 in Hertz.
859.9625 is 859962500 in Hertz.

Adding these two together and dividing by two gets you the median (or in another means of saying, the average of the two). The two added together comes to 1711225000. The median at this point comes to 855612500. Skipping the math, we convert 855612500 back to MHz, 855.6125. Looking at the table above, 855.6125 does not have a collision with any frequencies used by the system. This will be my center frequency.

NOTICE: HackRF does have a DC Offset in the center, this will also need to be taken into account when finding a proper central frequency to lock the HackRF to for the P25 system.

Leave error at zero, it’s simply not worth it messing with this option on a RTLSDR or HackRF.

PPM Value

Modify ppm after using something like gqrx on a Debian GUI or SDR# (SDR Sharp) on Windows. Both applications will accomplish the same thing, however installing a GUI on the linux operating system may be a viable option. As you can swap between the two (TrunkRecorder and GQRX) without too much issue.

Do note that PPM WILL MOST LIKELY DRIFT after a couple hours of run time. Do not expect your PPM to always be -0.1 or 0.5 or 5.5 or whatever it might initially show as. You need to give your SDR time to heat up and begin doing its thing: monitoring the P25 control channel(s) or spectrum space, and dumping frequency ‘slices’ to the machine to decode and save as a wav file.

Even running the SDR or HackRF in SDR# or GQRX for many hours would be an option. If you’re bored, and know your way around either application, save the waterfall of the P25 control channel frequency from the center frequency setting. You WILL see it drift with time as a slant on the water fall. Once that slant begins to straighten up, you most likely have found your initial PPM value. For me, as written above, it ranges from 15.9 to 17.9.

Worth noting that the PPM can also be in the negatives!!

‘Dem Gains….

Oh yeah, dem gains. If you set them too low, the TrunkRecorder application will fail to decode anything, and what does get decoded, will be a garbled mess. Set the gain too high, you end up with flooding your receiver with too much signal and actually create noise to the point that TrunkRecorder is indeed getting an…. extremely strong signal, but failing to “hear” what is going on. The saying of “not too fat, not too slim – just right” comes to mind.

As for me, I’m somewhat distanced away from most towers…evenly. Am actually in the middle of three simulcast towers for the same system. So from time to time, the signal quality gets a little bit funky when the weaker tower begins to fade-in due to atmospheric ducting during the night and morning hours.

			"gain": "0",
			"ifGain": "24",
			"bbGain": "36",

On the HackRF, from my findings, gain doesn’t do anything. You will need to modify ifGain and bbGain respectively. Once again, you can set this to an appropriate value by using SDR# or GQRX. For me, being a little bit distant, 0, 24, and 36 – as shown above was more than sufficient.

It’s also worth noting that the antenna and cable in use!

The antenna is a Discone, from Diamond Antenna. The D3000N series. The cable to and from is a quad-shielded coax cable using a N-type connector to a normal coax connector at the antenna and a Coax to SMA connector on the Hack RF One port. It’s funky, but it works well.

Off-Topic Fun fact: even though this antenna is rated for 25 to 3000MHz, I am able to receive all the way down to the 2.5MHz range with it on the HackRF! The antenna is super sensitive, same goes for the HackRF.

Anyhow, ifGain and bbGain – what’s the full name to these?!

bbGain is Base Band Gain. Range is 0 to 62, in steps of +2 or -2.
ifGain is LNA Gain. Range is 0 to 40, in steps of +8 or -8.

Gain on a SDR is quite important, as you need to spend a decent amount of time to get the values just right. If you use SDR#, anything in reds on the waterfall is simply too much, anything in yellows are likely too weak and anything in orange / extremely light red is right on the money from my findings. The gain will vary from antenna to antenna, and what cable (and length) is used from the HackRF to the Antenna.

A generic whip antenna will be decent, but will likely have difficulty decoding a P25 system – especially if indoors. I have two antennas in use, one is a 800-900MHz Yagi antenna and the other is a Diamond D3000n Discone. I’ve found that the Discone gets the job done without too much dinkering with antenna placement. The yagi, is quite positional (as is what a Yagi antenna does!), so you need to know where the strongest signal originates from and point the antenna to it. Be aware if you use a yagi however, those antennas have a HUGE amount of gain versus a whip and discone antenna.



To make any use of TrunkRecorder, you have to specify how many “slots” to allocate to recording P25 data into audio. Below are the four required settings to have set.

			"digitalRecorders": 15,
			"debugRecorders": 0,
			"analogRecorders": 0,
			"digitalLevels": 9,

Since we are only caring about monitoring a P25 Phase 1 system, digitalRecorders is set to 15. Why 15? Any more, risk overloading the HackRF One USB driver and having terrible results. Any less, and you’ll sometimes run out of recorders for active transmissions! The system I monitor has about 20 voice channels, 2 alt control channels and 1 primary control channel.

debugRecorders and analogRecorders are both set to 0, as debugRecorders outputs raw digital audio, which is a waste of space and analogRecorders isn’t used on a Project 25 digital trunking system.

Off-Topic Rambling: The only time analog recorders would be of use is when a severe failure of the P25 system occurs. That would mean, 1) the Primary CC is “dead”, 2) secondary CC’s are “dead”, 3) the portable radios cannot find a nearby tower to affiliate with, 4) the assigned band plan on user radios will revert radios into a basic walkie talkie analog mode. If such a failure would occur, it’d likely be the result of a large-scale natural disaster, or terrorist attack on the system. The likelihood of the latter is near zero than that of a natural disaster occurrence. This region sees an extensive amount of severe weather yearly, be it severe lightning storms, hurricane-force straight line winds, hail and wind-driven hail ranging in size from peas to softballs, flooding, and tornadoes.

digitalLevels is something that is best modified within one of the default, which is 8. I set mine to 9, and call it good. This controls the decoded audio loudness. In the other article (TrunkPlayer), this plays a role to an extent. You don’t want to deafen yourself (and others), nor do you want to listen to a whispering disaster.

			"driver": "osmosdr",
			"device": "hackrf=56b35f",
			"modulation": "qpsk"

driver is just going to be osmosdr if you’re using a HackRF or RTLSDR device. Nothing more to it than that.

device is the actual device type followed by the last six alphanumeric values on the SDR serial. You can safely type hackrf_info into your terminal window to retrieve the HackRF’s serial ID (or as the application calls it, the USB Descriptor String). It will look something like this…

root@TrunkRecorder:~# hackrf_info
Found HackRF board 0:
USB descriptor string: 0000000000000000##########56b35f
Board ID Number: 2 (HackRF One)
Firmware Version: 2018.01.1
Part ID Number: 0xa000cb3c 0x006e4756
Serial Number: 0x00000000 0x00000000 0x######## 0x##56b35f

I’ve intentionally hashed out the first digits of the HackRF serial, within those #’s are additional numbers and letters. We don’t need those to be known, nor are they used in the config.json file. The last six is all we need here.

modulation – this can be one of two options: qpsk or fsk4 per each device source. In most installations, qpsk is the go-to.

Systems config section

	"systems": [{
			"control_channels": [853775000],
			"type": "p25",
			"talkgroupsFile": "joco.csv",
			"recordUnknown": true,
			"shortName": "JoCoMARRS",
			"callLog": true,
			"hideEncrypted": false,
			"hideUnknownTalkgroups": false,
			"uploadScript": ""

This section here is the gold and glory of making the P25 TrunkRecorder function. Most of the configuration options are fairly straight forward, but in the event they are not, here’s the details…

  • control_channels is a singular or multi frequency input for ONE P25 system. Do not attempt to put two different P25 systems into this field.
  • type is just p25 nothing more to it than that. There also conventionalP25, conventional and smartnet.
  • talkgroupsFile is a comma separated list of the P25 systems talkgroups. If the P25 system is a part of a larger P25 network, then you will need to include ALL talkgroups on this file. Otherwise, if you plan on only monitoring a certain set of talkgroups, your talkgroupsFile contents should only contain said talkgroup ID’s. Below this list, will show an example and details in a table format of the headers to each column.
  • recordUnknown unless you plan to only monitor a certain set of talkgroups as outlined in the talkgroupsFile, leave this to true.
  • shortName is only used (currently) in the terminal window UI. This can be anything from a single letter or a full word. For me, I just use the county and system name, JoCoMARRS.
  • callLog is what TrunkPlayer uses to identify radio users on a talk group. it generates a little JSON file along with the created .wav audio file. Leave this to true.
  • hideEncrypted is a display only setting within the terminal. Some states and territories do not permit a person to even log the fact that a talkgroup is encrypted.
  • hideUnknownTalkgroups is set to false. If the entry does not exist in the talkgroupsFile csv, it won’t be shown.
  • uploadScript is a little file that we call for TrunkPlayer to do its thing.

Those above configuration options should be sufficient on getting your TrunkRecorder up and running.

The talkgroupsFile option

// Talkgroup configuration columns:
// [0] - talkgroup number
// [1] - unused
// [2] - mode
// [3] - alpha_tag
// [4] - description
// [5] - tag
// [6] - group
// [7] - priority

As per the source code of TrunkRecorder, this is the current column layout.

Column 1 is the talkgroup DECIMAL ID number.
Column 2 is not used currently. However, it’d be wise to use the dec2hex excel function, converting the decimal ID’s into a Hex ID, and pasting those values 1 : 1 into this column, as audioplayer.php utilizes this column as the talkgroups HEX ID value, and not DEC ID value. That is wholly optional.
Column 3 is the operating mode of the talk group. It’s either D or E. D = Digital/Clear and E = Encrypted.
Column 4 is the alpha tag. Alpha tags traditionally should not exceed 12 alphanumeric characters.
Column 5 is the description of the talkgroup. It defines what the TG actually is.
Column 6 is the tag, such as Fire, Police, Public Works, etc.
Column 7 is the grouping of the talkgroup, such as linking multiple TG’s to a City or agency.
Column 8 is the talkgroup priority. Where 1 is the highest and 98 is the lowest. At 99 or more on a priority value, it supposedly causes trunk recorder to skip recording the talkgroup (never tested this!).

Other config.json settings…

	"defaultMode": "digital",
	"captureDir": "/root/trunk-build/audio_files",
	"callTimeout": "4",
	"logFile": "1",
	"frequencyFormat": "mhz",
	"controlWarnRate": "5",
	"statusAsString": true

These options are fairly straight forward, and will require minimal explanation.

  • defaultMode tells TrunkRecorder that if a mode isn’t specified, it’s going to be digital.
  • captureDir is where wav files are dumped to.
  • callTimeout can be anything between 1 and 60, granted a sane value is between 2 to 4 seconds. Higher the callTimeout value, the lesser amount of digitalRecorders being available.
  • logFile leave at 1. Useful for debugging if required.
  • frequencyFormat can be mhz, hz, or exp. I’ve it set to MHz, as it’s the most logical and sane to read on terminal.
  • controlWarnRate is just a nag line in the terminal window if there’s severe issues with the decode rate to a system.
  • statusAsString is a new feature, it’s coupled with statusServer – which i do not use yet with my installation.

With all of that configuration out of the way, lets try running TrunkRecorder

Assuming you’re in the directory with the compiled “recorder” application, and assuming you’ve installed screen, let’s get the ball rolling…

  1. screen -S trunkrec -d -m $HOME/trunk-build/recorder
    • This should start an instance of screen, running the recorder in a screen session called trunkrec.
  2. Type screen -r to attach to the trunkrec screen instance.
    • Observe if there’s actual traffic rolling into the scanner.

If things are working as intended, without errors or issues, you should see something like this at the top.

[JoCoMARRS]	Started with Control Channel: 853.774963
	 P25 Trunking - SysNum: 0
	 P25 Recorder Initial Rate: 500000 Resampled Rate: 50000 Initial Decimation: 20 Decimation: 10 System Rate: 48000 ARB Rate: 0.96
P25 Recorder Taps - initial: 1515 channel: 1137 ARB: 757 Total: 3409
[JoCoMARRS]	Decoding System ID 2B0 WACN: BEE00 NAC: 2B5

Note that your control channel will likely not be exact as you’ve configured it in config.json, this is due to the PPM drifting – as you configured above. As far as I am concerned, having a CC lock on 853.77496 is about as bang-on correct as it can get to 853.7750 MHz.

Common Errors

Control Channel Message Decode Rate: 1/sec, count: 3

Your SDR has one of three issues:

  1. The SDR’s PPM error setting is incorrect. Use GQRX or SDR# to find the PPM error rate, reapply it to config.json and restart the ./recorder process while in screen.
  2. The SDR’s is in use elsewhere. you can either restart the machine or physically disconnect and reconnect the USB from the machine. Additionally, on a VM, you can disconnect the USB device from the VM and reattach it to the VM.
  3. The SDR is having USB locking issues (another device or recorder did not release the lock on the device).