Cyberpunk Fever Dream
"""
cyberpunk_fever_dream.py
========================
Generates a 30-second cyberpunk AV piece:
- Audio: glitch hop bass + FM lead + city hum + probabilistic glitch events
- Video: perspective grid + silhouette + scan tears + chromatic aberration
- Sync: audio envelopes drive visual parameters per-frame
- Output: cyberpunk_fever_dream.mp4 (H.264 + AAC, 30fps, 1280x720)
"""
import numpy as np
from scipy.io import wavfile
import subprocess, os, sys
try:
from PIL import Image, ImageDraw
HAS_PIL = True
except ImportError:
HAS_PIL = False
try:
import cv2
HAS_CV2 = True
except ImportError:
HAS_CV2 = False
# ══════════════════════════════════════════════════════════════════════════════
# CONSTANTS
# ══════════════════════════════════════════════════════════════════════════════
SR = 44100
DURATION = 30
N_AUDIO = SR * DURATION
FPS = 30
N_FRAMES = FPS * DURATION
W, H = 1280, 720
BPM = 120.0
BEAT = int(SR * 60.0 / BPM)
INT16_MAX = 32767
COL_BG = (8, 5, 5 )
COL_CYAN = (255, 245, 0 ) # BGR
np.random.seed(77)
# ══════════════════════════════════════════════════════════════════════════════
# 1. AUDIO
# ══════════════════════════════════════════════════════════════════════════════
def sawtooth(freq, dur, amp=1.0):
t = np.linspace(0, dur, int(SR*dur), endpoint=False)
return amp * (2.0*((t*freq)%1.0) - 1.0)
def square(freq, dur, amp=1.0, duty=0.5):
t = np.linspace(0, dur, int(SR*dur), endpoint=False)
return amp * np.where((t*freq)%1.0 < duty, 1.0, -1.0).astype(np.float64)
def sine(freq, dur, amp=1.0):
t = np.linspace(0, dur, int(SR*dur), endpoint=False)
return amp * np.sin(2*np.pi*freq*t)
def adsr(sig, a=0.01, d=0.05, s=0.7, r=0.08):
n = len(sig)
ai = max(1, int(a*n)); di = max(1, int(d*n)); ri = max(1, int(r*n))
si = max(0, n-ai-di-ri)
env = np.concatenate([np.linspace(0,1,ai), np.linspace(1,s,di),
np.full(si,s), np.linspace(s,0,ri)])
return sig * env[:n]
def place(buf, sig, start):
end = min(start+len(sig), len(buf))
buf[start:end] += sig[:end-start]
def pink_noise(n, amp=1.0):
w = np.fft.rfft(np.random.randn(n))
f = np.fft.rfftfreq(n); f[0] = 1e-9
p = np.fft.irfft(w/np.sqrt(f), n=n)
return amp * p[:n] / (np.max(np.abs(p))+1e-9)
def fx_bitcrush(sig, bits=8):
lvl = 2**max(1,bits-1)
return np.round(sig*lvl)/lvl
def fx_stutter(sig, min_ms=20, max_ms=80, repeats=3):
out = sig.copy(); n = len(sig)
chunk = np.random.randint(int(SR*min_ms/1000), int(SR*max_ms/1000)+1)
start = np.random.randint(0, max(1, n-chunk*repeats))
grain = sig[start:start+chunk].copy()
for r in range(repeats):
pos = start+r*chunk; end2 = min(pos+chunk, n)
if end2 > pos: out[pos:end2] = grain[:end2-pos]
return out
def synthesize_audio():
L = np.zeros(N_AUDIO)
R = np.zeros(N_AUDIO)
sixteenth = BEAT // 4
swing = int(sixteenth * 0.12)
# Bass riff
bass_notes = [65.41, 98.00, 116.54, 77.78]
for i in range(DURATION*4):
pos = i*sixteenth + (swing if i%2==1 else 0)
if pos >= N_AUDIO: break
note = bass_notes[i % len(bass_notes)]
dur = sixteenth/SR + 0.01
seg = sawtooth(note, dur, 0.65) + 0.3*square(note, dur, 0.35)
seg = adsr(seg, a=0.01, d=0.04, s=0.65, r=0.05)
seg = np.tanh(seg*3.5)/np.tanh(3.5)
if np.random.random() < 0.22: seg = fx_stutter(seg)
place(L, seg*0.72, pos); place(R, seg*0.72, pos)
# Kick
for i in range(DURATION*4):
if i%8 not in (0,4): continue
pos = i*sixteenth
if pos >= N_AUDIO: break
n2 = int(SR*0.5); t = np.linspace(0,0.5,n2)
freq2 = 110*np.exp(-t*20)+35
k = np.sin(2*np.pi*np.cumsum(freq2)/SR)*np.exp(-t*10)
k = adsr(k*0.92, a=0.002, d=0.2, s=0.0, r=0.08)
k = np.tanh(k*2)/np.tanh(2)
place(L, k, pos); place(R, k, pos)
# Snare
for i in range(DURATION*4):
if i%8 not in (2,6): continue
pos = i*sixteenth + swing
if pos >= N_AUDIO: break
n2 = int(SR*0.18); t = np.linspace(0,0.18,n2)
sn = (0.5*np.sin(2*np.pi*200*t)*np.exp(-t*35) +
0.65*pink_noise(n2)*np.exp(-t*20))
sn = adsr(sn*0.85, a=0.003, d=0.08, s=0.0, r=0.05)
place(L, sn, pos); place(R, sn, pos)
# FM lead (enters at 8s)
lead_start = int(SR*8)
for i in range(DURATION*2):
pos = lead_start + i*BEAT//2
if pos >= N_AUDIO: break
dur = BEAT/2/SR + 0.02
t = np.linspace(0, dur, int(SR*dur))
lfo_phase = i/max(1,DURATION*2)
mod_idx = 1.0 + 7.0*lfo_phase
mod = mod_idx*np.sin(2*np.pi*440*2.73*t)
seg = np.sin(2*np.pi*440*t + mod) * 0.50
seg = adsr(seg, a=0.02, d=0.06, s=0.55, r=0.08)
pan_side = -0.75 if (i//2)%2==0 else 0.75
gl = seg*np.sqrt((1-pan_side)/2)
gr = seg*np.sqrt((1+pan_side)/2)
if (i//4)%2==1:
gl = fx_bitcrush(gl, bits=6); gr = fx_bitcrush(gr, bits=6)
if np.random.random() < 0.12:
semitones = np.random.uniform(-7,7)
ratio = 2**(semitones/12)
idx = np.clip((np.arange(len(gl))*ratio).astype(int),0,len(gl)-1)
gl = gl[idx][:len(gl)]; gr = gr[idx][:len(gr)]
place(L, gl, pos); place(R, gr, pos)
# City hum
hum_raw = pink_noise(N_AUDIO, amp=0.12)
lo = np.convolve(hum_raw, np.ones(220)/220, mode='same')
hi = np.convolve(hum_raw, np.ones(55)/55, mode='same')
hum = hi - lo
t_full = np.linspace(0, DURATION, N_AUDIO)
hum *= (0.85 + 0.15*np.sin(2*np.pi*0.25*t_full))
L += hum*0.8; R += hum*0.75
# 4-bit crush at 27s
cs, ce = int(SR*27), int(SR*27.3)
L[cs:ce] = fx_bitcrush(L[cs:ce], bits=4)
R[cs:ce] = fx_bitcrush(R[cs:ce], bits=4)
# Ear-spikes
for i in range(DURATION*4):
pos = i*sixteenth
if pos >= N_AUDIO: break
if np.random.random() < 0.04:
sp = pos + np.random.randint(0, sixteenth)
if sp < N_AUDIO:
L[sp] += np.random.choice([-0.6,0.6])
R[sp] += np.random.choice([-0.6,0.6])
L = np.tanh(L*1.3)/np.tanh(1.3)
R = np.tanh(R*1.3)/np.tanh(1.3)
peak = max(np.max(np.abs(L)), np.max(np.abs(R)))
if peak > 1e-9: L,R = L/peak*0.92, R/peak*0.92
return L, R
def save_wav_stereo(path, L, R):
stereo = np.column_stack([np.clip(L,-1,1), np.clip(R,-1,1)])
pcm = (stereo*INT16_MAX).astype(np.int16)
wavfile.write(path, SR, pcm)
# ══════════════════════════════════════════════════════════════════════════════
# 2. ENVELOPE EXTRACTION
# ══════════════════════════════════════════════════════════════════════════════
def extract_envelopes(L, R):
mono = (L+R)*0.5
fsize = SR//FPS
rms = np.zeros(N_FRAMES)
kick_env = np.zeros(N_FRAMES)
treble_energy = np.zeros(N_FRAMES)
for f in range(N_FRAMES):
s = f*fsize; e = min(s+fsize, N_AUDIO)
chunk = mono[s:e]
if len(chunk)==0: continue
rms[f] = np.sqrt(np.mean(chunk**2))
fft = np.abs(np.fft.rfft(chunk))
freqs= np.fft.rfftfreq(len(chunk), 1/SR)
kick_env[f] = np.sum(fft[freqs<120])/(len(chunk)+1)
treble_energy[f] = np.sum(fft[(freqs>=4000)&(freqs<12000)])/(len(chunk)+1)
def norm(x): return x/(np.max(x)+1e-9)
return {'rms': norm(rms), 'kick': norm(kick_env), 'treble': norm(treble_energy)}
# ══════════════════════════════════════════════════════════════════════════════
# 3. FRAME RENDERING
# ══════════════════════════════════════════════════════════════════════════════
def hue_to_bgr(hue_norm):
"""Cycle magenta → cyan → amber and back."""
h = hue_norm % 1.0
if h < 0.33:
t = h/0.33
return (int(255*(1-t)), int(0*(1-t)+245*t), int(110*(1-t)+255*t))
elif h < 0.66:
t = (h-0.33)/0.33
return (int(t*255), int(245*(1-t)+107*t), int(255*(1-t)))
else:
t = (h-0.66)/0.34
return (255, int(107*(1-t)), int(t*110))
def draw_grid(img, scroll_y, hue_offset):
horizon = H//2
vp_x = W//2
color = hue_to_bgr(hue_offset/360.0)
# Horizontal lines
n_horiz = 18
for i in range(1, n_horiz+1):
persp = (i/n_horiz)**2.4
y_raw = horizon + (H-horizon)*persp
y = int((y_raw - horizon + scroll_y) % (H-horizon) + horizon)
if horizon < y < H:
alpha = 0.25 + 0.75*persp
c = tuple(int(v*alpha) for v in color)
cv2.line(img, (0,y), (W,y), c, 1)
# Vertical converging lines
n_vert = 20
for i in range(n_vert+1):
x_base = int(i * W/n_vert)
cv2.line(img, (vp_x, horizon), (x_base, H), color, 1)
# Horizon glow line
glow_color = tuple(min(255, int(v*1.5)) for v in color)
cv2.line(img, (0, horizon), (W, horizon), glow_color, 2)
return img
def draw_silhouette(img, glitch_offsets):
cx, cy = W//2, int(H*0.40)
sc = 155
segments = [
(0.0,-1.0, 0.0,-0.70), # 0 neck
(0.0,-0.70, 0.0, 0.10), # 1 torso
(0.0,-0.60,-0.45,-0.30), # 2 L upper arm
(-0.45,-0.30,-0.55,0.20), # 3 L lower arm
(0.0,-0.60, 0.45,-0.30), # 4 R upper arm
(0.45,-0.30, 0.55,0.20), # 5 R lower arm
(0.0, 0.10,-0.22, 0.65), # 6 L thigh
(-0.22,0.65,-0.22,1.10), # 7 L shin
(0.0, 0.10, 0.22, 0.65), # 8 R thigh
(0.22,0.65, 0.22,1.10), # 9 R shin
]
offset_map = {idx:(dx,dy) for idx,dx,dy in glitch_offsets}
col = (255, 245, 0) # cyan BGR
for si, (x1n,y1n,x2n,y2n) in enumerate(segments):
dx, dy = offset_map.get(si,(0,0))
p1 = (int(cx+x1n*sc+dx), int(cy+y1n*sc+dy))
p2 = (int(cx+x2n*sc+dx), int(cy+y2n*sc+dy))
p1 = (np.clip(p1[0],0,W-1), np.clip(p1[1],0,H-1))
p2 = (np.clip(p2[0],0,W-1), np.clip(p2[1],0,H-1))
cv2.line(img, p1, p2, col, 1)
# Head
hc = (cx, int(cy-0.85*sc))
if 0<=hc[0]<W and 0<=hc[1]<H:
cv2.circle(img, hc, int(0.18*sc), col, 1)
return img
def chromatic_aberration(img, rms_val):
shift = int(rms_val*8)
if shift==0: return img
b,g,r = cv2.split(img)
rows,cols = img.shape[:2]
Mr = np.float32([[1,0, shift],[0,1,0]])
Mb = np.float32([[1,0,-shift],[0,1,0]])
r = cv2.warpAffine(r, Mr, (cols,rows))
b = cv2.warpAffine(b, Mb, (cols,rows))
return cv2.merge([b,g,r])
def scan_tears(img, treble_val):
n_tears = int(treble_val*9)
out = img.copy()
for _ in range(n_tears):
y = np.random.randint(0,H)
h2 = np.random.randint(1,5)
sh = int(np.random.uniform(-1,1)*treble_val*65)
y2 = min(y+h2,H)
band = img[y:y2,:].copy()
out[y:y2,:] = np.roll(band, sh, axis=1)
return out
def vhs_dropout(img):
n = np.random.poisson(3)
out = img.copy()
for _ in range(n):
x = np.random.randint(0, W-80)
y = np.random.randint(0, H-5)
w2 = np.random.randint(5,80)
h2 = np.random.randint(1,4)
aw = min(w2, W-x)
ah = min(h2, H-y)
if aw<=0 or ah<=0: continue
noise = np.random.randint(0,256,(ah,aw,3),dtype=np.uint8)
out[y:y+ah, x:x+aw] = noise
return out
def vignette(img, kick_val):
inner = 0.22 + 0.12*kick_val
cx2,cy2 = W//2, H//2
Y,X = np.mgrid[0:H,0:W]
dist = np.sqrt(((X-cx2)/(W/2))**2 + ((Y-cy2)/(H/2))**2)
mask = np.clip((dist-inner)/(0.88-inner),0,1)
dark = img.astype(np.float32)*(1.0 - mask[:,:,np.newaxis]*0.88)
return dark.clip(0,255).astype(np.uint8)
def render_frame(f, envs, scroll_y, hue_offset, glitch_offsets,
datamosh_buf, is_datamosh):
rms = envs['rms'][f]
kick = envs['kick'][f]
treble = envs['treble'][f]
t_s = f/FPS
# Hard white flash on strong kick
if kick > 0.88 and np.random.random() < 0.45:
return np.full((H,W,3), 255, dtype=np.uint8)
img = np.full((H,W,3), COL_BG, dtype=np.uint8)
img = draw_grid(img, scroll_y, hue_offset)
img = draw_silhouette(img, glitch_offsets)
# Datamosh smear
if is_datamosh and datamosh_buf is not None:
k = int(rms*50+1)|1
smear = cv2.GaussianBlur(datamosh_buf,(k,k),0)
img = cv2.addWeighted(img,0.25,smear,0.75,0)
# 4-bit crush desaturate
if 27.0 <= t_s <= 27.3:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
# Slam oversaturated on the snap-back frame
if 27.28 <= t_s <= 27.32:
img = cv2.convertScaleAbs(img, alpha=2.5, beta=30)
img = chromatic_aberration(img, rms)
img = scan_tears(img, treble)
img = vhs_dropout(img)
img = vignette(img, kick)
return img
# ══════════════════════════════════════════════════════════════════════════════
# 4. MAIN
# ══════════════════════════════════════════════════════════════════════════════
def generate():
print("=" * 58)
print(" CYBERPUNK FEVER DREAM — AV Generator")
print(" 30s · 120 BPM · 1280×720 · 30fps")
print("=" * 58)
# Audio
print("\n[1/4] Synthesizing audio …")
L, R = synthesize_audio()
audio_path = "/home/claude/cyberpunk_audio.wav"
save_wav_stereo(audio_path, L, R)
kb = os.path.getsize(audio_path)//1024
print(f" ✓ {audio_path} ({kb} KB)")
# Envelopes
print("[2/4] Extracting per-frame envelopes …")
envs = extract_envelopes(L, R)
if not HAS_CV2:
print("[error] opencv-python required. pip install opencv-python")
return
# Video
print("[3/4] Rendering 900 frames …")
video_path = "/home/claude/cyberpunk_frames.mp4"
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(video_path, fourcc, FPS, (W, H))
scroll_y = 0.0
hue_offset = 0.0
glitch_offsets = []
glitch_timer = 0
datamosh_buf = None
prev_frame = None
for f in range(N_FRAMES):
kick = envs['kick'][f]
rms = envs['rms'][f]
# Grid scroll: base + kick lurch
scroll_y += 2.5 + kick*42.0
scroll_y %= (H - H//2)
hue_offset += 0.3 + rms*1.8
# Silhouette glitch timer
glitch_timer -= 1
if glitch_timer <= 0:
glitch_timer = np.random.randint(FPS*3, FPS*7)
n_segs = np.random.randint(1,3)
glitch_offsets = [
(np.random.randint(0,10),
np.random.randint(-110,110),
np.random.randint(-55,55))
for _ in range(n_segs)
]
elif glitch_timer > 2 and np.random.random() < 0.35:
glitch_offsets = []
# Datamosh trigger ~every 11s
t_s = f/FPS
is_datamosh = (int(t_s) % 11 == 0 and f % FPS < int(FPS*0.28))
if is_datamosh and prev_frame is not None:
datamosh_buf = prev_frame.copy()
frame = render_frame(f, envs, scroll_y, hue_offset,
glitch_offsets, datamosh_buf, is_datamosh)
# Frame-hold stutter: duplicate frame on high-rms stutter events
writer.write(frame)
if rms > 0.75 and np.random.random() < 0.08:
writer.write(frame) # duplicate = stutter
prev_frame = frame.copy()
if f % 150 == 0:
pct = f/N_FRAMES*100
print(f" {f}/{N_FRAMES} frames ({pct:.0f}%) …")
writer.release()
print(f" ✓ {video_path}")
# FFmpeg mux
print("[4/4] Muxing audio + video with FFmpeg …")
out_path = "/home/claude/cyberpunk_fever_dream.mp4"
cmd = [
"ffmpeg", "-y",
"-i", video_path,
"-i", audio_path,
"-c:v", "libx264", "-preset", "fast", "-crf", "18",
"-c:a", "aac", "-b:a", "192k",
"-shortest", out_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
mb = os.path.getsize(out_path)/1024/1024
print(f" ✓ {out_path} ({mb:.1f} MB)")
os.remove(video_path)
os.remove(audio_path)
else:
print("[ffmpeg stderr]", result.stderr[-500:])
print("\nDone.")
if __name__ == "__main__":
generate()