Add remote Focusrite calibration capture

This commit is contained in:
Meta Vens 2026-05-18 16:45:01 +01:00
commit 0281a1cc62
3 changed files with 115 additions and 0 deletions

View file

@ -126,6 +126,14 @@ After dropping the neighbor Focusrite WAV into the run's `sources/` folder, esti
.\.venv\Scripts\python.exe .\scripts\audio_field.py assemble-aligned --profile .\config\audio-field.json --run .\calibration\runs\<run-folder>
```
Record the neighbor Focusrite shotgun directly into an existing distributed run over SSH/SFTP:
```powershell
.\.venv\Scripts\python.exe .\scripts\audio_field.py record-remote-focusrite --profile .\config\audio-field.json --run .\calibration\runs\<run-folder> --seconds 10 --sample-rate 48000
```
This records `Analogue 1 + 2 (Focusrite USB Audio)` on `192.168.1.84` with remote FFmpeg, pulls it back as `sources/mic_focusrite_neighbor.wav`, and updates the run manifest. Use this for calibration imports; the existing OBS SRT source is still the live monitoring/streaming path.
`analyze-distributed` now does a deadline-grade chirplet refinement by default: it uses the coarse sweep matched-filter peak as the initial time-of-arrival, then searches a small fractional-delay and chirp-rate neighborhood with chirplet atoms. Tune the local search when needed:
```powershell

View file

@ -1,10 +1,12 @@
import argparse
import json
import math
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from tempfile import NamedTemporaryFile
import numpy as np
from scipy.io import wavfile
@ -38,6 +40,14 @@ def write_json(path: Path, data) -> None:
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
def run_checked(command, *, label: str) -> subprocess.CompletedProcess:
completed = subprocess.run(command, text=True, capture_output=True)
if completed.returncode != 0:
details = "\n".join(part for part in [completed.stdout, completed.stderr] if part)
raise SystemExit(f"{label} failed with exit {completed.returncode}\n{details}")
return completed
def load_profile(path: Path):
profile = read_json(path)
validate_profile_shape(profile)
@ -538,6 +548,87 @@ def cmd_record_local_calibration(args):
print(out)
def find_profile_mic(profile, mic_id: str):
for mic in profile["microphones"]:
if mic["id"] == mic_id:
return mic
raise SystemExit(f"No microphone {mic_id!r} exists in profile")
def upsert_manifest_source(run: Path, source: dict) -> None:
manifest_path = run / "manifest.json"
manifest = read_json(manifest_path)
sources = [item for item in manifest.get("sources", []) if item.get("micId") != source["micId"]]
sources.append(source)
sources.sort(key=lambda item: int(item.get("fieldChannel", 999)))
manifest["sources"] = sources
write_json(manifest_path, manifest)
def sftp_get(ssh_target: str, remote_file: str, local_file: Path) -> None:
remote_sftp_path = "/C:/" + remote_file.replace("\\", "/").split("C:/", 1)[-1]
batch = f'get "{remote_sftp_path}" "{local_file}"\n'
with NamedTemporaryFile("w", encoding="ascii", delete=False, suffix=".sftp") as fh:
fh.write(batch)
batch_path = fh.name
try:
run_checked(["sftp", "-b", batch_path, ssh_target], label="SFTP pull")
finally:
Path(batch_path).unlink(missing_ok=True)
def cmd_record_remote_focusrite(args):
profile = load_profile(args.profile)
run = args.run
if not (run / "manifest.json").exists():
raise SystemExit(f"Run manifest does not exist: {run / 'manifest.json'}")
mic = find_profile_mic(profile, args.mic_id)
device_name = args.device or args.dshow_device or "Analogue 1 + 2 (Focusrite USB Audio)"
sample_rate = int(args.sample_rate or profile.get("capturePolicy", {}).get("focusriteCalibrationSampleRate") or profile["sampleRate"])
seconds = float(args.seconds)
remote_dir = args.remote_dir.rstrip("\\/")
remote_file = f"{remote_dir}\\{args.mic_id}-{utc_stamp().replace(':', '').replace('.', '')}.wav"
local_file = run / "sources" / f"{args.mic_id}.wav"
local_file.parent.mkdir(parents=True, exist_ok=True)
ffmpeg = args.ffmpeg
remote_cmd = (
f'cmd /c if not exist "{remote_dir}" mkdir "{remote_dir}" && '
f'"{ffmpeg}" -y -hide_banner -nostdin -loglevel info '
f'-f dshow -t {seconds:.3f} -i audio="{device_name}" '
f'-vn -ac {int(args.channels)} -ar {sample_rate} -c:a pcm_f32le "{remote_file}"'
)
if args.dry_run:
print(remote_cmd)
return
print(f"Recording {args.mic_id} on {args.ssh_target} for {seconds:.1f}s...")
run_checked(["ssh", args.ssh_target, remote_cmd], label="Remote Focusrite capture")
sftp_get(args.ssh_target, remote_file, local_file)
if not local_file.exists() or local_file.stat().st_size == 0:
raise SystemExit(f"Remote capture did not produce a usable local file: {local_file}")
upsert_manifest_source(
run,
{
"micId": mic["id"],
"fieldChannel": mic_channel(mic),
"machine": mic.get("machine"),
"clockDomain": mic.get("clockDomain"),
"file": f"sources/{mic['id']}.wav",
"sampleRate": sample_rate,
"capture": {
"kind": "ssh-ffmpeg-dshow",
"sshTarget": args.ssh_target,
"remoteFile": remote_file,
"device": device_name,
"channels": int(args.channels),
},
},
)
print(local_file)
def record_loopback(query, seconds, sample_rate, channels=2, play_data=None, play_rate=None, play_device=None):
try:
import soundcard as sc
@ -1196,6 +1287,21 @@ def main():
p.add_argument("--loopback-channels", type=int, default=2)
p.set_defaults(func=cmd_record_local_calibration)
p = sub.add_parser("record-remote-focusrite", help="Record the neighbor Focusrite to a run source WAV over SSH/SFTP.")
p.add_argument("--profile", type=Path, default=ROOT / "config" / "audio-field.example.json")
p.add_argument("--run", type=Path, required=True)
p.add_argument("--mic-id", default="mic_focusrite_neighbor")
p.add_argument("--ssh-target", default="madman's lullaby@192.168.1.84")
p.add_argument("--ffmpeg", default=r"C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe")
p.add_argument("--remote-dir", default=r"C:\Meta\LocalCastBridge\calibration\remote-captures")
p.add_argument("--device", default="Analogue 1 + 2 (Focusrite USB Audio)")
p.add_argument("--dshow-device", help=argparse.SUPPRESS)
p.add_argument("--seconds", type=float, default=8.0)
p.add_argument("--sample-rate", type=int, default=48000)
p.add_argument("--channels", type=int, default=1)
p.add_argument("--dry-run", action="store_true")
p.set_defaults(func=cmd_record_remote_focusrite)
p = sub.add_parser("play-record", help="Play each speaker calibration sweep while recording all microphones.")
p.add_argument("--profile", type=Path, default=ROOT / "config" / "audio-field.example.json")
p.add_argument("--output", type=Path)

View file

@ -38,3 +38,4 @@
{"ts": "2026-05-18T15:23:38+00:00", "type": "decision", "status": "ok", "note": "Active sync probes are now an explicit optimizer responsibility: when passive chirplet telemetry leaves any source below confidence threshold, the system may emit bounded extra chirplets in masked windows, maximizing confidence gain per audible intrusion while respecting spacing and level limits."}
{"ts": "2026-05-18T15:35:24+00:00", "type": "verification", "status": "partial", "note": "Passive sync test using Scarlett output loopback as ground-truth content: 20s music run with normalized 4s/1s window correlation produced local Focusrite median delay 20115.5 samples/419.07 ms with 17 sample/0.35 ms MAD; PS Eye left 23848 samples/496.83 ms with 17 sample/0.35 ms MAD; Kiyo-left and PS Eye-right were noisier at about 3.9-4.2 ms MAD. Soundcard WASAPI loopback reported discontinuities, so loopback clock quality remains suspect."}
{"ts": "2026-05-18T15:38:07+00:00", "type": "verification", "status": "ok", "note": "OBS puppet pass installed win-spout, enabled local obs-websocket without auth, created Spout2 Capture source LocalCastBridge Point Cloud, and verified a nonblack OBS screenshot from the live Spout sender. Zyphos sample/research shifted the deadline sink from point sprites toward compact anisotropic screen brush packets."}
{"ts": "2026-05-18T15:44:14+00:00", "type": "verification", "status": "ok", "note": "Remote Focusrite shotgun capture path verified: SSH to 192.168.1.84 runs remote FFmpeg against Analogue 1 + 2 (Focusrite USB Audio), SFTP pulls a mono 48 kHz float WAV into a distributed calibration run, and the manifest records mic_focusrite_neighbor as field channel 5."}