field-audio-tools on a long dual-mic F3 takeYema Haizi, Kangding, Sichuan
We ran all three commands on Take 002, a three-hour Zoom F3 recording. The notes below cover the input, the files written, and two gaps we found in lowdom reporting and threshold calibration.
The GPX trail to the recording site begins on highway S434 at Yagya Ge, Sichuan, at roughly 3,894 m elevation, with roadside access on the pass before the walk in. It ends at the dome tent at Yema Haizi, about 4,055 m, with Yema Haizi lake to the east of that camp. We were guided by Dong Sheng & Xiao Zhang (local mountain guides in Kangding) and made most of the recordings the next morning.
The recording files used in this report were captured at specific positions on Yema Haizi lake itself. The season was suitable and the lake had frozen over. On the northeast side, the ice thinned toward the deeper water shown in Fig. 2 and along the shelf leading into the area of Fig. 3; that is, to the right of where I stood in Fig. 4, where thinning ice and open water were clearly visible. We treated that edge with caution when passing through. Other positions on the lake were safe for extended recording.
In fact, melting ice that season produced especially vivid sounds. Our local guides call the phenomenon long hou (“dragon roar”); people who live in the area know it well. On the multimedia report, that signature is easy to find in green bar areas (it is of course not limited to them). It sits within the wider biological soundscape of the basin in a way that still feels astonishing.
Those same green bars are what we handed to birdidpv. Running BirdNET over the 135 unflagged ranges, 43 minutes of the three-hour take, returned 128 detections in 11 species. Red-billed Chough and Large-billed Crow account for 99 of them and are unambiguous. The other nine are all deep-voiced waterbirds, on a lake that had frozen over, and we think most of them are the dragon roar being heard as birds.
Not all of them, though. The second highest score any species reached in three hours, Ruddy Shelduck at 0.928, is Xiao Zhang calling across the lake to me. There is no bird in it. The identification report renders every detected frame as a spectrogram so these readings can be checked rather than taken on trust, and that frame is the one to open first: it shows a voice, it shows how easily a voice reads as a call, and it shows why the only thing that settled the question was listening.
This is worth more thought. We would like the toolkit to find human speech on its own and leave it out of what gets uploaded, so that an outdoor ecological recording can be shared without a voice riding along in it. This matters most on trips with a guide. We value the talk that happens along the way and would not want to lose it, but when the aim is the soundscape itself, or a soundscape captured while walking, every voice left in the take becomes work someone has to do by hand later.
Our recording setups in the field vary; for this three-hour test take, the chain we used is described under Input. See Fig. 4 for a field photograph of the session.
field-audio-toolsThree CLI tools for long Zoom F3 takes: excerpt a timestamped range with provenance, screen fixed windows for low-frequency dominance, or ask BirdNET what it hears in the windows that screening left alone.
PATH, both ffmpeg and ffprobeThere is no PyPI release yet. Install from a checkout:
python -m venv .venv
. .venv/bin/activate
python -m pip install -e '.[test]'
pytestA checkout on FAT or exFAT has no symbolic links, which venv needs for its
bin/. Build the environment on a Linux filesystem instead and point it at the
checkout:
python -m venv ~/.venvs/field-audio-tools
~/.venvs/field-audio-tools/bin/pip install -e '/path/to/field-audio-tools[test]'That is all soundcite and lowdom need. birdidpv takes one more extra:
pip install -e '.[birdnet]'It pulls in TensorFlow, which is much heavier than the rest of the project, and
downloads the BirdNET model files on first run. Without it birdidpv exits with
an install hint. soundcite and lowdom are unaffected either way.
Bare HTML from the CLI; demo packages linked here add site navigation and shared media styling.
Both commands used the same pair of synchronized mono WAV exports from Take 002. They read the files in track order, in place, without creating a merged stereo intermediate. Site access, camp, and on-lake conditions are under Field details above.
260115_002_Tr1.WAV and 260115_002_Tr2.WAVsoundcite: an arbitrary excerptWe cut a 20-second passage beginning one hour into the take. Because we launched the command from the recording directory, the manifest kept the source filenames without exposing local absolute paths.
soundcite 260115_002_Tr1.WAV 260115_002_Tr2.WAV \
--start 01:00:00 \
--duration 20 \
--title "Take 002 - 01:00:00 excerpt" \
--recordist "Gewenxin Yu" \
--recorded-at "2026-01-15T13:19:25+08:00" \
--location "Yema Haizi, Kangding, Sichuan" \
--license "CC BY-NC-SA 4.0" \
--notes "Recorded with two Primo EM273 omni-directional microphones (−37 dB ± 3 dB at 1 kHz, 80 dB S/N ratio, 60 Hz ~ 20 kHz) connected to a Zoom F3." \
--output 260115-002-soundciteSHA256SUMS, and static HTML
Browser preview and two-channel logarithmic spectrogram generated in the same run. The preview is convenient but lossy; the spectrogram is generated from the two WAV excerpts, which preserve floating-point samples.
soundcite outputSHA256SUMS covers both excerpts, the preview, spectrogram, manifest, and
static page. We did not ask it to hash the complete source files in this run.Open the generated packageOpen the bare CLI package page (tool-generated HTML)Read its manifestVerify its checksums
lowdom: screening the takelowdom decoded both tracks to mono at 4 kHz and divided them into
10-second windows. It measured those windows without writing a transformed
audio file.
lowdom 260115_002_Tr1.WAV 260115_002_Tr2.WAV \
--window 10 \
--threshold 0.95 \
--report both \
--output 260115-002-lowdomWe tested 0.95 with two blinded listening rounds: each round contained ten randomly selected windows above the threshold and ten below it. Of the 20 windows above 0.95, 19 were judged to contain obvious wind noise. Of the 20 below it, 17 were judged not to contain obvious wind noise.
The threshold and listening judgement agreed on 36 of 40 samples. This is useful for prioritising listening on this take, but it is not a validation study: the sample is small, it comes from one recording setup, and it was reviewed by one listener. The score remains a low-frequency screening heuristic rather than a wind probability.
lowdom outputwindows.csv retains both tracks' score, RMS level, low-frequency ratio,
spectral flatness, and peak for every interval.candidate-clean-intervals.csv lists contiguous below-threshold ranges for
listening queues (135 intervals on this take).summary.json records the frequency bands, sample rate, threshold, merge
rule, source metadata, and totals.lowdom writes to report.html is also kept in the package as
report-bare.html.report-multimedia.html is also kept here as
report-multimedia-bare.html (same recording-preview-48k/ segments).Open the multimedia reportOpen the bare CLI multimedia reportOpen the site data reportOpen the bare CLI timeline reportDownload the window dataDownload candidate clean intervalsRead the summary
summary.json or the reports.birdidpv: the clean windowslowdom says where to listen. It does not say what is there. It has already
found the stretches of this three-hour take where wind is low enough to work
with, and birdidpv starts from that answer. The
candidate-clean-intervals.csv file above becomes the list
of ranges we hand to BirdNET, as timestamps
rather than as copied audio. The model then hears only the 23.86% of the take
that screening left unflagged.
None of this says the windows at or above 0.95 are empty. Plenty of birds call through wind, and the orange stretches of the timeline hold some of them (the orange bars next to a green one are the ones to try first, and they often turn something up). The aim here is narrower: find which species turn up where wind is low, and collect those seconds as recordings clean enough to send to a free archive like xeno-canto.
birdidpv 260115_002_Tr1.WAV \
--intervals 260115-002-lowdom/candidate-clean-intervals.csv \
--lat 29.898 --lon 102.030 --week 3 \
--min-conf 0.25 \
--photos \
--spectrograms \
--report both \
--output 260115-002-birdidpv260115_002_Tr1.WAV onlyThe coordinates come from the end of the GPX trace, at the camp on Yema Haizi. With them, BirdNET cuts its list of 6,522 species down to the 788 that could be there in mid-January. Without them, the model offers birds from the wrong continent, and it sounds just as sure when it does. A three-hour take gives it many chances. Even the shorter list is long enough to hold every mistake below.
Two corvids make up 99 of the 128 frames, and neither is in doubt: Red-billed Chough (82 frames, highest 0.9998) and Large-billed Crow (17 frames, highest 0.9118). Both have harmonic structure well above 2 kHz, and both sit 3–11 dB louder than everything else in the run.
The other nine species share 29 frames. All of them are deep-voiced waterbirds: Great Bittern, Ruddy Shelduck, Eurasian Coot, Graylag Goose, Eurasian Curlew, Eurasian Wigeon, Mallard, Common Shelduck, Gray Heron. The lake had frozen over. Great Bittern is the clearest error: twelve detections, highest 0.9064, arriving in long runs with eight of them inside seventy seconds. Not one of their spectrograms shows a call above the low-frequency bed. That bed is the melting-ice resonance described under Field details. Our guides call it long hou, and the model hears it as the birds whose calls are booms.
It is worth comparing the two by ear. The first page of xeno-canto for Botaurus stellaris has XC891071, XC1000766, XC832807 and XC741523. The booms really do sound like the dragon roar. They are close enough that the model's mistake looks fair. What the bird does not have is the electronic quality the ice carries. That is the one thing that tells them apart. It is easy to hear, and none of the spectrogram numbers we worked out found it.
This one frame matters more than the pattern. The model calls the frame at 01:41:05 Ruddy Shelduck at 0.928, the second highest score any species reached in the run. There is no bird in it. It is Xiao Zhang calling across the lake to the recordist, who knew the voice as soon as he heard it.
We only got there after two wrong answers. The band measurements put the frame with the ice, but for the wrong reason. Then its spectrogram showed clear harmonic stacks between 1 and 3 kHz. Ice does not make those, so we read it as a real call and took the species off the doubtful list. Both readings were wrong, in opposite directions, and the picture that fixed the first one is what led us into the second. A voice looks like that too: a pitch contour sliding through its harmonics, formants near 1.2 and 2 kHz, four syllables in the last second.
Three things follow. First, confidence does not tell true from false on this take. At every threshold from 0.25 to 0.9, the false positives match or beat the real detections, so raising the bar only drops the harmless errors and keeps the dangerous ones. Second, anthropophony is not in a filtered species list. Human sound has no label of its own, so it has to come out as some bird. Third, a spectrogram tells you that something is there. It does not tell you what. Only listening settled this one.
lowdom screened both tracks and kept the higher score of the
two. This
birdidpv run read Tr1 on its own. A call that came out better on the second
microphone could be missed, and nothing here says anything about stereo or
direction. The tool can read both tracks and keep the highest confidence per
frame. That is the next thing to do.
birdidpv outputdetections.csv has every 3-second frame above the minimum confidence, with
its timecodes, species, and score. species.csv totals them by species.summary.json records the source, the ranges analysed, the position and week
used to filter, and every other setting of the run.detection-spectrograms/ holds 126 WebP images, one per detected frame.
FFmpeg made them from the source WAV, not from the lossy preview. Frames that
start at the same time share an image, so 128 detections need only 126 files.
Each one is a single call, here the Red-billed Chough frame at 00:40:35:ffmpeg -ss 2435 -t 3 -i 260115_002_Tr1.WAV \
-lavfi "[0:a:0]showspectrumpic=s=560x260:legend=1:scale=log:fscale=log:color=intensity" \
-frames:v 1 frame-002435000.webpfscale=log is the part that decides what you see. On a linear frequency axis
the whole band below 500 Hz collapses into a line at the bottom of the image,
and that band is where the ice sits and where most of these detections are
argued about. scale=log does the same for amplitude, and legend=1 prints
the frequency, time and dBFS axes. s=560x260 sets the spectrum area; the
legend adds roughly 280x130 around it, so the files are 844x388. The four
xeno-canto recordings above are rendered with the same call, over their full
length instead of three seconds.
species-photos/ holds one reference photograph per species from
iNaturalist, kept in the package instead of
hot-linked, so the page still works if it is saved and no one else learns who
read it. credits.json records each photographer, licence, and source. A
photograph shows what a species looks like. It is not proof that the bird was
there, and nine of these eleven go with rows we are asking you to doubt.lowdom's recording-preview-48k/ segments
instead of copying 62 MB of audio. Click a detection to play its three seconds
and see its spectrogram. Click a species to show only that species on the
timeline.lowdom, the site reports add navigation and
styling. The bare
HTML 4.01 pages the CLI writes are kept next to them as report-bare.html
and report-multimedia-bare.html.Open the multimedia reportOpen the bare CLI multimedia reportOpen the site data reportOpen the bare CLI timeline reportDownload every detectionDownload the species roll-upRead the summary
soundcite: citing a detectionThe excerpt at the top of this test starts at 01:00:00. We chose that time from the clock, before we had looked at the recording. It shows what the output format is, not what is in the take.
After birdidpv there is a better time to cut. Sort detections.csv by time
and the longest unbroken run in the take starts at 00:40:29: seven frames back
to back, all Pyrrhocorax pyrrhocorax, scoring 0.9863 to 0.9998. The single
highest frame in the whole take sits inside it, at 00:40:35. A run is better
evidence than one frame, so cut from the start of the run rather than from its
best frame. detections.csv stores that time as start_timecode in
HH:MM:SS.mmm, which is one of the formats --start accepts, so the value can
be copied straight across:
soundcite 260115_002_Tr1.WAV 260115_002_Tr2.WAV \
--start 00:40:29.000 \
--duration 21 \
--pad-start 3 \
--pad-end 4 \
--title "Take 002 - Red-billed Chough at 00:40:29" \
--recordist "Gewenxin Yu" \
--recorded-at "2026-01-15T12:59:54+08:00" \
--location "Yema Haizi, Kangding, Sichuan" \
--license "CC BY-NC-SA 4.0" \
--notes "Seven consecutive BirdNET frames identified Pyrrhocorax pyrrhocorax here, scoring 0.9863 to 0.9998. Recorded with two Primo EM273 omni-directional microphones (-37 dB +/- 3 dB at 1 kHz, 80 dB S/N ratio, 60 Hz ~ 20 kHz) connected to a Zoom F3." \
--output 260115-002-chough-004029--duration 21 covers the seven frames. The two --pad values keep three
seconds before them and four after, because 00:40:29 is a frame boundary, not
the moment the bird began. BirdNET only says the call falls somewhere inside
those three seconds, so cutting exactly on the line can clip the first call.
The written excerpt therefore runs 00:40:26 to 00:40:54, starting at sample
manifest.json keeps the two ranges apart: the top level describes
the 28 seconds in the WAV, because that is what the checksums cover, while
clip.cited records the 21 seconds being cited. The excerpt is lossless and
links back to both source tracks.The 28 seconds this section keeps pointing at: the 21 seconds being cited, plus three seconds before and four after. This player uses the lossy preview; the two 32-bit float WAV excerpts in the package are what the checksums and the spectrogram are taken from.
Set --duration to cover the last frame you want, then use --pad for
listening room around it. --pad sets both ends at once; --pad-start and
--pad-end override it when the two differ.
This step is manual. Before citing a time you have to listen to it, and the
0.93 that was a person
shows why. A script that sent detections straight to soundcite would have
built an evidence package for a shout.
Open the bare CLI package page (tool-generated HTML)Read its manifestSee its spectrogramVerify its checksums
soundcite did its job on this recording: it turned a timestamp into a small
package with enough information to trace the excerpt back to its sources.
lowdom completed the analysis, and the provisional 0.95 threshold produced
a useful listening split on this take. birdidpv then turned that split into a
listening queue with names attached, and the names are where the caution has to
move next.
The three tools now form a loop rather than a line. Three hours become 135
ranges worth hearing, those ranges become 128 frames worth checking, and the
frames that hold up go back to soundcite as times worth citing. Each step
cuts down what a person has to listen to. Only the first step is automatic:
birdidpv reads the CSV lowdom writes. The step back to soundcite is
manual, by choice. None of this removes the listening, and the 01:41:05 frame
shows why.
Next, we need to report partial windows, repeat the listening test across other
recordings, microphones, wind protection and environments, and run birdidpv
across both tracks rather than one. Low-frequency screening remains more
accurate than wind detection, and a machine identification remains a
prompt to listen rather than a record.
Kahl, S., Wood, C. M., Eibl, M., & Klinck, H. (2021). BirdNET: A deep learning solution for avian diversity monitoring. Ecological Informatics, 61, 101236.
xeno-canto. Four Eurasian Bittern (Botaurus stellaris) recordings, used for the listening comparison in What came back: XC891071 (Christian Bøggild, Denmark), XC1000766 (Christian Bøggild, Denmark), XC832807 (Cedric Mroczko, Ukraine), XC741523 (Romuald Mikusek, Poland).
iNaturalist. Eleven reference photographs, one per detected species, by
Pavel Shukov, egorbirder, Joe Bourget, Tatyana Zarubo, Анна Голубева, Frans Vandewalle, Wei Li Jiang, zametnya, one anonymous, thegreatdodo and Frank Sengpiel. Licences and source URLs are in species-photos/credits.json.
Wikidata. Taxon identifiers for the eleven species, matched on iNaturalist taxon ID and resolved to Avibase, eBird, GBIF, ITIS, IUCN, EOL and iNaturalist.