This tool is a heuristic based algorithm for phonocardiogram (PCG) Analysis. It analyzes audio recordings of heart sounds to detect heartbeats and graphs the Beats Per Minute (BPM) over time.
pcg/
engine/ the algorithm: PCG audio in, beats / states / BPM out (pure computation)
recording.py Recording identity (fingerprint of the decoded audio), audio loading, channels
analysis.py Analysis files: the saved result of one run, kept in the library folder
annotation.py Annotations: hand-verified S1 / S2 / noisy spans (the ground truth)
batch.py run queue, BPM filename tags
cli.py python -m pcg analyze / inspect / export / rename
app/ the desktop app: run screen + workspace
Terms (Recording, Analysis, Annotation, Defect, Disagreement, ...) are defined in CONTEXT.md; the design is in docs/workspace-requirements.md.
- Analyses are stored in
library/(or$PCG_LIBRARY), keyed by a fingerprint of the recording's audio samples, so renaming a file (including writing BPM into its name) never loses its Analysis. Nothing is written next to your recordings except Annotations. - An Analysis shows a stale badge when
pcg/engine/config.pyor the engine code changed since it was made. It is only re-run when you ask (Ctrl+R / Run). - Annotations are
<recording>.annotation.jsonnext to the recording. For recordings inside a protected folder (defaultinputs/) saving opens Save As, starting in Downloads.
All engine parameters live in pcg/engine/config.py — the only place they are tuned; the app has
no parameter editor. Per-run settings (algorithm, auto-switch, channel, start offset, start-BPM
hint) are chosen on the run screen or the CLI. tests/test_engine_boundary.py keeps UI and file
code out of the engine.
pip install -r requirements.txtFFmpeg on your PATH is only needed for formats libsndfile can't decode (e.g. m4a / video files).
python -m pcg # the app (run screen + workspace)
python -m pcg app path/to/rec.wav # open one recording in the workspaceRun screen: drop recordings or folders, pick the algorithm / auto-switch / channel / parallel
jobs, optionally set a start BPM or skip per row, then Run. Double-click a row to open it.
"Write BPM into filenames" renames the selected recordings with their [start,min-maxbpm] tag.
Workspace: stacked lanes on one time axis — states (Analysis vs Annotation, with Disagreements and Defects), signal (envelopes, peaks; hover a peak for its labels, scores and reasoning), heart rate, and optional lanes for every debug trace the engine emits (toggle them in the Traces panel; choices persist). Keys (also under Help → Keys):
The sliders button adds playback gain points to the Signal graph. Beside it, the descending-curve button adds recovery starts to the Heart rate graph. Click the button, then the graph; drag a diamond to reposition it, or select it and press Delete/Backspace. Right-click retains the BPM readout. Click the button again or press Escape to cancel placement. Recovery starts are remembered per recording in the app settings.
Recovery lines run from each start to 1, 2, 3, 5, 10, 15, 20 and 30 minutes, where data permits. Hover an endpoint for its signed slope in bpm/s and BPM change. Each endpoint uses the time-weighted average over ±2 seconds; the start uses the curve value at the marker. Measurements follow the visible smoothed Annotation BPM when available, otherwise Result BPM. Lines stop before missing data (including gaps over 5 seconds), the next recovery start, or a rise of at least 5 BPM above the running recovery low that persists for 10 seconds. This cutoff scans the intervening curve, so a later endpoint cannot bridge renewed exertion just because BPM has fallen again.
| Key | Action |
|---|---|
| Space / T | play-pause / toggle original vs filtered audio |
| Click · Shift+drag · Shift+click | move playhead · loop region · clear it |
| 1 / 2 | place S1 / S2 at the playhead |
| X or Del / F | delete / relabel S1↔S2 |
| N + drag, drag a noisy edge | paint / resize a Noisy span |
| A | replace the loop region with the algorithm's output |
| ] / [ | next / previous Defect or Disagreement |
| Ctrl+Z / Ctrl+Shift+Z / Ctrl+S | undo / redo / save Annotation |
| Ctrl+R | re-run the engine in a fresh process (picks up code edits) |
| Ctrl+Shift+R | re-detect and re-label the selected range with Springer or Native |
| Ctrl+Shift+C | copy the visible window as text for an LLM |
To re-label just part of a recording, Shift+drag a range and click Re-label selection… (Ctrl+Shift+R). Choose Springer 2015 or Native, optionally supply a BPM hint, then run. Both algorithms process original audio with up to 15 seconds of context on each side. Only Annotation labels inside the selection are replaced; sounds crossing its edges are clipped, preserving their portions outside it. The edit is one undo step (Ctrl+Z) and is saved only with Ctrl+S. The full Analysis remains available for comparison. Failed/cancelled runs and selections with no detected sounds keep the existing labels. If you edit the selection while a run is in progress, its result is discarded so your edits are preserved.
python -m pcg analyze inputs/ -j 8 --rename # analyze, store in the library, tag filenames
python -m pcg analyze rec.wav --springer --start 5
python -m pcg reanalyze-range rec.wav --from 120 --to 126 --algorithm springer --bpm 90
python -m pcg inspect rec.wav --from 120 --to 126 # the same text as Ctrl+Shift+C
python -m pcg export rec.wav --bpm-csv bpm.csv [--source annotation] --summary summary.txt
python -m pcg rename inputs/ # write BPM tags from existing Analysesreanalyze-range also accepts --algorithm native, --channel mixed|left|right
and --context SECONDS (default 15 per side). It returns the selected S1/S2 spans
as JSON on the recording's clock and writes no Annotation or library files.
The start-BPM hint defaults to the filename tag. Springer and auto-switch runs always go one at a
time: the Springer HSMM needs several GB of memory per minute of audio. For breakpoint debugging,
run python -m pcg analyze <file> under a debugger.
Unit tests — fast, deterministic, no audio fixtures:
pip install -r requirements-dev.txt
python -m pytest tests/ -qSee tests/README.md for the per-module coverage table.
Benchmark runner — runs the engine on every annotated recording and compares predicted S1
states against the Annotation, reporting phase_flip / miss / extra per file:
python benchmarking/run_benchmark.py [input_dir] -j 4 # default: inputs/Invariant gate — python benchmarking/state_invariants.py (no ground truth needed; see
debug_helpers/README.md).
Import the generated heart rate graph into Blender to easily calculate the change in bpm over time. Blender file and scripts are located in Blender BPM tool folder
Select the Geometry Nodes object and enter edit mode. This will allow you to calculate:
- Heart Rate Recovery (HRR)
- maximal rate of heart rate increase
You can also make any BPM/Time graph and export it out of blender using the Export graph data.py script
Import any CSV file with format: Time(Seconds), Beats Per Minute — e.g. File → Export → BPM CSV in the
workspace, or python -m pcg export rec.wav --bpm-csv bpm.csv.