Publish aligned mic field for Faust separation
This commit is contained in:
parent
e7a4bff764
commit
a4c3042628
12 changed files with 452 additions and 2 deletions
|
|
@ -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))
|
||||
|
|
|
|||
35
scripts/start-live-faust-mic-field.ps1
Normal file
35
scripts/start-live-faust-mic-field.ps1
Normal 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"
|
||||
30
scripts/stop-live-faust-mic-field.ps1
Normal file
30
scripts/stop-live-faust-mic-field.ps1
Normal 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
|
||||
104
scripts/stream_faust_mic_field.py
Normal file
104
scripts/stream_faust_mic_field.py
Normal 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()
|
||||
Loading…
Reference in a new issue