pizzawave
is a set of cross-platform .NET applications and tools for processing audio data streamed by the callstream plugin of trunk-recorder. The audio data consists of calls recorded by trunk-recorder from conventional and trunked radio systems, such as local fire/rescue/EMS. pizzawave
tooling transcribes these calls to text using OpenAI's Whisper AI model as exposed through whisper.net toolchain. Among other features, the application allows you to monitor and set alerts for keywords of interest.
The pizzawave
Visual Studio solution consists of these tools:
- A windows-only .NET UI (
pizzaui
in source) - A cross-platform .NET command line application (
pizzacmd
in source) - A cross-platform .NET library (
pizzalib
in source), used by the UI and CLI application
Please be sure to read the README for each individual tool.
Regardless of whether you choose to use the UI, command line application, or roll your own application that uses the cross-platform library, you will need to observe these requirements:
- A Linux system running trunk-recorder with the callstream plugin configured correctly
- An operating system capable of running .NET 8.0 runtime (e.g, Win, Lin or Mac)
- The pizzawave tools currently target .NET 8.0, but if you are building from source, earlier versions should work as well.
- The requirements as specified in the tool of choice:
As shown in the illustration, pizzawave uses a server
-client
model, where the server is either the pizzawave UI or command line application and the client is one or more trunk-recorder systems. This design allows pizzawave to accept radio transmissions from multiple instances of trunk-recorder, which might be recording audio data from separate SDR device arrays monitoring broadcasts from different trunked radio systems.
Pizzawave listens for audio data from trunk-recorder systems, translates the data into textual transcriptions using Whisper AI, and processes alert rules to notify you of interesting broadcasts.
Note that it is possible (and even desirable) to run pizzawave
on the same system running trunk-recorder. In this setup, you would of course need to use pizzacmd
which is cross-platform.
You can use Visual Studio Community Edition for free.
- Install .NET core
- clone this repo
- CD into repo source
- run
dotnet build --runtime <RID>
where RID can be found here
You will want to develop pizzacmd using VS Code on Linux/Mac using the C# extension. To debug the program, use a launch configuration such as:
{
"version": "0.2.0",
"configurations": [
{
"type": "coreclr",
"request": "launch",
"preLaunchTask": "dotnet: build /home/<USER>/pizzawave/pizzacmd/pizzacmd.csproj",
"program": "/home/<USER>/pizzawave/pizzacmd/bin/Debug/net8.0/pizzacmd",
"name": "Test pizzawave",
"args": ["--talkgroups=/home/<USER>/my_talkgroups.csv"]
}
]
}
Pizzawave configuration lives in <user profile>\pizzawave\settings.json
. On Windows, this is Users\<user>\AppData\Roaming\pizzawave\settings.json
. Please see the READMEs for each individual tool you are using for what settings options are available and how to use them in your setup. pizzaui
includes a feature that allows you to setup your configuration in a more automated way, but you can always create the file manually. If you run the UI or command line application without a settings file, the default one will be created in the location specified above.
Important: Make sure your trunk-recorder
system is configured to connect to the right IP address. In an exotic scenario where you're running pizzacmd
from both a Windows host system and a WSL2 Ubuntu system, the host system and the virtual Ubuntu system will have different IP addresses! In this scenario, you might forget to set the correct IP address on the trunk-recorder
system, and only one of these machines will receive audio data, while the other might be stuck on this:
StreamServer Verbose: 1 : 3/22/2024 3:39 PM: Listening on port 9123
To create a live audio capture within PizzaUI, navigate to File
->Call Manager
->Start
. This will connect to the callstream
plugin running on your configured trunk-recorder system.
Whether you use pizzaui
, pizzacmd
or your own .NET application built on pizzalib
, all calls streamed in real-time from a callstream
server will be stored in a capture
, which is a folder in the root working directory (<user profile>\pizzawave\
). When you stop your live session with the callstream
server, the capture
is ended and a new capture will be created if you reconnect later. Older captures can be loaded in pizzawave
tooling later by opening the capture
folder directly.
The capture
folder consists of:
calljournal.json
: Each line contains a JSON-serializedTranscribedCall
structure. The audio data can be linked to this record via theLocation
field.<timestamp>.mp3
: call audio files
The call journal can be deserialized into a list of TranscribedCall
objects using NewtonSoft.Json
as follows:
var lines = File.ReadAllLines("calljournal.json");
var calls = new List<TranscribedCall>();
foreach (var line in lines)
{
var call = (TranscribedCall)JsonConvert.DeserializeObject(line, typeof(TranscribedCall))!;
calls.Add(call);
}
The callstream plugin allows you to redirect call records to an SFTP server. These call records are stored on disk in a raw binary format identical to data streamed to a live capture. These are referred to as offline captures
in pizzawave parlance. The callstream
plugin uploads offline capture records to the SFTP server according to the following naming and organization convention:
- YEAR
- MONTH
- DAY
- HOUR
- YEAR-MONTH-DAY.HOURMINUTESECOND.bin.bin
- HOUR
- DAY
- MONTH
Offline captures can be loaded at any directory level by pizzaui
or by the following code (pizzalib
required):
var targets = Directory.GetFiles(offlineDir, "*.*", SearchOption.AllDirectories).ToList();
foreach (var file in targets)
{
using (var stream = new MemoryStream(File.ReadAllBytes(file)))
{
var wavStream = new WavStreamData(m_Settings);
var cancelSource = new CancellationTokenSource();
var result = await wavStream.ProcessClientData(stream, cancelSource);
if (result)
{
var call = new TranscribedCall();
call.UniqueId = Guid.NewGuid();
try
{
var jsonObject = wavStream.GetJsonObject();
call.StopTime = jsonObject["StopTime"]!.ToObject<long>();
call.StartTime = jsonObject["StartTime"]!.ToObject<long>();
call.CallId = jsonObject["CallId"]!.ToObject<long>();
call.Source = jsonObject["Source"]!.ToObject<int>();
call.Talkgroup = jsonObject["Talkgroup"]!.ToObject<long>();
call.PatchedTalkgroups = jsonObject["PatchedTalkgroups"]!.ToObject<List<long>>();
call.Frequency = jsonObject["Frequency"]!.ToObject<double>();
call.SystemShortName = jsonObject["SystemShortName"]!.ToObject<string>();
}
catch (Exception ex)
{
var err = $"Unable to parse JSON data: {ex.Message}";
throw new Exception(err);
}
try
{
//
// Transcribe the wav audio
//
call.Transcription = await m_Whisper.TranscribeCall(wavStream.GetRawStream());
wavStream.RewindStream();
}
catch (Exception ex)
{
throw; // back up to worker thread
}
}
}
}
Offline captures are slow to load, because many audio recordings are being transcribed at one time (whereas in live mode, calls are transcribed as they are received over the air). As a result, after an offline capture is loaded, the contained call records are also exported to a live capture for easier retrieval later.
Alerts are NOT processed when older captures are loaded, for both live and offline captures. You can see what alerts would match a loaded capture by navigating to View
->Show alert matches only
.
All logs, model files, settings files, and alert data can be found in your operating system's user profile folder.
alerts
- this folder contains WAV data for matched alertsLogs
- this folder contains all log filesmodel
- this folder contains all auto-downloaded GGML model files
If your logs are not detailed enough, adjust the TraceLevelApp
parameter in settings.json
.
I dunno, I like pizza and Teenage Mutant Ninja Turtles, so it seemed to work.
For those that are new to SDRs (also check out this Getting Started Guide), I thought it might be helpful to show how I setup my SDR array:
The discone antenna is mounted about 20 feet in the air above my workshop, using a 1.5" PVC mast and a sturdy set of stand-off mounts. I have a short 8-ft run of UHF cable that brings the signal indoors to a wall-mounted, inline amplifier, which then connects to a 25-ft run of UHF cable that splits the signal 6-ways to an SDR array. I did my best to match up the impedence among all these cables and adapters to 50-ohm. For a similar rig that favors transmission insteead of Rx-only, 75-ohm would be better.
Here is the parts list, all of which can be purchased on Amazon:
- Discone antenna
- Skywalker 12" stand-off brace mounts for mast
- PL-259/UHF cable
- RTL-SDR Blog LNA
- PL-259/UHF-F to F-type-M
- PL-259/UHF-F to SMA-M
- DirecTV F-type 8-way splitter
- F to SMA jumpers
- Other adapters you might need: SMA adapters), F-type to SMA adapters and these too
- RTL-SDR v4 dongles
- 10-port USB hub with sufficient port spacing for side-by-side RTL-SDR dongles!
- Monoprice USB extension cable
Also, here are some sample configurations to get you started:
- trunk-recorder with callstream and two SDRs
- [trunk-recorder with callstream, openMhz and two SDRs](https://github.com/lilhoser/pizzawave/blob/main/docs/sample-config-openmhz.json - requires an openmhz account and related API key
- If you're struggling to setup trunk-recorder, I recommend this extremely well-written intro guide.
- Use this tool to calculate some trunk-recorder configuration parameters like center frequency and to understand how many SDR dongles you will need to cover channels of interest
- Other trunk-recorder related projects performing transcription: