Sih / src /transforms.py
CypherNotHere's picture
Upload src/transforms.py
f65e8b1 verified
Raw History Blame Contribute Delete
11.2 kB
"""Telephony Audio Degradation Pipeline for ASVspoof 5.
Simulates realistic voice call channel artifacts:
1. PSTN Narrowband Bandpass Filtering (300 Hz - 3400 Hz).
2. 8 kHz Downsampling (Nyquist = 4000 Hz).
3. G.711 Mu-law 8-bit Companding and Quantization.
4. VoIP / RTP Packet Loss Simulation (frame erasure / burst drops).
"""
from typing import Optional, Tuple
import torch
import torch.nn as nn
import torchaudio.transforms as T
import torchaudio.functional as F
class TelephonyBandpassFilter(nn.Module):
"""Simulates the 300 Hz - 3400 Hz acoustic passband of standard PSTN telephone networks."""
def __init__(self, sample_rate: int = 16000, low_freq: float = 300.0, high_freq: float = 3400.0):
super().__init__()
self.sample_rate = sample_rate
self.low_freq = low_freq
self.high_freq = high_freq
def forward(self, waveform: torch.Tensor) -> torch.Tensor:
"""Apply highpass at 300 Hz and lowpass at 3400 Hz.
waveform: Tensor of shape (..., num_samples)
"""
# Highpass to remove low-frequency rumble / DC offset (< 300 Hz)
filtered = F.highpass_biquad(waveform, sample_rate=self.sample_rate, cutoff_freq=self.low_freq)
# Lowpass to limit to narrowband telephone bandwidth (< 3400 Hz)
filtered = F.lowpass_biquad(filtered, sample_rate=self.sample_rate, cutoff_freq=self.high_freq)
return filtered
class PacketLossSimulator(nn.Module):
"""Simulates VoIP / RTP burst packet loss with frame erasure and packet loss concealment."""
def __init__(
self,
sample_rate: int = 8000,
packet_duration_ms: float = 20.0,
loss_rate: float = 0.05,
max_burst_packets: int = 3,
concealment: str = "zero" # 'zero', 'noise', or 'attenuate'
):
super().__init__()
self.sample_rate = sample_rate
self.packet_size = int(sample_rate * (packet_duration_ms / 1000.0))
self.loss_rate = loss_rate
self.max_burst_packets = max_burst_packets
self.concealment = concealment
def forward(self, waveform: torch.Tensor) -> torch.Tensor:
"""Apply packet drops on 20ms frames.
waveform: Tensor of shape (..., num_samples)
"""
if self.loss_rate <= 0.0 or not self.training:
return waveform
out = waveform.clone()
num_samples = out.shape[-1]
num_packets = num_samples // self.packet_size
if num_packets == 0:
return out
i = 0
while i < num_packets:
if torch.rand(1).item() < self.loss_rate:
burst_len = torch.randint(1, self.max_burst_packets + 1, (1,)).item()
burst_len = min(burst_len, num_packets - i)
start_sample = i * self.packet_size
end_sample = min((i + burst_len) * self.packet_size, num_samples)
if self.concealment == "zero":
out[..., start_sample:end_sample] = 0.0
elif self.concealment == "noise":
comfort_noise = torch.randn_like(out[..., start_sample:end_sample]) * 0.001
out[..., start_sample:end_sample] = comfort_noise
elif self.concealment == "attenuate":
out[..., start_sample:end_sample] *= 0.1
i += burst_len
else:
i += 1
return out
class TelephonyDegradation(nn.Module):
"""Complete telephony degradation pipeline.
Transforms 16 kHz input speech into realistic 8 kHz narrowband telephony speech:
1. Bandpass filter: 300 - 3400 Hz.
2. Resampling: 16 kHz -> 8 kHz.
3. G.711 Mu-law companding & 8-bit quantization.
4. VoIP packet loss simulation (during training).
5. Amplitude normalization.
"""
def __init__(
self,
orig_sample_rate: int = 16000,
target_sample_rate: int = 8000,
apply_bandpass: bool = True,
apply_mulaw: bool = True,
packet_loss_rate: float = 0.05,
packet_duration_ms: float = 20.0
):
super().__init__()
self.orig_sample_rate = orig_sample_rate
self.target_sample_rate = target_sample_rate
self.apply_bandpass = apply_bandpass
self.apply_mulaw = apply_mulaw
# 1. Bandpass (300 - 3400 Hz at 16 kHz)
self.bandpass = TelephonyBandpassFilter(orig_sample_rate, 300.0, 3400.0)
# 2. Resampler (16 kHz -> 8 kHz)
self.resampler = T.Resample(orig_freq=orig_sample_rate, new_freq=target_sample_rate)
# 3. G.711 Mu-law (8-bit, 256 quantization channels)
self.mulaw_encoder = T.MuLawEncoding(quantization_channels=256)
self.mulaw_decoder = T.MuLawDecoding(quantization_channels=256)
# 4. Packet Loss Simulator (at 8 kHz)
self.packet_loss = PacketLossSimulator(
sample_rate=target_sample_rate,
packet_duration_ms=packet_duration_ms,
loss_rate=packet_loss_rate
)
def forward(self, waveform: torch.Tensor) -> torch.Tensor:
"""Apply telephony degradation pipeline.
Args:
waveform: Tensor of shape (channels, num_samples) or (batch, channels, num_samples)
Sampled at orig_sample_rate (16 kHz).
Returns:
degraded: Tensor at target_sample_rate (8 kHz) in [-1.0, 1.0].
"""
# Ensure float32
x = waveform.float()
# Step 1: Bandpass filter (PSTN standard passband 300-3400 Hz)
if self.apply_bandpass:
x = self.bandpass(x)
# Step 2: Resample to 8 kHz
x = self.resampler(x)
# Step 3: G.711 Mu-law companding & 8-bit non-linear quantization
if self.apply_mulaw:
# Clamp to [-1.0, 1.0] before mu-law companding
x = torch.clamp(x, -1.0, 1.0)
encoded = self.mulaw_encoder(x)
x = self.mulaw_decoder(encoded)
# Step 4: VoIP packet loss simulation
x = self.packet_loss(x)
# Step 5: Peak normalize to avoid clipping while maintaining dynamics
max_val = torch.max(torch.abs(x), dim=-1, keepdim=True)[0]
max_val = torch.clamp(max_val, min=1e-6)
# Avoid amplifying pure silence
scale = torch.where(max_val > 0.01, 0.95 / max_val, torch.ones_like(max_val))
x = x * scale
return x
class TandemTelephonyDegradation(nn.Module):
"""Dual-Codec (Tandem) Telephony Degradation Pipeline.
Simulates real-world phone calls traversing multiple heterogeneous networks:
1. Bandpass filter: 300 - 3400 Hz.
2. Resampling: 16 kHz -> 8 kHz.
3. Step 1 (Cellular Carrier): GSM-FR 6.10 (13 kbps, 8 kHz RPE-LTP).
4. Step 2 (VoIP / Web Gateway): G.711 µ-law or Opus compression.
5. VoIP packet loss simulation (burst drops on 20ms frames).
6. Peak normalization.
"""
def __init__(
self,
orig_sample_rate: int = 16000,
target_sample_rate: int = 8000,
cellular_codec: str = "GSM610", # GSM-FR 6.10
voip_codec: str = "ULAW", # G.711 µ-law or 'OPUS'
packet_loss_rate: float = 0.05,
packet_duration_ms: float = 20.0
):
super().__init__()
self.orig_sample_rate = orig_sample_rate
self.target_sample_rate = target_sample_rate
self.cellular_codec = cellular_codec
self.voip_codec = voip_codec.upper()
self.bandpass = TelephonyBandpassFilter(orig_sample_rate, 300.0, 3400.0)
self.resampler = T.Resample(orig_freq=orig_sample_rate, new_freq=target_sample_rate)
self.packet_loss = PacketLossSimulator(
sample_rate=target_sample_rate,
packet_duration_ms=packet_duration_ms,
loss_rate=packet_loss_rate
)
def _compress_gsm_single(self, audio_np: "np.ndarray") -> "np.ndarray":
"""In-memory GSM 6.10 Full Rate compression/decompression via libsndfile."""
import io
import soundfile as sf
bio = io.BytesIO()
sf.write(bio, audio_np, self.target_sample_rate, format="WAV", subtype="GSM610")
bio.seek(0)
decoded, _ = sf.read(bio, dtype="float32")
return decoded
def _compress_voip_single(self, audio_np: "np.ndarray") -> "np.ndarray":
"""In-memory VoIP compression (G.711 ULAW or OPUS) via libsndfile."""
import io
import soundfile as sf
bio = io.BytesIO()
if self.voip_codec == "OPUS":
sf.write(bio, audio_np, self.target_sample_rate, format="OGG", subtype="OPUS")
else:
sf.write(bio, audio_np, self.target_sample_rate, format="WAV", subtype="ULAW")
bio.seek(0)
decoded, _ = sf.read(bio, dtype="float32")
return decoded
def forward(self, waveform: torch.Tensor) -> torch.Tensor:
"""Apply sequential tandem compression (Cellular GSM-FR -> VoIP ULAW/Opus).
Args:
waveform: Tensor of shape (..., num_samples) at 16 kHz.
Returns:
degraded: Tensor of shape (..., target_samples) at 8 kHz.
"""
import numpy as np
x = waveform.float()
# Step 1: Bandpass filter (300 - 3400 Hz)
x = self.bandpass(x)
# Step 2: Downsample to 8 kHz
x = self.resampler(x)
# Flatten batch dimensions for in-memory codec passes
orig_shape = x.shape
x_flat = x.reshape(-1, orig_shape[-1]) # (B, N)
target_len = orig_shape[-1]
processed_batch = []
for i in range(x_flat.shape[0]):
sig = x_flat[i].detach().cpu().numpy()
# Normalize to [-0.95, 0.95] to prevent codec clipping
max_abs = np.max(np.abs(sig))
if max_abs > 1e-4:
sig = sig * (0.95 / max_abs)
# Tandem Stage 1: Cellular GSM-FR 6.10
gsm_out = self._compress_gsm_single(sig)
# Ensure exact length alignment
if len(gsm_out) > target_len:
gsm_out = gsm_out[:target_len]
elif len(gsm_out) < target_len:
gsm_out = np.pad(gsm_out, (0, target_len - len(gsm_out)))
# Tandem Stage 2: VoIP G.711 µ-law or Opus
voip_out = self._compress_voip_single(gsm_out)
if len(voip_out) > target_len:
voip_out = voip_out[:target_len]
elif len(voip_out) < target_len:
voip_out = np.pad(voip_out, (0, target_len - len(voip_out)))
processed_batch.append(voip_out)
out_tensor = torch.from_numpy(np.stack(processed_batch)).to(device=x.device, dtype=x.dtype)
out_tensor = out_tensor.reshape(*orig_shape[:-1], target_len)
# Step 5: VoIP Packet loss simulation
out_tensor = self.packet_loss(out_tensor)
# Step 6: Peak normalization
max_val = torch.max(torch.abs(out_tensor), dim=-1, keepdim=True)[0]
max_val = torch.clamp(max_val, min=1e-6)
scale = torch.where(max_val > 0.01, 0.95 / max_val, torch.ones_like(max_val))
out_tensor = out_tensor * scale
return out_tensor