"""A small DDSP-style generative model of the struck-sheet clips. python3 ml/ddsp.py train --data DATASET_DIR --out RUN_DIR [--minutes 55] [--latent 8] python3 ml/ddsp.py sample --model RUN_DIR/model.pt --out DIR [--n 8] [--seed 1] Encoder: a convolutional network on the cached log-frequency spectrogram (384 x 133) gives the mean and log-variance of an 8-dimensional latent z (a VAE, so that draws from the prior and interpolations are valid sounds). Decoder: z -> the controls of a parametric synthesizer: - K partials with frequencies f_k (sorted, 30 Hz to 8 kHz), initial amplitudes a_k, decay rates d_k, and a pitch-glide amount g_k, so that partial k is a_k exp(-d_k t) sin(phase of f_k (1 + g_k exp(-t / tau))); - a noise burst with a 48-band spectral shape, an amplitude, and a decay, for the attack transient and the broadband part of the ring. Synthesis is plain arithmetic at 24 kHz (the simulations carry little energy above 6 kHz), so the same model can be run in JavaScript. Loss: multi-scale log-spectral distance between the synthesized and the original clip (FFT sizes 256 to 4096), plus a small KL term. """ import argparse import json import math import os import sys import time import wave import numpy as np import torch import torch.nn as nn import torch.nn.functional as Fn from scipy.signal import resample_poly sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from features import build_cache # noqa: E402 torch.backends.cudnn.enabled = False SR = 24000 DUR = 3.0 N = int(SR * DUR) K = 128 NOISE_BANDS = 48 F_LO, F_HI = 30.0, 8000.0 def read_wav24(path): with wave.open(path) as w: x = np.frombuffer(w.readframes(w.getnframes()), dtype="= args.sample_every or (time.time() - t0) / 60 >= args.minutes: last_sample = (time.time() - t0) / 60 torch.save({"state": model.state_dict(), "latent": args.latent, "step": step}, os.path.join(args.out, "model.pt")) write_samples(model, X, ids, fixed, os.path.join(args.out, "samples", f"m{int(last_sample):03d}"), dev, step) write_index(args.out) if (time.time() - t0) / 60 >= args.minutes: break print("done", step, "steps") @torch.no_grad() def write_samples(model, X, ids, fixed, out, dev, step, n_prior=6): model.eval() os.makedirs(out, exist_ok=True) names = [] spec = X[fixed].to(dev).float().unsqueeze(1) mu, _ = model.encode(spec) y = synthesize(model.dec(mu), noise_seed=1).cpu().numpy() for i, k in enumerate(fixed): write_wav(os.path.join(out, f"recon-{ids[k]}.wav"), y[i]); names.append((f"reconstruction of {ids[k]}", f"recon-{ids[k]}.wav")) g = torch.Generator(device=dev); g.manual_seed(7) zp = torch.randn(n_prior, model.latent, device=dev, generator=g) y = synthesize(model.dec(zp), noise_seed=2).cpu().numpy() for i in range(n_prior): write_wav(os.path.join(out, f"prior-{i}.wav"), y[i]); names.append((f"prior sample {i}", f"prior-{i}.wav")) steps = 5 zi = torch.stack([mu[0] + (mu[1] - mu[0]) * s / (steps - 1) for s in range(steps)]) y = synthesize(model.dec(zi), noise_seed=3).cpu().numpy() for s in range(steps): write_wav(os.path.join(out, f"interp-{s}.wav"), y[s]); names.append((f"interpolation {ids[fixed[0]]} -> {ids[fixed[1]]}, step {s}", f"interp-{s}.wav")) with open(os.path.join(out, "list.json"), "w") as f: json.dump({"step": step, "items": names}, f) model.train() def write_index(run): sdir = os.path.join(run, "samples") snaps = sorted(d for d in os.listdir(sdir) if os.path.isdir(os.path.join(sdir, d))) blocks = [] for d in reversed(snaps): try: meta = json.load(open(os.path.join(sdir, d, "list.json"))) except Exception: continue rows = "".join(f"{label}" for label, fn in meta["items"]) blocks.append(f"

after {int(d[1:])} min, step {meta['step']}

{rows}
") html = ("DDSP sheet model samples" "

DDSP-style sheet model: samples during training

Reconstructions of four fixed dataset clips (compare with clips/ID.wav in the gallery), six draws from the prior, and a five-step interpolation between two clips. Newest first.

" + "".join(blocks)) with open(os.path.join(run, "samples.html"), "w") as f: f.write(html) @torch.no_grad() def sample(args): ck = torch.load(args.model, map_location="cpu", weights_only=False) model = Model(ck["latent"]); model.load_state_dict(ck["state"]); model.eval() os.makedirs(args.out, exist_ok=True) g = torch.Generator(); g.manual_seed(args.seed) z = torch.randn(args.n, ck["latent"], generator=g) if args.z is None else torch.tensor([[float(v) for v in args.z.split(",")]]) y = synthesize(model.dec(z), noise_seed=args.seed).numpy() for i in range(y.shape[0]): write_wav(os.path.join(args.out, f"sample-{i}.wav"), y[i]) print(f"sample-{i}.wav z = " + ", ".join(f"{v:.2f}" for v in z[i].tolist())) if __name__ == "__main__": ap = argparse.ArgumentParser() sub = ap.add_subparsers(dest="cmd", required=True) t = sub.add_parser("train"); t.add_argument("--data", required=True); t.add_argument("--out", required=True) t.add_argument("--minutes", type=float, default=55); t.add_argument("--latent", type=int, default=8); t.add_argument("--batch", type=int, default=16) t.add_argument("--lr", type=float, default=1e-3); t.add_argument("--beta", type=float, default=1e-3); t.add_argument("--sample-every", type=float, default=10) s = sub.add_parser("sample"); s.add_argument("--model", required=True); s.add_argument("--out", required=True) s.add_argument("--n", type=int, default=8); s.add_argument("--seed", type=int, default=1); s.add_argument("--z", default=None, help="comma-separated latent vector for one clip") a = ap.parse_args() (train if a.cmd == "train" else sample)(a)