audio_processor.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """
  2. Audio Processor - Handle audio capture and buffering
  3. """
  4. import numpy as np
  5. import queue
  6. import threading
  7. from typing import Optional, Callable
  8. from dataclasses import dataclass
  9. @dataclass
  10. class AudioChunk:
  11. """Represents a chunk of audio data."""
  12. data: np.ndarray
  13. sample_rate: int
  14. timestamp: float
  15. class AudioBuffer:
  16. """
  17. Rolling buffer for streaming audio.
  18. """
  19. def __init__(self, max_duration: float = 30.0, sample_rate: int = 16000):
  20. """
  21. Initialize audio buffer.
  22. Args:
  23. max_duration: Maximum buffer duration in seconds
  24. sample_rate: Audio sample rate
  25. """
  26. self.sample_rate = sample_rate
  27. self.max_samples = int(max_duration * sample_rate)
  28. self.buffer = np.zeros(0, dtype=np.float32)
  29. self.lock = threading.Lock()
  30. def append(self, data: np.ndarray):
  31. """Append audio data to buffer."""
  32. with self.lock:
  33. self.buffer = np.concatenate([self.buffer, data])
  34. # Trim if exceeds max duration
  35. if len(self.buffer) > self.max_samples:
  36. self.buffer = self.buffer[-self.max_samples:]
  37. def get_all(self) -> np.ndarray:
  38. """Get all buffered audio."""
  39. with self.lock:
  40. return self.buffer.copy()
  41. def get_last(self, duration: float) -> np.ndarray:
  42. """Get last N seconds of audio."""
  43. with self.lock:
  44. num_samples = int(duration * self.sample_rate)
  45. return self.buffer[-num_samples:].copy()
  46. def clear(self):
  47. """Clear the buffer."""
  48. with self.lock:
  49. self.buffer = np.zeros(0, dtype=np.float32)
  50. def __len__(self):
  51. with self.lock:
  52. return len(self.buffer)
  53. class VADProcessor:
  54. """
  55. Voice Activity Detection using simple energy-based detection.
  56. """
  57. def __init__(
  58. self,
  59. threshold: float = 0.02,
  60. min_speech_duration: float = 0.3,
  61. min_silence_duration: float = 0.5,
  62. sample_rate: int = 16000
  63. ):
  64. """
  65. Initialize VAD processor.
  66. Args:
  67. threshold: Energy threshold for speech detection
  68. min_speech_duration: Minimum speech duration in seconds
  69. min_silence_duration: Minimum silence to end speech
  70. sample_rate: Audio sample rate
  71. """
  72. self.threshold = threshold
  73. self.min_speech_samples = int(min_speech_duration * sample_rate)
  74. self.min_silence_samples = int(min_silence_duration * sample_rate)
  75. self.sample_rate = sample_rate
  76. self.speech_buffer = []
  77. self.silence_counter = 0
  78. self.in_speech = False
  79. def process(self, audio: np.ndarray) -> list:
  80. """
  81. Process audio and return speech segments.
  82. Returns:
  83. List of (start_sample, end_sample) tuples
  84. """
  85. segments = []
  86. frame_size = 1024
  87. for i in range(0, len(audio), frame_size):
  88. frame = audio[i:i + frame_size]
  89. energy = np.sqrt(np.mean(frame ** 2))
  90. if energy > self.threshold:
  91. # Speech detected
  92. self.speech_buffer.append(frame)
  93. self.silence_counter = 0
  94. self.in_speech = True
  95. else:
  96. # Silence
  97. if self.in_speech:
  98. self.silence_counter += len(frame)
  99. if self.silence_counter >= self.min_silence_samples:
  100. # End of speech segment
  101. speech_audio = np.concatenate(self.speech_buffer)
  102. if len(speech_audio) >= self.min_speech_samples:
  103. start_sample = i - len(speech_audio) - self.silence_counter + frame_size
  104. end_sample = i
  105. segments.append((start_sample, end_sample))
  106. self.speech_buffer = []
  107. self.silence_counter = 0
  108. self.in_speech = False
  109. return segments
  110. def reset(self):
  111. """Reset VAD state."""
  112. self.speech_buffer = []
  113. self.silence_counter = 0
  114. self.in_speech = False
  115. class AudioProcessor:
  116. """
  117. Complete audio processor for streaming ASR.
  118. """
  119. def __init__(
  120. self,
  121. sample_rate: int = 16000,
  122. chunk_duration: float = 1.0,
  123. buffer_duration: float = 30.0,
  124. enable_vad: bool = True
  125. ):
  126. """
  127. Initialize audio processor.
  128. Args:
  129. sample_rate: Audio sample rate
  130. chunk_duration: Duration of each processing chunk
  131. buffer_duration: Maximum buffer duration
  132. enable_vad: Enable voice activity detection
  133. """
  134. self.sample_rate = sample_rate
  135. self.chunk_duration = chunk_duration
  136. self.chunk_samples = int(chunk_duration * sample_rate)
  137. self.buffer = AudioBuffer(buffer_duration, sample_rate)
  138. self.vad = VADProcessor() if enable_vad else None
  139. self.audio_queue = queue.Queue()
  140. self.is_running = False
  141. def process_chunk(self, audio_data: bytes):
  142. """
  143. Process incoming audio chunk.
  144. Args:
  145. audio_data: Raw PCM16 audio bytes
  146. """
  147. # Convert to numpy array
  148. int16_array = np.frombuffer(audio_data, dtype=np.int16)
  149. float_array = int16_array.astype(np.float32) / 32768.0
  150. # Add to buffer
  151. self.buffer.append(float_array)
  152. # Put in queue for ASR processing
  153. self.audio_queue.put(float_array.copy())
  154. def get_chunk(self, timeout: float = 1.0) -> Optional[np.ndarray]:
  155. """
  156. Get next audio chunk from queue.
  157. Args:
  158. timeout: Timeout in seconds
  159. Returns:
  160. Audio chunk as numpy array or None
  161. """
  162. try:
  163. return self.audio_queue.get(timeout=timeout)
  164. except queue.Empty:
  165. return None
  166. def get_buffer_audio(self, duration: float = None) -> np.ndarray:
  167. """
  168. Get buffered audio.
  169. Args:
  170. duration: Duration in seconds (None for all)
  171. Returns:
  172. Buffered audio as numpy array
  173. """
  174. if duration is None:
  175. return self.buffer.get_all()
  176. return self.buffer.get_last(duration)
  177. def clear(self):
  178. """Clear buffer and queue."""
  179. self.buffer.clear()
  180. while not self.audio_queue.empty():
  181. try:
  182. self.audio_queue.get_nowait()
  183. except queue.Empty:
  184. break
  185. def bytes_to_array(self, audio_bytes: bytes) -> np.ndarray:
  186. """Convert PCM16 bytes to float32 array."""
  187. int16_array = np.frombuffer(audio_bytes, dtype=np.int16)
  188. return int16_array.astype(np.float32) / 32768.0