"""Test stereo phase cancellation using a generated Robot 36 signal.

Usage: python reproduce.py ORIGINAL.wav PINNED_OPEN_SSTV_REPO NEW_OUTPUT_DIRECTORY
Requires Open-SSTV dependencies and FFmpeg. No audio device or network is used.
"""
from pathlib import Path
import hashlib, json, os, subprocess, sys, tomllib, wave
import numpy as np
from PIL import Image, ImageChops

source, repo, out = [Path(p).resolve() for p in sys.argv[1:]]
expected = 'dbb64c8cb34d3599c5aa7c36144746039632832da554eefff324a9fc68ae0166'
assert hashlib.sha256(source.read_bytes()).hexdigest() == expected
base = 'f773bd4fcb9da0c68ab915e9a1fa2c024bfdcc98'
subprocess.run(['git','diff','--exit-code',base,'--','src'],cwd=repo,check=True,capture_output=True)
out.mkdir(parents=True,exist_ok=False)
with wave.open(str(source)) as w:
    assert (w.getframerate(),w.getnchannels(),w.getsampwidth()) == (48000,1,2)
    samples=np.frombuffer(w.readframes(w.getnframes()),dtype='<i2').astype(np.int32)
assert samples.min() > -32768
for name, right in [('same-polarity-stereo', samples), ('opposite-polarity-stereo', -samples)]:
    stereo=np.stack([samples,right],axis=1).astype('<i2')
    with wave.open(str(out/(name+'.wav')),'wb') as w:
        w.setparams((2,2,48000,0,'NONE','not compressed'));w.writeframes(stereo.tobytes())
cmd=['ffmpeg','-hide_banner','-nostdin','-loglevel','error','-i','opposite-polarity-stereo.wav','-af','pan=mono|c0=c0','-c:a','pcm_s16le','left-channel.wav']
subprocess.run(cmd,cwd=out,check=True,capture_output=True)
version=tomllib.loads((repo/'pyproject.toml').read_text())['project']['version']
report={'experiment':'Stereo channel cancellation / 2026-11-04','input_kind':'Generated Robot36 practice signal; not RF, a microphone or the SSTV app','source_sha256':expected,'decoder_version':version,'decoder_source_base':base,'decoder_revision':subprocess.check_output(['git','rev-parse','HEAD'],cwd=repo,text=True).strip(),'python':sys.version.split()[0],'ffmpeg':subprocess.check_output(['ffmpeg','-version'],text=True).splitlines()[0],'recovery_command':cmd,'new_upstream_submission':False,'cases':[]}
for name in ['same-polarity-stereo','opposite-polarity-stereo','left-channel']:
    p=out/(name+'.wav')
    with wave.open(str(p)) as w:
        channels=w.getnchannels();rate=w.getframerate();count=w.getnframes();pcm=np.frombuffer(w.readframes(count),dtype='<i2').reshape(-1,channels).astype(float)
    mono=pcm.mean(axis=1);rms=float(np.sqrt(np.mean(mono**2)))
    args=[sys.executable,'-m','open_sstv.cli.decode',str(p),'-o',str(out/(name+'.png'))]
    result=subprocess.run(args,cwd=repo,env={**os.environ,'PYTHONPATH':str(repo/'src')},capture_output=True,text=True,timeout=180)
    log=(result.stdout+result.stderr).replace(str(out),'OUTPUT').replace(str(repo),'OPEN_SSTV_REPO')
    (out/(name+'.log')).write_text(log)
    image=out/(name+'.png')
    row={'name':name,'rate_hz':rate,'channels':channels,'frames':count,'duration_seconds':count/rate,'file_sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'channel_rms_int16':[float(np.sqrt(np.mean(pcm[:,c]**2))) for c in range(channels)],'arithmetic_mono_peak_int16':float(np.abs(mono).max()),'arithmetic_mono_rms_int16':rms,'exit_code':result.returncode,'decoded_dimensions':list(Image.open(image).size) if image.exists() else None,'decoder_output':log,'decode_command':['python','-m','open_sstv.cli.decode',p.name,'-o',image.name]}
    report['cases'].append(row);(out/'report.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(row),flush=True)
assert [r['exit_code'] for r in report['cases']]==[0,1,0]
assert report['cases'][1]['arithmetic_mono_rms_int16']==0
assert np.array_equal(pcm[:,0].astype(np.int32),samples)
a=Image.open(out/'same-polarity-stereo.png').convert('RGB');b=Image.open(out/'left-channel.png').convert('RGB')
assert a.size==b.size==(320,240)
report['recovered_image_pixels_match_same_polarity_control']=ImageChops.difference(a,b).getbbox() is None
assert report['recovered_image_pixels_match_same_polarity_control']
assert hashlib.sha256(source.read_bytes()).hexdigest()==expected
(out/'report.json').write_text(json.dumps(report,indent=2)+'\n')
print('PASS: non-silent channels cancel in mean; explicit left-channel extraction restores control-identical decoded pixels',flush=True)
