| """Build background-music loops from FreePD (public-domain / CC0). |
| |
| FreePD (Kevin MacLeod) releases its catalogue as 100% public-domain music, mirrored on the |
| Internet Archive (item `freepd`). We pull four mood tracks, trim each to a ~60s segment, |
| normalise, fade, and save a compact mono OGG loop used by tts/music.py. No attribution is |
| required (public domain), but FreePD is credited in the README. |
| |
| Run from the repo root: python scripts/build_music_loops.py |
| OGG files are tracked via Git LFS (see .gitattributes). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import io |
| import os |
| import urllib.parse |
| import urllib.request |
|
|
| import numpy as np |
| import soundfile as sf |
|
|
| IA_BASE = "https://archive.org/download/freepd" |
| TARGET_SR = 44100 |
| SEG_START = 8.0 |
| SEG_LEN = 60.0 |
| OUT_DIR = os.path.join("tts", "music_loops") |
|
|
| |
| TRACKS = { |
| "ambient_drift": "Page2/Ambient C Motion.mp3", |
| "lofi_pulse": "Page2/Chill Beat.mp3", |
| "cinematic_rise": "Page2/Wonder Flow.mp3", |
| "newsroom_bed": "Page2/Action Investigation.mp3", |
| } |
|
|
|
|
| def _fetch(track: str) -> bytes: |
| url = IA_BASE + "/" + urllib.parse.quote(track) |
| req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) |
| with urllib.request.urlopen(req, timeout=120) as r: |
| return r.read() |
|
|
|
|
| def _resample(x: np.ndarray, sr: int, target: int) -> np.ndarray: |
| if sr == target: |
| return x |
| n = int(round(len(x) * target / sr)) |
| xp = np.linspace(0, 1, len(x), dtype=np.float64) |
| fp = np.linspace(0, 1, n, dtype=np.float64) |
| return np.interp(fp, xp, x).astype(np.float32) |
|
|
|
|
| def build(): |
| os.makedirs(OUT_DIR, exist_ok=True) |
| for bed_id, track in TRACKS.items(): |
| audio, sr = sf.read(io.BytesIO(_fetch(track)), dtype="float32") |
| if audio.ndim > 1: |
| audio = audio.mean(axis=1) |
| audio = _resample(audio, sr, TARGET_SR) |
|
|
| start = int(SEG_START * TARGET_SR) |
| if start + int(5 * TARGET_SR) >= len(audio): |
| start = 0 |
| seg = audio[start:start + int(SEG_LEN * TARGET_SR)] |
|
|
| peak = float(np.abs(seg).max()) or 1.0 |
| seg = (seg / peak * 0.9).astype(np.float32) |
|
|
| f = int(1.0 * TARGET_SR) |
| if len(seg) > 2 * f: |
| seg[:f] *= np.linspace(0, 1, f, dtype=np.float32) |
| seg[-f:] *= np.linspace(1, 0, f, dtype=np.float32) |
|
|
| |
| |
| out = os.path.join(OUT_DIR, f"{bed_id}.flac") |
| sf.write(out, seg, TARGET_SR, format="FLAC", subtype="PCM_16") |
| print(f" {bed_id:16s} <- {track} {len(seg)/TARGET_SR:.1f}s {os.path.getsize(out)//1024}KB") |
|
|
|
|
| if __name__ == "__main__": |
| build() |
| print("done.") |
|
|