1Solar position (day/night shading, twilight windows)
Every sunrise/sunset boundary on the timeline — the day/night background stripes, and the dawn/dusk twilight windows behind the Matutinal/Vespertine/Crepuscular sort modes — comes from a standalone implementation of the general-purpose "sunrise equation": Julian day number → solar mean anomaly → equation of center → ecliptic longitude → solar declination → hour angle, evaluated for the site's fixed coordinates and the calendar date in question, with the horizon taken at −0.833° to account for atmospheric refraction and the sun's apparent radius.[1] This is the same derivation NOAA's own public solar calculator is built on.[2]
computeSunTimes(), getDayNightBoundaries() — falls back to a fixed 06:00/18:00 split above the Arctic/Antarctic circles or when coordinates aren't set.
2Lunar phase
The moon-phase icons on the timeline, the Full Moon/New Moon/Waxing/Waning sort modes, and the 4-bar moon-phase histogram all come from a linear synodic-month approximation: phase advances at a constant rate from a known reference new moon, using the standard mean synodic month length of 29.530589 days.[1] Illumination fraction follows the usual (1 − cos(2π·phase)) / 2 approximation. This is a linear model, not full orbital mechanics, so exact phase moments can be off by roughly an hour up to half a day depending on time of year — closest for full moons, since the model is anchored there.
moonPhaseFraction(), moonPhaseQuadrant(), lunarPhaseEventsInRange()
3Diel-activity classification & entropy
Nocturnal/Diurnal split at true sunrise/sunset (§1) and are complementary by construction. Matutinal/Vespertine/Crepuscular use fixed 90-minute twilight windows centred on sunrise/sunset. Cathemeral — how evenly an ID's activity spreads across the 24-hour day, rather than favouring any particular window — is scored as the normalised Shannon entropy of the ID's hourly detection histogram: H = −∑p ln p over the 24 hourly bins, divided by ln 24 so the result runs 0–100%.[3] This is the original 1948 definition of information entropy, repurposed here as an evenness index rather than a measure of information content — a common reuse of the same formula in ecology, where it's often called a diversity or evenness index.
computeDayNightById() — evennessPct field
4Species behavioural heuristics
The five species/sub-species probability scores (Walleye, Bass, Muskellunge, and the Smallmouth/Largemouth split of Bass) are heuristic classifiers, not identifications — each one scores an ID's existing timing/speed metrics (§1–3, §5) against a hand-tuned profile of published behaviour for that species, then normalises the scores to sum to 100%. The behavioural pattern each profile is caricaturing is real and documented:
Walleye are strongly light-averse (negatively phototactic) due to a tapetum lucidum that enhances low-light vision at the cost of visual acuity in bright conditions, driving pronounced nocturnal/crepuscular foraging — the foundational study on this is Ryder's work on light-controlled walleye behaviour.[4] This motivates the heuristic's reward for high Nocturnal % and concentrated (low-Cathemeral) activity.
Largemouth bass show a documented diel timing shift tied to water temperature and time of day — radio-tracked fish in a Georgia reservoir spent daylight hours largely stationary offshore in deep water near cover, with movement shifting toward shoreline areas at dusk and into the night.[5] This is the basis for rewarding a high Diurnal % and evenly-spread (high-Cathemeral) activity relative to the more concentrated, ambush-oriented pattern below.
Muskellunge are ambush predators with a well-documented crepuscular activity peak — an acceleration-telemetry study on the Rideau River found activity lowest at dawn, rising through the day, and peaking sharply at dusk before declining overnight.[6] This directly motivates rewarding high Crepuscular % together with high Max Speed (burst movement around the dusk peak).
Smallmouth vs. largemouth (the Bass sub-split, shown only when Bass is the top guess) is grounded in a direct comparative telemetry study: smallmouth bass ranged over home ranges an order of magnitude larger than largemouth bass in the same system (142.6 ha vs. 12.9 ha) and moved in and out of a harbour in direct response to water temperature, while largemouth bass stayed comparatively put.[7] This is the basis for weighting Smallmouth toward high Diurnal % and high Max Speed (active, wide-ranging), and Largemouth toward high Nocturnal % and low Max Speed (sedentary, restricted-range).
speciesProbability(), computeSpeciesPctById(), bassSubtypeProbability(), computeBassSubtypePctById()
5Signal strength as a movement proxy
Tag Explorer has no positional data — only a single receiver's signal-strength reading per detection — so "Max Speed" is a proxy: the largest |change in signal strength| per minute between two detections of the same ID within a 30-minute window, scaled to a nominal m/s figure. This follows the general logic used throughout single-receiver acoustic and radio telemetry, where received signal strength (RSS/RSSI) is used as an indirect indicator of an animal's proximity and movement relative to a fixed receiver, in the absence of true multi-receiver positioning.[8] It is a relative, receiver-specific measure, not a calibrated physical speed.
computeMaxSpeedById() — SPEED_MAX_GAP_MS caps the window at 30 minutes
6Residency & co-occurrence ("Find my buddies")
"IDs by Residency (Days)" — the span between an ID's first and last detection — is a simplified version of the residency metrics widely used in acoustic-telemetry fish and shark studies to summarise how long a tagged animal stayed within range of a receiver array, one of many derived metrics the field has standardised around as telemetry has scaled up.[8] "Find my buddies" — crediting another ID if it has a detection within ±30 minutes of the target ID's detection, for at least one such pairing anywhere in the file — is a simple, single-tool version of association analysis: the general study of which individuals in a tracked population tend to be detected together, most formally developed for cetacean and other social-mammal telemetry.[9] The tool's ±30-minute window-and-count approach is a hand-built heuristic, not an implementation of any particular association index from that literature (e.g. it doesn't compute a simple ratio index or a full association matrix).
computeResidencyDaysById(); the buddies click-handler on findBuddiesBtn
7Tag Extractor: narrow-band tone detection
Tag Extractor's job is to find one known tone — the tag's 69 kHz pulse — inside a raw WAV recording, window by window. Rather than computing a full spectrum (an FFT) and reading off one bin, it evaluates a single frequency directly with the Goertzel algorithm: a second-order recursive filter that computes one point of the discrete Fourier transform in a fixed, small amount of work per sample, independent of how many frequency bins a full FFT of the same length would produce.[10] This is evaluated three times per analysis window, not once — at 69 kHz (the tag tone) and at two flanking reference bands, 60 kHz and 80 kHz, used for noise blanking (§8) — over windows as short as 5 ms, so the tool localizes a pulse in time as well as detects its presence.
goertzelMagnitude(), streamExtractGoertzel() — REF_LOW / TARGET_FREQ / REF_HIGH constants (60/69/80 kHz)
8Tag Extractor: windowing & noise blanking
Before each Goertzel evaluation, the window's samples are multiplied by a Hann window — a raised-cosine taper — to control spectral leakage. This addresses a well-characterized problem: a plain (rectangular) segment smears a tone's energy across neighbouring frequency bins whenever the segment isn't an exact whole number of the tone's cycles, and the classic survey of window functions for harmonic analysis with the DFT catalogues exactly this trade-off between leakage suppression and main-lobe width across window shapes.[11] The Hann window is a common, moderate choice on that spectrum.
Separately, broadband noise that happens to fall inside the 69 kHz window — clicks, wind, electrical interference — is suppressed by estimating a noise floor from the two flanking 60/80 kHz bands (their average Goertzel magnitude) and subtracting it from the raw 69 kHz reading, scaled by an adjustable strength factor. This is the same underlying idea as spectral subtraction, the foundational technique for removing broadband noise from a signal by estimating the noise spectrum from a reference where the signal of interest is absent, then subtracting that estimate from the signal-plus-noise spectrum — originally developed for speech enhancement, adapted here to a single pair of narrow reference bands rather than a full estimated noise spectrum across the audible range.[12]
hann array in streamExtractGoertzel(); computeDenoised() — blanking-strength slider
9Tag Extractor: automatic & adaptive thresholding
Deciding how strong a denoised reading has to be before it counts as a real pulse, rather than leftover noise, is handled two ways. The "Suggest" feature first runs Otsu's method on a log-scaled histogram of the non-blanked signal — the standard technique for automatically picking a threshold that best separates a two-cluster distribution (here, noise residue vs. genuine pulses) by maximizing the variance between the two candidate classes while minimizing variance within each.[13] Because that split sits exactly at the boundary between the two clusters rather than clear of the noise cluster's own spread, the tool then uses Otsu's result only to identify a provisional noise cluster, and sets the actual threshold at that cluster's median plus a user-adjustable multiple of its median absolute deviation (MAD) — a robust measure of spread that, unlike the standard deviation, isn't itself distorted by the very outliers (the real pulses) it needs to sit clear of.[14] Setting a detection threshold at median-plus-k×MAD above an estimated noise level is itself an established technique, most widely known from its use in setting amplitude thresholds for detecting neural spikes buried in noisy extracellular recordings.[15] The threshold can be computed once for a whole recording, or recomputed independently in rolling time windows so it tracks a noise floor that drifts over the course of a file.
otsuBinIndex(), computeNoiseLogStats(), applyStrictness() — strictness (k) slider, local/global & rolling-window mode toggle
10Tag Extractor: pulse-train segmentation & ID decoding
Once individual pulses are detected (the peak reading within each threshold-crossing burst), consecutive pulses are grouped into trains: a train is triggered by two consecutive pulses whose gap falls inside a configured window, after which every pulse within a fixed duration of the first is folded into that same train regardless of its own spacing. Grouping discrete threshold-crossing events into trains by their inter-event timing is a hand-built heuristic tuned to this tag system's pulse pattern, not an implementation of a particular published algorithm — the closest formal parallel is interval-based burst detection in spike-train analysis, where a train (there, a "burst") is likewise identified from the timing between successive threshold-crossing events rather than from their individual shapes.[16]
Each train is then assigned a hex ID by decoding its sequence of inter-pulse gaps one digit at a time — for each gap: subtract 420, divide by 20, round to the nearest integer, and clamp to [0, 15] — concatenating the resulting hex digits in gap order. This is the tag manufacturer's own inter-pulse-interval ID encoding for this transmitter line, applied here as specified for the project rather than derived from a general published method; it has no external citation for the same reason the species-behaviour weights in §4 don't — it's a fixed, hardware-defined constant, not a tuned or borrowed one.
extractPulses(), extractTrains() — Min/Max Gap, Train Window; calcTrainId()
11Tag Extractor: file formats & real-world timestamps
WAV files are parsed directly against the RIFF/WAVE chunk structure — a 12-byte RIFF/WAVE header followed by a sequence of tagged chunks, of which a fmt chunk (sample rate, channel count, bit depth) and a data chunk (the samples themselves) are read here — the same canonical layout described in the community-maintained WAVE PCM soundfile format reference, itself a distillation of the original IBM/Microsoft Multimedia Programming Interface and Data Specifications.[17] Files are streamed in fixed-size chunks rather than loaded whole, so recordings much larger than available memory can still be processed.
Where amplitude is shown on a dB scale instead of linear, the conversion is the standard digital-audio dBFS (decibels relative to full scale) convention — 20 × log₁₀(amplitude), with 0 dBFS representing the maximum representable sample value — as covered in any standard digital audio engineering text.[18]
Converting a detection's position within a WAV file into a real-world date and time draws on two further pieces, both external to Tag Extractor itself: the recording's local start time as embedded by the recorder in GUANO, an open, extensible metadata format now used across most current bat/ultrasonic recorders to embed timestamps and related fields directly in the WAV file;[19] and a per-file correction for the silence-compression time drift AudioMoth recordings can accumulate, looked up from a companion dataset (time2wav.csv) rather than computed here. The lookup and correction logic is a direct in-browser port of a standalone conversion function written for the project's WAV Silence Analyzer tool, restructured to fetch that dataset once per batch of files instead of once per detection.
parseWavHeader(); toDb() / DB_FLOOR; computeTrainTimes(), parseFormattedLocalTimestamp(), findDelta() — ported from convertOneDetection.js
12Fix Broken IDs: edit-distance correction
Fix Broken IDs treats a detection CSV's ID column the way a spelling checker treats a document: a small set of known-good "dictionary" entries (here, the Top 10 most frequent IDs, standing in for a real dictionary of valid tag IDs), and a much larger pool of candidate strings to check against it. Comparing a suspect string to every dictionary entry by edit distance and flagging the closest one within some threshold is the core technique of essentially all classical spelling-correction systems, as catalogued in the standard survey of the field.[20] The distance measure used throughout — count of single-character insertions, deletions, and substitutions needed to turn one string into another — is the original Levenshtein distance, computed here with the standard dynamic-programming recurrence over a full matrix of prefix distances.[21]
The same distance function is reused in two different roles. "Possible Errors" measures every ID against the Top 10 directly, at a caller-adjustable maximum distance. The "Cross-Length Typo Check" instead measures the whole ID pool against itself, grouped by character length, independent of Top 10 membership — comparing every ID of length N against every ID of length N+1 or longer, on the reasoning that a single dropped or altered character is the dominant real-world error mode for a fixed-length decoded ID, so the two lengths most likely to explain each other are adjacent ones. Neither the Top-10-anchored search nor the length-paired self-comparison is a published algorithm in its own right — both are this tool's own application of the general edit-distance-correction idea above to a Top 10 reference set and a length-bucketed ID pool respectively, rather than a free-text dictionary.
editDistance(); findCloseMatches() — "Max closeness" (maxDistInput) slider; findAdjacentLengthMatches() — the 8v9+, 7v8+, ... 4v5+ tiers; the Auto Fix sequence in runAutoFix() runs distance 1 before distance 2, on the same logic Kukich's survey describes for ranking correction candidates by decreasing confidence.
13Fix Broken IDs: shared-substring matching
Alongside edit distance, the Top-10-vs-pool comparison is repeated with a stricter, purely exact test: does the candidate ID contain some contiguous run of characters — 9, 8, 7, 6, 5, 4, or down to 3 characters, checked longest-first per pair so the strongest available match wins — that also appears somewhere in a Top 10 ID? This is a windowed, fixed-length version of the general idea behind shingling: representing a string by the set of its fixed-length substrings ("shingles" or n-grams) and comparing two strings by how much those substring sets overlap, the technique underlying large-scale near-duplicate and resemblance detection for documents.[22] The tool's version is a direct, unhashed containment check between two specific strings rather than shingling's usual sketch-and-compare approach across a large corpus (no hashing, no MinHash sketch, no corpus-wide index) — a methodological parallel at small scale, not an implementation of the full technique.
The 8- and 7-character tiers are additionally allowed to compare Top 10 entries against each other, not just against the wider pool — catching the case where a mis-decoded variant of a tag ID is itself frequent enough to have made the Top 10. To avoid the same pair being flagged in both directions, only the lower-frequency entry of any such pair may be the one "corrected" (§14).
findSubstringMatches() — the tiers array (len / allowedFoundLengths per tier) in renderSubstringMatches() and runAutoFix()
14Fix Broken IDs: frequency-based canonical selection
Whenever a candidate ID could plausibly be corrected toward more than one Top 10 entry — genuine ties in edit distance or match length — the tool breaks the tie toward whichever candidate occurs more often in the file, on the reasoning that the more frequently detected of two similar-looking IDs is more likely to be the real one. Preferring the more frequent of several near-duplicate variants as the canonical value is a standard tie-breaking rule in duplicate detection and record-linkage systems, discussed generally in the standard survey of that field as one of several common "merge/survivorship" rules for resolving conflicting duplicate records.[23] The same rule is applied more strictly for the Top-10-vs-Top-10 comparisons in §13: there, the higher-frequency entry is not just preferred on a tie, it is the only permitted direction of correction, since both candidates are already known-frequent IDs and "correcting" the more common one toward the rarer one would very likely be backwards.
buildReplacementsFromMatches() — first-candidate-wins tie-breaking, fed by matches already sorted by count; the count >= topCount guard in findSubstringMatches()
15Fix Broken IDs: CSV parsing & escaping
Reading and rewriting the detection CSV follows the de facto standard for the format: comma-separated fields, records separated by CRLF, and any field containing a comma, double quote, or line break wrapped in double quotes with embedded double quotes doubled — documented (after decades of informal, inconsistent use) as an IETF informational RFC.[24] The parser and writer here implement that quoting rule directly rather than via a library, which is what lets the tool round-trip a file's original field formatting untouched except where a fix or normalization step deliberately changes a value.
parseCsvLine(), serializeCsvField()
16Fix Broken IDs: native file access
Opening a file and saving corrections back to disk use the browser's File System Access API where available, rather than the older pattern of a hidden <input type="file"> for reading and a synthetic download link for writing. This gives the tool two things the older pattern can't: a real FileSystemFileHandle that can be reopened for writing without a fresh picker dialog each time, and a save dialog that can be told which folder to open in via its startIn option — used here to default every save to the same folder the working file was originally opened from. The API is a WHATWG/W3C Community Group living standard, implemented in Chromium-based browsers at the time of writing.[25] Where the API is unavailable or blocked (Firefox, Safari, or a sandboxed preview context), the tool falls back to the classic file-input-plus-download-link pattern automatically.
openViaPicker(), saveTextAsCsv() — showOpenFilePicker() / showSaveFilePicker() / startIn, with a plain <input> and URL.createObjectURL() download fallback
Reference list
- Meeus, J. (1998). Astronomical Algorithms (2nd ed.). Willmann-Bell. Standard reference for the sunrise/sunset (solar position) equation and lunar-phase calculations.
- NOAA Global Monitoring Laboratory. Solar Calculator. gml.noaa.gov/grad/solcalc Public implementation of the same underlying solar-position algorithm.
- Shannon, C. E. (1948). A mathematical theory of communication. The Bell System Technical Journal, 27(3), 379–423. Origin of the entropy formula reused here as the Cathemeral evenness score.
- Ryder, R. A. (1977). Effects of ambient light variations on behavior of yearling, subadult, and adult walleyes (Stizostedion vitreum vitreum). Journal of the Fisheries Research Board of Canada, 34(9), 1481–1491. doi:10.1139/f77-213
- Sammons, S. M., & Maceina, M. J. (2005). Activity patterns of largemouth bass in a subtropical US reservoir. Fisheries Management and Ecology. doi:10.1111/j.1365-2400.2005.00456.x
- Landsman, S. J., Martins, E. G., Gutowsky, L. F. G., Suski, C. D., Arlinghaus, R., & Cooke, S. J. (2015). Locomotor activity patterns of muskellunge (Esox masquinongy) assessed using tri-axial acceleration sensing acoustic transmitters. Environmental Biology of Fishes, 98, 2109–2121. doi:10.1007/s10641-015-0433-1
- Carter, M. W., et al. (2012). Movement patterns of smallmouth and largemouth bass in and around a Lake Michigan harbor: The importance of water temperature. Journal of Great Lakes Research, 38, 396–401.
- Hussey, N. E., Kessel, S. T., Aarestrup, K., Cooke, S. J., Cowley, P. D., Fisk, A. T., Harcourt, R. G., Holland, K. N., Iverson, S. J., Kocik, J. F., Mills Flemming, J. E., & Whoriskey, F. G. (2015). Aquatic animal telemetry: A panoramic window into the underwater world. Science, 348(6240), 1255642. doi:10.1126/science.1255642 General acoustic-telemetry methodology overview — relevant background for the signal-strength-as-movement-proxy and residency metrics both.
- Whitehead, H. (2008). Analyzing Animal Societies: Quantitative Methods for Vertebrate Social Analysis. University of Chicago Press. Standard reference for co-occurrence / association analysis in animal telemetry — a methodological parallel to "Find my buddies," not its literal source.
- Goertzel, G. (1958). An algorithm for the evaluation of finite trigonometric series. The American Mathematical Monthly, 65(1), 34–35. Core detection algorithm behind Tag Extractor's 69 kHz tone detection — see §7.
- Harris, F. J. (1978). On the use of windows for harmonic analysis with the discrete Fourier transform. Proceedings of the IEEE, 66(1), 51–83. doi:10.1109/PROC.1978.10837 Standard survey of window functions and spectral leakage; motivates the Hann window applied before each Goertzel evaluation.
- Boll, S. F. (1979). Suppression of acoustic noise in speech using spectral subtraction. IEEE Transactions on Acoustics, Speech, and Signal Processing, 27(2), 113–120. doi:10.1109/TASSP.1979.1163209 Origin of spectral subtraction — the general technique the flanking-band noise-blanking step is a narrow, two-reference-band adaptation of.
- Otsu, N. (1979). A threshold selection method from gray-level histograms. IEEE Transactions on Systems, Man, and Cybernetics, 9(1), 62–66. doi:10.1109/TSMC.1979.4310076 Automatic two-class threshold selection used as the first stage of the "Suggest" threshold feature.
- Leys, C., Ley, C., Klein, O., Bernard, P., & Licata, L. (2013). Detecting outliers: Do not use standard deviation around the mean, use absolute deviation around the median. Journal of Experimental Social Psychology, 49(4), 764–766. doi:10.1016/j.jesp.2013.03.013 General case for median absolute deviation (MAD) as a robust, outlier-resistant alternative to the standard deviation — the statistic the strictness slider scales.
- Quian Quiroga, R., Nadasdy, Z., & Ben-Shaul, Y. (2004). Unsupervised spike detection and sorting with wavelets and superparamagnetic clustering. Neural Computation, 16(8), 1661–1687. doi:10.1162/089976604774201631 Directly analogous use of a median-plus-k×MAD amplitude threshold to separate real events from noise in a continuous signal — there, neural spikes; here, tag pulses.
- Legendy, C. R., & Salcman, M. (1985). Bursts and recurrences of bursts in the spike trains of spontaneously active striate cortex neurons. Journal of Neurophysiology, 53(4), 926–939. doi:10.1152/jn.1985.53.4.926 Methodological parallel for interval-based grouping of discrete events into trains/bursts — not the literal source of the pulse-train grouping rule, which is a hand-built heuristic (see §10).
- Stanford CCRMA. WAVE PCM soundfile format. ccrma.stanford.edu/courses/422-winter-2014/projects/WaveFormat Widely used community reference for the canonical RIFF/WAVE chunk layout Tag Extractor parses; distilled from IBM & Microsoft's original Multimedia Programming Interface and Data Specifications 1.0 (1991).
- Pohlmann, K. C. (2010). Principles of Digital Audio (6th ed.). McGraw-Hill. Standard digital-audio engineering reference for the dBFS (decibels relative to full scale) convention used on the amplitude chart's dB scale.
- Riggs, D. GUANO: the Grand Unified Acoustic Notation Ontology — bat acoustic metadata format specification. guano-md.org
Open metadata format embedded in WAV files by most current bat/ultrasonic recorders, including the source of the local-time field
time2wav.csvis built from; not parsed directly by Tag Extractor itself. - Kukich, K. (1992). Techniques for automatically correcting words in text. ACM Computing Surveys, 24(4), 377–439. doi:10.1145/146370.146380 Standard survey of spelling-correction techniques — edit distance against a reference dictionary, frequency-informed ranking of candidates — that Fix Broken IDs' overall approach (Top 10 as reference set, IDs as "words" to correct) parallels.
- Levenshtein, V. I. (1966). Binary codes capable of correcting deletions, insertions, and reversals. Soviet Physics Doklady, 10(8), 707–710.
Origin of the edit-distance metric (
editDistance()) used throughout the close-matches and cross-length typo checks. - Broder, A. Z. (1997). On the resemblance and containment of documents. Proceedings of Compression and Complexity of Sequences (SEQUENCES '97), 21–29. doi:10.1109/SEQUEN.1997.666900 Foundational paper on shingling / fixed-length substring overlap for comparing strings — a methodological parallel for the substring-matching tiers, not an implementation of the full (hashed, corpus-scale) technique.
- Naumann, F., & Herschel, M. (2010). An Introduction to Duplicate Detection. Morgan & Claypool (Synthesis Lectures on Data Management). doi:10.2200/S00262ED1V01Y201003DTM003 Survey of duplicate-detection and record-linkage methods, including frequency-based "survivorship" rules for choosing a canonical value among near-duplicates — the general parallel for preferring the higher-count ID when two candidates tie.
- Shafranovich, Y. (2005). Common Format and MIME Type for Comma-Separated Values (CSV) Files. RFC 4180, IETF. rfc-editor.org/rfc/rfc4180
Documents the comma/quote/CRLF conventions
parseCsvLine()andserializeCsvField()implement directly. - WICG. File System Access API (Living Standard). wicg.github.io/file-system-access
Browser API behind
showOpenFilePicker()/showSaveFilePicker()and the folder-defaultingstartInoption; Chromium-only at time of writing, with a classic-download fallback elsewhere.