audio_buffer.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. # -*- coding: utf-8 -*-
  2. """
  3. Audio Buffer für Aufnahmen im Arbeitsspeicher.
  4. """
  5. from dataclasses import dataclass, field
  6. from datetime import datetime
  7. from typing import Iterator
  8. import threading
  9. import io
  10. @dataclass
  11. class BufferConfig:
  12. """Konfiguration für Audio-Buffer."""
  13. # Maximale Aufnahmedauer in Sekunden
  14. max_duration_seconds: float = 60.0
  15. # Audio-Format
  16. sample_rate: int = 16000
  17. channels: int = 1
  18. bit_depth: int = 16 # 16-bit PCM
  19. # Buffer-Verwaltung
  20. chunk_size: int = 1280 # Samples pro Chunk (80ms bei 16kHz)
  21. pre_buffer_seconds: float = 0.5 # Audio vor dem Trigger behalten
  22. @property
  23. def bytes_per_sample(self) -> int:
  24. """Bytes pro Sample."""
  25. return self.bit_depth // 8
  26. @property
  27. def bytes_per_second(self) -> int:
  28. """Bytes pro Sekunde."""
  29. return self.sample_rate * self.channels * self.bytes_per_sample
  30. @property
  31. def max_buffer_size(self) -> int:
  32. """Maximale Buffer-Größe in Bytes."""
  33. return int(self.max_duration_seconds * self.bytes_per_second)
  34. @property
  35. def pre_buffer_size(self) -> int:
  36. """Pre-Buffer-Größe in Bytes."""
  37. return int(self.pre_buffer_seconds * self.bytes_per_second)
  38. @dataclass
  39. class AudioChunk:
  40. """Ein Audio-Chunk mit Metadaten."""
  41. data: bytes
  42. timestamp: datetime
  43. sequence_number: int
  44. duration_ms: float
  45. def __len__(self) -> int:
  46. return len(self.data)
  47. class AudioBuffer:
  48. """
  49. Thread-sicherer Audio-Buffer für Aufnahmen.
  50. Speichert Audio-Daten im Arbeitsspeicher mit konfigurierbarer
  51. maximaler Dauer und Pre-Buffer-Unterstützung.
  52. """
  53. def __init__(self, config: BufferConfig | None = None):
  54. """
  55. Initialisiert den Audio-Buffer.
  56. Args:
  57. config: Buffer-Konfiguration
  58. """
  59. self._config = config or BufferConfig()
  60. self._lock = threading.RLock()
  61. # Hauptbuffer
  62. self._buffer = io.BytesIO()
  63. self._chunks: list[AudioChunk] = []
  64. # Pre-Buffer (Ring-Buffer für Audio vor Trigger)
  65. self._pre_buffer: list[AudioChunk] = []
  66. self._pre_buffer_bytes = 0
  67. # State
  68. self._is_recording = False
  69. self._start_time: datetime | None = None
  70. self._sequence_counter = 0
  71. self._total_bytes = 0
  72. @property
  73. def config(self) -> BufferConfig:
  74. """Gibt Konfiguration zurück."""
  75. return self._config
  76. @property
  77. def is_recording(self) -> bool:
  78. """Prüft ob Aufnahme läuft."""
  79. with self._lock:
  80. return self._is_recording
  81. @property
  82. def duration_seconds(self) -> float:
  83. """Aktuelle Aufnahmedauer in Sekunden."""
  84. with self._lock:
  85. return self._total_bytes / self._config.bytes_per_second
  86. @property
  87. def size_bytes(self) -> int:
  88. """Aktuelle Größe in Bytes."""
  89. with self._lock:
  90. return self._total_bytes
  91. @property
  92. def chunk_count(self) -> int:
  93. """Anzahl gespeicherter Chunks."""
  94. with self._lock:
  95. return len(self._chunks)
  96. @property
  97. def start_time(self) -> datetime | None:
  98. """Startzeit der Aufnahme."""
  99. with self._lock:
  100. return self._start_time
  101. def add_to_pre_buffer(self, audio_data: bytes) -> None:
  102. """
  103. Fügt Audio zum Pre-Buffer hinzu.
  104. Wird verwendet um Audio vor dem Wakeword zu behalten.
  105. Args:
  106. audio_data: Audio-Daten (16-bit PCM)
  107. """
  108. with self._lock:
  109. if self._is_recording:
  110. # Während Aufnahme zum Hauptbuffer hinzufügen
  111. self._add_chunk(audio_data)
  112. return
  113. # Zum Pre-Buffer hinzufügen
  114. chunk = AudioChunk(
  115. data=audio_data,
  116. timestamp=datetime.now(),
  117. sequence_number=self._sequence_counter,
  118. duration_ms=len(audio_data) / self._config.bytes_per_second * 1000,
  119. )
  120. self._sequence_counter += 1
  121. self._pre_buffer.append(chunk)
  122. self._pre_buffer_bytes += len(audio_data)
  123. # Pre-Buffer beschränken
  124. while self._pre_buffer_bytes > self._config.pre_buffer_size:
  125. removed = self._pre_buffer.pop(0)
  126. self._pre_buffer_bytes -= len(removed.data)
  127. def start_recording(self, include_pre_buffer: bool = True) -> None:
  128. """
  129. Startet die Aufnahme.
  130. Args:
  131. include_pre_buffer: Pre-Buffer einschließen
  132. """
  133. with self._lock:
  134. if self._is_recording:
  135. return
  136. self._is_recording = True
  137. self._start_time = datetime.now()
  138. self._buffer = io.BytesIO()
  139. self._chunks.clear()
  140. self._total_bytes = 0
  141. # Pre-Buffer übernehmen
  142. if include_pre_buffer and self._pre_buffer:
  143. for chunk in self._pre_buffer:
  144. self._chunks.append(chunk)
  145. self._buffer.write(chunk.data)
  146. self._total_bytes += len(chunk.data)
  147. self._pre_buffer.clear()
  148. self._pre_buffer_bytes = 0
  149. def add_chunk(self, audio_data: bytes) -> bool:
  150. """
  151. Fügt Audio-Chunk zur Aufnahme hinzu.
  152. Args:
  153. audio_data: Audio-Daten (16-bit PCM)
  154. Returns:
  155. True wenn hinzugefügt, False wenn Buffer voll
  156. """
  157. with self._lock:
  158. if not self._is_recording:
  159. self.add_to_pre_buffer(audio_data)
  160. return True
  161. return self._add_chunk(audio_data)
  162. def _add_chunk(self, audio_data: bytes) -> bool:
  163. """Interne Methode zum Hinzufügen eines Chunks."""
  164. # Prüfe maximale Größe
  165. if self._total_bytes + len(audio_data) > self._config.max_buffer_size:
  166. return False
  167. chunk = AudioChunk(
  168. data=audio_data,
  169. timestamp=datetime.now(),
  170. sequence_number=self._sequence_counter,
  171. duration_ms=len(audio_data) / self._config.bytes_per_second * 1000,
  172. )
  173. self._sequence_counter += 1
  174. self._chunks.append(chunk)
  175. self._buffer.write(audio_data)
  176. self._total_bytes += len(audio_data)
  177. return True
  178. def stop_recording(self) -> bytes:
  179. """
  180. Stoppt die Aufnahme und gibt alle Daten zurück.
  181. Returns:
  182. Alle aufgenommenen Audio-Daten
  183. """
  184. with self._lock:
  185. self._is_recording = False
  186. # Hole alle Daten
  187. self._buffer.seek(0)
  188. data = self._buffer.read()
  189. return data
  190. def get_data(self) -> bytes:
  191. """
  192. Gibt alle aktuellen Daten zurück ohne die Aufnahme zu stoppen.
  193. Returns:
  194. Alle aufgenommenen Audio-Daten
  195. """
  196. with self._lock:
  197. self._buffer.seek(0)
  198. return self._buffer.read()
  199. def get_chunks(self) -> list[AudioChunk]:
  200. """
  201. Gibt alle Chunks zurück.
  202. Returns:
  203. Liste aller Audio-Chunks
  204. """
  205. with self._lock:
  206. return list(self._chunks)
  207. def iterate_chunks(self) -> Iterator[AudioChunk]:
  208. """
  209. Iteriert über alle Chunks.
  210. Yields:
  211. Audio-Chunks
  212. """
  213. with self._lock:
  214. for chunk in self._chunks:
  215. yield chunk
  216. def clear(self) -> None:
  217. """Löscht alle Daten."""
  218. with self._lock:
  219. self._buffer = io.BytesIO()
  220. self._chunks.clear()
  221. self._pre_buffer.clear()
  222. self._pre_buffer_bytes = 0
  223. self._is_recording = False
  224. self._start_time = None
  225. self._total_bytes = 0
  226. def get_stats(self) -> dict:
  227. """
  228. Gibt Statistiken zurück.
  229. Returns:
  230. Dictionary mit Statistiken
  231. """
  232. with self._lock:
  233. return {
  234. "is_recording": self._is_recording,
  235. "duration_seconds": self.duration_seconds,
  236. "size_bytes": self._total_bytes,
  237. "chunk_count": len(self._chunks),
  238. "pre_buffer_chunks": len(self._pre_buffer),
  239. "pre_buffer_bytes": self._pre_buffer_bytes,
  240. "start_time": self._start_time.isoformat() if self._start_time else None,
  241. }
  242. class AudioBufferPool:
  243. """
  244. Pool von Audio-Buffern für mehrere gleichzeitige Aufnahmen.
  245. """
  246. def __init__(self, pool_size: int = 5, config: BufferConfig | None = None):
  247. """
  248. Initialisiert den Buffer-Pool.
  249. Args:
  250. pool_size: Anzahl vorzuhaltender Buffer
  251. config: Gemeinsame Buffer-Konfiguration
  252. """
  253. self._config = config or BufferConfig()
  254. self._lock = threading.Lock()
  255. # Verfügbare Buffer
  256. self._available: list[AudioBuffer] = [
  257. AudioBuffer(self._config) for _ in range(pool_size)
  258. ]
  259. # In Verwendung
  260. self._in_use: dict[str, AudioBuffer] = {}
  261. def acquire(self, buffer_id: str) -> AudioBuffer:
  262. """
  263. Holt einen Buffer aus dem Pool.
  264. Args:
  265. buffer_id: Eindeutige ID für den Buffer
  266. Returns:
  267. Audio-Buffer
  268. Raises:
  269. RuntimeError: Wenn kein Buffer verfügbar
  270. """
  271. with self._lock:
  272. if buffer_id in self._in_use:
  273. return self._in_use[buffer_id]
  274. if not self._available:
  275. # Erstelle neuen Buffer wenn Pool leer
  276. buffer = AudioBuffer(self._config)
  277. else:
  278. buffer = self._available.pop()
  279. buffer.clear()
  280. self._in_use[buffer_id] = buffer
  281. return buffer
  282. def release(self, buffer_id: str) -> bytes | None:
  283. """
  284. Gibt Buffer zurück an den Pool.
  285. Args:
  286. buffer_id: Buffer-ID
  287. Returns:
  288. Aufgenommene Daten oder None
  289. """
  290. with self._lock:
  291. if buffer_id not in self._in_use:
  292. return None
  293. buffer = self._in_use.pop(buffer_id)
  294. # Hole Daten bevor wir zurückgeben
  295. data = buffer.stop_recording() if buffer.is_recording else buffer.get_data()
  296. # Zurück in den Pool
  297. buffer.clear()
  298. self._available.append(buffer)
  299. return data
  300. def get(self, buffer_id: str) -> AudioBuffer | None:
  301. """
  302. Gibt Buffer für ID zurück ohne ihn zu releasen.
  303. Args:
  304. buffer_id: Buffer-ID
  305. Returns:
  306. Audio-Buffer oder None
  307. """
  308. with self._lock:
  309. return self._in_use.get(buffer_id)
  310. @property
  311. def active_count(self) -> int:
  312. """Anzahl aktiver Buffer."""
  313. with self._lock:
  314. return len(self._in_use)
  315. @property
  316. def available_count(self) -> int:
  317. """Anzahl verfügbarer Buffer."""
  318. with self._lock:
  319. return len(self._available)