audio_features.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. """
  2. Audio feature extraction for Voice Recognition system.
  3. This module provides comprehensive audio feature extraction capabilities including
  4. Log-Mel spectrograms and MFCC features optimized for speaker recognition tasks.
  5. """
  6. import logging
  7. import warnings
  8. from typing import Optional, Tuple, Union, List, Dict, Any
  9. from dataclasses import dataclass
  10. import numpy as np
  11. import torch
  12. import torch.nn as nn
  13. import torch.nn.functional as F
  14. import torchaudio
  15. import torchaudio.transforms as T
  16. from pathlib import Path
  17. # Configure logging
  18. logger = logging.getLogger(__name__)
  19. @dataclass
  20. class LogMelConfig:
  21. """Configuration for Log-Mel spectrogram extraction."""
  22. sample_rate: int = 16000
  23. n_mels: int = 40
  24. n_fft: int = 512
  25. hop_length: int = 160 # 10ms at 16kHz
  26. win_length: int = 400 # 25ms at 16kHz
  27. f_min: float = 80.0
  28. f_max: Optional[float] = None
  29. power: float = 2.0
  30. normalized: bool = True
  31. center: bool = True
  32. pad_mode: str = "reflect"
  33. def __post_init__(self):
  34. """Validate configuration parameters."""
  35. if self.f_max is None:
  36. self.f_max = self.sample_rate // 2
  37. if self.f_min >= self.f_max:
  38. raise ValueError("f_min must be less than f_max")
  39. if self.hop_length > self.win_length:
  40. raise ValueError("hop_length must be <= win_length")
  41. if self.n_mels < 1:
  42. raise ValueError("n_mels must be positive")
  43. @dataclass
  44. class MFCCConfig:
  45. """Configuration for MFCC feature extraction."""
  46. sample_rate: int = 16000
  47. n_mfcc: int = 40
  48. n_mels: int = 80
  49. n_fft: int = 512
  50. hop_length: int = 160 # 10ms at 16kHz
  51. win_length: int = 400 # 25ms at 16kHz
  52. f_min: float = 80.0
  53. f_max: Optional[float] = None
  54. dct_type: int = 2
  55. norm: str = "ortho"
  56. log_mels: bool = False
  57. melkwargs: Dict[str, Any] = None
  58. def __post_init__(self):
  59. """Validate configuration and set defaults."""
  60. if self.f_max is None:
  61. self.f_max = self.sample_rate // 2
  62. if self.melkwargs is None:
  63. self.melkwargs = {}
  64. if self.n_mfcc > self.n_mels:
  65. raise ValueError("n_mfcc must be <= n_mels")
  66. if self.dct_type not in [1, 2, 3, 4]:
  67. raise ValueError("dct_type must be 1, 2, 3, or 4")
  68. class VRAudioFeatureExtractor(nn.Module):
  69. """
  70. Audio feature extractor for voice recognition.
  71. Supports both Log-Mel spectrograms and MFCC features with optional
  72. preprocessing and normalization.
  73. """
  74. def __init__(self,
  75. feature_type: str = "log_mel",
  76. log_mel_config: Optional[LogMelConfig] = None,
  77. mfcc_config: Optional[MFCCConfig] = None,
  78. normalize_features: bool = True,
  79. apply_cmvn: bool = True,
  80. add_deltas: bool = False,
  81. add_delta_deltas: bool = False):
  82. """
  83. Initialize audio feature extractor.
  84. Args:
  85. feature_type: Type of features to extract ("log_mel" or "mfcc")
  86. log_mel_config: Configuration for Log-Mel spectrogram
  87. mfcc_config: Configuration for MFCC features
  88. normalize_features: Whether to normalize features
  89. apply_cmvn: Whether to apply Cepstral Mean and Variance Normalization
  90. add_deltas: Whether to add delta features
  91. add_delta_deltas: Whether to add delta-delta features
  92. """
  93. super().__init__()
  94. self.feature_type = feature_type.lower()
  95. self.normalize_features = normalize_features
  96. self.apply_cmvn = apply_cmvn
  97. self.add_deltas = add_deltas
  98. self.add_delta_deltas = add_delta_deltas
  99. if self.feature_type == "log_mel":
  100. self.config = log_mel_config or LogMelConfig()
  101. self._setup_log_mel_transforms()
  102. elif self.feature_type == "mfcc":
  103. self.config = mfcc_config or MFCCConfig()
  104. self._setup_mfcc_transforms()
  105. else:
  106. raise ValueError(f"Unsupported feature type: {feature_type}")
  107. # Statistics for normalization
  108. self.register_buffer('feature_mean', torch.zeros(1))
  109. self.register_buffer('feature_std', torch.ones(1))
  110. self.register_buffer('num_updates', torch.zeros(1))
  111. # Delta computation
  112. if self.add_deltas or self.add_delta_deltas:
  113. self.delta_transform = T.ComputeDeltas()
  114. def _setup_log_mel_transforms(self):
  115. """Setup transforms for Log-Mel spectrogram extraction."""
  116. self.spectrogram = T.Spectrogram(
  117. n_fft=self.config.n_fft,
  118. hop_length=self.config.hop_length,
  119. win_length=self.config.win_length,
  120. power=self.config.power,
  121. normalized=self.config.normalized,
  122. center=self.config.center,
  123. pad_mode=self.config.pad_mode
  124. )
  125. self.mel_scale = T.MelScale(
  126. n_mels=self.config.n_mels,
  127. sample_rate=self.config.sample_rate,
  128. f_min=self.config.f_min,
  129. f_max=self.config.f_max,
  130. n_stft=self.config.n_fft // 2 + 1
  131. )
  132. self.amplitude_to_db = T.AmplitudeToDB()
  133. def _setup_mfcc_transforms(self):
  134. """Setup transforms for MFCC feature extraction."""
  135. self.mfcc_transform = T.MFCC(
  136. sample_rate=self.config.sample_rate,
  137. n_mfcc=self.config.n_mfcc,
  138. melkwargs={
  139. "n_fft": self.config.n_fft,
  140. "hop_length": self.config.hop_length,
  141. "win_length": self.config.win_length,
  142. "f_min": self.config.f_min,
  143. "f_max": self.config.f_max,
  144. "n_mels": self.config.n_mels,
  145. **self.config.melkwargs
  146. },
  147. dct_type=self.config.dct_type,
  148. norm=self.config.norm,
  149. log_mels=self.config.log_mels
  150. )
  151. def forward(self, waveform: torch.Tensor) -> torch.Tensor:
  152. """
  153. Extract features from audio waveform.
  154. Args:
  155. waveform: Input audio tensor of shape (batch_size, num_samples) or (num_samples,)
  156. Returns:
  157. features: Extracted features of shape (batch_size, n_features, n_frames)
  158. """
  159. # Ensure batch dimension
  160. if waveform.dim() == 1:
  161. waveform = waveform.unsqueeze(0)
  162. # Validate input
  163. if waveform.dim() != 2:
  164. raise ValueError(f"Expected 1D or 2D tensor, got {waveform.dim()}D")
  165. # Extract features based on type
  166. if self.feature_type == "log_mel":
  167. features = self._extract_log_mel(waveform)
  168. else: # mfcc
  169. features = self._extract_mfcc(waveform)
  170. # Add delta features if requested
  171. if self.add_deltas:
  172. deltas = self.delta_transform(features)
  173. features = torch.cat([features, deltas], dim=1)
  174. if self.add_delta_deltas:
  175. delta_deltas = self.delta_transform(deltas)
  176. features = torch.cat([features, delta_deltas], dim=1)
  177. # Apply CMVN if requested
  178. if self.apply_cmvn:
  179. features = self._apply_cmvn(features)
  180. # Normalize features if requested
  181. if self.normalize_features:
  182. features = self._normalize_features(features)
  183. return features
  184. def _extract_log_mel(self, waveform: torch.Tensor) -> torch.Tensor:
  185. """Extract Log-Mel spectrogram features."""
  186. # Compute spectrogram
  187. specgram = self.spectrogram(waveform)
  188. # Apply mel scale
  189. mel_specgram = self.mel_scale(specgram)
  190. # Convert to dB
  191. log_mel = self.amplitude_to_db(mel_specgram)
  192. return log_mel
  193. def _extract_mfcc(self, waveform: torch.Tensor) -> torch.Tensor:
  194. """Extract MFCC features."""
  195. return self.mfcc_transform(waveform)
  196. def _apply_cmvn(self, features: torch.Tensor) -> torch.Tensor:
  197. """Apply Cepstral Mean and Variance Normalization."""
  198. # Compute mean and std across time dimension
  199. mean = features.mean(dim=-1, keepdim=True)
  200. std = features.std(dim=-1, keepdim=True)
  201. # Avoid division by zero
  202. std = torch.clamp(std, min=1e-8)
  203. # Normalize
  204. features = (features - mean) / std
  205. return features
  206. def _normalize_features(self, features: torch.Tensor) -> torch.Tensor:
  207. """Normalize features using running statistics."""
  208. if self.training:
  209. # Update running statistics
  210. batch_mean = features.mean(dim=(0, 2), keepdim=True)
  211. batch_var = features.var(dim=(0, 2), keepdim=True, unbiased=False)
  212. # Update running mean and std
  213. momentum = 0.1
  214. self.feature_mean = (1 - momentum) * self.feature_mean + momentum * batch_mean.squeeze()
  215. # For std, we need to be careful with the square root
  216. batch_std = torch.sqrt(batch_var + 1e-8)
  217. if self.feature_std.numel() == 1: # First update
  218. self.feature_std = batch_std.squeeze()
  219. else:
  220. self.feature_std = (1 - momentum) * self.feature_std + momentum * batch_std.squeeze()
  221. self.num_updates += 1
  222. # Apply normalization
  223. if self.num_updates > 0:
  224. mean = self.feature_mean.view(1, -1, 1)
  225. std = torch.clamp(self.feature_std.view(1, -1, 1), min=1e-8)
  226. features = (features - mean) / std
  227. return features
  228. def get_feature_dim(self) -> int:
  229. """Get the output feature dimension."""
  230. if self.feature_type == "log_mel":
  231. base_dim = self.config.n_mels
  232. else: # mfcc
  233. base_dim = self.config.n_mfcc
  234. # Account for delta features
  235. multiplier = 1
  236. if self.add_deltas:
  237. multiplier += 1
  238. if self.add_delta_deltas:
  239. multiplier += 1
  240. return base_dim * multiplier
  241. def get_output_length(self, input_length: int) -> int:
  242. """Calculate output sequence length given input length."""
  243. if self.feature_type == "log_mel":
  244. hop_length = self.config.hop_length
  245. else: # mfcc
  246. hop_length = self.config.hop_length
  247. return (input_length + hop_length // 2) // hop_length
  248. def reset_stats(self):
  249. """Reset normalization statistics."""
  250. self.feature_mean.zero_()
  251. self.feature_std.fill_(1.0)
  252. self.num_updates.zero_()
  253. class BatchVRAudioFeatureExtractor(nn.Module):
  254. """
  255. Batch-optimized version of the audio feature extractor.
  256. Designed for efficient processing of multiple audio samples simultaneously.
  257. """
  258. def __init__(self,
  259. feature_extractor: VRAudioFeatureExtractor,
  260. fixed_length: Optional[int] = None,
  261. padding_mode: str = "constant"):
  262. """
  263. Initialize batch feature extractor.
  264. Args:
  265. feature_extractor: Base feature extractor to use
  266. fixed_length: Fixed output length (for padding/truncation)
  267. padding_mode: Padding mode for sequences ("constant", "reflect", "replicate")
  268. """
  269. super().__init__()
  270. self.feature_extractor = feature_extractor
  271. self.fixed_length = fixed_length
  272. self.padding_mode = padding_mode
  273. def forward(self, waveforms: torch.Tensor, lengths: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, torch.Tensor]:
  274. """
  275. Extract features from batch of waveforms.
  276. Args:
  277. waveforms: Batch of waveforms [batch_size, max_length]
  278. lengths: Actual lengths of each waveform [batch_size]
  279. Returns:
  280. features: Extracted features [batch_size, n_features, n_frames]
  281. feature_lengths: Lengths of feature sequences [batch_size]
  282. """
  283. batch_size = waveforms.size(0)
  284. # Process each waveform
  285. features_list = []
  286. feature_lengths = []
  287. for i in range(batch_size):
  288. waveform = waveforms[i]
  289. # Trim to actual length if provided
  290. if lengths is not None:
  291. waveform = waveform[:lengths[i]]
  292. # Extract features
  293. features = self.feature_extractor(waveform)
  294. features_list.append(features.squeeze(0))
  295. feature_lengths.append(features.size(-1))
  296. # Convert to tensor
  297. feature_lengths = torch.tensor(feature_lengths, device=waveforms.device)
  298. # Pad/truncate to fixed length if specified
  299. if self.fixed_length is not None:
  300. max_length = self.fixed_length
  301. else:
  302. max_length = max(feature_lengths.tolist())
  303. # Pad features to same length
  304. padded_features = []
  305. for features in features_list:
  306. if features.size(-1) < max_length:
  307. # Pad
  308. pad_amount = max_length - features.size(-1)
  309. if self.padding_mode == "constant":
  310. padded = F.pad(features, (0, pad_amount), mode='constant', value=0)
  311. else:
  312. padded = F.pad(features, (0, pad_amount), mode=self.padding_mode)
  313. elif features.size(-1) > max_length:
  314. # Truncate
  315. padded = features[:, :max_length]
  316. else:
  317. padded = features
  318. padded_features.append(padded)
  319. # Stack into batch
  320. batch_features = torch.stack(padded_features, dim=0)
  321. return batch_features, feature_lengths
  322. def create_feature_extractor(feature_type: str,
  323. sample_rate: int = 16000,
  324. **kwargs) -> VRAudioFeatureExtractor:
  325. """
  326. Factory function to create feature extractor with common configurations.
  327. Args:
  328. feature_type: Type of features ("log_mel" or "mfcc")
  329. sample_rate: Audio sample rate
  330. **kwargs: Additional configuration parameters
  331. Returns:
  332. Configured feature extractor
  333. """
  334. if feature_type.lower() == "log_mel":
  335. config = LogMelConfig(sample_rate=sample_rate, **kwargs)
  336. return VRAudioFeatureExtractor(
  337. feature_type="log_mel",
  338. log_mel_config=config
  339. )
  340. elif feature_type.lower() == "mfcc":
  341. config = MFCCConfig(sample_rate=sample_rate, **kwargs)
  342. return VRAudioFeatureExtractor(
  343. feature_type="mfcc",
  344. mfcc_config=config
  345. )
  346. else:
  347. raise ValueError(f"Unsupported feature type: {feature_type}")
  348. def validate_feature_config(config: Union[LogMelConfig, MFCCConfig]) -> List[str]:
  349. """
  350. Validate feature extraction configuration.
  351. Args:
  352. config: Feature extraction configuration
  353. Returns:
  354. List of validation warnings
  355. """
  356. warnings = []
  357. if isinstance(config, LogMelConfig):
  358. # Check for common issues with Log-Mel configuration
  359. if config.n_mels < 20:
  360. warnings.append("Low number of mel bins may reduce feature quality")
  361. if config.n_mels > 128:
  362. warnings.append("High number of mel bins may be redundant")
  363. # Check frequency range
  364. nyquist = config.sample_rate // 2
  365. if config.f_max > nyquist:
  366. warnings.append(f"f_max ({config.f_max}) exceeds Nyquist frequency ({nyquist})")
  367. # Check hop length
  368. hop_ms = (config.hop_length / config.sample_rate) * 1000
  369. if hop_ms > 20:
  370. warnings.append(f"Large hop length ({hop_ms:.1f}ms) may reduce temporal resolution")
  371. elif isinstance(config, MFCCConfig):
  372. # Check for common issues with MFCC configuration
  373. if config.n_mfcc < 12:
  374. warnings.append("Low number of MFCC coefficients may reduce feature quality")
  375. if config.n_mfcc > config.n_mels:
  376. warnings.append("n_mfcc should not exceed n_mels")
  377. # Check mel filterbank configuration
  378. if config.n_mels < config.n_mfcc * 2:
  379. warnings.append("n_mels should be at least 2x n_mfcc for good frequency resolution")
  380. return warnings
  381. def compute_feature_statistics(features: torch.Tensor) -> Dict[str, float]:
  382. """
  383. Compute statistics for feature analysis.
  384. Args:
  385. features: Feature tensor [batch_size, n_features, n_frames]
  386. Returns:
  387. Dictionary of feature statistics
  388. """
  389. with torch.no_grad():
  390. # Flatten spatial dimensions
  391. flat_features = features.view(features.size(0), -1)
  392. stats = {
  393. 'mean': float(flat_features.mean()),
  394. 'std': float(flat_features.std()),
  395. 'min': float(flat_features.min()),
  396. 'max': float(flat_features.max()),
  397. 'dynamic_range': float(flat_features.max() - flat_features.min()),
  398. 'zero_crossing_rate': float((torch.diff(torch.sign(flat_features)) != 0).float().mean()),
  399. 'sparsity': float((flat_features.abs() < 1e-6).float().mean())
  400. }
  401. # Compute per-feature statistics
  402. feature_means = features.mean(dim=(0, 2))
  403. feature_stds = features.std(dim=(0, 2))
  404. stats['feature_mean_range'] = float(feature_means.max() - feature_means.min())
  405. stats['feature_std_range'] = float(feature_stds.max() - feature_stds.min())
  406. return stats
  407. # Example usage and testing functions
  408. def test_feature_extraction():
  409. """Test feature extraction with sample audio."""
  410. # Create sample audio
  411. sample_rate = 16000
  412. duration = 3.0
  413. num_samples = int(sample_rate * duration)
  414. # Generate test signal (sine wave + noise)
  415. t = torch.linspace(0, duration, num_samples)
  416. signal = torch.sin(2 * torch.pi * 440 * t) + 0.1 * torch.randn(num_samples)
  417. # Test Log-Mel extraction
  418. log_mel_extractor = create_feature_extractor("log_mel", sample_rate=sample_rate)
  419. log_mel_features = log_mel_extractor(signal)
  420. print(f"Log-Mel features shape: {log_mel_features.shape}")
  421. print(f"Log-Mel stats: {compute_feature_statistics(log_mel_features.unsqueeze(0))}")
  422. # Test MFCC extraction
  423. mfcc_extractor = create_feature_extractor("mfcc", sample_rate=sample_rate)
  424. mfcc_features = mfcc_extractor(signal)
  425. print(f"MFCC features shape: {mfcc_features.shape}")
  426. print(f"MFCC stats: {compute_feature_statistics(mfcc_features.unsqueeze(0))}")
  427. # Test batch processing
  428. batch_waveforms = signal.unsqueeze(0).repeat(4, 1)
  429. batch_extractor = BatchVRAudioFeatureExtractor(log_mel_extractor, fixed_length=100)
  430. batch_features, batch_lengths = batch_extractor(batch_waveforms)
  431. print(f"Batch features shape: {batch_features.shape}")
  432. print(f"Batch lengths: {batch_lengths}")
  433. if __name__ == "__main__":
  434. test_feature_extraction()