podify / scripts /build_music_loops.py
jayaspjacob
Use real public-domain music loops for the sound beds
d07d1a1
Raw
History Blame Contribute Delete
2.84 kB
"""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 # skip any intro
SEG_LEN = 60.0
OUT_DIR = os.path.join("tts", "music_loops")
# bed id -> FreePD track (path within the IA item)
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)
# FLAC: stable libsndfile write + lossless/compact. Loops are read server-side
# only (mixed into the speech), so browser codec support is irrelevant.
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.")