Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """Procedural audio synth (numpy + stdlib wave). | |
| Generates calm, professional UI SFX and a sparse ambient music loop in the spirit of | |
| Minecraft's quiet themes. Used as the offline placeholder until an open audio model | |
| (Stable Audio Open / MusicGen) bakes higher-fidelity assets via scripts/prebake_audio.py. | |
| """ | |
| from __future__ import annotations | |
| import wave | |
| from pathlib import Path | |
| import numpy as np | |
| from .manifest import MUSIC_DIR, SFX_DIR | |
| SAMPLE_RATE = 22050 | |
| def _write_wav(path: Path, samples: np.ndarray) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| clipped = np.clip(samples, -1.0, 1.0) | |
| pcm = (clipped * 32767.0).astype("<i2") | |
| with wave.open(str(path), "wb") as wav: | |
| wav.setnchannels(1) | |
| wav.setsampwidth(2) | |
| wav.setframerate(SAMPLE_RATE) | |
| wav.writeframes(pcm.tobytes()) | |
| def _t(duration: float) -> np.ndarray: | |
| return np.linspace(0.0, duration, int(SAMPLE_RATE * duration), endpoint=False) | |
| def _adsr(n: int, attack: float = 0.01, release: float = 0.2) -> np.ndarray: | |
| env = np.ones(n) | |
| a = min(max(1, int(SAMPLE_RATE * attack)), n) | |
| r = min(max(1, int(SAMPLE_RATE * release)), max(0, n - a)) | |
| if a: | |
| env[:a] = np.linspace(0.0, 1.0, a) | |
| if r: | |
| env[-r:] = np.linspace(1.0, 0.0, r) | |
| return env | |
| def _tone(freq: float, duration: float, *, kind: str = "sine", gain: float = 0.4, | |
| attack: float = 0.01, release: float = 0.2) -> np.ndarray: | |
| t = _t(duration) | |
| phase = 2 * np.pi * freq * t | |
| if kind == "square": | |
| wave_data = np.sign(np.sin(phase)) | |
| elif kind == "triangle": | |
| wave_data = 2 / np.pi * np.arcsin(np.sin(phase)) | |
| else: | |
| wave_data = np.sin(phase) | |
| return wave_data * _adsr(len(t), attack, release) * gain | |
| def _sweep(f0: float, f1: float, duration: float, gain: float = 0.35) -> np.ndarray: | |
| t = _t(duration) | |
| freqs = np.linspace(f0, f1, len(t)) | |
| phase = 2 * np.pi * np.cumsum(freqs) / SAMPLE_RATE | |
| return np.sin(phase) * _adsr(len(t), 0.005, 0.15) * gain | |
| def _noise(duration: float, gain: float = 0.25) -> np.ndarray: | |
| n = int(SAMPLE_RATE * duration) | |
| rng = np.random.default_rng(7) | |
| return rng.uniform(-1, 1, n) * _adsr(n, 0.002, 0.08) * gain | |
| def _sfx(name: str) -> np.ndarray: | |
| if name == "click": | |
| return _tone(660, 0.06, kind="triangle", release=0.05) | |
| if name == "select": | |
| return np.concatenate([_tone(523, 0.06, release=0.04), _tone(784, 0.08, release=0.06)]) | |
| if name == "present": | |
| return _sweep(400, 900, 0.22) | |
| if name == "accuse": | |
| return _tone(110, 0.5, kind="triangle", gain=0.5, release=0.4) | |
| if name == "success": | |
| return np.concatenate([_tone(f, 0.12, release=0.1) for f in (523, 659, 784, 1046)]) | |
| if name == "fail": | |
| return np.concatenate([_tone(f, 0.16, kind="triangle", release=0.12) for f in (392, 311, 233)]) | |
| if name == "page": | |
| return _noise(0.18) | |
| return _tone(440, 0.1) | |
| def generate_sfx(out_dir: Path | None = None) -> list[Path]: | |
| out = out_dir or SFX_DIR | |
| from .manifest import SFX_EVENTS | |
| paths: list[Path] = [] | |
| for event, filename in SFX_EVENTS.items(): | |
| path = out / filename | |
| _write_wav(path, _sfx(event)) | |
| paths.append(path) | |
| return paths | |
| # A slow lofi-noir jazz loop: i - iv - V7 - i in A minor, soft Rhodes-like pads, | |
| # a gentle bass, brushed percussion, and faint vinyl crackle. | |
| _CHORDS: tuple[tuple[float, tuple[float, ...]], ...] = ( | |
| (110.00, (261.63, 329.63, 392.00)), # Am7 (A bass; C E G) | |
| (146.83, (349.23, 440.00, 523.25)), # Dm7 (D bass; F A C) | |
| (164.81, (415.30, 493.88, 587.33)), # E7 (E bass; G# B D) | |
| (110.00, (261.63, 329.63, 392.00)), # Am7 | |
| ) | |
| def _pad(freqs: tuple[float, ...], duration: float) -> np.ndarray: | |
| t = _t(duration) | |
| vibrato = 1.0 + 0.004 * np.sin(2 * np.pi * 5.0 * t) | |
| tone = np.zeros(len(t)) | |
| for f in freqs: | |
| tone += np.sin(2 * np.pi * f * t * vibrato) + 0.4 * np.sin(2 * np.pi * 2 * f * t) | |
| env = np.ones(len(t)) | |
| a, r = int(SAMPLE_RATE * 0.25), int(SAMPLE_RATE * 0.6) | |
| env[:a] = np.linspace(0, 1, a) | |
| env[-r:] = np.linspace(1, 0.0, r) | |
| return tone / len(freqs) * env * 0.16 | |
| def generate_music(out_dir: Path | None = None, chord_seconds: float = 4.0) -> Path: | |
| out = out_dir or MUSIC_DIR | |
| duration = chord_seconds * len(_CHORDS) | |
| n = int(SAMPLE_RATE * duration) | |
| mix = np.zeros(n) | |
| rng = np.random.default_rng(1989) | |
| for i, (bass, voicing) in enumerate(_CHORDS): | |
| start = int(i * chord_seconds * SAMPLE_RATE) | |
| chord = _pad(voicing, chord_seconds) | |
| bass_line = _tone(bass, chord_seconds, kind="sine", gain=0.18, attack=0.05, | |
| release=chord_seconds * 0.4) | |
| seg = chord[: len(bass_line)] + bass_line[: len(chord)] | |
| end = min(n, start + len(seg)) | |
| mix[start:end] += seg[: end - start] | |
| # Brushed percussion on a slow ~80 BPM pulse. | |
| beat = 60.0 / 80.0 | |
| pos = 0.0 | |
| while pos < duration - 0.1: | |
| tick = _noise(0.05, gain=0.04) | |
| s = int(pos * SAMPLE_RATE) | |
| e = min(n, s + len(tick)) | |
| mix[s:e] += tick[: e - s] | |
| pos += beat | |
| # Faint vinyl crackle. | |
| crackle = rng.uniform(-1, 1, n) | |
| crackle[np.abs(crackle) < 0.985] = 0.0 | |
| mix += crackle * 0.05 | |
| # Seamless loop fade. | |
| fade = int(SAMPLE_RATE * 0.8) | |
| mix[:fade] *= np.linspace(0, 1, fade) | |
| mix[-fade:] *= np.linspace(1, 0, fade) | |
| mix *= 0.85 / (np.max(np.abs(mix)) or 1.0) | |
| path = out / "ambient_theme.wav" | |
| _write_wav(path, mix) | |
| return path | |
| def generate_placeholder_pack() -> list[Path]: | |
| return [*generate_sfx(), generate_music()] | |