Commit 0633e2e6 by PLN (Algolia)

mix: Take110 shape chart and master-cut scripts, saved from /tmp

parent fd797b08
......@@ -150,5 +150,8 @@ Take110 stems under `~/Work/Ardour/Tidal Live/interchange/Tidal Live/audiofiles/
| `d8.py` | section 3 timeline and 1/3-octave levels | `/usr/bin/python3` |
| `ab.py` | section 4 excerpts (needs scipy) | `/usr/bin/python3` |
| `sheet.py` | the PNG (needs matplotlib) | pyenv `python3` (the system python's matplotlib breaks against numpy 2.5) |
| `shape.py` | `Take110/Take110_shape.png`: summed-stem envelope, per-orbit RMS, CC/s and eval marks on one clock | pyenv `python3` |
| `bass.py`, `tail.py` | bass presence and dynamics, and the ending analysis behind the v0 cut | `/usr/bin/python3` |
| `cuts.py` | the two cut masters (`_cut_fade`, `_cut_verb`) from `Take110_master_v0.flac` | `/usr/bin/python3` |
Next step: a reusable per-voice card, starting with kicks: `TODO.d/2026-11-01-voice-cards.md`.
import os, subprocess, numpy as np
AUD=os.path.expanduser("~/Work/Sound/Ardour/Tidal Live/interchange/Tidal Live/audiofiles")
SR=48000
def dec(path,ss=None,t=None):
cmd=["ffmpeg","-nostdin","-v","error"]
if ss is not None: cmd+=["-ss",str(ss)]
if t is not None: cmd+=["-t",str(t)]
cmd+=["-i",path,"-f","f32le","-ac","1","-ar",str(SR),"-"]
return np.frombuffer(subprocess.run(cmd,capture_output=True,check=True).stdout,dtype=np.float32)
db=lambda x:20*np.log10(max(x,1e-9))
# 1) first d3 onset (take clock): first 0.1 s whose rms > -55 dBFS
d3=dec(f"{AUD}/Take110_Tidal 03-1%L.wav",0,120); h=SR//10
r=np.sqrt((d3[:len(d3)//h*h].reshape(-1,h)**2).mean(1)); on=np.argmax(20*np.log10(r+1e-9)>-55)*0.1
p11=dec(f"{AUD}/Take110_Tidal 11-1%L.wav",0,60); r11=np.sqrt((p11[:len(p11)//h*h].reshape(-1,h)**2).mean(1))
print(f"first d3 onset (take clock): {on:.1f}s ; pad (d11) level there {20*np.log10(r11[int(on*10)]+1e-9):.1f} dBFS")
others={}
for n in (1,2,4,5,7,8,10):
x=dec(f"{AUD}/Take110_Tidal {n:02d}-1%L.wav",0,60); rr=np.sqrt((x[:len(x)//h*h].reshape(-1,h)**2).mean(1))
others[n]=np.argmax(20*np.log10(rr+1e-9)>-55)*0.1
print("first onsets before 60s:", {k:round(v,1) for k,v in others.items() if v>0})
# 2) band energies in the full-groove window 130-200 s
bands=[("sub 20-60",20,60),("bass 60-150",60,150),("lowmid 150-400",150,400),("mid 400-2k",400,2000),("hi 2k-8k",2000,8000),("air 8k+",8000,24000)]
def bandrms(x):
n=1<<int(np.floor(np.log2(len(x)))); X=np.fft.rfft(x[:n]*np.hanning(n)); f=np.fft.rfftfreq(n,1/SR)
P=(np.abs(X)**2); P/=np.sum(np.hanning(n)**2)*n/2 # per-bin power scale
return [db(np.sqrt(P[(f>=a)&(f<b)].sum()/2)) for _,a,b in bands], f[(f>=30)&(f<400)][np.argmax(P[(f>=30)&(f<400)])]
print("\nstem band levels in dBFS (130-200 s), peak freq 30-400 Hz")
print("orbit "+" ".join(f"{b[0]:>14}" for b in bands)+" peak_f")
tot=np.zeros(70*SR,dtype=np.float32)
for n in range(1,13):
x=dec(f"{AUD}/Take110_Tidal {n:02d}-1%L.wav",130,70)
if len(x)<70*SR: x=np.pad(x,(0,70*SR-len(x)))
tot+=x[:70*SR]
if np.abs(x).max()<1e-6: continue
lv,pf=bandrms(x); print(f"d{n:<4} "+" ".join(f"{v:14.1f}" for v in lv)+f" {pf:6.0f} Hz")
lv,pf=bandrms(tot); print("SUM "+" ".join(f"{v:14.1f}" for v in lv)+f" {pf:6.0f} Hz")
import os, subprocess, sys, numpy as np
SR=48000
M=os.path.expanduser("~/Work/Sound/Tidal/output/Take110/Take110_master_v0.flac")
OUT=os.path.expanduser("~/Work/Sound/Tidal/output/Take110")
raw=subprocess.run(["ffmpeg","-nostdin","-v","error","-i",M,"-f","f32le","-ac","2","-ar",str(SR),"-"],capture_output=True,check=True).stdout
x=np.frombuffer(raw,dtype=np.float32).reshape(-1,2).copy()
db=lambda a:20*np.log10(max(a,1e-9))
# where does the pad really stop? (master clock)
h=SR//20
r=np.sqrt((x[:,0][:len(x)//h*h].reshape(-1,h)**2).mean(1))
print("rms around the stop (master clock):",[(round(i*0.05,2),round(db(r[i]),1)) for i in range(int(310.6/0.05),int(312.6/0.05),4)])
stop=None
for i in range(int(305/0.05),len(r)):
if db(r[i])<-60: stop=i*0.05; break
print("pad gone (<-60 dBFS rms) at master", stop)
START=38.0; END=stop-0.15
seg=x[int(START*SR):int(END*SR)].copy(); n=len(seg)
def fade(a,t,kind):
k=int(t*SR);
if kind=="in": a[:k]*= (np.sin(np.linspace(0,np.pi/2,k))**2)[:,None]
else: a[-k:]*= (np.cos(np.linspace(0,np.pi/2,k))**2)[:,None]
def write(name,a,target=-13.9):
tmp="/tmp/claude-take110/"+name+".f32"; a.astype(np.float32).tofile(tmp)
src=["-f","f32le","-ar",str(SR),"-ac","2","-i",tmp]
m=subprocess.run(["ffmpeg","-nostdin","-hide_banner"]+src+["-af","ebur128=peak=true","-f","null","-"],capture_output=True,text=True).stderr
import re
I=float(re.findall(r"I:\s+(-?[\d.]+) LUFS",m)[-1]); g=target-I
out=f"{OUT}/{name}.flac"
assert not os.path.exists(out), out
subprocess.run(["ffmpeg","-nostdin","-v","error"]+src+["-af",f"volume={g}dB,alimiter=limit=0.89:level=disabled","-c:a","flac","-sample_fmt","s32","-bits_per_raw_sample","24",out],check=True)
m=subprocess.run(["ffmpeg","-nostdin","-hide_banner","-i",out,"-af","ebur128=peak=true","-f","null","-"],capture_output=True,text=True).stderr
I2=re.findall(r"I:\s+(-?[\d.]+) LUFS",m)[-1]; P=re.findall(r"Peak:\s+(-?[\d.]+) dBFS",m)[-1]
print(f"{name}: {len(a)/SR:.1f}s gain {g:+.1f} dB -> I={I2} LUFS TP~{P} dBFS {os.path.getsize(out)/1e6:.0f} MB")
os.remove(tmp)
# A: plain fade
A=seg.copy(); fade(A,1.0,"in"); fade(A,8.0,"out")
write("Take110_master_v0_cut_fade",A)
# B: pad blooms into a reverb that rings out after the pad stops
rng=np.random.default_rng(110)
L=int(8.0*SR); t=np.arange(L)/SR
def ir():
w=rng.standard_normal(L);
a=np.exp(-2.0*np.pi*5000/SR); y=np.empty(L); s=0.0 # one-pole lowpass ~5 kHz
for i in range(L): s=(1-a)*w[i]+a*s; y[i]=s
y*=np.exp(-6.9*t/6.0)*np.minimum(1,t/0.03) # RT60 ~6 s, soft onset
y=np.concatenate([np.zeros(int(0.025*SR)),y]); return y/np.sqrt((y**2).sum())
IR=[ir(),ir()]
tail=int(8.0*SR); bloom=int(28*SR)
xin=seg[-bloom:].copy()
g=(np.sin(np.linspace(0,np.pi/2,bloom))**2)[:,None]; xin*=g # wet send grows over the last 28 s
N=1<<int(np.ceil(np.log2(bloom+len(IR[0])+tail)))
wet=np.zeros((bloom+len(IR[0]),2))
for c in range(2):
wet[:,c]=np.fft.irfft(np.fft.rfft(xin[:,c],N)*np.fft.rfft(IR[c],N),N)[:bloom+len(IR[0])]
B=np.zeros((n+tail,2),dtype=np.float64); B[:n]=seg
fade(B[:n],0.06,"out")
B[n-bloom:n-bloom+len(wet)]+=0.8*wet[: min(len(wet),n+tail-(n-bloom))]
B=B[:n+tail]; fade(B,3.0,"out")
write("Take110_master_v0_cut_verb",B)
"""Take110 shape: stem-sum envelope, per-orbit activity, hands + code events."""
import json, os, subprocess, sys, time
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
AUD = os.path.expanduser("~/Work/Sound/Ardour/Tidal Live/interchange/Tidal Live/audiofiles")
LOG = os.path.expanduser("~/.local/share/parvagues/gig-log/gig-20261101-132654.jsonl")
SR = 48000
T0 = time.mktime(time.strptime("2026-11-01 14:54:48", "%Y-%m-%d %H:%M:%S"))
GO = time.mktime(time.strptime("2026-11-01 14:55:19", "%Y-%m-%d %H:%M:%S"))
HOP = 0.5 # seconds per envelope bucket
def decode(path):
p = subprocess.run(["ffmpeg", "-nostdin", "-v", "error", "-i", path, "-f", "f32le",
"-ac", "1", "-ar", str(SR), "-"], capture_output=True, check=True)
return np.frombuffer(p.stdout, dtype=np.float32)
stems = {}
for n in range(1, 13):
stems[n] = decode(f"{AUD}/Take110_Tidal {n:02d}-1%L.wav")
N = min(len(v) for v in stems.values())
dur = N / SR
mix = np.zeros(N, dtype=np.float32)
for v in stems.values():
mix += v[:N]
hop = int(HOP * SR)
nb = N // hop
t = (np.arange(nb) + 0.5) * HOP
def buckets(x):
x = x[: nb * hop].reshape(nb, hop)
return x.min(axis=1), x.max(axis=1), np.sqrt((x ** 2).mean(axis=1))
mn, mx, rms = buckets(mix)
db = lambda a: 20 * np.log10(np.maximum(a, 1e-5))
act = np.vstack([db(buckets(stems[n])[2]) for n in range(1, 13)])
# gig-log: hands and code, same clock
cc_t, evals, marks = [], [], []
for l in open(LOG):
try:
r = json.loads(l)
except ValueError:
continue
if not (T0 <= r["t"] <= T0 + dur):
continue
if r["k"] == "cc":
cc_t.append(r["t"] - T0)
elif r["k"] == "eval":
evals.append((r["t"] - T0, os.path.basename(r["path"]), r["d"]))
elif r["k"] == "mark":
marks.append((r["t"] - T0, r["label"]))
# start/end by the mix envelope (-50 dBFS RMS above the floor)
loud = db(rms) > -50
first = t[np.argmax(loud)]
last = t[len(loud) - 1 - np.argmax(loud[::-1])]
peak_t = t[np.argmax(rms)]
print(f"take {dur:.1f}s sound from {first:.1f}s to {last:.1f}s (GO mark at {GO - T0:.1f}s)")
print(f"loudest bucket at {peak_t:.1f}s rms {db(rms.max()):.1f} dBFS mix peak {db(np.abs(mix).max()):.1f} dBFS (sum of 12 L stems)")
# last 60 s RMS profile in 10 s steps for the outro
for a in range(int(max(0, last - 60)), int(dur), 10):
seg = rms[int(a / HOP): int((a + 10) / HOP)]
print(f" {a:4d}-{a+10:4d}s mean rms {db(seg.mean()):6.1f} dBFS")
print("evals:", [(round(a, 1), b, d) for a, b, d in evals])
print("cc/s: total", len(cc_t), " marks:", [(round(a, 1), b) for a, b in marks])
sil = [n for n in range(1, 13) if db(buckets(stems[n])[2]).max() < -80]
print("silent orbits:", sil)
INK, MUT, ACC, GRID = "#1f2328", "#6b7280", "#2563eb", "#e5e7eb"
fig, ax = plt.subplots(3, 1, figsize=(14, 9), sharex=True,
gridspec_kw={"height_ratios": [3, 3, 1.4]}, facecolor="white")
for a in ax:
a.set_facecolor("white")
for s in ("top", "right"):
a.spines[s].set_visible(False)
a.spines["left"].set_color(MUT); a.spines["bottom"].set_color(MUT)
a.tick_params(colors=MUT, labelsize=9)
a.grid(axis="x", color=GRID, lw=0.6)
a0 = ax[0]
a0.fill_between(t, mn, mx, color=ACC, lw=0, alpha=0.35, label="min / max per 0.5 s")
a0.plot(t, rms, color=ACC, lw=1.2, label="RMS")
a0.plot(t, -rms, color=ACC, lw=1.2)
a0.axhline(0, color=MUT, lw=0.5)
a0.set_ylabel("amplitude (linear, sum of 12 L stems)", color=MUT)
a0.legend(frameon=False, loc="upper right", fontsize=9)
a0.set_title("Take110 shape: sound, orbits, hands and code on one clock", loc="left",
color=INK, fontsize=13, fontweight="bold")
a1 = ax[1]
im = a1.imshow(act, aspect="auto", origin="lower", extent=[0, dur, 0.5, 12.5],
cmap="Blues", vmin=-70, vmax=0, interpolation="nearest")
a1.set_yticks(range(1, 13)); a1.set_ylabel("orbit (dN)", color=MUT)
cb = fig.colorbar(im, ax=ax[:2], pad=0.01, fraction=0.025)
cb.set_label("RMS dBFS per 0.5 s (dark = loud)", color=MUT, fontsize=9)
cb.ax.tick_params(colors=MUT, labelsize=8)
a2 = ax[2]
h, edges = np.histogram(cc_t, bins=np.arange(0, dur + 1, 1.0))
a2.bar(edges[:-1] + 0.5, h, width=0.8, color=INK, lw=0)
a2.set_ylabel("CC / s (hands)", color=MUT)
a2.set_xlabel("seconds from take start (14:54:48)", color=MUT)
for a in ax:
a.axvline(GO - T0, color="#b45309", lw=1.2, ls="--")
a.axvline(dur, color=MUT, lw=1.0, ls=":")
for et, name, d in evals:
a.axvline(et, color="#047857", lw=1.2)
a0.text(GO - T0 + 1, a0.get_ylim()[1] * 0.92, "GO mark 14:55:19", color="#b45309", fontsize=9, va="top")
for et, name, d in evals:
a0.text(et + 1, a0.get_ylim()[1] * 0.78, f"eval {name}\n{len(d)} streams", color="#047857", fontsize=9, va="top")
a0.text(dur - 1, a0.get_ylim()[1] * 0.92, "halt", color=MUT, fontsize=9, ha="right", va="top")
a0.axvspan(first, last, ymin=0.0, ymax=0.04, color=ACC, alpha=0.9)
a2.set_xlim(0, dur)
fig.canvas.draw()
p0, p2 = a0.get_position(), a2.get_position()
a2.set_position([p0.x0, p2.y0, p0.width, p2.height]) # same x as the panels above
fig.savefig("/tmp/claude-take110/take110_shape.png", dpi=110, bbox_inches="tight")
print("wrote /tmp/claude-take110/take110_shape.png")
import os, subprocess, numpy as np
AUD=os.path.expanduser("~/Work/Sound/Ardour/Tidal Live/interchange/Tidal Live/audiofiles")
SR=48000
def dec(path):
p=subprocess.run(["ffmpeg","-nostdin","-v","error","-i",path,"-f","f32le","-ac","1","-ar",str(SR),"-"],capture_output=True,check=True)
return np.frombuffer(p.stdout,dtype=np.float32)
st={n:dec(f"{AUD}/Take110_Tidal {n:02d}-1%L.wav") for n in range(1,13)}
N=min(len(v) for v in st.values()); mix=sum(v[:N] for v in st.values())
hop=SR//20
def env(x):
x=x[:len(x)//hop*hop].reshape(-1,hop); return np.sqrt((x**2).mean(1))
e_mix=env(mix)
m=dec(os.path.expanduser("~/Work/Sound/Tidal/output/Take110/Take110_master_v0.flac"))
e_m=env(m)
# lag: compare log envelopes over the first 120 s
a=np.log10(e_mix[:2400]+1e-6); b=np.log10(e_m[:2400]+1e-6)
best=max(range(-80,81),key=lambda L:(np.corrcoef(a[max(0,L):2400+min(0,L)], b[max(0,-L):2400-max(0,L)])[0,1]))
print("lag (master starts later than take by) ~", best*0.05, "s [positive = master dropped that much from the start]")
db=lambda x:20*np.log10(max(x,1e-6))
print("t(s) o3 o4 o10 o11 (RMS dBFS per 0.25 s, take clock)")
step=SR//4
for s in np.arange(285,333,1.0):
row=[]
for n in (3,4,10,11):
seg=st[n][int(s*SR):int(s*SR)+step]; row.append(db(np.sqrt((seg**2).mean())))
print(f"{s:5.0f} "+" ".join(f"{v:6.1f}" for v in row))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment