kinaar111's picture
Production: mystery shell + physics-audio from staging
d46bde8 verified
Raw
History Blame Contribute Delete
5.47 kB
"""
GPU-accelerated streaming STFT for sub-10ms frame latency.
Uses a ring buffer + torch.stft on CUDA (or CPU fallback) for incremental
magnitude-spectrum frames suitable for real-time monitoring and fast STFT loss.
"""
from __future__ import annotations
import time
import torch
import torch.nn.functional as F
from typing import Optional
from .config import device, REAL_AUDIO_STFT_HOP_RATIO
class StreamingGPUSTFT:
"""
Incremental STFT on GPU with ring-buffered audio.
Default n_fft=512, hop=128 → ~2.9ms hop latency @ 44.1kHz (well under 10ms).
"""
def __init__(
self,
n_fft: int = 512,
hop_length: Optional[int] = None,
sr: int = 44100,
dev: Optional[torch.device] = None,
):
self.n_fft = n_fft
self.hop_length = hop_length or max(64, int(n_fft * REAL_AUDIO_STFT_HOP_RATIO))
self.sr = sr
self.dev = dev or device
self._window = torch.hann_window(n_fft, device=self.dev)
self._buffer = torch.zeros(n_fft, device=self.dev, dtype=torch.float32)
self._pending = torch.zeros(0, device=self.dev, dtype=torch.float32)
self._frames: list[torch.Tensor] = []
self._total_samples = 0
self._last_frame_ms: float = 0.0
@property
def hop_latency_ms(self) -> float:
return 1000.0 * self.hop_length / self.sr
@property
def window_latency_ms(self) -> float:
return 1000.0 * self.n_fft / self.sr
def reset(self) -> None:
self._buffer.zero_()
self._pending = torch.zeros(0, device=self.dev, dtype=torch.float32)
self._frames.clear()
self._total_samples = 0
def push(self, chunk: torch.Tensor) -> list[torch.Tensor]:
"""
Push audio chunk (1D). Returns list of new magnitude frames produced.
Each frame shape: (n_fft // 2 + 1,).
"""
t0 = time.perf_counter()
chunk = chunk.to(self.dev, dtype=torch.float32).flatten()
self._pending = torch.cat([self._pending, chunk])
new_frames: list[torch.Tensor] = []
while self._pending.numel() >= self.hop_length:
step = self._pending[:self.hop_length]
self._pending = self._pending[self.hop_length:]
self._buffer = torch.cat([self._buffer[self.hop_length:], step])
self._total_samples += self.hop_length
spec = torch.stft(
self._buffer, n_fft=self.n_fft, hop_length=self.n_fft,
window=self._window, return_complex=True, center=False,
)
mag = spec.abs().squeeze(-1)
new_frames.append(mag)
self._frames.append(mag)
self._last_frame_ms = (time.perf_counter() - t0) * 1000.0
return new_frames
def get_accumulated_magnitude(self) -> Optional[torch.Tensor]:
"""Return (n_bins, n_frames) magnitude spectrogram accumulated so far."""
if not self._frames:
return None
return torch.stack(self._frames, dim=1)
def streaming_stft_loss(
self,
target_frames: list[torch.Tensor],
pred_frames: list[torch.Tensor],
) -> torch.Tensor:
"""L1 loss between matched streaming magnitude frames."""
n = min(len(target_frames), len(pred_frames))
if n == 0:
return torch.tensor(0.0, device=self.dev)
loss = torch.tensor(0.0, device=self.dev)
for i in range(n):
loss = loss + F.l1_loss(pred_frames[i], target_frames[i])
return loss / n
def benchmark(self, n_chunks: int = 200, chunk_samples: int = 128) -> dict:
"""Benchmark push() latency — reports mean/max ms per chunk."""
self.reset()
latencies = []
dummy = torch.randn(chunk_samples, device=self.dev)
for _ in range(n_chunks):
t0 = time.perf_counter()
self.push(dummy)
latencies.append((time.perf_counter() - t0) * 1000.0)
return {
'device': str(self.dev),
'n_fft': self.n_fft,
'hop_length': self.hop_length,
'hop_latency_ms': self.hop_latency_ms,
'mean_push_ms': float(sum(latencies) / len(latencies)),
'max_push_ms': float(max(latencies)),
'under_10ms': max(latencies) < 10.0,
}
def multi_resolution_stft_loss_gpu(
y_pred: torch.Tensor,
y_true: torch.Tensor,
fft_sizes: list | None = None,
hop_ratio: float = REAL_AUDIO_STFT_HOP_RATIO,
dev: Optional[torch.device] = None,
) -> torch.Tensor:
"""GPU-optimized multi-resolution STFT loss (all tensors on device)."""
dev = dev or y_pred.device
if fft_sizes is None:
from .config import REAL_AUDIO_STFT_FFT_SIZES
fft_sizes = REAL_AUDIO_STFT_FFT_SIZES
y_pred = y_pred.float().flatten().to(dev)
y_true = y_true.float().flatten().to(dev)
min_len = min(y_pred.shape[0], y_true.shape[0])
y_pred = y_pred[:min_len]
y_true = y_true[:min_len]
total = torch.tensor(0.0, device=dev)
for n_fft in fft_sizes:
hop = max(1, int(n_fft * hop_ratio))
window = torch.hann_window(n_fft, device=dev)
spec_pred = torch.stft(y_pred, n_fft=n_fft, hop_length=hop, window=window, return_complex=True)
spec_true = torch.stft(y_true, n_fft=n_fft, hop_length=hop, window=window, return_complex=True)
total = total + F.l1_loss(spec_pred.abs(), spec_true.abs())
return total / len(fft_sizes)