Initial commit: ap_ds 音频播放库 (Audio Player By DVS AFS)
This commit is contained in:
@@ -0,0 +1,946 @@
|
||||
# _opusdll.py - Opus DLL constants, structures, bindings and auto-download loader
|
||||
# Reference: ap_ds/_sdl2.py structure
|
||||
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
import hashlib
|
||||
import tempfile
|
||||
import shutil
|
||||
import urllib.request
|
||||
import ctypes
|
||||
import ctypes.wintypes as wt
|
||||
from ctypes import *
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Opus DLL Library Loader
|
||||
# ============================================================
|
||||
|
||||
# Global library handles
|
||||
opusfile = None # libopusfile-0.dll
|
||||
winmm = None # winmm.dll (system)
|
||||
kernel32 = None # kernel32.dll (system)
|
||||
_opus_dll_error = None # Records DLL load failure reason
|
||||
|
||||
# DLL file list (from the download API)
|
||||
OPUS_DLL_FILES = [
|
||||
{
|
||||
"filename": "libopusurl-0.dll",
|
||||
"url": "https://dvsyun.top/ap_ds/download/libopusurl-0.dll",
|
||||
"size": 76772,
|
||||
},
|
||||
{
|
||||
"filename": "libopus-0.dll",
|
||||
"url": "https://dvsyun.top/ap_ds/download/libopus-0.dll",
|
||||
"size": 500112,
|
||||
},
|
||||
{
|
||||
"filename": "libogg-0.dll",
|
||||
"url": "https://dvsyun.top/ap_ds/download/libogg-0.dll",
|
||||
"size": 40580,
|
||||
},
|
||||
{
|
||||
"filename": "libopusfile-0.dll",
|
||||
"url": "https://dvsyun.top/ap_ds/download/libopusfile-0.dll",
|
||||
"size": 55884,
|
||||
},
|
||||
]
|
||||
|
||||
# DLL SHA256 hashes for verification (after download)
|
||||
OPUS_DLL_HASHES = {
|
||||
"libopusfile-0.dll": "fc8ff75c5e0180e73b0528dc78c51ed0fb493741375cdc227f50c2a33cabf727",
|
||||
"libopus-0.dll": "90aa25a0a6525d7da48a7ae8dd3306e45b0c28ce09a73d2a02b56cd95418d5be",
|
||||
"libogg-0.dll": "3038ce8d161324a6349bf7c83b78493857ff6a3501e3adb3d541c6a07bd94a57",
|
||||
"libopusurl-0.dll": "a6cde968a23f2d0067332a13718c52e265653a2c35d65862e8dff4cf2a0346d9",
|
||||
}
|
||||
|
||||
|
||||
def _get_package_dir():
|
||||
"""Get the directory containing this module."""
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _check_opus_libraries_exist(directory):
|
||||
"""Check if all Opus DLLs exist in the given directory."""
|
||||
required = ["libopusfile-0.dll", "libopus-0.dll", "libogg-0.dll"]
|
||||
for dll in required:
|
||||
if not os.path.exists(os.path.join(directory, dll)):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _load_from_directory(directory):
|
||||
"""Load Opus libraries from the specified directory.
|
||||
|
||||
Supports both Windows (.dll) and Linux (.so) library names.
|
||||
|
||||
Returns:
|
||||
bool: True if the Opus library loaded successfully
|
||||
"""
|
||||
global opusfile
|
||||
platform = sys.platform
|
||||
|
||||
# Determine library filename based on platform
|
||||
if platform == "win32":
|
||||
libopusfile_path = os.path.join(directory, "libopusfile-0.dll")
|
||||
elif platform.startswith("linux"):
|
||||
# Try multiple Linux .so naming conventions
|
||||
names_to_try = [
|
||||
"libopusfile.so",
|
||||
"libopusfile.so.0",
|
||||
"libopusfile-0.so",
|
||||
]
|
||||
libopusfile_path = None
|
||||
for name in names_to_try:
|
||||
candidate = os.path.join(directory, name)
|
||||
if os.path.exists(candidate):
|
||||
libopusfile_path = candidate
|
||||
break
|
||||
if libopusfile_path is None:
|
||||
return False
|
||||
else:
|
||||
# macOS (not added yet) or other
|
||||
libopusfile_path = os.path.join(directory, "libopusfile-0.dll")
|
||||
if not os.path.exists(libopusfile_path):
|
||||
return False
|
||||
|
||||
if not os.path.exists(libopusfile_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Add directory to library search path
|
||||
if platform == "win32":
|
||||
if hasattr(os, 'add_dll_directory'):
|
||||
os.add_dll_directory(directory)
|
||||
os.environ['PATH'] = directory + os.pathsep + os.environ.get('PATH', '')
|
||||
elif platform.startswith("linux"):
|
||||
if 'LD_LIBRARY_PATH' not in os.environ:
|
||||
os.environ['LD_LIBRARY_PATH'] = directory
|
||||
else:
|
||||
os.environ['LD_LIBRARY_PATH'] = directory + ':' + os.environ['LD_LIBRARY_PATH']
|
||||
|
||||
opusfile = ctypes.CDLL(libopusfile_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
_opus_dll_error = f"Opus library load error: {e}"
|
||||
return False
|
||||
|
||||
|
||||
def _load_from_system():
|
||||
"""Try loading libopusfile from system paths."""
|
||||
global opusfile
|
||||
try:
|
||||
import ctypes.util
|
||||
found = ctypes.util.find_library("opusfile")
|
||||
if found:
|
||||
opusfile = ctypes.CDLL(found)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _load_user_config():
|
||||
"""Load user-saved Opus library paths from config file."""
|
||||
global opusfile
|
||||
try:
|
||||
config_file = os.path.expanduser('~/.config/ap_ds/opus_paths.conf')
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
opusfile_path = None
|
||||
for line in f:
|
||||
if line.startswith('OPUSFILE_PATH='):
|
||||
opusfile_path = line.strip().split('=', 1)[1]
|
||||
if opusfile_path and os.path.exists(opusfile_path):
|
||||
opusfile = ctypes.CDLL(opusfile_path)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _run_sudo_command(cmd, packages):
|
||||
"""Run a sudo command with interactive password input.
|
||||
|
||||
First tries without password (if already root or passwordless sudo).
|
||||
If that fails, prompts for the sudo password interactively.
|
||||
|
||||
Args:
|
||||
cmd: Base command list (e.g. ['apt-get', 'install', '-y'])
|
||||
packages: Package names to install
|
||||
|
||||
Returns:
|
||||
bool: True if command succeeded
|
||||
"""
|
||||
import subprocess
|
||||
import getpass
|
||||
|
||||
full_cmd = cmd + packages
|
||||
|
||||
# Try without password first (if already root / passwordless sudo)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['sudo', '-n'] + full_cmd,
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prompt for sudo password interactively
|
||||
print("🔑 sudo password required for package installation")
|
||||
try:
|
||||
password = getpass.getpass("Enter sudo password: ")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n❌ Password input cancelled")
|
||||
return False
|
||||
|
||||
# Use sudo -S to read password from stdin
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['sudo', '-S'] + full_cmd,
|
||||
input=password + '\n',
|
||||
capture_output=True, text=True, timeout=180
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("✅ Packages installed successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Installation failed: {result.stderr.strip()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Installation error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _linux_auto_install():
|
||||
"""Try automatic package manager installation on Linux.
|
||||
|
||||
Uses interactive sudo password input to avoid hanging.
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
if shutil.which('apt-get'):
|
||||
print("📦 Detected apt-based system (Ubuntu/Debian)")
|
||||
if _run_sudo_command(['apt-get', 'install', '-y'],
|
||||
['libopusfile-dev', 'libopus-dev', 'libogg-dev']):
|
||||
if _load_from_system():
|
||||
print("✅ Opus libraries installed and loaded")
|
||||
return True
|
||||
|
||||
elif shutil.which('dnf'):
|
||||
print("📦 Detected dnf-based system (Fedora)")
|
||||
if _run_sudo_command(['dnf', 'install', '-y'],
|
||||
['opusfile-devel', 'opus-devel', 'libogg-devel']):
|
||||
if _load_from_system():
|
||||
print("✅ Opus libraries installed and loaded")
|
||||
return True
|
||||
|
||||
elif shutil.which('pacman'):
|
||||
print("📦 Detected pacman-based system (Arch)")
|
||||
if _run_sudo_command(['pacman', '-S', '--noconfirm'],
|
||||
['opusfile', 'opus', 'libogg']):
|
||||
if _load_from_system():
|
||||
print("✅ Opus libraries installed and loaded")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Automatic installation failed: {e}")
|
||||
return False
|
||||
|
||||
def _linux_interactive_setup():
|
||||
"""Linux interactive setup for Opus libraries."""
|
||||
global opusfile
|
||||
print("\n" + "=" * 70)
|
||||
print("Linux Opus Library Loading")
|
||||
print("=" * 70)
|
||||
print("Options:")
|
||||
print("1. Use system-installed libraries (re-check)")
|
||||
print("2. Specify path to your compiled .so files")
|
||||
print("3. Show installation instructions")
|
||||
print("=" * 70)
|
||||
|
||||
while True:
|
||||
choice = input("\nChoose option (1/2/3): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
if _load_from_system():
|
||||
print("✅ Opus libraries loaded from system")
|
||||
return True
|
||||
print("❌ System libraries not found")
|
||||
continue
|
||||
|
||||
elif choice == "2":
|
||||
opusfile_path = input("Enter full path to libopusfile.so: ").strip()
|
||||
if os.path.exists(opusfile_path):
|
||||
try:
|
||||
opusfile = ctypes.CDLL(opusfile_path)
|
||||
# Save for future
|
||||
try:
|
||||
config_dir = os.path.expanduser('~/.config/ap_ds')
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
with open(os.path.join(config_dir, 'opus_paths.conf'), 'w') as f:
|
||||
f.write(f"OPUSFILE_PATH={opusfile_path}\n")
|
||||
except Exception:
|
||||
pass
|
||||
print("✅ Libraries loaded from user-specified paths")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load: {e}")
|
||||
else:
|
||||
print("❌ File not found")
|
||||
continue
|
||||
|
||||
elif choice == "3":
|
||||
print("\n📚 Installation instructions:")
|
||||
print("=" * 50)
|
||||
print("For Ubuntu/Debian:")
|
||||
print(" sudo apt-get install libopusfile-dev libopus-dev libogg-dev")
|
||||
print("\nFor Fedora:")
|
||||
print(" sudo dnf install opusfile-devel opus-devel libogg-devel")
|
||||
print("\nFor Arch:")
|
||||
print(" sudo pacman -S opusfile opus libogg")
|
||||
print("=" * 50)
|
||||
continue
|
||||
|
||||
else:
|
||||
print("❌ Invalid choice. Please enter 1, 2, or 3.")
|
||||
|
||||
|
||||
def _macos_apology():
|
||||
"""Print a humble apology about missing Opus precompiled packages on macOS."""
|
||||
print("\n" + "=" * 70)
|
||||
print("⚠️ macOS Opus Support Notice")
|
||||
print("=" * 70)
|
||||
print("We sincerely apologize.")
|
||||
print("On macOS, SDL2 libraries can be downloaded automatically, but we")
|
||||
print("could NOT find any precompiled Opus framework packages for macOS.")
|
||||
print("This is a limitation of the Opus ecosystem, not of ap_ds.")
|
||||
print("")
|
||||
print("We recommend using a package manager to install the Opus libraries.")
|
||||
print("ap_ds will first try to auto-install them for you.")
|
||||
print("If that fails, we will guide you through manual installation.")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
|
||||
def _macos_detect_system():
|
||||
"""Detect if Opus libraries are already present on macOS system paths."""
|
||||
global opusfile
|
||||
try:
|
||||
import ctypes.util
|
||||
found = ctypes.util.find_library("opusfile")
|
||||
if found:
|
||||
opusfile = ctypes.CDLL(found)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
# Check common macOS library paths
|
||||
for path in [
|
||||
"/opt/homebrew/lib/libopusfile.dylib", # Apple Silicon Homebrew
|
||||
"/usr/local/lib/libopusfile.dylib", # Intel Homebrew
|
||||
"/opt/local/lib/libopusfile.dylib", # MacPorts
|
||||
]:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
opusfile = ctypes.CDLL(path)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _macos_install_macports():
|
||||
"""Try installing Opus libraries via MacPorts."""
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
if shutil.which('port'):
|
||||
print("📦 Detected MacPorts")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['sudo', '-n', 'port', 'install', 'opus', 'opusfile', 'libogg'],
|
||||
capture_output=True, text=True, timeout=180
|
||||
)
|
||||
if result.returncode == 0:
|
||||
if _macos_detect_system():
|
||||
print("✅ Opus libraries installed via MacPorts")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
# If sudo -n failed (needs password), prompt for password
|
||||
import getpass
|
||||
print("🔑 sudo password required for MacPorts installation")
|
||||
try:
|
||||
password = getpass.getpass("Enter sudo password: ")
|
||||
result = subprocess.run(
|
||||
['sudo', '-S', 'port', 'install', 'opus', 'opusfile', 'libogg'],
|
||||
input=password + '\n', capture_output=True, text=True, timeout=300
|
||||
)
|
||||
if result.returncode == 0 and _macos_detect_system():
|
||||
print("✅ Opus libraries installed via MacPorts")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ MacPorts installation failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _macos_install_homebrew():
|
||||
"""Try installing Opus libraries via Homebrew."""
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
if shutil.which('brew'):
|
||||
print("📦 Detected Homebrew")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['brew', 'install', 'opus', 'opusfile', 'libogg'],
|
||||
capture_output=True, text=True, timeout=300
|
||||
)
|
||||
if result.returncode == 0:
|
||||
if _macos_detect_system():
|
||||
print("✅ Opus libraries installed via Homebrew")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Homebrew installation failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _macos_guide_manual_install():
|
||||
"""Guide the user through manual installation of Opus libraries."""
|
||||
print("\n" + "=" * 70)
|
||||
print("📚 Manual Installation Guide (macOS)")
|
||||
print("=" * 70)
|
||||
print("We could not auto-install the Opus libraries. Please install them")
|
||||
print("using one of the following methods:")
|
||||
print("")
|
||||
print("Method 1: Install Homebrew (if not installed)")
|
||||
print(" /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
|
||||
print(" Then: brew install opus opusfile libogg")
|
||||
print("")
|
||||
print("Method 2: Install MacPorts")
|
||||
print(" https://www.macports.org/install.php")
|
||||
print(" Then: sudo port install opus opusfile libogg")
|
||||
print("")
|
||||
print("Method 3: Compile from source")
|
||||
print(" Download from https://opus-codec.org/downloads/")
|
||||
print(" opus-1.6.1.tar.gz, opusfile-0.12.tar.gz, libogg-1.3.6.tar.gz")
|
||||
print(" Compile each with: ./configure && make && sudo make install")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
|
||||
def _macos_auto_install():
|
||||
"""Try to auto-install Opus libraries on macOS.
|
||||
|
||||
Order: detect system -> MacPorts -> Homebrew -> manual guide.
|
||||
"""
|
||||
_macos_apology()
|
||||
|
||||
# Step 1: Detect if already present
|
||||
if _macos_detect_system():
|
||||
print("✅ Opus libraries already present on system")
|
||||
return True
|
||||
|
||||
# Step 2: Try MacPorts
|
||||
print("\n📦 Attempting MacPorts installation...")
|
||||
if _macos_install_macports():
|
||||
return True
|
||||
|
||||
# Step 3: Try Homebrew
|
||||
print("\n📦 Attempting Homebrew installation...")
|
||||
if _macos_install_homebrew():
|
||||
return True
|
||||
|
||||
# Step 4: Guide manual installation
|
||||
_macos_guide_manual_install()
|
||||
return False
|
||||
|
||||
|
||||
def _check_opus_libraries_exist_linux(directory):
|
||||
"""Check if Opus .so libraries exist in directory (Linux)."""
|
||||
return os.path.exists(os.path.join(directory, "libopusfile.so"))
|
||||
|
||||
|
||||
def verify_file_hash(file_path, expected_hash):
|
||||
"""Verify the SHA256 hash of a file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to verify
|
||||
expected_hash: Expected SHA256 hash (hex string)
|
||||
|
||||
Returns:
|
||||
bool: True if hash matches (or no hash configured)
|
||||
"""
|
||||
if not expected_hash:
|
||||
print(f" ⚠️ No hash configured for {os.path.basename(file_path)}, skipping verification")
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
content = f.read()
|
||||
file_hash = hashlib.sha256(content).hexdigest()
|
||||
print(f" Existing file SHA256: {file_hash}")
|
||||
if file_hash.lower() == expected_hash.lower():
|
||||
print(f" ✅ Hash verification passed")
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ Hash verification failed! Expected: {expected_hash}, Got: {file_hash}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ❌ Error verifying hash: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def download_opus_libraries():
|
||||
"""Download all Opus DLLs to the package directory with auto-download and hash verification.
|
||||
|
||||
Returns:
|
||||
bool: True if all DLLs downloaded successfully
|
||||
"""
|
||||
current_dir = _get_package_dir()
|
||||
print(f"Package directory: {current_dir}")
|
||||
|
||||
def download_file(url, filename, expected_hash=None):
|
||||
"""Download a single file with SSL fallback and hash verification."""
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_file.close()
|
||||
try:
|
||||
print(f" Downloading {filename} from {url}...")
|
||||
try:
|
||||
# Try with SSL verification first
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
content = response.read()
|
||||
print(f" ✅ Download successful with SSL verification")
|
||||
except (urllib.error.URLError, ssl.SSLError) as e:
|
||||
print(f" SSL verification failed: {e}")
|
||||
print(f" Retrying without SSL verification...")
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=30, context=context) as response:
|
||||
content = response.read()
|
||||
print(f" ✅ Download successful without SSL verification")
|
||||
|
||||
with open(temp_file.name, 'wb') as f:
|
||||
f.write(content)
|
||||
print(f" Downloaded {len(content)} bytes")
|
||||
|
||||
# Verify hash before moving
|
||||
if expected_hash:
|
||||
file_hash = hashlib.sha256(content).hexdigest()
|
||||
print(f" Downloaded file SHA256: {file_hash}")
|
||||
if file_hash.lower() != expected_hash.lower():
|
||||
print(f" ❌ Hash verification failed! Expected: {expected_hash}, Got: {file_hash}")
|
||||
try:
|
||||
os.unlink(temp_file.name)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
print(f" ✅ Hash verification passed")
|
||||
|
||||
# Move to package directory
|
||||
dest_path = os.path.join(current_dir, filename)
|
||||
shutil.move(temp_file.name, dest_path)
|
||||
print(f" ✅ Saved to {dest_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ❌ Download failed for {filename}: {e}")
|
||||
try:
|
||||
os.unlink(temp_file.name)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
# Download each DLL with hash verification
|
||||
success_count = 0
|
||||
for dll_info in OPUS_DLL_FILES:
|
||||
filename = dll_info["filename"]
|
||||
expected_hash = OPUS_DLL_HASHES.get(filename)
|
||||
file_path = os.path.join(current_dir, filename)
|
||||
if os.path.exists(file_path):
|
||||
print(f"\n📁 {filename} already exists, verifying hash...")
|
||||
if verify_file_hash(file_path, expected_hash):
|
||||
print(f"✅ {filename} is valid, skipping download")
|
||||
success_count += 1
|
||||
continue
|
||||
else:
|
||||
print(f"⚠️ {filename} hash mismatch, re-downloading...")
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except Exception:
|
||||
pass
|
||||
print(f"\n📥 Downloading {filename}...")
|
||||
if download_file(dll_info["url"], filename, expected_hash):
|
||||
success_count += 1
|
||||
|
||||
print(f"\n✅ Downloaded {success_count}/{len(OPUS_DLL_FILES)} DLLs")
|
||||
return success_count == len(OPUS_DLL_FILES)
|
||||
|
||||
|
||||
def import_opus():
|
||||
"""Main function: Import Opus libraries with cross-platform support.
|
||||
|
||||
Returns:
|
||||
bool: True if Opus libraries loaded successfully
|
||||
"""
|
||||
global opusfile
|
||||
|
||||
# Already loaded?
|
||||
if opusfile is not None:
|
||||
return True
|
||||
|
||||
current_dir = _get_package_dir()
|
||||
platform = sys.platform
|
||||
|
||||
# Windows: use DLL + auto-download
|
||||
if platform == "win32":
|
||||
# Layer 1: Load from current directory
|
||||
if _load_from_directory(current_dir):
|
||||
return True
|
||||
# Layer 2: Load from system
|
||||
if _load_from_system():
|
||||
return True
|
||||
# Layer 3: Auto-download DLLs
|
||||
print("Opus DLLs not found, downloading...")
|
||||
if download_opus_libraries():
|
||||
if _load_from_directory(current_dir):
|
||||
print("✅ Opus DLLs loaded after download")
|
||||
return True
|
||||
|
||||
# Linux: use system .so libraries (reference _sdl2.py)
|
||||
# NOTE: The package directory contains Windows .dll files only.
|
||||
# Linux Opus libraries (.so) are installed via system package manager.
|
||||
elif platform.startswith("linux"):
|
||||
# Layer 1: User config (custom .so paths)
|
||||
if _load_user_config():
|
||||
print("✅ Opus loaded from user config")
|
||||
return True
|
||||
# Layer 2: System libraries
|
||||
if _load_from_system():
|
||||
print("✅ Opus loaded from system")
|
||||
return True
|
||||
# Layer 3: Auto install via package manager
|
||||
if _linux_auto_install():
|
||||
return True
|
||||
# Layer 4: Interactive setup
|
||||
if _linux_interactive_setup():
|
||||
return True
|
||||
|
||||
# macOS: no precompiled Opus framework, use package manager
|
||||
# Order: detect system -> MacPorts -> Homebrew -> manual guide
|
||||
elif platform == "darwin":
|
||||
if _macos_auto_install():
|
||||
return True
|
||||
|
||||
global _opus_dll_error
|
||||
if not _opus_dll_error:
|
||||
_opus_dll_error = "Failed to load Opus libraries"
|
||||
return False
|
||||
|
||||
|
||||
def check_opus_dll():
|
||||
"""Check whether the Opus DLL can be loaded normally.
|
||||
|
||||
Returns:
|
||||
(bool, str): (whether normal, error message)
|
||||
"""
|
||||
if not import_opus():
|
||||
return (False, _opus_dll_error or "Opus DLL load failed")
|
||||
try:
|
||||
if not hasattr(opusfile, 'op_open_file'):
|
||||
return (False, "libopusfile-0.dll not loaded correctly (missing op_open_file)")
|
||||
return (True, "Opus DLL OK")
|
||||
except Exception as e:
|
||||
return (False, f"Opus DLL check failed: {e}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Opus structures
|
||||
# ============================================================
|
||||
|
||||
class OpusHead(Structure):
|
||||
_fields_ = [
|
||||
("version", c_int),
|
||||
("channel_count", c_int),
|
||||
("pre_skip", c_uint),
|
||||
("input_sample_rate", c_uint),
|
||||
("output_gain", c_int),
|
||||
("mapping_family", c_int),
|
||||
("stream_count", c_int),
|
||||
("coupled_count", c_int),
|
||||
("mapping", c_ubyte * 255),
|
||||
]
|
||||
|
||||
|
||||
class OpusTags(Structure):
|
||||
_fields_ = [
|
||||
("user_comments", POINTER(c_char_p)),
|
||||
("comment_lengths", POINTER(c_int)),
|
||||
("comments", c_int),
|
||||
("vendor", c_char_p),
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Windows Wave API structures and constants
|
||||
# ============================================================
|
||||
|
||||
WAVE_FORMAT_PCM = 1
|
||||
WAVE_MAPPER = 0xFFFFFFFF
|
||||
CALLBACK_EVENT = 0x00050000
|
||||
WHDR_DONE = 0x1
|
||||
MMSYSERR_NOERROR = 0
|
||||
WAIT_OBJECT_0 = 0
|
||||
|
||||
|
||||
class WAVEFORMATEX(Structure):
|
||||
_fields_ = [
|
||||
("wFormatTag", wt.WORD),
|
||||
("nChannels", wt.WORD),
|
||||
("nSamplesPerSec", wt.DWORD),
|
||||
("nAvgBytesPerSec", wt.DWORD),
|
||||
("nBlockAlign", wt.WORD),
|
||||
("wBitsPerSample", wt.WORD),
|
||||
("cbSize", wt.WORD),
|
||||
]
|
||||
|
||||
|
||||
class WAVEHDR(Structure):
|
||||
pass
|
||||
WAVEHDR._fields_ = [
|
||||
("lpData", wt.LPSTR),
|
||||
("dwBufferLength", wt.DWORD),
|
||||
("dwBytesRecorded", wt.DWORD),
|
||||
("dwUser", c_void_p), # DWORD_PTR (8 bytes)
|
||||
("dwFlags", wt.DWORD),
|
||||
("dwLoops", wt.DWORD),
|
||||
("lpNext", POINTER(WAVEHDR)),
|
||||
("reserved", c_void_p), # DWORD_PTR (8 bytes)
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Opus file function bindings (wrapper functions)
|
||||
# ============================================================
|
||||
|
||||
def op_open_file(path, error):
|
||||
"""Open an Opus file. Returns handle or None."""
|
||||
return opusfile.op_open_file(path, error)
|
||||
|
||||
|
||||
def op_free(of):
|
||||
"""Free an Opus file handle."""
|
||||
opusfile.op_free(of)
|
||||
|
||||
|
||||
def op_head(of, li):
|
||||
"""Get OpusHead for a link."""
|
||||
return opusfile.op_head(of, li)
|
||||
|
||||
|
||||
def op_tags(of, li):
|
||||
"""Get OpusTags for a link."""
|
||||
return opusfile.op_tags(of, li)
|
||||
|
||||
|
||||
def op_channel_count(of, li):
|
||||
"""Get channel count."""
|
||||
return opusfile.op_channel_count(of, li)
|
||||
|
||||
|
||||
def op_pcm_total(of, li):
|
||||
"""Get total PCM samples."""
|
||||
return opusfile.op_pcm_total(of, li)
|
||||
|
||||
|
||||
def op_bitrate(of, li):
|
||||
"""Get average bitrate."""
|
||||
return opusfile.op_bitrate(of, li)
|
||||
|
||||
|
||||
def op_seekable(of):
|
||||
"""Check if stream is seekable."""
|
||||
return opusfile.op_seekable(of)
|
||||
|
||||
|
||||
def op_link_count(of):
|
||||
"""Get number of links."""
|
||||
return opusfile.op_link_count(of)
|
||||
|
||||
|
||||
def op_read_stereo(of, pcm, buf_size):
|
||||
"""Read decoded stereo PCM."""
|
||||
return opusfile.op_read_stereo(of, pcm, buf_size)
|
||||
|
||||
|
||||
def op_pcm_seek(of, pos):
|
||||
"""Seek to PCM sample position."""
|
||||
return opusfile.op_pcm_seek(of, pos)
|
||||
|
||||
|
||||
def op_pcm_tell(of):
|
||||
"""Get current PCM position."""
|
||||
return opusfile.op_pcm_tell(of)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Windows Wave API function bindings
|
||||
# ============================================================
|
||||
|
||||
def waveOutOpen(phwo, device_id, fmt, callback, instance, flags):
|
||||
return winmm.waveOutOpen(phwo, device_id, fmt, callback, instance, flags)
|
||||
|
||||
|
||||
def waveOutPrepareHeader(hwo, pwh, cbwh):
|
||||
return winmm.waveOutPrepareHeader(hwo, pwh, cbwh)
|
||||
|
||||
|
||||
def waveOutWrite(hwo, pwh, cbwh):
|
||||
return winmm.waveOutWrite(hwo, pwh, cbwh)
|
||||
|
||||
|
||||
def waveOutUnprepareHeader(hwo, pwh, cbwh):
|
||||
return winmm.waveOutUnprepareHeader(hwo, pwh, cbwh)
|
||||
|
||||
|
||||
def waveOutClose(hwo):
|
||||
return winmm.waveOutClose(hwo)
|
||||
|
||||
|
||||
def waveOutSetVolume(hwo, volume):
|
||||
return winmm.waveOutSetVolume(hwo, volume)
|
||||
|
||||
|
||||
def waveOutGetVolume(hwo, volume):
|
||||
return winmm.waveOutGetVolume(hwo, volume)
|
||||
|
||||
|
||||
def waveOutPause(hwo):
|
||||
return winmm.waveOutPause(hwo)
|
||||
|
||||
|
||||
def waveOutRestart(hwo):
|
||||
return winmm.waveOutRestart(hwo)
|
||||
|
||||
|
||||
def waveOutReset(hwo):
|
||||
return winmm.waveOutReset(hwo)
|
||||
|
||||
|
||||
def waveOutGetErrorTextW(code, buf, size):
|
||||
return winmm.waveOutGetErrorTextW(code, buf, size)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Kernel32 function bindings
|
||||
# ============================================================
|
||||
|
||||
def CreateEventW(lp_attrs, b_manual, b_initial, name):
|
||||
return kernel32.CreateEventW(lp_attrs, b_manual, b_initial, name)
|
||||
|
||||
|
||||
def WaitForSingleObject(handle, ms):
|
||||
return kernel32.WaitForSingleObject(handle, ms)
|
||||
|
||||
|
||||
def ResetEvent(handle):
|
||||
return kernel32.ResetEvent(handle)
|
||||
|
||||
|
||||
def CloseHandle(handle):
|
||||
return kernel32.CloseHandle(handle)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Function prototypes (set after libraries are loaded)
|
||||
# ============================================================
|
||||
|
||||
def _setup_prototypes():
|
||||
"""Set up function prototypes for opusfile, winmm, and kernel32."""
|
||||
# --- opusfile prototypes ---
|
||||
opusfile.op_open_file.restype = c_void_p
|
||||
opusfile.op_open_file.argtypes = [c_char_p, POINTER(c_int)]
|
||||
opusfile.op_free.restype = None
|
||||
opusfile.op_free.argtypes = [c_void_p]
|
||||
opusfile.op_head.restype = POINTER(OpusHead)
|
||||
opusfile.op_head.argtypes = [c_void_p, c_int]
|
||||
opusfile.op_tags.restype = POINTER(OpusTags)
|
||||
opusfile.op_tags.argtypes = [c_void_p, c_int]
|
||||
opusfile.op_channel_count.restype = c_int
|
||||
opusfile.op_channel_count.argtypes = [c_void_p, c_int]
|
||||
opusfile.op_pcm_total.restype = c_longlong
|
||||
opusfile.op_pcm_total.argtypes = [c_void_p, c_int]
|
||||
opusfile.op_bitrate.restype = c_int
|
||||
opusfile.op_bitrate.argtypes = [c_void_p, c_int]
|
||||
opusfile.op_seekable.restype = c_int
|
||||
opusfile.op_seekable.argtypes = [c_void_p]
|
||||
opusfile.op_link_count.restype = c_int
|
||||
opusfile.op_link_count.argtypes = [c_void_p]
|
||||
opusfile.op_read_stereo.restype = c_int
|
||||
opusfile.op_read_stereo.argtypes = [c_void_p, POINTER(c_int16), c_int]
|
||||
opusfile.op_pcm_seek.restype = c_int
|
||||
opusfile.op_pcm_seek.argtypes = [c_void_p, c_longlong]
|
||||
opusfile.op_pcm_tell.restype = c_longlong
|
||||
opusfile.op_pcm_tell.argtypes = [c_void_p]
|
||||
|
||||
# --- winmm prototypes (Windows only) ---
|
||||
if winmm is None:
|
||||
return
|
||||
winmm.waveOutOpen.restype = wt.DWORD
|
||||
winmm.waveOutOpen.argtypes = [
|
||||
POINTER(wt.HANDLE), wt.UINT, POINTER(WAVEFORMATEX),
|
||||
wt.DWORD, wt.DWORD, wt.DWORD]
|
||||
winmm.waveOutPrepareHeader.restype = wt.DWORD
|
||||
winmm.waveOutPrepareHeader.argtypes = [wt.HANDLE, POINTER(WAVEHDR), wt.UINT]
|
||||
winmm.waveOutWrite.restype = wt.DWORD
|
||||
winmm.waveOutWrite.argtypes = [wt.HANDLE, POINTER(WAVEHDR), wt.UINT]
|
||||
winmm.waveOutUnprepareHeader.restype = wt.DWORD
|
||||
winmm.waveOutUnprepareHeader.argtypes = [wt.HANDLE, POINTER(WAVEHDR), wt.UINT]
|
||||
winmm.waveOutClose.restype = wt.DWORD
|
||||
winmm.waveOutClose.argtypes = [wt.HANDLE]
|
||||
winmm.waveOutSetVolume.restype = wt.DWORD
|
||||
winmm.waveOutSetVolume.argtypes = [wt.HANDLE, wt.DWORD]
|
||||
winmm.waveOutGetVolume.restype = wt.DWORD
|
||||
winmm.waveOutGetVolume.argtypes = [wt.HANDLE, POINTER(wt.DWORD)]
|
||||
winmm.waveOutPause.restype = wt.DWORD
|
||||
winmm.waveOutPause.argtypes = [wt.HANDLE]
|
||||
winmm.waveOutRestart.restype = wt.DWORD
|
||||
winmm.waveOutRestart.argtypes = [wt.HANDLE]
|
||||
winmm.waveOutReset.restype = wt.DWORD
|
||||
winmm.waveOutReset.argtypes = [wt.HANDLE]
|
||||
winmm.waveOutGetErrorTextW.restype = wt.DWORD
|
||||
winmm.waveOutGetErrorTextW.argtypes = [wt.DWORD, wt.LPWSTR, wt.UINT]
|
||||
|
||||
# --- kernel32 prototypes ---
|
||||
kernel32.CreateEventW.restype = wt.HANDLE
|
||||
kernel32.CreateEventW.argtypes = [c_void_p, wt.BOOL, wt.BOOL, wt.LPCWSTR]
|
||||
kernel32.WaitForSingleObject.restype = wt.DWORD
|
||||
kernel32.WaitForSingleObject.argtypes = [wt.HANDLE, wt.DWORD]
|
||||
kernel32.ResetEvent.restype = wt.BOOL
|
||||
kernel32.ResetEvent.argtypes = [wt.HANDLE]
|
||||
kernel32.CloseHandle.restype = wt.BOOL
|
||||
kernel32.CloseHandle.argtypes = [wt.HANDLE]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Initialize: Load system DLLs and Opus DLLs
|
||||
# ============================================================
|
||||
|
||||
# Windows-specific system DLLs (winmm/kernel32) are only available on Windows.
|
||||
# On Linux/macOS, these are set to None; Opus decoding still works via opusfile,
|
||||
# but waveOut playback is Windows-only.
|
||||
if sys.platform == "win32":
|
||||
winmm = ctypes.WinDLL("winmm")
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
else:
|
||||
winmm = None
|
||||
kernel32 = None
|
||||
|
||||
# Load Opus libraries (with auto-download on Windows, system .so on Linux)
|
||||
if import_opus():
|
||||
_setup_prototypes()
|
||||
Reference in New Issue
Block a user