Metadata-Version: 2.4
Name: ap_ds
Version: 4.0.1
Summary: Audio Player By DVS - Advanced audio processing and playback
Home-page: https://apds.top
Author: DVS
Author-email: me@dvsyun.top
License: Custom Open Source License
Project-URL: Documentation, https://apds.top
Project-URL: License Info, https://apds.top
Keywords: audio music player playback sdl2 dvs
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE.md
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-python
Dynamic: summary

# ap_ds: A Lightweight Python Audio Library

**Current Version: v4.0.1 – Test Package Fix**

**Release Date: August 11, 2026**

---

## ⚠️ Important Notice: Test Suite Missing from v4.0.0 on PyPI (Upgrade Optional)

### TL;DR

**If you only use ap_ds for playback and metadata parsing — v4.0.0 is completely fine. No upgrade needed.**

**If you want to run the CI/CD test suite after installation — please upgrade to **v4.0.1** (Test directory included).**

### What Happened?

When publishing v4.0.0 to PyPI, we discovered a **packaging misconfiguration**: the `ap_ds/Test/` directory was **not included** in the published wheel and source archives.

In other words, users who installed via `pip install ap-ds==4.0.0` will **not see the `Test/` folder** under `site-packages/ap_ds/`, which means the three test scripts — `CI,CD_TEST.py`, `GUI_TEST.py`, and `IMPORT_TEST.py` — are unavailable after installation.

### Why Did This Happen?

The culprit is `find_packages()` in `setup.py`.

`find_packages()` is setuptools' automatic package discovery mechanism, and its rule is simple: **only directories containing an `__init__.py` file are recognized as Python packages**. Our `ap_ds/Test/` directory only contained the three test scripts — **no `__init__.py`** — so setuptools silently skipped it during the build. It wasn't deliberately excluded; it was simply **never seen**.

Think of it like a courier who only grabs what's on the table while the user manual sits forgotten in a drawer — **nothing was lost, it just never made it into the box**.

### How Big Is the Impact?

**Very small.** Specifically:

- ✅ **Core functionality is completely unaffected**: `AudioLibrary` playback, `get_audio_duration()`, `get_audio_metadata()`, `batch_get_metadata()`, and all other APIs work exactly as intended
- ✅ **All 418 tests pass**: We ran the full CI/CD test suite against the source tree — **418 PASS / 0 FAIL** (including 8 real listening tests)
- ❌ **The only impact**: after installing the PyPI release, you don't have the test scripts locally to run — that's it

### Already Fixed

We have added an `__init__.py` to `ap_ds/Test/`, turning it into a proper sub-package. **v4.0.1** now includes the complete Test directory, so `pip install ap-ds==4.0.1` gives you a runnable test suite right away.

### Upgrade Recommendation

| Your Situation | Recommendation |
|----------------|---------------|
| Only use the library for playback/parsing | **Don't upgrade** — v4.0.0 is stable |
| Need to run tests or contribute code | **Upgrade** to v4.0.1 |
| Want the latest fixes | **Upgrade** for the complete package |

Whether you upgrade or not, **the core functionality is identical**. This notice is simply to keep users informed, so nobody mistakes the missing Test directory for a code bug.

— The DVS Development Team

---
## ⚠️ Important Notice: v3.1.x Bugs and the Road to 4.0.0

### The 3.1.x Issue – Yes, We Broke It. Again.

Let's not sugarcoat this: **v3.1.2 is still broken**. The `get_audio_duration()` and `get_audio_metadata()` functions **fail to return correct durations** in certain environments. Sometimes they return `0`. Sometimes they return `None`. Sometimes they return a value that's completely wrong. The worst part? **It's inconsistent** – the bug only appears under specific import orders, specific Python versions, and seemingly random combinations of file formats. Trying to reproduce it was like trying to catch a ghost.

Honestly, we're not entirely sure ourselves. The codebase had become such an **unholy spaghetti nightmare** that tracing the execution path required a flowchart, a debugger, and multiple cups of coffee. There were circular imports. There were redundant wrapper functions. There was a `audio_info.py` that existed only to call functions from `audio_parser.py` which then called back to `audio_info.py`. It was a **house of cards held together by hope and bad decisions**.

### The Self‑Deprecating Truth – We Rewrote Everything

After spending **hours** trying to patch the mess, we came to a painful realization: **this code is beyond repair**. It wasn't just one bug – it was a structural problem caused by years of incremental changes, quick fixes, and "it works on my machine" patches. The entire metadata parsing layer was fundamentally flawed.

So we did what any reasonable developer would do when faced with a broken codebase: **we deleted everything and started over**.

#### What We Did:

| Before (v3.1.x) | After (v4.0.0) |
|-----------------|----------------|
| `audio_parser.py` (wrapper) + `audio_info.py` (actual parsers) → **circular imports everywhere** | **Merged everything into `audio_parser.py`** – one file, no circular dependencies, clean and maintainable |
| `player.py` contained SDL2 loader + constants + bindings + AudioLibrary → **2000+ lines of chaos** | **Split into `player.py` (AudioLibrary only) + `_sdl2.py` (loader + constants + bindings)** – each file has a single responsibility |
| Import order mattered – changing imports could break things randomly | **Guaranteed import safety** – no more "import roulette" |
| Debugging required tracing through 4+ files | **Everything is where it belongs** – find it quickly |

**Top‑level APIs are unchanged** – if you were using `AudioLibrary`, `batch_get_metadata()`, `get_audio_duration()`, etc., your code will continue to work exactly as before. The only breakage would occur if you were directly importing internal submodules like `ap_ds.audio_info` or `ap_ds.player._sdl2` – which you shouldn't have been doing anyway.

And honestly? This is probably **overkill** for a library that gets a few hundred downloads a month – most of which are probably **bots and crawlers** scraping PyPI. But you know what? We'd rather rewrite it properly than leave a broken mess for the few actual humans who rely on it. So we did it anyway.

---

## 📨 A Sincere Apology – We Owe You This

We are **truly, deeply sorry.**

The v3.1.x series was a **disaster**. We shipped broken code, we didn't catch the bugs in testing, and we let down the people who trusted us. There is no excuse. We failed.

### The Anatomy of Our Failure

v3.1.0 through v3.1.2 were released as **LFV (Latest Feature Version)** – a rapid‑release track designed to deliver new features **as fast as possible** to users who wanted to experiment. The idea was simple:

- **LFV** = bleeding edge, new features, early adoption
- **LTS** = stable, battle‑tested, production‑ready

We made this distinction clear on our website ([apds.top](https://apds.top)). LFVs are **deliberately less stable** – they are not meant for production environments. That was the trade‑off: you get new features immediately, but you accept some risk.

**However**, being an LFV does **not** mean we should ignore bugs. It does **not** mean we can ship broken code and just shrug. We always fix critical issues – and we have done so with 4.0.0 – but we should have caught this earlier. We should have tested more thoroughly. We should have listened to users who reported issues sooner. We didn't. That's on us.

### What We Should Have Done Differently

1. **More comprehensive testing** – Our test suite didn't catch the circular import bug because it only appeared in specific environments. We should have tested across more Python versions and import configurations.

2. **Better communication** – We should have been more transparent about the stability trade‑offs of LFV releases. We mentioned it, but we didn't emphasize it enough.

3. **Faster response** – When users reported issues, we should have prioritized them immediately, not let them sit while we debated whether it was "our problem" or "their environment."

### Our Commitment Going Forward

We are doubling down on quality and transparency:

- **v4.0.0** is a **complete rewrite** of the core parsing and playback architecture. We have eliminated the circular dependencies, simplified the module structure, and expanded our test suite to cover more environments.

- **v3.0.0 LTS** remains the **recommended version for production** – it is stable, battle‑tested, and will receive security updates until March 2031. If you need reliability above all else, stick with LTS.

- The **next LTS (v4.0.0 LTS)** will be released **after Python 3.15 reaches stable status** (expected October 2026). That release will combine the new architecture with long‑term support guarantees. Until then, v4.0.0 remains an LFV – use it if you want the latest features, but test thoroughly before deploying.

### What You Should Do

| Your Situation | Recommendation |
|----------------|----------------|
| **Production environment** | Continue using **v3.0.0 LTS**. It is stable, secure, and will remain supported until 2031. Do not upgrade to v4.0.0 unless you are willing to test thoroughly. |
| **Development / testing** | Upgrade to **v4.0.0** – experience the new architecture, batch parsing, and Python 3.15t support. Report any issues immediately. |
| **Previously affected by v3.1.x bugs** | **Must upgrade** to v4.0.0 – the bug is fixed, and we guarantee it will not return. |
| **Using Python 3.15t** | **Must upgrade** to v4.0.0 – this version fully supports GIL‑less free‑threading mode. |

### One Final Apology

To everyone who wasted time debugging our broken code: **we are sorry**.

To everyone who reported issues and got ignored: **we are sorry**.

To everyone who trusted us and was let down: **we are sorry**.

We will do better. We promise.

**We won't make excuses. We'll make it right.**

---

## 🚀 v4.0.0 – Performance & Batch Parsing Edition (Full Feature Set)

v4.0.0 inherits and improves upon all features introduced in v3.1.2, plus the architectural refactoring and bug fixes. It is the **most capable and reliable version** of ap_ds to date.

### ✨ New & Enhanced Features

| Feature | Description |
|---------|-------------|
| **Batch Parsing API** | `batch_get_metadata()`, `batch_get_duration()`, `batch_get_metadata_by_type()` – process hundreds of files in parallel with `ProcessPoolExecutor`. |
| **Python 3.15t Free‑Threading Support** | True GIL‑less parallelism: batch parsing scales linearly on multi‑core CPUs. |
| **O(1) DAP Deduplication** | `_add_to_dap_recordings()` now uses set‑based deduplication for near‑instant duplicate checking. |
| **Lazy Imports (Python 3.15+)** | Heavy modules are loaded on demand, speeding up `import ap_ds`. |
| **Runtime Self‑Check** | Automatic environment diagnostics on import (can be disabled with `AP_DS_SKIP_AUTO_CHECK=1`). |
| **Smart WAV Mode** | WAV files shorter than `AP_DS_WAV_THRESHOLD` (default 6s) are played as sound effects; longer files are streamed as music with full seek/fade support. |
| **Fade Controls** | `fadein_music()`, `fadein_music_pos()`, `fadeout_music()`, and status checks. |
| **Cross‑Platform SDL2 Loader** | Automatic download and hash‑verified installation of SDL2 binaries on Windows and macOS; intelligent fallback on Linux. |
| **Unified Error Handling** | All methods now return consistent error tuples: `(error_code, error_message, suggestion)`. No more guessing what went wrong – every error comes with a code and actionable advice. See the Error Codes Reference section for details. |

### 🚀 Error Handling Revolution – The Signature Improvement of v4.0.0

v4.0.0 introduces a **complete, unified error-handling system** — the single most impactful usability improvement in this release. Every public method now speaks **one language**:

```
(error_code, error_message, suggestion)
```

**Why this is a game-changer:**

- **Zero exception surprises** — public methods never raise for expected failures. The old `FileNotFoundError` / `RuntimeError` / `ValueError` explosion is gone from the API surface.
- **No more `None` / `False` / `0` guessing games** — every failure carries a machine-readable code and a human-readable message.
- **Actionable suggestions** — every error includes a concrete next step (e.g. *"Verify the file path exists and is accessible"*).
- **Defense in depth** — invalid argument *types* are validated at every entry point, so a programming mistake produces a clean error tuple instead of a cryptic `ctypes.ArgumentError` crash deep inside the SDL bindings.
- **18 named error codes + a full argument-validation mapping** — from `AP_DS_ERR_FILE_NOT_FOUND (1001)` to `AP_DS_ERR_UNKNOWN (1999)`, every condition has a name, a message, and a suggestion.

**Example — a failing call is fully self-describing:**

```python
from ap_ds import AudioLibrary

lib = AudioLibrary()
result = lib.play_from_file("missing.mp3")

# result -> (1001, 'Audio file not found: missing.mp3',
#            'Verify the file path exists and is accessible')

if isinstance(result, tuple):
    code, msg, suggestion = result
    print(f"Error {code}: {msg}")
    print(f"  -> {suggestion}")
```

**Before v4.0.0** — callers had to juggle exceptions, `None`, `False`, `0` and `0.0`:

```python
# Old v3.x behaviour (gone in v4.0.0):
try:
    aid = lib.play_from_file("missing.mp3")   # raised FileNotFoundError
except FileNotFoundError:
    pass

if lib.pause_audio(99999) is None:           # silent None
    pass

volume = lib.set_volume(aid, 200)            # returned a bool
```

**After v4.0.0** — every call is predictable, testable, and consistent:

```python
result = lib.play_from_file("missing.mp3")   # (1001, msg, suggestion)
result = lib.pause_audio(99999)              # (1002, msg, suggestion)
result = lib.set_volume(aid, 200)            # (1015, msg, suggestion)
result = lib.batch_get_metadata(files, max_workers=0)  # (1999, msg, suggestion)
```

---

### 🆕 Incremental Enhancements (v4.0.0)

#### 1. Argument Type Validation (参数类型验证)

Every public entry point now performs **strict type checking before touching the SDL layer**. This guarantees that a programming mistake (passing `None`, a string where an `int` is expected, etc.) produces a clean error tuple instead of a cryptic `ctypes.ArgumentError` or `TypeError` deep inside the bindings.

| Parameter | Methods | Accepted Types | Error Returned |
|-----------|---------|----------------|----------------|
| `file_path` | `play_from_file()`, `new_aid()` | `str`, `bytes`, `os.PathLike` | `(1001, "Invalid file path type: ...", ...)` |
| `file_path` | `play_from_memory()` | `str`, `bytes`, `os.PathLike` | `(1013, "Invalid file path type: ...", ...)` |
| `loops` | `play_from_file()`, `play_from_memory()` | `int` | `(1999, "Invalid loops type: ...", ...)` |
| `position` | `seek_audio()` | `int`, `float` | `(1999, "Invalid position type: ...", ...)` |
| `volume` | `set_volume()` | `int` | `(1015, "Invalid volume type: ...", ...)` |
| `ms`, `loops` | `fadein_music()`, `fadein_music_pos()` | `int` | `(1999, "Invalid fade parameters: ...", ...)` |
| `position` | `fadein_music_pos()` | `int`, `float` | `(1999, "Invalid fade parameters: ...", ...)` |

**Example — invalid file path type:**

```python
lib = AudioLibrary()
print(lib.play_from_file(None))
# -> (1001, 'Invalid file path type: NoneType',
#     'file_path must be a string or os.PathLike')

print(lib.play_from_file("song.mp3", loops="2"))
# -> (1999, 'Invalid loops type: str. Expected int.',
#     'loops must be an integer (-1=infinite, 0=once, >0=count)')
```

**Example — invalid seek position and volume:**

```python
print(lib.seek_audio(aid, None))
# -> (1999, 'Invalid position type: NoneType. Expected int or float.',
#     'Position must be a number (seconds)')

print(lib.set_volume(aid, "loud"))
# -> (1015, 'Invalid volume type: str (must be integer 0-128)',
#     'Volume must be an integer between 0 and 128')
```

#### 2. Batch `max_workers` Error Tuple (批量解析参数错误元组)

`batch_get_metadata()` now **catches invalid `max_workers` values** — non-integer, `<= 0`, or above the platform limit — and returns a clean error tuple instead of letting a `ValueError` / `TypeError` escape to the caller.

```python
from ap_ds import batch_get_metadata

result = batch_get_metadata(["a.mp3", "b.mp3"], max_workers=0)
# -> (1999, 'Invalid max_workers: max_workers must be greater than 0',
#     'max_workers must be a positive integer or None for automatic')
```

**Before this enhancement**, the same call raised `ValueError: max_workers must be greater than 0` and crashed the program. Now the caller receives a predictable tuple and can handle it gracefully. The same applies to `max_workers=1.5` (TypeError), `max_workers="2"` (TypeError), and `max_workers=1000` (platform limit `ValueError`).

#### 3. Robust SDL2 Handle Management (SDL 句柄管理增强)

`player.py` now **explicitly imports the SDL2 library handles** (`_mix_lib`, `_sdl_lib`) alongside the star-import. This makes every SDL2-backed operation (`fadein_music_pos`, `Delay`, volume, seeking) deterministic and resilient, and it is required for the new fade-in-position validation path.

---

### ⚠️ CRITICAL: Windows Batch Parsing & BrokenProcessPool

If you are using the **batch parsing APIs** (`batch_get_metadata()`, `batch_get_duration()`, `batch_get_metadata_by_type()`) on **Windows**, you **MUST** protect your entry point with `if __name__ == "__main__"`.

**Why?**

On Windows, `ProcessPoolExecutor` uses `spawn` to create new processes. This means each subprocess re-imports your main module. Without the entry point guard, this creates an **infinite recursion loop** that crashes your program with a `BrokenProcessPool` error.

**This is NOT optional. It is MANDATORY.**

#### ❌ WRONG (Will crash on Windows):

```python
from ap_ds import batch_get_metadata

# This will crash on Windows with BrokenProcessPool!
results = batch_get_metadata("/music/", max_workers=4)
```

#### ✅ CORRECT (Always works):

```python
from ap_ds import batch_get_metadata

def main():
    results = batch_get_metadata("/music/", max_workers=4)
    print(f"Parsed {len(results)} files")

if __name__ == "__main__":
    main()
```

#### ✅ CORRECT (For scripts with configuration):

```python
from ap_ds import batch_get_metadata

def get_config():
    # ... configuration logic ...
    return config

def main():
    config = get_config()
    results = batch_get_metadata(config["audio_dir"], max_workers=4)
    print(f"Parsed {len(results)} files")

if __name__ == "__main__":
    main()
```

**This applies to:**

- ✅ Any script that imports ap_ds and uses batch parsing on Windows
- ✅ Jupyter notebooks (if running on Windows, wrap the batch call in a function and use `if __name__ == "__main__"`)
- ✅ Any test scripts (like `CI-CD-TEST.py`)

**Does this apply to Linux/macOS?**

No. Linux and macOS use `fork` by default, which does not have this issue. However, it's still **good practice** to use the entry point guard for cross-platform compatibility.

---

## 🎉 GIL Disabled Message

When running on Python 3.15t (free‑threading build), you will see:

```
🎉 ap_ds: GIL disabled (free-threading mode)
```

When running on standard Python (GIL enabled), you will see a warning with upgrade instructions.

---

## ⚡ Performance Comparison

### Environment

| Item | v3.0.0 LTS | v3.1.2 (with GIL) | v4.0.0 (3.15t) |
|------|------------|-------------------|----------------|
| **Python Version** | 3.13.4 (with GIL) | 3.13.4 (with GIL) | 3.15.0b4 (without GIL) |
| **Concurrency** | ThreadPoolExecutor | ThreadPoolExecutor | ProcessPoolExecutor |
| **Batch Parsing** | ❌ No | ✅ Yes | ✅ Yes (optimized) |
| **Test Files** | 120 MP3s | 120 MP3s | 120 MP3s |

### Results

| Configuration | Time | Speedup vs Serial |
|---------------|------|-------------------|
| v3.0.0 (8 threads) | 1.285s | 0.66x ❌ |
| v3.1.2 (8 threads) | 1.367s (serial) | 1.00x |
| Mutagen (single‑thread) | 0.973s | 1.00x |
| **v4.0.0 (8 processes)** | **0.331s** | **4.13x** 🚀 |

**Key Takeaways:**

- v3.0.0 multi‑threading was bottlenecked by the GIL – more threads slowed things down.
- v4.0.0 uses process‑level parallelism, achieving true multi‑core scaling.
- v4.0.0 is **2.94× faster** than Mutagen and **3.88× faster** than v3.0.0 with 8 threads.

---

## 🧪 CI/CD – All Tests Pass (Green)

Below is the full output of the **comprehensive CICD test suite** (`cicd_test.py`) run on a Windows machine with Python 3.13.4. It covers **all public APIs, error handling, edge cases, boundary values, and interactive listening tests** — **421 tests passed, 0 failed, 0 skipped**. The same suite has also been validated on **macOS** and **Ubuntu** with identical results; their full output is omitted here to keep the document concise.

```
Python 3.13.4 (tags/v3.13.4:8a526ec, Jun  3 2025, 17:46:04) [MSC v.1943 64 bit (AMD64)] on win32
Enter "help" below or click "Help" above for more information.

========================= RESTART: D:\test\cicd_test.py ========================
AP_DS © - Audio Library By DVS  v4.0.0 | https://apds.top
✅ SDL2 loaded from package directory
ℹ️ _IS_PYTHON_315_PLUS defined now: False (Python 3.13)
🎵 WAV playback mode threshold: 6s (Files >= 6s use music mode, < 6s use sound effect mode)

============================================================
 AP_DS 4.0.0 CICD Test Suite
============================================================
Enter path to an MP3 file for playback / metadata tests
(or press Enter to skip MP3-dependent tests): "C:\Users\dvs.新年快乐\Music\新建文件夹\test (4).mp3"

  Python: 3.13.4 | Platform: win32 | Mode: full
  ap_ds version: 4.0.0 | Path: C:\Users\dvs.新年快乐\AppData\Local\Programs\Python\Python313\Lib\site-packages\ap_ds
  MP3 test file: C:\Users\dvs.新年快乐\Music\新建文件夹\test (4).mp3 (exists=True)

==================================================================
 [A] Package Imports / Exports / Error Codes
==================================================================
  [PASS] ap_ds version | v4.0.0
  [PASS] AudioLibrary importable
  [PASS] get_audio_duration importable
  [PASS] get_audio_metadata importable
  [PASS] batch_get_metadata importable
  [PASS] batch_get_duration importable
  [PASS] batch_get_metadata_by_type importable
  [PASS] is_full_performance importable
  [PASS] get_runtime_info importable
  [PASS] __all__ complete | missing=[]
  [PASS] AP_DS_SUCCESS=0
  [PASS] AP_DS_ERR_FILE_NOT_FOUND=1001
  [PASS] AP_DS_ERR_INVALID_AID=1002
  [PASS] AP_DS_ERR_AUDIO_LOAD_FAILED=1003
  [PASS] AP_DS_ERR_PLAYBACK_FAILED=1004
  [PASS] AP_DS_ERR_DAP_INVALID_EXT=1009
  [PASS] AP_DS_ERR_DAP_SAVE_FAILED=1010
  [PASS] AP_DS_ERR_METADATA_PARSE_FAILED=1011
  [PASS] AP_DS_ERR_FADE_NOT_SUPPORTED=1012
  [PASS] AP_DS_ERR_AUDIO_NOT_LOADED=1013
  [PASS] AP_DS_ERR_INVALID_SOURCE=1014
  [PASS] AP_DS_ERR_INVALID_VOLUME=1015
  [PASS] AP_DS_ERR_SEEK_NOT_SUPPORTED=1016
  [PASS] AP_DS_ERR_UNKNOWN=1999
  [PASS] SDL_Init bound
  [PASS] SDL_GetError bound
  [PASS] Mix_LoadMUS bound
  [PASS] Mix_PlayMusic bound
  [PASS] Mix_SetMusicPosition bound
  [PASS] Mix_FadeInMusicPos bound
  [PASS] WAV_THRESHOLD=6 | got=6

==================================================================
 [B] Metadata Parsing (WAV/MP3 + batch)
==================================================================
  [PASS] WAV short duration=2s | got=2
  [PASS] WAV long duration=10s | got=10
  [PASS] WAV metadata is dict
  [PASS] WAV sample_rate=22050 | got=22050
  [PASS] WAV channels=1 | got=1
  [PASS] WAV fields complete
  [PASS] MP3 duration>0 | got=198
  [PASS] MP3 metadata is dict
  [PASS] MP3 format=mp3 | got=mp3
  [PASS] MP3 duration field>0 | got=198
  [PASS] Corrupted WAV duration=0 | got=0
  [PASS] Corrupted WAV metadata=None | got=None
  [PASS] batch_get_metadata returns 2 | got=2
  [PASS] batch_get_duration returns 2 | got={'D:\\test\\cicd_tmp\\t_short.wav': 2, 'D:\\test\\cicd_tmp\\t_long.wav': 10}
  [PASS] batch_by_type filters wav=2 | got=2
⚠️  Parse failed: t_bad.wav
  [PASS] batch with corrupted -> 1 kept | got=1
⚠️  Parse failed: empty.aac
⚠️  Parse failed: empty.flac
⚠️  Parse failed: empty.mp3
⚠️  Parse failed: empty.ogg
⚠️  Parse failed: t_bad.wav
  [PASS] batch on directory >0 | got=5
  [PASS] Unsupported format raises ValueError

==================================================================
 [C] AudioLibrary Initialization
==================================================================
  [PASS] Default init ok
  [PASS] AID counter starts at 0
  [PASS] Caches initially empty
  [PASS] DAP initially empty
  [PASS] MUS_NO_FADING=0
  [PASS] Custom-param init ok
  [PASS] frequency stored=48000
  [PASS] channels stored=1

==================================================================
 [D] Playback
==================================================================
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
  [PASS] play_from_file(short WAV) returns AID | got=1
  [PASS] short WAV is_music_playing=False
  [PASS] short WAV in audio_cache
📝 Recorded DAP file: D:\test\cicd_tmp\t_long.wav
  [PASS] play_from_file(long WAV) returns AID | got=2
  [PASS] long WAV is_music_playing=True
  [PASS] long WAV in music_cache
  [PASS] play_from_file(start_pos=3) returns AID | got=3
  [PASS] Missing file -> 1001 | got=(1001, 'Audio file not found: D:\\test\\cicd_tmp\\missing.mp3', 'Verify the file path exists and is accessible')
  [PASS] .ap-ds-dap -> 1003 | got=(1003, 'Failed to load audio file: D:\\test\\cicd_tmp\\t.ap-ds-dap', 'Check file format and integrity')
  [PASS] play_from_memory(not loaded) -> 1013 | got=(1013, 'Audio not loaded in memory: D:\\test\\cicd_tmp\\never_loaded.wav', 'Call new_aid() first to load the file')
  [PASS] new_aid returns AID | got=6
  [PASS] play_from_memory after new_aid ok | got=7
  [PASS] new_aid(missing) -> 1001 | got=(1001, 'Audio file not found: D:\\test\\cicd_tmp\\missing.wav', 'Verify the file path exists and is accessible')

==================================================================
 [E] Playback Control
==================================================================
  [PASS] Play long WAV ok | got=8
  [PASS] pause_audio ok | got=(0, '', '')
  [PASS] is_music_paused=True
  [PASS] play_audio resume ok | got=(0, '', '')
  [PASS] is_music_playing=True after resume
  [PASS] stop_audio returns float | got=0.5987825393676758
  [PASS] pause_audio(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')
  [PASS] play_audio(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')
  [PASS] stop_audio(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')
  [PASS] seek_audio(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')

==================================================================
 [F] Volume Control
==================================================================
  [PASS] set_volume(music,64) ok | got=(0, '', '')
  [PASS] get_volume(music) is int | got=64
  [PASS] set_volume(sound,100) ok | got=(0, '', '')
  [PASS] get_volume(sound) is int | got=100
  [PASS] set_volume(-1) -> 1015 | got=(1015, 'Invalid volume: -1 (must be 0-128)', 'Volume range is 0-128')
  [PASS] set_volume(129) -> 1015 | got=(1015, 'Invalid volume: 129 (must be 0-128)', 'Volume range is 0-128')
  [PASS] set_volume(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')
  [PASS] get_volume(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')

==================================================================
 [G] Seek
==================================================================
  [PASS] seek_audio(music,5s) ok | got=(0, '', '')
  [PASS] music still playing after seek
  [PASS] seek_audio(sound) ok | got=(0, '', '')
  [PASS] sound entry retained after seek
  [PASS] sound controllable after seek | got=(0, '', '')

==================================================================
 [H] Fade In / Out
==================================================================
  [PASS] fadein_music ok | got=(0, '', '')
  [PASS] get_music_fading returns 0/1/2 | got=2
  [PASS] fadeout_music ok | got=(0, '', '')
  [PASS] fadein_music_pos ok | got=(0, '', '')
  [PASS] fadein_music(invalid AID) -> 1002 | got=(1002, 'AID 99999 not found or not a music file', 'Verify the AID corresponds to a music file')
  [PASS] fadein_music_pos(invalid AID) -> 1002 | got=(1002, 'AID 99999 not found or not a music file', 'Verify the AID corresponds to a music file')

==================================================================
 [I] DAP Recording System
==================================================================
🗑️ Cleared all DAP recordings
  [PASS] clear_dap empties list
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
📝 Recorded DAP file: D:\test\cicd_tmp\t_long.wav
  [PASS] DAP records 2 entries | got=2
  [PASS] DAP dedupe keeps 2 | got=2
  [PASS] DAP record fields complete
  [PASS] DAP record missing file no crash
✅ Saved 2 DAP records to: D:\test\cicd_tmp\out.ap-ds-dap
  [PASS] save_dap_to_json ok | got=(0, '', '')
  [PASS] DAP file created
  [PASS] save_dap_to_json(.json) -> 1009 | got=(1009, "Invalid file extension. Expected '.ap-ds-dap' but got '.json'", 'Use .ap-ds-dap extension for DAP files')
  [PASS] save_dap_to_json(bad path) -> 1010 | got=(1010, "Error saving DAP to JSON: [Errno 2] No such file or directory: 'Z:\\no\\such\\dir\\out.ap-ds-dap'", 'Check write permissions and disk space')
🗑️ Cleared all DAP recordings
  [PASS] clear_dap again empties

==================================================================
 [J] Metadata Methods
==================================================================
📝 Recorded DAP file: D:\test\cicd_tmp\t_long.wav
  [PASS] get_audio_metadata_by_aid is dict | got=dict
  [PASS] get_audio_metadata_by_path is dict | got=dict
  [PASS] get_audio_metadata(str path) is dict | got=dict
  [PASS] get_audio_metadata(int AID) is dict | got=dict
  [PASS] get_audio_metadata(float) -> 1014 | got=(1014, "Invalid source type: <class 'float'>. Expected str or int.", 'Use file path (str) or AID (int)')
  [PASS] get_audio_duration(AID)=10 | got=10
  [PASS] get_audio_duration(path)=10 | got=10
  [PASS] get_audio_duration(invalid AID) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid')
  [PASS] _get_sample_rate=22050 | got=22050
  [PASS] _get_channels=1 | got=1
  [PASS] simple_mp3_duration_estimation>0 | got=27.43725
  [PASS] _get_playing_duration>=10 | got=10.0
  [PASS] _get_file_duration=10 | got=10.0
  [PASS] get_audio_metadata_by_aid(invalid) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid')
  [PASS] get_audio_metadata_by_path(missing) -> 1001 | got=(1001, 'File not found: D:\\test\\cicd_tmp\\missing.mp3', 'Verify the file path exists')
  [PASS] get_audio_duration(missing path) -> 1001 | got=(1001, 'File not found: D:\\test\\cicd_tmp\\missing.mp3', 'Verify the file path exists')

==================================================================
 [K] Helper Methods
==================================================================
  [PASS] _is_music_file(.mp3)=True
  [PASS] _is_music_file(.ogg)=True
  [PASS] _is_music_file(.flac)=True
  [PASS] _is_music_file(long wav)=True
  [PASS] _is_music_file(short wav)=False
  [PASS] _is_music_file(.aif)=False
  [PASS] _is_music_file(.txt)=False
  [PASS] _find_channel_by_aid found | got=-1
  [PASS] _find_channel_by_aid(invalid)=None
  [PASS] _get_file_path_by_aid returns path | got=D:\test\cicd_tmp\t_long.wav
  [PASS] _get_file_path_by_aid(invalid) -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid')
  [PASS] _get_aid_for_music found | got=16
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
  [PASS] _get_aid_for_audio found | got=17
  [PASS] _get_aid_for_audio(music file) -> 1002 | got=(1002, 'No AID found for file: D:\\test\\cicd_tmp\\t_long.wav', 'File may not be loaded or is a music file')

==================================================================
 [L] Resource Management
==================================================================
  [PASS] clear_memory_cache empties caches
  [PASS] cleanup_function completes

==================================================================
 [M] Top-Level API
==================================================================
  [PASS] get_runtime_info is dict | got=dict
  [PASS] is_full_performance is bool | got=False

==================================================================
 [O] Edge Cases & Error Handling
==================================================================
  --- O1 invalid argument types ---
  [PASS] play_from_file(NoneType) -> tuple(1001) | got=(1001, 'Invalid file path type: NoneType', 'file_path must be a string or os.PathLike')
  [PASS] play_from_file(float) -> tuple(1001) | got=(1001, 'Invalid file path type: float', 'file_path must be a string or os.PathLike')
  [PASS] play_from_file(list) -> tuple(1001) | got=(1001, 'Invalid file path type: list', 'file_path must be a string or os.PathLike')
  [PASS] play_from_file(dict) -> tuple(1001) | got=(1001, 'Invalid file path type: dict', 'file_path must be a string or os.PathLike')
  [PASS] play_from_file(tuple) -> tuple(1001) | got=(1001, 'Invalid file path type: tuple', 'file_path must be a string or os.PathLike')
  [PASS] play_from_memory(list) -> tuple | got=(1013, 'Invalid file path type: list', 'file_path must be a string or os.PathLike')
  [PASS] play_from_memory(dict) -> tuple | got=(1013, 'Invalid file path type: dict', 'file_path must be a string or os.PathLike')
  [PASS] new_aid(NoneType) -> tuple(1001) | got=(1001, 'Invalid file path type: NoneType', 'file_path must be a string or os.PathLike')
  [PASS] new_aid(float) -> tuple(1001) | got=(1001, 'Invalid file path type: float', 'file_path must be a string or os.PathLike')
  [PASS] new_aid(list) -> tuple(1001) | got=(1001, 'Invalid file path type: list', 'file_path must be a string or os.PathLike')
  --- O2 boundary values ---
📝 Recorded DAP file: D:\test\cicd_tmp\t_long.wav
  [PASS] seek_audio(None) -> tuple | got=(1999, 'Invalid position type: NoneType. Expected int or float.', 'Position must be a number (seconds)')
  [PASS] seek_audio('5') -> tuple | got=(1999, 'Invalid position type: str. Expected int or float.', 'Position must be a number (seconds)')
  [PASS] seek_audio(3.0) -> success | got=(0, '', '')
  [PASS] set_volume(None) -> 1015 | got=(1015, 'Invalid volume type: NoneType (must be integer 0-128)', 'Volume must be an integer between 0 and 128')
  [PASS] set_volume('50') -> 1015 | got=(1015, 'Invalid volume type: str (must be integer 0-128)', 'Volume must be an integer between 0 and 128')
  [PASS] set_volume(1.5) -> 1015 | got=(1015, 'Invalid volume type: float (must be integer 0-128)', 'Volume must be an integer between 0 and 128')
  [PASS] set_volume(0) -> success | got=(0, '', '')
  [PASS] set_volume(128) -> success | got=(0, '', '')
  [PASS] set_volume(-1) -> 1015 | got=(1015, 'Invalid volume: -1 (must be 0-128)', 'Volume range is 0-128')
  [PASS] set_volume(129) -> 1015 | got=(1015, 'Invalid volume: 129 (must be 0-128)', 'Volume range is 0-128')
  [PASS] fadein_music(ms=None) -> tuple | got=(1999, 'Invalid fade parameters: ms and loops must be integers', 'Check fade parameters (ms: int milliseconds, loops: int)')
  [PASS] fadein_music_pos(position=None) -> tuple | got=(1999, 'Invalid fade parameters: ms/loops must be int, position must be a number', 'Check fade parameters')
  --- O3 exact error codes ---
  [PASS] 1001 missing file -> 1001 | got=(1001, 'Audio file not found: D:\\test\\cicd_tmp\\no_such_dir.mp3', 'Verify the file path exists and is accessible')
  [PASS] 1002 invalid AID -> 1002 | got=(1002, 'Invalid AID: 99999', 'Check that the AID is valid and the audio is loaded')
  [PASS] 1003 load failure (DAP file) -> 1003 | got=(1003, 'Failed to load audio file: D:\\test\\cicd_tmp\\t.ap-ds-dap', 'Check file format and integrity')
  [PASS] 1009 DAP bad extension -> 1009 | got=(1009, "Invalid file extension. Expected '.ap-ds-dap' but got '.json'", 'Use .ap-ds-dap extension for DAP files')
  [PASS] 1010 DAP save failure -> 1010 | got=(1010, "Error saving DAP to JSON: [Errno 2] No such file or directory: 'Z:\\no\\such\\dir\\x.ap-ds-dap'", 'Check write permissions and disk space')
  [PASS] 1011 metadata parse failure -> 1011 | got=(1011, 'Failed to parse metadata for: D:\\test\\cicd_tmp\\t_bad.wav', 'File may be corrupted or unsupported')
  [PASS] 1013 not loaded in memory -> 1013 | got=(1013, 'Audio not loaded in memory: D:\\test\\cicd_tmp\\x.wav', 'Call new_aid() first to load the file')
  [PASS] 1014 invalid source type -> 1014 | got=(1014, "Invalid source type: <class 'float'>. Expected str or int.", 'Use file path (str) or AID (int)')
  [PASS] 1015 invalid volume -> 1015 | got=(1015, 'Invalid volume: 200 (must be 0-128)', 'Volume range is 0-128')
  --- O4 valid return values ---
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
  [PASS] play_from_file(short WAV) -> int AID | got=5
  [PASS] get_audio_duration(path) -> valid int>0 | got=10
  [PASS] get_audio_metadata_by_path -> dict complete | got={'path': 'D:\\test\\cicd_tmp\\t_short.wav', 'format': 'wav', 'duration': 2, 'length': 2.0, 'sample_rate': 22050, 'channels': 1, 'bitrate': 352800}
🗑️ Cleared all DAP recordings
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
✅ Saved 1 DAP records to: D:\test\cicd_tmp\edge.ap-ds-dap
  [PASS] save_dap_to_json -> (0,'','') | got=(0, '', '')
  [PASS] batch_get_metadata -> valid list | got=[{'path': 'D:\\test\\cicd_tmp\\t_short.wav', 'format': 'wav', 'duration': 2, 'length': 2.0, 'sample_rate': 22050, 'channels': 1, 'bitrate': 352800}, {'path': 'D:\\test\\cicd_tmp\\t_long.wav', 'format': 'wav', 'duration': 10, 'length': 10.0, 'sample_rate': 22050, 'channels': 1, 'bitrate': 352800}]
  [PASS] batch_get_metadata([]) -> [] | got=[]
  [PASS] batch_get_metadata(None) -> [] | got=[]
  [PASS] batch max_workers=0 -> error tuple(1999) | got=(1999, 'Invalid max_workers: max_workers must be greater than 0', 'max_workers must be a positive integer or None for automatic')
  [PASS] batch max_workers=-1 -> error tuple(1999) | got=(1999, 'Invalid max_workers: max_workers must be greater than 0', 'max_workers must be a positive integer or None for automatic')
  [PASS] batch max_workers=1.5 -> error tuple(1999) | got=(1999, "Invalid max_workers: 'float' object cannot be interpreted as an integer", 'max_workers must be a positive integer or None for automatic')
  [PASS] batch max_workers=2 -> error tuple(1999) | got=(1999, "Invalid max_workers: '<=' not supported between instances of 'str' and 'int'", 'max_workers must be a positive integer or None for automatic')
  [PASS] batch max_workers=1000 -> error tuple(1999) | got=(1999, 'Invalid max_workers: max_workers must be <= 61', 'max_workers must be a positive integer or None for automatic')

==================================================================
 [P] __init__.py Module Coverage
==================================================================
  [PASS] __version__ = 4.0.0 | 4.0.0
  [PASS] top-level get_audio_duration callable
  [PASS] top-level get_audio_metadata callable
  [PASS] top-level batch_get_metadata callable
  [PASS] top-level batch_get_duration callable
  [PASS] top-level batch_get_metadata_by_type callable
  [PASS] top-level is_full_performance callable
  [PASS] top-level get_runtime_info callable
  [PASS] top-level auto_check_runtime callable
  [PASS] top-level check_runtime_mode callable
  [PASS] top-level show_tech_manual callable
  [PASS] __all__ contains __version__
  [PASS] __all__ contains AudioLibrary
  [PASS] __all__ contains get_audio_duration
  [PASS] __all__ contains get_audio_metadata
  [PASS] __all__ contains batch_get_metadata
  [PASS] __all__ contains batch_get_duration
  [PASS] __all__ contains batch_get_metadata_by_type
  [PASS] __all__ contains auto_check_runtime
  [PASS] __all__ contains check_runtime_mode
  [PASS] __all__ contains show_tech_manual
  [PASS] show_tech_manual output >1000 chars | 16303
  [PASS] manual contains 'TECHNICAL MANUAL'
  [PASS] manual contains 'AudioLibrary'
  [PASS] manual contains 'AP_DS_WAV_THRESHOLD'
  [PASS] manual contains 'DAP'
  [PASS] manual contains 'SDL2'
  [PASS] _RUNTIME_CHECKED=True after import
  [PASS] SUPPRESS_WARNINGS matches env | module=True env=True
  [PASS] SHOW_CONGRATS matches env | module=True env=True
  [PASS] _AUTO_CHECK_SKIP matches env | module=True env=True
  [PASS] ensure_runtime_checked sets True
  [PASS] ensure_runtime_checked idempotent
  [PASS] check_runtime_mode returns bool | True
  [PASS] _check_runtime_mode returns bool | True
  [PASS] check_runtime_mode == _check_runtime_mode
  [PASS] auto_check_runtime(SKIP) returns None
  [PASS] _auto_check_runtime(SKIP) returns None
  [PASS] get_runtime_info(SKIP) returns {}
  [PASS] is_full_performance(SKIP) returns False
  [PASS] _auto_check_runtime(SKIP=0) returns dict
  [PASS] auto dict 11 keys complete | missing=[]
  [PASS] info[library_name]=AP_DS
  [PASS] info[library_version]=4.0.0
  [PASS] info[gil_enabled] is bool
  [PASS] info[has_profiling] is bool
  [PASS] info[is_full_performance] is bool
  [PASS] info[cpu_count]>0
  [PASS] info[platform]=win32
  [PASS] self-check prints 'Runtime Self-Check'
  [PASS] self-check prints 'Python'
  [PASS] self-check prints 'GIL'
  [PASS] self-check prints 'CPU Cores'

============================================================
🔍 ap_ds Runtime Self-Check
============================================================
📚 Library: AP_DS (Audio Library By DVS)
📌 Version: 4.0.0
📂 Install Path: C:\Users\dvs.新年快乐\AppData\Local\Programs\Python\Python313\Lib\site-packages\ap_ds
🌐 Website: https://apds.top
📦 PyPI: https://pypi.org/project/ap-ds/
📦 Mirror: https://pypi.tuna.tsinghua.edu.cn/simple/ap-ds/

📥 Installation:
   pip install ap-ds==4.0.0
   pip install ap-ds==4.0.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
   pip install /path/to/ap-ds-4.0.0-py3-none-any.whl
👤 Author: DVS

📖 Description:
   AP_DS (Audio Playback & Data Service) is a cross-platform audio
   library built on SDL2 and SDL2_mixer, designed for Python applications
   requiring high-performance audio playback and metadata management.

   Core Features:
   • Audio Playback: MP3, WAV, FLAC, OGG, AAC, and more
   • Smart WAV Handling: Auto-switch between music/sound effect mode
   • Metadata Parsing: Duration, sample rate, channels, bitrate
   • DAP Recording: O(1) deduplication playlist generation
   • Batch Processing: Multi-core parallel metadata extraction
   • Fade Control: Fade in/out with position seeking support
   • Memory Management: Efficient caching with automatic cleanup

   Performance:
   • Native SDL2 bindings with zero-copy audio processing
   • Free-threading support (Python 3.15t) for maximum parallelism
   • ProcessPoolExecutor for CPU-bound batch operations

   Platform Support:
   • Windows (x64)  • macOS (x64/ARM)  • Linux (x64/ARM)

   Documentation: https://apds.top/docs
   Source Code: https://gitcode.com/dvsxt/ap_ds
   License: DVS Audio Library (ap_ds) Open Source License Version 2.0

🐍 Python: 3.13.4 (cpython)
🔒 GIL: Enabled
📊 Profiling: Not available (requires Python 3.15+)
🚀 Full Performance Mode: ❌ No (degraded mode)
💻 CPU Cores: 16
🖥️  Platform: win32
============================================================
💡 Tip: Upgrade to Python 3.15t for full performance:
   https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-amd64.zip
   To suppress this auto-check, set AP_DS_SKIP_AUTO_CHECK=1
============================================================

  [PASS] get_runtime_info(SKIP=0) returns dict
  [PASS] get_runtime_info keys complete | keys=['library_name', 'library_version', 'library_install_path', 'library_website', 'library_author', 'python_version', 'gil_enabled', 'has_profiling', 'is_full_performance', 'cpu_count', 'platform']
  [PASS] get_runtime_info[library_name]=AP_DS
  [PASS] get_runtime_info same source as auto

============================================================
🔍 ap_ds Runtime Self-Check
============================================================
📚 Library: AP_DS (Audio Library By DVS)
📌 Version: 4.0.0
📂 Install Path: C:\Users\dvs.新年快乐\AppData\Local\Programs\Python\Python313\Lib\site-packages\ap_ds
🌐 Website: https://apds.top
📦 PyPI: https://pypi.org/project/ap-ds/
📦 Mirror: https://pypi.tuna.tsinghua.edu.cn/simple/ap-ds/

📥 Installation:
   pip install ap-ds==4.0.0
   pip install ap-ds==4.0.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
   pip install /path/to/ap-ds-4.0.0-py3-none-any.whl
👤 Author: DVS

📖 Description:
   AP_DS (Audio Playback & Data Service) is a cross-platform audio
   library built on SDL2 and SDL2_mixer, designed for Python applications
   requiring high-performance audio playback and metadata management.

   Core Features:
   • Audio Playback: MP3, WAV, FLAC, OGG, AAC, and more
   • Smart WAV Handling: Auto-switch between music/sound effect mode
   • Metadata Parsing: Duration, sample rate, channels, bitrate
   • DAP Recording: O(1) deduplication playlist generation
   • Batch Processing: Multi-core parallel metadata extraction
   • Fade Control: Fade in/out with position seeking support
   • Memory Management: Efficient caching with automatic cleanup

   Performance:
   • Native SDL2 bindings with zero-copy audio processing
   • Free-threading support (Python 3.15t) for maximum parallelism
   • ProcessPoolExecutor for CPU-bound batch operations

   Platform Support:
   • Windows (x64)  • macOS (x64/ARM)  • Linux (x64/ARM)

   Documentation: https://apds.top/docs
   Source Code: https://gitcode.com/dvsxt/ap_ds
   License: DVS Audio Library (ap_ds) Open Source License Version 2.0

🐍 Python: 3.13.4 (cpython)
🔒 GIL: Enabled
📊 Profiling: Not available (requires Python 3.15+)
🚀 Full Performance Mode: ❌ No (degraded mode)
💻 CPU Cores: 16
🖥️  Platform: win32
============================================================
💡 Tip: Upgrade to Python 3.15t for full performance:
   https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-amd64.zip
   To suppress this auto-check, set AP_DS_SKIP_AUTO_CHECK=1
============================================================

  [PASS] is_full_performance(SKIP=0) is bool
  [PASS] is_full_performance matches info
  [PASS] after restore auto_check_runtime returns None
  [PASS] after restore get_runtime_info returns {}

==================================================================
 [Q] _sdl2.py Constants / Structures / Bindings
==================================================================
  [PASS] constant SDL_TRUE=1 | got=1
  [PASS] constant SDL_FALSE=0 | got=0
  [PASS] constant SDL_INIT_TIMER=1 | got=1
  [PASS] constant SDL_INIT_AUDIO=16 | got=16
  [PASS] constant SDL_INIT_VIDEO=32 | got=32
  [PASS] constant SDL_INIT_JOYSTICK=512 | got=512
  [PASS] constant SDL_INIT_HAPTIC=4096 | got=4096
  [PASS] constant SDL_INIT_GAMECONTROLLER=8192 | got=8192
  [PASS] constant SDL_INIT_EVENTS=16384 | got=16384
  [PASS] constant AUDIO_U8=8 | got=8
  [PASS] constant AUDIO_S8=32776 | got=32776
  [PASS] constant AUDIO_U16LSB=16 | got=16
  [PASS] constant AUDIO_S16LSB=32784 | got=32784
  [PASS] constant AUDIO_U16MSB=4112 | got=4112
  [PASS] constant AUDIO_S16MSB=36880 | got=36880
  [PASS] constant AUDIO_S32LSB=32800 | got=32800
  [PASS] constant AUDIO_S32MSB=36896 | got=36896
  [PASS] constant AUDIO_F32LSB=33056 | got=33056
  [PASS] constant AUDIO_F32MSB=37152 | got=37152
  [PASS] constant MIX_INIT_FLAC=1 | got=1
  [PASS] constant MIX_INIT_MOD=2 | got=2
  [PASS] constant MIX_INIT_MP3=8 | got=8
  [PASS] constant MIX_INIT_OGG=16 | got=16
  [PASS] constant MIX_INIT_MID=32 | got=32
  [PASS] constant MIX_INIT_OPUS=64 | got=64
  [PASS] constant MIX_CHANNEL_POST=-2 | got=-2
  [PASS] constant MIX_DEFAULT_CHANNELS=2 | got=2
  [PASS] constant MUS_NONE=0 | got=0
  [PASS] constant MUS_CMD=1 | got=1
  [PASS] constant MUS_WAV=2 | got=2
  [PASS] constant MUS_MOD=3 | got=3
  [PASS] constant MUS_MID=4 | got=4
  [PASS] constant MUS_OGG=5 | got=5
  [PASS] constant MUS_MP3=6 | got=6
  [PASS] constant MUS_FLAC=7 | got=7
  [PASS] constant MUS_OPUS=8 | got=8
  [PASS] MIX_DEFAULT_FORMAT==AUDIO_S16SYS
  [PASS] SDL_INIT_EVERYTHING combination
  [PASS] AUDIO_S16SYS is S16LSB on little-endian
  [PASS] SDL_AudioSpec.freq field
  [PASS] SDL_AudioSpec.format field
  [PASS] SDL_AudioSpec.channels field
  [PASS] SDL_AudioSpec.silence field
  [PASS] SDL_AudioSpec.samples field
  [PASS] SDL_AudioSpec.padding field
  [PASS] SDL_AudioSpec.size field
  [PASS] SDL_AudioSpec.callback field
  [PASS] SDL_AudioSpec.userdata field
  [PASS] Mix_Chunk.allocated field
  [PASS] Mix_Chunk.abuf field
  [PASS] Mix_Chunk.alen field
  [PASS] Mix_Chunk.volume field
  [PASS] _sdl_lib loaded
  [PASS] _mix_lib loaded
  [PASS] import_sdl2 returns 2-tuple
  [PASS] _check_sdl2_loaded() True
  [PASS] _check_sdl_libraries_exist(package dir)
  [PASS] binding SDL_Init exists
  [PASS] binding SDL_Quit exists
  [PASS] binding SDL_GetError exists
  [PASS] binding SDL_RWFromFile exists
  [PASS] binding SDL_Delay exists
  [PASS] binding Mix_OpenAudio exists
  [PASS] binding Mix_CloseAudio exists
  [PASS] binding Mix_LoadWAV exists
  [PASS] binding Mix_LoadMUS exists
  [PASS] binding Mix_FreeChunk exists
  [PASS] binding Mix_FreeMusic exists
  [PASS] binding Mix_PlayChannel exists
  [PASS] binding Mix_PlayMusic exists
  [PASS] binding Mix_Pause exists
  [PASS] binding Mix_PauseMusic exists
  [PASS] binding Mix_Resume exists
  [PASS] binding Mix_ResumeMusic exists
  [PASS] binding Mix_HaltChannel exists
  [PASS] binding Mix_HaltMusic exists
  [PASS] binding Mix_SetMusicPosition exists
  [PASS] binding Mix_MusicDuration exists
  [PASS] binding Mix_Volume exists
  [PASS] binding Mix_VolumeMusic exists
  [PASS] binding Mix_AllocateChannels exists
  [PASS] binding Mix_GetMusicType exists
  [PASS] binding Mix_FadingMusic exists
  [PASS] binding Mix_FadeInMusic exists
  [PASS] binding Mix_FadeOutMusic exists
  [PASS] binding Mix_FadeInChannel exists
  [PASS] binding Mix_FadeOutChannel exists
  [PASS] binding Mix_Playing exists
  [PASS] binding Mix_PlayingMusic exists
  [PASS] binding Mix_Paused exists
  [PASS] binding Mix_PausedMusic exists
  [PASS] binding Mix_SetPanning exists
  [PASS] binding Mix_SetDistance exists
  [PASS] binding Mix_SetPosition exists
  [PASS] binding Mix_SetReverseStereo exists
  [PASS] binding Mix_FadeInMusicPos exists
  [PASS] binding _load_from_directory exists
  [PASS] binding _load_from_system exists
  [PASS] binding _load_user_config exists
  [PASS] binding _linux_auto_install exists
  [PASS] binding _linux_interactive_setup exists
  [PASS] binding _check_sdl_libraries_exist exists
  [PASS] binding _check_sdl2_loaded exists
  [PASS] binding import_sdl2 exists
  [PASS] binding _setup_prototypes exists
  [PASS] SDL_Init.argtypes=[c_uint32]
  [PASS] SDL_GetError.restype=c_char_p
  [PASS] Mix_OpenAudio.argtypes set

==================================================================
 [R] audio_parser.py Deep Parser Coverage
==================================================================
  [PASS] StreamInfo.length=10.5
  [PASS] StreamInfo.sample_rate=44100
  [PASS] StreamInfo.channels=2
  [PASS] StreamInfo.bitrate=128000
  [PASS] StreamInfo repr contains StreamInfo
  [PASS] FileType._parse raises ValueError
  [PASS] read_u32_be(0x0100)
  [PASS] read_u32_le(0x0100)
  [PASS] read_u16_le(0x0100)
  [PASS] open_audio(.wav) -> WAVFile
  [PASS] open_audio(.mp3) -> MP3File
  [PASS] open_audio(.txt) raises ValueError
  [PASS] open_audio(.xyz) raises ValueError
  [PASS] open_audio(.flac_bad) raises ValueError
  [PASS] metadata 16bit mono (2s/22050/1ch) | got={'path': 'D:\\test\\cicd_tmp\\t_short.wav', 'format': 'wav', 'duration': 2, 'length': 2.0, 'sample_rate': 22050, 'channels': 1, 'bitrate': 352800}
  [PASS] metadata 16bit stereo (2s/22050/2ch) | got={'path': 'D:\\test\\cicd_tmp\\r_stereo.wav', 'format': 'wav', 'duration': 2, 'length': 2.0, 'sample_rate': 22050, 'channels': 2, 'bitrate': 352800}
  [PASS] metadata 10s long (10s/22050/1ch) | got={'path': 'D:\\test\\cicd_tmp\\t_long.wav', 'format': 'wav', 'duration': 10, 'length': 10.0, 'sample_rate': 22050, 'channels': 1, 'bitrate': 352800}
  [PASS] Constructed FLAC parses (10s/44100/2ch) | got={'path': 'D:\\test\\cicd_tmp\\r_test.flac', 'format': 'flac', 'duration': 10, 'length': 10.0, 'sample_rate': 44100, 'channels': 2, 'bitrate': 33}
  [PASS] empty .flac -> 0 | got=0
  [PASS] empty .aac -> 0 | got=0
  [PASS] empty .ogg -> 0 | got=0
  [PASS] empty .mp3 -> 0 | got=0
  [PASS] by_type filters flac -> 1 | got=1
  [PASS] batch mixed formats -> 3 | got=3

==================================================================
 [S] Supplementary Cases
==================================================================
  --- 1. init boundary parameters ---
  [PASS] init(frequency=0) accepted
  [PASS] init(frequency=-44100) rejected by SDL
  [PASS] init(channels=0) accepted
  [PASS] init(channels=9) rejected by SDL
  [PASS] init(channels=32) rejected by SDL
  [PASS] init(chunksize=0) accepted
  [PASS] init(chunksize=-1) accepted
  [PASS] init(format=0) accepted
  [PASS] init(format=0xFFFF) rejected by SDL
  --- 2. loops parameter ---
📝 Recorded DAP file: D:\test\cicd_tmp\t_long.wav
  [PASS] play_from_file(loops=-1) returns AID | got=1
  [PASS] loops=-1 still playing
  [PASS] play_from_file(loops=2) returns AID | got=2
📝 Recorded DAP file: D:\test\cicd_tmp\t_short.wav
  [PASS] play_from_file(short WAV, loops=-1) returns AID | got=3
  [PASS] play_from_file(loops='2') no crash | got=(1999, 'Invalid loops type: str. Expected int.', 'loops must be an integer (-1=infinite, 0=once, >0=count)')
  [PASS] play_from_file(loops=None) no crash | got=(1999, 'Invalid loops type: NoneType. Expected int.', 'loops must be an integer (-1=infinite, 0=once, >0=count)')
  [PASS] play_from_memory(loops=-1) returns AID | got=5
  --- 3. batch show_progress output ---
  [PASS] show_progress returns list(2) | got=2
  [PASS] show_progress prints 'Batch parse complete' | ✅ Batch parse complete: 2/2 files successful

  --- 4. Delay method ---
  [PASS] Delay(100) ~100ms | 0.101s
  [PASS] Delay(0) fast
  --- 5. FileType base class ---
  [PASS] FileType.filename
  [PASS] FileType.info is StreamInfo
  [PASS] FileType.length property | 10.0
  [PASS] FileType.sample_rate property | 22050
  [PASS] FileType.channels property | 1
  [PASS] FileType.bitrate property | 352800
  --- 6. _get_sample_rate / _get_channels ---
  [PASS] _get_sample_rate(AID)=22050 | 22050
  [PASS] _get_channels(AID)=1 | 1
  [PASS] _get_sample_rate(invalid)=44100 fallback
  [PASS] _get_channels(invalid)=2 fallback
  --- 7. _get_aid_for_* success paths ---
  [PASS] _get_aid_for_music success | got=7
  [PASS] _get_aid_for_audio success | got=8
  [PASS] _get_aid_for_music(sound file) -> 1002 | got=(1002, 'No AID found for music file: D:\\test\\cicd_tmp\\t_short.wav', 'File may not be loaded or is not a music file')
  [PASS] _get_aid_for_audio(music file) -> 1002 | got=(1002, 'No AID found for file: D:\\test\\cicd_tmp\\t_long.wav', 'File may not be loaded or is a music file')

==================================================================
 [N] Listening Tests (please put on headphones / turn on speakers)
==================================================================

  Now playing at default volume 128 for 3 seconds...
📝 Recorded DAP file: C:\Users\dvs.新年快乐\Music\新建文件夹\test (4).mp3
  >>> Did you hear clear, normal-volume music? (y/n): y
  [PASS] Listening 1: normal playback audible

  Volume set to 40/128, playing for 2 seconds...
  >>> Was the volume clearly lower (very quiet)? (y/n): y
  [PASS] Listening 2: volume lowered

  Volume restored to 128, playing for 1 second...
  >>> Did the volume return to loud? (y/n): y
  [PASS] Listening 3: volume restored

  Pausing for 2 seconds (should be silent)...
  >>> Was it completely silent while paused? (y/n): y
  [PASS] Listening 4: pause is silent

  Resuming for 2 seconds...
  >>> Did playback resume from where it paused? (y/n): y
  [PASS] Listening 5: resume works

  Fade-in test: fadein(2000ms), playing for 3 seconds...
  >>> Did the sound fade in from silence to full volume? (y/n): y
  [PASS] Listening 6: fade-in effect

  Fade-out test: fadeout(2000ms), waiting 3 seconds...
  >>> Did the sound fade out gradually to silence? (y/n): y
  [PASS] Listening 7: fade-out effect

  Seek test: jump to 90s, playing for 2 seconds...
  >>> Did you hear the middle of the song (not the beginning)? (y/n): y
  [PASS] Listening 8: seek positioning

  Listening tests finished

==================================================================
 CICD Test Summary
==================================================================
   Passed : 421
   Failed : 0
   Skipped: 0
==================================================================
```

**You can run the test suite yourself** by executing `cicd_test.py` (included in the package). It will prompt you for an MP3 file path and run all checks:

```python
python cicd_test.py --auto      # automated tests only
python cicd_test.py --listen    # interactive listening tests only
python cicd_test.py --full      # everything (default)
```

## 🧪 API Import Validation Test

We provide a comprehensive test script `IMPORT-TEST.py` that verifies all documented APIs exist and are properly exported. This ensures the documentation stays accurate and users won't encounter missing imports.

### Test Coverage

The script tests:

1. **Top-level imports**: All functions exported at package level
2. **AudioLibrary methods**: All public and documented methods of the AudioLibrary class
3. **Extra APIs**: Additional functions mentioned in documentation but not in `__all__`

### Running the Test

```python
# Run the test
python IMPORT-TEST.py
```

### Test Output

Below is the full output of the API import validation test on a Windows system with Python 3.13.4:

```
============================================================
🧪 Testing All ap_ds API Exports
============================================================

📦 Testing top-level imports:
----------------------------------------

Warning (from warnings module):
  File "C:\Users\dvs.新年快乐\AppData\Local\Programs\Python\Python313\Lib\site-packages\ap_ds\audio_parser.py", line 74
  _check_runtime_mode()
RuntimeWarning: ⚠️ ap_ds: GIL is enabled (multi-core parallelism limited).
  For full performance, upgrade to Python 3.15t:
  https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-amd64.zip
  To suppress this warning, set AP_DS_SUPPRESS_WARNINGS=1
AP_DS © - Audio Library By DVS  v4.0.0 | https://apds.top

Warning (from warnings module):
  File "C:\Users\dvs.新年快乐\AppData\Local\Programs\Python\Python313\Lib\site-packages\ap_ds\__init__.py", line 635
  _check_runtime_mode()
RuntimeWarning: ⚠️ ap_ds: GIL is enabled (multi-core parallelism limited).
  For full performance, upgrade to Python 3.15t:
  https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-amd64.zip
  To suppress this warning, set AP_DS_SUPPRESS_WARNINGS=1
✅ SDL2 loaded from package directory
ℹ️ _IS_PYTHON_315_PLUS defined now: False (Python 3.13)
🎵 WAV playback mode threshold: 6s (Files >= 6s use music mode, < 6s use sound effect mode)
  ✅ 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

----------------------------------------
🎯 Testing AudioLibrary methods:
----------------------------------------
  ✅ AudioLibrary.__init__
  ✅ AudioLibrary.play_from_file
  ✅ AudioLibrary.play_from_memory
  ✅ AudioLibrary.new_aid
  ✅ AudioLibrary.play_audio
  ✅ AudioLibrary.pause_audio
  ✅ AudioLibrary.stop_audio
  ✅ AudioLibrary.seek_audio
  ✅ AudioLibrary.set_volume
  ✅ AudioLibrary.get_volume
  ✅ AudioLibrary.fadein_music
  ✅ AudioLibrary.fadein_music_pos
  ✅ AudioLibrary.fadeout_music
  ✅ AudioLibrary.is_music_playing
  ✅ AudioLibrary.is_music_paused
  ✅ AudioLibrary.get_music_fading
  ✅ AudioLibrary.get_audio_duration
  ✅ AudioLibrary.get_audio_metadata
  ✅ AudioLibrary.get_audio_metadata_by_path
  ✅ AudioLibrary.get_audio_metadata_by_aid
  ✅ AudioLibrary.batch_get_metadata
  ✅ AudioLibrary.batch_get_duration
  ✅ AudioLibrary.batch_get_metadata_by_type
  ✅ AudioLibrary.save_dap_to_json
  ✅ AudioLibrary.get_dap_recordings
  ✅ AudioLibrary.clear_dap_recordings
  ✅ AudioLibrary.clear_memory_cache
  ✅ AudioLibrary.cleanup_function
  ✅ AudioLibrary._find_channel_by_aid
  ✅ AudioLibrary._get_file_path_by_aid
  ✅ AudioLibrary._is_music_file
  ✅ AudioLibrary._seek_audio
  ✅ AudioLibrary._get_duration_by_filepath
  ✅ AudioLibrary._get_file_duration

----------------------------------------
🔍 Checking extra APIs (mentioned in docs but maybe not exported):
----------------------------------------
  ✅ is_full_performance (exists!)
  ✅ get_runtime_info (exists!)

============================================================
📊 FINAL SUMMARY
============================================================
🎉 ALL APIs EXIST! Documentation is accurate.
============================================================
✅ Passed: 45
❌ Failed: 0
```

### Result

**All 45 APIs tested passed successfully!**

| Category | Count | Status |
|----------|-------|--------|
| Top-level imports | 9 | ✅ All pass |
| AudioLibrary methods | 34 | ✅ All pass |
| Extra APIs | 2 | ✅ All pass |
| **Total** | **45** | **✅ 45/45 passed** |

### Test Script

The `IMPORT-TEST.py` script is included in the package and can be run at any time to verify API integrity:

```python
# IMPORT-TEST.py
# Full script available in the package
# Tests all documented APIs for existence
```

This test is automatically run during CI/CD and should always pass before any release.

---

## 📦 Version Relationship

| Version | Type | Support Period | Use Case |
|---------|------|----------------|----------|
| **v3.0.0 LTS** | Long‑Term Support | Until March 2031 | Production environments |
| **v3.1.x** | LFV (Latest Feature Version) | ~6 months | Early adopters (now superseded) |
| **v4.0.0** | LFV (Latest Feature Version) | ~6 months | Early adopters, new features |
| **v4.0.0 LTS** (future) | Long‑Term Support | TBD (after Python 3.15 stable) | Next stable production base |

---

## 📌 Version Upgrade Recommendations

| User Type | Recommendation |
|-----------|----------------|
| Production environment | Continue using **v3.0.0 LTS**, wait for v4.0.0 LTS |
| Development / Testing | Upgrade to **v4.0.0** to experience the new architecture and fixes |
| Need batch parsing / Python 3.15t support | **Must upgrade** to v4.0.0 |
| Previously affected by v3.1.x metadata bug | **Must upgrade** to v4.0.0 |

```bash
pip install --upgrade ap_ds
```

---

## 🌐 apds.top is Now Live!

**The official ap_ds project homepage is now live with TLS encryption!**

🎉 Visit: **https://apds.top**

### Website Features

- 📄 **Complete Documentation**: API reference, user guides, FAQs
- 📦 **Release Distribution**: All version download links and changelogs
- 🔗 **Repository Navigation**: GitCode (primary), Gitee (China mirror)
- ✉️ **Feedback System**: Users can submit feedback directly via the website
- 🔒 **Full-site TLS Encryption**: All pages served over HTTPS

---

## ⚠️ Repository Migration Notice

### GitHub Deprecation, GitLab Abandonment, Migration to GitCode – apds.top Becomes the Permanent Home

This section explains in detail the history of ap_ds's official code repository migrations and final destination.

#### 1. Why GitHub Was Deprecated

The developer's GitHub account was locked due to the loss of two‑factor authentication (2FA) devices. After multiple attempts to contact GitHub support, only automated bot replies were received. Due to the complete lack of human assistance, the developer decided to permanently abandon that GitHub account and will not create a new one in the foreseeable future. The old `dvs-web/ap_ds` repository is now officially deprecated and will no longer receive any updates.

#### 2. Why GitLab (JiHu) Was Abandoned

Following the GitHub issue, the project migrated its primary repository to Gitee and GitLab (JiHu). However, GitLab was recently abandoned due to platform policy changes that made basic account login a paid feature. Since the project relies on free open‑source collaboration, this change created an unacceptable barrier for contributors and users. The developer attempted to find alternatives but found none within the platform's free tier. Therefore, the GitLab repository is no longer actively maintained.

#### 3. Why Gitee Is Now a Backup (Not Primary)

Gitee is an excellent platform, especially for developers within China, offering fast and stable access. It remains a strongly recommended choice. However, its role has been adjusted to backup or China‑facing mirror for two primary reasons:

- **International Accessibility**: Gitee's servers are primarily located within China. For developers outside mainland China, access can be slow, unstable, and in some cases, completely blocked due to international network policies. This creates a poor experience for a significant portion of the user base.

- **User Interface and Workflow**: While functional, Gitee's UI and workflow are often considered outdated and less aligned with the modern Git workflows many international developers are accustomed to.

For these reasons, while Gitee is by no means "bad" and will continue to be fully supported as a China‑facing mirror, it is no longer suitable as the sole primary repository for a project with a global audience.

#### 4. The Solution: GitCode Becomes the New Primary Repository

After evaluating the landscape of free Git hosting platforms, **GitCode** emerged as the ideal solution. GitCode offers a modern interface, a robust feature set, and most importantly, excellent accessibility for both domestic and international developers. It has become the new primary official repository for the ap_ds project.

**The new primary repository is located at:** [https://gitcode.com/dvsxt/ap_ds](https://gitcode.com/dvsxt/ap_ds)

#### 5. The Permanent Home: apds.top

**apds.top is now officially live and fully operational!**

This is not just another repository mirror – it is the **permanent official home** of ap_ds. It resolves all platform dependency issues:

- ✅ **Full independent control**: No longer affected by third‑party platform policy changes
- ✅ **TLS encryption**: Full‑site HTTPS secure access
- ✅ **Permanent stability**: Even if all third‑party platforms fail, apds.top remains available
- ✅ **One‑stop service**: Documentation, downloads, feedback, and repository navigation all integrated

**Official Project Homepage:** [https://apds.top](https://apds.top)

---

## 🔄 New: GitHub Repository (Compatibility Mirror)

### A Message from the Developer

Recently, **Clint Shepherd**, a developer from the United States, reached out with a valid concern:

> *"Why is there no GitHub repository? GitCode is in China and I'm not used to it. apds.top doesn't have collaboration features like issues, pull requests, or forks..."*

After careful consideration, we realized he was absolutely right.

While we have no intention of ever returning to GitHub as our **primary** platform – their customer support was abysmal during our account lockout, and we still hold a grudge – the reality is undeniable:

- **GitHub has an enormous user base**
- **The ecosystem is incredibly mature**
- **Many developers are simply more comfortable with it**
- **Collaboration features (issues, PRs, forks) are standard expectations**

So, we've made a pragmatic decision: **we created a new GitHub account** (`dvs-dvsxt`) and established a compatibility mirror at:

🔗 **[https://github.com/dvs-dvsxt/ap_ds](https://github.com/dvs-dvsxt/ap_ds)**

### Important: This Is a Compatibility Mirror, NOT the Primary Repository

| Aspect | Details |
|--------|---------|
| **Purpose** | Compatibility mirror for GitHub-centric developers |
| **Primary Home** | **[apds.top](https://apds.top)** – Permanent official source |
| **Primary Mirror** | **[GitCode](https://gitcode.com/dvsxt/ap_ds)** – For global access |
| **China Mirror** | **[Gitee](https://gitee.com/dssxt/ap_ds)** – For China-based users |
| **GitHub Status** | ⚠️ **Compatibility Mirror** – Updated periodically, may lag behind |
| **Best For** | ✅ GitHub users who want to star, watch, or clone via familiar workflows |

### What This Means for You

- **If you're a GitHub user**: You can now clone, fork, and open issues on GitHub. We'll respond to GitHub issues, but **please be patient** – response times may be slower than on GitCode or apds.top.

- **If you're a China-based developer**: **Gitee** remains the fastest and most stable option for you. Use it without hesitation.

- **If you want the latest updates**: Always check **[apds.top](https://apds.top)** first. That's where the most current version, changelogs, and announcements live.

- **If you want to contribute**: We welcome contributions on **any** platform – GitCode, Gitee, or GitHub. PRs will be reviewed regardless of where they come from.

### Final Repository Strategy (Updated)

| Platform | Status | Purpose |
|----------|--------|---------|
| **apds.top** | ✅ **Permanent Home** | Official source, documentation, downloads |
| **GitCode** | ✅ Primary Mirror | Code hosting for global users |
| **GitHub** | ✅ Compatibility Mirror | For GitHub-centric developers (new!) |
| **Gitee** | ✅ China Mirror | Fast access for China‑based users |
| **GitLab (JiHu)** | ❌ Abandoned | No longer maintained |

> **Special thanks to Clint Shepherd** for asking the hard question and pushing us to make ap_ds more accessible to the global Python community. Your feedback made this better. 🙏

---

## Overview

ap_ds is a lightweight (2.5MB) Python audio library for playing and high‑precision metadata parsing of MP3, FLAC, OGG, and WAV files. It has zero external Python dependencies, using only the Python standard library, and provides non‑blocking playback suitable for GUI applications.

**Core Features:**

- **Extremely lightweight:** 2.5MB on Windows / 3.36MB on macOS complete solution
- **Zero Python dependencies:** Uses only the standard library
- **High‑precision metadata:** WAV/FLAC 100%, OGG 99.99%, MP3 >98%
- **Batch parsing:** Process hundreds of files in parallel using `batch_get_metadata()`
- **Non‑blocking playback:** Perfect for GUI applications
- **Cross‑platform:** Windows, macOS, Linux, embedded ARM64
- **DAP recording system:** Automatic playback history with metadata only
- **Python 3.15t support:** GIL‑less true parallelism, full multi‑core performance
- **LTS support:** First long‑term support version with 5‑year maintenance commitment

---

## Contact & Support

### 📧 Licensing Inquiries

me@dvsyun.top or dvs6666@163.com · Response within 7 business days

### 🛠️ Technical Support

apds.top Issues · GitCode Issues · GitHub Issues · Gitee Issues · Email (completely free)

---

## Official Repositories & Project Sources

**✅ Official Primary Repository (First Recommendation):** [https://apds.top](https://apds.top) – ap_ds's permanent official home, fully independently controlled, TLS encrypted, unaffected by third‑party platform policies.

**✅ Primary Mirror (Global Access):** [GitCode](https://gitcode.com/dvsxt/ap_ds) – Globally accessible, modern UI, actively maintained as a public mirror.

**✅ Compatibility Mirror (GitHub Users):** [GitHub](https://github.com/dvs-dvsxt/ap_ds) – For GitHub-centric developers. May lag behind primary sources. Issues and PRs welcome but may have slower response times.

**✅ China Mirror (Fast & Stable):** [Gitee](https://gitee.com/dssxt/ap_ds) – Full mirror for China‑based developers, fast access, classic stable UI.

**❌ Deprecated & Abandoned:**

- **[GitHub (dvs-web/ap_ds)](https://github.com/dvs-web/ap_ds)** – Deprecated due to permanent account lockout, no longer maintained.
- **[GitLab (JiHu)](https://jihulab.com/dvs/ap_ds)** – Abandoned due to platform policy changes (basic account login became a paid feature), no longer maintained.

> **ℹ️ ap_ds v3.0.0 LTS – First Long‑Term Support Release**
> This version consolidates all previous improvements, adds deterministic resource cleanup, hash‑verified downloads, and a 5‑year support commitment. The old GitHub repository (`dvs-web/ap_ds`) is deprecated and no longer updated. The GitLab repository has been abandoned. For future updates and contributions, please use the official primary repository **[apds.top](https://apds.top)** or the public mirrors **GitCode**, **GitHub (dvs-dvsxt/ap_ds)**, and **Gitee**.
>
> **Developer Personal Homepage & Blog:** [https://dvsx.top](https://dvsx.top) – Blog is currently undergoing maintenance and upgrades. Stay tuned.
> **ap_ds Project Homepage:** [https://apds.top](https://apds.top) (Official documentation, releases, and license center).

**🔗 Canonical URL:** [https://apds.top/](https://apds.top/)
**📖 Blog & Author:** [dvsx.top](https://dvsx.top) – Blog is currently undergoing maintenance and upgrades. Stay tuned.

---

## About the Author

**Developer:** Dvs (DvsXT)
**Personal Homepage & Blog:** [https://dvsx.top](https://dvsx.top) – Blog is currently undergoing maintenance and upgrades. Stay tuned
**Author Bio:** [https://dvsyun.top/me/dvs](https://dvsyun.top/me/dvs)
**Email:** me@dvsyun.top · dvs6666@163.com

## ap_ds Official Portal

**🎵 ap_ds Official Website (Primary):** [https://apds.top](https://apds.top) – Permanent official home, complete documentation, releases, license center
**📦 PyPI Project Page:** [https://pypi.org/project/ap_ds/](https://pypi.org/project/ap_ds/) – Installable via pip
**🌐 Mirror Documentation Site:** [https://www.dvsyun.top/ap_ds](https://www.dvsyun.top/ap_ds) – Backup documentation access

> 👉 The ap_ds project homepage ([apds.top](https://apds.top)) is the **first recommendation** for official sources, hosting complete documentation, license details, version changelogs, and official releases. The author's personal blog ([dvsx.top](https://dvsx.top)) is currently undergoing maintenance and upgrades. Stay tuned.

---

## Let's Get Started!

### Installation

```bash
pip install ap_ds
```

Upgrade from an older version:

```bash
pip install --upgrade ap_ds
```

### 💡 Why Python 3.15t / 3.14t?

Python's **Free‑Threading version** (filename with `t`) **removes the GIL (Global Interpreter Lock)** enabling true multi‑core parallelism. Combined with ap_ds v4.0.0's batch parsing, **120 MP3 files can be parsed in just 0.33 seconds**.

> ⚠️ **Version Selection Note**: Python 3.15t is currently a beta release (b4) and may have unknown issues. For a more stable environment, we recommend Python 3.14t (stable). Both support GIL‑less free‑threading mode.

#### Windows Users

**Python 3.14t (Stable) Downloads:**

| Architecture | Download Link |
|--------------|---------------|
| Windows 64‑bit | [python-3.14.4t-amd64.zip](https://mirrors.huaweicloud.com/python/3.14.4/python-3.14.4t-amd64.zip) |
| Windows 32‑bit | [python-3.14.4t-win32.zip](https://mirrors.huaweicloud.com/python/3.14.4/python-3.14.4t-win32.zip) |
| ARM64 | [python-3.14.4t-arm64.zip](https://mirrors.huaweicloud.com/python/3.14.4/python-3.14.4t-arm64.zip) |

**Python 3.15t (Beta) Downloads:**

| Architecture | Download Link |
|--------------|---------------|
| Windows 64‑bit | [python-3.15.0b4t-amd64.zip](https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-amd64.zip) |
| Windows 32‑bit | [python-3.15.0b4t-win32.zip](https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-win32.zip) |
| ARM64 | [python-3.15.0b4t-arm64.zip](https://mirrors.huaweicloud.com/python/3.15.0/python-3.15.0b4t-arm64.zip) |

> 📦 **ZIP – Extract and Use**: Download and extract to any directory, add the `python.exe` path to your system PATH, and you're ready to go. No need to run an EXE installer – deployment takes seconds.

#### Linux Users

**Option 1: Using Package Manager**

**Fedora:**

```bash
sudo dnf install python3.14-freethreading
```

After installation, the interpreter is located at `/usr/bin/python3.14t`.

**Ubuntu/Debian (using deadsnakes PPA):**

```bash
sudo add-apt-repository ppa:deadsnakes
sudo apt-get update
sudo apt-get install python3.14-nogil
```

> This PPA provides the `-nogil` version, which is also a GIL-disabled build.

**Option 2: Using Conda (Cross-platform)**

Install from the `conda-forge` channel:

```bash
conda create -n nogil -c conda-forge python-freethreading
mamba create -n nogil -c conda-forge python-freethreading
```

**Option 3: Compiling from Source (General Method)**

```bash
# Download Python 3.14 source
wget https://www.python.org/ftp/python/3.14.0/Python-3.14.0.tgz
tar -xzf Python-3.14.0.tgz
cd Python-3.14.0

# Configure: --disable-gil is the key parameter
./configure --disable-gil

# Compile and install
make -j$(nproc)
sudo make install
```

#### macOS Users

**Option 1: Official Installer (Graphical)**

1. Download the macOS installer package from [python.org](https://www.python.org/downloads/)
2. Run the installer, click the **"Customize"** button on the "Installation Type" screen
3. In the component list that appears, **check the "Free-threaded Python"** option, and continue with the installation

**Option 2: Using Homebrew**

```bash
brew install python-freethreading
```

After installation, the interpreter is located at `$(brew --prefix)/bin/python3.14t`.

#### Verify Installation

Run the following commands to verify that the free-threading version is working correctly:

```bash
# Check version information (should include "free-threading build")
python3.14t --version

# Check GIL status (output False means GIL is disabled)
python3.14t -c "import sys; print(sys._is_gil_enabled())"
```

#### Create a Virtual Environment

```bash
python3.14t -m venv my_env
source my_env/bin/activate  # Linux/macOS
my_env\Scripts\activate     # Windows
```

> 💡 **Tip**: Using `python3.14t -m venv` creates a GIL-less isolated environment.

### Quick Start

```python
from ap_ds import AudioLibrary

# Initialize the library
lib = AudioLibrary()

# Play an audio file
aid = lib.play_from_file("music/song.mp3")

# Control playback
lib.pause_audio(aid)      # Pause
lib.play_audio(aid)       # Resume
lib.seek_audio(aid, 30.5) # Seek to 30.5 seconds

# Stop and get the played duration
duration = lib.stop_audio(aid)
print(f"Played {duration:.2f} seconds")
```

### 🚀 Batch Parsing

```python
from ap_ds import batch_get_metadata

# Batch parse an entire folder (120 MP3s in just 0.33 seconds!)
results = batch_get_metadata("/music/playlist/", max_workers=8)

for meta in results:
    print(f"{meta['path']}: {meta['duration']}s, {meta['bitrate']}bps")
```

**Batch Parsing APIs at a Glance:**

| API | Description |
|-----|-------------|
| `batch_get_metadata()` | Batch parse, returns full metadata list |
| `batch_get_duration()` | Batch get durations, returns `{path: duration}` |
| `batch_get_metadata_by_type()` | Filter batch parsing by format |

### ⚠️ CRITICAL: Windows Batch Parsing & BrokenProcessPool

If you are using the **batch parsing APIs** on **Windows**, you **MUST** protect your entry point with `if __name__ == "__main__"`.

**Why?**

On Windows, `ProcessPoolExecutor` uses `spawn` to create new processes. Each subprocess re-imports your main module. Without the entry point guard, this creates an **infinite recursion loop** that crashes your program with a `BrokenProcessPool` error.

**This is NOT optional. It is MANDATORY.**

#### ❌ WRONG (Will crash on Windows):

```python
from ap_ds import batch_get_metadata

# This will crash on Windows with BrokenProcessPool!
results = batch_get_metadata("/music/", max_workers=4)
```

#### ✅ CORRECT (Always works):

```python
from ap_ds import batch_get_metadata

def main():
    results = batch_get_metadata("/music/", max_workers=4)
    print(f"Parsed {len(results)} files")

if __name__ == "__main__":
    main()
```

#### ✅ CORRECT (For scripts with configuration):

```python
from ap_ds import batch_get_metadata

def get_config():
    # ... configuration logic ...
    return config

def main():
    config = get_config()
    results = batch_get_metadata(config["audio_dir"], max_workers=4)
    print(f"Parsed {len(results)} files")

if __name__ == "__main__":
    main()
```

**This applies to:**

- ✅ Any script that imports ap_ds and uses batch parsing on Windows
- ✅ Jupyter notebooks (if running on Windows, wrap the batch call in a function and use `if __name__ == "__main__"`)
- ✅ Any test scripts (like `CI-CD-TEST.py`)

**Does this apply to Linux/macOS?**

No. Linux and macOS use `fork` by default, which does not have this issue. However, it's still **good practice** to use the entry point guard for cross-platform compatibility.

### DAP Playlist System

Audio files are automatically recorded to DAP (Dvs Audio Playlist) when played:

```python
# Files are automatically recorded
aid1 = lib.play_from_file("song1.mp3")
aid2 = lib.play_from_file("song2.ogg")

# Get all recordings
recordings = lib.get_dap_recordings()
print(f"Recorded {len(recordings)} files")

# Save as JSON
success = lib.save_dap_to_json("my_playlist.ap-ds-dap")
```

DAP stores only metadata (path, duration, bitrate, channels), not audio data. Each record uses approximately 150 bytes of memory.

### Platform Support

#### Windows

- Automatically downloads SDL2.dll and SDL2_mixer.dll with hash verification
- No manual configuration required
- Supports Windows 7 and above

#### macOS

- Automatically downloads SDL2.framework and SDL2_mixer.framework with hash verification
- No manual configuration required
- Supports macOS 10.9 and above

#### Linux

**Intelligent multi-layer import system:**

1. **System library check:** Uses system-installed SDL2 libraries
2. **User configuration:** Checks paths saved from previous runs
3. **Automatic installation:** Detects package manager and installs required packages
4. **Interactive guidance:** Provides manual options if all above fail

**Package manager support:**

```bash
# Ubuntu/Debian
sudo apt-get install libsdl2-dev libsdl2-mixer-dev

# Fedora
sudo dnf install SDL2-devel SDL2_mixer-devel

# Arch
sudo pacman -S sdl2 sdl2_mixer
```

#### Embedded ARM64

Tested on:

- **Orange Pi 4 Pro** (Allwinner A733, 2xA76 + 6xA55 @ 2.0GHz)
- **Raspberry Pi 5** (BCM2712, 4xA76 @ 2.4GHz)

Both run Ubuntu 22.04 with full audio functionality via 3.5mm output. Memory growth after extensive testing: ~4MB.

---

# ap_ds Audio Library – Complete API Reference

**Version: v4.0.0**
**Documentation Date: August 2026**
**Project Homepage: https://apds.top**

## Table of Contents

1. AudioLibrary Class – Complete API
   - Initialization
   - Playback Methods
   - Control Methods
   - Volume Methods
   - Fade & Transition Methods
   - Metadata Methods
   - Batch Parsing Methods
   - DAP System Methods
   - Resource Management
   - Internal Helper Methods

2. Top-Level Convenience Functions

3. AudioParser Module – Metadata API

4. SDL2 Integration Layer

5. Constants Reference

6. Unified Error Handling & Error Codes Reference

7. Environment Variables Reference

8. Technical Manual (show_tech_manual())

## AudioLibrary Class – Complete API

The `AudioLibrary` class is the primary interface for audio playback, control, and metadata management. Every method that can fail returns a **unified error tuple**:

```
(code: int, message: str, suggestion: str)
```

On success, methods return their documented success value. On failure, they **never raise** (except `__init__`, which is a constructor) — they return the error tuple above. See the *Unified Error Handling & Error Codes Reference* section for the complete code table.

### Initialization

#### `__init__(frequency: int = 44100, format: int = MIX_DEFAULT_FORMAT, channels: int = 2, chunksize: int = 2048) -> None`

**Description:**

Initializes the SDL2 audio subsystem and SDL2_mixer library. This method must be called before any playback operations. It sets up the audio device with the specified parameters and registers an exit handler for automatic resource cleanup.

> **Note:** `__init__` is a **constructor** — it cannot return an error tuple. If SDL2 or mixer initialization fails, it raises `RuntimeError` (this is the one intentional exception in the library).

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `frequency` | `int` | `44100` | Audio sample rate (Hz). Common values: 44100 (CD quality), 48000 (DVD/video), 22050 (voice). |
| `format` | `int` | `MIX_DEFAULT_FORMAT` | Audio sample format. Typically `AUDIO_S16SYS` (16-bit signed, system endianness). |
| `channels` | `int` | `2` | Number of audio channels. `1` = mono, `2` = stereo. |
| `chunksize` | `int` | `2048` | Buffer size in samples. Larger values reduce CPU but increase latency. |

**Raises (constructor only):**

- `RuntimeError`: If SDL2 initialization fails (e.g., no audio device available).
- `RuntimeError`: If mixer initialization fails (e.g., unsupported format).

**Example:**

```python
from ap_ds import AudioLibrary

lib = AudioLibrary()                              # default (CD quality, stereo)
lib_voice = AudioLibrary(frequency=22050, channels=1, chunksize=1024)
```

---

### Playback Methods

#### `play_from_file(file_path: str, loops: int = 0, start_pos: float = 0.0) -> Union[int, Tuple[int, str, str]]`

**Description:**

Loads and plays an audio file directly from disk. `.ap-ds-dap` files are DAP **export records** (output format) and are **not** supported as playback input — passing one returns `(1003, msg, suggestion)`.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `file_path` | `str` / `bytes` / `os.PathLike` | Required | Full path to the audio file. Supported formats: MP3, WAV, FLAC, OGG. |
| `loops` | `int` | `0` | Loops after the first play. `0` = once, `-1` = infinite, `>0` = count. |
| `start_pos` | `float` | `0.0` | Starting position in seconds. Not supported for sound effects. |

**Returns:**

- **Success:** `int` – A unique Audio ID (AID) identifying this playback instance.
- **Failure:** `Tuple[int, str, str]` – `(error_code, error_message, suggestion)`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | `file_path` does not exist, or `file_path` has an invalid type (`None`, `int`, `list`, `dict`, `tuple`, etc.). |
| `1999` | `loops` has an invalid type (must be `int`). |
| `1003` | Audio failed to load (e.g., a `.ap-ds-dap` file, corrupted file, or unsupported format). |
| `1004` | Playback failed (e.g., no available channel or audio device issue). |

**Behavior by File Type:**

| File Type | Mode | Seek Support | Fade Support |
|-----------|------|--------------|--------------|
| MP3, OGG, FLAC | Music (Mix_PlayMusic) | Yes | Yes |
| WAV (duration ≥ threshold) | Music (Mix_PlayMusic) | Yes | Yes |
| WAV (duration < threshold) | Sound effect (Mix_PlayChannel) | No | No |
| Other formats | Sound effect (Mix_PlayChannel) | No | No |

**Example:**

```python
aid = lib.play_from_file("song.mp3")                  # play once
aid = lib.play_from_file("beep.wav", loops=5)         # loop 5 times
aid = lib.play_from_file("podcast.mp3", start_pos=30.0)
aid = lib.play_from_file("ambient.ogg", loops=-1)     # infinite loop

# Invalid inputs return error tuples (no exception):
result = lib.play_from_file(None)          # (1001, 'Invalid file path type: NoneType', ...)
result = lib.play_from_file("x.mp3", loops="2")  # (1999, 'Invalid loops type: str...', ...)
```

**Internal Workflow:**

1. Validates `file_path` and `loops` types.
2. Increments `_aid_counter` to generate a new AID.
3. Calls `_add_to_dap_recordings()` to record metadata (if available).
4. Determines playback mode via `_is_music_file()`.
5. For music files: `Mix_LoadMUS()` → `Mix_PlayMusic()` → store in `_music_cache`, `channel = -1`.
6. For sound effects: `Mix_LoadWAV()` → `Mix_PlayChannel(-1, audio, loops)` → store in `_audio_cache`.
7. Stores playback info in `_channel_info[channel]`.
8. If `start_pos > 0`, calls `_seek_audio()`.

---

#### `play_from_memory(file_path: str, loops: int = 0, start_pos: float = 0.0) -> Union[int, Tuple[int, str, str]]`

**Description:**

Plays an audio file that has been preloaded into memory via `new_aid()`. Faster than `play_from_file()` for repeated plays since the file is already cached.

**Parameters:** Same as `play_from_file()`.

**Returns:**

- **Success:** `int` (AID).
- **Failure:** `Tuple[int, str, str]` – `(error_code, error_message, suggestion)`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1013` | `file_path` is not loaded in memory, or `file_path` has an invalid type (`None`, `list`, `dict`, etc.). |
| `1999` | `loops` has an invalid type (must be `int`). |
| `1004` | Playback from cache failed. |

**Example:**

```python
lib.new_aid("gunshot.wav")
lib.new_aid("explosion.wav")
aid = lib.play_from_memory("gunshot.wav")   # instant, no disk I/O
```

**Internal Workflow:**

1. Validates `file_path` and `loops` types.
2. Generates a new AID and records to DAP.
3. Checks `_music_cache` / `_audio_cache`.
4. Plays from the cached object and records info in `_channel_info`.

---

#### `new_aid(file_path: str) -> Union[int, Tuple[int, str, str]]`

**Description:**

Preloads an audio file into memory without playing it. Useful for caching sounds or tracks that will be played multiple times.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `file_path` | `str` / `bytes` / `os.PathLike` | Path to the audio file to cache. |

**Returns:**

- **Success:** `int` (AID) for the cached file.
- **Failure:** `Tuple[int, str, str]` – `(error_code, error_message, suggestion)`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | `file_path` does not exist or has an invalid type (`None`, `float`, `list`, etc.). |
| `1003` | Audio failed to load. |

**Example:**

```python
sounds = {
    'hit':  lib.new_aid("hit.wav"),
    'jump': lib.new_aid("jump.wav"),
    'coin': lib.new_aid("coin.wav"),
}
lib.play_from_memory(sounds['hit'])
```

**Internal Workflow:**

1. Validates `file_path` type.
2. Increments AID counter and records to DAP.
3. Determines music vs sound effect via `_is_music_file()`.
4. Loads via `Mix_LoadMUS()` / `Mix_LoadWAV()` and stores in the appropriate cache.
5. Maps AID to file path in `_aid_to_filepath`.

---

### Control Methods

#### `play_audio(aid: int) -> Tuple[int, str, str]`

**Description:**

Resumes a paused audio instance identified by `aid`.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID returned by a play/load method. |

**Returns:**

- **Success:** `(0, "", "")` – `AP_DS_SUCCESS`.
- **Failure:** `Tuple[int, str, str]` – `(error_code, error_message, suggestion)`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid (no matching active playback instance). |

**Example:**

```python
lib.pause_audio(aid)
lib.play_audio(aid)      # resume
result = lib.play_audio(99999)   # (1002, 'Invalid AID: 99999', ...)
```

---

#### `pause_audio(aid: int) -> Tuple[int, str, str]`

**Description:**

Pauses the audio instance identified by `aid`. Playback can be resumed with `play_audio()`.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid. |

**Example:**

```python
lib.pause_audio(aid)
result = lib.pause_audio(99999)   # (1002, 'Invalid AID: 99999', ...)
```

---

#### `stop_audio(aid: int) -> Union[float, Tuple[int, str, str]]`

**Description:**

Stops playback and returns the played duration in seconds.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |

**Returns:**

- **Success:** `float` – Played duration in seconds.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid. |

**Example:**

```python
played = lib.stop_audio(aid)          # e.g. 3.42
result = lib.stop_audio(99999)        # (1002, 'Invalid AID: 99999', ...)
```

---

#### `seek_audio(aid: int, position: float) -> Tuple[int, str, str]`

**Description:**

Seeks the audio instance to a specified position (seconds). Only supported for music-mode files (MP3, OGG, FLAC, long WAV).

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |
| `position` | `int` / `float` | Position in seconds. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid. |
| `1999` | `position` has an invalid type (`None`, `str`, etc.). |

**Example:**

```python
lib.seek_audio(aid, 30.5)
result = lib.seek_audio(aid, None)    # (1999, 'Invalid position type: NoneType...', ...)
```

---

### Volume Methods

#### `set_volume(aid: int, volume: int) -> Tuple[int, str, str]`

**Description:**

Sets the volume for the audio instance. Volume range is **0–128**.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |
| `volume` | `int` | Volume value 0–128. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid. |
| `1015` | `volume` is not an `int`, or is outside 0–128. |
| `1004` | Mixer failed to apply the volume. |

**Example:**

```python
lib.set_volume(aid, 64)
result = lib.set_volume(aid, "loud")   # (1015, 'Invalid volume type: str...', ...)
result = lib.set_volume(aid, 200)      # (1015, 'Invalid volume: 200 (must be 0-128)', ...)
```

---

#### `get_volume(aid: int) -> Union[int, Tuple[int, str, str]]`

**Description:**

Returns the current volume (0–128) of the audio instance.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |

**Returns:**

- **Success:** `int` – Current volume (0–128).
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid. |

**Example:**

```python
v = lib.get_volume(aid)      # e.g. 64
result = lib.get_volume(99999)   # (1002, 'Invalid AID: 99999', ...)
```

---

### Fade & Transition Methods

#### `fadein_music(aid: int, loops: int = -1, ms: int = 0) -> Tuple[int, str, str]`

**Description:**

Fades in music from silence to full volume over `ms` milliseconds.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `aid` | `int` | — | AID of a music file. |
| `loops` | `int` | `-1` | `-1` = infinite, `0` = once, `>0` = count. |
| `ms` | `int` | `0` | Fade-in duration in milliseconds. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid or not a music file. |
| `1999` | `ms` / `loops` has an invalid type (must be `int`). |
| `1003` | Music failed to load. |
| `1004` | SDL_mixer fade failure. |

**Example:**

```python
lib.fadein_music(aid, ms=2000)          # 2-second fade in
result = lib.fadein_music(aid, ms=None) # (1999, 'Invalid fade parameters...', ...)
```

---

#### `fadein_music_pos(aid: int, loops: int = -1, ms: int = 0, position: float = 0.0) -> Tuple[int, str, str]`

**Description:**

Fades in music from a specified position.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `aid` | `int` | — | AID of a music file. |
| `loops` | `int` | `-1` | Loop count. |
| `ms` | `int` | `0` | Fade-in duration in milliseconds. |
| `position` | `int` / `float` | `0.0` | Start position in seconds. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | `aid` is invalid or not a music file. |
| `1999` | `ms` / `loops` / `position` has an invalid type. |
| `1012` | `Mix_FadeInMusicPos` is not supported by this SDL_mixer version. |
| `1003` | Music failed to load. |
| `1004` | SDL_mixer fade failure. |

**Example:**

```python
lib.fadein_music_pos(aid, ms=2000, position=30.0)
result = lib.fadein_music_pos(aid, ms=100, position=None)  # (1999, ...)
```

---

#### `fadeout_music(ms: int = 0) -> Tuple[int, str, str]`

**Description:**

Fades out the currently playing music over `ms` milliseconds.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ms` | `int` | `0` | Fade-out duration in milliseconds. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1004` | No music is playing, or the fade-out failed. |

**Example:**

```python
lib.fadeout_music(2000)   # 2-second fade out
```

---

#### `is_music_playing() -> bool`

**Description:**

Returns whether music is currently playing.

**Returns:** `bool` – `True` if playing, `False` otherwise.

---

#### `is_music_paused() -> bool`

**Description:**

Returns whether music is currently paused.

**Returns:** `bool` – `True` if paused, `False` otherwise.

---

#### `get_music_fading() -> int`

**Description:**

Returns the current fade state.

**Returns:** `int`:

- `0` (`MUS_NO_FADING`): No fade in progress.
- `1` (`MUS_FADING_IN`): Fading in.
- `2` (`MUS_FADING_OUT`): Fading out.

---

### Metadata Methods

#### `get_audio_duration(source: Union[str, int], is_file: bool = False) -> Union[int, Tuple[int, str, str]]`

**Description:**

Returns the duration of an audio file in seconds. Accepts either a file path (`str`) or an AID (`int`).

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `source` | `str` or `int` | — | File path or AID. |
| `is_file` | `bool` | `False` | If `True`, treats `source` as a file path. |

**Returns:**

- **Success:** `int` – Duration in seconds (floor).
- **Failure:** `Tuple[int, str, str]` – `(error_code, error_message, suggestion)`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | File does not exist. |
| `1002` | Invalid AID. |
| `1011` | Metadata parse failure. |
| `1999` | Unknown error (e.g., unsupported file). |

**Example:**

```python
d = lib.get_audio_duration("song.mp3", is_file=True)   # e.g. 240
d = lib.get_audio_duration(aid)                        # by AID
result = lib.get_audio_duration(99999)                 # (1002, 'Invalid AID: 99999', ...)
```

---

#### `get_audio_metadata(source: Union[str, int], is_file: bool = False) -> Union[Dict, Tuple[int, str, str]]`

**Description:**

Returns complete metadata for an audio file. Accepts a file path (`str`) or an AID (`int`).

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `source` | `str` or `int` | — | File path or AID. |
| `is_file` | `bool` | `False` | If `True`, treats `source` as a file path. |

**Returns:**

- **Success:** `Dict` with keys: `path`, `format`, `duration`, `length`, `sample_rate`, `channels`, `bitrate`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | File does not exist. |
| `1002` | Invalid AID. |
| `1011` | Metadata parse failure. |
| `1014` | `source` has an invalid type (not `str` / `int`). |

**Example:**

```python
meta = lib.get_audio_metadata("song.mp3", is_file=True)
print(meta['duration'], meta['sample_rate'], meta['channels'])
result = lib.get_audio_metadata(1.5)   # (1014, 'Invalid source type...', ...)
```

---

#### `get_audio_metadata_by_path(file_path: str) -> Union[Dict, Tuple[int, str, str]]`

**Description:**

Returns complete metadata for an audio file by path.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `file_path` | `str` | Path to the audio file. |

**Returns:**

- **Success:** `Dict` (same keys as above).
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | File does not exist. |
| `1011` | Metadata parse failure. |
| `1999` | Unknown error. |

---

#### `get_audio_metadata_by_aid(aid: int) -> Union[Dict, Tuple[int, str, str]]`

**Description:**

Returns complete metadata for an audio file by AID.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `aid` | `int` | The Audio ID. |

**Returns:**

- **Success:** `Dict`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | Invalid AID. |
| `1011` | Metadata parse failure. |
| `1999` | Unknown error. |

---

#### `simple_mp3_duration_estimation(filename: str) -> float`

**Description:**

Estimates MP3 duration from file size and a typical bitrate. Fallback when frame parsing fails.

**Returns:** `float` – Estimated duration in seconds, `0.0` on error.

---

### Batch Parsing Methods

> **Windows note:** When using batch APIs, protect your entry point with `if __name__ == "__main__":` (see the CRITICAL section at the top of this document).

#### `batch_get_metadata(file_paths: Union[List[str], str], max_workers: Optional[int] = None, show_progress: bool = False) -> List[Dict]`

**Description:**

Parses multiple audio files in parallel using `ProcessPoolExecutor`. Accepts a list of file paths or a directory path (recursively scanned for supported formats).

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `file_paths` | `List[str]` or `str` | — | File list, or a directory path. |
| `max_workers` | `int` | `None` | Worker count. Defaults to CPU count. Must be a positive `int` (within platform limits). |
| `show_progress` | `bool` | `False` | Print progress to stdout. |

**Returns:**

- **Success:** `List[Dict]` – Metadata dicts for successfully parsed files (failed ones omitted).
- **Failure:** `Tuple[int, str, str]` – returned when `max_workers` is invalid.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1999` | `max_workers` is invalid (0, negative, non-int, or above the platform limit). |

**Example:**

```python
results = batch_get_metadata("/music/", max_workers=4, show_progress=True)
result = batch_get_metadata(["a.mp3"], max_workers=0)  # (1999, 'Invalid max_workers...', ...)
```

---

#### `batch_get_duration(file_paths: Union[List[str], str], max_workers: Optional[int] = None) -> Dict[str, int]`

**Description:**

Returns durations for multiple files in parallel.

**Returns:**

- **Success:** `Dict[str, int]` – `{path: duration_seconds}`.
- **Failure:** `Tuple[int, str, str]` – invalid `max_workers`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1999` | `max_workers` is invalid. |

---

#### `batch_get_metadata_by_type(file_paths: Union[List[str], str], file_type: str, max_workers: Optional[int] = None) -> List[Dict]`

**Description:**

Parses multiple files but only returns results matching a specific format (e.g., `"mp3"`, `"flac"`).

**Returns:**

- **Success:** `List[Dict]` – Metadata for matching files.
- **Failure:** `Tuple[int, str, str]` – invalid `max_workers`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1999` | `max_workers` is invalid. |

---

### DAP System Methods

#### `save_dap_to_json(save_path: str) -> Tuple[int, str, str]`

**Description:**

Saves DAP recordings to a `.ap-ds-dap` JSON file.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `save_path` | `str` | Output path. Must end with `.ap-ds-dap`. |

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1009` | Extension is not `.ap-ds-dap`. |
| `1010` | Write failure (bad path, permissions, disk full). |

**Example:**

```python
result = lib.save_dap_to_json("history.ap-ds-dap")   # (0, '', '')
result = lib.save_dap_to_json("out.json")            # (1009, ...)
```

---

#### `get_dap_recordings() -> List[Dict]`

**Description:**

Returns the current DAP recordings (a copy).

**Returns:** `List[Dict]` – each record has keys `path`, `duration`, `bitrate`, `channels`.

---

#### `clear_dap_recordings() -> None`

**Description:**

Clears all DAP recordings from memory.

**Returns:** `None`.

---

#### `_add_to_dap_recordings(file_path: str) -> None`

**Description (internal):**

Records an audio file's metadata into the DAP list with O(1) set-based deduplication (O(n) linear fallback). Silently skips unparseable files.

**Returns:** `None`.

---

### Resource Management

#### `clear_memory_cache() -> None`

**Description:**

Frees all cached `Mix_Chunk` and `Mix_Music` objects and clears the caches.

**Returns:** `None`.

---

#### `cleanup_function() -> None`

**Description:**

Cleans up all resources: clears caches, closes the mixer, and quits SDL. Registered with `atexit` automatically.

**Returns:** `None`.

---

### Internal Helper Methods

#### `_find_channel_by_aid(aid: int) -> Optional[int]`

**Description (internal):**

Finds the channel number associated with an AID.

**Returns:** `Optional[int]` – channel number, or `None` if not found.

---

#### `_get_file_path_by_aid(aid: int) -> Union[str, Tuple[int, str, str]]`

**Description (internal):**

Returns the file path associated with an AID.

**Returns:**

- **Success:** `str` – file path.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | Invalid AID. |

---

#### `_is_music_file(file_path: str) -> bool`

**Description (internal):**

Determines whether a file should use music mode or sound-effect mode.

**Returns:** `bool` – `True` for music mode (MP3/OGG/FLAC, or WAV ≥ threshold), `False` for sound-effect mode.

---

#### `_seek_audio(channel: int, position: float) -> Tuple[int, str, str]`

**Description (internal):**

Performs the actual seek operation on a channel.

**Returns:**

- **Success:** `(0, "", "")`.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | Channel not in tracking. |
| `1003` | Music failed to load for seeking. |
| `1004` | Playback failed after reload. |
| `1013` | Sound effect not in cache. |

---

#### `_get_duration_by_filepath(file_path: str) -> Union[int, Tuple[int, str, str]]`

**Description (internal):**

Gets duration for a file path using the metadata parser, with a file-size estimation fallback.

**Returns:**

- **Success:** `int` – duration in seconds.
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1001` | File not found. |
| `1011` | Metadata parse failure (estimation also failed). |
| `1999` | Unknown error. |

---

#### `_get_file_duration(file_path: str) -> float`

**Description (internal):**

Returns the file duration as `float`; returns `0.0` on failure.

**Returns:** `float`.

---

#### `_get_sample_rate(source: Union[str, int]) -> int`

**Description (internal):**

Returns the sample rate of an audio source.

**Returns:** `int` – sample rate in Hz, or `44100` fallback.

---

#### `_get_channels(source: Union[str, int]) -> int`

**Description (internal):**

Returns the channel count of an audio source.

**Returns:** `int` – channels, or `2` fallback.

---

#### `_get_aid_for_audio(file_path: str) -> Union[int, Tuple[int, str, str]]`

**Description (internal):**

Finds the AID of a sound-effect file by path.

**Returns:**

- **Success:** `int` (AID).
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | No matching sound-effect AID. |

---

#### `_get_aid_for_music(file_path: str) -> Union[int, Tuple[int, str, str]]`

**Description (internal):**

Finds the AID of a music file by path.

**Returns:**

- **Success:** `int` (AID).
- **Failure:** `Tuple[int, str, str]`.

**Error Codes:**

| Code | Condition |
|------|-----------|
| `1002` | No matching music AID. |

---

#### `_get_playing_duration(aid: int) -> float`

**Description (internal):**

Returns the total duration of the playing audio.

**Returns:** `float`.

---

## Top-Level Convenience Functions

The following functions are exported at the package top level (`from ap_ds import ...`).

#### `batch_get_metadata(file_paths: Union[List[str], str], max_workers: Optional[int] = None, show_progress: bool = False) -> List[Dict]`

**Description:**

Parses multiple audio files in parallel. Accepts a list of file paths or a directory (recursively scanned).

**Returns:**

- **Success:** `List[Dict]` – metadata for parsed files (failed ones omitted).
- **Failure:** `Tuple[int, str, str]` – invalid `max_workers` → `(1999, ...)`.

**Example:**

```python
from ap_ds import batch_get_metadata

results = batch_get_metadata("/music/", max_workers=4)
result = batch_get_metadata(["a.mp3"], max_workers=0)   # (1999, 'Invalid max_workers...', ...)
```

---

#### `batch_get_duration(file_paths: Union[List[str], str], max_workers: Optional[int] = None) -> Dict[str, int]`

**Description:**

Returns `{path: duration_seconds}` for multiple files in parallel.

**Returns:**

- **Success:** `Dict[str, int]`.
- **Failure:** `Tuple[int, str, str]` – invalid `max_workers`.

---

#### `batch_get_metadata_by_type(file_paths: Union[List[str], str], file_type: str, max_workers: Optional[int] = None) -> List[Dict]`

**Description:**

Parses multiple files and returns only those matching `file_type` (e.g., `"mp3"`).

**Returns:**

- **Success:** `List[Dict]`.
- **Failure:** `Tuple[int, str, str]` – invalid `max_workers`.

---

#### `get_audio_duration(file_path: str) -> int`

**Description:**

Returns the duration (seconds) of a single audio file.

**Returns:** `int` – duration in seconds, `0` on failure.

---

#### `get_audio_metadata(file_path: str) -> Optional[Dict]`

**Description:**

Returns complete metadata for a single audio file.

**Returns:** `Optional[Dict]` – metadata dict, or `None` on failure.

---

#### `auto_check_runtime() -> Optional[Dict]`

**Description:**

Runs the runtime self-check. By default (with `AP_DS_SKIP_AUTO_CHECK=1`) it is skipped and returns `None`. With the check enabled it returns a dict of runtime information.

**Returns:** `Optional[Dict]` – `None` (skipped) or a runtime-info dict.

---

#### `check_runtime_mode() -> bool`

**Description:**

Checks whether the GIL is enabled.

**Returns:** `bool` – `True` if GIL is enabled, `False` if free-threading.

---

#### `show_tech_manual() -> None`

**Description:**

Prints the complete built-in technical manual to stdout.

**Returns:** `None`.

**Example:**

```python
from ap_ds import show_tech_manual
show_tech_manual()
```

---

#### `is_full_performance() -> bool`

**Description:**

Returns whether the library is running in full performance mode (free-threading + profiling available).

**Returns:** `bool`.

---

#### `get_runtime_info() -> Dict`

**Description:**

Returns the runtime information dictionary.

**Returns:** `Dict` – with keys `library_name`, `library_version`, `library_install_path`, `library_website`, `library_author`, `python_version`, `gil_enabled`, `has_profiling`, `is_full_performance`, `cpu_count`, `platform`. Returns `{}` if the self-check is skipped.

---

## AudioParser Module – Metadata API

The `audio_parser` module provides pure-Python metadata parsers with zero external dependencies.

### Format-Specific Parser Classes

| Class | Format | Accuracy | Parsing Method |
|-------|--------|----------|----------------|
| `WAVFile` | WAV | 100% | RIFF chunk structure |
| `FLACFile` | FLAC | 100% | STREAMINFO metadata block |
| `MP3File` | MP3 | >98% | Frame-by-frame sync-word scanning |
| `AACFile` | AAC (ADTS) | >99% | ADTS frame parsing |
| `OGGFile` | OGG Vorbis | 99.99% | Granule position + Vorbis header |

All parser classes inherit from `FileType` and expose the properties `length`, `sample_rate`, `channels`, and `bitrate`.

#### `open_audio(filename: str) -> FileType`

**Description:**

Factory function that returns the appropriate parser instance for a file.

**Raises:**

- `ValueError`: If the file format is unsupported (this is the documented behavior of the factory).

**Example:**

```python
from ap_ds.audio_parser import open_audio
parser = open_audio("song.mp3")       # -> MP3File
print(parser.length, parser.sample_rate, parser.channels, parser.bitrate)
```

---

#### `StreamInfo` Class

**Description:**

Container for parsed stream metadata.

**Attributes:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `length` | `float` | Duration in seconds. |
| `sample_rate` | `int` | Sample rate in Hz. |
| `channels` | `int` | Channel count (1 = mono, 2 = stereo). |
| `bitrate` | `int` | Bitrate in bits per second. |

---

#### `get_audio_duration(file_path: str) -> int`

**Description:**

Returns the duration (seconds) of a single audio file.

**Returns:** `int` – duration, `0` on failure.

---

#### `get_audio_metadata(file_path: str) -> Optional[Dict]`

**Description:**

Returns complete metadata for a single audio file.

**Returns:** `Optional[Dict]` – dict with keys `path`, `format`, `duration`, `length`, `sample_rate`, `channels`, `bitrate`, or `None` on failure.

---

## SDL2 Integration Layer

The `_sdl2` module provides the cross-platform SDL2 loader, constants, structures, and ctypes bindings. It is an internal module; users interact with it indirectly through `AudioLibrary`.

### Global SDL2 Functions

| Function | Description |
|----------|-------------|
| `SDL_Init(flags)` | Initializes SDL subsystems. Returns 0 on success. |
| `SDL_InitSubSystem(flags)` | Initializes specific subsystems. |
| `SDL_Quit()` | Shuts down SDL. |
| `SDL_QuitSubSystem(flags)` | Shuts down specific subsystems. |
| `SDL_WasInit(flags)` | Returns which subsystems are initialized. |
| `SDL_GetError()` | Returns the last SDL error message. |
| `SDL_RWFromFile(file, mode)` | Opens a file as an SDL RWops handle. |
| `SDL_Delay(ms)` | Pauses the calling thread for `ms` milliseconds. |

### Global SDL2_mixer Functions

| Function | Description |
|----------|-------------|
| `Mix_OpenAudio(freq, format, channels, chunksize)` | Opens the audio mixer. Returns 0 on success. |
| `Mix_CloseAudio()` | Closes the mixer. |
| `Mix_LoadWAV(file)` | Loads a WAV sound effect into a `Mix_Chunk`. |
| `Mix_LoadMUS(file)` | Loads a music file into a `Mix_Music`. |
| `Mix_FreeChunk(chunk)` | Frees a sound-effect chunk. |
| `Mix_FreeMusic(music)` | Frees a music object. |
| `Mix_PlayChannel(channel, chunk, loops)` | Plays a chunk on a channel. |
| `Mix_PlayMusic(music, loops)` | Plays music. |
| `Mix_Pause(channel)` / `Mix_PauseMusic()` | Pauses a channel / music. |
| `Mix_Resume(channel)` / `Mix_ResumeMusic()` | Resumes a channel / music. |
| `Mix_HaltChannel(channel)` / `Mix_HaltMusic()` | Stops a channel / music. |
| `Mix_SetMusicPosition(position)` | Seeks music to a position (seconds). |
| `Mix_MusicDuration(music)` | Returns music duration in seconds. |
| `Mix_Volume(channel, volume)` | Sets/gets channel volume (0–128, `-1` = get). |
| `Mix_VolumeMusic(volume)` | Sets/gets music volume (0–128, `-1` = get). |
| `Mix_AllocateChannels(num)` | Allocates mixer channels. |
| `Mix_GetMusicType(music)` | Returns the music type. |
| `Mix_FadingMusic()` | Returns the current music fade state. |
| `Mix_FadeInMusic(music, loops, ms)` | Fades music in. |
| `Mix_FadeOutMusic(ms)` | Fades music out. |
| `Mix_FadeInChannel(channel, chunk, loops, ms)` | Fades a channel in. |
| `Mix_FadeOutChannel(channel, ms)` | Fades a channel out. |
| `Mix_Playing(channel)` / `Mix_PlayingMusic()` | Query playing state. |
| `Mix_Paused(channel)` / `Mix_PausedMusic()` | Query paused state. |
| `Mix_SetPanning(channel, left, right)` | Sets channel panning. |
| `Mix_SetDistance(channel, distance)` | Sets channel distance. |
| `Mix_SetPosition(channel, angle, distance)` | Sets channel position. |
| `Mix_SetReverseStereo(channel, flip)` | Reverses stereo channels. |
| `Mix_FadeInMusicPos(music, loops, ms, position)` | Fades music in from a position. |

---

## Constants Reference

### SDL Initialization Flags

| Constant | Value | Description |
|----------|-------|-------------|
| `SDL_INIT_TIMER` | `0x00000001` | Timer subsystem. |
| `SDL_INIT_AUDIO` | `0x00000010` | Audio subsystem. |
| `SDL_INIT_VIDEO` | `0x00000020` | Video subsystem. |
| `SDL_INIT_JOYSTICK` | `0x00000200` | Joystick subsystem. |
| `SDL_INIT_HAPTIC` | `0x00001000` | Haptic subsystem. |
| `SDL_INIT_GAMECONTROLLER` | `0x00002000` | Game controller subsystem. |
| `SDL_INIT_EVENTS` | `0x00004000` | Events subsystem. |
| `SDL_INIT_EVERYTHING` | `0x00007231` | All subsystems combined. |
| `SDL_TRUE` / `SDL_FALSE` | `1` / `0` | Boolean constants. |

### Audio Formats

| Constant | Value | Description |
|----------|-------|-------------|
| `AUDIO_U8` | `0x0008` | Unsigned 8-bit. |
| `AUDIO_S8` | `0x8008` | Signed 8-bit. |
| `AUDIO_U16LSB` | `0x0010` | Unsigned 16-bit little-endian. |
| `AUDIO_S16LSB` | `0x8010` | Signed 16-bit little-endian. |
| `AUDIO_U16MSB` | `0x1010` | Unsigned 16-bit big-endian. |
| `AUDIO_S16MSB` | `0x9010` | Signed 16-bit big-endian. |
| `AUDIO_S32LSB` | `0x8020` | Signed 32-bit little-endian. |
| `AUDIO_S32MSB` | `0x9020` | Signed 32-bit big-endian. |
| `AUDIO_F32LSB` | `0x8120` | Float 32-bit little-endian. |
| `AUDIO_F32MSB` | `0x9120` | Float 32-bit big-endian. |
| `AUDIO_U16SYS` / `AUDIO_S16SYS` | — | System-endian 16-bit aliases. |
| `MIX_DEFAULT_FORMAT` | `AUDIO_S16SYS` | Default mixer format. |

### Mixer Initialization Flags

| Constant | Value | Description |
|----------|-------|-------------|
| `MIX_INIT_FLAC` | `0x00000001` | FLAC support. |
| `MIX_INIT_MOD` | `0x00000002` | MOD support. |
| `MIX_INIT_MP3` | `0x00000008` | MP3 support. |
| `MIX_INIT_OGG` | `0x00000010` | OGG support. |
| `MIX_INIT_MID` | `0x00000020` | MIDI support. |
| `MIX_INIT_OPUS` | `0x00000040` | Opus support. |

### Music Type Constants (Returned by `Mix_GetMusicType`)

| Constant | Value | Description |
|----------|-------|-------------|
| `MUS_NONE` | `0` | No music. |
| `MUS_CMD` | `1` | Command-based. |
| `MUS_WAV` | `2` | WAV. |
| `MUS_MOD` | `3` | MOD. |
| `MUS_MID` | `4` | MIDI. |
| `MUS_OGG` | `5` | OGG. |
| `MUS_MP3` | `6` | MP3. |
| `MUS_FLAC` | `7` | FLAC. |
| `MUS_OPUS` | `8` | Opus. |

### Fade Status Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `MUS_NO_FADING` | `0` | No fade in progress. |
| `MUS_FADING_IN` | `1` | Fading in. |
| `MUS_FADING_OUT` | `2` | Fading out. |

### Other Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `MIX_CHANNEL_POST` | `-2` | Post-mix channel. |
| `MIX_DEFAULT_CHANNELS` | `2` | Default channel count. |

---

## Unified Error Handling & Error Codes Reference

### Overview

Starting from v4.0.0, ap_ds uses a **unified error handling system**. All methods that can fail return a consistent tuple format:

```
(error_code: int, error_message: str, suggestion: str)
```

**Success:** `(AP_DS_SUCCESS, "", "")` — This indicates the operation completed successfully.

**Failure:** `(error_code, error_message, suggestion)` — The `error_code` identifies the specific issue, `error_message` provides a human-readable description, and `suggestion` offers actionable advice for resolving the problem.

### Why This Change?

In previous versions (v3.x), error handling was inconsistent:

- Some methods raised exceptions (`FileNotFoundError`, `ValueError`, `RuntimeError`)
- Some methods returned `None`
- Some methods returned `False`
- Some methods returned `0` or `0.0`
- Users had to remember different error handling patterns for different methods

This inconsistency made error handling confusing and error-prone. With v4.0.0, we've standardized everything.

### Complete Error Code Reference

| Code | Constant | Meaning | Suggestion |
|------|----------|---------|------------|
| 0 | `AP_DS_SUCCESS` | Operation completed successfully | No action needed |
| 1001 | `AP_DS_ERR_FILE_NOT_FOUND` | The specified file does not exist | Verify the file path exists and is accessible |
| 1002 | `AP_DS_ERR_INVALID_AID` | The Audio ID is invalid or expired | Check that the AID is valid and the audio is loaded |
| 1003 | `AP_DS_ERR_AUDIO_LOAD_FAILED` | Failed to load audio file | Check file format and integrity; file may be corrupted |
| 1004 | `AP_DS_ERR_PLAYBACK_FAILED` | Audio playback failed | Check audio device and file format |
| 1005 | `AP_DS_ERR_SDL_INIT_FAILED` | SDL2 initialization failed | Check SDL2 installation and audio drivers |
| 1006 | `AP_DS_ERR_MIXER_INIT_FAILED` | SDL2_mixer initialization failed | Check audio device and available formats |
| 1007 | `AP_DS_ERR_UNSUPPORTED_FORMAT` | Audio format is not supported | Use one of: MP3, WAV, FLAC, OGG (AAC for metadata only) |
| 1008 | `AP_DS_ERR_NOT_MUSIC_FILE` | Operation only works on music files | This operation (e.g., seeking) is not supported for sound effects |
| 1009 | `AP_DS_ERR_DAP_INVALID_EXT` | Invalid file extension for DAP | Use `.ap-ds-dap` extension when saving DAP recordings |
| 1010 | `AP_DS_ERR_DAP_SAVE_FAILED` | Failed to save DAP recordings | Check write permissions and disk space |
| 1011 | `AP_DS_ERR_METADATA_PARSE_FAILED` | Failed to parse audio metadata | File may be corrupted or use an unsupported variant |
| 1012 | `AP_DS_ERR_FADE_NOT_SUPPORTED` | Fade operation not supported by SDL_mixer | Update SDL_mixer or use alternative fade methods |
| 1013 | `AP_DS_ERR_AUDIO_NOT_LOADED` | Audio file is not loaded in memory | Call `new_aid()` first to load the file into cache |
| 1014 | `AP_DS_ERR_INVALID_SOURCE` | Invalid source type for metadata query | Use file path (str) or AID (int) as source |
| 1015 | `AP_DS_ERR_INVALID_VOLUME` | Volume value is out of range | Volume must be between 0 and 128 inclusive |
| 1016 | `AP_DS_ERR_SEEK_NOT_SUPPORTED` | Seeking is not supported for this audio | Sound effects (short WAVs) do not support seeking |
| 1999 | `AP_DS_ERR_UNKNOWN` | An unexpected error occurred | Check file integrity and try again; report if persistent |

### Argument Validation Error Codes

In addition to the operation-specific codes above, ap_ds performs **argument type validation** at every public entry point. Passing an invalid type returns the following codes:

| Parameter | Methods | Valid Type | Error Code |
|-----------|---------|-----------|------------|
| `file_path` | `play_from_file`, `new_aid` | `str` / `bytes` / `os.PathLike` | `1001` |
| `file_path` | `play_from_memory` | `str` / `bytes` / `os.PathLike` | `1013` |
| `loops` | `play_from_file`, `play_from_memory` | `int` | `1999` |
| `position` | `seek_audio` | `int` / `float` | `1999` |
| `volume` | `set_volume` | `int` | `1015` |
| `ms` / `loops` | `fadein_music`, `fadein_music_pos` | `int` | `1999` |
| `position` | `fadein_music_pos` | `int` / `float` | `1999` |
| `max_workers` | `batch_get_metadata` | `int` (1 .. platform max) | `1999` |

---

### How to Handle Errors

#### Example 1: Checking Return Values

```python
from ap_ds import AudioLibrary

lib = AudioLibrary()

# Method that returns int on success, tuple on failure
result = lib.play_from_file("song.mp3")

if isinstance(result, tuple):
    code, msg, suggestion = result
    print(f"Error {code}: {msg}")
    print(f"Suggestion: {suggestion}")
else:
    aid = result
    print(f"Playing with AID: {aid}")
```

#### Example 2: Using Helper Functions

```python
def is_success(result):
    """Helper to check if a result indicates success."""
    if isinstance(result, tuple):
        return result[0] == 0
    return True  # Non-tuple results are success values

def get_error(result):
    """Helper to extract error details."""
    if isinstance(result, tuple) and result[0] != 0:
        return {
            'code': result[0],
            'message': result[1],
            'suggestion': result[2]
        }
    return None
```

#### Example 3: Graceful Degradation

```python
# Try to get duration, fall back gracefully
result = lib.get_audio_duration("problematic_file.mp3", is_file=True)

if isinstance(result, tuple):
    print(f"Using fallback duration: 0 seconds")
    print(f"Error: {result[1]}")
else:
    print(f"Duration: {result} seconds")
```

### Method Return Type Summary

Every public method follows the unified **error-tuple convention**. The table below lists every method, its success return value, and the complete set of error codes it may return.

| Method | Success Return | Failure Return | Possible Error Codes |
|--------|----------------|----------------|----------------------|
| `play_from_file()` | `int` (AID) | `(code, msg, suggestion)` | 1001, 1003, 1004, 1999 |
| `play_from_memory()` | `int` (AID) | `(code, msg, suggestion)` | 1004, 1013, 1999 |
| `new_aid()` | `int` (AID) | `(code, msg, suggestion)` | 1001, 1003, 1999 |
| `play_audio()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002 |
| `pause_audio()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002 |
| `stop_audio()` | `float` (duration) | `(code, msg, suggestion)` | 1002 |
| `seek_audio()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002, 1999 |
| `set_volume()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002, 1004, 1015 |
| `get_volume()` | `int` (volume) | `(code, msg, suggestion)` | 1002 |
| `get_audio_duration()` | `int` (seconds) | `(code, msg, suggestion)` | 1001, 1002, 1011, 1999 |
| `get_audio_metadata()` | `Dict` | `(code, msg, suggestion)` | 1001, 1002, 1011, 1014 |
| `get_audio_metadata_by_path()` | `Dict` | `(code, msg, suggestion)` | 1001, 1011, 1999 |
| `get_audio_metadata_by_aid()` | `Dict` | `(code, msg, suggestion)` | 1002, 1011, 1999 |
| `save_dap_to_json()` | `(0, "", "")` | `(code, msg, suggestion)` | 1009, 1010 |
| `fadein_music()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002, 1003, 1004, 1999 |
| `fadein_music_pos()` | `(0, "", "")` | `(code, msg, suggestion)` | 1002, 1003, 1004, 1012, 1999 |
| `fadeout_music()` | `(0, "", "")` | `(code, msg, suggestion)` | 1004 |
| `batch_get_metadata()` | `List[Dict]` | `(code, msg, suggestion)` | 1999 |
| `batch_get_duration()` | `Dict[str, int]` | `(code, msg, suggestion)` | 1999 |
| `batch_get_metadata_by_type()` | `List[Dict]` | `(code, msg, suggestion)` | 1999 |
| `Delay()` | `None` | — (low-level SDL delay, no error tuple) | — |
| `_get_file_path_by_aid()` | `str` (path) | `(code, msg, suggestion)` | 1002 |
| `_get_aid_for_audio()` | `int` (AID) | `(code, msg, suggestion)` | 1002 |
| `_get_aid_for_music()` | `int` (AID) | `(code, msg, suggestion)` | 1002 |

**Error code legend** (the full meaning of every code is documented in the Error Codes Reference section):

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1001 | File not found / invalid file path type |
| 1002 | Invalid AID |
| 1003 | Audio load failed |
| 1004 | Playback failed |
| 1009 | DAP invalid extension |
| 1010 | DAP save failed |
| 1011 | Metadata parse failed |
| 1012 | Fade not supported |
| 1013 | Audio not loaded / invalid path type for memory play |
| 1014 | Invalid source type |
| 1015 | Invalid volume |
| 1999 | Unknown error / invalid argument type (loops, position, ms, max_workers) |

**Detailed error scenarios per method:**

| Method | Scenario | Error Code |
|--------|----------|------------|
| `play_from_file()` | File does not exist | 1001 |
| `play_from_file()` | `file_path` is `None` / `int` / `list` / `dict` | 1001 |
| `play_from_file()` | `loops` is a string / `None` | 1999 |
| `play_from_file()` | Music file load failed (e.g. DAP file) | 1003 |
| `play_from_file()` | SDL mixer failed to start playback | 1004 |
| `play_from_memory()` | `file_path` invalid type | 1013 |
| `play_from_memory()` | Audio not loaded in cache | 1013 |
| `play_from_memory()` | `loops` invalid type | 1999 |
| `play_from_memory()` | Cache playback failed | 1004 |
| `new_aid()` | File does not exist | 1001 |
| `new_aid()` | `file_path` invalid type | 1001 |
| `new_aid()` | Load failed | 1003 |
| `seek_audio()` | Invalid AID | 1002 |
| `seek_audio()` | `position` is `None` / string | 1999 |
| `set_volume()` | Invalid AID | 1002 |
| `set_volume()` | `volume` is not an int | 1015 |
| `set_volume()` | `volume` out of 0-128 range | 1015 |
| `set_volume()` | Mixer failed to apply | 1004 |
| `fadein_music()` | Invalid AID / not a music file | 1002 |
| `fadein_music()` | `ms` / `loops` invalid type | 1999 |
| `fadein_music()` | Music load failed | 1003 |
| `fadein_music()` | SDL mixer fade failure | 1004 |
| `fadein_music_pos()` | `Mix_FadeInMusicPos` unavailable | 1012 |
| `fadein_music_pos()` | `position` invalid type | 1999 |
| `save_dap_to_json()` | Extension is not `.ap-ds-dap` | 1009 |
| `save_dap_to_json()` | Write failure (bad path, permissions) | 1010 |
| `get_audio_duration()` | Invalid AID | 1002 |
| `get_audio_duration()` | File not found | 1001 |
| `get_audio_duration()` | Metadata parse failure | 1011 |
| `get_audio_metadata()` | Source is `float` / other non-str-int | 1014 |
| `batch_get_metadata()` | `max_workers` is 0 / -1 / 1.5 / "2" / >61 | 1999 |

## Environment Variables Reference

### 1. AP_DS_HIDE_SUPPORT_PROMPT

**Purpose:** Controls whether the startup banner is displayed when importing the library.

**Default:** Not set (banner displayed)

**Behavior:**

- When set to `1`, the startup message is completely suppressed.
- Useful for GUI applications, daemons, or any environment where console output should be minimized.

**Usage:**

```bash
# Linux/macOS
export AP_DS_HIDE_SUPPORT_PROMPT=1

# Windows Command Prompt
set AP_DS_HIDE_SUPPORT_PROMPT=1

# Windows PowerShell
$env:AP_DS_HIDE_SUPPORT_PROMPT=1
```

**Code Example:**

```python
import os
os.environ['AP_DS_HIDE_SUPPORT_PROMPT'] = '1'
import ap_ds  # No banner output
```

### 2. AP_DS_WAV_THRESHOLD

**Purpose:** Determines whether a WAV file is played as a sound effect (non-seekable) or as a music file (seekable, with fade support).

**Default:** `6` seconds

**Behavior:**

- Files with duration **less than** the threshold: treated as sound effects (using `Mix_PlayChannel`). **Seek operations are not supported.**
- Files with duration **greater than or equal to** the threshold: treated as music (using `Mix_PlayMusic`). Full seek and fade operations are supported.
- If the threshold is set to `30` or higher, it is automatically reset to `6` to prevent potential memory issues.
- Negative values are also reset to `6`.
- Invalid (non-numeric) values fall back to the default.

**Usage:**

```bash
# Set threshold to 10 seconds
export AP_DS_WAV_THRESHOLD=10

# Use a very low threshold (all WAVs become sound effects)
export AP_DS_WAV_THRESHOLD=0

# Use a high threshold (only very long WAVs become music)
export AP_DS_WAV_THRESHOLD=20
```

### 3. AP_DS_SUPPRESS_WARNINGS

**Purpose:** Suppresses runtime downgrade warnings (e.g., GIL-enabled warnings).

**Default:** Not set (warnings displayed)

**Behavior:**

- When set to `1`, all runtime downgrade warnings are completely suppressed.
- Useful for production environments or scenarios where you don't want to see warning output.

**Usage:**

```bash
export AP_DS_SUPPRESS_WARNINGS=1
```

### 4. AP_DS_SHOW_CONGRATS

**Purpose:** Controls whether the congratulations message for full-performance mode is displayed.

**Default:** Not set (congratulations displayed)

**Behavior:**

- When set to `0`, hides the congratulations message for full-performance mode (GIL disabled).
- Useful for quiet mode or headless environments.

**Usage:**

```bash
export AP_DS_SHOW_CONGRATS=0
```

### 5. AP_DS_SKIP_AUTO_CHECK

**Purpose:** Controls whether the automatic runtime self-check on import is skipped.

**Default:** `1` (self-check skipped)

**Behavior:**

- When set to `0`, executes the runtime self-check on import.
- Useful for debugging or when you want to see environment diagnostics.

**Usage:**

```bash
# Enable self-check
export AP_DS_SKIP_AUTO_CHECK=0

# Disable self-check (default)
export AP_DS_SKIP_AUTO_CHECK=1
```

## Technical Manual (show_tech_manual())

AP_DS 4.0.0 includes a built-in technical manual that can be displayed by calling `show_tech_manual()`. This manual contains:

- Overview of the library
- Supported audio formats
- Core components (AudioLibrary, Metadata Parsers, SDL2 Loader)
- DAP System documentation
- WAV Smart Mode documentation
- Environment variables reference
- Performance optimization tips
- Cross-platform notes
- Troubleshooting guide
- API reference
- Version history
- Contributing & support information

To view the manual:

```python
from ap_ds import show_tech_manual
show_tech_manual()
```

The manual is printed to stdout and contains comprehensive technical information about the library.

The manual (generated by `show_tech_manual()`) consists of **12 comprehensive sections**, each covering a distinct aspect of the library:

#### 1. Overview

- Library goals and positioning: a **lightweight (2.5 MB), zero-dependency** Python audio library built on SDL2 / SDL2_mixer.
- Architecture summary: `player.py` (AudioLibrary) + `_sdl2.py` (SDL2 loader, constants, bindings) + `audio_parser.py` (pure-Python metadata) + `__init__.py` (package entry).
- Feature highlights: batch parsing, DAP recording, smart WAV mode, fade controls, free-threading support.

#### 2. Supported Audio Formats

- **MP3**: frame-by-frame scanning, >98% accuracy.
- **WAV**: RIFF chunk parsing, 100% accuracy.
- **FLAC**: STREAMINFO metadata block, 100% accuracy.
- **OGG Vorbis**: granule position, 99.99% accuracy.
- **AAC (ADTS)**: frame parsing, >99% accuracy (metadata only; not playable).

#### 3. Core Components

- **AudioLibrary** (`player.py`): playback (`play_from_file`, `play_from_memory`, `new_aid`), control (`pause`, `play`, `stop`, `seek`), volume, fade, and the DAP system.
- **Metadata Parsers** (`audio_parser.py`): `get_audio_duration`, `get_audio_metadata`, and the batch APIs (`batch_get_metadata`, `batch_get_duration`, `batch_get_metadata_by_type`).
- **SDL2 Loader** (`_sdl2.py`): cross-platform library discovery, automatic download with SHA-256 verification, constants, structures, and ctypes bindings.

#### 4. DAP (Dvs Audio Playlist) System

- Automatically records every file that is played or loaded through `AudioLibrary`.
- **O(1) set-based deduplication** with an O(n) linear-scan fallback.
- Persists records to `.ap-ds-dap` JSON files via `save_dap_to_json()`.
- Stores metadata only (path, duration, bitrate, channels) — no audio data.

#### 5. WAV Smart Mode

- Files shorter than `AP_DS_WAV_THRESHOLD` (default **6 seconds**) → **sound-effect mode** (`Mix_PlayChannel`, in-memory `Mix_Chunk`).
- Files at or above the threshold → **music mode** (`Mix_PlayMusic`, streaming, full seek / fade support).
- The threshold is configurable via the `AP_DS_WAV_THRESHOLD` environment variable.

#### 6. Environment Variables

- `AP_DS_WAV_THRESHOLD` — WAV mode-switching threshold (default 6, range 0–29).
- `AP_DS_SUPPRESS_WARNINGS` — suppress the GIL warning (default off).
- `AP_DS_SHOW_CONGRATS` — show the "GIL disabled" congratulations message (default on).
- `AP_DS_SKIP_AUTO_CHECK` — skip the import-time runtime self-check (default on).
- `AP_DS_HIDE_SUPPORT_PROMPT` — hide the import banner (default off).
- Full reference with defaults, accepted values, and examples.

#### 7. Performance Optimization

- **Free-threading (Python 3.15t)**: GIL-less parallelism for batch parsing.
- **Batch parsing**: `ProcessPoolExecutor`-based; tune `max_workers` (defaults to CPU count).
- **Memory management**: audio caching, `new_aid()` preloading, `clear_memory_cache()` for long-running apps.
- Short WAVs cached as `Mix_Chunk`; long files streamed via `Mix_Music`.

#### 8. Cross-Platform Notes

- **Windows**: SDL2 DLLs auto-downloaded from CDN with SHA-256 verification; `os.add_dll_directory()` and PATH handling.
- **macOS**: SDL2 frameworks auto-downloaded as DMG and extracted.
- **Linux**: no automatic download; package-manager installation (apt / dnf / pacman) or manual `.so` paths; `LD_LIBRARY_PATH` handling.

#### 9. Troubleshooting Guide

- "Failed to load music file" → verify the file exists, is readable, and the format is supported.
- "SDL initialization failed" → check SDL2 binaries, antivirus on Windows, or install SDL2 dev packages on Linux.
- "GIL is enabled" warning → upgrade to Python 3.15t, or suppress with `AP_DS_SUPPRESS_WARNINGS=1`.
- DAP save failures → use the `.ap-ds-dap` extension and verify write permissions.

#### 10. API Reference

- `AudioLibrary` — full method reference with signatures, return types, and error codes (see the *Method Return Type Summary*).
- Top-level convenience functions — `batch_get_metadata`, `get_audio_duration`, `get_audio_metadata`, `show_tech_manual`, `is_full_performance`, `get_runtime_info`.
- `audio_parser` module API — parser classes, `open_audio`, `StreamInfo`.
- SDL2 integration layer — constants, structures, and all function bindings.

#### 11. Version History

- Changelog from v1.0.0 through v4.0.0, including the architecture refactoring and the **error-handling revolution**.

#### 12. Contributing & Support

- Official home: [apds.top](https://apds.top).
- Mirrors: GitCode (primary), GitHub (compatibility), Gitee (China).
- License: MIT.
- Author: DVS — me@dvsyun.top.

**How to view the manual:**

```python
from ap_ds import show_tech_manual

show_tech_manual()   # prints the full manual to stdout
```

**End of API Reference**

This document covers all public and internal APIs for the ap_ds library version v4.0.0. For additional examples and usage patterns, please refer to the main README.md or visit [https://apds.top](https://apds.top).

## Version History

### v4.0.0 (August 2026) – Architecture Refactoring & Performance Edition

**This is a complete rewrite of the core architecture** following the v3.1.x bug reports.

#### 🔧 Architecture Changes

**1. Complete Module Restructuring**

| Before (v3.1.x) | After (v4.0.0) |
|-----------------|----------------|
| `audio_parser.py` (wrapper) + `audio_info.py` (actual parsers) → **circular imports everywhere** | **Merged everything into `audio_parser.py`** – one file, no circular dependencies |
| `player.py` contained SDL2 loader + constants + bindings + AudioLibrary → **2000+ lines of chaos** | **Split into `player.py` (AudioLibrary only) + `_sdl2.py` (loader + constants + bindings)** |
| Import order mattered – changing imports could break things randomly | **Guaranteed import safety** – no more "import roulette" |

**2. Bug Fixes**

- Fixed circular import bug causing `get_audio_duration()` and `get_audio_metadata()` to return `0` or `None` in specific environments
- Fixed inconsistent import behavior across Python versions
- Eliminated redundant wrapper layer that caused confusion

**3. Unified Error Handling (New!)**

- All methods now return consistent error tuples: `(error_code, error_message, suggestion)`
- Error codes for every failure type (see Error Codes Reference section)
- Actionable suggestions included with every error
- No more guessing what went wrong – code, message, and advice are all provided

**4. Code Quality Improvements**

- Clean separation of concerns: each module has a single responsibility
- No circular dependencies
- Easier to debug and maintain
- All internal APIs properly documented

**5. Backward Compatibility**

- **Top-level APIs are unchanged** – `AudioLibrary`, `batch_get_metadata()`, `get_audio_duration()`, etc. work exactly as before
- Only internal submodule imports (`ap_ds.audio_info`, `ap_ds.player._sdl2`) are affected
- No breaking changes for normal usage

**6. Windows BrokenProcessPool Protection (New!)**

- Added explicit warning about Windows `ProcessPoolExecutor` entry point requirements
- All batch parsing methods now include documentation about `if __name__ == "__main__"` protection
- Example code in docs includes proper entry point guards

**7. GitHub Compatibility Mirror (New!)**

- New GitHub repository at `dvs-dvsxt/ap_ds` for GitHub-centric developers
- Compatibility mirror only – primary home remains apds.top

### v3.1.2 (July 2026) – Performance & Batch Parsing Edition (LFV)

**This was a major feature release** focusing on batch parsing performance and Python 3.15t free-threading support.

#### 🚀 New Features

**1. Batch Parsing API (Brand New)**

| API | Description |
|-----|-------------|
| `batch_get_metadata()` | Batch parse audio files, returns full metadata list |
| `batch_get_duration()` | Batch get audio durations, returns `{path: duration}` |
| `batch_get_metadata_by_type()` | Filter batch parsing by format |

**2. Python 3.15t Free-Threading Support**

- Fully adapted for Python 3.15t (GIL-less) environments
- Runtime self-check: Automatically detects GIL status on import
- Full-performance users see `🎉 ap_ds: GIL disabled (free-threading mode)`

**3. DAP Deduplication Optimization**

`_add_to_dap_recordings()` upgraded from O(n) linear scan to **O(1) set-based deduplication**. O(n) fallback mechanism retained for stability.

**4. Startup Acceleration: Lazy Import**

Python 3.15+ users automatically benefit from `lazy import` – heavy modules are loaded on demand.

**5. Runtime Diagnostic Functions**

- `is_full_performance()` – Checks if running in full-performance mode
- `get_runtime_info()` – Returns diagnostic info

**6. Runtime Self-Check**

Automatically executes a runtime self-check on import (can be skipped with `AP_DS_SKIP_AUTO_CHECK=1`).

**7. New Environment Variables**

| Variable | Default | Description |
|----------|---------|-------------|
| `AP_DS_SUPPRESS_WARNINGS` | Not set | Set to `1` to suppress downgrade warnings |
| `AP_DS_SHOW_CONGRATS` | Not set | Set to `0` to hide full-performance congratulations |
| `AP_DS_SKIP_AUTO_CHECK` | `0` | Set to `1` to skip import-time self-check |

#### ⚡ Performance Improvements

**120 MP3 File Test Results:**

| Method | Time | Speedup |
|--------|------|---------|
| Serial parsing | 1.367s | 1.00x |
| 8-process parallel | **0.331s** | **4.13x** |

**Compared to Mutagen:**

| Library | Method | 120 Files Time |
|---------|--------|----------------|
| Mutagen | Single-threaded | 0.973s |
| **ap_ds v3.1.2** | **8-process parallel** | **0.331s** (2.94x faster) |

**Compared to v3.0.0:**

| Test | v3.0.0 (with GIL) | v3.1.2 (without GIL) | Conclusion |
|------|-------------------|----------------------|------------|
| 8-way 120 files | 1.285s (0.66x) ❌ | **0.331s (4.13x)** | 🚀 v3.1.2 is 3.88x faster |
| Best time | 0.789s (2 threads) | **0.331s (8 processes)** | 🚀 **v3.1.2 is 2.38x faster** |

#### ⚠️ Known Issues

- **Critical bug**: `get_audio_duration()` and `get_audio_metadata()` can return `0` or `None` in certain environments due to circular imports
- **Resolution**: Fixed in v4.0.0

### v3.0.0 LTS (March 22, 2026) – First Long-Term Support Release

**This is ap_ds's first LTS release.** After years of refinement, extensive real-world testing, and a thorough internal resource management refactor, this release is production-ready for mission-critical applications, enterprise deployments, and personal projects.

**New Features:**

- **Deterministic resource cleanup** – Replaced unreliable `__del__` finalizers with explicit exit-time handlers
- **Hash-verified downloads** – Every downloaded SDL2 library is validated against hardcoded SHA-256 hashes before use
- **Full test coverage** – Tested across all platforms with zero memory leaks
- **5-year support period** – Until March 22, 2031, with free technical support

**No breaking changes** – Fully backward compatible with v2.x.

### v2.4.2 (March 22, 2026) – Development Mistake

**This version was accidentally uploaded with a development-stage `player.py` file.** While it technically works, it may contain subtle issues and **is not recommended for use in any real project.**

> ⚠️ This version was a development mistake and is intended only for curiosity – please do not use it in production.

### v2.4.1 (March 1, 2026) – Documentation Update

Updated PyPI documentation to fully reflect v2.4.0's new features.

**Changes:**

- Updated PyPI project description
- Added detailed examples for all new fade functions
- Documented `AP_DS_HIDE_SUPPORT_PROMPT` environment variable
- Improved quick-start guide

> **Note:** This release contains no code changes – only documentation improvements.

### v2.4.0 (March 1, 2026) – Audio Effects & Engineering Improvements

**Introducing professional audio transitions and important internal engineering upgrades.**

**🎵 New Audio Control Functions:**

| Function | Description |
|----------|-------------|
| `fadein_music(aid, loops=-1, ms=0)` | Fades in music over the specified milliseconds |
| `fadein_music_pos(aid, loops=-1, ms=0, position=0.0)` | Fades in music from a specified position |
| `fadeout_music(ms=0)` | Fades out currently playing music |
| `is_music_playing()` | Checks if music is currently playing |
| `is_music_paused()` | Checks if music is paused |
| `get_music_fading()` | Gets the current fade status |

**🧠 Engineering Improvements:**

- Cleaner startup banner, controllable via `AP_DS_HIDE_SUPPORT_PROMPT`
- Centralized version management
- Robust import system (dual-layer fallback)
- Unified project URLs

**No breaking changes** – All existing code continues to work.

### v2.3.6 (February 27, 2026) – Documentation Update

Updated PyPI documentation with detailed license information and version history, added more examples.

### v2.3.5 (February 26, 2026) – Stability Optimization & Embedded Validation

**Six-dimensional test coverage:**

1. Library loading & initialization
2. Playback testing (MP3, FLAC, OGG, WAV)
3. Seek testing
4. Memory pressure & leak detection (~4MB growth)
5. Metadata parsing accuracy
6. DAP system validation

**Embedded Platform Support:**

- Orange Pi 4 Pro (Allwinner A733)
- Raspberry Pi 5 (BCM2712)

**Bug Fixes:**

- Fixed WAV files being incorrectly treated as sound effects – configurable via `AP_DS_WAV_THRESHOLD`

### v2.3.4 (February 10, 2026) – Linux Smart Import System

**Revolutionary Linux support improvements with four-layer fallback strategy:**

1. System library check
2. User configuration check
3. Automatic package manager installation (apt-get, dnf, pacman)
4. Interactive guidance

**Automatic Configuration Saving:**

- Environment variables (`AP_DS_SDL2_PATH`, `AP_DS_SDL2_MIXER_PATH`)
- Persistent configuration file (`~/.config/ap_ds/sdl_paths.conf`)

### v2.3.3 (February 9, 2026) – Critical Bug Fix & Platform Stabilization

**🚨 Critical Update:** Fixed a severe segfault that caused the library to fail on macOS and Linux.

**Root Cause:** Earlier versions only defined C function prototypes (ctypes argtypes/restype) on Windows, leading to memory access violations on other operating systems.

**Solution:** All necessary C function bindings are now defined unconditionally after loading the SDL2 libraries.

### v2.3.2 (February 9, 2026) – Linux Support Enhancement

**Expanded Linux support with interactive setup.**

**Interactive Linux Support:**

1. Use system-installed libraries
2. Specify compiled .so file path
3. Get detailed compilation instructions

### v2.3.1 (February 9, 2026) – Documentation Update

Improved README.md with better examples and explanations. Fixed minor errors in documentation examples.

### v2.3.0 (January 31, 2026) – DAP Recording System

**Introducing the DAP (Dvs Audio Playlist) system.**

**Core Features:**

- **Intelligent auto-recording:** Automatically triggered in `play_from_file()`, `play_from_memory()`
- **Lightweight design:** Metadata only, no audio data
- **Standardized file format:** `.ap_ds-dap` extension, JSON format
- **Intelligent deduplication:** Automatically avoids duplicate records for the same file

**New APIs:**

- `_add_to_dap_recordings(file_path)` – Internal use
- `save_dap_to_json(save_path)` – Save as JSON
- `get_dap_recordings()` – Get all records
- `clear_dap_recordings()` – Clear records

### v2.2.0 (January 19, 2026) – Cross-Platform Revolution

**From single-platform to cross-platform.**

**Major New Features:**

**1. Full macOS Support**

- Automatic download and installation of SDL2.framework, SDL2_mixer.framework
- Intelligent .dmg file extraction and framework loading
- Maintains extreme lightness: only 3.36MB (vs Windows 2.5MB)

**2. Enhanced Automatic Dependency Management**

- Cross-platform intelligent download strategy
- Full error handling and retry mechanism
- Local caching of dependency files

### v2.1.4 (January 18, 2026) – Stable Release

**Production-ready stable version.**

- Core stability: Extensively tested, no known critical bugs
- Extremely lightweight: Only 2.5MB complete solution
- Full documentation: Detailed technical manual and examples

### v2.1.0 (December 26, 2025) – Feature Enhancement

**Professional feature expansion.**

**New Features:**

- Metadata enhancement: More precise audio information parsing
- Playback accuracy improvements: Better time control and seeking

### v2.0.0 (November 5, 2025) – Architecture Refactor

**Introducing the modern audio management system.**

**Major Improvements:**

- **AID System:** Unified audio instance management
- **Architecture Refactor:** Modular design for improved maintainability
- **Smart Memory Management:** Automatic cleanup of unused audio resources
- **State Management:** Unified playback state tracking

### v1.0.0 (July 8, 2025) – Initial Release

**Project birth, foundational functionality.**

**Core Features:**

- Basic audio playback: MP3, WAV, FLAC, OGG formats
- Playback controls: Play, pause, stop, seek basic API
- Volume control: Real-time volume adjustment (0-100%)
- Lightweight design: ~2MB initial release

---

## License

This project is licensed under the **DVS Audio Library (ap_ds) Open Source License Version 2.0**. The full license text follows. By using, copying, modifying, or distributing this software, you accept all terms and conditions of this license.

---

# DVS Audio Library (ap_ds) Open Source License Version 2.0

**Version: 2.0**
**Effective Date: March 22, 2026**
**Applies to: ap_ds version 2.4.1 and above (except for subsequent license updates)**
**Project Homepage: https://apds.top**

---

## 1. Definitions

1.1. **"Software"** means the DVS Audio Library (ap_ds) project and all its components, source code, object code, and related documentation. The official name of this project is "ap_ds", and the following names are also granted as officially recognized brand identifiers:
  - AP_DS
  - Audio Library By DVS
  - DVS Audio Player
  (All of the above names are case-insensitive and are considered officially recognized brand names.)

1.2. **"Source Code"** means the human-readable form of the Software, which is the basis for modification, study, and distribution.

1.3. **"Modified Version"** means any derivative work created by modifying, supplementing, translating, or otherwise altering the Software, in whole or in part.

1.4. **"Distribute"** means making the Software or a Modified Version available to any third party by any means or medium.

1.5. **"You"** means any individual or legal entity exercising the rights granted under this License.

1.6. **"Independent Brand"** means a completely new project name, logo, and brand identity that has no confusing association with the official names of the Software (including but not limited to "ap_ds", "AP_DS", "Audio Library By DVS", "DVS Audio Player", and any variants thereof).

---

## 2. Grant of License

Subject to the terms and conditions of this License, the Author hereby grants You a perpetual, worldwide, royalty-free, non-exclusive, irrevocable right to:

2.1. **Use and Run**: Run the Software on any computer system for any lawful purpose.

2.2. **Copy and Distribute**: Make any number of copies of the Software and Distribute them.

2.3. **Study and Modify**: Study the Software's Source Code and make any modifications to meet Your needs.

2.4. **Integrate and Commercially Use**: Integrate the Software into Your products or projects, and use it in any commercial context, including but not limited to commercial product integration, cloud service deployment, selling solutions incorporating the Software, and internal corporate use.

---

## 3. Obligations and Restrictions

### 3.1. Attribution and Source Identification

Any time the Software or a Modified Version is used, Distributed, or integrated, You must:

  a) **Retain Original Copyright Notices**: Keep intact all original copyright, patent, and trademark notices in all copies of the Software.

  b) **Provide Prominent Source Attribution**: Clearly and conspicuously state the following information in the software documentation, official website, user interface, or related materials:
     ```
     Based on DVS Audio Library (ap_ds) v[version number]
     Original Author: Dvs (DvsXT)
     Project Homepage: https://apds.top
     ```

  c) **Add Notice for Modified Versions**: If You Distribute a Modified Version, in addition to the attribution above, You must add the following notice:
     ```
     This is a modified version maintained by [Your Name/Organization].
     Support: [Your Contact Information].
     This version is not the official version and is not affiliated with the original author.
     ```

### 3.2. Brand Protection

To prevent brand confusion and project fragmentation, Modified Versions must comply with the following strict rules:

  a) **Prohibition on Using Original Brand Names**: You must not name a Modified Version "ap_ds", "AP_DS", "Audio Library By DVS", "DVS Audio Player", or any variant, combination, or derivative that could cause confusion.

  b) **Requirement for Independent Brand**: Modified Versions must use a completely independent project name and establish their own independent project identity, documentation, and community.

  c) **Maintainer Responsibility Statement**: The distributor of a Modified Version must state prominently on their project homepage or in a conspicuous location:
     ```
     This project is based on DVS Audio Library (ap_ds) but has evolved independently and is fully maintained by [Your Name].
     For the original version, please visit: https://apds.top.
     The maintainer is solely responsible for any issues related to this project.
     ```

### 3.3. Quality Commitment for Modified Versions

If You Distribute a Modified Version, You must:

  a) **Clearly State the Nature of Modifications**: Clearly indicate that this is a modified version and list the key modifications and compatibility notes compared to the original version.

  b) **Provide Technical Support**: Provide a valid means of technical support contact for the Modified Version You distribute, and define the scope of support.

  c) **Not Mislead Users**: You must not imply in any way that Your Modified Version is officially endorsed, supported, or is a continuation of the original project.

### 3.4. Prohibited Uses

You must not use the Software for any illegal activities, malicious purposes, or actions that violate local laws or regulations, including but not limited to:
  a) Disrupting computer systems or network security.
  b) Distributing malware or viruses.
  c) Infringing on the intellectual property or privacy rights of others.

---

## 4. Patent Grant

4.1. **Patent License**: The Author hereby grants You a worldwide, royalty-free, non-exclusive, non-transferable patent license to make, use, sell, offer for sale, import, or otherwise transfer the Software.

4.2. **Patent Defense Termination**: If You or Your affiliates file a patent infringement lawsuit against the Author regarding the Software, all rights granted to You under this License will automatically and immediately terminate.

---

## 5. Technical Transparency and Security

5.1. **Right to Security Review**: Any user has the right to conduct a security audit of the Software's Source Code. Commercial users may engage third-party professionals for this purpose.

5.2. **Security Reporting**: Reporting discovered security issues to the original Author (me@dvsyun.top) is encouraged, and public disclosure after resolution is supported.

5.3. **No Backdoors Commitment**: The officially released version commits to containing no malicious code, backdoors, or user-data collection features without explicit user consent.

---

## 6. Disclaimer of Warranty and Limitation of Liability

6.1. **Disclaimer of Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND ABSENCE OF ERRORS.

6.2. **Limitation of Liability**: TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES (INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA LOSS, OR BUSINESS INTERRUPTION) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE.

---

## 7. License Management and Termination

7.1. **Version Control**: This License is version 2.0. Subsequent versions will be published on the project homepage. You may choose to follow the terms of this version or any later version.

7.2. **Compatibility**: This License is compatible with the MIT, BSD 3-Clause, and Apache 2.0 licenses.

7.3. **Automatic Termination**: Your rights under this License will terminate automatically if You fail to comply with its terms. However, if You cease all non-compliance and cure all violations within 30 days of receiving notice from the copyright holder, and the copyright holder has not terminated Your rights within that period, Your rights will be reinstated.

---

## 8. Governing Law and Dispute Resolution

8.1. **Governing Law**: This License shall be governed by the laws of the People's Republic of China, without regard to its conflict of law provisions.

8.2. **Dispute Resolution**: Any dispute arising out of or in connection with this License shall first be resolved through friendly negotiation. If negotiation fails, either party may submit the dispute to the competent people's court located in the project author's domicile.

---

## 9. Contact Information

9.1. **Licensing and Inquiries**:
  - Email: me@dvsyun.top or dvs6666@163.com
  - Project Homepage: https://apds.top
  - Response Time: Within 7 business days

9.2. **Technical Support**:
  - Priority should be given to submitting issues via GitCode Issues.
  - Urgent matters can be directed to the emails above.

---

**BY USING, COPYING, MODIFYING, OR DISTRIBUTING THE SOFTWARE, YOU ACCEPT ALL TERMS AND CONDITIONS OF THIS LICENSE.**

---

## SDL2 Acknowledgments

### A Sincere and Heartfelt Thank You

ap_ds would not exist without the incredible work of the **SDL2** development team. We owe them a debt of gratitude that cannot be overstated.

**To Sam Lantinga and the entire SDL development community:**

Thank you. From the bottom of our hearts, thank you.

You have built something truly extraordinary. For over two decades, SDL has been the backbone of countless games, multimedia applications, and creative projects across the world. It runs on everything – Windows, macOS, Linux, Android, iOS, consoles, and embedded devices. It is stable, efficient, and beautifully designed. It is, quite simply, one of the most important open-source projects of our time.

We are just one small library among thousands that rely on your work. But we are deeply grateful. Every time a user plays an audio file through ap_ds, it is SDL2 doing the heavy lifting – decoding, mixing, and streaming audio with low latency and rock-solid reliability. We just provide the Python wrapper. You provide the magic.

**What you have given the world:**

- A cross-platform multimedia library that actually works, without compromise
- A clean, consistent API that developers love
- Decades of maintenance, bug fixes, and performance improvements
- A welcoming community that helps newcomers and experts alike
- An open-source license that allows projects like ours to exist without legal barriers
- The freedom to build, create, and share without fear

**What we have learned from you:**

- The value of stability over chasing the latest trends
- The importance of backward compatibility
- How to write clean C code that stands the test of time
- What it means to truly support developers across every platform

We are not worthy, but we are grateful.

### Legal Compliance

This library uses the **Simple DirectMedia Layer (SDL2)** and **SDL2_mixer** libraries.

- **SDL2 Website:** https://www.libsdl.org/
- **SDL2 License:** zlib/libpng license
- **SDL2_mixer Website:** https://www.libsdl.org/projects/SDL_mixer/
- **SDL2_mixer License:** zlib/libpng license

The zlib/libpng license is a permissive free software license that allows the software to be used, modified, and distributed freely, including in commercial products, with only minimal attribution requirements.

**SDL2 License Text (zlib/libpng):**

```
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>

This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it freely,
subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
  claim that you wrote the original software. If you use this software
  in a product, an acknowledgment in the product documentation would be
  appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
  misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
```

ap_ds includes precompiled binaries of SDL2 and SDL2_mixer for Windows and macOS platforms. These binaries are distributed under the same zlib/libpng license. The source code for these libraries is available at the official SDL2 website.

For Linux platforms, ap_ds uses system-installed SDL2 libraries or provides instructions for installation via package managers.

### Thank You, SDL Team

To Sam Lantinga, Ryan C. Gordon, and everyone who has contributed to SDL over the years:

**You are the unsung heroes of the open-source world.**

While others chase billion-dollar valuations and fleeting trends, you quietly build the infrastructure that makes creativity possible. You ask for nothing in return but the satisfaction of seeing others build great things with your work.

We see you. We appreciate you. And we will never forget that everything we have built stands on your shoulders.

**With deepest respect and gratitude,**

The ap_ds Team

---

## Final Note

ap_ds is built on a simple philosophy: **focus on playback and parsing, stay lightweight, and let developers build great applications.**

We welcome feedback, bug reports, and contributions. If you have questions or concerns, please contact us through the official channels.

**Thank you for using ap_ds!**
