errorCode === $code, 'expected ' . $code . ', got ' . $e->errorCode); return; } throw new RuntimeException('Expected ' . $code); }; $directory = sys_get_temp_dir() . '/fa-stereo-media-' . bin2hex(random_bytes(6)); mkdir($directory, 0700); $pcm = static function (int $seconds, int $amplitude, int $frequency): string { $data = ''; for ($i = 0; $i < $seconds * 16000; $i++) { $data .= pack('v', ((int) round($amplitude * sin(2 * M_PI * $frequency * $i / 16000))) & 65535); } return $data; }; $wave = static function (string $name, array $channels) use ($directory): string { $count = count($channels); $data = ''; $size = strlen($channels[0]); for ($i = 0; $i < $size; $i += 2) { foreach ($channels as $samples) { $data .= substr($samples, $i, 2); } } $path = $directory . '/' . $name . '.wav'; file_put_contents($path, 'RIFF' . pack('V', 36 + strlen($data)) . 'WAVEfmt ' . pack('VvvVVvv', 16, 1, $count, 16000, 32000 * $count, 2 * $count, 16) . 'data' . pack('V', strlen($data)) . $data); chmod($path, 0600); return $path; }; $data = static function (string $path): string { $bytes = file_get_contents($path); $offset = 12; while ($offset + 8 <= strlen($bytes)) { $length = unpack('V', substr($bytes, $offset + 4, 4))[1]; if (substr($bytes, $offset, 4) === 'data') { return substr($bytes, $offset + 8, $length); } $offset += 8 + $length + ($length % 2); } throw new RuntimeException('Missing PCM data'); }; $settings = ['asr_stereo_chunk_seconds' => 2, 'providers' => ['qwen' => ['driver' => 'asr_then_llm', 'label' => 'Synthetic', 'asr' => ['base_url' => 'https://asr.invalid/v1', 'api_key' => 'synthetic', 'model' => 'synthetic-asr'], 'extraction' => ['base_url' => 'https://llm.invalid/v1', 'api_key' => 'synthetic', 'model' => 'synthetic-llm']]]]; $provider = Provider::resolve('qwen', $settings, []); $taskFor = static fn (string $path, float $seconds): array => ['id' => 1, 'sha256' => hash_file('sha256', $path), 'duration_seconds' => $seconds, 'recorded_at' => '2026-09-29 10:00:00', 'model_key' => 'qwen', 'upstream_started_at' => 0, 'upstream_ids_json' => json_encode(['provider_fingerprint' => $provider['fingerprint']])]; try { $left = $pcm(5, 1000, 440); $right = $pcm(5, 3000, 880); $source = $wave('stereo', [$left, $right]); $task = $taskFor($source, 5); $media = new Media($settings); $audio = $media->inspect($source, $task['sha256']); $plan = Checkpoint::initial($task, $provider['fingerprint'], 2000, 2); $expect($plan['schema_version'] === 2 && $plan['source_channels'] === 2 && $plan['channel_policy'] === 'separate-fixed-windows-v1', 'new bound v2 checkpoint'); $expect(count($plan['chunks']) === 6 && array_column($plan['chunks'], 'channel') === [0, 1, 0, 1, 0, 1], 'both channels each window, temporal interleave'); $expect(array_column($plan['chunks'], 'start_ms') === [0, 0, 2000, 2000, 4000, 4000] && array_column($plan['chunks'], 'end_ms') === [2000, 2000, 4000, 4000, 5000, 5000], 'true shared offsets including tail'); foreach ($plan['chunks'] as $segment) { $chunk = $directory . '/chunk.wav'; $media->chunk($audio, $segment, $chunk, static fn (): bool => true); $expected = substr($segment['channel'] === 0 ? $left : $right, $segment['start_ms'] * 32, ($segment['end_ms'] - $segment['start_ms']) * 32); $expect($data($chunk) === $expected && Media::digitalSilence($chunk) === null, 'exact independent PCM samples, no cross-channel mixing'); unlink($chunk); } $expect(hash_file('sha256', $source) === $task['sha256'], 'original stereo bytes immutable'); $swap = $wave('swapped', [$right, $left]); $swapTask = $taskFor($swap, 5); $swapAudio = $media->inspect($swap, $swapTask['sha256']); $swapPlan = Checkpoint::initial($swapTask, $provider['fingerprint'], 2000, 2); $chunk = $directory . '/swapped-chunk.wav'; $media->chunk($swapAudio, $swapPlan['chunks'][0], $chunk, static fn (): bool => true); $expect($data($chunk) === substr($right, 0, 64000) && $swapPlan['chunks'][0]['id'] !== $plan['chunks'][0]['id'], 'physical channel swap observed without assigning roles'); unlink($chunk); $reject(static fn () => $media->chunk($audio, ['start_ms' => 0, 'end_ms' => 1000], $directory . '/forbidden.wav', static fn (): bool => true), 'AUDIO_CHANNEL_REQUIRED'); $mono = $wave('mono', [$left]); $monoTask = $taskFor($mono, 5); $monoPlan = Checkpoint::initial($monoTask, $provider['fingerprint'], 120000, 1); $expect(count($monoPlan['chunks']) === 1 && !isset($monoPlan['chunks'][0]['channel']), 'mono retains whole120s compatibility without channel invention'); $three = $wave('three', [$left, $right, $left]); $reject(static fn () => $media->inspect($three, hash_file('sha256', $three)), 'AUDIO_CHANNELS_UNSUPPORTED'); $expect(Media::assertChunkPlan(3600, 15000, 2) === 480 && Media::assertChunkPlan(500, 1000, 2) === 1000, 'budget counts both channels'); $reject(static fn () => Media::assertChunkPlan(501, 1000, 2), 'ASR_SEGMENT_LIMIT'); $badSettings = $settings; $badSettings['asr_stereo_chunk_seconds'] = 31; $reject(static fn () => Provider::resolve('qwen', $badSettings, []), 'CONFIG_INVALID'); $newSettings = $settings; $newSettings['asr_stereo_chunk_seconds'] = 3; $expect(Provider::resolve('qwen', $newSettings, [])['fingerprint'] !== $provider['fingerprint'], 'stereo window binds provider identity'); $expect($provider['silence_policy'] === 'exact-zero-pcm-v1' && $provider['channel_policy'] === Checkpoint::CHANNEL_POLICY, 'explicit policies in provider binding'); // Exact-zero left vs nonzero one-LSB right. No RMS threshold or made-up ASR request. $silent = str_repeat("\0", 32000); $quiet = str_repeat(pack('v', 1), 16000); $mixed = $wave('zero-and-low', [$silent, $quiet]); $mixedTask = $taskFor($mixed, 1); $state = []; $calls = 0; $intents = 0; $heartbeat = static function (array $fields) use (&$state, &$intents, $mixedTask): bool { if (isset($fields['upstream_started_at'])) { $intents++; } if (isset($fields['pipeline_checkpoint'])) { $cp = $fields['pipeline_checkpoint']; Checkpoint::transition($state, $cp['state'], $mixedTask, $cp['expected_revision']); $state = $cp['state']; } return true; }; $transport = static function (array $spec) use (&$calls, &$state, $expect): array { $calls++; $expect($state['chunks'][0]['state'] === 'local_silence' && $state['chunks'][1]['state'] === 'intent', 'only nonzero right is a supplier intent'); $expect(Media::digitalSilence($spec['multipart']['file']->getFilename()) === null, 'one-LSB audio is not discarded'); return ['http_code' => 429, 'errno' => 0, 'body' => '{}']; }; $pipeline = new Pipeline($settings, $provider, $transport); $reject(static fn () => $pipeline->run($mixed, $mixedTask, $heartbeat), 'ASR_REJECTED'); $expect($calls === 1 && $intents === 1 && !isset($state['chunks'][0]['request_id']) && $state['chunks'][0]['text'] === '', 'local silence has zero billable request IDs'); $segments = Checkpoint::segments($state); $expect($segments[0]['channel'] === 0 && $segments[0]['source'] === 'local_silence' && $segments[0]['start_ms'] === 0 && $segments[0]['end_ms'] === 1000, 'silence coverage explicit, not omitted'); $forged = $state; $forged['chunks'][0]['request_id'] = 'fa-' . str_repeat('a', 32); $reject(static fn () => Checkpoint::validate($forged, $mixedTask), 'PIPELINE_CHECKPOINT_INVALID'); $forged = $state; $forged['chunks'][0]['pcm_sha256'] = str_repeat('a', 64); $reject(static fn () => Checkpoint::validate($forged, $mixedTask), 'PIPELINE_CHECKPOINT_INVALID'); $forged = $state; $forged['chunks'][0]['text'] = 'fake response'; $reject(static fn () => Checkpoint::validate($forged, $mixedTask), 'PIPELINE_CHECKPOINT_INVALID'); $forged = $state; $forged['chunks'][0]['channel'] = 1; $reject(static fn () => Checkpoint::validate($forged, $mixedTask), 'PIPELINE_CHECKPOINT_INVALID'); $pending = Checkpoint::initial($mixedTask, $provider['fingerprint'], 2000, 2); $pending['revision'] = 1; $intent = $pending; $intent['revision'] = 2; $intent['chunks'][0]['state'] = 'intent'; $intent['chunks'][0]['request_id'] = 'fa-' . str_repeat('a', 32); $notResolved = $pending; $notResolved['revision'] = 3; $notResolved['chunks'][0] = $state['chunks'][0]; $reject(static fn () => Checkpoint::transition($intent, $notResolved, $mixedTask, 2), 'PIPELINE_CHECKPOINT_INVALID'); // Historical v1 remains readable but may not be rebound to the new policy for retry. $legacy = ['schema_version' => 1, 'revision' => 1, 'source_sha256' => $monoTask['sha256'], 'fingerprint' => $provider['fingerprint'], 'duration_ms' => 5000, 'chunk_ms' => 120000, 'chunks' => [['id' => 'asr-' . hash('sha256', $monoTask['sha256'] . ':' . $provider['fingerprint'] . ':0:5000'), 'start_ms' => 0, 'end_ms' => 5000, 'state' => 'complete', 'request_id' => 'fa-' . str_repeat('b', 32), 'text' => 'legacy synthetic']], 'extraction' => ['state' => 'pending']]; $bytes = json_encode($legacy); Checkpoint::validate($legacy, $monoTask); $expect(json_encode($legacy) === $bytes && Checkpoint::segments($legacy)[0]['text'] === 'legacy synthetic', 'old v1 reading unchanged'); $before = $calls; $reject(static fn () => $pipeline->run($mono, $monoTask, static fn (): bool => true, $legacy), 'PIPELINE_POLICY_CHANGED'); $expect($calls === $before, 'legacy state cannot reroute requests under new policy'); echo 'FOLLOWUP_AUDIO_STEREO_MEDIA assertions=' . $checks . ' PASS separate_pcm=1 both_channels_covered=1 offsets=1 swaps=1 mono=1 unsupported_channels_rejected=1 exact_zero_local=1 low_volume_sent=1 legacy_read_only=1 live_model_calls=0' . PHP_EOL; } finally { foreach (glob($directory . '/*') as $file) { unlink($file); } rmdir($directory); }