test_audio_player.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Streaming Audio Player.
  4. Testet:
  5. - BlockingStreamPlayer Chunk-Handling
  6. - AudioOutputManager TTS/Musik-Verwaltung
  7. - Prebuffer und State-Management
  8. """
  9. import asyncio
  10. import pytest
  11. import threading
  12. import time
  13. from unittest.mock import MagicMock, patch, AsyncMock
  14. from trixy_core.audio.player import (
  15. BlockingStreamPlayer,
  16. AudioOutputManager,
  17. PlayerConfig,
  18. PlayerState,
  19. SimpleStreamingPlayer,
  20. RobustStreamingPlayer,
  21. )
  22. # =============================================================================
  23. # Fixtures
  24. # =============================================================================
  25. @pytest.fixture
  26. def player_config():
  27. """Standard Player-Konfiguration für Tests."""
  28. return PlayerConfig(
  29. sample_rate=22050,
  30. channels=1,
  31. sample_width=2,
  32. prebuffer_ms=100, # Kurzer Prebuffer für schnelle Tests
  33. )
  34. @pytest.fixture
  35. def player(player_config):
  36. """Erstellt einen BlockingStreamPlayer."""
  37. return BlockingStreamPlayer(player_config)
  38. @pytest.fixture
  39. def audio_manager():
  40. """Erstellt einen AudioOutputManager."""
  41. return AudioOutputManager(
  42. tts_sample_rate=22050,
  43. music_sample_rate=44100,
  44. )
  45. # =============================================================================
  46. # Tests für PlayerConfig
  47. # =============================================================================
  48. class TestPlayerConfig:
  49. """Tests für PlayerConfig Datenklasse."""
  50. def test_default_values(self):
  51. """Testet Standard-Werte."""
  52. config = PlayerConfig()
  53. assert config.sample_rate == 44100
  54. assert config.channels == 2
  55. assert config.sample_width == 2
  56. assert config.prebuffer_ms == 1000
  57. def test_custom_values(self):
  58. """Testet benutzerdefinierte Werte."""
  59. config = PlayerConfig(
  60. sample_rate=16000,
  61. channels=1,
  62. sample_width=2,
  63. prebuffer_ms=500,
  64. )
  65. assert config.sample_rate == 16000
  66. assert config.channels == 1
  67. assert config.sample_width == 2
  68. assert config.prebuffer_ms == 500
  69. def test_bytes_per_second(self):
  70. """Testet bytes_per_second Berechnung."""
  71. config = PlayerConfig(
  72. sample_rate=16000,
  73. channels=1,
  74. sample_width=2,
  75. )
  76. # 16000 * 1 * 2 = 32000 bytes/sec
  77. assert config.bytes_per_second == 32000
  78. def test_bytes_per_second_stereo(self):
  79. """Testet bytes_per_second bei Stereo."""
  80. config = PlayerConfig(
  81. sample_rate=44100,
  82. channels=2,
  83. sample_width=2,
  84. )
  85. # 44100 * 2 * 2 = 176400 bytes/sec
  86. assert config.bytes_per_second == 176400
  87. def test_prebuffer_bytes(self):
  88. """Testet prebuffer_bytes Berechnung."""
  89. config = PlayerConfig(
  90. sample_rate=16000,
  91. channels=1,
  92. sample_width=2,
  93. prebuffer_ms=500,
  94. )
  95. # 32000 bytes/sec * 500ms / 1000 = 16000 bytes
  96. assert config.prebuffer_bytes == 16000
  97. def test_frame_size(self):
  98. """Testet frame_size Berechnung."""
  99. config = PlayerConfig(
  100. sample_rate=44100,
  101. channels=2,
  102. sample_width=2,
  103. )
  104. # 2 channels * 2 bytes = 4 bytes per frame
  105. assert config.frame_size == 4
  106. def test_frame_size_mono(self):
  107. """Testet frame_size bei Mono."""
  108. config = PlayerConfig(
  109. sample_rate=22050,
  110. channels=1,
  111. sample_width=2,
  112. )
  113. # 1 channel * 2 bytes = 2 bytes per frame
  114. assert config.frame_size == 2
  115. # =============================================================================
  116. # Tests für PlayerState
  117. # =============================================================================
  118. class TestPlayerState:
  119. """Tests für PlayerState Enum."""
  120. def test_states_exist(self):
  121. """Testet, dass alle Zustände existieren."""
  122. assert PlayerState.STOPPED
  123. assert PlayerState.BUFFERING
  124. assert PlayerState.PLAYING
  125. def test_states_are_unique(self):
  126. """Testet, dass alle Zustände unterschiedlich sind."""
  127. states = [
  128. PlayerState.STOPPED,
  129. PlayerState.BUFFERING,
  130. PlayerState.PLAYING,
  131. ]
  132. assert len(states) == len(set(states))
  133. # =============================================================================
  134. # Tests für BlockingStreamPlayer - Basis
  135. # =============================================================================
  136. class TestBlockingStreamPlayerBasic:
  137. """Basis-Tests für BlockingStreamPlayer."""
  138. def test_initial_state(self, player):
  139. """Testet den Initialzustand."""
  140. assert player.state == PlayerState.STOPPED
  141. assert player.is_playing is False
  142. def test_default_config(self):
  143. """Testet Player mit Standard-Konfiguration."""
  144. player = BlockingStreamPlayer()
  145. assert player.state == PlayerState.STOPPED
  146. assert player._config.sample_rate == 44100
  147. def test_custom_config(self, player_config):
  148. """Testet Player mit benutzerdefinierter Konfiguration."""
  149. player = BlockingStreamPlayer(player_config)
  150. assert player._config.sample_rate == 22050
  151. assert player._config.channels == 1
  152. def test_stats_initial(self, player):
  153. """Testet initiale Statistiken."""
  154. stats = player.stats
  155. assert stats["state"] == "STOPPED"
  156. assert stats["bytes_played"] == 0
  157. assert stats["queue_size"] == 0
  158. def test_buffer_ms_empty(self, player):
  159. """Testet buffer_ms bei leerem Buffer."""
  160. assert player.buffer_ms == 0
  161. # =============================================================================
  162. # Tests für BlockingStreamPlayer - Aliases
  163. # =============================================================================
  164. class TestPlayerAliases:
  165. """Tests für Klassen-Aliases."""
  166. def test_simple_streaming_player_alias(self):
  167. """Testet SimpleStreamingPlayer Alias."""
  168. assert SimpleStreamingPlayer is BlockingStreamPlayer
  169. def test_robust_streaming_player_alias(self):
  170. """Testet RobustStreamingPlayer Alias."""
  171. assert RobustStreamingPlayer is BlockingStreamPlayer
  172. # =============================================================================
  173. # Tests für BlockingStreamPlayer - Chunk-Handling
  174. # =============================================================================
  175. class TestBlockingStreamPlayerChunks:
  176. """Tests für Chunk-Handling."""
  177. def test_add_chunk_starts_buffering(self, player):
  178. """Testet, dass add_chunk den Zustand auf BUFFERING setzt."""
  179. audio_data = b'\x00' * 1024
  180. result = player.add_chunk(audio_data)
  181. assert result is True
  182. assert player.state == PlayerState.BUFFERING
  183. def test_add_chunk_returns_true(self, player):
  184. """Testet, dass add_chunk True zurückgibt."""
  185. result = player.add_chunk(b'\x00' * 1024)
  186. assert result is True
  187. def test_add_empty_chunk_returns_false(self, player):
  188. """Testet, dass leere Chunks abgelehnt werden."""
  189. result = player.add_chunk(b'')
  190. assert result is False
  191. def test_add_none_chunk_returns_false(self, player):
  192. """Testet, dass None-Chunks abgelehnt werden."""
  193. result = player.add_chunk(None)
  194. assert result is False
  195. def test_prebuffer_fills_before_playing(self, player_config):
  196. """Testet, dass Prebuffer gefüllt wird bevor Wiedergabe startet."""
  197. # Prebuffer = 100ms bei 22050Hz mono = ~4410 bytes
  198. player = BlockingStreamPlayer(player_config)
  199. # Kleiner Chunk - sollte in Prebuffer bleiben
  200. player.add_chunk(b'\x00' * 1000)
  201. assert player.state == PlayerState.BUFFERING
  202. def test_buffer_ms_increases_with_chunks(self, player):
  203. """Testet, dass buffer_ms mit Chunks steigt."""
  204. initial_ms = player.buffer_ms
  205. # Chunks hinzufügen
  206. for _ in range(5):
  207. player.add_chunk(b'\x00' * 1024)
  208. # buffer_ms sollte größer sein (Prebuffer zählt)
  209. # Note: buffer_ms berechnet aus queue und prebuffer
  210. assert player.buffer_ms >= 0
  211. # =============================================================================
  212. # Tests für BlockingStreamPlayer - State Transitions
  213. # =============================================================================
  214. class TestBlockingStreamPlayerStateTransitions:
  215. """Tests für Zustandsübergänge."""
  216. def test_stop_from_stopped(self, player):
  217. """Testet stop() im STOPPED-Zustand."""
  218. assert player.state == PlayerState.STOPPED
  219. player.stop()
  220. assert player.state == PlayerState.STOPPED
  221. def test_stop_clears_queue(self, player):
  222. """Testet, dass stop() die Queue leert."""
  223. player.add_chunk(b'\x00' * 1024)
  224. player.stop()
  225. assert player._queue.empty()
  226. def test_stop_clears_prebuffer(self, player):
  227. """Testet, dass stop() den Prebuffer leert."""
  228. player.add_chunk(b'\x00' * 1024)
  229. assert player._prebuffer_size > 0
  230. player.stop()
  231. assert player._prebuffer_size == 0
  232. assert len(player._prebuffer) == 0
  233. def test_finish_flushes_prebuffer(self, player):
  234. """Testet, dass finish() den Prebuffer in die Queue schiebt."""
  235. player.add_chunk(b'\x00' * 1024)
  236. assert player.state == PlayerState.BUFFERING
  237. assert player._prebuffer_size > 0
  238. player.finish()
  239. # Prebuffer sollte in Queue verschoben sein
  240. assert player._prebuffer_size == 0
  241. # Queue sollte Daten + None (Ende-Marker) haben
  242. assert not player._queue.empty()
  243. def test_finish_adds_none_marker(self, player):
  244. """Testet, dass finish() einen None-Marker hinzufügt."""
  245. player.finish()
  246. # Queue sollte None enthalten
  247. item = player._queue.get_nowait()
  248. assert item is None
  249. def test_is_playing_false_when_stopped(self, player):
  250. """Testet is_playing bei STOPPED-Zustand."""
  251. assert player.state == PlayerState.STOPPED
  252. assert player.is_playing is False
  253. def test_is_playing_false_when_buffering(self, player):
  254. """Testet is_playing bei BUFFERING-Zustand."""
  255. player.add_chunk(b'\x00' * 100)
  256. assert player.state == PlayerState.BUFFERING
  257. assert player.is_playing is False
  258. # =============================================================================
  259. # Tests für BlockingStreamPlayer - Pause
  260. # =============================================================================
  261. class TestBlockingStreamPlayerPause:
  262. """Tests für Pause-Funktionalität."""
  263. def test_pause_from_stopped(self, player):
  264. """Testet, dass pause() im STOPPED-Zustand nichts tut."""
  265. assert player.state == PlayerState.STOPPED
  266. player.pause()
  267. # State bleibt STOPPED (kann nicht von STOPPED pausieren)
  268. assert player.state == PlayerState.STOPPED
  269. def test_resume_from_stopped(self, player):
  270. """Testet, dass resume() im STOPPED-Zustand nichts tut."""
  271. assert player.state == PlayerState.STOPPED
  272. player.resume()
  273. assert player.state == PlayerState.STOPPED
  274. def test_toggle_pause_from_stopped(self, player):
  275. """Testet toggle_pause() im STOPPED-Zustand."""
  276. assert player.state == PlayerState.STOPPED
  277. result = player.toggle_pause()
  278. assert result is False
  279. assert player.state == PlayerState.STOPPED
  280. def test_is_paused_false_initially(self, player):
  281. """Testet is_paused Property initial."""
  282. assert player.is_paused is False
  283. def test_is_paused_false_when_stopped(self, player):
  284. """Testet is_paused bei STOPPED-Zustand."""
  285. assert player.state == PlayerState.STOPPED
  286. assert player.is_paused is False
  287. def test_pause_sets_state_paused(self, player):
  288. """Testet, dass pause() den State auf PAUSED setzt."""
  289. # Manuell auf PLAYING setzen (ohne echten Thread)
  290. player._state = PlayerState.PLAYING
  291. player.pause()
  292. assert player.state == PlayerState.PAUSED
  293. assert player.is_paused is True
  294. def test_resume_sets_state_playing(self, player):
  295. """Testet, dass resume() den State auf PLAYING setzt."""
  296. # Manuell auf PAUSED setzen
  297. player._state = PlayerState.PAUSED
  298. player._pause_event.set()
  299. player.resume()
  300. assert player.state == PlayerState.PLAYING
  301. assert player.is_paused is False
  302. def test_toggle_pause_from_playing(self, player):
  303. """Testet toggle_pause() von PLAYING nach PAUSED."""
  304. player._state = PlayerState.PLAYING
  305. result = player.toggle_pause()
  306. assert result is True
  307. assert player.state == PlayerState.PAUSED
  308. assert player._pause_event.is_set()
  309. def test_toggle_pause_from_paused(self, player):
  310. """Testet toggle_pause() von PAUSED nach PLAYING."""
  311. player._state = PlayerState.PAUSED
  312. player._pause_event.set()
  313. result = player.toggle_pause()
  314. assert result is False
  315. assert player.state == PlayerState.PLAYING
  316. assert not player._pause_event.is_set()
  317. def test_stop_clears_pause_event(self, player):
  318. """Testet, dass stop() das Pause-Event löscht."""
  319. player._state = PlayerState.PAUSED
  320. player._pause_event.set()
  321. player.stop()
  322. assert not player._pause_event.is_set()
  323. assert player.state == PlayerState.STOPPED
  324. # =============================================================================
  325. # Tests für BlockingStreamPlayer - Callbacks
  326. # =============================================================================
  327. class TestBlockingStreamPlayerCallbacks:
  328. """Tests für Callback-Registrierung."""
  329. def test_on_finished_callback_registration(self, player):
  330. """Testet on_finished Callback-Registrierung."""
  331. callback = MagicMock()
  332. player.on_finished(callback)
  333. assert player._on_finished is callback
  334. def test_on_finished_callback_none(self, player):
  335. """Testet, dass on_finished None akzeptiert."""
  336. player.on_finished(None)
  337. assert player._on_finished is None
  338. # =============================================================================
  339. # Tests für BlockingStreamPlayer - Thread-Safety
  340. # =============================================================================
  341. class TestBlockingStreamPlayerThreadSafety:
  342. """Tests für Thread-Sicherheit."""
  343. def test_concurrent_chunk_adding(self, player):
  344. """Testet gleichzeitiges Hinzufügen von Chunks aus mehreren Threads."""
  345. chunks_added = []
  346. lock = threading.Lock()
  347. def add_chunks(thread_id):
  348. for i in range(10):
  349. chunk_data = bytes([thread_id] * 100)
  350. result = player.add_chunk(chunk_data)
  351. if result:
  352. with lock:
  353. chunks_added.append((thread_id, i))
  354. threads = [
  355. threading.Thread(target=add_chunks, args=(i,))
  356. for i in range(5)
  357. ]
  358. for t in threads:
  359. t.start()
  360. for t in threads:
  361. t.join()
  362. # Alle Chunks sollten hinzugefügt worden sein
  363. assert len(chunks_added) == 50
  364. def test_state_access_thread_safe(self, player):
  365. """Testet Thread-sicheren Zugriff auf state."""
  366. states_read = []
  367. def read_state(count):
  368. for _ in range(count):
  369. states_read.append(player.state)
  370. time.sleep(0.001)
  371. threads = [
  372. threading.Thread(target=read_state, args=(20,))
  373. for _ in range(5)
  374. ]
  375. for t in threads:
  376. t.start()
  377. for t in threads:
  378. t.join()
  379. # Alle Zustandsabfragen sollten erfolgreich sein
  380. assert len(states_read) == 100
  381. # =============================================================================
  382. # Tests für AudioOutputManager - Basis
  383. # =============================================================================
  384. class TestAudioOutputManagerBasic:
  385. """Basis-Tests für AudioOutputManager."""
  386. def test_initialization(self, audio_manager):
  387. """Testet die Initialisierung."""
  388. assert audio_manager._tts_player is not None
  389. assert audio_manager._music_player is not None
  390. assert audio_manager._initialized is False
  391. def test_custom_sample_rates(self):
  392. """Testet benutzerdefinierte Sample-Rates."""
  393. manager = AudioOutputManager(
  394. tts_sample_rate=16000,
  395. music_sample_rate=48000,
  396. )
  397. assert manager._tts_player._config.sample_rate == 16000
  398. assert manager._music_player._config.sample_rate == 48000
  399. def test_tts_player_config(self, audio_manager):
  400. """Testet TTS-Player-Konfiguration."""
  401. config = audio_manager._tts_player._config
  402. assert config.sample_rate == 22050
  403. assert config.channels == 1
  404. assert config.prebuffer_ms == 500
  405. def test_music_player_config(self, audio_manager):
  406. """Testet Musik-Player-Konfiguration."""
  407. config = audio_manager._music_player._config
  408. assert config.sample_rate == 44100
  409. assert config.channels == 2
  410. assert config.prebuffer_ms == 2000
  411. # =============================================================================
  412. # Tests für AudioOutputManager - Initialize/Shutdown
  413. # =============================================================================
  414. class TestAudioOutputManagerLifecycle:
  415. """Tests für Initialisierung und Shutdown."""
  416. @pytest.mark.asyncio
  417. async def test_initialize_returns_true(self, audio_manager):
  418. """Testet, dass initialize() True zurückgibt."""
  419. result = await audio_manager.initialize()
  420. assert result is True
  421. assert audio_manager._initialized is True
  422. @pytest.mark.asyncio
  423. async def test_shutdown(self, audio_manager):
  424. """Testet Shutdown."""
  425. await audio_manager.initialize()
  426. await audio_manager.shutdown()
  427. assert audio_manager._initialized is False
  428. # =============================================================================
  429. # Tests für AudioOutputManager - TTS
  430. # =============================================================================
  431. class TestAudioOutputManagerTTS:
  432. """Tests für TTS-Funktionen."""
  433. def test_play_tts_chunk(self, audio_manager):
  434. """Testet TTS-Chunk-Wiedergabe."""
  435. result = audio_manager.play_tts_chunk(b'\x00' * 1024)
  436. assert result is True
  437. def test_play_tts_chunk_empty(self, audio_manager):
  438. """Testet, dass leere TTS-Chunks abgelehnt werden."""
  439. result = audio_manager.play_tts_chunk(b'')
  440. assert result is False
  441. def test_finish_tts(self, audio_manager):
  442. """Testet TTS-Stream beenden."""
  443. audio_manager.play_tts_chunk(b'\x00' * 1024)
  444. audio_manager.finish_tts()
  445. # Sollte None-Marker in Queue haben
  446. assert not audio_manager._tts_player._queue.empty()
  447. def test_stop_tts(self, audio_manager):
  448. """Testet TTS-Wiedergabe stoppen."""
  449. audio_manager.play_tts_chunk(b'\x00' * 1024)
  450. audio_manager.stop_tts()
  451. assert audio_manager._tts_player.state == PlayerState.STOPPED
  452. def test_tts_playing_property(self, audio_manager):
  453. """Testet tts_playing Property."""
  454. assert audio_manager.tts_playing is False
  455. # =============================================================================
  456. # Tests für AudioOutputManager - Musik
  457. # =============================================================================
  458. class TestAudioOutputManagerMusic:
  459. """Tests für Musik-Funktionen."""
  460. def test_play_music_chunk(self, audio_manager):
  461. """Testet Musik-Chunk-Wiedergabe."""
  462. result = audio_manager.play_music_chunk(b'\x00' * 1024)
  463. assert result is True
  464. def test_play_music_chunk_empty(self, audio_manager):
  465. """Testet, dass leere Musik-Chunks abgelehnt werden."""
  466. result = audio_manager.play_music_chunk(b'')
  467. assert result is False
  468. def test_finish_music(self, audio_manager):
  469. """Testet Musik-Stream beenden."""
  470. audio_manager.play_music_chunk(b'\x00' * 1024)
  471. audio_manager.finish_music()
  472. # Sollte None-Marker in Queue haben
  473. assert not audio_manager._music_player._queue.empty()
  474. def test_stop_music(self, audio_manager):
  475. """Testet Musik-Wiedergabe stoppen."""
  476. audio_manager.play_music_chunk(b'\x00' * 1024)
  477. audio_manager.stop_music()
  478. assert audio_manager._music_player.state == PlayerState.STOPPED
  479. def test_music_playing_property(self, audio_manager):
  480. """Testet music_playing Property."""
  481. assert audio_manager.music_playing is False
  482. # =============================================================================
  483. # Tests für AudioOutputManager - Kombiniert
  484. # =============================================================================
  485. class TestAudioOutputManagerCombined:
  486. """Tests für kombinierte Funktionen."""
  487. def test_stop_all(self, audio_manager):
  488. """Testet alle Wiedergaben stoppen."""
  489. audio_manager.play_tts_chunk(b'\x00' * 1024)
  490. audio_manager.play_music_chunk(b'\x00' * 1024)
  491. audio_manager.stop_all()
  492. assert audio_manager._tts_player.state == PlayerState.STOPPED
  493. assert audio_manager._music_player.state == PlayerState.STOPPED
  494. def test_stats(self, audio_manager):
  495. """Testet kombinierte Statistiken."""
  496. stats = audio_manager.stats
  497. assert "tts" in stats
  498. assert "music" in stats
  499. assert "state" in stats["tts"]
  500. assert "state" in stats["music"]
  501. def test_simultaneous_tts_and_music(self, audio_manager):
  502. """Testet gleichzeitige TTS- und Musik-Pufferung."""
  503. # TTS-Chunks
  504. for _ in range(3):
  505. audio_manager.play_tts_chunk(b'\x00' * 1024)
  506. # Musik-Chunks
  507. for _ in range(3):
  508. audio_manager.play_music_chunk(b'\x00' * 2048)
  509. # Beide sollten Daten haben
  510. assert audio_manager._tts_player._prebuffer_size > 0
  511. assert audio_manager._music_player._prebuffer_size > 0
  512. # Nur TTS stoppen
  513. audio_manager.stop_tts()
  514. assert audio_manager._tts_player.state == PlayerState.STOPPED
  515. assert audio_manager._music_player.state == PlayerState.BUFFERING
  516. # =============================================================================
  517. # Tests für AudioOutputManager - Musik-Pause
  518. # =============================================================================
  519. class TestAudioOutputManagerMusicPause:
  520. """Tests für Musik-Pause-Funktionalität."""
  521. def test_music_paused_initial(self, audio_manager):
  522. """Testet music_paused Property initial."""
  523. assert audio_manager.music_paused is False
  524. def test_pause_music(self, audio_manager):
  525. """Testet pause_music() Methode."""
  526. # Musik-Player auf PLAYING setzen
  527. audio_manager._music_player._state = PlayerState.PLAYING
  528. audio_manager.pause_music()
  529. assert audio_manager.music_paused is True
  530. assert audio_manager._music_player.state == PlayerState.PAUSED
  531. def test_resume_music(self, audio_manager):
  532. """Testet resume_music() Methode."""
  533. # Musik-Player auf PAUSED setzen
  534. audio_manager._music_player._state = PlayerState.PAUSED
  535. audio_manager._music_player._pause_event.set()
  536. audio_manager.resume_music()
  537. assert audio_manager.music_paused is False
  538. assert audio_manager._music_player.state == PlayerState.PLAYING
  539. def test_toggle_music_pause_from_playing(self, audio_manager):
  540. """Testet toggle_music_pause() von PLAYING nach PAUSED."""
  541. audio_manager._music_player._state = PlayerState.PLAYING
  542. result = audio_manager.toggle_music_pause()
  543. assert result is True
  544. assert audio_manager.music_paused is True
  545. def test_toggle_music_pause_from_paused(self, audio_manager):
  546. """Testet toggle_music_pause() von PAUSED nach PLAYING."""
  547. audio_manager._music_player._state = PlayerState.PAUSED
  548. audio_manager._music_player._pause_event.set()
  549. result = audio_manager.toggle_music_pause()
  550. assert result is False
  551. assert audio_manager.music_paused is False
  552. def test_stop_all_clears_pause(self, audio_manager):
  553. """Testet, dass stop_all() auch Pause aufhebt."""
  554. audio_manager._music_player._state = PlayerState.PAUSED
  555. audio_manager._music_player._pause_event.set()
  556. audio_manager.stop_all()
  557. assert not audio_manager._music_player._pause_event.is_set()
  558. assert audio_manager._music_player.state == PlayerState.STOPPED
  559. # =============================================================================
  560. # Tests für AudioOutputManager - Buffer-Status
  561. # =============================================================================
  562. class TestAudioOutputManagerBufferStatus:
  563. """Tests für Buffer-Status-Properties."""
  564. def test_music_buffer_full_initial(self, audio_manager):
  565. """Testet music_buffer_full bei leerem Buffer."""
  566. assert audio_manager.music_buffer_full is False
  567. def test_music_buffer_low_initial(self, audio_manager):
  568. """Testet music_buffer_low bei leerem Buffer."""
  569. # Leerer Buffer = low
  570. assert audio_manager.music_buffer_low is True
  571. def test_music_buffer_ms_initial(self, audio_manager):
  572. """Testet music_buffer_ms bei leerem Buffer."""
  573. assert audio_manager.music_buffer_ms == 0
  574. def test_tts_buffer_full_initial(self, audio_manager):
  575. """Testet tts_buffer_full bei leerem Buffer."""
  576. assert audio_manager.tts_buffer_full is False
  577. def test_can_accept_music_chunk_empty(self, audio_manager):
  578. """Testet can_accept_music_chunk bei leerer Queue."""
  579. assert audio_manager.can_accept_music_chunk() is True
  580. def test_can_accept_tts_chunk_empty(self, audio_manager):
  581. """Testet can_accept_tts_chunk bei leerer Queue."""
  582. assert audio_manager.can_accept_tts_chunk() is True
  583. # =============================================================================
  584. # Tests für Audio-Format-Konfiguration
  585. # =============================================================================
  586. class TestAudioFormatConfiguration:
  587. """Tests für dynamische Audio-Format-Konfiguration."""
  588. def test_tts_format_property(self, audio_manager):
  589. """Testet tts_format Property."""
  590. format_info = audio_manager.tts_format
  591. assert format_info["sample_rate"] == 22050
  592. assert format_info["channels"] == 1
  593. assert format_info["sample_width"] == 2
  594. def test_music_format_property(self, audio_manager):
  595. """Testet music_format Property."""
  596. format_info = audio_manager.music_format
  597. assert format_info["sample_rate"] == 44100
  598. assert format_info["channels"] == 2
  599. assert format_info["sample_width"] == 2
  600. @pytest.mark.asyncio
  601. async def test_update_tts_format(self, audio_manager):
  602. """Testet TTS-Format-Aktualisierung."""
  603. assert audio_manager.tts_format["sample_rate"] == 22050
  604. await audio_manager.update_tts_format(
  605. sample_rate=16000,
  606. channels=1,
  607. sample_width=2
  608. )
  609. assert audio_manager.tts_format["sample_rate"] == 16000
  610. assert audio_manager.tts_format["channels"] == 1
  611. @pytest.mark.asyncio
  612. async def test_update_music_format(self, audio_manager):
  613. """Testet Musik-Format-Aktualisierung."""
  614. assert audio_manager.music_format["sample_rate"] == 44100
  615. await audio_manager.update_music_format(
  616. sample_rate=48000,
  617. channels=2,
  618. sample_width=2
  619. )
  620. assert audio_manager.music_format["sample_rate"] == 48000
  621. assert audio_manager.music_format["channels"] == 2
  622. @pytest.mark.asyncio
  623. async def test_update_format_stops_current_playback(self, audio_manager):
  624. """Testet, dass Format-Aktualisierung die Wiedergabe stoppt."""
  625. # Chunks hinzufügen
  626. for _ in range(5):
  627. audio_manager.play_tts_chunk(b'\x00' * 1024)
  628. # Format aktualisieren
  629. await audio_manager.update_tts_format(sample_rate=16000)
  630. # Neuer Player sollte leer sein
  631. assert audio_manager._tts_player._prebuffer_size == 0
  632. assert audio_manager._tts_player.state == PlayerState.STOPPED
  633. @pytest.mark.asyncio
  634. async def test_update_format_preserves_initialized_state(self, audio_manager):
  635. """Testet, dass _initialized nach Format-Update erhalten bleibt."""
  636. await audio_manager.initialize()
  637. assert audio_manager._initialized is True
  638. await audio_manager.update_tts_format(sample_rate=16000)
  639. # _initialized sollte noch True sein
  640. assert audio_manager._initialized is True
  641. # =============================================================================
  642. # Integration Tests
  643. # =============================================================================
  644. class TestAudioPlayerIntegration:
  645. """Integrationstests für Audio-Player."""
  646. def test_full_tts_flow(self, audio_manager):
  647. """Testet den vollständigen TTS-Flow ohne echte Wiedergabe."""
  648. # Chunks hinzufügen
  649. for _ in range(5):
  650. result = audio_manager.play_tts_chunk(b'\x00' * 1024)
  651. assert result is True
  652. # Stream beenden
  653. audio_manager.finish_tts()
  654. # Aufräumen
  655. audio_manager.stop_tts()
  656. assert audio_manager._tts_player.state == PlayerState.STOPPED
  657. def test_full_music_flow(self, audio_manager):
  658. """Testet den vollständigen Musik-Flow ohne echte Wiedergabe."""
  659. # Chunks hinzufügen
  660. for _ in range(5):
  661. result = audio_manager.play_music_chunk(b'\x00' * 2048)
  662. assert result is True
  663. # Stream beenden
  664. audio_manager.finish_music()
  665. # Aufräumen
  666. audio_manager.stop_music()
  667. assert audio_manager._music_player.state == PlayerState.STOPPED
  668. def test_interleaved_tts_and_music(self, audio_manager):
  669. """Testet abwechselnde TTS- und Musik-Chunks."""
  670. for i in range(10):
  671. if i % 2 == 0:
  672. audio_manager.play_tts_chunk(b'\x00' * 512)
  673. else:
  674. audio_manager.play_music_chunk(b'\x00' * 1024)
  675. # Beide sollten Daten gepuffert haben
  676. assert audio_manager._tts_player._prebuffer_size > 0
  677. assert audio_manager._music_player._prebuffer_size > 0
  678. @pytest.mark.asyncio
  679. async def test_full_lifecycle(self, audio_manager):
  680. """Testet den vollständigen Lebenszyklus."""
  681. # Initialisieren
  682. result = await audio_manager.initialize()
  683. assert result is True
  684. # TTS abspielen
  685. audio_manager.play_tts_chunk(b'\x00' * 1024)
  686. # Musik abspielen
  687. audio_manager.play_music_chunk(b'\x00' * 2048)
  688. # Shutdown
  689. await audio_manager.shutdown()
  690. assert audio_manager._initialized is False
  691. assert audio_manager._tts_player.state == PlayerState.STOPPED
  692. assert audio_manager._music_player.state == PlayerState.STOPPED
  693. # =============================================================================
  694. # Edge Cases
  695. # =============================================================================
  696. class TestEdgeCases:
  697. """Tests für Grenzfälle."""
  698. def test_multiple_stops(self, player):
  699. """Testet mehrfaches Aufrufen von stop()."""
  700. player.add_chunk(b'\x00' * 1024)
  701. player.stop()
  702. player.stop()
  703. player.stop()
  704. assert player.state == PlayerState.STOPPED
  705. def test_finish_without_chunks(self, player):
  706. """Testet finish() ohne vorherige Chunks."""
  707. player.finish()
  708. # Sollte nur None-Marker haben
  709. item = player._queue.get_nowait()
  710. assert item is None
  711. def test_add_chunk_after_stop(self, player):
  712. """Testet add_chunk nach stop()."""
  713. player.add_chunk(b'\x00' * 1024)
  714. player.stop()
  715. # Neuer Chunk sollte wieder funktionieren
  716. result = player.add_chunk(b'\x00' * 1024)
  717. assert result is True
  718. assert player.state == PlayerState.BUFFERING
  719. def test_very_small_chunks(self, player):
  720. """Testet sehr kleine Chunks."""
  721. for _ in range(100):
  722. result = player.add_chunk(b'\x00' * 10)
  723. assert result is True
  724. def test_large_chunk(self, player):
  725. """Testet großen Chunk."""
  726. # 1 MB Chunk
  727. large_chunk = b'\x00' * (1024 * 1024)
  728. result = player.add_chunk(large_chunk)
  729. assert result is True
  730. def test_stats_after_operations(self, audio_manager):
  731. """Testet Stats nach verschiedenen Operationen."""
  732. # Initial
  733. stats1 = audio_manager.stats
  734. assert stats1["tts"]["bytes_played"] == 0
  735. # Nach Chunks
  736. audio_manager.play_tts_chunk(b'\x00' * 1024)
  737. stats2 = audio_manager.stats
  738. assert stats2["tts"]["state"] == "BUFFERING"
  739. # Nach Stop
  740. audio_manager.stop_tts()
  741. stats3 = audio_manager.stats
  742. assert stats3["tts"]["state"] == "STOPPED"