woodfireind's picture
HOA7 Spatial Field Decoder (hoa64 v0.5.0): 7th-order Ambisonics encode/decode, Wigner-D rotation, DOA analysis, vision fuse, diffusion conditioning
570b87b verified
Raw
History Blame Contribute Delete
2.72 kB
"""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