| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930 |
- """
- Audio Pipeline Management for Trixy ML Integration.
- This module provides comprehensive audio pipeline management for real-time processing
- in the Trixy voice assistant. It handles audio stream management, buffering,
- format conversion, and integration with ML components.
- Key Features:
- - Real-time audio stream processing
- - Audio buffer management with circular buffers
- - Format conversion and resampling
- - Volume analysis and silence detection
- - Audio quality monitoring
- - Thread-safe audio handling
- - Integration with wakeword and voice recognition
- - Performance optimization for low-latency processing
- Usage:
- from trixy_core.ml import AudioPipeline, AudioPipelineConfig
-
- # Create audio pipeline
- config = AudioPipelineConfig(
- sample_rate=16000,
- chunk_size=1024,
- buffer_duration=10.0
- )
-
- pipeline = AudioPipeline(config, ml_manager)
-
- # Start processing
- pipeline.start()
-
- # Feed audio data
- pipeline.process_audio_chunk(audio_data)
- """
- import asyncio
- import logging
- import threading
- import time
- import queue
- from typing import Optional, Dict, Any, List, Callable, Union, Tuple
- from dataclasses import dataclass, field
- from enum import Enum
- import numpy as np
- from collections import deque
- from .performance_monitor import PerformanceMonitor, MetricType
- # Audio processing libraries (with fallbacks)
- try:
- import librosa
- import soundfile as sf
- AUDIO_LIBS_AVAILABLE = True
- except ImportError:
- AUDIO_LIBS_AVAILABLE = False
- librosa = None
- sf = None
- class AudioFormat(Enum):
- """Supported audio formats."""
- INT16 = "int16"
- INT32 = "int32"
- FLOAT32 = "float32"
- FLOAT64 = "float64"
- class AudioPipelineState(Enum):
- """Audio pipeline processing states."""
- STOPPED = "stopped"
- STARTING = "starting"
- RUNNING = "running"
- PAUSED = "paused"
- ERROR = "error"
- class BufferType(Enum):
- """Audio buffer types."""
- CIRCULAR = "circular"
- LINEAR = "linear"
- DYNAMIC = "dynamic"
- @dataclass
- class AudioPipelineConfig:
- """Configuration for audio pipeline."""
- # Basic audio settings
- sample_rate: int = 16000
- chunk_size: int = 1024
- channels: int = 1
- audio_format: AudioFormat = AudioFormat.FLOAT32
-
- # Buffer configuration
- buffer_type: BufferType = BufferType.CIRCULAR
- buffer_duration_seconds: float = 10.0
- max_buffer_size_mb: float = 100.0
-
- # Processing configuration
- enable_resampling: bool = True
- enable_format_conversion: bool = True
- enable_volume_analysis: bool = True
- enable_silence_detection: bool = True
-
- # Silence detection
- silence_threshold: float = 0.01
- silence_duration_threshold: float = 0.5
-
- # Performance settings
- processing_threads: int = 2
- max_queue_size: int = 1000
- processing_timeout: float = 1.0
-
- # Quality monitoring
- enable_quality_monitoring: bool = True
- quality_check_interval: float = 1.0
-
- # Debug settings
- enable_debug_logging: bool = False
- save_debug_audio: bool = False
- debug_audio_path: str = "/tmp/trixy_audio_debug"
- @dataclass
- class AudioChunk:
- """Audio chunk with metadata."""
- data: np.ndarray
- timestamp: float
- sample_rate: int
- channels: int
- format: AudioFormat
- sequence_number: int = 0
- volume: float = 0.0
- is_silence: bool = False
- quality_score: float = 1.0
- @dataclass
- class AudioQualityMetrics:
- """Audio quality metrics."""
- sample_rate: int
- bit_depth: int
- channels: int
- duration_seconds: float
- rms_level: float
- peak_level: float
- snr_estimate: float
- dynamic_range: float
- silence_ratio: float
- distortion_estimate: float
- timestamp: float = field(default_factory=time.time)
- class CircularAudioBuffer:
- """Thread-safe circular audio buffer."""
-
- def __init__(self, max_duration: float, sample_rate: int, channels: int = 1):
- """
- Initialize circular buffer.
-
- Args:
- max_duration: Maximum buffer duration in seconds
- sample_rate: Audio sample rate
- channels: Number of audio channels
- """
- self.max_duration = max_duration
- self.sample_rate = sample_rate
- self.channels = channels
- self.max_samples = int(max_duration * sample_rate * channels)
-
- # Initialize buffer
- self._buffer = np.zeros(self.max_samples, dtype=np.float32)
- self._write_pos = 0
- self._read_pos = 0
- self._size = 0
- self._lock = threading.RLock()
-
- # Metadata tracking
- self._timestamps = deque(maxlen=1000)
- self._sequence_numbers = deque(maxlen=1000)
- self._current_sequence = 0
-
- def write(self, audio_data: np.ndarray, timestamp: float = None) -> bool:
- """
- Write audio data to buffer.
-
- Args:
- audio_data: Audio data to write
- timestamp: Timestamp of the audio data
-
- Returns:
- True if write was successful
- """
- if timestamp is None:
- timestamp = time.time()
-
- with self._lock:
- data_length = len(audio_data)
-
- # Check if we have space
- if data_length > self.max_samples:
- return False
-
- # Write data (may wrap around)
- end_pos = self._write_pos + data_length
-
- if end_pos <= self.max_samples:
- # No wrap around
- self._buffer[self._write_pos:end_pos] = audio_data
- else:
- # Wrap around
- first_part = self.max_samples - self._write_pos
- self._buffer[self._write_pos:] = audio_data[:first_part]
- self._buffer[:end_pos - self.max_samples] = audio_data[first_part:]
-
- # Update positions
- self._write_pos = end_pos % self.max_samples
- self._size = min(self._size + data_length, self.max_samples)
-
- # Update metadata
- self._timestamps.append(timestamp)
- self._sequence_numbers.append(self._current_sequence)
- self._current_sequence += 1
-
- return True
-
- def read(self, num_samples: int) -> Optional[np.ndarray]:
- """
- Read audio data from buffer.
-
- Args:
- num_samples: Number of samples to read
-
- Returns:
- Audio data or None if not enough data
- """
- with self._lock:
- if num_samples > self._size:
- return None
-
- # Read data (may wrap around)
- end_pos = self._read_pos + num_samples
-
- if end_pos <= self.max_samples:
- # No wrap around
- data = self._buffer[self._read_pos:end_pos].copy()
- else:
- # Wrap around
- first_part = self.max_samples - self._read_pos
- data = np.concatenate([
- self._buffer[self._read_pos:],
- self._buffer[:end_pos - self.max_samples]
- ])
-
- # Update position
- self._read_pos = end_pos % self.max_samples
- self._size -= num_samples
-
- return data
-
- def peek(self, num_samples: int, offset: int = 0) -> Optional[np.ndarray]:
- """
- Peek at buffer data without consuming it.
-
- Args:
- num_samples: Number of samples to peek
- offset: Offset from current read position
-
- Returns:
- Audio data or None if not enough data
- """
- with self._lock:
- if num_samples + offset > self._size:
- return None
-
- start_pos = (self._read_pos + offset) % self.max_samples
- end_pos = (start_pos + num_samples) % self.max_samples
-
- if end_pos > start_pos:
- # No wrap around
- data = self._buffer[start_pos:end_pos].copy()
- else:
- # Wrap around
- data = np.concatenate([
- self._buffer[start_pos:],
- self._buffer[:end_pos]
- ])
-
- return data
-
- def available_samples(self) -> int:
- """Get number of available samples."""
- with self._lock:
- return self._size
-
- def available_duration(self) -> float:
- """Get available duration in seconds."""
- return self.available_samples() / self.sample_rate / self.channels
-
- def clear(self) -> None:
- """Clear the buffer."""
- with self._lock:
- self._write_pos = 0
- self._read_pos = 0
- self._size = 0
- self._timestamps.clear()
- self._sequence_numbers.clear()
- class AudioProcessor:
- """Audio processing utilities."""
-
- @staticmethod
- def convert_format(audio_data: np.ndarray, target_format: AudioFormat) -> np.ndarray:
- """
- Convert audio data to target format.
-
- Args:
- audio_data: Input audio data
- target_format: Target audio format
-
- Returns:
- Converted audio data
- """
- if target_format == AudioFormat.INT16:
- return (audio_data * 32767).astype(np.int16)
- elif target_format == AudioFormat.INT32:
- return (audio_data * 2147483647).astype(np.int32)
- elif target_format == AudioFormat.FLOAT32:
- return audio_data.astype(np.float32)
- elif target_format == AudioFormat.FLOAT64:
- return audio_data.astype(np.float64)
- else:
- return audio_data
-
- @staticmethod
- def normalize_audio(audio_data: np.ndarray, target_level: float = 0.7) -> np.ndarray:
- """
- Normalize audio to target level.
-
- Args:
- audio_data: Input audio data
- target_level: Target RMS level (0.0 to 1.0)
-
- Returns:
- Normalized audio data
- """
- current_level = np.sqrt(np.mean(audio_data ** 2))
- if current_level > 0:
- scale_factor = target_level / current_level
- return audio_data * scale_factor
- return audio_data
-
- @staticmethod
- def resample_audio(audio_data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
- """
- Resample audio data.
-
- Args:
- audio_data: Input audio data
- orig_sr: Original sample rate
- target_sr: Target sample rate
-
- Returns:
- Resampled audio data
- """
- if not AUDIO_LIBS_AVAILABLE or orig_sr == target_sr:
- return audio_data
-
- try:
- return librosa.resample(audio_data, orig_sr=orig_sr, target_sr=target_sr)
- except Exception:
- # Fallback to simple resampling
- ratio = target_sr / orig_sr
- new_length = int(len(audio_data) * ratio)
- return np.interp(
- np.linspace(0, len(audio_data) - 1, new_length),
- np.arange(len(audio_data)),
- audio_data
- )
-
- @staticmethod
- def calculate_volume(audio_data: np.ndarray) -> float:
- """
- Calculate audio volume (RMS).
-
- Args:
- audio_data: Input audio data
-
- Returns:
- Volume level (0.0 to 1.0)
- """
- return float(np.sqrt(np.mean(audio_data ** 2)))
-
- @staticmethod
- def detect_silence(audio_data: np.ndarray, threshold: float = 0.01) -> bool:
- """
- Detect silence in audio data.
-
- Args:
- audio_data: Input audio data
- threshold: Silence threshold
-
- Returns:
- True if silence detected
- """
- volume = AudioProcessor.calculate_volume(audio_data)
- return volume < threshold
-
- @staticmethod
- def calculate_quality_metrics(audio_data: np.ndarray, sample_rate: int) -> AudioQualityMetrics:
- """
- Calculate audio quality metrics.
-
- Args:
- audio_data: Input audio data
- sample_rate: Audio sample rate
-
- Returns:
- Audio quality metrics
- """
- duration = len(audio_data) / sample_rate
- rms_level = np.sqrt(np.mean(audio_data ** 2))
- peak_level = np.max(np.abs(audio_data))
-
- # Estimate SNR (simplified)
- signal_power = np.mean(audio_data ** 2)
- noise_estimate = np.var(audio_data - np.mean(audio_data))
- snr_estimate = 10 * np.log10(signal_power / (noise_estimate + 1e-10))
-
- # Dynamic range
- dynamic_range = 20 * np.log10(peak_level / (rms_level + 1e-10))
-
- # Silence ratio
- silence_samples = np.sum(np.abs(audio_data) < 0.01)
- silence_ratio = silence_samples / len(audio_data)
-
- # Distortion estimate (simplified THD)
- distortion_estimate = 0.0 # Placeholder for more complex analysis
-
- return AudioQualityMetrics(
- sample_rate=sample_rate,
- bit_depth=32, # Assuming float32
- channels=1,
- duration_seconds=duration,
- rms_level=rms_level,
- peak_level=peak_level,
- snr_estimate=snr_estimate,
- dynamic_range=dynamic_range,
- silence_ratio=silence_ratio,
- distortion_estimate=distortion_estimate
- )
- class AudioPipelineError(Exception):
- """Audio pipeline exception."""
- pass
- class AudioPipeline:
- """
- Audio pipeline for real-time audio processing in Trixy ML integration.
-
- This class manages the complete audio processing pipeline from input
- to ML component integration, handling buffering, format conversion,
- and quality monitoring.
- """
-
- def __init__(
- self,
- config: AudioPipelineConfig,
- ml_manager: Optional[Any] = None,
- logger: Optional[logging.Logger] = None
- ):
- """
- Initialize audio pipeline.
-
- Args:
- config: Audio pipeline configuration
- ml_manager: ML manager for processing integration
- logger: Logger instance
- """
- self.config = config
- self.ml_manager = ml_manager
- self.logger = logger or logging.getLogger(__name__)
-
- # State management
- self._state = AudioPipelineState.STOPPED
- self._state_lock = threading.RLock()
-
- # Audio buffer
- self._audio_buffer = CircularAudioBuffer(
- max_duration=config.buffer_duration_seconds,
- sample_rate=config.sample_rate,
- channels=config.channels
- )
-
- # Processing queue
- self._processing_queue = queue.Queue(maxsize=config.max_queue_size)
- self._processing_threads: List[threading.Thread] = []
- self._stop_event = threading.Event()
-
- # Sequence tracking
- self._sequence_counter = 0
- self._sequence_lock = threading.Lock()
-
- # Quality monitoring
- self._quality_metrics: List[AudioQualityMetrics] = []
- self._quality_lock = threading.Lock()
- self._last_quality_check = 0.0
-
- # Performance metrics
- self._performance_stats = {
- "chunks_processed": 0,
- "chunks_dropped": 0,
- "processing_errors": 0,
- "average_processing_time": 0.0,
- "buffer_overruns": 0,
- "format_conversions": 0,
- "resampling_operations": 0
- }
-
- # Callbacks
- self.on_audio_processed: Optional[Callable] = None
- self.on_silence_detected: Optional[Callable] = None
- self.on_quality_alert: Optional[Callable] = None
- self.on_error: Optional[Callable] = None
-
- self.logger.info("Audio pipeline initialized")
-
- def start(self) -> None:
- """Start the audio pipeline."""
- with self._state_lock:
- if self._state != AudioPipelineState.STOPPED:
- self.logger.warning(f"Audio pipeline already running (state: {self._state.value})")
- return
-
- self.logger.info("Starting audio pipeline...")
- self._state = AudioPipelineState.STARTING
-
- try:
- # Clear stop event
- self._stop_event.clear()
-
- # Start processing threads
- for i in range(self.config.processing_threads):
- thread = threading.Thread(
- target=self._processing_worker,
- name=f"AudioPipeline-Worker-{i}",
- daemon=True
- )
- thread.start()
- self._processing_threads.append(thread)
-
- # Start quality monitoring if enabled
- if self.config.enable_quality_monitoring:
- quality_thread = threading.Thread(
- target=self._quality_monitoring_worker,
- name="AudioPipeline-QualityMonitor",
- daemon=True
- )
- quality_thread.start()
- self._processing_threads.append(quality_thread)
-
- self._state = AudioPipelineState.RUNNING
- self.logger.info("Audio pipeline started successfully")
-
- except Exception as e:
- self._state = AudioPipelineState.ERROR
- self.logger.error(f"Failed to start audio pipeline: {e}")
- if self.on_error:
- self.on_error(f"Start failed: {e}")
- raise AudioPipelineError(f"Failed to start audio pipeline: {e}")
-
- def stop(self) -> None:
- """Stop the audio pipeline."""
- with self._state_lock:
- if self._state == AudioPipelineState.STOPPED:
- return
-
- self.logger.info("Stopping audio pipeline...")
- self._state = AudioPipelineState.STOPPED
-
- # Signal stop to all threads
- self._stop_event.set()
-
- # Wait for threads to complete
- for thread in self._processing_threads:
- if thread.is_alive():
- thread.join(timeout=2.0)
-
- # Clear threads list
- self._processing_threads.clear()
-
- # Clear buffers and queues
- self._audio_buffer.clear()
-
- # Clear processing queue
- while not self._processing_queue.empty():
- try:
- self._processing_queue.get_nowait()
- except queue.Empty:
- break
-
- self.logger.info("Audio pipeline stopped")
-
- def process_audio_chunk(self, audio_data: np.ndarray, timestamp: float = None) -> bool:
- """
- Process an audio chunk through the pipeline.
-
- Args:
- audio_data: Audio data to process
- timestamp: Timestamp of the audio data
-
- Returns:
- True if processing was successful
- """
- if self._state != AudioPipelineState.RUNNING:
- return False
-
- if timestamp is None:
- timestamp = time.time()
-
- try:
- # Create audio chunk with metadata
- with self._sequence_lock:
- sequence_number = self._sequence_counter
- self._sequence_counter += 1
-
- chunk = AudioChunk(
- data=audio_data.copy(),
- timestamp=timestamp,
- sample_rate=self.config.sample_rate,
- channels=self.config.channels,
- format=self.config.audio_format,
- sequence_number=sequence_number
- )
-
- # Add to processing queue
- try:
- self._processing_queue.put(chunk, block=False)
- return True
- except queue.Full:
- self.logger.warning("Audio processing queue full, dropping chunk")
- self._performance_stats["chunks_dropped"] += 1
- return False
-
- except Exception as e:
- self.logger.error(f"Error processing audio chunk: {e}")
- self._performance_stats["processing_errors"] += 1
- return False
-
- def get_audio_buffer(self, duration: float) -> Optional[np.ndarray]:
- """
- Get audio data from buffer.
-
- Args:
- duration: Duration in seconds to retrieve
-
- Returns:
- Audio data or None if not enough data
- """
- num_samples = int(duration * self.config.sample_rate * self.config.channels)
- return self._audio_buffer.read(num_samples)
-
- def peek_audio_buffer(self, duration: float, offset: float = 0.0) -> Optional[np.ndarray]:
- """
- Peek at audio buffer without consuming data.
-
- Args:
- duration: Duration in seconds to peek
- offset: Offset in seconds from current position
-
- Returns:
- Audio data or None if not enough data
- """
- num_samples = int(duration * self.config.sample_rate * self.config.channels)
- offset_samples = int(offset * self.config.sample_rate * self.config.channels)
- return self._audio_buffer.peek(num_samples, offset_samples)
-
- def get_buffer_status(self) -> Dict[str, Any]:
- """Get audio buffer status."""
- return {
- "available_duration": self._audio_buffer.available_duration(),
- "available_samples": self._audio_buffer.available_samples(),
- "max_duration": self._audio_buffer.max_duration,
- "sample_rate": self._audio_buffer.sample_rate,
- "channels": self._audio_buffer.channels
- }
-
- def get_performance_stats(self) -> Dict[str, Any]:
- """Get performance statistics."""
- return self._performance_stats.copy()
-
- def get_quality_metrics(self) -> List[AudioQualityMetrics]:
- """Get recent quality metrics."""
- with self._quality_lock:
- return self._quality_metrics.copy()
-
- def clear_buffers(self) -> None:
- """Clear all audio buffers."""
- self._audio_buffer.clear()
- self.logger.info("Audio buffers cleared")
-
- def _processing_worker(self) -> None:
- """Audio processing worker thread."""
- self.logger.info("Audio processing worker started")
-
- while not self._stop_event.is_set():
- try:
- # Get chunk from queue
- try:
- chunk = self._processing_queue.get(timeout=0.1)
- except queue.Empty:
- continue
-
- # Process the chunk
- start_time = time.time()
- processed_chunk = self._process_chunk(chunk)
- processing_time = time.time() - start_time
-
- # Update performance stats
- self._performance_stats["chunks_processed"] += 1
- self._performance_stats["average_processing_time"] = (
- (self._performance_stats["average_processing_time"] *
- (self._performance_stats["chunks_processed"] - 1) + processing_time) /
- self._performance_stats["chunks_processed"]
- )
-
- # Store in buffer
- self._audio_buffer.write(processed_chunk.data, processed_chunk.timestamp)
-
- # Send to ML manager if available
- if self.ml_manager and hasattr(self.ml_manager, 'process_audio_chunk'):
- asyncio.run(self.ml_manager.process_audio_chunk(processed_chunk.data))
-
- # Call callback if set
- if self.on_audio_processed:
- self.on_audio_processed(processed_chunk)
-
- # Mark task done
- self._processing_queue.task_done()
-
- except Exception as e:
- self.logger.error(f"Error in audio processing worker: {e}")
- self._performance_stats["processing_errors"] += 1
- time.sleep(0.01) # Prevent tight error loop
-
- self.logger.info("Audio processing worker stopped")
-
- def _process_chunk(self, chunk: AudioChunk) -> AudioChunk:
- """
- Process an individual audio chunk.
-
- Args:
- chunk: Audio chunk to process
-
- Returns:
- Processed audio chunk
- """
- processed_data = chunk.data.copy()
-
- # Format conversion if needed
- if self.config.enable_format_conversion:
- processed_data = AudioProcessor.convert_format(processed_data, self.config.audio_format)
- self._performance_stats["format_conversions"] += 1
-
- # Resampling if needed
- if self.config.enable_resampling and chunk.sample_rate != self.config.sample_rate:
- processed_data = AudioProcessor.resample_audio(
- processed_data,
- chunk.sample_rate,
- self.config.sample_rate
- )
- self._performance_stats["resampling_operations"] += 1
-
- # Calculate volume if enabled
- volume = 0.0
- if self.config.enable_volume_analysis:
- volume = AudioProcessor.calculate_volume(processed_data)
-
- # Detect silence if enabled
- is_silence = False
- if self.config.enable_silence_detection:
- is_silence = AudioProcessor.detect_silence(processed_data, self.config.silence_threshold)
- if is_silence and self.on_silence_detected:
- self.on_silence_detected(chunk)
-
- # Create processed chunk
- processed_chunk = AudioChunk(
- data=processed_data,
- timestamp=chunk.timestamp,
- sample_rate=self.config.sample_rate,
- channels=self.config.channels,
- format=self.config.audio_format,
- sequence_number=chunk.sequence_number,
- volume=volume,
- is_silence=is_silence
- )
-
- return processed_chunk
-
- def _quality_monitoring_worker(self) -> None:
- """Quality monitoring worker thread."""
- self.logger.info("Quality monitoring worker started")
-
- while not self._stop_event.is_set():
- try:
- current_time = time.time()
-
- # Check if it's time for quality analysis
- if current_time - self._last_quality_check >= self.config.quality_check_interval:
- self._perform_quality_check()
- self._last_quality_check = current_time
-
- # Wait before next check
- self._stop_event.wait(0.1)
-
- except Exception as e:
- self.logger.error(f"Error in quality monitoring: {e}")
- time.sleep(1.0)
-
- self.logger.info("Quality monitoring worker stopped")
-
- def _perform_quality_check(self) -> None:
- """Perform audio quality check."""
- try:
- # Get audio data for analysis
- analysis_duration = 1.0 # Analyze 1 second of audio
- audio_data = self.peek_audio_buffer(analysis_duration)
-
- if audio_data is None or len(audio_data) == 0:
- return
-
- # Calculate quality metrics
- metrics = AudioProcessor.calculate_quality_metrics(audio_data, self.config.sample_rate)
-
- # Store metrics
- with self._quality_lock:
- self._quality_metrics.append(metrics)
- # Keep only last 100 measurements
- if len(self._quality_metrics) > 100:
- self._quality_metrics.pop(0)
-
- # Check for quality alerts
- self._check_quality_alerts(metrics)
-
- except Exception as e:
- self.logger.error(f"Error performing quality check: {e}")
-
- def _check_quality_alerts(self, metrics: AudioQualityMetrics) -> None:
- """Check for quality alerts and trigger callbacks."""
- alerts = []
-
- # Check SNR
- if metrics.snr_estimate < 20.0: # SNR below 20dB
- alerts.append(f"Low SNR: {metrics.snr_estimate:.1f}dB")
-
- # Check silence ratio
- if metrics.silence_ratio > 0.9: # More than 90% silence
- alerts.append(f"High silence ratio: {metrics.silence_ratio:.1%}")
-
- # Check peak level
- if metrics.peak_level > 0.95: # Potential clipping
- alerts.append(f"Potential clipping: {metrics.peak_level:.3f}")
- elif metrics.peak_level < 0.01: # Very low signal
- alerts.append(f"Very low signal: {metrics.peak_level:.3f}")
-
- # Trigger alerts if any
- if alerts and self.on_quality_alert:
- self.on_quality_alert(metrics, alerts)
- def create_audio_pipeline(
- config: AudioPipelineConfig,
- ml_manager: Optional[Any] = None,
- logger: Optional[logging.Logger] = None
- ) -> AudioPipeline:
- """
- Factory function to create audio pipeline.
-
- Args:
- config: Audio pipeline configuration
- ml_manager: ML manager for processing integration
- logger: Logger instance
-
- Returns:
- Configured audio pipeline
- """
- return AudioPipeline(config, ml_manager, logger)
- def create_default_audio_config() -> AudioPipelineConfig:
- """
- Create default audio pipeline configuration.
-
- Returns:
- Default audio pipeline configuration
- """
- return AudioPipelineConfig()
- def create_optimized_audio_config() -> AudioPipelineConfig:
- """
- Create optimized audio pipeline configuration for production.
-
- Returns:
- Optimized audio pipeline configuration
- """
- config = AudioPipelineConfig()
- config.processing_threads = 4
- config.max_queue_size = 2000
- config.enable_quality_monitoring = True
- config.quality_check_interval = 0.5
- return config
- def create_debug_audio_config() -> AudioPipelineConfig:
- """
- Create debug audio pipeline configuration.
-
- Returns:
- Debug audio pipeline configuration
- """
- config = AudioPipelineConfig()
- config.enable_debug_logging = True
- config.save_debug_audio = True
- config.enable_quality_monitoring = True
- config.quality_check_interval = 0.1
- return config
|