Initial commit: ap_ds 音频播放库 (Audio Player By DVS AFS)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_all_apis.py - 测试 ap_ds 文档中所有 API 是否存在
|
||||
包括 AudioLibrary 类的所有方法
|
||||
"""
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
print("=" * 60)
|
||||
print("🧪 Testing All ap_ds API Exports")
|
||||
print("=" * 60)
|
||||
|
||||
# ============================================================
|
||||
# 1. 测试顶级函数导入
|
||||
# ============================================================
|
||||
|
||||
TOP_LEVEL_APIS = [
|
||||
"AudioLibrary",
|
||||
"batch_get_metadata",
|
||||
"batch_get_duration",
|
||||
"batch_get_metadata_by_type",
|
||||
"get_audio_duration",
|
||||
"get_audio_metadata",
|
||||
"auto_check_runtime",
|
||||
"check_runtime_mode",
|
||||
"show_tech_manual",
|
||||
]
|
||||
|
||||
print("\n📦 Testing top-level imports:")
|
||||
print("-" * 40)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
missing = []
|
||||
|
||||
for name in TOP_LEVEL_APIS:
|
||||
try:
|
||||
exec(f"from ap_ds import {name}")
|
||||
print(f" ✅ {name}")
|
||||
passed += 1
|
||||
except ImportError as e:
|
||||
print(f" ❌ {name}: {e}")
|
||||
failed += 1
|
||||
missing.append(name)
|
||||
|
||||
# ============================================================
|
||||
# 2. 测试 AudioLibrary 类的所有方法
|
||||
# ============================================================
|
||||
|
||||
AUDIOLIBRARY_METHODS = [
|
||||
# 初始化
|
||||
"__init__",
|
||||
|
||||
# 播放方法
|
||||
"play_from_file",
|
||||
"play_from_memory",
|
||||
"new_aid",
|
||||
|
||||
# 控制方法
|
||||
"play_audio",
|
||||
"pause_audio",
|
||||
"stop_audio",
|
||||
"seek_audio",
|
||||
|
||||
# 音量方法
|
||||
"set_volume",
|
||||
"get_volume",
|
||||
|
||||
# 淡入淡出与过渡方法
|
||||
"fadein_music",
|
||||
"fadein_music_pos",
|
||||
"fadeout_music",
|
||||
"is_music_playing",
|
||||
"is_music_paused",
|
||||
"get_music_fading",
|
||||
|
||||
# 元数据方法
|
||||
"get_audio_duration",
|
||||
"get_audio_metadata",
|
||||
"get_audio_metadata_by_path",
|
||||
"get_audio_metadata_by_aid",
|
||||
|
||||
# 批量解析方法
|
||||
"batch_get_metadata",
|
||||
"batch_get_duration",
|
||||
"batch_get_metadata_by_type",
|
||||
|
||||
# DAP 系统方法
|
||||
"save_dap_to_json",
|
||||
"get_dap_recordings",
|
||||
"clear_dap_recordings",
|
||||
|
||||
# 资源管理
|
||||
"clear_memory_cache",
|
||||
"cleanup_function",
|
||||
|
||||
# 内部辅助方法 (文档中列出但通常是私有的)
|
||||
"_find_channel_by_aid",
|
||||
"_get_file_path_by_aid",
|
||||
"_is_music_file",
|
||||
"_seek_audio",
|
||||
"_get_duration_by_filepath",
|
||||
"_get_file_duration",
|
||||
]
|
||||
|
||||
print("\n" + "-" * 40)
|
||||
print("🎯 Testing AudioLibrary methods:")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from ap_ds import AudioLibrary
|
||||
|
||||
# 获取 AudioLibrary 类的所有方法
|
||||
lib_methods = [m for m in dir(AudioLibrary) if not m.startswith('__') or m == '__init__']
|
||||
|
||||
for method_name in AUDIOLIBRARY_METHODS:
|
||||
if hasattr(AudioLibrary, method_name):
|
||||
print(f" ✅ AudioLibrary.{method_name}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ AudioLibrary.{method_name} (NOT FOUND)")
|
||||
failed += 1
|
||||
missing.append(f"AudioLibrary.{method_name}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f" ❌ Cannot import AudioLibrary: {e}")
|
||||
failed += 1
|
||||
|
||||
# ============================================================
|
||||
# 3. 检查文档中可能遗漏的额外 API
|
||||
# ============================================================
|
||||
|
||||
EXTRA_APIS = [
|
||||
"is_full_performance",
|
||||
"get_runtime_info",
|
||||
]
|
||||
|
||||
print("\n" + "-" * 40)
|
||||
print("🔍 Checking extra APIs (mentioned in docs but maybe not exported):")
|
||||
print("-" * 40)
|
||||
|
||||
for name in EXTRA_APIS:
|
||||
try:
|
||||
exec(f"from ap_ds import {name}")
|
||||
print(f" ✅ {name} (exists!)")
|
||||
passed += 1
|
||||
except ImportError:
|
||||
print(f" ❌ {name} (NOT FOUND - remove from docs or add to __init__.py)")
|
||||
failed += 1
|
||||
missing.append(name)
|
||||
|
||||
# ============================================================
|
||||
# 4. 汇总
|
||||
# ============================================================
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 FINAL SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
if failed == 0:
|
||||
print("🎉 ALL APIs EXIST! Documentation is accurate.")
|
||||
else:
|
||||
print(f"⚠️ {failed} API(s) missing:")
|
||||
for name in missing:
|
||||
print(f" - {name}")
|
||||
print("\n💡 Fix:")
|
||||
print(" Either remove these from documentation, or add them to __init__.py")
|
||||
|
||||
print("=" * 60)
|
||||
print(f"✅ Passed: {passed}")
|
||||
print(f"❌ Failed: {failed}")
|
||||
@@ -0,0 +1,893 @@
|
||||
#!/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('<h', 0) for _ in range(n)))
|
||||
return path
|
||||
_make_wav(WAV_FILE)
|
||||
|
||||
# Corrupted Opus file
|
||||
BAD_OPUS = os.path.join(TMP_DIR, 'bad.opus')
|
||||
with open(BAD_OPUS, 'wb') as f:
|
||||
f.write(b'\x00\x01\x02\x03' * 100) # not a valid Opus stream
|
||||
|
||||
# ============================================================================
|
||||
# Test framework
|
||||
# ============================================================================
|
||||
class OpusTest:
|
||||
def __init__(self):
|
||||
self.passed = 0
|
||||
self.failed = 0
|
||||
self.skipped = 0
|
||||
self.failures = []
|
||||
|
||||
def log(self, msg):
|
||||
print(msg, flush=True)
|
||||
|
||||
def check(self, name, cond, detail=""):
|
||||
if cond:
|
||||
self.passed += 1
|
||||
self.log(f" [PASS] {name}" + (f" | {detail}" if detail else ""))
|
||||
else:
|
||||
self.failed += 1
|
||||
self.failures.append((name, detail))
|
||||
self.log(f" [FAIL] {name}" + (f" | {detail}" if detail else ""))
|
||||
|
||||
def skip(self, name, reason):
|
||||
self.skipped += 1
|
||||
self.log(f" [SKIP] {name} | {reason}")
|
||||
|
||||
def section(self, title):
|
||||
self.log("\n" + "=" * 66)
|
||||
self.log(f" {title}")
|
||||
self.log("=" * 66)
|
||||
|
||||
def summary(self):
|
||||
self.log("\n" + "=" * 66)
|
||||
self.log(" Opus Test Summary")
|
||||
self.log("=" * 66)
|
||||
self.log(f" Passed : {self.passed}")
|
||||
self.log(f" Failed : {self.failed}")
|
||||
self.log(f" Skipped: {self.skipped}")
|
||||
if self.failures:
|
||||
self.log(" --- Failed details ---")
|
||||
for name, detail in self.failures:
|
||||
self.log(f" [FAIL] {name}: {detail}")
|
||||
self.log("=" * 66)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# [OP-1] Opus Module Imports / Constants / Error Codes
|
||||
# ============================================================================
|
||||
def test_opus_imports(t):
|
||||
t.section("[OP-1] Opus Module Imports / Constants / Error Codes")
|
||||
# Module imports
|
||||
t.check("opusplayer importable", callable(opusplayer.OpusAudio))
|
||||
t.check("_opusdll importable", opusdll is not None)
|
||||
t.check("OpusAudio class exists", hasattr(opusplayer, 'OpusAudio'))
|
||||
t.check("check_opus_dll callable", callable(opusplayer.check_opus_dll))
|
||||
t.check("import_opus callable", callable(opusdll.import_opus))
|
||||
t.check("download_opus_libraries callable", callable(opusdll.download_opus_libraries))
|
||||
# Constants
|
||||
t.check("MUS_NO_FADING=0", opusplayer.MUS_NO_FADING == 0)
|
||||
t.check("MUS_FADING_IN=1", opusplayer.MUS_FADING_IN == 1)
|
||||
t.check("MUS_FADING_OUT=2", opusplayer.MUS_FADING_OUT == 2)
|
||||
t.check("WAVE_FORMAT_PCM=1", opusplayer.WAVE_FORMAT_PCM == 1)
|
||||
t.check("CALLBACK_EVENT set", opusplayer.CALLBACK_EVENT == 0x00050000)
|
||||
# Structures
|
||||
t.check("OpusHead structure", hasattr(opusdll, 'OpusHead'))
|
||||
t.check("OpusTags structure", hasattr(opusdll, 'OpusTags'))
|
||||
t.check("WAVEFORMATEX structure", hasattr(opusdll, 'WAVEFORMATEX'))
|
||||
t.check("WAVEHDR structure", hasattr(opusdll, 'WAVEHDR'))
|
||||
# DLL file presence
|
||||
pkg = os.path.dirname(os.path.abspath(opusdll.__file__))
|
||||
for dll in ('libopusfile-0.dll', 'libopus-0.dll', 'libogg-0.dll', 'libopusurl-0.dll'):
|
||||
t.check(f"{dll} exists", os.path.exists(os.path.join(pkg, dll)))
|
||||
# DLL hash table
|
||||
t.check("OPUS_DLL_HASHES defined", hasattr(opusdll, 'OPUS_DLL_HASHES'))
|
||||
if hasattr(opusdll, 'OPUS_DLL_HASHES'):
|
||||
for dll in ('libopusfile-0.dll', 'libopus-0.dll', 'libogg-0.dll', 'libopusurl-0.dll'):
|
||||
t.check(f"hash for {dll}", dll in opusdll.OPUS_DLL_HASHES)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# [OP-2] _opusdll.py DLL Loading & Auto-Download
|
||||
# ============================================================================
|
||||
def test_opus_dll(t):
|
||||
t.section("[OP-2] _opusdll.py DLL Loading & Auto-Download")
|
||||
ok, msg = opusplayer.check_opus_dll()
|
||||
t.check("check_opus_dll returns (bool,str)", isinstance(ok, bool) and isinstance(msg, str), f"got={ok},{msg}")
|
||||
t.check("check_opus_dll ok", ok is True, f"msg={msg}")
|
||||
r = opusdll.import_opus()
|
||||
t.check("import_opus returns bool", isinstance(r, bool), f"got={r}")
|
||||
t.check("import_opus ok", r is True)
|
||||
t.check("opusfile handle loaded", opusdll.opusfile is not None)
|
||||
t.check("OPUS_DLL_FILES has 4", len(opusdll.OPUS_DLL_FILES) == 4, f"got={len(opusdll.OPUS_DLL_FILES)}")
|
||||
if hasattr(opusdll, 'OPUS_DLL_FILES'):
|
||||
for dll in opusdll.OPUS_DLL_FILES:
|
||||
t.check(f"DLL config {dll['filename']}", 'url' in dll and 'filename' in dll)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# [OP-3] Opus Metadata Parsing
|
||||
# ============================================================================
|
||||
def test_opus_metadata(t):
|
||||
t.section("[OP-3] Opus Metadata Parsing (basic + extended)")
|
||||
if not os.path.exists(OPUS_FILE):
|
||||
t.skip("Opus metadata", "no Opus file available")
|
||||
return
|
||||
lib = opusplayer.OpusAudio()
|
||||
meta = lib.get_audio_metadata_by_path(OPUS_FILE)
|
||||
t.check("get_audio_metadata_by_path is dict", isinstance(meta, dict), f"got={type(meta).__name__}")
|
||||
if isinstance(meta, dict):
|
||||
t.check("format=opus", meta.get('format') == 'opus', f"got={meta.get('format')}")
|
||||
t.check("duration>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())
|
||||
@@ -0,0 +1 @@
|
||||
# CI/CD.
|
||||
Reference in New Issue
Block a user