HOA7 Spatial Field Decoder (hoa64 v0.5.0): 7th-order Ambisonics encode/decode, Wigner-D rotation, DOA analysis, vision fuse, diffusion conditioning
570b87b verified | """Minimal STFT / ISTFT in pure NumPy (no scipy).""" | |
| from __future__ import annotations | |
| import numpy as np | |
| def hann_window(n: int) -> np.ndarray: | |
| if n <= 1: | |
| return np.ones(n, dtype=np.float64) | |
| return 0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(n, dtype=np.float64) / n) | |
| def stft( | |
| x: np.ndarray, | |
| *, | |
| n_fft: int = 1024, | |
| hop: int = 256, | |
| window: np.ndarray | None = None, | |
| ) -> tuple[np.ndarray, np.ndarray]: | |
| """Short-time FFT. | |
| Parameters | |
| ---------- | |
| x : (n_samples,) real | |
| Returns | |
| ------- | |
| freqs_bins : (n_fft//2+1,) (normalized later by caller with sr) | |
| S : complex64 (n_bins, n_frames) | |
| """ | |
| x = np.asarray(x, dtype=np.float64).reshape(-1) | |
| if window is None: | |
| window = hann_window(n_fft) | |
| window = np.asarray(window, dtype=np.float64) | |
| if window.shape[0] != n_fft: | |
| raise ValueError("window length must equal n_fft") | |
| if x.shape[0] < n_fft: | |
| x = np.pad(x, (0, n_fft - x.shape[0])) | |
| n_frames = 1 + (x.shape[0] - n_fft) // hop | |
| n_bins = n_fft // 2 + 1 | |
| S = np.empty((n_bins, n_frames), dtype=np.complex128) | |
| for i in range(n_frames): | |
| start = i * hop | |
| frame = x[start : start + n_fft] * window | |
| spec = np.fft.rfft(frame, n=n_fft) | |
| S[:, i] = spec | |
| return S | |
| def stft_freqs(n_fft: int, sample_rate: int) -> np.ndarray: | |
| return np.fft.rfftfreq(n_fft, d=1.0 / sample_rate) | |
| def frame_signal( | |
| x: np.ndarray, | |
| *, | |
| frame_len: int, | |
| hop: int, | |
| window: np.ndarray | None = None, | |
| ) -> np.ndarray: | |
| """Slice a 1-D signal into overlapping frames (n_frames, frame_len).""" | |
| x = np.asarray(x, dtype=np.float64).reshape(-1) | |
| if window is None: | |
| window = hann_window(frame_len) | |
| if x.shape[0] < frame_len: | |
| x = np.pad(x, (0, frame_len - x.shape[0])) | |
| n_frames = 1 + (x.shape[0] - frame_len) // hop | |
| out = np.empty((n_frames, frame_len), dtype=np.float64) | |
| for i in range(n_frames): | |
| start = i * hop | |
| out[i] = x[start : start + frame_len] * window | |
| return out | |
| def frame_multichannel( | |
| audio: np.ndarray, | |
| *, | |
| frame_len: int, | |
| hop: int, | |
| ) -> np.ndarray: | |
| """Frame multi-channel audio (C, T) → (n_frames, C, frame_len) rectangular (no window).""" | |
| a = np.asarray(audio, dtype=np.float64) | |
| if a.ndim != 2: | |
| raise ValueError("audio must be (C,T)") | |
| C, T = a.shape | |
| if T < frame_len: | |
| a = np.pad(a, ((0, 0), (0, frame_len - T))) | |
| T = a.shape[1] | |
| n_frames = 1 + (T - frame_len) // hop | |
| out = np.empty((n_frames, C, frame_len), dtype=np.float64) | |
| for i in range(n_frames): | |
| start = i * hop | |
| out[i] = a[:, start : start + frame_len] | |
| return out | |