Publish aligned mic field for Faust separation

This commit is contained in:
Meta Vens 2026-05-18 22:09:10 +01:00
commit a4c3042628
12 changed files with 452 additions and 2 deletions

View file

@ -86,6 +86,18 @@ def msgpack_head(path: Path):
"needsActiveProbe": payload[9] if len(payload) > 9 else True,
}
)
elif summary["type"] == "localcast.audio.mic_field":
summary.update(
{
"frameId": payload[1],
"audioTimeNs": payload[3],
"sampleRate": payload[4],
"startSample": payload[5],
"frameCount": payload[6],
"channels": payload[7] if len(payload) > 7 else [],
"graphId": payload[9] if len(payload) > 9 else "",
}
)
elif summary["type"] in ("localcast.visual.stream_status", "localcast.audio.stream_status"):
summary["payloadHead"] = head
return summary
@ -110,6 +122,7 @@ def main() -> None:
"visual-stream-status.msgpack",
"audio-stream-status.msgpack",
"audio-phase-field.msgpack",
"audio-mic-field.msgpack",
"active-probes/active-probes.jsonl",
"stream-spout-status.json",
"av-sync-status.json",
@ -124,6 +137,7 @@ def main() -> None:
"audioHead": msgpack_head(runs / "audio-state.msgpack"),
"eventsHead": msgpack_head(runs / "audio-events.msgpack"),
"phaseFieldHead": msgpack_head(runs / "audio-phase-field.msgpack"),
"micFieldHead": msgpack_head(runs / "audio-mic-field.msgpack"),
"lastActiveProbe": read_last_jsonl(runs / "active-probes" / "active-probes.jsonl"),
}
print(json.dumps(payload, indent=2, default=str))

View file

@ -0,0 +1,35 @@
param(
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path,
[string]$Python = ".\.venv\Scripts\python.exe",
[string]$InputPath = ".\calibration\runs\audio-program-live-20260518-180226\field-program-cleaned.wav",
[string]$Cache = ".\calibration\runs\audio-mic-field.msgpack"
)
$ErrorActionPreference = "Stop"
Set-Location -LiteralPath $Root
$logDir = Join-Path $Root "logs"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$resolvedPython = if ([System.IO.Path]::IsPathRooted($Python)) { $Python } else { Join-Path $Root $Python }
$stdout = Join-Path $logDir "faust-mic-field.stdout.log"
$stderr = Join-Path $logDir "faust-mic-field.stderr.log"
$pidPath = Join-Path $logDir "faust-mic-field.pid"
$arguments = @(
".\scripts\stream_faust_mic_field.py",
"--input", $InputPath,
"--cache", $Cache,
"--loop",
"--realtime",
"--smoke-readback"
)
$process = Start-Process -FilePath $resolvedPython -ArgumentList $arguments -RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru -WindowStyle Hidden
Set-Content -Path $pidPath -Value ([string]$process.Id) -Encoding ASCII
Write-Host "Started live Faust mic-field publisher PID $($process.Id)"
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "pid: $pidPath"

View file

@ -0,0 +1,30 @@
param(
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
)
$ErrorActionPreference = "Stop"
Set-Location -LiteralPath $Root
$pidPath = Join-Path $Root "logs\faust-mic-field.pid"
if (-not (Test-Path -LiteralPath $pidPath)) {
Write-Host "No live Faust mic-field PID file found."
exit 0
}
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
if ($pidText.Length -eq 0) {
Remove-Item -LiteralPath $pidPath -Force
Write-Host "Empty PID file removed."
exit 0
}
$process = Get-Process -Id ([int]$pidText) -ErrorAction SilentlyContinue
if ($null -ne $process) {
Stop-Process -Id $process.Id -Force
Write-Host "Stopped live Faust mic-field PID $($process.Id)"
} else {
Write-Host "Live Faust mic-field PID $pidText is not running."
}
Remove-Item -LiteralPath $pidPath -Force

View file

@ -0,0 +1,104 @@
import argparse
from pathlib import Path
import sys
import time
import numpy as np
from scipy.io import wavfile
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from audio_field.cultcache_audio import ( # noqa: E402
DEFAULT_MIC_FIELD_CHANNELS,
get_live_mic_field_frame,
make_mic_field_frame,
put_live_mic_field_frame,
)
def read_float_wav(path: Path) -> tuple[int, np.ndarray]:
rate, data = wavfile.read(path)
samples = data.astype(np.float32)
if np.issubdtype(data.dtype, np.integer):
samples /= np.iinfo(data.dtype).max
if samples.ndim == 1:
samples = samples[:, None]
return int(rate), samples
def source_block(samples: np.ndarray, start_sample: int, frame_count: int, *, loop: bool) -> np.ndarray | None:
if start_sample >= len(samples) and not loop:
return None
block = np.zeros((frame_count, samples.shape[1]), dtype=np.float32)
written = 0
cursor = start_sample
while written < frame_count:
if cursor >= len(samples):
if not loop:
break
cursor %= len(samples)
count = min(frame_count - written, len(samples) - cursor)
block[written : written + count] = samples[cursor : cursor + count]
written += count
cursor += count
return block
def main() -> None:
parser = argparse.ArgumentParser(description="Publish aligned mic channels as a Faust-ready voice-separation input field.")
parser.add_argument("--input", type=Path, default=ROOT / "calibration" / "runs" / "audio-program-live-20260518-180226" / "field-program-cleaned.wav")
parser.add_argument("--cache", type=Path, default=ROOT / "calibration" / "runs" / "audio-mic-field.msgpack")
parser.add_argument("--graph-id", default="localcast.faust.voice_separation.v1")
parser.add_argument("--channel", action="append")
parser.add_argument("--chunk-frames", type=int, default=1024)
parser.add_argument("--duration", type=float)
parser.add_argument("--loop", action="store_true")
parser.add_argument("--realtime", action="store_true")
parser.add_argument("--smoke-readback", action="store_true")
args = parser.parse_args()
sample_rate, source = read_float_wav(args.input)
channels = tuple(args.channel) if args.channel else DEFAULT_MIC_FIELD_CHANNELS
if len(channels) != source.shape[1]:
raise SystemExit(f"input has {source.shape[1]} channels but {len(channels)} channel ids were provided")
start_monotonic = time.monotonic()
audio_origin_ns = time.monotonic_ns()
frame_id = 0
start_sample = 0
while True:
now = time.monotonic()
if args.duration is not None and now - start_monotonic >= args.duration:
break
block = source_block(source, start_sample, args.chunk_frames, loop=args.loop)
if block is None:
break
audio_time_ns = audio_origin_ns + int(start_sample * 1_000_000_000 / sample_rate)
frame = make_mic_field_frame(
block,
frame_id=frame_id,
sample_rate=sample_rate,
start_sample=start_sample,
audio_time_ns=audio_time_ns,
channels=channels,
graph_id=args.graph_id,
)
put_live_mic_field_frame(args.cache, frame)
if args.smoke_readback:
loaded = get_live_mic_field_frame(args.cache)
if loaded is None or loaded.frame_id != frame_id:
raise SystemExit("Mic-field CultCache smoke readback failed")
frame_id += 1
start_sample += args.chunk_frames
if args.realtime:
target_time = start_monotonic + start_sample / sample_rate
sleep_for = target_time - time.monotonic()
if sleep_for > 0:
time.sleep(sleep_for)
if __name__ == "__main__":
main()