Work out where and when an audio recording was made using only the thunder in it.
Lightning networks log every strike with millisecond timing and positions accurate to a few hundred metres. Each strike's thunder reaches a listener after a delay set by distance. A recording of a storm therefore carries the listener's position, as the delay pattern across dozens of strikes. thunderprint searches a day of strikes from MET Norway's Frost API for the place and start time whose predicted thunder best matches the recording. It outputs a probability map with a 90% credible region.
The output is a self-contained HTML page, shown above for a real 4-minute recording from northern Norway. The map shows the posterior heatmap and the best candidate with its 90% ellipse. The panel below it plots modeled (red) against observed (blue) thunder over UTC time, above a spectrogram of the clip.
Pick one:
pip install thunderprint # CLI + Python API, prebuilt for Linux, macOS, Windows
cargo install thunderprint # CLI from crates.ioor download a prebuilt binary from Releases.
You also need:
-
ffmpegonPATHto decode audio. Any format ffmpeg reads works. -
A free Frost client ID from frost.met.no, set as
FROST_CLIENT_IDin the environment or in a.envfile in the working directory:FROST_CLIENT_ID=your-client-id
To build from source (Rust stable, edition 2024): git clone https://github.com/Hexay/thunderprint && cargo build --release.
You don't need a real recording to try it. sim synthesises thunder from real strikes at a listener you
choose, and you can then check whether locate finds that listener:
thunderprint sim --date 2025-07-19 --lat 65.089 --lon 19.339 --start 14:40:30 \
--temp=12 --wind-e=4 --wind-n=4 --noise 0.03 --seed 25 --out data/sim/demo
thunderprint locate --audio data/sim/demo/audio.wav --date 2025-07-19 --out out/demo
thunderprint eval --run out/demo --truth data/sim/demo/truth.tomleval should report an error of about 0.4 km. Open out/demo/index.html to see the map, with modeled and
observed thunder and a spectrogram underneath it. Lightning for each UTC day is downloaded once and cached in data/frost/
(override with THUNDERPRINT_CACHE).
pip install thunderprintThe wheel bundles the compiled binary, so Rust isn't needed. ffmpeg still has to be on PATH.
import thunderprint as tp
res = tp.locate("storm.opus", "2025-07-19", frost_client_id="...") # or set FROST_CLIENT_ID
res.best # Region(rank=1, lat=65.659, lon=14.117, start=datetime(...), reliable=True, ...)
res.regions # all candidates, best first
env = res.envelope() # .times (UTC datetimes), .observed, .modeled
res.spectrogram # .freqs_hz, .offsets_s, .power[band][frame]
res.viewer # path to the interactive HTML map
res.geopackage # path to result.gpkg for ArcGIS / QGIS (geopandas.read_file(res.geopackage, layer="cells"))
# Your own lightning data: a CSV path, a list of dicts, or a pandas DataFrame
res = tp.locate("storm.opus", "2023-06-02", strikes=df) # columns: time, lat, lon[, peak_ka, cloud, error_km]
tp.simulate("2025-07-19", 65.089, 19.339, "14:40:30", "sim/", temp=12, seed=25) # synthetic test clip
tp.load("out/run") # re-read a previous runFrost downloads are cached in the user cache directory. To use a different location, set THUNDERPRINT_CACHE.
| Command | What it does |
|---|---|
locate --audio F --date D [--start HH:MM:SS] |
Searches every strike on UTC day D for where and when F was recorded. If you know the start time, --start narrows the search to ±0.5 s. |
eval --run DIR --truth T |
Scores a finished run against a truth file (lat/lon TOML). |
sim --date D --lat .. --lon .. --start .. |
Writes a synthetic audio.wav and truth.toml. Run --help for noise, wind, stereo and propagation options. |
fetch --date D |
Downloads and caches one day of Frost lightning ahead of time. |
probe ... |
Diagnostics: scores one point at every pipeline stage. |
locate, sim and probe take --strikes file.csv to use lightning data from any network instead of Frost:
| Column | Required | Meaning |
|---|---|---|
time |
yes | UTC, as epoch seconds or ISO-8601 (2025-07-19T16:00:01.25Z) |
lat, lon |
yes | Strike position, in degrees |
peak_ka |
no | Peak current in kA. Default 10. |
cloud |
no | 1/true for in-cloud, otherwise cloud-to-ground |
error_km |
no | Location error, as a 50% radius. Default 0.5. |
Accuracy depends on the network's timing and position errors. Frost/NORDLIS reaches about 200 m; Blitzortung's public feed is about 3 km.
locate writes five files to --out (default out/run):
index.html: the map viewer. Under the map, it plots the modeled and observed thunder envelopes over UTC time, with a 10–1000 Hz spectrogram on the same time axis. Clicking a candidate region switches the modeled curve to that region.result.gpkg: a GeoPackage for ArcGIS Pro or QGIS (drag it in), in WGS 84. It has four layers:cells: posterior grid polygonscandidates: pointsellipses: 90% regions, linked to candidates byrankonsets: onset-solve fixes
heatmap.geojson: the same features in a single GeoJSON filecandidates.json: ranked regions, each with its start time, fitted atmosphere, 90% area and areliableflagtrace.json: the envelope curves and spectrogram shown in the viewer
--model improved(the default) is the model described below.--model baselinereimplements the method from the video that inspired this project (see Prior art). It's there for comparison.--overrides file.tomloverrides search settings. The fields are declared insrc/config.rsandexperiments/has examples.
- Observed envelope. The audio is band-passed to 20–250 Hz, where thunder carries far and through walls. It's then reduced to a smoothed, compressed loudness envelope.
- Predicted envelope. For a candidate listener position, every strike is rendered as a thunder pulse.
- Cloud-to-ground strikes are modelled as extended vertical channels, intra-cloud strikes as horizontal sources aloft.
- Arrival time is slant distance divided by the speed of sound, corrected for wind.
- Loudness scales with peak current and falls off with range.
- Each pulse has a rumble tail whose length grows with the channel's spread of arrival times.
- Match. The predicted envelope is correlated with the observed one at every possible start time in the day, using FFT cross-correlation.
- Search, coarse to fine. A coarse grid covers the whole strike region, then mid and fine grids refine the best peaks.
- The fine stage marginalises over nuisance atmospheres: temperature (speed of sound), wind, refraction lapse rate and the effective height thunder onsets are timed from.
- Posterior. Scores are tempered into a posterior, from which the tool reports ranked regions and 90% credible ellipses. A region is marked unreliable when fewer than 30 strikes would have been audible from it.
Two physical findings drove the model. Both are written up in research/:
- CG onset height. Real thunder onsets behave as if they come from about 6 km up, not from the channel base (see empirical_thunder_response.md).
- Refraction shadow. Upward refraction hides the low part of distant channels (see thunder_physics_lit.md).
| Case | Error |
|---|---|
| Synthetic clips, listener near active storms | 0.4–1.0 km |
| Synthetic twin of the real clip | 1.4 km |
| Real 4-minute recording, northern Norway | 1.3 km, versus 2.2 km for the baseline method. Not blind: the truth was known during development. |
Limitations:
- Coverage. Frost lightning covers the Nordic detection network (Norway and neighbouring countries) and nothing else. For anywhere else, use
--strikeswith data from another network. - Recording length. You need several minutes of unedited audio recorded inside an active storm. Short or edited clips don't hear enough strikes to fix a position.
- Geometry. If every strike comes from one direction, the result is a stretched ellipse. Strikes from several directions give a tight fix.
- Video clocks. In a video, the visible flashes give the frame clock. The audio track can be seconds away from that, so
--starttaken from flashes needs care. - Real clips fit worse than simulations. The model is still incomplete, and the credible regions on real audio should be read as optimistic.
- The baseline method comes from colsto's videos Can you geolocate a video using background thunder sounds? and I located a fan's video using thunder sounds. The method is a point source, 343 m/s, inverse-square attenuation and cosine similarity.
- Related work on timing and locating recordings is surveyed in
research/prior_art_receiver_location.md.
- Lightning data: from MET Norway via the Frost API, licensed under NLOD 2.0 / CC BY 4.0. Raw strike files are not redistributed here; each user downloads them with their own client ID.
- Map tiles in the viewer: © Esri, HERE, Garmin and © OpenStreetMap contributors.
