Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| def hop_latency_ms(self) -> float: | |
| return 1000.0 * self.hop_length / self.sr | |
| 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) |