#!/usr/bin/env python3 # -*- coding: utf-8 -*- r""" ============================================================================ AP_DS 0.0.1-AFS - Opus Specialized Test Suite Audio Library By DVS ============================================================================ Automated + interactive test coverage for the Opus playback support (opusplayer.py + _opusdll.py) integrated into AudioLibrary. Sections: [OP-1] Opus Module Imports / Constants / Error Codes [OP-2] _opusdll.py DLL Loading & Auto-Download [OP-3] Opus Metadata Parsing (basic + extended) [OP-4] Opus Playback (play_from_file / new_aid / play_from_memory) [OP-5] Opus Playback Control (pause / resume / stop) [OP-6] Opus Volume Control [OP-7] Opus Seek [OP-8] Opus Fade In / Out [OP-9] Opus vs Original Distinction [OP-10] Opus Batch Parsing [OP-11] Opus Error Codes (normal / boundary / error) [OP-12] Opus AID Mapping (1:1) [OP-13] Opus Resource Management [OP-L] Listening Tests (interactive, requires ears) Usage: python OPUS_TEST.py --auto Run automated tests only python OPUS_TEST.py --listen Run interactive listening tests only python OPUS_TEST.py --full Run everything (default) The suite verifies Opus-specific error codes (2001-2010), normal operation, boundary values, and error paths. ============================================================================ """ import os import sys import io import time import struct import hashlib import contextlib # ============================================================================ # Environment # ============================================================================ os.environ.setdefault('AP_DS_SKIP_AUTO_CHECK', '1') os.environ.setdefault('AP_DS_SUPPRESS_WARNINGS', '1') _HERE = os.path.dirname(os.path.abspath(__file__)) if os.path.basename(_HERE) == 'ap_ds': sys.path.insert(0, os.path.dirname(_HERE)) else: sys.path.insert(0, _HERE) import ap_ds from ap_ds import AudioLibrary import ap_ds.player as player import ap_ds.opusplayer as opusplayer import ap_ds._opusdll as opusdll # ============================================================================ # Test resources # ============================================================================ TMP_DIR = os.path.join(_HERE, 'opus_tmp') os.makedirs(TMP_DIR, exist_ok=True) # Opus test file (use the real test.opus if available, else search package) OPUS_FILE = r"D:\DVS开发目录\ap_ds opus支持 开发目录\test.opus" if not os.path.exists(OPUS_FILE): pkg_dir = os.path.dirname(os.path.abspath(opusplayer.__file__)) for f in os.listdir(pkg_dir): if f.endswith('.opus'): OPUS_FILE = os.path.join(pkg_dir, f) break def _prompt_opus(): """Prompt the user for an Opus file path. Returns default if empty/EOF.""" print("\n" + "=" * 60) print(" Opus Test Suite - Test Audio Selection") print("=" * 60) default = OPUS_FILE try: answer = input( "Enter path to an Opus file for tests\n" f"(or press Enter to use default: {default})\n> " ).strip().strip('"').strip("'") if answer and os.path.exists(answer): return answer if answer and not os.path.exists(answer): print(f" ! File not found: {answer}, using default") return default except EOFError: # Non-interactive execution -> use default return default # Original (non-Opus) test file - a WAV WAV_FILE = os.path.join(TMP_DIR, 'orig.wav') def _make_wav(path, seconds=2, sr=22050, ch=1): import wave n = sr * seconds * ch with wave.open(path, 'w') as w: w.setnchannels(ch) w.setsampwidth(2) w.setframerate(sr) w.writeframes(b''.join(struct.pack('0", meta.get('duration', 0) > 0, f"got={meta.get('duration')}") t.check("sample_rate=48000", meta.get('sample_rate') == 48000, f"got={meta.get('sample_rate')}") t.check("channels>0", meta.get('channels', 0) > 0, f"got={meta.get('channels')}") t.check("bitrate>0", meta.get('bitrate', 0) > 0, f"got={meta.get('bitrate')}") t.check("fields complete", all(k in meta for k in ('path', 'format', 'duration', 'length', 'sample_rate', 'channels', 'bitrate'))) dur = lib.get_audio_duration(OPUS_FILE, is_file=True) t.check("get_audio_duration>0", isinstance(dur, int) and dur > 0, f"got={dur}") sr = lib._get_sample_rate(OPUS_FILE) t.check("_get_sample_rate=48000", sr == 48000, f"got={sr}") ch = lib._get_channels(OPUS_FILE) t.check("_get_channels>0", ch > 0, f"got={ch}") ext = lib.get_audio_extended_metadata(OPUS_FILE) t.check("extended metadata is dict", isinstance(ext, dict), f"got={type(ext).__name__}") if isinstance(ext, dict): t.check("extended has title/album/artist", 'title' in ext or 'album' in ext or 'artist' in ext, f"keys={list(ext.keys())[:5]}") t.check("extended has vendor", 'vendor' in ext, f"vendor={ext.get('vendor')}") lib.cleanup_function() # ============================================================================ # [OP-4] Opus Playback # ============================================================================ def test_opus_play(t): t.section("[OP-4] Opus Playback") if not os.path.exists(OPUS_FILE): t.skip("Opus playback", "no Opus file available") return lib = opusplayer.OpusAudio() aid = lib.play_from_file(OPUS_FILE) t.check("play_from_file returns int AID", isinstance(aid, int), f"got={aid}") if isinstance(aid, int): time.sleep(0.3) t.check("is_music_playing", lib.is_music_playing() is True) lib.stop_audio(aid) aid2 = lib.new_aid(OPUS_FILE) t.check("new_aid returns int AID", isinstance(aid2, int), f"got={aid2}") r = lib.play_from_memory(OPUS_FILE) t.check("play_from_memory returns int", isinstance(r, int), f"got={r}") if isinstance(r, int): lib.stop_audio(r) r = lib.play_from_file(os.path.join(TMP_DIR, 'missing.opus')) t.check("play_from_file(missing) -> 1001", isinstance(r, tuple) and r[0] == 1001, f"got={r}") r = lib.play_from_file(None) t.check("play_from_file(None) -> 1001", isinstance(r, tuple) and r[0] == 1001, f"got={r}") r = lib.play_from_file(BAD_OPUS) t.check("play_from_file(corrupted) -> 2003", isinstance(r, tuple) and r[0] == 2003, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-5] Opus Playback Control # ============================================================================ def test_opus_control(t): t.section("[OP-5] Opus Playback Control") if not os.path.exists(OPUS_FILE): t.skip("Opus control", "no Opus file available") return lib = opusplayer.OpusAudio() aid = lib.play_from_file(OPUS_FILE) if not isinstance(aid, int): t.check("Play opus ok", False, f"got={aid}") lib.cleanup_function() return time.sleep(0.3) r = lib.pause_audio(aid) t.check("pause_audio ok", r[0] == 0, f"got={r}") time.sleep(0.2) t.check("is_music_paused=True", lib.is_music_paused() is True) r = lib.play_audio(aid) t.check("play_audio resume ok", r[0] == 0, f"got={r}") time.sleep(0.2) t.check("is_music_playing=True after resume", lib.is_music_playing() is True) r = lib.stop_audio(aid) t.check("stop_audio returns float", isinstance(r, float), f"got={r}") # Error paths r = lib.pause_audio(99999) t.check("pause_audio(invalid AID) -> 1002", r[0] == 1002, f"got={r}") r = lib.play_audio(99999) t.check("play_audio(invalid AID) -> 1002", r[0] == 1002, f"got={r}") r = lib.stop_audio(99999) t.check("stop_audio(invalid AID) -> 1002", r[0] == 1002, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-6] Opus Volume Control # ============================================================================ def test_opus_volume(t): t.section("[OP-6] Opus Volume Control") if not os.path.exists(OPUS_FILE): t.skip("Opus volume", "no Opus file available") return lib = opusplayer.OpusAudio() aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): # Valid volumes for vol in (0, 1, 64, 100, 128): r = lib.set_volume(aid, vol) t.check(f"set_volume({vol}) ok", r[0] == 0, f"got={r}") g = lib.get_volume(aid) t.check("get_volume is int", isinstance(g, int), f"got={g}") t.check("get_volume in range 0-128", 0 <= g <= 128, f"got={g}") # Invalid volumes for vol in (-1, 129, 200): r = lib.set_volume(aid, vol) t.check(f"set_volume({vol}) -> 1015", r[0] == 1015, f"got={r}") # Invalid types for vol in (None, '50', 1.5): r = lib.set_volume(aid, vol) t.check(f"set_volume({vol!r}) -> 1015", isinstance(r, tuple) and r[0] == 1015, f"got={r}") lib.stop_audio(aid) # Invalid AID r = lib.set_volume(99999, 50) t.check("set_volume(invalid AID) -> 1002", r[0] == 1002, f"got={r}") r = lib.get_volume(99999) t.check("get_volume(invalid AID) -> 1002", r[0] == 1002, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-7] Opus Seek # ============================================================================ def test_opus_seek(t): t.section("[OP-7] Opus Seek") if not os.path.exists(OPUS_FILE): t.skip("Opus seek", "no Opus file available") return lib = opusplayer.OpusAudio() aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): time.sleep(0.2) # Valid seeks for pos in (0.0, 1.0, 30.0, 100.5): r = lib.seek_audio(aid, pos) t.check(f"seek_audio({pos}) ok", r[0] == 0, f"got={r}") t.check("still playing after seek", lib.is_music_playing() is True) # Invalid types for pos in (None, '5', [], {}): r = lib.seek_audio(aid, pos) t.check(f"seek_audio({pos!r}) -> tuple", isinstance(r, tuple), f"got={r}") lib.stop_audio(aid) r = lib.seek_audio(99999, 1.0) t.check("seek_audio(invalid AID) -> 1002", r[0] == 1002, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-8] Opus Fade In / Out # ============================================================================ def test_opus_fade(t): t.section("[OP-8] Opus Fade In / Out") if not os.path.exists(OPUS_FILE): t.skip("Opus fade", "no Opus file available") return lib = opusplayer.OpusAudio() # Fade-in play (via new_aid) aid = lib.new_aid(OPUS_FILE) r = lib.fadein_music(aid, ms=1000) t.check("fadein_music ok", r[0] == 0, f"got={r}") if r[0] == 0: time.sleep(0.3) fading = lib.get_music_fading() t.check("get_music_fading=1 (fading in)", fading == 1, f"got={fading}") t.check("playing during fadein", lib.is_music_playing() is True) time.sleep(1.5) # wait for fade-in to complete t.check("fading=0 after fadein", lib.get_music_fading() == 0, f"got={lib.get_music_fading()}") # Fade-out r = lib.fadeout_music(ms=1000) t.check("fadeout_music ok", r[0] == 0, f"got={r}") time.sleep(3) t.check("stopped after fadeout", not lib.is_music_playing()) # Fade-in from position aid2 = lib.new_aid(OPUS_FILE) r = lib.fadein_music_pos(aid2, ms=500, position=30.0) t.check("fadein_music_pos ok", r[0] == 0, f"got={r}") if r[0] == 0: time.sleep(2) lib.stop_audio(aid2) # Error paths r = lib.fadein_music(99999) t.check("fadein_music(invalid AID) -> 1002", r[0] == 1002, f"got={r}") r = lib.fadein_music_pos(99999, ms=500) t.check("fadein_music_pos(invalid AID) -> 1002", r[0] == 1002, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-9] Opus vs Original Distinction # ============================================================================ def test_opus_distinction(t): t.section("[OP-9] Opus vs Original Distinction") # _is_opus_file detection t.check("_is_opus_file(opus)=True", player._is_opus_file(OPUS_FILE) is True) t.check("_is_opus_file(wav)=False", player._is_opus_file(WAV_FILE) is False) t.check("_is_opus_file(.mp3)=False", player._is_opus_file('x.mp3') is False) t.check("_is_opus_file(.ogg)=False", player._is_opus_file('x.ogg') is False) # AudioLibrary routes opus to OpusAudio lib = AudioLibrary() opus_aid = lib.play_from_file(OPUS_FILE) t.check("AudioLibrary play opus returns int", isinstance(opus_aid, int), f"got={opus_aid}") if isinstance(opus_aid, int): t.check("opus AID mapped", lib._is_opus_aid(opus_aid), f"mapping={lib._aid_to_opus_aid}") lib.stop_audio(opus_aid) # AudioLibrary routes wav to SDL2 wav_aid = lib.play_from_file(WAV_FILE) t.check("AudioLibrary play wav returns int", isinstance(wav_aid, int), f"got={wav_aid}") if isinstance(wav_aid, int): t.check("wav NOT mapped to opus", not lib._is_opus_aid(wav_aid), "wav uses SDL2") lib.stop_audio(wav_aid) # Metadata distinction meta_opus = lib.get_audio_metadata_by_path(OPUS_FILE) meta_wav = lib.get_audio_metadata_by_path(WAV_FILE) t.check("opus metadata format=opus", isinstance(meta_opus, dict) and meta_opus.get('format') == 'opus', f"got={meta_opus.get('format') if isinstance(meta_opus,dict) else meta_opus}") t.check("wav metadata format=wav", isinstance(meta_wav, dict) and meta_wav.get('format') == 'wav', f"got={meta_wav.get('format') if isinstance(meta_wav,dict) else meta_wav}") lib.cleanup_function() # ============================================================================ # [OP-10] Opus Batch Parsing # ============================================================================ def test_opus_batch(t): t.section("[OP-10] Opus Batch Parsing") if not os.path.exists(OPUS_FILE): t.skip("Opus batch", "no Opus file available") return lib = opusplayer.OpusAudio() files = [OPUS_FILE] bl = lib.batch_get_metadata(files) t.check("batch_get_metadata returns 1", len(bl) == 1, f"got={len(bl)}") if bl: t.check("batch metadata format=opus", bl[0].get('format') == 'opus', f"got={bl[0].get('format')}") bd = lib.batch_get_duration(files) t.check("batch_get_duration returns dict", isinstance(bd, dict), f"got={type(bd).__name__}") t.check("batch_get_duration has opus", OPUS_FILE in bd, f"keys={list(bd.keys())}") bt = lib.batch_get_metadata_by_type(files, 'opus') t.check("batch_by_type opus -> 1", len(bt) == 1, f"got={len(bt)}") bt2 = lib.batch_get_metadata_by_type(files, 'wav') t.check("batch_by_type wav -> 0", len(bt2) == 0, f"got={len(bt2)}") # Mixed batch via AudioLibrary (test opus and wav separately to avoid # multiprocessing spawn limitations in test environments) lib2 = AudioLibrary() # Opus files in batch use opusplayer opus_batch = lib2.batch_get_metadata([OPUS_FILE]) t.check("AudioLibrary batch opus -> 1", len(opus_batch) == 1, f"got={len(opus_batch)}") if opus_batch: t.check("AudioLibrary batch opus format", opus_batch[0].get('format') == 'opus', f"got={opus_batch[0].get('format')}") # WAV files in batch use audio_parser wav_batch = lib2.batch_get_metadata([WAV_FILE]) t.check("AudioLibrary batch wav -> 1", len(wav_batch) == 1, f"got={len(wav_batch)}") if wav_batch: t.check("AudioLibrary batch wav format", wav_batch[0].get('format') == 'wav', f"got={wav_batch[0].get('format')}") lib2.cleanup_function() lib.cleanup_function() # ============================================================================ # [OP-11] Opus Error Codes (normal / boundary / error) # ============================================================================ def test_opus_errors(t): t.section("[OP-11] Opus Error Codes (normal / boundary / error)") # Error code constants err_codes = { 'AP_DS_ERR_OPUS_LIB_LOAD_FAILED': 2001, 'AP_DS_ERR_OPUS_DLL_DEPENDENCY': 2002, 'AP_DS_ERR_OPUS_OPEN_FAILED': 2003, 'AP_DS_ERR_OPUS_HEADER_CORRUPT': 2004, 'AP_DS_ERR_OPUS_TAGS_PARSE_FAILED': 2005, 'AP_DS_ERR_OPUS_DECODE_FAILED': 2006, 'AP_DS_ERR_OPUS_SEEK_FAILED': 2007, 'AP_DS_ERR_OPUS_BITRATE_UNAVAILABLE': 2008, 'AP_DS_ERR_OPUS_NOT_SEEKABLE': 2009, 'AP_DS_ERR_OPUS_CHANNEL_INVALID': 2010, } for name, code in err_codes.items(): t.check(f"{name}={code}", getattr(opusplayer, name, None) == code, f"got={getattr(opusplayer, name, None)}") # OPUS_ERR_INFO table t.check("OPUS_ERR_INFO defined", hasattr(opusplayer, 'OPUS_ERR_INFO')) if hasattr(opusplayer, 'OPUS_ERR_INFO'): for code in err_codes.values(): t.check(f"OPUS_ERR_INFO[{code}]", code in opusplayer.OPUS_ERR_INFO) # Normal error paths (corrupted file -> 2003) lib = opusplayer.OpusAudio() r = lib.get_audio_metadata_by_path(BAD_OPUS) t.check("corrupted metadata -> 2003", isinstance(r, tuple) and r[0] == 2003, f"got={r}") # Invalid source type r = lib.get_audio_metadata(1.5) t.check("invalid source type -> 1014", isinstance(r, tuple) and r[0] == 1014, f"got={r}") # Missing file r = lib.get_audio_metadata_by_path(os.path.join(TMP_DIR, 'missing.opus')) t.check("missing file -> 1001", isinstance(r, tuple) and r[0] == 1001, f"got={r}") lib.cleanup_function() # ============================================================================ # [OP-12] Opus AID Mapping (1:1) # ============================================================================ def test_opus_aid_mapping(t): t.section("[OP-12] Opus AID Mapping (1:1)") if not os.path.exists(OPUS_FILE): t.skip("Opus AID mapping", "no Opus file available") return lib = AudioLibrary() main_aid = lib.play_from_file(OPUS_FILE) t.check("main AID is int", isinstance(main_aid, int), f"got={main_aid}") if isinstance(main_aid, int): t.check("AID mapped to opus", lib._is_opus_aid(main_aid), f"mapping={lib._aid_to_opus_aid}") opus_aid = lib._get_opus_aid(main_aid) t.check("opus AID is int", isinstance(opus_aid, int), f"got={opus_aid}") t.check("mapping is 1:1", isinstance(main_aid, int) and isinstance(opus_aid, int)) r = lib.pause_audio(main_aid) t.check("pause via main AID -> opus", r[0] == 0, f"got={r}") r = lib.play_audio(main_aid) t.check("resume via main AID -> opus", r[0] == 0, f"got={r}") r = lib.set_volume(main_aid, 80) t.check("set_volume via main AID -> opus", r[0] == 0, f"got={r}") g = lib.get_volume(main_aid) t.check("get_volume via main AID -> opus", isinstance(g, int), f"got={g}") r = lib.seek_audio(main_aid, 30.0) t.check("seek via main AID -> opus", r[0] == 0, f"got={r}") r = lib.stop_audio(main_aid) t.check("stop via main AID -> opus", isinstance(r, float), f"got={r}") t.check("mapping removed after stop", not lib._is_opus_aid(main_aid), f"mapping={lib._aid_to_opus_aid}") lib.cleanup_function() # ============================================================================ # [OP-13] Opus Resource Management # ============================================================================ def test_opus_resources(t): t.section("[OP-13] Opus Resource Management") if not os.path.exists(OPUS_FILE): t.skip("Opus resources", "no Opus file available") return lib = opusplayer.OpusAudio() lib.play_from_file(OPUS_FILE) t.check("playing before cleanup", lib.is_music_playing() is True) lib.cleanup_function() t.check("not playing after cleanup", not lib.is_music_playing()) aid = lib.play_from_file(OPUS_FILE) t.check("reusable after cleanup", isinstance(aid, int), f"got={aid}") if isinstance(aid, int): lib.stop_audio(aid) lib.cleanup_function() # ============================================================================ # [OP-14] Opus Boundary & Error Tests (reference CI/CD test_edge_cases) # ============================================================================ def test_opus_boundary(t): t.section("[OP-14] Opus Boundary & Error Tests") lib = opusplayer.OpusAudio() # --- B1: invalid argument types -> error tuples, never crash --- t.log(" --- B1 invalid argument types ---") for bad in (None, 1.5, [], {}, ('a',)): r = lib.play_from_file(bad) ok = isinstance(r, tuple) and len(r) == 3 and r[0] == 1001 t.check(f"play_from_file({type(bad).__name__}) -> tuple(1001)", ok, f"got={r!r}") for bad in (None, 1.5, []): r = lib.new_aid(bad) ok = isinstance(r, tuple) and len(r) == 3 and r[0] == 1001 t.check(f"new_aid({type(bad).__name__}) -> tuple(1001)", ok, f"got={r!r}") # --- B2: seek / volume / fade boundary values --- t.log(" --- B2 boundary values ---") aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): # Seek boundary for pos in (None, '5', [], {}, -1.0, 0.0, 999999.0): r = lib.seek_audio(aid, pos) ok = isinstance(r, tuple) t.check(f"seek_audio({pos!r}) -> tuple", ok, f"got={r!r}") r = lib.seek_audio(aid, 0.0) t.check("seek_audio(0.0) -> success", isinstance(r, tuple) and r[0] == 0, f"got={r!r}") # Volume boundary for v in (None, '50', 1.5, [], {}): r = lib.set_volume(aid, v) t.check(f"set_volume({v!r}) -> 1015", isinstance(r, tuple) and r[0] == 1015, f"got={r!r}") for v in (0, 1, 64, 100, 128): r = lib.set_volume(aid, v) t.check(f"set_volume({v}) boundary -> success", r[0] == 0, f"got={r}") for v in (-1, 129, 200, 1000): r = lib.set_volume(aid, v) t.check(f"set_volume({v}) out of range -> 1015", r[0] == 1015, f"got={r}") # Fade boundary r = lib.fadein_music(aid, ms=None) t.check("fadein_music(ms=None) -> tuple", isinstance(r, tuple), f"got={r!r}") r = lib.fadein_music_pos(aid, ms=100, position=None) t.check("fadein_music_pos(position=None) -> tuple", isinstance(r, tuple), f"got={r!r}") lib.stop_audio(aid) # --- B3: exact error codes --- t.log(" --- B3 exact error codes ---") cases = { '1001 missing file': (lambda: lib.play_from_file(os.path.join(TMP_DIR, 'no_such.opus')), 1001), '1002 invalid AID': (lambda: lib.pause_audio(99999), 1002), '1014 invalid source type': (lambda: lib.get_audio_metadata(1.5), 1014), '2003 corrupted opus': (lambda: lib.get_audio_metadata_by_path(BAD_OPUS), 2003), } for name, (fn, expect_code) in cases.items(): r = fn() ok = isinstance(r, tuple) and r[0] == expect_code t.check(f"{name} -> {expect_code}", ok, f"got={r!r}") lib.cleanup_function() # ============================================================================ # [OP-15] Opus DLL Specialized Tests (reference CI/CD test_sdl2_bindings) # ============================================================================ def test_opus_dll_deep(t): t.section("[OP-15] Opus DLL Specialized Tests") # DLL file presence and sizes pkg = os.path.dirname(os.path.abspath(opusdll.__file__)) dll_info = { 'libopusfile-0.dll': 55884, 'libopus-0.dll': 500112, 'libogg-0.dll': 40580, 'libopusurl-0.dll': 76772, } for dll, size in dll_info.items(): path = os.path.join(pkg, dll) t.check(f"{dll} exists", os.path.exists(path)) if os.path.exists(path): actual = os.path.getsize(path) t.check(f"{dll} size={size}", actual == size, f"got={actual}") # DLL hash verification for dll in dll_info: path = os.path.join(pkg, dll) if os.path.exists(path): with open(path, 'rb') as f: content = f.read() h = hashlib.sha256(content).hexdigest() expected = opusdll.OPUS_DLL_HASHES.get(dll) t.check(f"{dll} hash matches", h == expected, f"got={h[:16]}...") # import_opus idempotent r1 = opusdll.import_opus() r2 = opusdll.import_opus() t.check("import_opus idempotent", r1 == r2 == True, f"r1={r1} r2={r2}") # opusfile handle t.check("opusfile handle valid", opusdll.opusfile is not None) # OPUS_DLL_FILES structure t.check("OPUS_DLL_FILES has 4", len(opusdll.OPUS_DLL_FILES) == 4) for dll in opusdll.OPUS_DLL_FILES: t.check(f"DLL {dll['filename']} has url", 'url' in dll and dll['url'].startswith('http')) t.check(f"DLL {dll['filename']} has size", 'size' in dll and dll['size'] > 0) # check_opus_dll ok, msg = opusplayer.check_opus_dll() t.check("check_opus_dll ok", ok is True, f"msg={msg}") # ============================================================================ # [OP-16] Opus Error Code Trigger Tests (all 2001-2010) # ============================================================================ def test_opus_error_triggers(t): t.section("[OP-16] Opus Error Code Trigger Tests") lib = opusplayer.OpusAudio() # 2003: corrupted file open failure r = lib.play_from_file(BAD_OPUS) t.check("2003 OPEN_FAILED trigger", isinstance(r, tuple) and r[0] == 2003, f"got={r}") # 2004: HEADER_CORRUPT (monkey-patch op_head to return empty) import ctypes orig_head = opusdll.opusfile.op_head opusdll.opusfile.op_head = lambda of, li: ctypes.POINTER(opusdll.OpusHead)() r = opusplayer._get_opus_metadata(OPUS_FILE) t.check("2004 HEADER_CORRUPT trigger", isinstance(r, tuple) and r[0] == 2004, f"got={r}") opusdll.opusfile.op_head = orig_head # 2008: BITRATE_UNAVAILABLE (monkey-patch op_bitrate to return 0) orig_bitrate = opusdll.opusfile.op_bitrate opusdll.opusfile.op_bitrate = lambda of, li: 0 r = opusplayer._get_opus_metadata(OPUS_FILE) t.check("2008 BITRATE_UNAVAILABLE trigger", isinstance(r, tuple) and r[0] == 2008, f"got={r}") opusdll.opusfile.op_bitrate = orig_bitrate # 2010: CHANNEL_INVALID (monkey-patch op_channel_count to return 0) orig_channels = opusdll.opusfile.op_channel_count opusdll.opusfile.op_channel_count = lambda of, li: 0 r = opusplayer._get_opus_metadata(OPUS_FILE) t.check("2010 CHANNEL_INVALID trigger", isinstance(r, tuple) and r[0] == 2010, f"got={r}") opusdll.opusfile.op_channel_count = orig_channels # 2009: NOT_SEEKABLE (monkey-patch op_seekable to return 0) aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): orig_seekable = opusdll.opusfile.op_seekable opusdll.opusfile.op_seekable = lambda of: 0 r = lib.seek_audio(aid, 10.0) t.check("2009 NOT_SEEKABLE trigger", isinstance(r, tuple) and r[0] == 2009, f"got={r}") opusdll.opusfile.op_seekable = orig_seekable lib.stop_audio(aid) # 2007: SEEK_FAILED (monkey-patch op_pcm_seek to return -1) aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): orig_seek = opusdll.opusfile.op_pcm_seek opusdll.opusfile.op_pcm_seek = lambda of, pos: -1 r = lib.seek_audio(aid, 10.0) t.check("2007 SEEK_FAILED trigger", isinstance(r, tuple) and r[0] == 2007, f"got={r}") opusdll.opusfile.op_pcm_seek = orig_seek lib.stop_audio(aid) # 2005: TAGS_PARSE_FAILED (monkey-patch op_tags to raise) class BadTags: @property def contents(self): raise ValueError("corrupt tags") orig_tags = opusdll.opusfile.op_tags opusdll.opusfile.op_tags = lambda of, li: BadTags() r = opusplayer._get_opus_extended_metadata(OPUS_FILE) t.check("2005 TAGS_PARSE_FAILED trigger", isinstance(r, tuple) and r[0] == 2005, f"got={r}") opusdll.opusfile.op_tags = orig_tags # 2006: DECODE_FAILED (monkey-patch op_read_stereo to return -1) orig_read = opusdll.opusfile.op_read_stereo opusdll.opusfile.op_read_stereo = lambda of, pcm, size: -1 aid = lib.play_from_file(OPUS_FILE) time.sleep(0.5) r = lib._decode_error t.check("2006 DECODE_FAILED trigger", r is not None and r[0] == 2006, f"got={r}") opusdll.opusfile.op_read_stereo = orig_read lib.stop_audio(aid) lib.cleanup_function() # ============================================================================ # [OP-L] Listening Tests (interactive, requires ears) # ============================================================================ def test_opus_listen(t): t.section("[OP-L] Opus Listening Tests (interactive)") if not os.path.exists(OPUS_FILE): t.skip("Opus listening", "no Opus file available") return try: ans = input("\nRun Opus listening tests? (y/n): ").strip().lower() if ans != 'y': t.skip("Opus listening", "user skipped") return except EOFError: t.skip("Opus listening", "non-interactive") return lib = opusplayer.OpusAudio() t.log("\n--- Listening 1: Normal playback ---") t.log(" Playing Opus at normal volume...") aid = lib.play_from_file(OPUS_FILE) if isinstance(aid, int): time.sleep(3) try: ans = input(" Did you hear audio? (y/n): ").strip().lower() t.check("normal playback audible", ans == 'y') except EOFError: t.skip("normal playback audible", "no input") lib.stop_audio(aid) t.log("\n--- Listening 2: Fade-in ---") t.log(" Fading in over 3 seconds...") aid = lib.new_aid(OPUS_FILE) r = lib.fadein_music(aid, ms=3000) if r[0] == 0: time.sleep(3) try: ans = input(" Did volume gradually increase? (y/n): ").strip().lower() t.check("fade-in audible", ans == 'y') except EOFError: t.skip("fade-in audible", "no input") time.sleep(1) lib.stop_audio(aid) t.log("\n--- Listening 3: Fade-out ---") t.log(" Playing at normal volume for 3 seconds, then fading out...") aid = lib.play_from_file(OPUS_FILE) time.sleep(3) # Play at normal volume first r = lib.fadeout_music(ms=3000) if r[0] == 0: time.sleep(3) try: ans = input(" Did volume gradually decrease then stop? (y/n): ").strip().lower() t.check("fade-out audible", ans == 'y') except EOFError: t.skip("fade-out audible", "no input") time.sleep(1) t.log("\n--- Listening 4: Volume change ---") t.log(" Playing at normal volume for 2s, then volume 128 -> 0 -> 128...") aid = lib.play_from_file(OPUS_FILE) time.sleep(2) # Play at normal volume first lib.set_volume(aid, 128) time.sleep(1) lib.set_volume(aid, 0) time.sleep(1) try: ans = input(" Did sound become silent at volume 0? (y/n): ").strip().lower() t.check("volume 0 silent", ans == 'y') except EOFError: t.skip("volume 0 silent", "no input") lib.set_volume(aid, 100) time.sleep(1) lib.stop_audio(aid) t.log("\n--- Listening 5: Seek position ---") t.log(" Playing for 3s, then seeking to 30s, 60s, 120s...") aid = lib.play_from_file(OPUS_FILE) time.sleep(3) # Play at normal position first for pos in (30, 60, 120): lib.seek_audio(aid, pos) time.sleep(1) try: ans = input(f" After seek to {pos}s, did position change? (y/n): ").strip().lower() t.check(f"seek to {pos}s audible", ans == 'y') except EOFError: t.skip(f"seek to {pos}s audible", "no input") lib.stop_audio(aid) lib.cleanup_function() # ============================================================================ # main() # ============================================================================ def main(): global OPUS_FILE mode = sys.argv[1].lower().lstrip('-') if len(sys.argv) > 1 else 'full' if mode not in ('auto', 'listen', 'full'): print(f"Unknown mode: {mode}, using full") mode = 'full' # Prompt for Opus file path (use default if empty) OPUS_FILE = _prompt_opus() print(f"\n Python: {sys.version.split()[0]} | Platform: {sys.platform} | Mode: {mode}") print(f" ap_ds version: {ap_ds.__version__} | Path: {os.path.dirname(ap_ds.__file__)}") print(f" Opus test file: {OPUS_FILE} (exists={os.path.exists(OPUS_FILE)})") t = OpusTest() if mode in ('auto', 'full'): test_opus_imports(t) test_opus_dll(t) test_opus_metadata(t) test_opus_play(t) test_opus_control(t) test_opus_volume(t) test_opus_seek(t) test_opus_fade(t) test_opus_distinction(t) test_opus_batch(t) test_opus_errors(t) test_opus_aid_mapping(t) test_opus_resources(t) test_opus_boundary(t) test_opus_dll_deep(t) test_opus_error_triggers(t) if mode in ('listen', 'full'): test_opus_listen(t) t.summary() return 0 if t.failed == 0 else 1 if __name__ == '__main__': sys.exit(main())