audio_pipeline.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. """
  2. Audio Pipeline Management for Trixy ML Integration.
  3. This module provides comprehensive audio pipeline management for real-time processing
  4. in the Trixy voice assistant. It handles audio stream management, buffering,
  5. format conversion, and integration with ML components.
  6. Key Features:
  7. - Real-time audio stream processing
  8. - Audio buffer management with circular buffers
  9. - Format conversion and resampling
  10. - Volume analysis and silence detection
  11. - Audio quality monitoring
  12. - Thread-safe audio handling
  13. - Integration with wakeword and voice recognition
  14. - Performance optimization for low-latency processing
  15. Usage:
  16. from trixy_core.ml import AudioPipeline, AudioPipelineConfig
  17. # Create audio pipeline
  18. config = AudioPipelineConfig(
  19. sample_rate=16000,
  20. chunk_size=1024,
  21. buffer_duration=10.0
  22. )
  23. pipeline = AudioPipeline(config, ml_manager)
  24. # Start processing
  25. pipeline.start()
  26. # Feed audio data
  27. pipeline.process_audio_chunk(audio_data)
  28. """
  29. import asyncio
  30. import logging
  31. import threading
  32. import time
  33. import queue
  34. from typing import Optional, Dict, Any, List, Callable, Union, Tuple
  35. from dataclasses import dataclass, field
  36. from enum import Enum
  37. import numpy as np
  38. from collections import deque
  39. from .performance_monitor import PerformanceMonitor, MetricType
  40. # Audio processing libraries (with fallbacks)
  41. try:
  42. import librosa
  43. import soundfile as sf
  44. AUDIO_LIBS_AVAILABLE = True
  45. except ImportError:
  46. AUDIO_LIBS_AVAILABLE = False
  47. librosa = None
  48. sf = None
  49. class AudioFormat(Enum):
  50. """Supported audio formats."""
  51. INT16 = "int16"
  52. INT32 = "int32"
  53. FLOAT32 = "float32"
  54. FLOAT64 = "float64"
  55. class AudioPipelineState(Enum):
  56. """Audio pipeline processing states."""
  57. STOPPED = "stopped"
  58. STARTING = "starting"
  59. RUNNING = "running"
  60. PAUSED = "paused"
  61. ERROR = "error"
  62. class BufferType(Enum):
  63. """Audio buffer types."""
  64. CIRCULAR = "circular"
  65. LINEAR = "linear"
  66. DYNAMIC = "dynamic"
  67. @dataclass
  68. class AudioPipelineConfig:
  69. """Configuration for audio pipeline."""
  70. # Basic audio settings
  71. sample_rate: int = 16000
  72. chunk_size: int = 1024
  73. channels: int = 1
  74. audio_format: AudioFormat = AudioFormat.FLOAT32
  75. # Buffer configuration
  76. buffer_type: BufferType = BufferType.CIRCULAR
  77. buffer_duration_seconds: float = 10.0
  78. max_buffer_size_mb: float = 100.0
  79. # Processing configuration
  80. enable_resampling: bool = True
  81. enable_format_conversion: bool = True
  82. enable_volume_analysis: bool = True
  83. enable_silence_detection: bool = True
  84. # Silence detection
  85. silence_threshold: float = 0.01
  86. silence_duration_threshold: float = 0.5
  87. # Performance settings
  88. processing_threads: int = 2
  89. max_queue_size: int = 1000
  90. processing_timeout: float = 1.0
  91. # Quality monitoring
  92. enable_quality_monitoring: bool = True
  93. quality_check_interval: float = 1.0
  94. # Debug settings
  95. enable_debug_logging: bool = False
  96. save_debug_audio: bool = False
  97. debug_audio_path: str = "/tmp/trixy_audio_debug"
  98. @dataclass
  99. class AudioChunk:
  100. """Audio chunk with metadata."""
  101. data: np.ndarray
  102. timestamp: float
  103. sample_rate: int
  104. channels: int
  105. format: AudioFormat
  106. sequence_number: int = 0
  107. volume: float = 0.0
  108. is_silence: bool = False
  109. quality_score: float = 1.0
  110. @dataclass
  111. class AudioQualityMetrics:
  112. """Audio quality metrics."""
  113. sample_rate: int
  114. bit_depth: int
  115. channels: int
  116. duration_seconds: float
  117. rms_level: float
  118. peak_level: float
  119. snr_estimate: float
  120. dynamic_range: float
  121. silence_ratio: float
  122. distortion_estimate: float
  123. timestamp: float = field(default_factory=time.time)
  124. class CircularAudioBuffer:
  125. """Thread-safe circular audio buffer."""
  126. def __init__(self, max_duration: float, sample_rate: int, channels: int = 1):
  127. """
  128. Initialize circular buffer.
  129. Args:
  130. max_duration: Maximum buffer duration in seconds
  131. sample_rate: Audio sample rate
  132. channels: Number of audio channels
  133. """
  134. self.max_duration = max_duration
  135. self.sample_rate = sample_rate
  136. self.channels = channels
  137. self.max_samples = int(max_duration * sample_rate * channels)
  138. # Initialize buffer
  139. self._buffer = np.zeros(self.max_samples, dtype=np.float32)
  140. self._write_pos = 0
  141. self._read_pos = 0
  142. self._size = 0
  143. self._lock = threading.RLock()
  144. # Metadata tracking
  145. self._timestamps = deque(maxlen=1000)
  146. self._sequence_numbers = deque(maxlen=1000)
  147. self._current_sequence = 0
  148. def write(self, audio_data: np.ndarray, timestamp: float = None) -> bool:
  149. """
  150. Write audio data to buffer.
  151. Args:
  152. audio_data: Audio data to write
  153. timestamp: Timestamp of the audio data
  154. Returns:
  155. True if write was successful
  156. """
  157. if timestamp is None:
  158. timestamp = time.time()
  159. with self._lock:
  160. data_length = len(audio_data)
  161. # Check if we have space
  162. if data_length > self.max_samples:
  163. return False
  164. # Write data (may wrap around)
  165. end_pos = self._write_pos + data_length
  166. if end_pos <= self.max_samples:
  167. # No wrap around
  168. self._buffer[self._write_pos:end_pos] = audio_data
  169. else:
  170. # Wrap around
  171. first_part = self.max_samples - self._write_pos
  172. self._buffer[self._write_pos:] = audio_data[:first_part]
  173. self._buffer[:end_pos - self.max_samples] = audio_data[first_part:]
  174. # Update positions
  175. self._write_pos = end_pos % self.max_samples
  176. self._size = min(self._size + data_length, self.max_samples)
  177. # Update metadata
  178. self._timestamps.append(timestamp)
  179. self._sequence_numbers.append(self._current_sequence)
  180. self._current_sequence += 1
  181. return True
  182. def read(self, num_samples: int) -> Optional[np.ndarray]:
  183. """
  184. Read audio data from buffer.
  185. Args:
  186. num_samples: Number of samples to read
  187. Returns:
  188. Audio data or None if not enough data
  189. """
  190. with self._lock:
  191. if num_samples > self._size:
  192. return None
  193. # Read data (may wrap around)
  194. end_pos = self._read_pos + num_samples
  195. if end_pos <= self.max_samples:
  196. # No wrap around
  197. data = self._buffer[self._read_pos:end_pos].copy()
  198. else:
  199. # Wrap around
  200. first_part = self.max_samples - self._read_pos
  201. data = np.concatenate([
  202. self._buffer[self._read_pos:],
  203. self._buffer[:end_pos - self.max_samples]
  204. ])
  205. # Update position
  206. self._read_pos = end_pos % self.max_samples
  207. self._size -= num_samples
  208. return data
  209. def peek(self, num_samples: int, offset: int = 0) -> Optional[np.ndarray]:
  210. """
  211. Peek at buffer data without consuming it.
  212. Args:
  213. num_samples: Number of samples to peek
  214. offset: Offset from current read position
  215. Returns:
  216. Audio data or None if not enough data
  217. """
  218. with self._lock:
  219. if num_samples + offset > self._size:
  220. return None
  221. start_pos = (self._read_pos + offset) % self.max_samples
  222. end_pos = (start_pos + num_samples) % self.max_samples
  223. if end_pos > start_pos:
  224. # No wrap around
  225. data = self._buffer[start_pos:end_pos].copy()
  226. else:
  227. # Wrap around
  228. data = np.concatenate([
  229. self._buffer[start_pos:],
  230. self._buffer[:end_pos]
  231. ])
  232. return data
  233. def available_samples(self) -> int:
  234. """Get number of available samples."""
  235. with self._lock:
  236. return self._size
  237. def available_duration(self) -> float:
  238. """Get available duration in seconds."""
  239. return self.available_samples() / self.sample_rate / self.channels
  240. def clear(self) -> None:
  241. """Clear the buffer."""
  242. with self._lock:
  243. self._write_pos = 0
  244. self._read_pos = 0
  245. self._size = 0
  246. self._timestamps.clear()
  247. self._sequence_numbers.clear()
  248. class AudioProcessor:
  249. """Audio processing utilities."""
  250. @staticmethod
  251. def convert_format(audio_data: np.ndarray, target_format: AudioFormat) -> np.ndarray:
  252. """
  253. Convert audio data to target format.
  254. Args:
  255. audio_data: Input audio data
  256. target_format: Target audio format
  257. Returns:
  258. Converted audio data
  259. """
  260. if target_format == AudioFormat.INT16:
  261. return (audio_data * 32767).astype(np.int16)
  262. elif target_format == AudioFormat.INT32:
  263. return (audio_data * 2147483647).astype(np.int32)
  264. elif target_format == AudioFormat.FLOAT32:
  265. return audio_data.astype(np.float32)
  266. elif target_format == AudioFormat.FLOAT64:
  267. return audio_data.astype(np.float64)
  268. else:
  269. return audio_data
  270. @staticmethod
  271. def normalize_audio(audio_data: np.ndarray, target_level: float = 0.7) -> np.ndarray:
  272. """
  273. Normalize audio to target level.
  274. Args:
  275. audio_data: Input audio data
  276. target_level: Target RMS level (0.0 to 1.0)
  277. Returns:
  278. Normalized audio data
  279. """
  280. current_level = np.sqrt(np.mean(audio_data ** 2))
  281. if current_level > 0:
  282. scale_factor = target_level / current_level
  283. return audio_data * scale_factor
  284. return audio_data
  285. @staticmethod
  286. def resample_audio(audio_data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
  287. """
  288. Resample audio data.
  289. Args:
  290. audio_data: Input audio data
  291. orig_sr: Original sample rate
  292. target_sr: Target sample rate
  293. Returns:
  294. Resampled audio data
  295. """
  296. if not AUDIO_LIBS_AVAILABLE or orig_sr == target_sr:
  297. return audio_data
  298. try:
  299. return librosa.resample(audio_data, orig_sr=orig_sr, target_sr=target_sr)
  300. except Exception:
  301. # Fallback to simple resampling
  302. ratio = target_sr / orig_sr
  303. new_length = int(len(audio_data) * ratio)
  304. return np.interp(
  305. np.linspace(0, len(audio_data) - 1, new_length),
  306. np.arange(len(audio_data)),
  307. audio_data
  308. )
  309. @staticmethod
  310. def calculate_volume(audio_data: np.ndarray) -> float:
  311. """
  312. Calculate audio volume (RMS).
  313. Args:
  314. audio_data: Input audio data
  315. Returns:
  316. Volume level (0.0 to 1.0)
  317. """
  318. return float(np.sqrt(np.mean(audio_data ** 2)))
  319. @staticmethod
  320. def detect_silence(audio_data: np.ndarray, threshold: float = 0.01) -> bool:
  321. """
  322. Detect silence in audio data.
  323. Args:
  324. audio_data: Input audio data
  325. threshold: Silence threshold
  326. Returns:
  327. True if silence detected
  328. """
  329. volume = AudioProcessor.calculate_volume(audio_data)
  330. return volume < threshold
  331. @staticmethod
  332. def calculate_quality_metrics(audio_data: np.ndarray, sample_rate: int) -> AudioQualityMetrics:
  333. """
  334. Calculate audio quality metrics.
  335. Args:
  336. audio_data: Input audio data
  337. sample_rate: Audio sample rate
  338. Returns:
  339. Audio quality metrics
  340. """
  341. duration = len(audio_data) / sample_rate
  342. rms_level = np.sqrt(np.mean(audio_data ** 2))
  343. peak_level = np.max(np.abs(audio_data))
  344. # Estimate SNR (simplified)
  345. signal_power = np.mean(audio_data ** 2)
  346. noise_estimate = np.var(audio_data - np.mean(audio_data))
  347. snr_estimate = 10 * np.log10(signal_power / (noise_estimate + 1e-10))
  348. # Dynamic range
  349. dynamic_range = 20 * np.log10(peak_level / (rms_level + 1e-10))
  350. # Silence ratio
  351. silence_samples = np.sum(np.abs(audio_data) < 0.01)
  352. silence_ratio = silence_samples / len(audio_data)
  353. # Distortion estimate (simplified THD)
  354. distortion_estimate = 0.0 # Placeholder for more complex analysis
  355. return AudioQualityMetrics(
  356. sample_rate=sample_rate,
  357. bit_depth=32, # Assuming float32
  358. channels=1,
  359. duration_seconds=duration,
  360. rms_level=rms_level,
  361. peak_level=peak_level,
  362. snr_estimate=snr_estimate,
  363. dynamic_range=dynamic_range,
  364. silence_ratio=silence_ratio,
  365. distortion_estimate=distortion_estimate
  366. )
  367. class AudioPipelineError(Exception):
  368. """Audio pipeline exception."""
  369. pass
  370. class AudioPipeline:
  371. """
  372. Audio pipeline for real-time audio processing in Trixy ML integration.
  373. This class manages the complete audio processing pipeline from input
  374. to ML component integration, handling buffering, format conversion,
  375. and quality monitoring.
  376. """
  377. def __init__(
  378. self,
  379. config: AudioPipelineConfig,
  380. ml_manager: Optional[Any] = None,
  381. logger: Optional[logging.Logger] = None
  382. ):
  383. """
  384. Initialize audio pipeline.
  385. Args:
  386. config: Audio pipeline configuration
  387. ml_manager: ML manager for processing integration
  388. logger: Logger instance
  389. """
  390. self.config = config
  391. self.ml_manager = ml_manager
  392. self.logger = logger or logging.getLogger(__name__)
  393. # State management
  394. self._state = AudioPipelineState.STOPPED
  395. self._state_lock = threading.RLock()
  396. # Audio buffer
  397. self._audio_buffer = CircularAudioBuffer(
  398. max_duration=config.buffer_duration_seconds,
  399. sample_rate=config.sample_rate,
  400. channels=config.channels
  401. )
  402. # Processing queue
  403. self._processing_queue = queue.Queue(maxsize=config.max_queue_size)
  404. self._processing_threads: List[threading.Thread] = []
  405. self._stop_event = threading.Event()
  406. # Sequence tracking
  407. self._sequence_counter = 0
  408. self._sequence_lock = threading.Lock()
  409. # Quality monitoring
  410. self._quality_metrics: List[AudioQualityMetrics] = []
  411. self._quality_lock = threading.Lock()
  412. self._last_quality_check = 0.0
  413. # Performance metrics
  414. self._performance_stats = {
  415. "chunks_processed": 0,
  416. "chunks_dropped": 0,
  417. "processing_errors": 0,
  418. "average_processing_time": 0.0,
  419. "buffer_overruns": 0,
  420. "format_conversions": 0,
  421. "resampling_operations": 0
  422. }
  423. # Callbacks
  424. self.on_audio_processed: Optional[Callable] = None
  425. self.on_silence_detected: Optional[Callable] = None
  426. self.on_quality_alert: Optional[Callable] = None
  427. self.on_error: Optional[Callable] = None
  428. self.logger.info("Audio pipeline initialized")
  429. def start(self) -> None:
  430. """Start the audio pipeline."""
  431. with self._state_lock:
  432. if self._state != AudioPipelineState.STOPPED:
  433. self.logger.warning(f"Audio pipeline already running (state: {self._state.value})")
  434. return
  435. self.logger.info("Starting audio pipeline...")
  436. self._state = AudioPipelineState.STARTING
  437. try:
  438. # Clear stop event
  439. self._stop_event.clear()
  440. # Start processing threads
  441. for i in range(self.config.processing_threads):
  442. thread = threading.Thread(
  443. target=self._processing_worker,
  444. name=f"AudioPipeline-Worker-{i}",
  445. daemon=True
  446. )
  447. thread.start()
  448. self._processing_threads.append(thread)
  449. # Start quality monitoring if enabled
  450. if self.config.enable_quality_monitoring:
  451. quality_thread = threading.Thread(
  452. target=self._quality_monitoring_worker,
  453. name="AudioPipeline-QualityMonitor",
  454. daemon=True
  455. )
  456. quality_thread.start()
  457. self._processing_threads.append(quality_thread)
  458. self._state = AudioPipelineState.RUNNING
  459. self.logger.info("Audio pipeline started successfully")
  460. except Exception as e:
  461. self._state = AudioPipelineState.ERROR
  462. self.logger.error(f"Failed to start audio pipeline: {e}")
  463. if self.on_error:
  464. self.on_error(f"Start failed: {e}")
  465. raise AudioPipelineError(f"Failed to start audio pipeline: {e}")
  466. def stop(self) -> None:
  467. """Stop the audio pipeline."""
  468. with self._state_lock:
  469. if self._state == AudioPipelineState.STOPPED:
  470. return
  471. self.logger.info("Stopping audio pipeline...")
  472. self._state = AudioPipelineState.STOPPED
  473. # Signal stop to all threads
  474. self._stop_event.set()
  475. # Wait for threads to complete
  476. for thread in self._processing_threads:
  477. if thread.is_alive():
  478. thread.join(timeout=2.0)
  479. # Clear threads list
  480. self._processing_threads.clear()
  481. # Clear buffers and queues
  482. self._audio_buffer.clear()
  483. # Clear processing queue
  484. while not self._processing_queue.empty():
  485. try:
  486. self._processing_queue.get_nowait()
  487. except queue.Empty:
  488. break
  489. self.logger.info("Audio pipeline stopped")
  490. def process_audio_chunk(self, audio_data: np.ndarray, timestamp: float = None) -> bool:
  491. """
  492. Process an audio chunk through the pipeline.
  493. Args:
  494. audio_data: Audio data to process
  495. timestamp: Timestamp of the audio data
  496. Returns:
  497. True if processing was successful
  498. """
  499. if self._state != AudioPipelineState.RUNNING:
  500. return False
  501. if timestamp is None:
  502. timestamp = time.time()
  503. try:
  504. # Create audio chunk with metadata
  505. with self._sequence_lock:
  506. sequence_number = self._sequence_counter
  507. self._sequence_counter += 1
  508. chunk = AudioChunk(
  509. data=audio_data.copy(),
  510. timestamp=timestamp,
  511. sample_rate=self.config.sample_rate,
  512. channels=self.config.channels,
  513. format=self.config.audio_format,
  514. sequence_number=sequence_number
  515. )
  516. # Add to processing queue
  517. try:
  518. self._processing_queue.put(chunk, block=False)
  519. return True
  520. except queue.Full:
  521. self.logger.warning("Audio processing queue full, dropping chunk")
  522. self._performance_stats["chunks_dropped"] += 1
  523. return False
  524. except Exception as e:
  525. self.logger.error(f"Error processing audio chunk: {e}")
  526. self._performance_stats["processing_errors"] += 1
  527. return False
  528. def get_audio_buffer(self, duration: float) -> Optional[np.ndarray]:
  529. """
  530. Get audio data from buffer.
  531. Args:
  532. duration: Duration in seconds to retrieve
  533. Returns:
  534. Audio data or None if not enough data
  535. """
  536. num_samples = int(duration * self.config.sample_rate * self.config.channels)
  537. return self._audio_buffer.read(num_samples)
  538. def peek_audio_buffer(self, duration: float, offset: float = 0.0) -> Optional[np.ndarray]:
  539. """
  540. Peek at audio buffer without consuming data.
  541. Args:
  542. duration: Duration in seconds to peek
  543. offset: Offset in seconds from current position
  544. Returns:
  545. Audio data or None if not enough data
  546. """
  547. num_samples = int(duration * self.config.sample_rate * self.config.channels)
  548. offset_samples = int(offset * self.config.sample_rate * self.config.channels)
  549. return self._audio_buffer.peek(num_samples, offset_samples)
  550. def get_buffer_status(self) -> Dict[str, Any]:
  551. """Get audio buffer status."""
  552. return {
  553. "available_duration": self._audio_buffer.available_duration(),
  554. "available_samples": self._audio_buffer.available_samples(),
  555. "max_duration": self._audio_buffer.max_duration,
  556. "sample_rate": self._audio_buffer.sample_rate,
  557. "channels": self._audio_buffer.channels
  558. }
  559. def get_performance_stats(self) -> Dict[str, Any]:
  560. """Get performance statistics."""
  561. return self._performance_stats.copy()
  562. def get_quality_metrics(self) -> List[AudioQualityMetrics]:
  563. """Get recent quality metrics."""
  564. with self._quality_lock:
  565. return self._quality_metrics.copy()
  566. def clear_buffers(self) -> None:
  567. """Clear all audio buffers."""
  568. self._audio_buffer.clear()
  569. self.logger.info("Audio buffers cleared")
  570. def _processing_worker(self) -> None:
  571. """Audio processing worker thread."""
  572. self.logger.info("Audio processing worker started")
  573. while not self._stop_event.is_set():
  574. try:
  575. # Get chunk from queue
  576. try:
  577. chunk = self._processing_queue.get(timeout=0.1)
  578. except queue.Empty:
  579. continue
  580. # Process the chunk
  581. start_time = time.time()
  582. processed_chunk = self._process_chunk(chunk)
  583. processing_time = time.time() - start_time
  584. # Update performance stats
  585. self._performance_stats["chunks_processed"] += 1
  586. self._performance_stats["average_processing_time"] = (
  587. (self._performance_stats["average_processing_time"] *
  588. (self._performance_stats["chunks_processed"] - 1) + processing_time) /
  589. self._performance_stats["chunks_processed"]
  590. )
  591. # Store in buffer
  592. self._audio_buffer.write(processed_chunk.data, processed_chunk.timestamp)
  593. # Send to ML manager if available
  594. if self.ml_manager and hasattr(self.ml_manager, 'process_audio_chunk'):
  595. asyncio.run(self.ml_manager.process_audio_chunk(processed_chunk.data))
  596. # Call callback if set
  597. if self.on_audio_processed:
  598. self.on_audio_processed(processed_chunk)
  599. # Mark task done
  600. self._processing_queue.task_done()
  601. except Exception as e:
  602. self.logger.error(f"Error in audio processing worker: {e}")
  603. self._performance_stats["processing_errors"] += 1
  604. time.sleep(0.01) # Prevent tight error loop
  605. self.logger.info("Audio processing worker stopped")
  606. def _process_chunk(self, chunk: AudioChunk) -> AudioChunk:
  607. """
  608. Process an individual audio chunk.
  609. Args:
  610. chunk: Audio chunk to process
  611. Returns:
  612. Processed audio chunk
  613. """
  614. processed_data = chunk.data.copy()
  615. # Format conversion if needed
  616. if self.config.enable_format_conversion:
  617. processed_data = AudioProcessor.convert_format(processed_data, self.config.audio_format)
  618. self._performance_stats["format_conversions"] += 1
  619. # Resampling if needed
  620. if self.config.enable_resampling and chunk.sample_rate != self.config.sample_rate:
  621. processed_data = AudioProcessor.resample_audio(
  622. processed_data,
  623. chunk.sample_rate,
  624. self.config.sample_rate
  625. )
  626. self._performance_stats["resampling_operations"] += 1
  627. # Calculate volume if enabled
  628. volume = 0.0
  629. if self.config.enable_volume_analysis:
  630. volume = AudioProcessor.calculate_volume(processed_data)
  631. # Detect silence if enabled
  632. is_silence = False
  633. if self.config.enable_silence_detection:
  634. is_silence = AudioProcessor.detect_silence(processed_data, self.config.silence_threshold)
  635. if is_silence and self.on_silence_detected:
  636. self.on_silence_detected(chunk)
  637. # Create processed chunk
  638. processed_chunk = AudioChunk(
  639. data=processed_data,
  640. timestamp=chunk.timestamp,
  641. sample_rate=self.config.sample_rate,
  642. channels=self.config.channels,
  643. format=self.config.audio_format,
  644. sequence_number=chunk.sequence_number,
  645. volume=volume,
  646. is_silence=is_silence
  647. )
  648. return processed_chunk
  649. def _quality_monitoring_worker(self) -> None:
  650. """Quality monitoring worker thread."""
  651. self.logger.info("Quality monitoring worker started")
  652. while not self._stop_event.is_set():
  653. try:
  654. current_time = time.time()
  655. # Check if it's time for quality analysis
  656. if current_time - self._last_quality_check >= self.config.quality_check_interval:
  657. self._perform_quality_check()
  658. self._last_quality_check = current_time
  659. # Wait before next check
  660. self._stop_event.wait(0.1)
  661. except Exception as e:
  662. self.logger.error(f"Error in quality monitoring: {e}")
  663. time.sleep(1.0)
  664. self.logger.info("Quality monitoring worker stopped")
  665. def _perform_quality_check(self) -> None:
  666. """Perform audio quality check."""
  667. try:
  668. # Get audio data for analysis
  669. analysis_duration = 1.0 # Analyze 1 second of audio
  670. audio_data = self.peek_audio_buffer(analysis_duration)
  671. if audio_data is None or len(audio_data) == 0:
  672. return
  673. # Calculate quality metrics
  674. metrics = AudioProcessor.calculate_quality_metrics(audio_data, self.config.sample_rate)
  675. # Store metrics
  676. with self._quality_lock:
  677. self._quality_metrics.append(metrics)
  678. # Keep only last 100 measurements
  679. if len(self._quality_metrics) > 100:
  680. self._quality_metrics.pop(0)
  681. # Check for quality alerts
  682. self._check_quality_alerts(metrics)
  683. except Exception as e:
  684. self.logger.error(f"Error performing quality check: {e}")
  685. def _check_quality_alerts(self, metrics: AudioQualityMetrics) -> None:
  686. """Check for quality alerts and trigger callbacks."""
  687. alerts = []
  688. # Check SNR
  689. if metrics.snr_estimate < 20.0: # SNR below 20dB
  690. alerts.append(f"Low SNR: {metrics.snr_estimate:.1f}dB")
  691. # Check silence ratio
  692. if metrics.silence_ratio > 0.9: # More than 90% silence
  693. alerts.append(f"High silence ratio: {metrics.silence_ratio:.1%}")
  694. # Check peak level
  695. if metrics.peak_level > 0.95: # Potential clipping
  696. alerts.append(f"Potential clipping: {metrics.peak_level:.3f}")
  697. elif metrics.peak_level < 0.01: # Very low signal
  698. alerts.append(f"Very low signal: {metrics.peak_level:.3f}")
  699. # Trigger alerts if any
  700. if alerts and self.on_quality_alert:
  701. self.on_quality_alert(metrics, alerts)
  702. def create_audio_pipeline(
  703. config: AudioPipelineConfig,
  704. ml_manager: Optional[Any] = None,
  705. logger: Optional[logging.Logger] = None
  706. ) -> AudioPipeline:
  707. """
  708. Factory function to create audio pipeline.
  709. Args:
  710. config: Audio pipeline configuration
  711. ml_manager: ML manager for processing integration
  712. logger: Logger instance
  713. Returns:
  714. Configured audio pipeline
  715. """
  716. return AudioPipeline(config, ml_manager, logger)
  717. def create_default_audio_config() -> AudioPipelineConfig:
  718. """
  719. Create default audio pipeline configuration.
  720. Returns:
  721. Default audio pipeline configuration
  722. """
  723. return AudioPipelineConfig()
  724. def create_optimized_audio_config() -> AudioPipelineConfig:
  725. """
  726. Create optimized audio pipeline configuration for production.
  727. Returns:
  728. Optimized audio pipeline configuration
  729. """
  730. config = AudioPipelineConfig()
  731. config.processing_threads = 4
  732. config.max_queue_size = 2000
  733. config.enable_quality_monitoring = True
  734. config.quality_check_interval = 0.5
  735. return config
  736. def create_debug_audio_config() -> AudioPipelineConfig:
  737. """
  738. Create debug audio pipeline configuration.
  739. Returns:
  740. Debug audio pipeline configuration
  741. """
  742. config = AudioPipelineConfig()
  743. config.enable_debug_logging = True
  744. config.save_debug_audio = True
  745. config.enable_quality_monitoring = True
  746. config.quality_check_interval = 0.1
  747. return config