"""audio_track_config 多轨道音频配置单测.""" import pytest from domain.audio_track_config import ( ALLOWED_AUDIO_EXTENSIONS, DEFAULT_VOLUMES, MAX_AUDIO_TRACKS, TRACK_TYPE_AMBIENT, TRACK_TYPE_BGM, TRACK_TYPE_MAIN, TRACK_TYPE_SFX, TRACK_TYPE_VOICEOVER, AudioTrack, MultiTrackMixConfig, clamp_volume, is_valid_audio_extension, ) # ── 常量测试 ────────────────────────────────────────────────────────────────── class TestConstants: """模块常量""" def test_track_type_constants(self): assert TRACK_TYPE_MAIN == "main" assert TRACK_TYPE_BGM == "bgm" assert TRACK_TYPE_VOICEOVER == "voiceover" assert TRACK_TYPE_SFX == "sfx" assert TRACK_TYPE_AMBIENT == "ambient" def test_max_tracks(self): assert MAX_AUDIO_TRACKS == 8 def test_default_volumes(self): assert DEFAULT_VOLUMES[TRACK_TYPE_MAIN] == 1.0 assert DEFAULT_VOLUMES[TRACK_TYPE_BGM] == 0.3 assert DEFAULT_VOLUMES[TRACK_TYPE_VOICEOVER] == 1.0 assert DEFAULT_VOLUMES[TRACK_TYPE_SFX] == 0.7 assert DEFAULT_VOLUMES[TRACK_TYPE_AMBIENT] == 0.2 def test_allowed_extensions(self): assert ".mp3" in ALLOWED_AUDIO_EXTENSIONS assert ".wav" in ALLOWED_AUDIO_EXTENSIONS assert ".aac" in ALLOWED_AUDIO_EXTENSIONS assert ".ogg" in ALLOWED_AUDIO_EXTENSIONS assert ".flac" in ALLOWED_AUDIO_EXTENSIONS assert ".m4a" in ALLOWED_AUDIO_EXTENSIONS assert ".wma" in ALLOWED_AUDIO_EXTENSIONS # ── AudioTrack ─────────────────────────────────────────────────────────────── class TestAudioTrackDefaults: """AudioTrack 默认值""" def test_default_values(self): t = AudioTrack() assert t.track_id == "" assert t.track_type == TRACK_TYPE_SFX assert t.audio_path == "" assert t.volume == 1.0 assert t.fade_in == 0.0 assert t.fade_out == 0.0 assert t.start_time == 0.0 assert t.duration == 0.0 assert t.enabled is True def test_custom_track(self): t = AudioTrack( track_id="bgm_001", track_type=TRACK_TYPE_BGM, audio_path="/music/bgm.mp3", volume=0.5, fade_in=1.5, fade_out=2.0, start_time=3.0, duration=30.0, enabled=False, ) assert t.track_id == "bgm_001" assert t.track_type == TRACK_TYPE_BGM assert t.audio_path == "/music/bgm.mp3" assert t.volume == 0.5 assert t.fade_in == 1.5 assert t.start_time == 3.0 assert t.duration == 30.0 assert t.enabled is False class TestAudioTrackFromDict: """AudioTrack.from_dict""" def test_empty_dict(self): t = AudioTrack.from_dict({}) assert t.track_type == TRACK_TYPE_SFX assert t.audio_path == "" assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_SFX] assert t.enabled is True def test_full_dict(self): t = AudioTrack.from_dict( { "track_id": "t1", "track_type": "bgm", "audio_path": "/a.mp3", "volume": 0.8, "fade_in": 1.0, "fade_out": 2.0, "start_time": 5.0, "duration": 60.0, "enabled": True, } ) assert t.track_id == "t1" assert t.track_type == "bgm" assert t.volume == 0.8 assert t.fade_in == 1.0 assert t.duration == 60.0 def test_volume_clamped_to_zero(self): t = AudioTrack.from_dict({"volume": -0.5}) assert t.volume == 0.0 def test_volume_clamped_to_two(self): t = AudioTrack.from_dict({"volume": 3.0}) assert t.volume == 2.0 def test_invalid_volume_falls_back_to_default(self): t = AudioTrack.from_dict({"track_type": "bgm", "volume": "abc"}) assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_BGM] def test_invalid_fade_in_falls_back(self): t = AudioTrack.from_dict({"fade_in": "bad"}) assert t.fade_in == 0.0 def test_negative_fade_in_clamped(self): t = AudioTrack.from_dict({"fade_in": -1.0}) assert t.fade_in == 0.0 def test_invalid_fade_out_falls_back(self): t = AudioTrack.from_dict({"fade_out": None}) assert t.fade_out == 0.0 def test_negative_start_time_clamped(self): t = AudioTrack.from_dict({"start_time": -5.0}) assert t.start_time == 0.0 def test_invalid_duration_falls_back(self): t = AudioTrack.from_dict({"duration": "long"}) assert t.duration == 0.0 def test_bgm_default_volume(self): t = AudioTrack.from_dict({"track_type": "bgm"}) assert t.volume == 0.3 def test_main_default_volume(self): t = AudioTrack.from_dict({"track_type": "main"}) assert t.volume == 1.0 def test_voiceover_default_volume(self): t = AudioTrack.from_dict({"track_type": "voiceover"}) assert t.volume == 1.0 def test_ambient_default_volume(self): t = AudioTrack.from_dict({"track_type": "ambient"}) assert t.volume == 0.2 def test_unknown_type_default_volume(self): t = AudioTrack.from_dict({"track_type": "unknown_type"}) assert t.volume == 1.0 def test_enabled_false(self): t = AudioTrack.from_dict({"enabled": False}) assert t.enabled is False class TestAudioTrackValidate: """AudioTrack.validate""" def test_empty_path_invalid(self): t = AudioTrack(audio_path="") valid, msg = t.validate() assert valid is False assert "audio_path" in msg def test_valid_track(self): t = AudioTrack(audio_path="/a.mp3", volume=0.5) valid, msg = t.validate() assert valid is True assert msg == "" def test_volume_below_zero_invalid(self): t = AudioTrack(audio_path="/a.mp3", volume=-0.1) valid, msg = t.validate() assert valid is False assert "volume" in msg def test_volume_above_two_invalid(self): t = AudioTrack(audio_path="/a.mp3", volume=2.1) valid, msg = t.validate() assert valid is False assert "volume" in msg def test_volume_zero_valid(self): t = AudioTrack(audio_path="/a.mp3", volume=0.0) valid, _ = t.validate() assert valid is True def test_volume_two_valid(self): t = AudioTrack(audio_path="/a.mp3", volume=2.0) valid, _ = t.validate() assert valid is True def test_negative_fade_in_invalid(self): t = AudioTrack(audio_path="/a.mp3", fade_in=-1.0) valid, msg = t.validate() assert valid is False assert "fade_in" in msg def test_negative_fade_out_invalid(self): t = AudioTrack(audio_path="/a.mp3", fade_out=-1.0) valid, msg = t.validate() assert valid is False assert "fade_out" in msg def test_negative_start_time_invalid(self): t = AudioTrack(audio_path="/a.mp3", start_time=-0.5) valid, msg = t.validate() assert valid is False assert "start_time" in msg def test_negative_duration_invalid(self): t = AudioTrack(audio_path="/a.mp3", duration=-1.0) valid, msg = t.validate() assert valid is False assert "duration" in msg class TestAudioTrackIsEffective: """AudioTrack.is_effective 属性""" def test_enabled_with_path_effective(self): t = AudioTrack(audio_path="/a.mp3", enabled=True) assert t.is_effective is True def test_disabled_not_effective(self): t = AudioTrack(audio_path="/a.mp3", enabled=False) assert t.is_effective is False def test_no_path_not_effective(self): t = AudioTrack(audio_path="", enabled=True) assert t.is_effective is False def test_disabled_no_path_not_effective(self): t = AudioTrack(audio_path="", enabled=False) assert t.is_effective is False # ── MultiTrackMixConfig ────────────────────────────────────────────────────── class TestMultiTrackMixConfigDefaults: """MultiTrackMixConfig 默认值""" def test_default_values(self): c = MultiTrackMixConfig() assert c.tracks == [] assert c.master_volume == 1.0 assert c.normalize is True assert c.max_output_volume == 1.5 def test_custom_config(self): t1 = AudioTrack(track_id="t1", audio_path="/a.mp3") c = MultiTrackMixConfig( tracks=[t1], master_volume=0.8, normalize=False, max_output_volume=2.0, ) assert len(c.tracks) == 1 assert c.master_volume == 0.8 assert c.normalize is False assert c.max_output_volume == 2.0 class TestMultiTrackFromConfigDict: """MultiTrackMixConfig.from_config_dict""" def test_none_returns_default(self): c = MultiTrackMixConfig.from_config_dict(None) assert len(c.tracks) == 0 assert c.master_volume == 1.0 def test_empty_dict_returns_default(self): c = MultiTrackMixConfig.from_config_dict({}) assert len(c.tracks) == 0 def test_non_dict_returns_default(self): c = MultiTrackMixConfig.from_config_dict("not a dict") assert len(c.tracks) == 0 def test_single_track(self): c = MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "t1", "track_type": "bgm", "audio_path": "/bgm.mp3", "volume": 0.5}, ], } ) assert len(c.tracks) == 1 assert c.tracks[0].track_id == "t1" assert c.tracks[0].volume == 0.5 def test_multiple_tracks(self): c = MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "t1", "track_type": "main", "audio_path": "/main.wav"}, {"track_id": "t2", "track_type": "bgm", "audio_path": "/bgm.mp3"}, {"track_id": "t3", "track_type": "sfx", "audio_path": "/sfx.wav"}, ], } ) assert len(c.tracks) == 3 assert c.tracks[0].track_type == "main" assert c.tracks[1].track_type == "bgm" assert c.tracks[2].track_type == "sfx" def test_skip_disabled_tracks(self): c = MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "t1", "audio_path": "/a.mp3", "enabled": True}, {"track_id": "t2", "audio_path": "/b.mp3", "enabled": False}, {"track_id": "t3", "audio_path": "/c.mp3"}, ], } ) assert len(c.tracks) == 2 ids = [t.track_id for t in c.tracks] assert "t1" in ids assert "t2" not in ids assert "t3" in ids def test_skip_no_path_tracks(self): c = MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "t1", "audio_path": "/a.mp3"}, {"track_id": "t2", "audio_path": ""}, {"track_id": "t3"}, ], } ) assert len(c.tracks) == 1 assert c.tracks[0].track_id == "t1" def test_skip_non_dict_tracks(self): c = MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "t1", "audio_path": "/a.mp3"}, "not a dict", 123, None, ], } ) assert len(c.tracks) == 1 def test_master_volume_clamped(self): c = MultiTrackMixConfig.from_config_dict({"master_volume": 3.0}) assert c.master_volume == 2.0 def test_master_volume_negative_clamped(self): c = MultiTrackMixConfig.from_config_dict({"master_volume": -1.0}) assert c.master_volume == 0.0 def test_invalid_master_volume_falls_back(self): c = MultiTrackMixConfig.from_config_dict({"master_volume": "high"}) assert c.master_volume == 1.0 def test_normalize_false(self): c = MultiTrackMixConfig.from_config_dict({"normalize": False}) assert c.normalize is False def test_max_output_volume_custom(self): c = MultiTrackMixConfig.from_config_dict({"max_output_volume": 2.0}) assert c.max_output_volume == 2.0 def test_invalid_max_output_volume_falls_back(self): c = MultiTrackMixConfig.from_config_dict({"max_output_volume": "big"}) assert c.max_output_volume == 1.5 def test_tracks_not_list_ignored(self): c = MultiTrackMixConfig.from_config_dict({"tracks": "not a list"}) assert len(c.tracks) == 0 class TestMultiTrackProperties: """MultiTrackMixConfig 属性方法""" def _make_config(self): return MultiTrackMixConfig.from_config_dict( { "tracks": [ {"track_id": "m1", "track_type": "main", "audio_path": "/m.wav"}, {"track_id": "b1", "track_type": "bgm", "audio_path": "/b1.mp3"}, {"track_id": "b2", "track_type": "bgm", "audio_path": "/b2.mp3", "enabled": False}, {"track_id": "s1", "track_type": "sfx", "audio_path": "/s.wav"}, {"track_id": "x", "track_type": "ambient", "audio_path": ""}, ], } ) def test_has_effect_true(self): c = self._make_config() assert c.has_effect is True def test_has_effect_false(self): c = MultiTrackMixConfig() assert c.has_effect is False def test_effective_track_count(self): c = self._make_config() # m1 + b1 + s1 = 3个有效(b2禁用,x无路径) assert c.effective_track_count == 3 def test_main_tracks(self): c = self._make_config() mains = c.main_tracks assert len(mains) == 1 assert mains[0].track_id == "m1" def test_bgm_tracks(self): c = self._make_config() bgms = c.bgm_tracks assert len(bgms) == 1 # 只有b1有效 assert bgms[0].track_id == "b1" def test_empty_tracks(self): c = MultiTrackMixConfig() assert c.effective_track_count == 0 assert c.main_tracks == [] assert c.bgm_tracks == [] # ── 工具函数 ───────────────────────────────────────────────────────────────── class TestIsValidAudioExtension: """is_valid_audio_extension 函数""" def test_mp3(self): assert is_valid_audio_extension("song.mp3") is True def test_wav(self): assert is_valid_audio_extension("sound.wav") is True def test_aac(self): assert is_valid_audio_extension("audio.aac") is True def test_ogg(self): assert is_valid_audio_extension("music.ogg") is True def test_flac(self): assert is_valid_audio_extension("lossless.flac") is True def test_m4a(self): assert is_valid_audio_extension("apple.m4a") is True def test_wma(self): assert is_valid_audio_extension("windows.wma") is True def test_uppercase_extension(self): assert is_valid_audio_extension("SONG.MP3") is True def test_mixed_case_extension(self): assert is_valid_audio_extension("song.Mp3") is True def test_mp4_not_valid(self): assert is_valid_audio_extension("video.mp4") is False def test_txt_not_valid(self): assert is_valid_audio_extension("notes.txt") is False def test_no_extension(self): assert is_valid_audio_extension("README") is False def test_full_path(self): assert is_valid_audio_extension("/home/user/music/song.mp3") is True class TestClampVolume: """clamp_volume 函数""" def test_within_range(self): assert clamp_volume(0.5) == 0.5 def test_exact_min(self): assert clamp_volume(0.0) == 0.0 def test_exact_max(self): assert clamp_volume(2.0) == 2.0 def test_below_min(self): assert clamp_volume(-1.0) == 0.0 def test_above_max(self): assert clamp_volume(3.0) == 2.0 def test_custom_bounds(self): assert clamp_volume(5.0, min_vol=1.0, max_vol=10.0) == 5.0 def test_custom_below_min(self): assert clamp_volume(0.5, min_vol=1.0) == 1.0 def test_custom_above_max(self): assert clamp_volume(15.0, max_vol=10.0) == 10.0