Skip to content

About

Turn a long ambient recording into a sample-accurate, seamlessly-looping `.m4a` suitable for meditation or breathwork apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

meditation-loop

Turn a long ambient recording into a sample-accurate, seamlessly-looping .m4a suitable for meditation or breathwork apps. The output's loudness and codec are matched to a reference file so it slots into an existing sound library without sticking out.

Two scripts, one human decision in the middle:

  1. scripts/scan.py analyzes the source and ranks the calmest N-minute windows by RMS stability, sudden-change absence, and transient density.
  2. You pick a start time from the ranked list.
  3. scripts/cut.py cross-correlates a sample-accurate loop seam, slices, LUFS-matches the reference, and encodes to AAC m4a.

Why m4a and not mp3

MP3 has built-in decoder padding that creates a perceptible gap at every loop boundary. AAC in an MP4 container (m4a) ships with edit-list "gapless playback" metadata that AVFoundation, modern Apple players, and Flutter's just_audio / audioplayers all honor. Combined with a sample-accurate loop seam from cross-correlation, the result loops without a click.

Requirements

  • macOS, Linux, or WSL
  • python3 ≥ 3.11 (uses the standard-library tomllib)
  • ffmpeg and ffprobe on PATH
  • Python deps: librosa, numpy, scipy, soundfile
brew install ffmpeg          # or apt install ffmpeg
pip install -r requirements.txt

Quickstart

# 1. Edit defaults.toml so [reference].path points at YOUR reference audio.
$EDITOR defaults.toml

# 2. Scan your source for the calmest 15-minute windows.
python3 scripts/scan.py /path/to/source.wav

#    Sample output:
#    --- TOP 5 CALM 15-MIN WINDOWS ---
#    rank        start          end   rms(dB)   rms_cv  maxJump(dB)   onset   score
#       1    43:05.000    58:05.000    -31.19    0.849        14.70   2.478   -3.89
#       2    13:55.000    28:55.000    -31.16    0.850        20.11   2.431   -1.79
#    ...

# 3. Cut at the start time you picked.
python3 scripts/cut.py \
    --input /path/to/source.wav \
    --output /path/to/source-loop.m4a \
    --start "43:05.000"

Configuration

defaults.toml holds every default. Each value is commented with what it means and when you'd change it. Common edits:

  • [reference].path — your library's reference audio (loudness + codec target).
  • [scan].window_min — different target loop length (10, 20, 30 minutes, etc).
  • [encode].bitrate / sample_rate / channels — uncomment to override what the script would otherwise probe from the reference.

CLI flags on either script override the corresponding config value. Use --config /path/to/other.toml to point at a different config file entirely.

How the calmness scoring works

For each candidate 15-minute window, scan.py computes three metrics on the audio envelope:

metric meaning why it matters for meditation
rms_cv std/mean of per-second RMS smooth windows have low variance
maxJump(dB) largest dB delta between adjacent 5-second sub-windows no sudden loud moments
onset mean of librosa.onset.onset_strength absence of percussive events

All three are z-scored across valid windows and summed. Windows whose mean RMS is in the bottom 10% (likely silence/fade) or top 10% (likely loud passages) are excluded. Lower combined score = calmer.

maxJump(dB) is the best perceptual tiebreaker — when two candidates score similarly, the one with the smaller max jump is the one a meditator won't be startled by.

How the loop seam works

cut.py does NOT just truncate at start + duration. Instead:

  1. Take the first 5 seconds of the cut as a "fingerprint".
  2. Search ±10 seconds around start + duration for the sample that best matches the fingerprint (via SciPy's signal.correlate).
  3. Cut to that exact sample.

The actual output duration may therefore differ from your requested duration by up to ±10 seconds. That's the price of a seam where the waveform at end ≈ waveform at start.

We do NOT crossfade the boundary, because a crossfade would make true-start differ from true-end and defeat the alignment.

Loudness matching

cut.py measures integrated loudness (LUFS-I) of both the reference and the temporary cut using ffmpeg's ebur128 filter, then applies a single volume=<gain>dB shift before encoding. No compression, no limiting, no loudnorm — those are dynamic processors and would change the meditative texture. Just a level shift.

After encoding, the output is re-measured and the delta vs the reference is printed. You should see ±0.5 LUFS, often exactly 0.

Repo layout

meditation-loop/
├── .claude/
│   └── skills/meditation-loop/SKILL.md   # makes the workflow available to Claude Code
├── scripts/
│   ├── _common.py                        # parse_time, fmt_time, db, load_config
│   ├── scan.py                           # Phase 1: find calmest windows
│   └── cut.py                            # Phase 2: cut, LUFS-match, encode
├── defaults.toml                         # annotated configuration
├── requirements.txt
├── CLAUDE.md                             # repo conventions + Python maintenance notes
├── README.md                             # this file
└── LICENSE

Using with Claude Code

This repo ships with a Claude Code skill at .claude/skills/meditation-loop/. Symlink it into your user skills directory once:

ln -s "$(pwd)/.claude/skills/meditation-loop" ~/.claude/skills/meditation-loop

Then in any Claude Code session, asking things like "find a calm 15-min section in <file>.wav and save as m4a" will trigger the skill and walk through the pipeline.

License

MIT. See LICENSE.

About

Turn a long ambient recording into a sample-accurate, seamlessly-looping `.m4a` suitable for meditation or breathwork apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages