Skip to content

Instantly share code, notes, and snippets.

@twobob
Created October 2, 2025 22:35
Show Gist options
  • Save twobob/077531cecbb77aa44fe97cec23628f5d to your computer and use it in GitHub Desktop.
Save twobob/077531cecbb77aa44fe97cec23628f5d to your computer and use it in GitHub Desktop.

Revisions

  1. twobob created this gist Oct 2, 2025.
    203 changes: 203 additions & 0 deletions make_vid_from_wav.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,203 @@
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.cm as cm
    import wave
    import contextlib
    import cv2
    import os
    import shutil
    from moviepy.editor import AudioFileClip, VideoFileClip

    # --- Configuration ---
    # Input WAV and output MP4 paths
    WAV_PATH = "garbage.wav"
    OUTPUT_MP4 = "garbage_VISUAL.mp4"

    # Visualization parameters
    FPS = 30
    FIG_SIZE = (10, 10)
    DPI = 120
    NUM_LOOPS = 4 # Number of concentric visualizer loops

    # Aesthetics
    BG_COLOR = '#0B0014'
    COLORMAP = cm.magma # Colormap for dynamic colors (e.g., magma, plasma, inferno)
    PARTICLE_COLOR = '#FFFFFF'
    TITLE_TEXT = 'GARBAGE'
    TITLE_COLOR = (1.0, 1.0, 1.0, 0.08)

    # --- Setup Directories ---
    FRAMES_DIR = "temp_visualization_frames"
    if os.path.exists(FRAMES_DIR):
    shutil.rmtree(FRAMES_DIR)
    os.makedirs(FRAMES_DIR, exist_ok=True)

    # --- Audio Processing ---
    try:
    with contextlib.closing(wave.open(WAV_PATH, 'r')) as wf:
    num_channels = wf.getnchannels()
    sampwidth = wf.getsampwidth()
    framerate = wf.getframerate()
    num_frames = wf.getnframes()
    audio_data = wf.readframes(num_frames)

    dtype_map = {1: np.int8, 2: np.int16, 4: np.int32}
    if sampwidth not in dtype_map:
    raise ValueError("Unsupported sample width")
    audio_np = np.frombuffer(audio_data, dtype=dtype_map[sampwidth])

    if num_channels == 2:
    audio_np = audio_np.reshape(-1, 2).mean(axis=1)

    print("Audio file loaded successfully.")
    print(f"Duration: {num_frames / float(framerate):.2f}s, Framerate: {framerate}Hz")

    except FileNotFoundError:
    print(f"Error: The file '{WAV_PATH}' was not found.")
    exit()
    except Exception as e:
    print(f"An error occurred while reading the audio file: {e}")
    exit()

    # --- Visualization Frame Generation ---
    samples_per_frame = framerate // FPS
    num_frames_to_generate = int(num_frames / samples_per_frame)
    max_amplitude = np.max(np.abs(audio_np)) if len(audio_np) > 0 else 1.0

    print(f"Generating {num_frames_to_generate} frames...")

    for i in range(num_frames_to_generate):
    start = i * samples_per_frame
    end = start + samples_per_frame
    frame_audio = audio_np[start:end]

    if frame_audio.size == 0:
    continue

    # --- Create the Radial Visualization ---
    fig = plt.figure(figsize=FIG_SIZE, facecolor=BG_COLOR)
    ax = plt.subplot(111, polar=True, facecolor=BG_COLOR)

    fig.text(0.5, 0.5, TITLE_TEXT, fontsize=120, color=TITLE_COLOR,
    ha='center', va='center', weight='bold')

    # Calculate RMS for intensity and get the dynamic color for this frame
    rms = np.sqrt(np.mean(np.square(frame_audio.astype(np.float64))))
    normalized_rms = rms / max_amplitude if max_amplitude > 0 else 0
    if not np.isfinite(normalized_rms):
    normalized_rms = 0

    # The color is determined by the loudness of the frame
    dynamic_color = COLORMAP(normalized_rms)
    # A slightly brighter color for the main line
    glow_color = COLORMAP(min(normalized_rms * 1.2, 1.0))

    num_samples = len(frame_audio)
    theta = np.linspace(0, 2 * np.pi, num_samples)

    # --- Add Particle Effects ---
    num_particles = int(150 * normalized_rms)
    particle_thetas = np.random.uniform(0, 2 * np.pi, num_particles)
    particle_radii = np.random.uniform(0, 1.5, num_particles) * (1 + normalized_rms)
    particle_sizes = (np.random.rand(num_particles) * 30) * (0.5 + normalized_rms)
    ax.scatter(particle_thetas, particle_radii, s=particle_sizes, color=PARTICLE_COLOR, alpha=0.4 * normalized_rms, zorder=2)

    # --- Plot Multiple Concentric Loops ---
    for j in range(NUM_LOOPS):
    loop_base_radius = 0.4 * (j + 1)

    # Modulate radius based on audio waveform
    radius_modulation = (np.abs(frame_audio) / max_amplitude) * 0.2 * (1 + normalized_rms) if max_amplitude > 0 else np.zeros_like(frame_audio)
    radii = loop_base_radius + radius_modulation

    # Ensure smooth wrap-around
    loop_theta = np.append(theta, theta[0])
    loop_radii = np.append(radii, radii[0])

    # Plot glow effect - increased alpha for more intensity
    ax.plot(loop_theta, loop_radii, color=glow_color, linewidth=5 + j*2, alpha=0.7 * (0.3 + normalized_rms), zorder=3)
    # Plot main line - increased linewidth and always visible
    ax.plot(loop_theta, loop_radii, color=dynamic_color, linewidth=2.5, alpha=0.9, zorder=4)

    # --- Final Touches on the Plot ---
    ax.set_ylim(0, 2.5) # Increased limit for multiple loops
    ax.grid(False)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.spines['polar'].set_visible(False)

    plt.tight_layout()
    frame_path = os.path.join(FRAMES_DIR, f"frame_{i:05d}.png")
    plt.savefig(frame_path, dpi=DPI, facecolor=BG_COLOR)
    plt.close(fig)

    if (i + 1) % 10 == 0:
    print(f" ... {i+1}/{num_frames_to_generate} frames rendered")


    # --- Video Compilation ---
    print("\nAll frames generated. Compiling video...")

    first_frame_path = os.path.join(FRAMES_DIR, "frame_00000.png")
    if not os.path.exists(first_frame_path):
    print("Error: No frames were generated. Cannot create video.")
    exit()

    frame_example = cv2.imread(first_frame_path)
    h, w, _ = frame_example.shape
    temp_video_path = os.path.join(FRAMES_DIR, "visual.avi")

    # Use MJPEG codec which is more compatible
    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    video = cv2.VideoWriter(temp_video_path, fourcc, FPS, (w, h))

    for i in range(num_frames_to_generate):
    frame_path = os.path.join(FRAMES_DIR, f"frame_{i:05d}.png")
    if os.path.exists(frame_path):
    frame = cv2.imread(frame_path)
    video.write(frame)
    video.release()

    # --- Combine Video and Audio using ffmpeg-python ---
    print("Combining video and audio...")

    try:
    import ffmpeg

    # Use ffmpeg-python to combine video and audio
    video_stream = ffmpeg.input(temp_video_path)
    audio_stream = ffmpeg.input(WAV_PATH)

    output = ffmpeg.output(
    video_stream,
    audio_stream,
    OUTPUT_MP4,
    vcodec='libx264',
    acodec='aac',
    shortest=None,
    **{'b:v': '5000k'} # Video bitrate for quality
    )

    # Overwrite output file if it exists
    output = ffmpeg.overwrite_output(output)

    # Run the ffmpeg command
    ffmpeg.run(output, capture_stdout=True, capture_stderr=True)

    print(f"\nSuccessfully created video: {OUTPUT_MP4}")

    except ImportError:
    print("\nError: ffmpeg-python library not found.")
    print("Please install it with: pip install ffmpeg-python")
    print("You can find the silent video at:", temp_video_path)

    except Exception as e:
    print(f"\nAn error occurred during the final video compilation: {e}")
    import traceback
    traceback.print_exc()
    print("You can find the silent video at:", temp_video_path)

    finally:
    print("Cleaning up temporary files...")
    if os.path.exists(FRAMES_DIR):
    shutil.rmtree(FRAMES_DIR)