<?phpnamespace JumPHP\Shorts;/** * The sound of a video, mixed offline from the notes a scene played (Recorder): every note a Voice (pluck, bell, meow...) at its time, * summed, a soft low-pass (no harsh edges), a short echo, a fade in / out, then normalised to the same peak for every video * and softly limited. Mono WAV, Voice::RATE Hz. No audio device needed. */final class Mixer{ /** * @param list<array{time: float, freq: float, length?: float, wave?: string, volume?: float}> $notes * @param float $fadeOut seconds of fade at the end (0: none, for a video that loops by itself) */ public static function mix(array $notes, float $seconds, float $fadeOut = 0.0, float $peak = 0.8): string { $rate = Voice::RATE; $count = max(1, (int)ceil($seconds * $rate)); $mix = array_fill(0, $count, 0.0); foreach ($notes as $note) { $start = (int)round($note['time'] * $rate); $samples = Voice::render((string)($note['wave'] ?? 'pluck'), (float)$note['freq'], (float)($note['length'] ?? 0.35), (float)($note['volume'] ?? 0.35)); $end = min($count, $start + count($samples)); for ($i = max(0, $start); $i < $end; $i++) { $mix[$i] += $samples[$i - $start]; } } // a soft low-pass (about 6 kHz) and an echo (0.21 s, a third of it, fading) $a = 1 - exp(-2 * M_PI * 6000 / $rate); $y = 0.0; $delay = (int)(0.21 * $rate); $out = array_fill(0, $count, 0.0); $max = 1e-6; for ($i = 0; $i < $count; $i++) { $y += $a * ($mix[$i] - $y); $v = $y + ($i >= $delay ? 0.32 * $out[$i - $delay] : 0.0); $out[$i] = $v; } $fadeIn = (int)(0.03 * $rate); $fadeLen = (int)($fadeOut * $rate); for ($i = 0; $i < $count; $i++) { if ($i < $fadeIn) { $out[$i] *= $i / $fadeIn; } if ($fadeLen > 0 && $i > $count - $fadeLen) { $out[$i] *= max(0.0, ($count - $i) / $fadeLen); } $max = max($max, abs($out[$i])); } $gain = $peak / $max; $data = ''; foreach ($out as $v) { $data .= pack('v', (int)(32767 * tanh($v * $gain * 1.05)) & 0xFFFF); } return Voice::header(strlen($data)) . $data; } /** notes.json (Recorder) -> a WAV file of $seconds; meta.json next to it may say {"fadeOut": s}. */ public static function mixFile(string $notesJson, float $seconds, string $wavFile): void { $notes = is_file($notesJson) ? (json_decode((string)file_get_contents($notesJson), true) ?: []) : []; $metaFile = dirname($notesJson) . '/meta.json'; $meta = is_file($metaFile) ? (json_decode((string)file_get_contents($metaFile), true) ?: []) : []; file_put_contents($wavFile, self::mix($notes, $seconds, (float)($meta['fadeOut'] ?? 0.0))); }}