Mimir/audio_field/runtime_sync.py
2026-05-18 16:24:10 +01:00

66 lines
2.3 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class ChirpletObservation:
source_id: str
frame_index: int
delay_samples: float
phase_radians: float
score: float
rate_scale: float = 1.0
@dataclass(frozen=True)
class SyncState:
delay_samples: float = 0.0
phase_radians: float = 0.0
rate_scale: float = 1.0
confidence: float = 0.0
last_frame_index: int = -1
class RuntimeSyncEstimator:
"""Maintains per-source delay/SRO/phase state from known speaker chirplets."""
def __init__(self, min_score: float = 0.18, smoothing: float = 0.2):
if not 0.0 <= smoothing <= 1.0:
raise ValueError("smoothing must be between 0 and 1")
self.min_score = min_score
self.smoothing = smoothing
self._states: dict[str, SyncState] = {}
def state_for(self, source_id: str) -> SyncState:
return self._states.get(source_id, SyncState())
def update(self, observation: ChirpletObservation) -> SyncState:
previous = self.state_for(observation.source_id)
if observation.score < self.min_score:
frozen = SyncState(
delay_samples=previous.delay_samples,
phase_radians=previous.phase_radians,
rate_scale=previous.rate_scale,
confidence=observation.score,
last_frame_index=previous.last_frame_index,
)
self._states[observation.source_id] = frozen
return frozen
alpha = self.smoothing
updated = SyncState(
delay_samples=blend(previous.delay_samples, observation.delay_samples, alpha, previous.confidence),
phase_radians=blend(previous.phase_radians, observation.phase_radians, alpha, previous.confidence),
rate_scale=blend(previous.rate_scale, observation.rate_scale, alpha, previous.confidence),
confidence=observation.score,
last_frame_index=observation.frame_index,
)
self._states[observation.source_id] = updated
return updated
def blend(previous: float, current: float, alpha: float, previous_confidence: float) -> float:
if previous_confidence <= 0.0:
return current
return (1.0 - alpha) * previous + alpha * current