Files
PyTaiko/test/libs/test_audio.py
2025-12-27 18:52:38 -05:00

519 lines
19 KiB
Python

import shutil
import struct
import unittest
import wave
from pathlib import Path
from unittest.mock import patch
from libs.audio import AudioEngine, audio
from libs.config import VolumeConfig
DEFAULT_CONFIG = VolumeConfig(sound=0.8, music=0.7, voice=0.6, hitsound=0.5, attract_mode=0.4)
class TestAudioEngine(unittest.TestCase):
"""Integration tests using the audio library."""
@classmethod
def setUpClass(cls):
"""Set up test fixtures once for all tests."""
# Create temporary directory for test audio files
cls.test_dir = Path().cwd() / Path("temp")
cls.test_dir.mkdir(exist_ok=True)
cls.sounds_dir = Path(cls.test_dir) / "Sounds"
cls.sounds_dir.mkdir(exist_ok=True)
# Create test WAV files
cls._create_test_wav(cls.sounds_dir / "don.wav")
cls._create_test_wav(cls.sounds_dir / "ka.wav")
cls._create_test_wav(cls.sounds_dir / "test_sound.wav")
cls._create_test_wav(cls.sounds_dir / "test_music.wav", duration=2.0)
# Create screen sounds directory
cls.screen_sounds = cls.sounds_dir / "menu"
cls.screen_sounds.mkdir()
cls._create_test_wav(cls.screen_sounds / "click.wav")
cls._create_test_wav(cls.screen_sounds / "hover.wav")
# Create global sounds directory
cls.global_sounds = cls.sounds_dir / "global"
cls.global_sounds.mkdir()
cls._create_test_wav(cls.global_sounds / "confirm.wav")
cls.volume_presets = DEFAULT_CONFIG
@classmethod
def tearDownClass(cls):
"""Clean up test files."""
shutil.rmtree(cls.test_dir)
@staticmethod
def _create_test_wav(filepath, duration=0.1, frequency=440):
"""Create a simple test WAV file."""
sample_rate = 44100
num_samples = int(sample_rate * duration)
with wave.open(str(filepath), 'w') as wav_file:
wav_file.setnchannels(1) # Mono
wav_file.setsampwidth(2) # 16-bit
wav_file.setframerate(sample_rate)
for i in range(num_samples):
# Generate a simple sine wave
value = int(32767.0 * 0.3 *
(i % (sample_rate // frequency)) /
(sample_rate // frequency))
wav_file.writeframes(struct.pack('h', value))
def setUp(self):
"""Set up each test."""
self.mock_config_path = self.sounds_dir
# Store original audio singleton state to avoid test pollution
self._original_audio_sounds_path = audio.sounds_path
def tearDown(self):
"""Tear down each test."""
# Restore original audio singleton state
audio.sounds_path = self._original_audio_sounds_path
# Clear any sounds or music loaded during tests
if hasattr(audio, 'sounds') and isinstance(audio.sounds, dict):
audio.sounds.clear()
if hasattr(audio, 'music_streams') and isinstance(audio.music_streams, dict):
audio.music_streams.clear()
@patch('libs.audio.get_config')
def test_initialization(self, mock_config):
"""Test AudioEngine initialization."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(
device_type=0,
sample_rate=44100.0,
buffer_size=512,
volume_presets=self.volume_presets,
sounds_path=self.sounds_dir
)
self.assertEqual(engine.device_type, 0)
self.assertEqual(engine.target_sample_rate, 44100.0)
self.assertEqual(engine.buffer_size, 512)
self.assertEqual(engine.volume_presets, self.volume_presets)
self.assertFalse(engine.audio_device_ready)
@patch('libs.audio.get_config')
def test_init_and_close_audio_device(self, mock_config):
"""Test initializing and closing audio device."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
# Initialize
success = engine.init_audio_device()
self.assertTrue(success)
self.assertTrue(engine.audio_device_ready)
self.assertTrue(engine.is_audio_device_ready())
# Close
engine.close_audio_device()
self.assertFalse(engine.audio_device_ready)
@patch('libs.audio.get_config')
def test_master_volume(self, mock_config):
"""Test master volume control."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Set and get master volume
engine.set_master_volume(0.75)
volume = engine.get_master_volume()
self.assertAlmostEqual(volume, 0.75, places=2)
# Test clamping
engine.set_master_volume(1.5)
volume = engine.get_master_volume()
self.assertLessEqual(volume, 1.0)
engine.set_master_volume(-0.5)
volume = engine.get_master_volume()
self.assertGreaterEqual(volume, 0.0)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_load_and_unload_sound(self, mock_config):
"""Test loading and unloading sounds."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Load sound
sound_path = self.sounds_dir / "test_sound.wav"
sound_id = engine.load_sound(sound_path, "test")
self.assertEqual(sound_id, "test")
self.assertIn("test", engine.sounds)
# Unload sound
engine.unload_sound("test")
self.assertNotIn("test", engine.sounds)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_load_nonexistent_sound(self, mock_config):
"""Test loading a non-existent sound file."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
sound_id = engine.load_sound(Path("nonexistent.wav"), "bad")
self.assertEqual(sound_id, "")
self.assertNotIn("bad", engine.sounds)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_play_and_stop_sound(self, mock_config):
"""Test playing and stopping sounds."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Load and play sound
sound_path = self.sounds_dir / "test_sound.wav"
engine.load_sound(sound_path, "test")
engine.play_sound("test", "sound")
# Give it a moment to start
import time
time.sleep(0.05)
# Check if playing (might not be if audio is very short)
# Just verify no exceptions were raised
is_playing = engine.is_sound_playing("test")
self.assertIsInstance(is_playing, bool)
# Stop sound
engine.stop_sound("test")
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_play_don_and_kat(self, mock_config):
"""Test playing the special don and kat sounds."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Play don
engine.play_sound("don", "sound")
is_playing = engine.is_sound_playing("don")
self.assertIsInstance(is_playing, bool)
engine.stop_sound("don")
# Play kat
engine.play_sound("kat", "sound")
is_playing = engine.is_sound_playing("kat")
self.assertIsInstance(is_playing, bool)
engine.stop_sound("kat")
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_sound_volume_control(self, mock_config):
"""Test setting sound volume."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
sound_path = self.sounds_dir / "test_sound.wav"
engine.load_sound(sound_path, "test")
# Set volume (should not raise exception)
engine.set_sound_volume("test", 0.5)
engine.play_sound("test", "")
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_sound_pan_control(self, mock_config):
"""Test setting sound pan."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
sound_path = self.sounds_dir / "test_sound.wav"
engine.load_sound(sound_path, "test")
# Set pan (should not raise exception)
engine.set_sound_pan("test", -0.5) # Left
engine.set_sound_pan("test", 0.5) # Right
engine.set_sound_pan("test", 0.0) # Center
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_load_screen_sounds(self, mock_config):
"""Test loading sounds for a screen."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
engine.load_screen_sounds("menu")
# Check that screen sounds were loaded
self.assertIn("click", engine.sounds)
self.assertIn("hover", engine.sounds)
# Check that global sounds were loaded
self.assertIn("confirm", engine.sounds)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_unload_all_sounds(self, mock_config):
"""Test unloading all sounds."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Load multiple sounds
engine.load_sound(self.sounds_dir / "test_sound.wav", "s1")
engine.load_sound(self.sounds_dir / "test_sound.wav", "s2")
engine.load_sound(self.sounds_dir / "test_sound.wav", "s3")
self.assertEqual(len(engine.sounds), 3)
# Unload all
engine.unload_all_sounds()
self.assertEqual(len(engine.sounds), 0)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_load_and_play_music_stream(self, mock_config):
"""Test loading and playing music streams."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
music_path = self.sounds_dir / "test_music.wav"
music_id = engine.load_music_stream(music_path, "bgm")
print(music_id)
self.assertEqual(music_id, "bgm")
self.assertIn("bgm", engine.music_streams)
# Play music
engine.play_music_stream("bgm", "music")
# Update music stream
engine.update_music_stream("bgm")
# Check if playing
is_playing = engine.is_music_stream_playing("bgm")
self.assertIsInstance(is_playing, bool)
# Stop music
engine.stop_music_stream("bgm")
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_music_time_functions(self, mock_config):
"""Test getting music time length and played."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
music_path = self.sounds_dir / "test_music.wav"
music_file = engine.load_music_stream(music_path, "bgm")
# Get time length
length = engine.get_music_time_length(music_file)
self.assertGreater(length, 0.0)
self.assertLess(length, 10.0) # Should be around 2 seconds
# Play and get time played
engine.play_music_stream(music_file, "music")
engine.update_music_stream(music_file)
import time
time.sleep(0.1)
engine.update_music_stream(music_file)
played = engine.get_music_time_played(music_file)
self.assertGreaterEqual(played, 0.0)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_music_volume_control(self, mock_config):
"""Test setting music volume."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
music_path = self.sounds_dir / "test_music.wav"
engine.load_music_stream(music_path, "bgm")
# Set volume (should not raise exception)
engine.set_music_volume("bgm", 0.6)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_seek_music_stream(self, mock_config):
"""Test seeking in music stream."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
music_path = self.sounds_dir / "test_music.wav"
engine.load_music_stream(music_path, "bgm")
# Seek to position (should not raise exception)
engine.seek_music_stream("bgm", 0.5)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_unload_music_stream(self, mock_config):
"""Test unloading music stream."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
music_path = self.sounds_dir / "test_music.wav"
engine.load_music_stream(music_path, "bgm")
self.assertIn("bgm", engine.music_streams)
engine.unload_music_stream("bgm")
self.assertNotIn("bgm", engine.music_streams)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_unload_all_music(self, mock_config):
"""Test unloading all music streams."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Load multiple music streams
music_path = self.sounds_dir / "test_music.wav"
engine.load_music_stream(music_path, "bgm1")
engine.load_music_stream(music_path, "bgm2")
self.assertEqual(len(engine.music_streams), 2)
engine.unload_all_music()
self.assertEqual(len(engine.music_streams), 0)
finally:
engine.close_audio_device()
@patch('libs.audio.get_config')
def test_host_api_functions(self, mock_config):
"""Test host API query functions."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
engine.init_audio_device()
# List host APIs (should not crash)
engine.list_host_apis()
# Get host API name
name = engine.get_host_api_name(0)
self.assertIsInstance(name, str)
@patch('libs.audio.get_config')
def test_full_lifecycle(self, mock_config):
"""Test complete audio engine lifecycle."""
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
if engine.init_audio_device():
try:
# Load sounds and music
engine.load_sound(self.sounds_dir / "test_sound.wav", "sfx")
engine.load_music_stream(self.sounds_dir / "test_music.wav", "bgm")
# Set volumes
engine.set_master_volume(0.8)
engine.set_sound_volume("sfx", 0.7)
engine.set_music_volume("bgm", 0.6)
# Play audio
engine.play_sound("sfx", "sound")
engine.play_music_stream("bgm", "music")
import time
time.sleep(0.1)
# Update music
engine.update_music_stream("bgm")
# Stop
engine.stop_sound("sfx")
engine.stop_music_stream("bgm")
# Cleanup
engine.unload_sound("sfx")
engine.unload_music_stream("bgm")
finally:
engine.close_audio_device()
if __name__ == '__main__':
# Run tests
unittest.main(verbosity=2)