138 lines
4.4 KiB
Bash
Executable File
138 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Generates dev test content: equirect compass video + orbiting ambiX tone.
|
|
# ambiX ACN/SN3D FOA, source at azimuth az(t) = 2*pi*t/8 (CCW from front):
|
|
# ch0 W = s(t) ch1 Y = s(t)*sin(az) (left positive)
|
|
# ch2 Z = 0 ch3 X = s(t)*cos(az) (front positive)
|
|
set -euo pipefail
|
|
|
|
# Check for required dependencies
|
|
if ! command -v ffmpeg &> /dev/null; then
|
|
echo "Error: ffmpeg not found. Install with: brew install ffmpeg" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v python3 &> /dev/null; then
|
|
echo "Error: python3 not found. Install with: brew install python3" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! python3 -c "import PIL" 2>/dev/null; then
|
|
echo "Error: Pillow (Python PIL) not installed." >&2
|
|
echo "Install with: pip install Pillow" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cd "$(dirname "$0")/.."
|
|
OUT=public/content/scenes/test-orbit
|
|
mkdir -p "$OUT"
|
|
DUR=24
|
|
|
|
ffmpeg -y -f lavfi -i "aevalsrc=\
|
|
sin(2*PI*440*t)|\
|
|
sin(2*PI*440*t)*sin(2*PI*t/8)|\
|
|
0|\
|
|
sin(2*PI*440*t)*cos(2*PI*t/8):c=quad:s=48000:d=${DUR}" \
|
|
-c:a pcm_s16le "$OUT/ambix.wav"
|
|
|
|
# Generate frames with PIL (video text rendering via frame-by-frame synthesis)
|
|
python3 - "$OUT" "$DUR" << 'PYTHON_EOF'
|
|
import sys
|
|
import subprocess
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
outdir = sys.argv[1]
|
|
duration = int(sys.argv[2])
|
|
fps = 30
|
|
width, height = 1920, 960
|
|
|
|
# Try to use a system font; fallback to default
|
|
try:
|
|
font_labels = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 72)
|
|
font_captions = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 56)
|
|
font_time = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 36)
|
|
except:
|
|
font_labels = ImageFont.load_default()
|
|
font_captions = ImageFont.load_default()
|
|
font_time = ImageFont.load_default()
|
|
|
|
frames_dir = f"{outdir}/frames"
|
|
import os
|
|
os.makedirs(frames_dir, exist_ok=True)
|
|
|
|
# Grid color: [email protected] alpha blended over background (0x203040 = RGB 32,48,64)
|
|
# Blend: (255*0.35 + 32*0.65, 255*0.35 + 48*0.65, 255*0.35 + 64*0.65) = (112, 124, 136)
|
|
grid_color = (112, 124, 136)
|
|
|
|
# Generate each frame
|
|
for frame_idx in range(duration * fps):
|
|
t = frame_idx / fps
|
|
|
|
# Create frame with dark background
|
|
img = Image.new('RGB', (width, height), color=(0x20, 0x30, 0x40))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Draw grid (pre-blended color for alpha effect)
|
|
grid_w, grid_h = 120, 120
|
|
for x in range(0, width, grid_w):
|
|
draw.line([(x, 0), (x, height)], fill=grid_color, width=1)
|
|
for y in range(0, height, grid_h):
|
|
draw.line([(0, y), (width, y)], fill=grid_color, width=1)
|
|
|
|
# Draw cardinal labels (FRONT, RIGHT, LEFT, BACK)
|
|
draw.text(((width - 120) // 2, (height - 60) // 2), "FRONT", fill=(255, 255, 255), font=font_labels)
|
|
draw.text((int(3 * width / 4 - 60), (height - 60) // 2), "RIGHT", fill=(255, 255, 255), font=font_labels)
|
|
draw.text((int(width / 4 - 40), (height - 60) // 2), "LEFT", fill=(255, 255, 255), font=font_labels)
|
|
draw.text((16, (height - 60) // 2), "BACK", fill=(255, 255, 255), font=font_labels)
|
|
|
|
# Determine caption based on time phase (8s cycle)
|
|
phase = t % 8
|
|
caption = None
|
|
if phase < 1 or phase >= 7:
|
|
caption = "SOUND FRONT"
|
|
elif 1 <= phase < 3:
|
|
caption = "SOUND LEFT"
|
|
elif 3 <= phase < 5:
|
|
caption = "SOUND BACK"
|
|
elif 5 <= phase < 7:
|
|
caption = "SOUND RIGHT"
|
|
|
|
# Draw caption
|
|
if caption:
|
|
cap_bbox = draw.textbbox((0, 0), caption, font=font_captions)
|
|
cap_w = cap_bbox[2] - cap_bbox[0]
|
|
draw.text(((width - cap_w) // 2, int(height * 0.72)), caption, fill=(255, 255, 0), font=font_captions)
|
|
|
|
# Draw timestamp
|
|
hms_str = f"t={int(t//3600):02d}:{int((t%3600)//60):02d}:{int(t%60):02d}"
|
|
draw.text((16, 16), hms_str, fill=(255, 255, 255), font=font_time)
|
|
|
|
# Save frame
|
|
frame_path = f"{frames_dir}/frame_{frame_idx:06d}.png"
|
|
img.save(frame_path)
|
|
|
|
# Encode PNG sequence to MP4 with error checking
|
|
try:
|
|
subprocess.run([
|
|
'ffmpeg', '-y',
|
|
'-framerate', str(fps),
|
|
'-i', f'{frames_dir}/frame_%06d.png',
|
|
'-c:v', 'libx264',
|
|
'-pix_fmt', 'yuv420p',
|
|
'-an',
|
|
f'{outdir}/equirect.mp4'
|
|
], check=True, capture_output=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error: ffmpeg encoding failed: {e.stderr.decode()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Clean up frames directory
|
|
import shutil
|
|
shutil.rmtree(frames_dir)
|
|
|
|
PYTHON_EOF
|
|
|
|
ffmpeg -y -ss 2 -i "$OUT/equirect.mp4" -frames:v 1 -update 1 \
|
|
-vf scale=480:240 "$OUT/thumb.jpg"
|
|
|
|
echo "fixtures written to $OUT"
|