Sign in
EN
games/satisfying-bounce / src / Lib / Shorts / Mixer.php · commit 8bdd6e4
<?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)));    }}