docs(bundle): add .probundle format spec and API documentation

This commit is contained in:
Thorsten Bus 2026-03-30 09:20:00 +02:00
parent deabfe4ffb
commit 22ebe5fd25
7 changed files with 485 additions and 15 deletions

View file

@ -18,8 +18,10 @@ All project documentation lives in `doc/`. Load only what you need.
|------|------|
| Parse/modify `.pro` song files | `doc/api/song.md` |
| Parse/modify `.proplaylist` files | `doc/api/playlist.md` |
| Parse/modify `.probundle` files | `doc/api/bundle.md` |
| Understand `.pro` binary format | `doc/formats/pp_song_spec.md` |
| Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` |
| Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` |
| Debug or troubleshoot | `doc/internal/issues.md` |
| Add new documentation | `doc/CONTRIBUTING.md` |
@ -32,10 +34,12 @@ doc/
├── CONTRIBUTING.md ← How to document new things
├── formats/ ← Binary file format specs
│ ├── pp_song_spec.md
│ └── pp_playlist_spec.md
│ ├── pp_playlist_spec.md
│ └── pp_bundle_spec.md
├── api/ ← PHP API docs (read/write/generate)
│ ├── song.md
│ └── playlist.md
│ ├── playlist.md
│ └── bundle.md
└── internal/ ← Dev notes (learnings, decisions, issues)
├── learnings.md
├── decisions.md
@ -48,6 +52,7 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files:
- **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations
- **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs
- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets
### CLI Tools

View file

@ -8,8 +8,10 @@
|------|------|
| Parse/modify `.pro` song files | [api/song.md](api/song.md) |
| Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) |
| Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) |
| Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
| Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| Add new documentation | [CONTRIBUTING.md](CONTRIBUTING.md) |
| Search by keyword | [keywords.md](keywords.md) |
@ -20,10 +22,12 @@
### File Format Specifications
- [formats/pp_song_spec.md](formats/pp_song_spec.md) — ProPresenter 7 `.pro` file format (protobuf structure, RTF handling, field reference)
- [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) — ProPresenter 7 `.proplaylist` file format (ZIP64 container, item types)
- [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) — ProPresenter 7 `.probundle` file format (ZIP container, media assets)
### PHP API Documentation
- [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files)
- [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files)
- [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media)
### Internal Reference
- [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered
@ -45,10 +49,12 @@ doc/
├── CONTRIBUTING.md ← Documentation guidelines
├── formats/ ← File format specifications
│ ├── pp_song_spec.md
│ └── pp_playlist_spec.md
│ ├── pp_playlist_spec.md
│ └── pp_bundle_spec.md
├── api/ ← PHP API documentation
│ ├── song.md
│ └── playlist.md
│ ├── playlist.md
│ └── bundle.md
└── internal/ ← Development notes (optional context)
├── learnings.md
├── decisions.md
@ -69,6 +75,11 @@ Load: doc/api/song.md
Load: doc/api/playlist.md
```
### Task: "Read/write a .probundle"
```
Load: doc/api/bundle.md
```
### Task: "Debug protobuf parsing issues"
```
Load: doc/formats/pp_song_spec.md (sections 2-5)
@ -83,6 +94,7 @@ Load: doc/formats/pp_song_spec.md (section 7: Translations)
### Task: "Fix ZIP64 issues"
```
Load: doc/formats/pp_playlist_spec.md (section 4: ZIP64 Container Format)
Load: doc/formats/pp_bundle_spec.md (section 4: ZIP64 EOCD Quirk)
Load: doc/internal/learnings.md (search: Zip64Fixer)
```
@ -94,6 +106,7 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
- **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations
- **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files
- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets
### Key Components
@ -107,6 +120,9 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
| `php/src/ProPlaylistReader.php` | Read `.proplaylist` files |
| `php/src/ProPlaylistWriter.php` | Write `.proplaylist` files |
| `php/src/ProPlaylistGenerator.php` | Generate `.proplaylist` files from scratch |
| `php/src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) |
| `php/src/ProBundleReader.php` | Read `.probundle` files |
| `php/src/ProBundleWriter.php` | Write `.probundle` files |
### CLI Tools

226
doc/api/bundle.md Normal file
View file

@ -0,0 +1,226 @@
# Bundle Parser API
> PHP module for reading, modifying, and writing ProPresenter `.probundle` files.
## Quick Reference
```php
use ProPresenter\Parser\ProBundleReader;
use ProPresenter\Parser\ProBundleWriter;
use ProPresenter\Parser\PresentationBundle;
// Read
$bundle = ProBundleReader::read('path/to/presentation.probundle');
// Access
$bundle->getName(); // Presentation name
$bundle->getSong(); // Song wrapper
$bundle->getMediaFiles(); // ['path' => bytes, ...]
// Write
ProBundleWriter::write($bundle, 'output.probundle');
```
---
## Reading Bundles
```php
use ProPresenter\Parser\ProBundleReader;
$bundle = ProBundleReader::read('path/to/presentation.probundle');
```
The reader automatically applies `Zip64Fixer` to handle ProPresenter's broken ZIP64 headers. Works with both PP7-exported bundles and library-generated bundles.
### Metadata Access
```php
$bundle->getName(); // Presentation name (from embedded Song)
$bundle->getProFilename(); // "SongName.pro" (filename inside archive)
$bundle->getMediaFileCount(); // Number of media files
```
---
## Presentation Access
```php
// Get the Song wrapper (same API as ProFileReader)
$song = $bundle->getSong();
$song->getName();
$song->getUuid();
$song->getGroups();
$song->getSlides();
$song->getArrangements();
// Get the raw protobuf Presentation
$presentation = $bundle->getPresentation();
```
The `Song` object returned by `getSong()` has the same API as songs from `ProFileReader::read()`. See [Song API](song.md) for full details.
---
## Media Files
```php
// All media files: path => raw bytes
$mediaFiles = $bundle->getMediaFiles();
foreach ($mediaFiles as $path => $bytes) {
echo "$path: " . strlen($bytes) . " bytes\n";
}
// Check if a specific media file exists
if ($bundle->hasMediaFile('/Users/me/Downloads/Media/image.png')) {
$bytes = $bundle->getMediaFile('/Users/me/Downloads/Media/image.png');
}
// Count
$bundle->getMediaFileCount(); // 0, 1, 2, ...
```
Media file paths are stored as absolute paths with a leading `/` (matching PP7 export format).
---
## Creating Bundles
Build a `PresentationBundle` from a `Song` and media files:
```php
use ProPresenter\Parser\PresentationBundle;
use ProPresenter\Parser\ProFileGenerator;
use ProPresenter\Parser\ProBundleWriter;
// Generate a song with a media slide
$song = ProFileGenerator::generate(
'My Presentation',
[
[
'name' => 'Background',
'color' => [0.2, 0.2, 0.2, 1.0],
'slides' => [
[
'media' => 'file:///Users/me/Downloads/Media/background.png',
'format' => 'png',
'label' => 'background.png',
],
],
],
],
[['name' => 'normal', 'groupNames' => ['Background']]],
);
// Read the media file
$imageBytes = file_get_contents('/Users/me/Downloads/Media/background.png');
// Create the bundle
$bundle = new PresentationBundle(
$song,
'My Presentation.pro',
['/Users/me/Downloads/Media/background.png' => $imageBytes],
);
// Write to disk
ProBundleWriter::write($bundle, 'output.probundle');
```
### Media Path Convention
Media entries use **absolute paths with a leading `/`**:
```php
$mediaFiles = [
'/Users/me/Downloads/Media/background.png' => $pngBytes,
'/Users/me/Downloads/Media/intro.mp4' => $mp4Bytes,
];
```
This matches PP7's export format. The `file:///` URL in the `.pro` protobuf maps to these paths (strip `file://` prefix).
---
## Writing Bundles
```php
use ProPresenter\Parser\ProBundleWriter;
ProBundleWriter::write($bundle, 'output.probundle');
```
The writer:
- Creates a standard ZIP archive (deflate compression)
- Writes media entries first, `.pro` file last
- Uses atomic write (temp file + rename) for safety
---
## Round-Trip Example
```php
use ProPresenter\Parser\ProBundleReader;
use ProPresenter\Parser\ProBundleWriter;
// Read
$bundle = ProBundleReader::read('input.probundle');
// Inspect
echo "Name: " . $bundle->getName() . "\n";
echo "Media: " . $bundle->getMediaFileCount() . " files\n";
// Modify the presentation
$song = $bundle->getSong();
$song->setName("Modified Presentation");
// Write back
ProBundleWriter::write($bundle, 'output.probundle');
```
---
## Error Handling
```php
try {
$bundle = ProBundleReader::read('presentation.probundle');
} catch (\InvalidArgumentException $e) {
// File not found or empty path
echo "Error: " . $e->getMessage();
} catch (\RuntimeException $e) {
// Empty file, invalid ZIP, no .pro file found, or invalid protobuf
echo "Error: " . $e->getMessage();
}
```
### Error Cases
| Condition | Exception | Message Pattern |
|-----------|-----------|-----------------|
| File not found | `InvalidArgumentException` | `Bundle file not found: ...` |
| Empty file | `RuntimeException` | `Bundle file is empty: ...` |
| Invalid ZIP | `RuntimeException` | `Failed to open bundle archive: ...` |
| No `.pro` entry | `RuntimeException` | `No .pro file found in bundle archive: ...` |
| Target dir missing (write) | `InvalidArgumentException` | `Target directory does not exist: ...` |
---
## Key Files
| File | Purpose |
|------|---------|
| `php/src/PresentationBundle.php` | Bundle wrapper (Song + media files) |
| `php/src/ProBundleReader.php` | Reads `.probundle` files (with Zip64Fixer) |
| `php/src/ProBundleWriter.php` | Writes `.probundle` files (standard ZIP) |
| `php/src/ProFileGenerator.php` | Generates `.pro` files with media support |
| `php/src/Zip64Fixer.php` | Fixes ProPresenter ZIP64 header bug |
| `ref/TestBild.probundle` | Generated reference file (PP7-verified) |
| `ref/RestBildExportFromPP.probundle` | PP7-exported reference file |
---
## See Also
- [Format Specification](../formats/pp_bundle_spec.md) -- Binary format details
- [Song API](song.md) -- `.pro` file handling (same Song object inside bundles)
- [Playlist API](playlist.md) -- `.proplaylist` file handling (similar ZIP pattern)

View file

@ -227,3 +227,4 @@ See [Format Specification](../formats/pp_playlist_spec.md) Section 4 for details
- [Format Specification](../formats/pp_playlist_spec.md) — Binary format details
- [Song API](song.md) — `.pro` file handling
- [Bundle API](bundle.md) — `.probundle` file handling (similar ZIP pattern)

View file

@ -299,3 +299,4 @@ try {
- [Format Specification](../formats/pp_song_spec.md) — Binary format details
- [Playlist API](playlist.md) — `.proplaylist` file handling
- [Bundle API](bundle.md) — `.probundle` file handling (Song objects inside bundles)

View file

@ -0,0 +1,206 @@
# ProPresenter 7 `.probundle` File Format Specification
**Version:** 1.0
**Target Audience:** AI agents, automated parsers, developers
**Proto Source:** greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT License)
---
## 1. Overview
### File Format
- **Extension:** `.probundle`
- **Container Format:** Standard ZIP archive (PKZIP 2.0+, default deflate compression)
- **Binary Format:** Protocol Buffers (Google protobuf v3) for the embedded `.pro` file
- **Top-level Message:** `rv.data.Presentation` (defined in `presentation.proto`)
- **Proto Definitions:** greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT)
- **Predecessor:** Pro6 `.pro6x` format
### Container Structure
- **Archive Type:** Standard ZIP with deflate compression (default)
- **ZIP64 EOCD Quirk:** ProPresenter 7 exports have the same 98-byte EOCD discrepancy as `.proplaylist` files
- **Entry Layout:**
- Media files at **absolute paths with leading `/`** (e.g., `/Users/me/Downloads/Media/image.png`)
- Single `.pro` file at root (filename only, no directory)
### Purpose
A `.probundle` packages a single ProPresenter presentation (`.pro` file) together with all its referenced media assets (images, videos, audio) into a single portable archive. This enables sharing presentations between machines without losing media references.
### File Validity
- **Empty files (0 bytes):** Invalid. Throw exception.
- **Archives without `.pro`:** Invalid. Throw exception.
- **Bundles without media:** Valid. A presentation with no media actions produces a ZIP containing only the `.pro` file.
---
## 2. Archive Structure
### Entry Layout
```
/Users/me/Downloads/pp-test/Media/background.png <-- Media file (absolute path with leading /)
SongName.pro <-- Protobuf-encoded presentation
```
### Entry Order
- **Media files first**, then the `.pro` file last
- ProPresenter does not enforce order, but this matches PP7 export behavior
### Media Entry Naming
- Media entries use **absolute filesystem paths with a leading `/`**
- Standard unzip tools strip the leading `/` with a warning: `warning: stripped absolute path spec from /Users/.../image.png`
- This is intentional and matches PP7 export behavior
### Compression
- **ProPresenter exports:** Standard deflate compression
- **Writer output:** Standard deflate compression (ZipArchive defaults)
- **No special attributes needed:** Standard permissions, no forced store compression
---
## 3. Protobuf Content (`.pro` File)
### Media URL Format
Media references in the `.pro` protobuf must use `file:///` absolute URLs with a `LocalRelativePath` in `URL.local`.
#### URL Structure
```protobuf
message URL {
string absolute_string = 1; // "file:///Users/me/Downloads/Media/image.png"
LocalRelativePath local = 4; // Root + relative path
Platform platform = 5; // PLATFORM_MACOS
}
```
#### LocalRelativePath
```protobuf
message LocalRelativePath {
Root root = 1; // Enum mapping to macOS directory
string path = 2; // Relative path from root directory
}
```
### Root Type Mappings
| Root Enum | Value | macOS Directory |
|-----------|-------|-----------------|
| `ROOT_BOOT_VOLUME` | 0 | `/` (fallback) |
| `ROOT_USER_HOME` | 2 | `~/` |
| `ROOT_USER_DOWNLOADS` | 4 | `~/Downloads/` |
| `ROOT_USER_DOCUMENTS` | 5 | `~/Documents/` |
| `ROOT_USER_DESKTOP` | 6 | `~/Desktop/` |
| `ROOT_USER_MUSIC` | 7 | `~/Music/` |
| `ROOT_USER_PICTURES` | 8 | `~/Pictures/` |
| `ROOT_USER_VIDEOS` | 9 | `~/Movies/` |
| `ROOT_SHOW` | 10 | ProPresenter library directory |
### Path Construction Example
For media at `file:///Users/thorsten/Downloads/pp-test/Media/background.png`:
```
URL.absolute_string = "file:///Users/thorsten/Downloads/pp-test/Media/background.png"
URL.local.root = ROOT_USER_DOWNLOADS (4)
URL.local.path = "pp-test/Media/background.png"
URL.platform = PLATFORM_MACOS
```
### Media Metadata
| Field | Expected Value | Notes |
|-------|---------------|-------|
| `Metadata.format` | Lowercase: `"png"`, `"jpg"`, `"mp4"` | PP7 uses lowercase |
| `Action.type` | `ACTION_TYPE_MEDIA` | Media action type |
| `MediaType.layer_type` | `LAYER_TYPE_FOREGROUND` | Default for slide media |
---
## 4. ZIP64 EOCD Quirk
### Issue
ProPresenter 7 exports `.probundle` files with the same ZIP64 EOCD bug as `.proplaylist` files: a 98-byte discrepancy between the ZIP64 EOCD locator offset and the actual EOCD position.
### Workaround
The reader applies `Zip64Fixer` before opening the archive. This searches backward from the end of file for the ZIP64 EOCD signature (`0x06064b50`) and corrects the offset.
### Writer Behavior
The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates clean archives that PP7 imports without issues.
---
## 5. Differences from `.proplaylist`
| Aspect | `.proplaylist` | `.probundle` |
|--------|---------------|-------------|
| **Purpose** | Playlist with multiple songs | Single presentation with media |
| **Compression** | Store only (method 0) | Deflate (default) |
| **Metadata entry** | `data` file (protobuf `rv.data.Playlist`) | None (`.pro` file IS the data) |
| **Song entries** | Multiple `.pro` files | Single `.pro` file |
| **Media paths** | Absolute minus leading `/` | Absolute **with** leading `/` |
| **ZIP64** | Always ZIP64 | Standard ZIP (PP7 exports as ZIP64) |
---
## 6. Edge Cases
### Bundles Without Media
- **Valid.** Archive contains only the `.pro` file.
- **Use case:** Sharing a lyrics-only presentation.
### Multiple Media Files
- **Valid.** Each media file gets its own ZIP entry at its absolute path.
- **Deduplication:** Same path stored once.
### Non-Image Media
- **Videos** (`.mp4`, `.mov`): Same URL format, different `Metadata.format`.
- **Audio** (`.mp3`, `.wav`): Same pattern, `MediaType.audio` field used.
### Case Sensitivity
- `.pro` file detection is case-insensitive (`.pro`, `.Pro`, `.PRO`).
- Media format strings should be **lowercase** to match PP7 behavior.
---
## 7. Reverse-Engineering Evidence
### Reference Files
- **TestBild.probundle:** Generated by this library, verified importable by PP7 with image found
- **RestBildExportFromPP.probundle:** Exported by PP7 after import, used as comparison reference
### Key Discoveries
1. **Absolute paths with leading `/`:** PP7 stores media at full absolute filesystem paths in the ZIP, including the leading `/`
2. **`URL.local` is required:** PP7 cannot find media without the `LocalRelativePath` in `URL.local`
3. **`file:///` prefix required:** `URL.absolute_string` must use the `file:///` protocol prefix
4. **Lowercase format:** PP7 uses lowercase format strings (`"png"` not `"PNG"`)
5. **Standard ZIP is fine:** PP7 imports standard deflate-compressed ZIPs without issues — the ZIP64/store/permission quirks in PP7 exports are artifacts, not requirements
6. **ZIP64 EOCD bug:** PP7 exports have the same 98-byte offset quirk as `.proplaylist` files
### What Didn't Work (Rejected Approaches)
- **Relative media paths:** PP7 cannot resolve `Media/image.png` — needs absolute paths
- **Missing `URL.local`:** PP7 shows "image not found" without `LocalRelativePath`
- **Missing `file:///`:** Plain paths like `/Users/me/image.png` are not recognized
- **Uppercase format:** `"PNG"` works but doesn't match PP7's own output
- **Forced store compression / 000 permissions:** Unnecessary hacks that don't affect import
---
## Appendix: PP7 Export vs Library Output
### PP7 Export Characteristics (informational only)
- ZIP64 format with 98-byte EOCD offset bug
- Store compression (method 0)
- File permissions set to `0000`
- These are PP7 artifacts — the library reader handles them, the writer doesn't reproduce them
### Library Output Characteristics
- Standard ZIP (PKZIP 2.0+)
- Deflate compression (ZipArchive default)
- Normal file permissions
- PP7 imports these without issues
---
**End of Specification**

View file

@ -8,9 +8,11 @@
|---------|----------|
| `.pro` | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
| `.proplaylist` | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| `.probundle` | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| protobuf | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| ZIP | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
## Song Structure
@ -26,6 +28,16 @@
| lyrics | [api/song.md](api/song.md) |
| CCLI | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 |
## Bundle Structure
| Keyword | Document |
|---------|----------|
| bundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| probundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| pro6x | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 1 |
| LocalRelativePath | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
| absolute path | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 2 |
## Playlist Structure
| Keyword | Document |
@ -55,9 +67,9 @@
| Keyword | Document |
|---------|----------|
| macro | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| media | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 |
| cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| label | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
@ -65,16 +77,19 @@
| Keyword | Document |
|---------|----------|
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
| generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
| ProFileReader | [api/song.md](api/song.md) |
| ProFileWriter | [api/song.md](api/song.md) |
| ProFileGenerator | [api/song.md](api/song.md) |
| ProPlaylistReader | [api/playlist.md](api/playlist.md) |
| ProPlaylistWriter | [api/playlist.md](api/playlist.md) |
| ProPlaylistGenerator | [api/playlist.md](api/playlist.md) |
| ProBundleReader | [api/bundle.md](api/bundle.md) |
| ProBundleWriter | [api/bundle.md](api/bundle.md) |
| PresentationBundle | [api/bundle.md](api/bundle.md) |
| Song | [api/song.md](api/song.md) |
| PlaylistArchive | [api/playlist.md](api/playlist.md) |
| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
@ -98,11 +113,11 @@
| Keyword | Document |
|---------|----------|
| error | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| exception | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| error | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
| exception | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
| empty file | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8 |
| edge case | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8, [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 9 |
| ZIP64 bug | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 4, [api/playlist.md](api/playlist.md) |
| ZIP64 bug | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 4, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 4, [api/playlist.md](api/playlist.md) |
| round-trip | [internal/learnings.md](internal/learnings.md) |
| fidelity | [internal/issues.md](internal/issues.md) |