Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.bin binary
*.png binary
*.ico binary

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Python / build outputs
__pycache__/
.venv/
*.egg-info/
.pytest_cache/
build/
dist/
# local only: third-party tools and reference repos; ackit verify --ingame logs
external/
logs/
# Arkham City artwork imported by tools/make_assets.py (Warner Bros.; kept local, bundled in your builds)
app/assets/
# game data, audio and dumps: never commit Arkham City files
*.wad
*.wem
*.bnk
*.upk
*.dmp
*.wav
*.ogg
*.mp3
*.flac
*.bin
!ackit/data/*.bin
*.acmod

16
CLAUDE.md Normal file
View File

@@ -0,0 +1,16 @@
# CLAUDE.md
Batman: Arkham City (PC) audio toolkit + editor. Read `NOTES.md` first (status, next steps, environment).
- Layout: `ackit/` library + CLI (`python -m ackit -h`), `app/` PySide6 editor, `tests/` pytest, `data/catalog/`
CSVs (+ user's original lists in `data/catalog/source/`, never edit them), `docs/`, `tools/` build scripts.
- Dev env: `.venv` (Python 3.14, `pip install -e .[app,dev]`). Run tests with `.venv/Scripts/python -m pytest -q`.
- Writing game files goes through `ackit.workspace.Workspace` only (originals store, atomic writes, conflict checks).
Speech/ambience: in place, never larger than the original WEM; music packs: any size (`docs/formats/wad.md`).
- Never use Steam "verify integrity". Keep game files original after tests (`ackit status`, `ackit revert --all`).
- Real-game checks: `ackit verify` (offline), `ackit verify --ingame` (Windows; needs Steam running and logged in,
WinDbg; ~1.5 min; reverts and checks byte-identity itself).
- Rebuild catalogs only from original game files: `python tools/build_catalog.py`.
- Shell: git at `C:\Program Files\Git\cmd` (prepend to PATH). Write Python with the Write/Edit tools; avoid
PowerShell here-strings and heredoc string surgery with backslash escapes (they produced NUL bytes before).
- The speaker loopback can contain other apps' audio (voice chat): never save raw captures.

81
NOTES.md Normal file
View File

@@ -0,0 +1,81 @@
# NOTES — Arkham City audio toolkit & editor
## Current status / Next steps (rewrite as you go)
- Mission 1 (2026-10-07): DONE. Format reversed, encoder + injector, verified in-game (no exe patch).
- Mission 2 Part 1 (2026-10-08): repository cleanup DONE — tag `pre-cleanup` holds the old state.
New layout (README "Where things are"); `ackit` refactored: portable encoder (libsndfile, no ffmpeg/vgmstream),
`Game` (Steam/Linux/Proton discovery), catalog from `data/catalog/*.csv` (user's character lists + rules),
originals store + `Workspace` (apply/revert/conflicts), mod packages, CLI. 63 tests pass; `ackit verify` PASS;
`ackit verify --ingame` PASS (E14: ncc 1.0, revert byte-identical).
- Part 2 (2026-10-08): Unheard City DONE (app/, PySide6): browser (search + group/category/character/
archive/tag/modified/favorites filters, sortable, multi-select, notes/tags/favorites), editor (original/installed/
replacement waveforms, A/B, loop, volume, file or recording, trim/gain/normalize/match loudness, apply/revert,
conflict + changed-underneath handling), mod export/import, settings. 72 tests incl. offscreen GUI tests.
Packaging: `python tools/build_app.py` -> dist/UnheardCity (self-contained, licenses); packaged exe passes
`--smoke-test` with PATH = C:\Windows only. Real e2e through the editor code path (tools/e2e_editor.py): PASS
(music heard in game ncc 1.0, reverts byte-identical, speech line apply/revert byte-identical).
- NEXT (optional): run the Linux build on a real Linux box/CI (procedure in docs/building.md, workflow in
.github/workflows/build.yml); verify >2-channel channel order in game; speech heard in-game (needs gameplay).
- OPEN: channel order for >2 channels unverified; Linux package not executed here (no WSL/Docker).
## Environment facts
- Game: Steam AppID 200260 "Batman: Arkham City GOTY", `C:\Program Files (x86)\Steam\steamapps\common\Batman Arkham
City GOTY`, exe `Binaries\Win32\BatmanAC.exe` (32-bit, SteamStub). buildid 3718300. Game language: Italian
(`Documents\WB Games\...\UserEngine.ini` is UTF-16, `Language=Ita`).
- Steam must be running and logged in for the in-game test (`steam.exe -silent`, wait for
HKCU\Software\Valve\Steam\ActiveProcess ActiveUser != 0). appmanifest AutoUpdateBehavior=0 (no updates expected).
- History: the ITA WAD in the game folder was once the user's modded copy; the clean original came from
`%USERPROFILE%\Desktop\ArkhamCityModding\Audio\49D84887_LOC_ITA\WAD\` (sha1 1238172C...). The game is clean now
(fingerprints match the old `_backup/`, deleted on 2026-10-08).
- Dev tools: `.venv` (Python 3.14.6, PySide6 6.10.3, soundfile 0.14.0 / libsndfile 1.2.2, soxr 1.1.0, numpy,
pytest, PyInstaller 6.22.3, soundcard 0.4.6). git at `C:\Program Files\Git\cmd`. WinDbg Store (path changes on
update; `ackit/verify/debugger.py: find_cdb`). No WSL distro, no Docker.
- User data (this PC): `%APPDATA%\UnheardCity\` (settings.json, stores\<id>\), cache `%LOCALAPPDATA%\UnheardCity\cache`.
- The user's own material: `%USERPROFILE%\Desktop\ArkhamCityModding` (old mod work, Ghidra project, dump);
their third-party downloads were moved from `tools/`, `repositories/` to `external/` (git-ignored).
- Ghidra project BAC: `.text` Ghidra address = runtime + 0xC00 (docs use runtime addresses).
## Where to look
- Findings, failed hypotheses, in-game experiment log E0-E14: `docs/reverse-engineering/findings.md`.
- Formats: `docs/formats/`. Decoder addresses: `docs/reverse-engineering/decoder.md`.
- Decisions (stack, deps, licenses, originals store, size rule): `docs/decisions.md`.
## Session log
- 2026-10-08: user added `my_catalog_for_ita/` (per-character lists, wemscan names) -> moved to
`data/catalog/source/ita/`; 3364 names labelled (2 wemscan name collisions label both candidates; one line had a
`.wem.wav` typo). Built catalogs for ITA/INT/FRA/DEU/ESN/SFX (identical names and counts across languages).
Assumption: speakers are the same in every language, so ITA labels apply to all.
- 2026-10-08: libsndfile's Vorbis writer crashed (C stack overflow) on a 30 s x 6 ch single write -> block writes.
- 2026-10-08: wad_fingerprint made structural (IDs + non-pack sizes) so music edits don't invalidate the catalog;
scan mode never borrows checksums from the shipped CSV (other builds / synthetic tests).
- 2026-10-08: editor async loading looked stuck: Job objects were garbage-collected before their queued result
signal arrived -> Runner keeps references until done. Qt 6.10 deprecates invalidateFilter/invalidateRowsFilter ->
SoundFilter.refilter uses begin/endFilterChange when available.
- 2026-10-08: search haystack must not contain the variation index (":2" matched "2"); IDs are matched separately
(words of >= 6 chars against the target id).
- Verification commands: `pytest -q`; `ackit verify`; `ackit verify --ingame`; `python tools/e2e_editor.py`
(needs Steam logged in); packaged: `dist/UnheardCity/UnheardCity.exe --smoke-test --report f.txt`.
- 2026-10-08: UI restyle on request: vector icon set (app/icons.py, no emoji), "Arkham" theme (app/theme.py: QSS +
palette, translucent panels over a wallpaper backdrop).
- 2026-10-08: user rejected the procedural icon/backdrop and supplied images (~/Downloads/Arkham City:
bac_logo_full.png, wallpaper.jpg). tools/make_assets.py builds git-ignored app/assets/ (icon = logo bat over a
red waveform; wallpaper copied); app/art.py, make_icon.py and the old icon files removed. Settings ->
Background toggle (background_enabled). Character filter is now a plain combobox like the others.
- 2026-10-08: theme selector and custom background image removed (one look); the wallpaper is painted only in the
filter panel (theme.Sidebar: scaled to panel height, left part shown).
- 2026-10-08: build simplified to one offline command: build_app.py runs make_assets when app/assets is missing
(--art FOLDER), and license texts live in tools/licenses/ (www.gnu.org was down; no more downloads).
- 2026-10-08: neutral dark theme (no blue), editor header icon removed, new icon: flat BAC bat with echolocation
arcs. build_app.py also regenerates app/assets when tools/make_assets.py is newer than the icon.
- 2026-10-08: background strength default 10 %; obsolete settings keys (theme, background_image) dropped on load.
Spinners (app/spinner.py): waveform overlay while loading, editor action row, status bar for global jobs.
Replacement files now decode in a background job.
- 2026-10-08: accent is now dark red (#b0242c; icons.ACCENT / theme.ACCENT, was AMBER). Icon: white rounded tile,
red waveform bars, black logo bat on top. Filter panel opens at its maximum width (320 px, can only shrink).
Sidebar veil alpha follows the background strength (at 10 % the wallpaper stays visible).
- 2026-10-09: editor renamed "Unheard City" (exe/zip UnheardCity, launcher unheard-city, user data %APPDATA%\UnheardCity).
Icon: 9 wider/taller bars (5 at <=24 px), bat 78% of the tile. Panel background #0c0c0c.
- 2026-10-09: editor panel opens fitted to its contents (MainWindow.fit_editor; refits on layout changes until
the divider is dragged). Windows installer: tools/installer (Install.cmd -> install.ps1/uninstall.ps1, per-user,
HKCU uninstall entry), copied into the build by build_app.py; install/upgrade/uninstall cycle verified.
User chose no new software (no Inno Setup).

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# Arkham City Audio Toolkit
Replace any voice line, music track or ambience in **Batman: Arkham City (PC, Steam)** with your own audio:
by name, from a file or a recording, with one-click revert. No Wwise, no game patch, no external programs.
- **Unheard City** (`app/`, [user guide](docs/user-guide.md)): the desktop editor for Windows and Linux. Browse and
search all ~11,500 sounds, filter by category and character, listen, replace or record, apply and revert.
- **`ackit`** (`ackit/`): the Python library and command-line tool behind it.
How it works in one paragraph: the game's Wwise 2011.1 decoder only knows the standard Vorbis codebooks, so ackit
encodes with libvorbis and rewrites the stream into exactly the layout the game expects (codebook IDs, packet
format, seek table, header hash and memory size reversed from the game), then writes it into the `.wad`
archives atomically, keeping the original bytes in a store for exact reverts. Details in `docs/`.
## Install
- **Editor (users), Windows:** unzip `UnheardCity-<version>-windows-amd64.zip` and double-click **Install.cmd**.
It installs for your user (no admin rights), adds a Start menu shortcut and an entry in Settings > Apps for
uninstalling. Or skip installing and run `UnheardCity.exe` straight from the unzipped folder.
- **Editor (users), Linux:** unpack the `.tar.gz` and run `UnheardCity`.
- Nothing else is needed (no Python, no Wwise, no ffmpeg). To build it yourself: [docs/building.md](docs/building.md).
- **Toolkit (developers):** Python 3.10+:
```
python -m venv .venv && .venv/Scripts/activate # Linux: source .venv/bin/activate
pip install -e .[app,dev] # app = editor, dev = tests/packaging
python -m app.main # run the editor from source
```
## Quick start (command line)
```
ackit find joker tape # search by name, category or character
ackit extract Speech_TAPE_JOKER_1_4 orig.wav # listen to the original
ackit replace Speech_TAPE_JOKER_1_4 my_line.wav --match-loudness
ackit revert Speech_TAPE_JOKER_1_4 # or: ackit revert --all
```
More: `ackit status`, `ackit list --character Joker`, `ackit export-mod my.acmod` / `ackit import-mod`,
`ackit verify` (offline self-check), `ackit game` (detected install, language, store). `ackit -h` lists all
commands. The game folder is found through Steam (Windows, Linux, Flatpak, Proton); otherwise pass `--game DIR`.
Rules worth knowing: speech/effects replacements must fit in the original's size (the tool squeezes or refuses
with the maximum length); music can be any length. Close the game before writing.
## Where things are
```
ackit/ library + CLI (formats, encoder, injector, catalog, originals store, workspace)
ackit/verify/ offline self-check, in-game test harness (Windows, WinDbg)
app/ Unheard City (PySide6)
tests/ pytest suite (synthetic game data; real-game tests run when the game is installed)
data/catalog/ sound catalogs (CSV), character lists (source/), schema in data/catalog/README.md
docs/ formats, reverse engineering, decisions, categories, user guide, building
tools/ catalog builder, packaging (build_app.py, installer/), artwork and license scripts
NOTES.md project status and working notes; CLAUDE.md: guidance for AI sessions
```
Tests: `pytest` (the real-game tests are skipped when the game is not installed).
In-game test (Windows, Steam running, WinDbg installed): `ackit verify --ingame`.

39
REPORT.md Normal file
View File

@@ -0,0 +1,39 @@
# Report — repository cleanup and the editor, now Unheard City (mission 2)
## What works (verified)
- **Repository**: clean layout (`ackit/`, `app/`, `tests/`, `data/catalog/`, `docs/`, `tools/`); old state in git
tag `pre-cleanup`. One CLI (`ackit`) and one app (`UnheardCity`).
- **Toolkit**: portable encoder (libvorbis via libsndfile; no Wwise, ffmpeg or vgmstream), WAD/pack injector,
originals store with byte-exact revert, conflict detection, mod packages, Steam/Linux/Proton game discovery.
- **Catalog**: CSVs for ITA/INT/FRA/DEU/ESN/SFX (11,467 sounds per speech language set), categories, verified
speakers from the user's lists (3,366 lines in every language), inferred characters for the rest, original
sizes and checksums. Schema in `data/catalog/README.md`.
- **Editor**: browse/search/filter, listen (A/B, loop, waveforms), replace from file or recording, trim/gain/
normalize/loudness, apply/revert/revert all, changed-underneath warnings, notes/tags/favorites, mod
export/import, settings.
- **Tests**: 75 automated tests (synthetic game, real-game read-only tests, offscreen GUI tests) pass.
`ackit verify` (offline self-check on the installed game) PASS.
- **Real game**: `ackit verify --ingame` PASS (ncc 1.0); `tools/e2e_editor.py` — replacement applied through the
editor is heard in the game (ncc 1.0, no crash), revert leaves WAD and pack index byte-identical; a speech line
applied and reverted byte-identically. Game files are original at the end.
- **Package (Windows x86-64)**: `dist/UnheardCity-2.0.0-windows-amd64.zip` (78 MB) with
`THIRD_PARTY_LICENSES.txt`; runs its smoke test and starts the GUI with `PATH` reduced to `C:\Windows`.
Contains a per-user installer (`Install.cmd`): install, upgrade and uninstall (Settings > Apps) verified.
## What is not verified / limitations
- **Linux package not executed**: no Linux machine, WSL distribution or container runtime was available. The code
is portable (no Windows-only calls outside the developer-only in-game harness), Linux/Proton discovery is unit
tested with a simulated home folder, and the build procedure + CI workflow are provided
(`docs/building.md`, `.github/workflows/build.yml`).
- Speech lines being *heard* in the game needs gameplay; verified indirectly (same decoder/WAD path as the music
test, byte-level apply/revert, boot test with modified speech WAD).
- Speech/effects replacements must fit the original's size (index lives in compressed `.upk` packages).
- Channel order for 4-6 channel sounds is unverified (tests used identical channels).
- Inputs: WAV/FLAC/OGG/OPUS/MP3/AIFF built in; M4A/AAC/WMA only with an ffmpeg installed on the system.
- Character labels outside the user's lists are inferred from names (marked as such), not verified speakers.
## Next steps
1. Run the CI workflow (or the Linux steps in `docs/building.md`) on Ubuntu 22.04 and test the tarball on a Steam
Deck/desktop with the game under Proton.
2. Extend the curated speaker lists (`data/catalog/source/<lang>/*.txt`) and rebuild with `tools/build_catalog.py`.
3. Optional: rewrite the BKPK indexes inside `.upk` packages (LZO) to lift the size limit for speech.

10
ackit/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""ackit - Batman: Arkham City (PC) audio toolkit.
Library layers (low -> high):
formats wad, pack, wem, ogg, bits, codebooks, wwvorbis, allocsize, builder, convert, known_setups
audio audio (load/convert/decode), encoder (PCM -> game WEM)
game paths, game (install discovery), catalog + classify (names/categories/characters),
inject (WAD writing), originals (store), workspace (apply/revert), modpack (share)
tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows)
"""
__version__ = "2.0.0"

3
ackit/__main__.py Normal file
View File

@@ -0,0 +1,3 @@
from .cli import main
main()

161
ackit/allocsize.py Normal file
View File

@@ -0,0 +1,161 @@
"""Model of the game's codec-setup bump-allocator usage (dwDecodeAllocSize).
Reversed from the dumped BatmanAC.exe (see docs/decoder_re.md):
WwiseVorbisParseSetupHeader (runtime 0x010B4D10) and its callees are a Tremor-lowmem
derivative; every pool allocation is rounded up to 4 bytes.
"""
from .bits import BitReader, ilog, book_maptype1_quantvals
from .codebooks import CodebookLibrary
def a4(n):
return (n + 3) & ~3
def _node_bytes(used, leafwidth):
if used < 2:
return 4
if leafwidth == 3:
leafwidth = 4
if ilog(3 * used - 6) + 1 <= leafwidth * 4:
return leafwidth // 2 if leafwidth // 2 else 1
return leafwidth
def _leaf_words(nodeb, leafwidth):
return 2 if leafwidth > nodeb else 1
def _decode_table(used, nodeb, leafw):
if nodeb == 4:
return used * 8 + 4
return a4((used * (leafw + 1) - 2) * nodeb)
def codebook_alloc(blob: bytes) -> int:
"""Pool bytes used when unpacking one compact Wwise codebook."""
r = BitReader(blob)
dims = r.get(4)
entries = r.get(14)
if r.get(1): # ordered
r.get(5)
cur = 0
while cur < entries:
cur += r.get(ilog(entries - cur))
used = entries
else:
cwll = r.get(3)
sparse = r.get(1)
used = 0
for _ in range(entries):
if not sparse or r.get(1):
r.get(cwll)
used += 1
maptype = r.get(1)
if maptype == 0:
lw = ilog(entries) // 8 + 1
nodeb = _node_bytes(used, lw)
return _decode_table(used, nodeb, _leaf_words(nodeb, lw))
r.get(32); r.get(32)
qbits = r.get(4) + 1
quantvals = book_maptype1_quantvals(entries, dims)
total1 = (qbits * dims + 8) // 8
total2 = (ilog(quantvals - 1) * dims + 8) // 8 + (qbits + 7) // 8
if total1 <= 4 and total1 <= total2:
nodeb = _node_bytes(used, total1)
return _decode_table(used, nodeb, _leaf_words(nodeb, total1))
qv = a4(quantvals) if qbits <= 8 else a4(quantvals * 2)
lw = (ilog(quantvals - 1) * dims + 8) // 8
nodeb = _node_bytes(used, lw)
return qv + _decode_table(used, nodeb, _leaf_words(nodeb, lw))
def setup_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary) -> dict:
"""Walk a stripped Wwise setup and sum pool allocations, by section."""
r = BitReader(ww_setup)
out = {}
nb = r.get(8) + 1
total = nb * 0x3C
cb = 0
for _ in range(nb):
cb += codebook_alloc(lib.books[r.get(10)])
out["codebooks"] = total + cb
nf = r.get(6) + 1
fl = nf * 0x24
for _ in range(nf):
parts = r.get(5)
classes = [r.get(4) for _ in range(parts)]
maxc = max(classes) if classes else -1
dims = []
for _ in range(maxc + 1):
dims.append(r.get(3) + 1)
sub = r.get(2)
if sub:
r.get(8)
for _ in range(1 << sub):
r.get(8)
r.get(2)
rb = r.get(4)
posts = sum(dims[c] for c in classes)
r.pos += posts * rb
fl += a4(parts) + a4(11 * (maxc + 1)) + a4(2 * (posts + 2)) + a4(posts + 2) + 2 * a4(posts)
out["floors"] = fl
floors_count = nf
nr = r.get(6) + 1
rs = nr * 0x1C
for _ in range(nr):
r.get(2); r.get(24); r.get(24); r.get(24)
ncls = r.get(6) + 1
r.get(8)
casc = []
for _ in range(ncls):
lo = r.get(3)
hi = r.get(5) if r.get(1) else 0
casc.append(hi * 8 + lo)
for c in casc:
r.pos += 8 * bin(c & 0xFF).count("1")
rs += a4(ncls) + 8 * ncls
out["residues"] = rs
nm = r.get(6) + 1
ms = nm * 0x14
for _ in range(nm):
submaps = r.get(4) + 1 if r.get(1) else 1
if r.get(1):
steps = r.get(8) + 1
b = ilog(channels - 1)
r.pos += steps * 2 * b
ms += a4(2 * steps)
r.get(2)
if submaps > 1:
r.pos += 4 * channels
ms += a4(channels)
r.pos += 24 * submaps
ms += a4(2 * submaps)
out["mappings"] = ms
nmodes = r.get(6) + 1
out["modes"] = a4(2 * nmodes)
out["total"] = sum(out.values())
out["floors_count"] = floors_count
return out
def fnv1_32(b: bytes) -> int:
"""uHashCodebook: FNV-1 32-bit over the stripped setup packet (verified on all 24 game setups)."""
h = 0x811C9DC5
for x in b:
h = (h * 0x01000193) & 0xFFFFFFFF
h ^= x
return h
def decode_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary):
"""(dwDecodeAllocSize, dwDecodeX64AllocSize) as the 2011.1 encoder writes them.
32-bit: 0x30 (setup info) + parser allocations + 4*nbooks + 8 -> exact for all 24 game setups.
x64: fitted (alloc + 8*nbooks + 40 + 52*nfloors); not read by the 32-bit game.
"""
a = setup_alloc(ww_setup, channels, lib)
nbooks = ww_setup[0] + 1
alloc = 0x30 + a["total"] + 4 * nbooks + 8
nfloors = a["floors_count"]
return alloc, alloc + 8 * nbooks + 40 + 52 * nfloors

136
ackit/audio.py Normal file
View File

@@ -0,0 +1,136 @@
"""Portable audio helpers (no external programs needed).
Audio is handled as float32 numpy arrays shaped (frames, channels).
Decoding uses libsndfile (WAV, FLAC, OGG/Vorbis/Opus, MP3, AIFF, ...); other formats (M4A, WMA, ...)
fall back to an ffmpeg found on the system, if any. Resampling uses soxr.
"""
import io
import os
import shutil
import subprocess
import numpy as np
import soundfile as sf
READABLE = (".wav", ".flac", ".ogg", ".oga", ".opus", ".mp3", ".aif", ".aiff", ".au", ".w64", ".caf")
FFMPEG_ONLY = (".m4a", ".aac", ".wma", ".mp4", ".webm", ".mkv", ".ac3")
class AudioError(Exception):
pass
def _as2d(x):
x = np.asarray(x, dtype=np.float32)
return x[:, None] if x.ndim == 1 else x
def load(path):
"""Read an audio file -> (float32 array (n, ch), rate)."""
try:
x, sr = sf.read(path, dtype="float32", always_2d=True)
return x, sr
except (sf.LibsndfileError, RuntimeError, TypeError) as e:
ff = shutil.which("ffmpeg")
if not ff:
raise AudioError("Cannot read '%s' (%s). Supported: WAV, FLAC, OGG, OPUS, MP3, AIFF. "
"Convert the file to one of these, or install ffmpeg for other formats."
% (os.path.basename(path), e))
raw = subprocess.run([ff, "-v", "error", "-i", path, "-f", "wav", "-"], capture_output=True)
if raw.returncode:
raise AudioError("ffmpeg could not read '%s': %s" % (path, raw.stderr.decode(errors="replace")[-300:]))
x, sr = sf.read(io.BytesIO(raw.stdout), dtype="float32", always_2d=True)
return x, sr
def save_wav(path, x, rate):
sf.write(path, _as2d(x), rate, subtype="PCM_16")
def resample(x, rate, new_rate):
x = _as2d(x)
if rate == new_rate or len(x) == 0:
return x
import soxr
return soxr.resample(x, rate, new_rate, quality="HQ").astype(np.float32)
def to_channels(x, channels):
"""Up/down-mix. Mono is copied to every channel; extra channels get the average."""
x = _as2d(x)
cur = x.shape[1]
if cur == channels:
return x
if channels == 1:
return x.mean(axis=1, keepdims=True)
if cur == 1:
return np.repeat(x, channels, axis=1)
out = np.empty((len(x), channels), dtype=np.float32)
keep = min(cur, channels)
out[:, :keep] = x[:, :keep]
if channels > keep:
out[:, keep:] = x.mean(axis=1, keepdims=True)
return out
def trim(x, rate, start=None, end=None):
x = _as2d(x)
a = int(round((start or 0) * rate))
b = len(x) if end is None else int(round(end * rate))
return x[max(0, a):max(a, min(len(x), b))]
def gain(x, db):
return (_as2d(x) * (10 ** (db / 20.0))).astype(np.float32)
def peak_db(x):
p = float(np.max(np.abs(x))) if len(x) else 0.0
return -120.0 if p <= 0 else 20 * np.log10(p)
def rms_db(x):
r = float(np.sqrt(np.mean(np.square(x)))) if len(x) else 0.0
return -120.0 if r <= 0 else 20 * np.log10(r)
def normalize(x, peak=-1.0):
"""Scale so the highest peak is `peak` dBFS."""
p = peak_db(x)
return _as2d(x) if p <= -119 else gain(x, peak - p)
def match_loudness(x, reference):
"""Scale x to the RMS loudness of `reference` (limited so it does not clip)."""
g = rms_db(reference) - rms_db(x)
g = min(g, -0.3 - peak_db(x))
return gain(x, g)
def trim_silence(x, threshold_db=-50.0, pad=0.02, rate=48000):
x = _as2d(x)
lvl = np.max(np.abs(x), axis=1)
idx = np.nonzero(lvl > 10 ** (threshold_db / 20.0))[0]
if len(idx) == 0:
return x
p = int(pad * rate)
return x[max(0, idx[0] - p):min(len(x), idx[-1] + p)]
def conform(x, rate, new_rate, channels):
"""Resample + channel conversion + clip guard, ready for encoding."""
y = to_channels(resample(x, rate, new_rate), channels)
return np.clip(y, -1.0, 1.0)
def decode_wem(data: bytes):
"""Decode a game WEM -> (float32 array (n, ch), rate), using the game's codebooks."""
from .convert import wem_to_ogg
from .wem import Wem
ogg, _ = wem_to_ogg(Wem.parse(data))
x, sr = sf.read(io.BytesIO(ogg), dtype="float32", always_2d=True)
return x, sr
def duration(x, rate):
return len(x) / float(rate) if rate else 0.0

67
ackit/bits.py Normal file
View File

@@ -0,0 +1,67 @@
"""LSB-first bit reader/writer (Vorbis bit packing order)."""
class BitReader:
def __init__(self, data: bytes, bitpos: int = 0):
self.data = data
self.pos = bitpos
def get(self, n: int) -> int:
v = 0
for i in range(n):
byte = self.data[(self.pos >> 3)] if (self.pos >> 3) < len(self.data) else 0
v |= ((byte >> (self.pos & 7)) & 1) << i
self.pos += 1
return v
def bits_left(self) -> int:
return len(self.data) * 8 - self.pos
class BitWriter:
def __init__(self):
self.buf = bytearray()
self.pos = 0
def put(self, n: int, v: int):
for i in range(n):
if (self.pos >> 3) >= len(self.buf):
self.buf.append(0)
if (v >> i) & 1:
self.buf[self.pos >> 3] |= 1 << (self.pos & 7)
self.pos += 1
def put_bits_from(self, r: BitReader, n: int):
"""Copy n bits from a reader (fast path in 32-bit chunks)."""
while n > 0:
k = 32 if n >= 32 else n
self.put(k, r.get(k))
n -= k
def bytes(self) -> bytes:
return bytes(self.buf)
def ilog(v: int) -> int:
r = 0
while v:
r += 1
v >>= 1
return r
def book_maptype1_quantvals(entries: int, dims: int) -> int:
bits = ilog(entries)
vals = entries >> ((bits - 1) * (dims - 1) // dims)
while True:
acc = 1
acc1 = 1
for _ in range(dims):
acc *= vals
acc1 *= vals + 1
if acc <= entries < acc1:
return vals
if acc > entries:
vals -= 1
else:
vals += 1

128
ackit/builder.py Normal file
View File

@@ -0,0 +1,128 @@
"""Build an Arkham City compatible WEM from a standard Ogg Vorbis stream.
Field rules were derived from all 2467 WEMs of the ITA WAD and are validated by
rebuilding original files byte-for-byte (scripts/validate_builder.py).
"""
import struct
from .ogg import read_packets
from .wem import Wem, Chunk
from .wwvorbis import V2W, transcode_setup, vorbis_packet_to_ww
from .convert import packet_samples, library
from .known_setups import lookup_setup
from .allocsize import fnv1_32, decode_alloc
SEEK_INTERVAL = 8192
def build_seek_table(pkt_offsets, pkt_end_granules, interval=SEEK_INTERVAL):
"""pkt_offsets: offset of each audio packet relative to the setup start (= post seek-table)."""
out = []
prev_g, prev_o = 0, 0
target = interval
for off, g in zip(pkt_offsets, pkt_end_granules):
if g >= target:
out.append((g - prev_g, off - prev_o))
prev_g, prev_o = g, off
target = g + interval
return out
def build_wem(channels, rate, bs0_exp, bs1_exp, ww_setup, ww_packets, blockflags, total_frames,
codebook_hash, decode_alloc, decode_alloc_x64, channel_mask, seek=True, seek_interval=SEEK_INTERVAL, valid_bits=0x18,
extra_chunks=()):
samples = packet_samples(blockflags, 1 << bs0_exp, 1 << bs1_exp)
cum = []
g = 0
for s in samples:
g += s
cum.append(g)
last_extra = g - total_frames
if not 0 <= last_extra < (1 << bs1_exp):
raise ValueError("inconsistent total_frames (extra=%d)" % last_extra)
setup_blob = struct.pack("<H", len(ww_setup)) + ww_setup
offs = []
pos = len(setup_blob)
audio = bytearray()
for p in ww_packets:
if len(p) > 0xFFFF:
raise ValueError("packet too large")
offs.append(pos)
audio += struct.pack("<H", len(p)) + p
pos += 2 + len(p)
seek_entries = build_seek_table(offs, cum, seek_interval) if seek else []
for dg, do in seek_entries:
if dg > 0xFFFF or do > 0xFFFF:
raise ValueError("seek delta overflow")
seek_blob = b"".join(struct.pack("<HH", a, b) for a, b in seek_entries)
data = seek_blob + setup_blob + bytes(audio)
st = len(seek_blob)
w = Wem([Chunk(b"fmt ", b"")] + list(extra_chunks) + [Chunk(b"data", data)])
w.channels = channels
w.rate = rate
w.avg_bps = (len(data) - st) * rate // total_frames
w.valid_bits = valid_bits
w.channel_mask = channel_mask
w.vorb = dict(total_frames=total_frames,
loop_start_pkt=len(setup_blob),
loop_end_pkt=len(data) - st,
loop_begin_extra=0,
loop_end_extra=last_extra,
seek_table_size=st,
vorbis_data_offset=st + len(setup_blob),
max_packet_size=max(len(p) for p in ww_packets),
last_granule_extra=last_extra,
decode_alloc=decode_alloc,
decode_alloc_x64=decode_alloc_x64,
codebook_hash=codebook_hash,
bs0_exp=bs0_exp, bs1_exp=bs1_exp)
return w.to_bytes()
DEFAULT_MASKS = {1: 0x4, 2: 0x3, 4: 0x33, 5: 0x37, 6: 0x3F}
def ogg_to_wem(ogg: bytes, total_frames=None, allow_unknown_setup=True):
"""Convert a libvorbis Ogg stream into a game-compatible WEM (bytes)."""
lib = library()
pk = read_packets(ogg)
idh = pk[0][0]
if idh[:7] != b"\x01vorbis":
raise ValueError("not Ogg Vorbis")
channels = idh[11]
rate = struct.unpack_from("<I", idh, 12)[0]
bs0_exp, bs1_exp = idh[28] & 15, idh[28] >> 4
ww_setup, info = transcode_setup(pk[2][0], V2W, channels, lib)
ww_packets, flags = [], []
last_gran = None
for p, g in pk[3:]:
if not p:
continue
wp, bf = vorbis_packet_to_ww(p, info, bs0_exp != bs1_exp)
ww_packets.append(wp)
flags.append(bf)
if g is not None:
last_gran = g
if total_frames is None:
total_frames = last_gran
hdr = setup_header_values(ww_setup, channels, allow_unknown_setup)
return build_wem(channels, rate, bs0_exp, bs1_exp, ww_setup, ww_packets, flags, total_frames,
hdr["hash"], hdr["alloc"], hdr["alloc64"],
DEFAULT_MASKS.get(channels, (1 << channels) - 1))
def setup_header_values(ww_setup: bytes, channels: int, allow_unknown_setup=True) -> dict:
"""hash/alloc values for a setup: from the game's own files when known, else computed
with the reversed formulas (identical results on all known setups)."""
known = lookup_setup(ww_setup)
h = fnv1_32(ww_setup)
alloc, alloc64 = decode_alloc(ww_setup, channels, library())
if known is not None:
if (known["hash"], known["alloc"]) != (h, alloc):
raise AssertionError("formula mismatch on known setup")
return dict(hash=known["hash"], alloc=known["alloc"], alloc64=known["alloc64"], known=True)
if not allow_unknown_setup:
raise ValueError("setup not used by any game file")
return dict(hash=h, alloc=alloc, alloc64=alloc64, known=False)

262
ackit/catalog.py Normal file
View File

@@ -0,0 +1,262 @@
"""Index of every sound in the game, with names, categories and characters.
Sources, in order:
1. data/catalog/<ARCHIVE>.csv -- shipped catalog (instant) when the WAD fingerprint matches;
2. otherwise a header scan of the WAD (a few seconds, cached in the user cache), classified with
`classify` and labelled with data/catalog/characters.csv (speaker per sound name).
Names follow the community hash list (ackit/data/hashlist.txt): an entry with one WEM is NAME
(alias NAME_1), entries with n variations are NAME_1..NAME_n (alias NAME#i); stream-pack media are
named by their ID or <Pack>_<ID>.
"""
import csv
import difflib
import json
import os
import re
import struct
from dataclasses import dataclass, asdict
from . import classify
from .game import wad_fingerprint
from .inject import pack_bnk_path
from .pack import Pack
from .paths import catalog_dir, package_data, user_dir
from .wad import Wad
FIELDS = ["archive", "target", "name", "aliases", "group", "category", "character",
"characters_inferred", "seconds", "rate", "channels", "size", "sha1", "notes"]
SCAN_VERSION = 1
@dataclass
class Sound:
archive: str
target: str
name: str
aliases: tuple = ()
group: str = "Unknown"
category: str = "Unknown"
character: str = "" # verified speaker(s), '; '-separated (curated catalog)
characters_inferred: str = "" # characters/roles mentioned by the name (hint)
seconds: float = 0.0
rate: int = 0
channels: int = 0
size: int = 0 # size of the ORIGINAL WEM (bytes) when known from the catalog
sha1: str = "" # first 16 hex digits of the original WEM's SHA-1 ('' if unknown)
notes: str = ""
@property
def key(self):
return "%s:%s" % (self.archive, self.target)
@property
def is_pack_media(self):
return "/" in self.target
@property
def limit(self):
"""Max replacement size in bytes (None = any size)."""
return None if self.is_pack_media else self.size
def names(self):
return (self.name,) + tuple(self.aliases)
# --- static data ---------------------------------------------------------------------------
_names = None
def hash_names():
global _names
if _names is None:
_names = {}
for ln in open(package_data("hashlist.txt"), encoding="latin-1"):
p = ln.rstrip("\r\n").split("\t")
if len(p) >= 2 and p[1]:
try:
_names[int(p[0], 16)] = p[1]
except ValueError:
pass
return _names
def load_characters(path=None):
"""{sound name (lower): 'Speaker' or 'A; B'} from data/catalog/characters.csv."""
path = path or os.path.join(catalog_dir(), "characters.csv")
out = {}
if os.path.exists(path):
for row in csv.DictReader(open(path, encoding="utf-8")):
out[row["name"].lower()] = row["character"]
return out
def read_csv(path):
out = []
for r in csv.DictReader(open(path, encoding="utf-8", newline="")):
out.append(Sound(archive=r["archive"], target=r["target"], name=r["name"],
aliases=tuple(a for a in r["aliases"].split("|") if a), group=r["group"],
category=r["category"], character=r["character"],
characters_inferred=r["characters_inferred"], seconds=float(r["seconds"] or 0),
rate=int(r["rate"] or 0), channels=int(r["channels"] or 0), size=int(r["size"] or 0),
sha1=r["sha1"], notes=r["notes"]))
return out
def write_csv(path, sounds):
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, FIELDS, lineterminator="\n")
w.writeheader()
for s in sounds:
d = asdict(s)
d["aliases"] = "|".join(s.aliases)
d["seconds"] = "%.3f" % s.seconds
w.writerow({k: d[k] for k in FIELDS})
# --- scanning ------------------------------------------------------------------------------
def _hdr(b):
"""(rate, channels, seconds) from the first bytes of a WEM (fmt is always the first chunk)."""
if len(b) >= 0x30 and b[:4] == b"RIFF" and b[12:16] == b"fmt ":
ch, rate = struct.unpack_from("<HI", b, 22)
frames = struct.unpack_from("<I", b, 44)[0]
return rate, ch, (frames / float(rate) if rate else 0.0)
return 0, 0, 0.0
def _walk_entry(f, offset, size):
"""Offsets/lengths of the WEMs inside one WAD entry, reading only headers."""
out = []
pos = 0
while pos + 12 <= size:
f.seek(offset + pos)
h = f.read(0x30)
if h[:4] != b"RIFF" or h[8:12] != b"WAVE":
break
ln = 8 + struct.unpack_from("<I", h, 4)[0]
out.append((pos, ln, h))
# next WEM starts at the first non-zero byte after this one (zero padding in between)
cand = pos + ln
f.seek(offset + cand)
nxt = None
while cand < size and nxt is None:
chunk = f.read(min(0x10000, size - cand))
if not chunk:
break
stripped = chunk.lstrip(b"\0")
if stripped:
nxt = cand + len(chunk) - len(stripped)
cand += len(chunk)
if nxt is None:
break
pos = nxt
return out
def scan_archive(archive, wad_path, characters=None, progress=None):
"""Header scan of one WAD -> [Sound]."""
names = hash_names()
characters = characters if characters is not None else load_characters()
wad = Wad(wad_path)
out = []
with open(wad_path, "rb") as f:
for n, e in enumerate(wad.entries):
if progress and n % 200 == 0:
progress(n, len(wad.entries))
bnk = pack_bnk_path(wad_path, e.hash)
if bnk and open(bnk, "rb").read(4) == b"BKPK":
pk = Pack(open(bnk, "rb").read())
pname = names.get(e.hash, "%08X" % e.hash)
for mid, size, off in pk.entries:
f.seek(e.offset + off)
rate, ch, secs = _hdr(f.read(0x30))
out.append(Sound(archive, "%08X/%08X" % (e.hash, mid), names.get(mid, "%s_%08X" % (pname, mid)),
seconds=secs, rate=rate, channels=ch, size=size))
out[-1].group, out[-1].category = classify.classify(out[-1].name, pack=pname)
continue
wems = _walk_entry(f, e.offset, e.size)
base = names.get(e.hash, "%08X" % e.hash)
for i, (off, ln, h) in enumerate(wems):
rate, ch, secs = _hdr(h)
if len(wems) == 1:
s = Sound(archive, "%08X" % e.hash, base, (base + "_1",))
else:
s = Sound(archive, "%08X:%d" % (e.hash, i), "%s_%d" % (base, i + 1), ("%s#%d" % (base, i + 1),))
s.seconds, s.rate, s.channels, s.size = secs, rate, ch, ln
s.group, s.category = classify.classify(s.name)
out.append(s)
for s in out:
s.characters_inferred = "; ".join(classify.infer_characters(s.name))
s.character = next((characters[a.lower()] for a in s.names() if a.lower() in characters), "")
return out
# --- catalog -------------------------------------------------------------------------------
def _shipped_index():
p = os.path.join(catalog_dir(), "index.json")
try:
return json.load(open(p))
except (OSError, ValueError):
return {}
def load_archive(game, archive, progress=None):
"""[Sound] for one archive of a game (shipped CSV, user-cache scan, or fresh scan)."""
path = game.wad(archive)
fp = wad_fingerprint(path)
shipped = _shipped_index().get(archive, {})
csv_path = os.path.join(catalog_dir(), "%s.csv" % archive)
if shipped.get("fingerprint") == fp and os.path.exists(csv_path):
return read_csv(csv_path)
cache = os.path.join(user_dir("cache"), "scan_%s_%s_v%d.csv" % (archive, fp, SCAN_VERSION))
if os.path.exists(cache):
return read_csv(cache)
sounds = scan_archive(archive, path, progress=progress)
# unknown build: no original checksums (states then rely on the originals store only)
write_csv(cache, sounds)
return sounds
class Catalog:
"""All sounds of the selected archives (default: the game's speech language + SFX)."""
def __init__(self, game, archives=None, progress=None):
self.game = game
self.archives = archives or [a for a in (game.language(), "SFX") if a in game.archives]
self.sounds = []
for a in self.archives:
self.sounds += load_archive(game, a, progress)
self.by_key = {s.key: s for s in self.sounds}
self._by_name = {}
for s in self.sounds:
for n in s.names():
self._by_name.setdefault(n.lower(), []).append(s)
def get(self, key):
return self.by_key[key]
def resolve(self, name):
"""One sound by name, alias, key ('ITA:B49CDF48:11') or file name ('X_3.wav')."""
if name in self.by_key:
return self.by_key[name]
key = os.path.splitext(os.path.basename(name))[0].lower() if re.search(r"\.\w{2,4}$", name) else name.lower()
hits = self._by_name.get(key, [])
uniq = list({id(s): s for s in hits}.values())
if len(uniq) == 1:
return uniq[0]
if len(uniq) > 1:
raise LookupError("'%s' is ambiguous; use one of: %s" % (name, ", ".join(
next((a for a in s.names() if a.lower() != key), s.key) for s in uniq)))
near = [s.name for s in self.search(key, limit=8)] or self.suggest(key)
raise LookupError("no sound named '%s'.%s" % (name, ("\n did you mean: " + ", ".join(near)) if near else ""))
def search(self, text, limit=None):
"""All words must appear in the name, category or character (case-insensitive)."""
words = [w for w in re.split(r"[\s_]+", text.lower()) if w]
res = [s for s in self.sounds
if all(w in s.name.lower() or w in s.category.lower() or w in s.character.lower() for w in words)]
res.sort(key=lambda s: (len(s.name), s.name))
return res[:limit] if limit else res
def suggest(self, text, n=8):
names = {s.name.lower(): s.name for s in self.sounds}
return [names[k] for k in difflib.get_close_matches(text.lower(), list(names), n=n, cutoff=0.6)]

107
ackit/classify.py Normal file
View File

@@ -0,0 +1,107 @@
"""Classify sounds into group / category and infer characters from Wwise names.
Scheme (documented in docs/categories.md):
group Speech | Music | Ambience | SFX | Unknown
category finer bucket derived from the name prefix (e.g. "Interview tapes", "Combat barks")
Characters: the authoritative speaker comes from the curated catalog (data/catalog/characters.csv,
built from the user's per-character lists). For other lines `infer_characters` returns the
characters/roles that the *name* mentions -- a hint, not a speaker label (conversations alternate
speakers between variations).
"""
import re
# (regex on the upper-case name, group, category) -- first match wins
_RULES = [
(r"^(CHR_RADIOSCANNER|PSO_CARRADIO|SPEECH_RADIO|SPEECH_TV|SPEECH_CAMERAS|DIA_)", "Speech", "Radio & TV"),
(r"^SPEECH_TAPE_", "Speech", "Interview tapes"),
(r"^SPEECH_INTERROGATION_", "Speech", "Interrogations"),
(r"^SPEECH_(CINEMATIC|NEW)_", "Speech", "Cinematics"),
(r"^SPEECH_BOSS_", "Speech", "Boss fights"),
(r"^SPEECH_RIDDLE(R|S)_", "Speech", "Riddler"),
(r"^SPEECH_SIDE_", "Speech", "Side missions"),
(r"^SPEECH_DLC_", "Speech", "DLC"),
(r"^SPEECH_PRED_", "Speech", "Predator chatter"),
(r"^SPEECH_(COMBAT|BULLY)_", "Speech", "Combat barks"),
(r"^SPEECH_EMOTE_", "Speech", "Efforts & emotes"),
(r"^SPEECH_REACT_", "Speech", "Reactions"),
(r"^SPEECH_(STREETS|ZONES|OUTCAST|VERTICAL)_", "Speech", "Open-world chatter"),
(r"^SPEECH_CHAR_", "Speech", "Character lines"),
(r"^SPEECH_(CH|SEQUENCE|STEEL|MUSEUM|CHURCH|COURT|UNDER|GCPD|ZP1)_", "Speech", "Story missions"),
(r"^SPEECH_(GAME|MAP|AUGMENTED|FORENSICS|TEMP)_", "Speech", "Gameplay & hints"),
(r"^NPC_", "Ambience", "Crowds"),
(r"^SPEECH_", "Speech", "Other speech"),
(r"^MUSIC_PDLC", "Music", "Music (DLC)"),
(r"^MUSIC", "Music", "Music"),
(r"^(AMB|PSA)_", "Ambience", "Ambience"),
(r"^WND_", "Ambience", "Wind"),
(r"^P?VOL_", "Ambience", "Room tone"),
(r"^SEQ_", "SFX", "Scripted sequences"),
(r"^OBJ_", "SFX", "Objects"),
(r"^WPN_", "SFX", "Weapons"),
(r"^CHR_", "SFX", "Character foley"),
(r"^GLOBAL", "SFX", "Global"),
]
_RULES = [(re.compile(p), g, c) for p, g, c in _RULES]
GROUPS = ("Speech", "Music", "Ambience", "SFX", "Unknown")
def classify(name: str, pack: str = None):
"""-> (group, category). `pack` = name of the stream pack a media belongs to, if any."""
n = name.upper()
if pack:
p = pack.upper()
if p.startswith("MUSIC"):
return "Music", "Music (DLC)" if "PDLC" in p else "Music"
for rx, g, c in _RULES:
if rx.search(n):
return g, c
return "Unknown", "Unknown"
# --- characters -------------------------------------------------------------------------------
# display names for the labels used in the user's catalog files (data/catalog/source/ita/*.txt)
USER_LABELS = {
"Alfred": "Alfred", "Azrael": "Azrael", "Bane": "Bane", "Batman": "Batman",
"Calendar": "Calendar Man", "Catwoman": "Catwoman", "Croc": "Killer Croc", "Deadshot": "Deadshot",
"Freeze": "Mr. Freeze", "Gordon": "Gordon", "Ivy": "Poison Ivy", "Joker": "Joker",
"Madhatter": "Mad Hatter", "Oracle": "Oracle", "Penguin": "Penguin", "Quinn": "Harley Quinn",
"Ras": "Ra's al Ghul", "Riddler": "Riddler", "Robin": "Robin", "Strange": "Hugo Strange",
"Talia": "Talia al Ghul", "Twoface": "Two-Face", "Unknown": "Unknown", "Vicky": "Vicki Vale",
"Zsazz": "Victor Zsasz",
}
# name tokens -> character / role (matched on whole "_"-separated tokens, digits ignored)
_TOKENS = {
"Batman": ("BATMAN", "BM"), "Catwoman": ("CATWOMAN", "CAT", "CW"), "Joker": ("JOKER",),
"Penguin": ("PENGUIN", "PENG"), "Two-Face": ("TWOFACE", "TF"), "Riddler": ("RIDDLER",),
"Mr. Freeze": ("FREEZE", "MRFREEZE", "FREEZESUIT"), "Poison Ivy": ("IVY",),
"Hugo Strange": ("STRANGE", "HUGO"), "Robin": ("ROBIN",), "Oracle": ("ORACLE",),
"Alfred": ("ALFRED", "ALF"), "Harley Quinn": ("QUINN", "HARLEY"), "Ra's al Ghul": ("RAS",),
"Talia al Ghul": ("TALIA",), "Victor Zsasz": ("ZSASZ", "ZSAZZ"), "Bane": ("BANE",),
"Deadshot": ("DEADSHOT",), "Mad Hatter": ("MADHATTER", "HATTER"), "Calendar Man": ("CALENDAR", "CM"),
"Hush": ("HUSH",), "Clayface": ("CLAYFACE", "CLAY"), "Solomon Grundy": ("GRUNDY",),
"Azrael": ("AZRAEL",), "Vicki Vale": ("VALE", "VICKI", "VICKY", "VIC"), "Gordon": ("GORDON",),
"Nightwing": ("NIGHTWING", "NW"), "Killer Croc": ("CROC",),
# roles
"Thug": ("THUG", "THUGS", "GENTHUG", "JTHUG", "JTHUGS", "PTHUG", "PTHUGS", "BULLY"),
"TYGER guard": ("TYGER",), "Ninja": ("NINJA", "ASSASSIN"), "Doctor": ("DOC", "DOCTOR"),
"Cop": ("COP", "COPS", "GCPD"), "Political prisoner": ("HOBO", "OUTCAST", "OC"),
"Hostage": ("HOST", "HOSTAGE"), "Guard": ("GUARD", "GUARDS"),
}
_TOK2CHAR = {t: c for c, toks in _TOKENS.items() for t in toks}
_STRIP_DIGITS = re.compile(r"\d+$")
def infer_characters(name: str):
"""Characters/roles mentioned by a sound name (ordered, unique). Empty if none."""
n = name.upper()
out = []
if n.startswith("SPEECH_TAPE_"):
out.append("Hugo Strange") # Strange's interview tapes
for tok in re.split(r"[_\s]+", n):
t = _STRIP_DIGITS.sub("", tok)
c = _TOK2CHAR.get(t) or _TOK2CHAR.get(tok)
if c and c not in out:
out.append(c)
return out

335
ackit/cli.py Normal file
View File

@@ -0,0 +1,335 @@
"""ackit - Batman: Arkham City audio toolkit (command line).
Everyday use, by sound name (see `ackit find`):
ackit find joker tape search names, categories, characters
ackit info Speech_TAPE_JOKER_1_4 details, size limit, modified state
ackit extract Speech_TAPE_JOKER_1_4 orig.wav save the original (or --current)
ackit replace Speech_TAPE_JOKER_1_4 line.wav encode + install a replacement
ackit revert Speech_TAPE_JOKER_1_4 put the original back (--all for everything)
ackit status what you have changed
ackit export-mod my.acmod / import-mod my.acmod share your changes
The game folder is detected from Steam (Windows, Linux, Proton); override with --game DIR.
"""
import argparse
import os
import sys
from . import audio, settings
from .game import Game, GameError, find_installs, default_game_dir
def _workspace(a, archives=None):
from .workspace import Workspace
st = settings.load()
gd = a.game or default_game_dir(st)
if not gd:
raise GameError("Arkham City was not found. Pass the install folder with --game DIR "
"(the folder that contains BmGame and Binaries).")
if a.game:
st["game_dir"] = os.path.abspath(a.game)
settings.save(st)
game = Game(gd)
if archives is None and getattr(a, "lang", None):
archives = [a.lang, "SFX"]
return Workspace(game, a.store or st.get("store_root"), archives,
full_backup=not getattr(a, "no_full_backup", False) and st.get("full_backup", True),
progress=lambda n, t: None)
def _row(s, ws=None):
mod = " *" if ws and s.key in ws.store.entries and ws.store.entries[s.key].get("applied") else ""
who = s.character or (("~" + s.characters_inferred) if s.characters_inferred else "")
return "%-46s %-4s %7.2fs %-20s %s%s" % (s.name, s.archive, s.seconds, s.category[:20], who[:28], mod)
# --- everyday ---------------------------------------------------------------------------------
def cmd_find(a):
ws = _workspace(a)
res = ws.catalog.search(" ".join(a.words))
if not res:
near = ws.catalog.suggest("_".join(a.words))
print("nothing found." + (" Did you mean: " + ", ".join(near) if near else ""))
return
for s in (res if a.all else res[:40]):
print(_row(s, ws))
if not a.all and len(res) > 40:
print("... %d more (--all)" % (len(res) - 40))
def cmd_list(a):
ws = _workspace(a)
rows = ws.catalog.sounds
if a.group:
rows = [s for s in rows if s.group.lower() == a.group.lower()]
if a.category:
rows = [s for s in rows if a.category.lower() in s.category.lower()]
if a.character:
c = a.character.lower()
rows = [s for s in rows if c in s.character.lower() or (a.inferred and c in s.characters_inferred.lower())]
if a.modified:
rows = [s for s in rows if s.key in ws.modified_keys()]
if a.csv:
from .catalog import write_csv
write_csv(a.csv, rows)
print("wrote %s (%d sounds)" % (a.csv, len(rows)))
return
for s in rows:
print(_row(s, ws))
print("%d sound(s)" % len(rows))
def cmd_info(a):
if a.name.lower().endswith(".wem") and os.path.exists(a.name):
from .wem import Wem
m = Wem.parse(open(a.name, "rb").read())
print("rate %d, channels %d, mask 0x%x" % (m.rate, m.channels, m.channel_mask))
for k, v in m.vorb.items():
print(" %-20s %s" % (k, hex(v) if k == "codebook_hash" else v))
return
ws = _workspace(a)
s = ws.sound(a.name)
for k in ("name", "key", "group", "category", "character", "characters_inferred", "seconds", "rate",
"channels"):
print("%-20s %s" % (k, getattr(s, k)))
print("%-20s %s" % ("aliases", ", ".join(s.aliases)))
lim = ws.limit(s)
print("%-20s %s" % ("max size", "any" if lim is None else "%d bytes (the original's size)" % lim))
print("%-20s %s" % ("state", ws.state(s)))
def cmd_extract(a):
ws = _workspace(a)
s = ws.sound(a.name)
data = ws.current_bytes(s) if a.current else ws.original_bytes(s)
if a.out.lower().endswith(".wem"):
open(a.out, "wb").write(data)
else:
x, sr = audio.decode_wem(data)
audio.save_wav(a.out, x, sr)
print("wrote %s (%s, %.2f s)" % (a.out, s.name, s.seconds))
def _process(a, x, sr, ws, s):
if a.trim:
start, _, end = a.trim.partition(":")
x = audio.trim(x, sr, float(start or 0), float(end) if end else None)
if a.trim_silence:
x = audio.trim_silence(x, rate=sr)
if a.match_loudness:
ref, _ = ws.original_audio(s)
x = audio.match_loudness(x, ref)
if a.normalize:
x = audio.normalize(x)
if a.gain:
x = audio.gain(x, a.gain)
return x
def cmd_replace(a):
ws = _workspace(a)
s = ws.sound(a.name)
x, sr = audio.load(a.audio)
x = _process(a, x, sr, ws, s)
secs = audio.duration(x, sr)
if not s.is_pack_media and secs > s.seconds * 1.15 + 0.25:
print("warning: your audio (%.2f s) is longer than the original (%.2f s); it will be squeezed into the "
"original's size and the game may cut it off" % (secs, s.seconds))
wem, info = ws.prepare(s, x, sr, a.quality, progress=print)
ws.apply([(s, wem, {"source": os.path.basename(a.audio)})], force=a.force, progress=print)
print("replaced %s (%d Hz, q%.1f, %d bytes). Undo: ackit revert %s" % (s.name, info["rate"], info["quality"],
len(wem), s.name))
def cmd_apply(a):
ws = _workspace(a)
changes, errors = [], []
for f in sorted(os.listdir(a.folder)):
p = os.path.join(a.folder, f)
if not os.path.isfile(p) or f.startswith("."):
continue
try:
s = ws.sound(f)
x, sr = audio.load(p)
wem, _ = ws.prepare(s, x, sr, a.quality)
changes.append((s, wem, {"source": f}))
print("ok %-40s -> %s" % (f, s.name))
except (LookupError, ValueError, audio.AudioError) as e:
errors.append(f)
print("skip %-40s %s" % (f, e))
if changes:
ws.apply(changes, force=a.force)
print("%d applied, %d skipped" % (len(changes), len(errors)))
def cmd_revert(a):
ws = _workspace(a)
if a.all:
done = ws.revert_all(progress=print)
else:
done = ws.revert([ws.sound(n) for n in a.names], progress=print)
print("reverted %d sound(s)" % len(done))
def cmd_status(a):
ws = _workspace(a)
keys = ws.modified_keys()
if not keys:
print("no sounds modified; game audio is original")
return
for k in keys:
s = ws.catalog.by_key.get(k)
st = ws.state(s) if s else "(other language)"
print("%-50s %s" % (s.name if s else k, st))
def cmd_export_mod(a):
from .modpack import export_mod
ws = _workspace(a)
keys = [ws.sound(n).key for n in a.names] if a.names else ws.modified_keys()
n = export_mod(ws, a.out, keys, a.title or os.path.splitext(os.path.basename(a.out))[0], a.author or "",
a.description or "")
print("wrote %s (%d sounds)" % (a.out, n))
def cmd_import_mod(a):
from .modpack import import_mod
ws = _workspace(a)
for im in import_mod(ws, a.file, force=a.force, progress=print):
print("%-8s %-46s %s" % (im.how, im.item.get("name"), im.note))
# --- advanced ---------------------------------------------------------------------------------
def cmd_encode(a):
from .encoder import encode
if a.like:
ws = _workspace(a)
s = ws.sound(a.like)
rate, ch = s.rate, s.channels
else:
rate, ch = a.rate, a.channels
x, sr = audio.load(a.audio)
wem, info = encode(x, sr, rate, ch, a.quality)
open(a.out, "wb").write(wem)
print("wrote %s (%d Hz, %d ch, q%.1f, %d bytes)" % (a.out, info["rate"], ch, info["quality"], len(wem)))
def cmd_inject(a):
ws = _workspace(a)
s = ws.sound(a.name)
ws.apply([(s, open(a.wem, "rb").read(), {"source": os.path.basename(a.wem)})], force=a.force)
print("injected %s" % s.name)
def cmd_decode(a):
x, sr = audio.decode_wem(open(a.wem, "rb").read())
audio.save_wav(a.out, x, sr)
print("wrote %s (%.2f s, %d Hz, %d ch)" % (a.out, len(x) / sr, sr, x.shape[1]))
def cmd_game(a):
st = settings.load()
print("installs found:", ", ".join(find_installs()) or "none")
gd = a.game or default_game_dir(st)
if gd:
g = Game(gd)
from .workspace import default_store_root
print("game folder: ", g.root)
print("archives: ", ", ".join("%s=%s" % (k, g.rel(v)) for k, v in g.archives.items()))
print("speech language: ", g.language())
print("originals store: ", a.store or st.get("store_root") or default_store_root(g))
print("settings file: ", settings.path())
def cmd_verify(a):
from .verify import selfcheck
ws = _workspace(a)
if not a.ingame:
ok = selfcheck.run(ws, log=print)
sys.exit(0 if ok else 1)
from .verify import ingame
if a.baseline:
rep = ingame.run(ws, a.target, mode="baseline", seconds=a.seconds)
elif a.installed:
rep = ingame.run(ws, a.target, mode="installed", seconds=a.seconds)
else:
s = ws.sound(a.target)
x, sr = audio.load(a.audio) if a.audio else ingame.probe_signal()
wem, _ = ws.prepare(s, x, sr)
rep = ingame.run(ws, a.target, wem=wem, mode="modded", seconds=a.seconds)
for k, v in rep.items():
print("%-24s %s" % (k, v))
sys.exit(0 if rep["result"] == "PASS" else 1)
def build_parser():
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--game", metavar="DIR", help="Arkham City install folder (remembered)")
common.add_argument("--lang", type=str.upper, help="speech language archive, e.g. ITA, INT (default: game setting)")
common.add_argument("--store", metavar="DIR", help="originals store folder (default: user data folder)")
enc = argparse.ArgumentParser(add_help=False)
enc.add_argument("--quality", default="auto", help="auto (default) or libvorbis quality 0..10")
enc.add_argument("--force", action="store_true", help="overwrite sounds changed outside this tool")
enc.add_argument("--no-full-backup", action="store_true", help="skip the one-time full WAD backup")
proc = argparse.ArgumentParser(add_help=False)
proc.add_argument("--trim", metavar="START:END", help="keep only this part (seconds), e.g. 0.5:4.2")
proc.add_argument("--trim-silence", action="store_true", help="cut leading/trailing silence")
proc.add_argument("--gain", type=float, metavar="DB", help="volume change in dB")
proc.add_argument("--normalize", action="store_true", help="peak-normalize to -1 dBFS")
proc.add_argument("--match-loudness", action="store_true", help="match the original's loudness")
p = argparse.ArgumentParser(prog="ackit", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sp = p.add_subparsers(dest="cmd", required=True, metavar="COMMAND")
def add(name, f, help, parents=(), **kw):
s = sp.add_parser(name, parents=[common] + list(parents), help=help, description=help, **kw)
s.set_defaults(f=f)
return s
s = add("find", cmd_find, "search sounds by name, category or character")
s.add_argument("words", nargs="+"); s.add_argument("--all", action="store_true")
s = add("list", cmd_list, "list sounds with filters (or export them as CSV)")
s.add_argument("--group"); s.add_argument("--category"); s.add_argument("--character")
s.add_argument("--inferred", action="store_true", help="also match characters inferred from names")
s.add_argument("--modified", action="store_true"); s.add_argument("--csv", metavar="FILE")
s = add("info", cmd_info, "show details of a sound (or of a .wem file)")
s.add_argument("name")
s = add("extract", cmd_extract, "save a sound as .wav (decoded) or .wem")
s.add_argument("name"); s.add_argument("out"); s.add_argument("--current", action="store_true",
help="the installed version instead of the original")
s = add("replace", cmd_replace, "replace a sound with an audio file (WAV/FLAC/OGG/MP3/...)", (enc, proc))
s.add_argument("name"); s.add_argument("audio")
s = add("apply", cmd_apply, "replace every sound named by the files in a folder", (enc,))
s.add_argument("folder")
s = add("revert", cmd_revert, "put original sounds back", aliases=["restore"])
s.add_argument("names", nargs="*"); s.add_argument("--all", action="store_true")
add("status", cmd_status, "list the sounds you modified")
s = add("export-mod", cmd_export_mod, "save your changes as a shareable .acmod package")
s.add_argument("out"); s.add_argument("names", nargs="*", help="default: everything you modified")
s.add_argument("--title"); s.add_argument("--author"); s.add_argument("--description")
s = add("import-mod", cmd_import_mod, "install a .acmod package", (enc,))
s.add_argument("file")
add("game", cmd_game, "show the detected game folder, archives, language and store")
s = add("verify", cmd_verify, "self-check (offline), or test in the real game with --ingame (Windows)")
s.add_argument("--ingame", action="store_true"); s.add_argument("--target", default="SFX:EDF036D6/393BC53A")
s.add_argument("--audio"); s.add_argument("--baseline", action="store_true")
s.add_argument("--installed", action="store_true"); s.add_argument("--seconds", type=float, default=40)
# advanced
s = add("encode", cmd_encode, "[advanced] audio -> .wem file", (enc,))
s.add_argument("audio"); s.add_argument("out"); s.add_argument("--like", metavar="NAME")
s.add_argument("--rate", type=int, default=28000); s.add_argument("--channels", type=int, default=1)
s = add("inject", cmd_inject, "[advanced] install a prepared .wem file", (enc,))
s.add_argument("name"); s.add_argument("wem")
s = add("decode", cmd_decode, "[advanced] .wem file -> .wav")
s.add_argument("wem"); s.add_argument("out")
return p
def main(argv=None):
a = build_parser().parse_args(argv)
try:
a.f(a)
except (GameError, LookupError, ValueError, audio.AudioError) as e:
sys.exit("error: %s" % e)
except PermissionError as e:
sys.exit("error: cannot write %s - close the game and try again" % getattr(e, "filename", "a game file"))

118
ackit/codebooks.py Normal file
View File

@@ -0,0 +1,118 @@
"""Wwise packed codebook library (ww2ogg 'packed_codebooks.bin' format).
The game's built-in table (runtime 0x0158E748, 598 entries) is byte-identical to
ww2ogg's standard packed_codebooks.bin; see docs/wem_format.md.
"""
import os
import struct
from .bits import BitReader, BitWriter, ilog, book_maptype1_quantvals
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_LIB = os.path.join(HERE, "data", "game_codebooks.bin")
def load_library(path: str = DEFAULT_LIB) -> list:
b = open(path, "rb").read()
table = struct.unpack_from("<I", b, len(b) - 4)[0]
n = (len(b) - table) // 4
offs = struct.unpack_from("<%dI" % n, b, table)
return [b[offs[i]:offs[i + 1]] for i in range(n - 1)]
def save_library(path: str, books: list):
out = bytearray()
offs = []
for bk in books:
offs.append(len(out))
out += bk
offs.append(len(out)) # table start == end of last book
out += struct.pack("<%dI" % len(offs), *offs)
open(path, "wb").write(bytes(out))
def wwise_book_to_vorbis(blob: bytes, w: BitWriter):
"""Expand a compact Wwise codebook into a standard Vorbis codebook (ww2ogg rebuild)."""
r = BitReader(blob)
w.put(24, 0x564342)
dims = r.get(4); w.put(16, dims)
entries = r.get(14); w.put(24, entries)
ordered = r.get(1); w.put(1, ordered)
if ordered:
w.put(5, r.get(5))
cur = 0
while cur < entries:
nb = ilog(entries - cur)
num = r.get(nb); w.put(nb, num)
cur += num
else:
cwll = r.get(3)
sparse = r.get(1); w.put(1, sparse)
if cwll == 0 or cwll > 5:
raise ValueError("bad codeword length length")
for _ in range(entries):
present = 1
if sparse:
present = r.get(1); w.put(1, present)
if present:
w.put(5, r.get(cwll))
lookup = r.get(1); w.put(4, lookup)
if lookup == 1:
w.put(32, r.get(32)); w.put(32, r.get(32))
vlen = r.get(4); w.put(4, vlen)
w.put(1, r.get(1))
for _ in range(book_maptype1_quantvals(entries, dims)):
w.put(vlen + 1, r.get(vlen + 1))
elif lookup != 0:
raise ValueError("bad lookup type")
return r.pos
def skip_vorbis_book(r: BitReader):
"""Advance reader past one standard Vorbis codebook; returns (dims, entries)."""
if r.get(24) != 0x564342:
raise ValueError("codebook sync lost")
dims = r.get(16)
entries = r.get(24)
if r.get(1): # ordered
r.get(5)
cur = 0
while cur < entries:
cur += r.get(ilog(entries - cur))
else:
sparse = r.get(1)
for _ in range(entries):
if not sparse or r.get(1):
r.get(5)
lookup = r.get(4)
if lookup in (1, 2):
r.get(32); r.get(32)
vlen = r.get(4); r.get(1)
n = book_maptype1_quantvals(entries, dims) if lookup == 1 else entries * dims
r.pos += n * (vlen + 1)
elif lookup != 0:
raise ValueError("bad lookup type %d" % lookup)
return dims, entries
def book_bits_key(data: bytes, start: int, end: int) -> tuple:
"""Hashable representation of a bit range [start,end)."""
n = end - start
v = int.from_bytes(data, "little") >> start
return (n, v & ((1 << n) - 1))
class CodebookLibrary:
def __init__(self, path: str = DEFAULT_LIB):
self.books = load_library(path)
self.by_vorbis = {}
self.vorbis = []
for i, blob in enumerate(self.books):
w = BitWriter()
wwise_book_to_vorbis(blob, w)
key = (w.pos, int.from_bytes(w.bytes(), "little") & ((1 << w.pos) - 1))
self.vorbis.append(key)
self.by_vorbis.setdefault(key, i)
def lookup(self, key):
return self.by_vorbis.get(key)

56
ackit/convert.py Normal file
View File

@@ -0,0 +1,56 @@
"""WEM <-> Ogg Vorbis conversion using the game's codebook library."""
from .codebooks import CodebookLibrary
from .ogg import OggWriter
from .wem import Wem
from .wwvorbis import (W2V, transcode_setup, build_id_header, build_comment_header,
ww_packet_to_vorbis)
_LIB = None
def library() -> CodebookLibrary:
global _LIB
if _LIB is None:
_LIB = CodebookLibrary()
return _LIB
def packet_samples(blockflags, bs0, bs1):
"""Samples produced by each packet (Vorbis overlap-add rule)."""
out = []
prev = None
for bf in blockflags:
cur = bs1 if bf else bs0
out.append(0 if prev is None else (prev // 4 + cur // 4))
prev = cur
return out
def wem_to_ogg(wem: Wem):
"""Returns (ogg_bytes, details dict)."""
lib = library()
v = wem.vorb
setup, info = transcode_setup(wem.setup_packet(), W2V, wem.channels, lib)
pkts, end = wem.audio_packets()
out_pkts = []
flags = []
prev_bf = 0
for i, (_, p) in enumerate(pkts):
nxt = pkts[i + 1][1][0] if i + 1 < len(pkts) else None
op, bf = ww_packet_to_vorbis(p, nxt, info, prev_bf, v["bs0_exp"] != v["bs1_exp"])
out_pkts.append(op)
flags.append(bf)
prev_bf = bf
bs0, bs1 = 1 << v["bs0_exp"], 1 << v["bs1_exp"]
samples = packet_samples(flags, bs0, bs1)
ow = OggWriter()
ow.page([build_id_header(wem.channels, wem.rate, v["bs0_exp"], v["bs1_exp"])], 0, bos=True)
ow.page([build_comment_header(), setup], 0)
g = 0
total = v["total_frames"]
for i, op in enumerate(out_pkts):
g += samples[i]
last = i == len(out_pkts) - 1
ow.page([op], min(g, total) if last else g, eos=last)
return ow.bytes(), dict(info=info, flags=flags, samples=samples, packets=pkts, data_end=end,
sum_samples=sum(samples))

Binary file not shown.

19063
ackit/data/hashlist.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
[
{
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d4410000008000000f000007051011d15c85c5054686c606478747000000c003001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
"hash": 2329567961,
"alloc": 15492,
"alloc64": 15892,
"count": 8819,
"formats": {
"28000/1ch/8-11": 8593,
"28000/5ch/8-11": 176,
"28000/4ch/8-11": 50
},
"conflict": false
},
{
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671815906b8a08bbb0e841084200495378002127070c20d4fbce109373289a6288c410000006000000f000007051011d15c85c5054686c606478747000000e002001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
"hash": 1136153196,
"alloc": 15492,
"alloc64": 15892,
"count": 2338,
"formats": {
"44100/5ch/8-11": 70,
"44100/1ch/8-11": 2242,
"44100/4ch/8-11": 8,
"44100/3ch/8-11": 18
},
"conflict": false
},
{
"setup": "16e5805702114f621065896afd16605711f468c4647a39075522fc21a90d6020426602810228309001000708095200406181a174a10b4288205d04593c70e1c48d276e38a1430b040000001c00f001007030001111cd35383c4044484c50545800000000000000",
"hash": 953446082,
"alloc": 13464,
"alloc64": 13740,
"count": 631,
"formats": {
"24000/1ch/10-10": 568,
"24000/5ch/10-10": 59,
"24000/4ch/10-10": 4
},
"conflict": false
},
{
"setup": "22870c218c290e39866353e5805702114f621065896afd166057e44d970f4a4af9925032e76c31937c35b1e45720202400c00041c10c00303840183910e8082070680d44c84c6050080d0e3201e00122422a00484c50942e74410811a48b208b072e9cb8f1c40d277468830000000002003e0000120a2022229ab90a8b0b8c0c8d0d8e0e8f0f90900000000004003e000012112022229ab90a8b0b8c0c8d0d8e0e8f0f90900000000000202020003000",
"hash": 3571053844,
"alloc": 17692,
"alloc64": 18116,
"count": 436,
"formats": {
"24000/4ch/9-10": 92,
"24000/1ch/9-10": 219,
"24000/5ch/9-10": 125
},
"conflict": false
},
{
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d4410000007000000f000007051011d15c85c5054686c6064787470000004003001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
"hash": 2903324457,
"alloc": 15492,
"alloc64": 15892,
"count": 71,
"formats": {
"32000/1ch/8-11": 71
},
"conflict": false
},
{
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095c33f4206918e66427463d11a1956955420202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b00151617691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d040000000700f00000705c001111cd6164686c7074787c8084040000003800f001007088001111cd6164686c7074787c80840400000000000101018001",
"hash": 3644545348,
"alloc": 16192,
"alloc64": 16616,
"count": 39,
"formats": {
"32000/4ch/8-11": 38,
"32000/5ch/8-11": 1
},
"conflict": false
},
{
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095c33f4206918e66427463d11a1956955420202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b001516176917179865800bbab8eb40084108425079032820010727dcf0c41b9e702393688ac218040000000600f00000705c001111cd6164686c7074787c8084040000002c00f001007088001111cd6164686c7074787c80840400000000000101018001",
"hash": 2446091980,
"alloc": 16192,
"alloc64": 16616,
"count": 15,
"formats": {
"48000/1ch/8-11": 14,
"48000/4ch/8-11": 1
},
"conflict": false
},
{
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095831c3130d181385c9472d1816328a14320202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b00151617691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d040000000800f00000705c001111cd6164686c7074787c8084040000003c00f001007088001111cd6164686c7074787c80840400000000000101018001",
"hash": 1876663221,
"alloc": 16872,
"alloc64": 17296,
"count": 12,
"formats": {
"32000/4ch/8-11": 12
},
"conflict": false
},
{
"setup": "0f3244050615634958db0f703c960d8bcfe9d74f3540800920304050300a418030020100008003007c00001c0540444473060708090a0b0c0d0e0f00000000000000",
"hash": 3867129568,
"alloc": 12084,
"alloc64": 12304,
"count": 5,
"formats": {
"6000/1ch/9-9": 5
},
"conflict": false
},
{
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671815906b8a08bbb0e841084200495378002127070c20d4fbce109373289a6288c410000006000000f000007051011d15c85c5054686c606478747000000a002001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
"hash": 3242781484,
"alloc": 15492,
"alloc64": 15892,
"count": 5,
"formats": {
"48000/1ch/8-11": 4,
"48000/5ch/8-11": 1
},
"conflict": false
},
{
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e5869d2d088b6e0f21f00210e02d372c9a514049800020304055f0801312208911922a1b00a1618944183c33c007880889008001213146917179865800bbab8eb40084108425079032820010727dcf0c41b9e702393688ac218080000000c00f000008050001111cd5c85c5054686c6064787c70788000000c005001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
"hash": 3080104147,
"alloc": 13300,
"alloc64": 13716,
"count": 5,
"formats": {
"44100/2ch/8-11": 5
},
"conflict": false
},
{
"setup": "269e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a09543905f0d164c1f2246879165c751e05d40206402810228309001000708095200406181a143840810a3c0c0b8b8b40842648648442c068909d54051311d002c2e30e4034086c646dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da252070100000002003c00001c1b404444731c1d1e1f2021222324250100000010007c00001c26404444731c1d1e1f2021222324250100000000404040006000",
"hash": 3462290664,
"alloc": 18180,
"alloc64": 18636,
"count": 5,
"formats": {
"32000/4ch/8-11": 5
},
"conflict": false
},
{
"setup": "299e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a095837f9b68309b42c8d538d406a8e0d7176c79741710089940a0000a0c6400c00142821400505860e8102102c42830302e2e2d8210992112118b4162423550544c07008b0b0cf90090a1b1917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d481000000d000000f0000c906101111cd1c4787c7074888c8084989c9098a0000002006001f0000490a101111cd1c4787c7074888c8084989c9094a20800000000820000101018001",
"hash": 73756765,
"alloc": 16080,
"alloc64": 16560,
"count": 5,
"formats": {
"48000/2ch/8-11": 5
},
"conflict": false
},
{
"setup": "12324405061563a147550072b5a6da831842219e351646821d106002080c10148c4210208c400000000001001f00004901101111cd9cc1014282c2024383c303448404000000000000",
"hash": 1050329265,
"alloc": 13788,
"alloc64": 14032,
"count": 4,
"formats": {
"12000/1ch/9-9": 4
},
"conflict": false
},
{
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e88e0955e5e6f2c894d403bfec14a95e14016049800020304055f0801312208911922a1b00a1618944183c33c00788088900800121314691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d080000000d00f000008050001111cd5c85c5054686c6064787c707880000008006001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
"hash": 2982013646,
"alloc": 15116,
"alloc64": 15532,
"count": 3,
"formats": {
"32000/2ch/8-11": 3
},
"conflict": false
},
{
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a09503319ce8b498461e9ab89483ec48c0804841fd14080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da252070200008003003c00002417404444347318191a1b1c1d1e1f2021222324020000001c007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
"hash": 3568494628,
"alloc": 14316,
"alloc64": 14764,
"count": 3,
"formats": {
"32000/2ch/8-11": 3
},
"conflict": false
},
{
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a095432339b5a2d842b8e592da44ebd145a0726f9d15080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da25207020000c003003c00002417404444347318191a1b1c1d1e1f2021222324020000001e007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
"hash": 1680511206,
"alloc": 14540,
"alloc64": 14988,
"count": 2,
"formats": {
"32000/2ch/8-11": 2
},
"conflict": false
},
{
"setup": "16e5805702114f621065896afd16605711f468c4647a39075522fc21a90d6020426602810228309001000708095200406181a174a10b4288205d04593c70e1c48d276e38a1430b040000001a00f001007030001111cd35383c4044484c50545800000000000000",
"hash": 3765588556,
"alloc": 13464,
"alloc64": 13740,
"count": 2,
"formats": {
"24000/1ch/10-10": 2
},
"conflict": false
},
{
"setup": "21870c218c290e39866353e5805702114f621065896afd166057ee31421b2db01c79d38a0d04928047770918080809003040503003000c0e10460e043a02081c5a0311321318144283834c00788088900a00121314a50b5d104204e922c8e2810b276e3c71c3091dda400000000001800f0080840208888866aec2e20223436383a3c3e303440000000002800f0080240408888866aec2e20223436383a3c3e303241040000000041080808000c000",
"hash": 2310130245,
"alloc": 13076,
"alloc64": 13492,
"count": 2,
"formats": {
"24000/2ch/9-10": 2
},
"conflict": false
},
{
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a09503319ce8b498461e9ab89483ec48c0804841fd14080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5056619e0822eee3a104210821054de000a48c0c109373cf18627dcc8249aa23006020000c002003c00002417404444347318191a1b1c1d1e1f20212223240200000016007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
"hash": 1844255845,
"alloc": 14316,
"alloc64": 14764,
"count": 2,
"formats": {
"48000/2ch/8-11": 2
},
"conflict": false
},
{
"setup": "0f3244050615634958db0f703c960d8bcfe9d74f3540800920304050300a418030020100000003007c00001c0540444473060708090a0b0c0d0e0f00000000000000",
"hash": 1823403872,
"alloc": 12084,
"alloc64": 12304,
"count": 1,
"formats": {
"11025/1ch/9-9": 1
},
"conflict": false
},
{
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e5869d2d088b6e0f21f00210e02d372c9a514049800020304055f0801312208911922a1b00a1618944183c33c00788088900800121314691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d080000000d00f000008050001111cd5c85c5054686c6064787c707880000008006001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
"hash": 2761019796,
"alloc": 13300,
"alloc64": 13716,
"count": 1,
"formats": {
"32000/2ch/8-11": 1
},
"conflict": false
},
{
"setup": "1f0d86028427660d044e695b7d849a08083147e36e1cf620dd3a78947554782d4413926bb93155087741800920304050300a4180302208911922a1b00a1618944183c33c00788088900800121314ad2e5c802e0670812edce580100441100461d1001420010e9ce0064f788327b88113e8284aea200000006000800f00808302888868aec2e20223436383a3c3230000001003800f0080e303888868aec2e20223436383a3c3230000000000080808000c00",
"hash": 1422405756,
"alloc": 17884,
"alloc64": 18284,
"count": 1,
"formats": {
"32000/5ch/9-12": 1
},
"conflict": false
},
{
"setup": "269e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a09543905f0d164c1f2246879165c751e05d40206402810228309001000708095200406181a143840810a3c0c0b8b8b40842648648442c068909d54051311d002c2e30e4034086c646dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da25207010000c001003c00001c1b404444731c1d1e1f202122232425010000800c007c00001c26404444731c1d1e1f2021222324250100000000404040006000",
"hash": 2465811007,
"alloc": 18180,
"alloc64": 18636,
"count": 1,
"formats": {
"48000/1ch/8-11": 1
},
"conflict": false
}
]

155
ackit/encoder.py Normal file
View File

@@ -0,0 +1,155 @@
"""Encode audio into game-compatible WEMs (portable: libvorbis via libsndfile, no Wwise, no ffmpeg).
Pipeline: float32 PCM -> libvorbis (Ogg) -> `builder.ogg_to_wem` (Wwise 2011.1 layout, game codebook
IDs, FNV-1 hash, decode-alloc size). The quality is chosen so that every codebook libvorbis uses exists
in the game's built-in library (docs/formats/wem.md); setups the game itself ships are preferred.
"""
import io
import json
import os
import numpy as np
import soundfile as sf
from . import audio
from .builder import ogg_to_wem
from .convert import library
from .known_setups import lookup_setup
from .ogg import read_packets
from .paths import user_dir
from .wwvorbis import V2W, transcode_setup
QUALITIES = [q / 2 for q in range(20, -1, -1)] # 10.0 .. 0.0 (libvorbis q; libsndfile cannot do q<0)
AUTO_MAX_Q_KNOWN = 6.0 # highest q considered when looking for a setup the game ships
AUTO_MAX_Q = 4.0 # otherwise, highest compatible q (game files sit around q3-5)
FALLBACK_RATES = (32000, 28000, 24000) # e.g. 6 channels at 44.1/48 kHz
FIT_RATES = (24000, 22050, 16000, 12000, 11025, 8000)
class EncodeError(ValueError):
pass
def vorbis_ogg(x, rate, quality):
"""Encode float32 PCM to Ogg Vorbis bytes at libvorbis quality `quality` (0..10)."""
b = io.BytesIO()
level = min(1.0, max(0.0, 1.0 - quality / 10.0))
x = np.ascontiguousarray(audio._as2d(x))
# write in blocks: libsndfile's Vorbis writer overflows the C stack on very large single writes
with sf.SoundFile(b, "w", rate, x.shape[1], format="OGG", subtype="VORBIS", compression_level=level) as f:
for i in range(0, len(x), 4096):
f.write(x[i:i + 4096])
return b.getvalue()
# --- setup probing (which q values are usable at a rate/channel count) ------------------------
_cache = None
def _cache_path():
return os.path.join(user_dir("cache"), "vorbis_probe.json")
def _probe_cache():
global _cache
if _cache is None:
try:
c = json.load(open(_cache_path()))
_cache = c if c.get("_lib") == sf.__libsndfile_version__ else None
except (OSError, ValueError):
_cache = None
_cache = _cache or {"_lib": sf.__libsndfile_version__}
return _cache
def probe_setup(rate, channels, quality):
"""(stripped Wwise setup or None, error) for libvorbis at these settings (cached)."""
key = "%d,%d,%.1f" % (rate, channels, quality)
c = _probe_cache()
if key not in c:
noise = (np.random.default_rng(7).standard_normal((rate, channels)) * 0.1).astype(np.float32)
pk = read_packets(vorbis_ogg(noise, rate, quality))
try:
s, _ = transcode_setup(pk[2][0], V2W, channels, library())
c[key] = [s.hex(), None]
except ValueError as e:
c[key] = [None, str(e)]
try:
json.dump(c, open(_cache_path(), "w"))
except OSError:
pass
s, err = c[key]
return (bytes.fromhex(s) if s else None), err
def choose_quality(rate, channels, quality="auto"):
"""-> (quality, known) for a rate/channel count, or raise EncodeError."""
if quality != "auto":
q = float(quality)
s, err = probe_setup(rate, channels, q)
if s is None:
raise EncodeError("quality %s cannot be used at %d Hz / %d ch: %s" % (quality, rate, channels, err))
return q, bool(lookup_setup(s))
compatible = None
for q in [x for x in QUALITIES if x <= AUTO_MAX_Q_KNOWN]:
s, _ = probe_setup(rate, channels, q)
if s is None:
continue
if lookup_setup(s):
return q, True
if compatible is None and q <= AUTO_MAX_Q:
compatible = q
if compatible is None:
raise EncodeError("no libvorbis quality yields game-compatible codebooks at %d Hz / %d ch" % (rate, channels))
return compatible, False
def pick_format(rate, channels, quality="auto"):
"""Find a usable (rate, quality). Falls back to lower rates when needed (Wwise resamples every
source, so playback speed is unaffected)."""
try:
return (rate,) + choose_quality(rate, channels, quality)
except EncodeError as first:
for r in FALLBACK_RATES:
if r < rate:
try:
return (r,) + choose_quality(r, channels, quality)
except EncodeError:
pass
raise first
def encode(x, src_rate, rate, channels, quality="auto"):
"""PCM -> WEM bytes in the requested format. Returns (wem, info)."""
r, q, known = pick_format(rate, channels, quality)
pcm = audio.conform(x, src_rate, r, channels)
if len(pcm) < 64:
raise EncodeError("the audio is empty")
wem = ogg_to_wem(vorbis_ogg(pcm, r, q))
return wem, dict(rate=r, quality=q, known_setup=known, size=len(wem), channels=channels,
seconds=len(pcm) / float(r), requested_rate=rate)
def encode_fit(x, src_rate, rate, channels, limit=None, quality="auto", progress=None):
"""Like `encode`, but if the result is larger than `limit` bytes lower the quality step by step,
then the sample rate, until it fits. Raises EncodeError if it cannot fit."""
wem, info = encode(x, src_rate, rate, channels, quality)
if limit is None or len(wem) <= limit:
return wem, info
first = len(wem)
for r in [info["rate"]] + [f for f in FIT_RATES if f < info["rate"]]:
pcm = audio.conform(x, src_rate, r, channels)
for q in QUALITIES:
if r == info["rate"] and q >= info["quality"]:
continue
if probe_setup(r, channels, q)[0] is None:
continue
if progress:
progress("shrinking: %d Hz, quality %.1f" % (r, q))
w = ogg_to_wem(vorbis_ogg(pcm, r, q))
if len(w) <= limit:
info.update(rate=r, quality=q, size=len(w), shrunk_from=first,
known_setup=bool(lookup_setup(probe_setup(r, channels, q)[0])))
return w, info
raise EncodeError("the audio is too long for this sound even at the lowest quality "
"(%d bytes, maximum %d). Shorten it." % (first, limit))

205
ackit/game.py Normal file
View File

@@ -0,0 +1,205 @@
"""The game installation: locating it (Steam on Windows / Linux / Proton), its WADs and language.
A `Game` is built from any folder path; nothing here is hard-coded to one machine.
"""
import glob
import hashlib
import os
import re
import struct
import sys
APPID = 200260
FOLDER_NAMES = ("Batman Arkham City GOTY", "Batman Arkham City")
LANGS = ("INT", "ITA", "FRA", "DEU", "ESN", "ESM", "POL", "RUS", "JPN", "KOR", "POR")
class GameError(Exception):
"""User-facing problem with the game folder (message is meant to be shown as-is)."""
def _iglob_one(base, *parts):
"""Case-insensitive path join (Linux file systems are case sensitive)."""
cur = base
for p in parts:
if not os.path.isdir(cur):
return None
hit = next((e for e in os.listdir(cur) if e.lower() == p.lower()), None)
if hit is None:
return None
cur = os.path.join(cur, hit)
return cur
# --- Steam discovery ------------------------------------------------------------------------
def steam_roots():
roots = []
if sys.platform == "win32":
try:
import winreg
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Valve\Steam") as k:
roots.append(winreg.QueryValueEx(k, "SteamPath")[0])
except OSError:
pass
roots += [r"C:\Program Files (x86)\Steam", r"C:\Program Files\Steam"]
else:
h = os.path.expanduser("~")
roots += [os.path.join(h, ".steam", "steam"), os.path.join(h, ".local", "share", "Steam"),
os.path.join(h, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"),
os.path.join(h, "snap", "steam", "common", ".local", "share", "Steam")]
out = []
for r in roots:
r = os.path.normpath(r)
if os.path.isdir(r) and r not in out:
out.append(r)
return out
def steam_libraries():
libs = []
for root in steam_roots():
libs.append(root)
vdf = _iglob_one(root, "steamapps", "libraryfolders.vdf")
if vdf:
for m in re.finditer(r'"path"\s+"([^"]+)"', open(vdf, encoding="utf-8", errors="replace").read()):
libs.append(m.group(1).replace("\\\\", "\\"))
seen, out = set(), []
for l in libs:
n = os.path.normcase(os.path.normpath(l))
if n not in seen and os.path.isdir(l):
seen.add(n)
out.append(os.path.normpath(l))
return out
def _true_case(path):
"""Windows paths from the registry may be lower-cased; rebuild the real spelling."""
path = os.path.normpath(path)
drive, rest = os.path.splitdrive(path)
cur = drive.upper() + os.sep if drive else os.sep
for part in [p for p in rest.split(os.sep) if p]:
hit = next((e for e in os.listdir(cur) if e.lower() == part.lower()), part) if os.path.isdir(cur) else part
cur = os.path.join(cur, hit)
return cur
def find_installs():
"""All Arkham City folders found in Steam libraries."""
out = []
for lib in steam_libraries():
for name in FOLDER_NAMES:
p = _iglob_one(lib, "steamapps", "common", name)
if p and is_game_dir(p) and _true_case(p) not in out:
out.append(_true_case(p))
return out
def is_game_dir(path):
return bool(path) and _iglob_one(path, "BmGame", "CookedPCConsole") is not None
# --- Game ------------------------------------------------------------------------------------
class Game:
def __init__(self, root):
root = os.path.normpath(os.path.abspath(root))
if not is_game_dir(root):
raise GameError("'%s' is not a Batman: Arkham City folder (BmGame\\CookedPCConsole is missing)." % root)
self.root = root
self.cooked = _iglob_one(root, "BmGame", "CookedPCConsole")
self.archives = self._find_archives()
if not self.archives:
raise GameError("No audio archives (.wad) found in %s." % self.cooked)
def _find_archives(self):
arch = {}
for p in glob.glob(os.path.join(self.cooked, "*", "*.wad")):
m = re.search(r"_LOC_([A-Za-z]{3})\.wad$", p)
if m:
arch[m.group(1).upper()] = p
elif os.path.basename(os.path.dirname(p)).upper() == "SFX":
arch["SFX"] = p
return dict(sorted(arch.items()))
@property
def languages(self):
return [a for a in self.archives if a != "SFX"]
def wad(self, archive):
try:
return self.archives[archive.upper()]
except KeyError:
raise GameError("This game has no '%s' audio archive (available: %s)."
% (archive, ", ".join(self.archives)))
def rel(self, path):
return os.path.relpath(path, self.root).replace("\\", "/")
@property
def exe(self):
return _iglob_one(self.root, "Binaries", "Win32", "BatmanAC.exe")
@property
def store_id(self):
"""Stable id of this install, used to name its originals store."""
return hashlib.sha1(os.path.normcase(self.root).encode("utf-8")).hexdigest()[:12]
# language -------------------------------------------------------------------------------
def config_dirs(self):
"""Possible BmGame\\Config folders (Windows Documents, Proton prefix)."""
dirs = [os.path.join(os.path.expanduser("~"), "Documents", "WB Games", "Batman Arkham City GOTY",
"BmGame", "Config")]
steamapps = os.path.dirname(os.path.dirname(self.root)) # .../steamapps
pfx = os.path.join(steamapps, "compatdata", str(APPID), "pfx", "drive_c", "users", "steamuser")
for docs in ("Documents", "My Documents"):
dirs.append(os.path.join(pfx, docs, "WB Games", "Batman Arkham City GOTY", "BmGame", "Config"))
return [d for d in dirs if os.path.isdir(d)]
def language(self):
"""Speech language the game is set to (falls back to INT, then the first available)."""
for d in self.config_dirs():
for f in ("UserEngine.ini", "BmEngine.ini"):
p = os.path.join(d, f)
if not os.path.exists(p):
continue
raw = open(p, "rb").read()
txt = raw.decode("utf-16") if raw[:2] in (b"\xff\xfe", b"\xfe\xff") else raw.decode("latin-1")
m = re.search(r"^\s*Language\s*=\s*(\w+)", txt, re.M | re.I)
if m and m.group(1).upper() in self.archives:
return m.group(1).upper()
if "INT" in self.archives:
return "INT"
return self.languages[0] if self.languages else "SFX"
# safety -----------------------------------------------------------------------------------
def check_writable(self, path):
"""Raise GameError if the file cannot be replaced (game running, read-only, ...)."""
try:
with open(path, "r+b"):
pass
except PermissionError:
raise GameError("Cannot write %s.\nClose the game (and anything else using the file) and try again."
% os.path.basename(path))
d = os.path.dirname(path)
if not os.access(d, os.W_OK):
raise GameError("The folder %s is not writable." % d)
def wad_fingerprint(path):
"""Identity of a WAD's game build: SHA-1 over its entry IDs and the sizes of all entries except
stream packs (whose size legitimately changes when music is replaced). In-place sound edits do not
change it; a different game build or language does."""
from .inject import is_pack
from .wad import Wad
w = Wad(path)
h = hashlib.sha1()
for e in w.entries:
h.update(struct.pack("<II", e.hash, 0 if is_pack(path, e.hash) else e.size))
return "%d-%s" % (len(w.entries), h.hexdigest()[:16])
def default_game_dir(settings=None):
"""Last used folder from settings, else the first Steam install found."""
if settings and settings.get("game_dir") and is_game_dir(settings["game_dir"]):
return settings["game_dir"]
found = find_installs()
return found[0] if found else None

133
ackit/inject.py Normal file
View File

@@ -0,0 +1,133 @@
"""Read and replace WEMs inside WAD archives.
Target syntax (stable ids used everywhere, also in mod packages):
'HASH' WAD entry holding one WEM e.g. 800BE4F0
'HASH:i' i-th WEM (0-based) of an entry holding several e.g. B49CDF48:11
'PACK/MEDIA' streamed WEM inside a BKPK stream pack (SFX) e.g. EDF036D6/393BC53A
Rules (see docs/formats/wad.md):
* entries / variations are indexed from inside the game's compressed .upk packages, so they are
replaced IN PLACE and must not exceed the original WEM size (`limit`); the WAD layout never moves;
* stream-pack media may change size: the pack index (SFX\\<PACK>.bnk) is rewritten too.
Writes go to a temporary file which atomically replaces the original (never half-written).
"""
import os
from .pack import Pack
from .wad import Wad, write_wad
from .wem import Wem, split_concatenated
class SizeLimitError(ValueError):
def __init__(self, target, limit, size):
super().__init__("the new sound is %d bytes but the original is %d bytes; the game reads exactly the "
"original size, so the replacement must not be larger (shorten the audio or lower "
"the quality)" % (size, limit))
self.target, self.limit, self.size = target, limit, size
def parse_target(t: str):
"""-> (entry_hash, index, media_id or None)"""
if "/" in t:
p, m = t.split("/", 1)
return int(p, 16), 0, int(m, 16)
h, _, idx = t.partition(":")
return int(h, 16), int(idx or 0), None
def pack_bnk_path(wad_path, pack_id):
"""SFX\\<PACK>.bnk next to the WAD (case-insensitive), or None."""
d = os.path.dirname(wad_path)
want = ("%08X.bnk" % pack_id).lower()
hit = next((f for f in os.listdir(d) if f.lower() == want), None)
return os.path.join(d, hit) if hit else None
def is_pack(wad_path, entry_hash):
p = pack_bnk_path(wad_path, entry_hash)
if not p:
return False
with open(p, "rb") as f:
return f.read(4) == b"BKPK"
def read_target(wad_path: str, target: str, wad: Wad = None) -> bytes:
"""Bytes of one WEM (seeks directly; does not load whole packs)."""
wad = wad or Wad(wad_path)
h, idx, mid = parse_target(target)
if h not in wad.index:
raise KeyError("entry %08X not in %s" % (h, os.path.basename(wad_path)))
e = wad.index[h]
if mid is not None:
pk = Pack(open(pack_bnk_path(wad_path, h), "rb").read())
_, size, off = pk.entries[pk.index[mid]]
with open(wad_path, "rb") as f:
f.seek(e.offset + off)
return f.read(size)
riffs, _ = split_concatenated(wad.read(h))
if idx >= len(riffs):
raise IndexError("entry %08X holds %d WEM(s)" % (h, len(riffs)))
return riffs[idx][1]
def replace_entry_payload(payload: bytes, index: int, new_wem: bytes, limit: int, target="") -> bytes:
"""Replace WEM `index` in place, zero-filled up to `limit` bytes; nothing else moves."""
riffs, _ = split_concatenated(payload)
if index >= len(riffs):
raise IndexError("entry holds %d WEM(s); index %d invalid" % (len(riffs), index))
if len(new_wem) > limit:
raise SizeLimitError(target, limit, len(new_wem))
off = riffs[index][0]
if off + limit > len(payload):
raise ValueError("entry is shorter than the original size limit (unexpected WAD state)")
return payload[:off] + new_wem + bytes(limit - len(new_wem)) + payload[off + limit:]
def inject(wad_path: str, replacements: dict, limits: dict, out_path: str = None, pack_gaps: dict = None):
"""Write several WEMs into one WAD in a single atomic rewrite.
replacements: {target: wem_bytes}; limits: {target: original size} for non-pack targets.
out_path: write a copy elsewhere instead of replacing the WAD (tests/dry runs).
pack_gaps: {pack hash: Pack.gaps()} original padding, so reverts restore packs byte-for-byte.
Returns the list of files written."""
src = Wad(wad_path)
payloads, pack_repl = {}, {}
for target, data in replacements.items():
Wem.parse(data) # must be a game-format WEM
h, idx, mid = parse_target(target)
if h not in src.index:
raise KeyError("entry %08X not in %s" % (h, os.path.basename(wad_path)))
if mid is not None:
pack_repl.setdefault(h, {})[mid] = data
continue
if limits.get(target) is None:
raise ValueError("no size limit given for %s" % target)
base = payloads.get(h) or src.read(h)
payloads[h] = replace_entry_payload(base, idx, data, limits[target], target)
new_bnks = {}
for h, media in pack_repl.items():
bpath = pack_bnk_path(wad_path, h)
new_bnks[bpath], payloads[h] = Pack(open(bpath, "rb").read()).rebuild(
src.read(h), media, (pack_gaps or {}).get(h))
dst = out_path or wad_path
bnk_dsts = {b: (b if out_path is None else os.path.join(os.path.dirname(out_path), os.path.basename(b)))
for b in new_bnks}
tmps = [dst + ".ackit-tmp"] + [d + ".ackit-tmp" for d in bnk_dsts.values()]
try:
write_wad(src, tmps[0], payloads)
problems = Wad(tmps[0]).check()
if problems:
raise RuntimeError("rebuilt WAD failed validation: %s" % problems[:3])
for b, d in bnk_dsts.items():
with open(d + ".ackit-tmp", "wb") as f:
f.write(new_bnks[b])
# commit: pack indexes first (tiny), then the WAD; each os.replace is atomic
for d in bnk_dsts.values():
os.replace(d + ".ackit-tmp", d)
os.replace(tmps[0], dst)
finally:
for p in tmps:
if os.path.exists(p):
os.remove(p)
return [dst] + list(bnk_dsts.values())

25
ackit/known_setups.py Normal file
View File

@@ -0,0 +1,25 @@
"""Database of Vorbis setups used by the game's own WEMs (from all shipped WADs).
Each entry: stripped Wwise setup (hex), its codebook hash, decode alloc sizes, and
formats seen. Reusing a known setup guarantees hash/alloc values the game accepts.
"""
import json
import os
PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "known_setups.json")
_DB = None
def db():
global _DB
if _DB is None:
_DB = json.load(open(PATH))
return _DB
def lookup_setup(ww_setup: bytes):
h = ww_setup.hex()
for e in db():
if e["setup"] == h:
return e
return None

120
ackit/modpack.py Normal file
View File

@@ -0,0 +1,120 @@
"""Portable mod packages (*.acmod): share replaced sounds between machines and languages.
A package is a zip:
manifest.json {"format": 1, "name", "author", "description", "created", "items": [...]}
items/NNN.wem the exact WEM that was applied (used as-is when the target matches)
items/NNN.flac the same audio, decoded (used to re-encode when the target differs, e.g. another
language whose original has a different size)
Each item records archive, target, name, group and the SHA-1 of the original it replaced.
"""
import io
import json
import os
import time
import zipfile
from dataclasses import dataclass
import soundfile as sf
from . import audio
from .originals import sha1_16
FORMAT = 1
def export_mod(ws, path, keys, name="", author="", description=""):
"""Write the applied versions of `keys` (from the originals store) into a package."""
items = []
with zipfile.ZipFile(path + ".tmp", "w", zipfile.ZIP_DEFLATED) as z:
for i, key in enumerate(keys):
e = ws.store.get(key)
if not e or not e.get("applied"):
raise ValueError("%s is not modified" % key)
s = ws.catalog.by_key.get(key)
wem = ws.store.get_blob(e["applied"])
x, sr = audio.decode_wem(wem)
buf = io.BytesIO()
sf.write(buf, x, sr, format="FLAC")
base = "items/%03d" % i
z.writestr(base + ".wem", wem)
z.writestr(base + ".flac", buf.getvalue())
items.append({"archive": e["archive"], "target": e["target"], "name": s.name if s else "",
"group": s.group if s else "", "original_sha1": e["orig_sha1"],
"wem": base + ".wem", "audio": base + ".flac", "meta": e.get("meta", {})})
z.writestr("manifest.json", json.dumps({"format": FORMAT, "name": name, "author": author,
"description": description, "created": time.time(),
"game": "Batman: Arkham City (PC)", "items": items}, indent=1))
os.replace(path + ".tmp", path)
return len(items)
@dataclass
class ImportItem:
item: dict
sound: object = None # matched Sound or None
how: str = "" # 'exact' | 'by name' | 'missing'
wem: bytes = None # ready-to-apply bytes (after `prepare_import`)
note: str = ""
def read_mod(path):
z = zipfile.ZipFile(path)
man = json.loads(z.read("manifest.json"))
if man.get("format") != FORMAT:
raise ValueError("unsupported mod package format %r" % man.get("format"))
return z, man
def match_mod(ws, path):
"""Match package items to sounds of this workspace. -> (manifest, [ImportItem])"""
z, man = read_mod(path)
out = []
for it in man["items"]:
key = "%s:%s" % (it["archive"], it["target"])
s = ws.catalog.by_key.get(key)
if s and it.get("name") and s.name != it["name"]:
s = None
if s:
out.append(ImportItem(it, s, "exact"))
continue
try:
cands = [c for c in ws.catalog.search(it["name"]) if c.name == it["name"]] if it.get("name") else []
except LookupError:
cands = []
if len(cands) == 1:
out.append(ImportItem(it, cands[0], "by name"))
else:
out.append(ImportItem(it, None, "missing", note="no sound named %r in this game/language" % it.get("name")))
z.close()
return man, out
def prepare_import(ws, path, items, progress=None):
"""Fill `wem` for matched items: reuse the packaged WEM when it targets the very same original,
otherwise re-encode the packaged audio for the local sound."""
z, _ = read_mod(path)
for im in items:
if not im.sound:
continue
orig = ws.original_bytes(im.sound)
wem = z.read(im.item["wem"])
lim = ws.limit(im.sound)
if im.how == "exact" and sha1_16(orig) == im.item.get("original_sha1") and (lim is None or len(wem) <= lim):
im.wem, im.note = wem, "exact copy"
else:
x, sr = sf.read(io.BytesIO(z.read(im.item["audio"])), dtype="float32", always_2d=True)
im.wem, info = ws.prepare(im.sound, x, sr, progress=progress)
im.note = "re-encoded (%d Hz, q%.1f)" % (info["rate"], info["quality"])
z.close()
return items
def import_mod(ws, path, force=False, progress=None):
"""Match, prepare and apply a package. Returns the list of ImportItem (see .how/.note)."""
_, items = match_mod(ws, path)
prepare_import(ws, path, items, progress)
changes = [(im.sound, im.wem, {"source": "mod:" + path.replace("\\", "/").split("/")[-1]})
for im in items if im.wem]
if changes:
ws.apply(changes, force=force, progress=progress)
return items

72
ackit/ogg.py Normal file
View File

@@ -0,0 +1,72 @@
"""Minimal Ogg page reader/writer."""
import struct
def _crc_table():
t = []
for i in range(256):
r = i << 24
for _ in range(8):
r = ((r << 1) ^ 0x04C11DB7) if r & 0x80000000 else (r << 1)
t.append(r & 0xFFFFFFFF)
return t
_T = _crc_table()
def ogg_crc(b: bytes) -> int:
c = 0
for x in b:
c = ((c << 8) & 0xFFFFFFFF) ^ _T[((c >> 24) & 0xFF) ^ x]
return c
def read_packets(b: bytes):
"""Return list of (packet_bytes, granule_of_page_if_packet_ends_page_else_None)."""
pos = 0
out = []
cur = b""
while pos + 27 <= len(b):
if b[pos:pos + 4] != b"OggS":
raise ValueError("lost Ogg sync at %d" % pos)
htype, gran, serial, seq, crc, nseg = struct.unpack_from("<BqIIIB", b, pos + 5)
segs = b[pos + 27: pos + 27 + nseg]
p = pos + 27 + nseg
ends = []
for s in segs:
cur += b[p:p + s]
p += s
if s < 255:
ends.append(cur)
cur = b""
for i, pk in enumerate(ends):
out.append((pk, gran if i == len(ends) - 1 else None))
pos = p
return out
class OggWriter:
def __init__(self, serial=0x41434B54):
self.serial = serial
self.seq = 0
self.out = bytearray()
def page(self, packets, granule, bos=False, eos=False):
segs = bytearray()
body = bytearray()
for pk in packets:
n = len(pk)
while n >= 255:
segs.append(255); n -= 255
segs.append(n)
body += pk
hdr = struct.pack("<4sBBqIIIB", b"OggS", 0, (2 if bos else 0) | (4 if eos else 0),
granule, self.serial, self.seq, 0, len(segs)) + bytes(segs)
page = bytearray(hdr + body)
struct.pack_into("<I", page, 22, ogg_crc(page))
self.out += page
self.seq += 1
def bytes(self):
return bytes(self.out)

143
ackit/originals.py Normal file
View File

@@ -0,0 +1,143 @@
"""Originals store: keeps the game's original bytes of every sound you modify, so any change can be
reverted exactly, without keeping full copies of the 0.4-0.5 GB WADs.
Layout (one store per game folder, in the user data folder by default; see docs/decisions.md):
<root>/store.json records: {key: {...}} + per-WAD info
<root>/blobs/<sha256>.wem original WEMs and the replacements you applied
<root>/gaps/<PACK>.json original non-zero padding of stream packs (byte-exact pack reverts)
<root>/full/<rel path> optional full copies of WADs / pack indexes (fallback, see `full_backup`)
Writes to store.json are atomic (temp file + rename).
"""
import hashlib
import json
import os
import shutil
import time
STORE_VERSION = 1
def sha256(b):
return hashlib.sha256(b).hexdigest()
def sha1_16(b):
return hashlib.sha1(b).hexdigest()[:16]
class OriginalsStore:
def __init__(self, root, game_root=None):
self.root = root
os.makedirs(os.path.join(root, "blobs"), exist_ok=True)
self.path = os.path.join(root, "store.json")
if os.path.exists(self.path):
self.data = json.load(open(self.path, encoding="utf-8"))
else:
self.data = {"version": STORE_VERSION, "game_root": game_root, "created": time.time(),
"entries": {}, "wads": {}}
self.save()
# persistence -------------------------------------------------------------------------------
def save(self):
tmp = self.path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(self.data, f, indent=1)
os.replace(tmp, self.path)
def put_blob(self, b):
h = sha256(b)
p = os.path.join(self.root, "blobs", h + ".wem")
if not os.path.exists(p):
tmp = p + ".tmp"
with open(tmp, "wb") as f:
f.write(b)
os.replace(tmp, p)
return h
def get_blob(self, h):
p = os.path.join(self.root, "blobs", h + ".wem")
b = open(p, "rb").read()
if sha256(b) != h:
raise IOError("originals store is damaged: %s" % p)
return b
# records ---------------------------------------------------------------------------------
@property
def entries(self):
return self.data["entries"]
def get(self, key):
return self.entries.get(key)
def original(self, key):
e = self.get(key)
return self.get_blob(e["orig"]) if e else None
def remember_original(self, key, archive, target, wad_rel, data):
"""Store the original bytes the first time a sound is modified (no-op afterwards)."""
if key not in self.entries:
self.entries[key] = {"archive": archive, "target": target, "wad": wad_rel,
"orig": self.put_blob(data), "orig_size": len(data),
"orig_sha1": sha1_16(data), "first_modified": time.time(),
"applied": None}
return self.entries[key]
def record_applied(self, key, data, meta=None):
e = self.entries[key]
e["applied"] = self.put_blob(data)
e["applied_at"] = time.time()
e["meta"] = meta or {}
def forget(self, key):
self.entries.pop(key, None)
def gc(self):
"""Delete blobs no record refers to."""
used = {e[k] for e in self.entries.values() for k in ("orig", "applied") if e.get(k)}
d = os.path.join(self.root, "blobs")
for f in os.listdir(d):
if f.endswith(".wem") and f[:-4] not in used:
os.remove(os.path.join(d, f))
# pack padding ------------------------------------------------------------------------------
def pack_gaps(self, pack_id):
p = os.path.join(self.root, "gaps", "%08X.json" % pack_id)
if not os.path.exists(p):
return None
return {int(k): bytes.fromhex(v) for k, v in json.load(open(p)).items()}
def remember_pack_gaps(self, pack_id, gaps):
p = os.path.join(self.root, "gaps", "%08X.json" % pack_id)
if os.path.exists(p):
return
os.makedirs(os.path.dirname(p), exist_ok=True)
json.dump({str(k): v.hex() for k, v in gaps.items()}, open(p, "w"))
# full-file fallback backups ----------------------------------------------------------------
def full_backup(self, src, rel):
"""Copy a game file once (before its first modification), verified by SHA-1."""
dst = os.path.join(self.root, "full", rel)
if os.path.exists(dst):
return dst
os.makedirs(os.path.dirname(dst), exist_ok=True)
tmp = dst + ".tmp"
shutil.copy2(src, tmp)
if _file_sha1(tmp) != _file_sha1(src):
os.remove(tmp)
raise IOError("backup copy of %s does not match the original" % rel)
os.replace(tmp, dst)
self.data.setdefault("full", {})[rel] = {"size": os.path.getsize(dst), "time": time.time()}
self.save()
return dst
def full_copy(self, rel):
p = os.path.join(self.root, "full", rel)
return p if os.path.exists(p) else None
def _file_sha1(p):
h = hashlib.sha1()
with open(p, "rb") as f:
for c in iter(lambda: f.read(1 << 22), b""):
h.update(c)
return h.hexdigest()

92
ackit/pack.py Normal file
View File

@@ -0,0 +1,92 @@
"""BKPK stream packs: SFX\\<ID>.bnk ("BKPK" header + embedded bank) indexes the streamed WEMs
stored inside the SFX WAD entry with the same ID (e.g. EDF036D6 'Music', E8F980BE 'Music_PDLC',
57572DAC 'Global', FCA4F778).
BKPK header (u32 LE):
0x00 'BKPK' 0x04 version 0x01020000 0x08 pack id (== WAD entry hash)
0x10 media count N 0x18 table size (12N) 0x1C table offset (0x48)
0x20 total stream size (== WAD entry size; end of last media aligned to 0x800)
0x2C end of table (0x48 + 12N) 0x38 embedded bank size 0x3C embedded bank offset
table: N x {u32 media id, u32 size, u32 offset-in-WAD-entry}, sorted by id; each media
starts at the next 0x800-aligned offset after the previous one.
See docs/wad_format.md.
"""
import struct
ALIGN = 0x800
def _a(x):
return (x + ALIGN - 1) // ALIGN * ALIGN
class Pack:
def __init__(self, bnk: bytes):
if bnk[:4] != b"BKPK":
raise ValueError("not a BKPK pack")
self.raw = bnk
self.id = struct.unpack_from("<I", bnk, 8)[0]
n = struct.unpack_from("<I", bnk, 0x10)[0]
self.table_off = struct.unpack_from("<I", bnk, 0x1C)[0]
self.total = struct.unpack_from("<I", bnk, 0x20)[0]
self.entries = [list(struct.unpack_from("<III", bnk, self.table_off + 12 * i)) for i in range(n)]
self.index = {e[0]: i for i, e in enumerate(self.entries)}
def media(self, payload: bytes, mid: int) -> bytes:
_, size, off = self.entries[self.index[mid]]
return payload[off:off + size]
def gaps(self, payload: bytes) -> dict:
"""Non-zero padding between media: {start offset: bytes}. Saved with the originals so that a
full revert restores the pack byte-for-byte."""
out = {}
for i, (mid, size, off) in enumerate(self.entries):
a = off + size
b = self.entries[i + 1][2] if i + 1 < len(self.entries) else self.total
g = payload[a:b]
if g.strip(b"\0"):
out[a] = g
return out
def rebuild(self, payload: bytes, replacements: dict, gaps: dict = None):
"""Returns (new_bnk_bytes, new_wad_payload).
Media are laid out by the pack rules (sorted order, 0x800-aligned, total aligned). Padding is
taken from `gaps` (original non-zero padding) when its position matches, else from the current
payload when that gap did not move, else zeros."""
missing = set(replacements) - set(self.index)
if missing:
raise KeyError("media not in pack: %s" % ", ".join("%08X" % m for m in missing))
gaps = gaps or {}
cur_gaps = {}
for i, (mid, size, off) in enumerate(self.entries):
nxt = self.entries[i + 1][2] if i + 1 < len(self.entries) else self.total
cur_gaps[off + size] = nxt
out = bytearray()
new_entries = []
def pad(upto):
a, n = len(out), upto - len(out)
if n <= 0:
return
g = gaps.get(a)
if g is not None and len(g) == n:
out.extend(g)
elif cur_gaps.get(a) == upto:
out.extend(payload[a:upto])
else:
out.extend(bytes(n))
for mid, size, off in self.entries:
pad(_a(len(out)))
blob = replacements[mid] if mid in replacements else payload[off:off + size]
new_entries.append((mid, len(blob), len(out)))
out += blob
total = _a(len(out))
pad(total)
bnk = bytearray(self.raw)
struct.pack_into("<I", bnk, 0x20, total)
for i, e in enumerate(new_entries):
struct.pack_into("<III", bnk, self.table_off + 12 * i, *e)
return bytes(bnk), bytes(out)

44
ackit/paths.py Normal file
View File

@@ -0,0 +1,44 @@
"""Filesystem locations: bundled resources and per-user data/config/cache folders.
Works from a source checkout and from a PyInstaller bundle (sys._MEIPASS). Override all user
folders with ACKIT_HOME (used by tests).
"""
import os
import sys
APP_NAME = "UnheardCity"
def resource_root():
"""Folder that contains `data/` (repo root, or the PyInstaller bundle root)."""
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
return sys._MEIPASS
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def package_data(*parts):
"""Files shipped inside the package (codebooks, known setups, hash list)."""
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", *parts)
def catalog_dir():
return os.path.join(resource_root(), "data", "catalog")
def _base(kind):
home = os.environ.get("ACKIT_HOME")
if home:
return os.path.join(home, kind)
if sys.platform == "win32":
root = os.environ.get("LOCALAPPDATA" if kind == "cache" else "APPDATA") or os.path.expanduser("~")
return os.path.join(root, APP_NAME, kind if kind == "cache" else "")
xdg = {"data": ("XDG_DATA_HOME", "~/.local/share"), "config": ("XDG_CONFIG_HOME", "~/.config"),
"cache": ("XDG_CACHE_HOME", "~/.cache")}[kind]
return os.path.join(os.environ.get(xdg[0]) or os.path.expanduser(xdg[1]), APP_NAME.lower())
def user_dir(kind):
"""kind: 'data' (originals store, notes), 'config' (settings), 'cache' (indexes)."""
p = os.path.normpath(_base(kind))
os.makedirs(p, exist_ok=True)
return p

32
ackit/settings.py Normal file
View File

@@ -0,0 +1,32 @@
"""Small JSON settings file shared by the CLI and the editor (user config folder)."""
import json
import os
from .paths import user_dir
DEFAULTS = {"game_dir": None, "store_root": None, "full_backup": True, "language": None,
"input_device": None, "output_device": None, "last_dir": None,
"background_enabled": True, "background_opacity": 0.1}
OBSOLETE = ("theme", "background_image")
def path():
return os.path.join(user_dir("config"), "settings.json")
def load():
s = dict(DEFAULTS)
try:
s.update(json.load(open(path(), encoding="utf-8")))
except (OSError, ValueError):
pass
for k in OBSOLETE:
s.pop(k, None)
return s
def save(s):
tmp = path() + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(s, f, indent=1)
os.replace(tmp, path())

0
ackit/verify/__init__.py Normal file
View File

183
ackit/verify/debugger.py Normal file
View File

@@ -0,0 +1,183 @@
"""Windows only. Unattended game run under cdb (WinDbg console debugger) with logging breakpoints.
Launch method (tested, see NOTES.md): with Steam running, BatmanAC.exe can be started directly
with SteamAppId=200260 in the environment (no relaunch through Steam). SteamStub decrypts .text
at runtime, so we wait until a known code signature is readable, then attach cdb non-intrusively
except for int3 breakpoints placed on already-decrypted code.
"""
import ctypes
import ctypes.wintypes as wt
import os
import re
import subprocess
import time
APPID = 200260
def find_cdb():
"""x86 cdb.exe from the WinDbg Store package (its path changes on every auto-update)."""
if os.environ.get("ACKIT_CDB"):
return os.environ["ACKIT_CDB"]
out = subprocess.run(["powershell", "-NoProfile", "-Command",
"(Get-AppxPackage Microsoft.WinDbg).InstallLocation"],
capture_output=True, text=True).stdout.strip()
for base in out.splitlines()[::-1]:
p = os.path.join(base.strip(), "x86", "cdb.exe")
if os.path.exists(p):
return p
for p in (r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe",):
if os.path.exists(p):
return p
raise FileNotFoundError("x86 cdb.exe not found (install WinDbg or set ACKIT_CDB)")
# Runtime addresses (image base 0x400000; ASLR off for this exe). See docs/decoder_re.md.
ADDR = dict(
get_or_create_setup=0x010B3400, # __thiscall(cache, AkSrcInfo*, ...) ; info+0x9c hash
parse_setup=0x010B4D10, # WwiseVorbisParseSetupHeader(info, channels, reader, pool) -> 0 ok
parse_setup_ret=0x010B351A, # in get_or_create_setup, right after the call (eax = result)
sig_site=0x010B347B, # mov ebx,[ebp+0x94] (decryption probe)
)
SIG = bytes.fromhex("8B9D94000000")
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400
k32.OpenProcess.restype = wt.HANDLE
k32.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
def read_mem(pid, addr, n):
h = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
if not h:
return None
try:
buf = ctypes.create_string_buffer(n)
got = ctypes.c_size_t()
if not k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, n, ctypes.byref(got)):
return None
return buf.raw[:got.value]
finally:
k32.CloseHandle(h)
u32 = ctypes.WinDLL("user32", use_last_error=True)
WM_KEYDOWN, WM_KEYUP = 0x0100, 0x0101
VK = dict(enter=0x0D, space=0x20, esc=0x1B, up=0x26, down=0x28, left=0x25, right=0x27, e=0x45)
def game_windows(pid):
"""Top-level windows of the game process."""
found = []
cb_t = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
def cb(hwnd, _):
p = wt.DWORD()
u32.GetWindowThreadProcessId(hwnd, ctypes.byref(p))
if p.value == pid and u32.IsWindowVisible(hwnd):
found.append(hwnd)
return True
u32.EnumWindows(cb_t(cb), 0)
return found
def post_key(pid, key, hold=0.08):
"""Post a key press to the game's window only (never global SendInput)."""
vk = VK[key] if isinstance(key, str) else key
sc = u32.MapVirtualKeyW(vk, 0)
n = 0
for hwnd in game_windows(pid):
u32.PostMessageW(hwnd, WM_KEYDOWN, vk, 1 | (sc << 16))
time.sleep(hold)
u32.PostMessageW(hwnd, WM_KEYUP, vk, 1 | (sc << 16) | (3 << 30))
n += 1
return n
def running_game_pids():
out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq BatmanAC.exe", "/FO", "CSV", "/NH"],
capture_output=True, text=True).stdout
return [int(m.group(1)) for m in re.finditer(r'"BatmanAC\.exe","(\d+)"', out)]
def kill_game():
for pid in running_game_pids():
subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True)
def launch_game(exe, extra_args=()):
"""Start BatmanAC.exe directly (Steam must be running; SteamAppId makes steam_api accept it)."""
env = dict(os.environ, SteamAppId=str(APPID), SteamGameId=str(APPID))
return subprocess.Popen([exe] + list(extra_args), cwd=os.path.dirname(exe), env=env)
def wait_decrypted(pid, timeout=60):
t0 = time.time()
while time.time() - t0 < timeout:
if read_mem(pid, ADDR["sig_site"], len(SIG)) == SIG:
return time.time() - t0
time.sleep(0.05)
raise TimeoutError("code never decrypted (signature not found)")
def cdb_script(extra_bps="", dump_info=False):
"""Debugger command file: log codec setup creation + parse result; log 2nd-chance crashes."""
a = ADDR
dump = "dd poi(@esp+4) L0x30; " if dump_info else ""
return "\n".join([
".echotime",
"sxd av",
'sxd -c2 ".echo ACKIT_CRASH; .echotime; r; kv 30; .ecxr; q" av',
"sxn ch", "sxn sse", "sxn eh", "sxn clr", "sxn ld", "sxn ud",
"sxi ibp",
# stream codec creation: log the AkVorbisInfo fields of the stream being opened
'bp %#x ".printf \\"ACKIT_SETUP hash=%%08x frames=%%u alloc=%%u ch=%%u rate=%%u\\\\n\\", '
'poi(poi(@esp+4)+0x9c), poi(poi(@esp+4)+0x78), poi(poi(@esp+4)+0x94), '
'wo(poi(@esp+4)+0x5a), poi(poi(@esp+4)+0x5c); %sgc"' % (a["get_or_create_setup"], dump),
# setup parse result (0 = OK)
'bp %#x ".printf \\"ACKIT_PARSE ret=%%x\\\\n\\", @eax; gc"' % a["parse_setup_ret"],
extra_bps,
".echo ACKIT_ATTACHED",
"g",
])
class Run:
def __init__(self, exe, log_path, script_extra="", dump_info=False):
self.exe = exe
self.log_path = log_path
self.script_extra = script_extra
self.dump_info = dump_info
self.game = None
self.dbg = None
def start(self):
kill_game()
self.t0 = time.time()
self.game = launch_game(self.exe)
pid = self.game.pid
dt = wait_decrypted(pid)
script = self.log_path + ".cdbcmds"
open(script, "w").write(cdb_script(self.script_extra, self.dump_info))
self.dbg = subprocess.Popen([find_cdb(), "-p", str(pid), "-pd", "-logo", self.log_path,
"-y", "", "-cf", script],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return dict(pid=pid, decrypt_s=dt)
def alive(self):
return self.game.poll() is None
def stop(self):
kill_game()
if self.dbg:
try:
self.dbg.wait(15)
except subprocess.TimeoutExpired:
self.dbg.kill()
def log(self):
try:
return open(self.log_path, encoding="utf-8", errors="replace").read()
except FileNotFoundError:
return ""

121
ackit/verify/ingame.py Normal file
View File

@@ -0,0 +1,121 @@
"""Automated in-game test (Windows): does a replaced sound actually play?
Flow: apply the replacement through `Workspace.apply` (the same code path as the editor) ->
start the game under cdb with logging breakpoints on the decoder + record the speakers (WASAPI
loopback) -> check: no crash, the codec setup of the injected WEM was created and parsed OK, and the
injected audio is heard -> kill the game -> revert and check the files are byte-identical again.
Default target: SFX music EDF036D6/393BC53A (intro/title music), which the game starts ~5 s after
launch without input. That slot ignores sources shorter than ~10 s, so probes are 30 s long.
Only correlation scores of the recording are kept (the loopback may contain other apps' audio).
"""
import hashlib
import json
import os
import re
import time
import numpy as np
from .. import audio
from ..originals import sha256
DEFAULT_TARGET = "SFX:EDF036D6/393BC53A"
RATE = 48000
def probe_signal(seconds=30.0, rate=RATE, seed=1234):
"""Pink-ish noise + vibrato tone: easy to recognise in a recording."""
rng = np.random.default_rng(seed)
n = int(seconds * rate)
white = rng.standard_normal(n)
pink = np.cumsum(white) * 0.002
pink -= np.convolve(pink, np.ones(2048) / 2048, mode="same")
t = np.arange(n) / rate
tone = 0.25 * np.sin(2 * np.pi * (440 + 20 * np.sin(2 * np.pi * 0.5 * t)) * t)
x = (pink / (np.max(np.abs(pink)) + 1e-9) * 0.35 + tone).astype(np.float32)
return x[:, None], rate
def _mono48(wem):
x, sr = audio.decode_wem(wem)
return audio.to_channels(audio.resample(x, sr, RATE), 1)[:, 0]
def _sha1_file(p):
h = hashlib.sha1()
with open(p, "rb") as f:
for c in iter(lambda: f.read(1 << 22), b""):
h.update(c)
return h.hexdigest()
def run(ws, key=DEFAULT_TARGET, wem=None, mode="modded", seconds=40.0, threshold=0.5,
logdir=None, revert_after=True, log=print):
"""mode: 'modded' (apply `wem`, test, revert), 'baseline' (nothing), 'installed' (test the
current state). Returns a report dict with report['result'] in ('PASS', 'FAIL')."""
from .debugger import Run, kill_game
from .loopback import LoopbackRecorder, detect_chunks
from ..inject import pack_bnk_path, parse_target
s = ws.catalog.resolve(key)
wad = ws.wad_path(s)
files = [wad] + ([pack_bnk_path(wad, parse_target(s.target)[0])] if s.is_pack_media else [])
logdir = logdir or os.path.join(os.getcwd(), "logs")
os.makedirs(logdir, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
logp = os.path.join(logdir, "ingame_%s.log" % stamp)
kill_game()
report = dict(target=s.key, name=s.name, mode=mode)
before = {f: _sha1_file(f) for f in files} if mode == "modded" else {}
if mode == "modded":
ws.apply([(s, wem, {"source": "ingame-test"})], progress=log)
expect = ws.current_bytes(s)
orig = ws.original_bytes(s)
from ..wem import Wem
report["expect_hash"] = "%08x" % Wem.parse(expect).vorb["codebook_hash"]
ref_expect, ref_orig = _mono48(expect), _mono48(orig)
rec = LoopbackRecorder(seconds + 10)
rec.start()
runner = Run(ws.game.exe, logp)
try:
report.update(runner.start())
while time.time() - runner.t0 < seconds and runner.alive():
time.sleep(0.5)
if re.search(r"^ACKIT_CRASH", runner.log(), re.M):
break
report["alive_at_end"] = runner.alive()
finally:
runner.stop()
rec.stop_flag = True
rec.join(5)
if mode == "modded" and revert_after:
ws.revert([s], progress=log)
report["reverted_byte_identical"] = all(_sha1_file(f) == h for f, h in before.items())
text = runner.log()
report["crash"] = bool(re.search(r"^ACKIT_CRASH", text, re.M))
ev = []
for ln in text.splitlines():
m = re.match(r"ACKIT_SETUP hash=([0-9a-f]+)", ln)
if m:
ev.append([m.group(1), "cached"])
m = re.match(r"ACKIT_PARSE ret=([0-9a-f]+)", ln)
if m and ev and ev[-1][1] == "cached":
ev[-1][1] = m.group(1)
report["setups"] = ["%s:%s" % tuple(e) for e in ev]
mine = [r for h, r in ev if h == report["expect_hash"]]
report["setup_ok"] = bool(mine) and all(r in ("0", "cached") for r in mine)
cap = rec.audio()
report["capture_error"] = rec.error
report["ncc_expected"] = round(detect_chunks(cap, ref_expect)[0], 3)
report["ncc_original"] = round(detect_chunks(cap, ref_orig)[0], 3)
ok = (not report["crash"] and report["alive_at_end"] and report["setup_ok"]
and report["ncc_expected"] >= threshold
and (mode == "baseline" or sha256(expect) == sha256(orig) or report["ncc_expected"] > report["ncc_original"])
and report.get("reverted_byte_identical", True))
report["result"] = "PASS" if ok else "FAIL"
json.dump(report, open(logp.replace(".log", ".json"), "w"), indent=1)
return report

87
ackit/verify/loopback.py Normal file
View File

@@ -0,0 +1,87 @@
"""WASAPI loopback capture of the default speaker + detection of a reference sound in it."""
import threading
import warnings
import numpy as np
RATE = 48000
class LoopbackRecorder(threading.Thread):
def __init__(self, seconds, rate=RATE):
super().__init__(daemon=True)
self.seconds = seconds
self.rate = rate
self.chunks = []
self.stop_flag = False
self.error = None
def run(self):
try:
import soundcard as sc
sp = sc.default_speaker()
mic = sc.get_microphone(id=str(sp.name), include_loopback=True)
block = self.rate // 10
with warnings.catch_warnings():
warnings.simplefilter("ignore") # "data discontinuity" warnings
with mic.recorder(samplerate=self.rate, blocksize=block) as rec:
n = int(self.seconds * 10)
for _ in range(n):
if self.stop_flag:
break
self.chunks.append(rec.record(numframes=block))
except Exception as e: # noqa
self.error = repr(e)
def audio(self):
if not self.chunks:
return np.zeros(0, dtype=np.float32)
a = np.concatenate(self.chunks, axis=0)
return a.mean(axis=1).astype(np.float32) if a.ndim == 2 else a.astype(np.float32)
def resample(x, src_rate, dst_rate):
if src_rate == dst_rate:
return x.astype(np.float32)
n = int(round(len(x) * dst_rate / src_rate))
t = np.linspace(0, len(x) - 1, n)
return np.interp(t, np.arange(len(x)), x).astype(np.float32)
def detect(capture, ref, rate=RATE):
"""Max normalized cross-correlation of ref against every same-length window of capture.
Returns (score 0..1, time_offset_seconds, ref_rms_in_window)."""
if len(capture) < len(ref) or len(ref) == 0:
return 0.0, None, 0.0
ref = ref - ref.mean()
rn = np.linalg.norm(ref)
if rn == 0:
return 0.0, None, 0.0
n = 1 << int(np.ceil(np.log2(len(capture) + len(ref))))
xc = np.fft.irfft(np.fft.rfft(capture, n) * np.conj(np.fft.rfft(ref, n)), n)[:len(capture) - len(ref) + 1]
c2 = np.concatenate([[0.0], np.cumsum(capture.astype(np.float64) ** 2)])
win = np.sqrt(np.maximum(c2[len(ref):] - c2[:-len(ref)], 1e-12))
ncc = xc / (rn * win)
# ignore near-silent windows
ncc[win < 1e-3 * np.sqrt(len(ref))] = 0
i = int(np.argmax(np.abs(ncc)))
return float(abs(ncc[i])), i / rate, float(win[i] / np.sqrt(len(ref)))
def detect_chunks(capture, ref, rate=RATE, chunk_s=1.0, step_s=0.5):
"""Best NCC over 1 s chunks of the reference (robust to partial playback and resampler drift).
Returns (best score, chunk start in ref [s], position in capture [s])."""
c = int(chunk_s * rate)
s = int(step_s * rate)
best = (0.0, None, None)
if len(ref) <= c:
sc, t, _ = detect(capture, ref, rate)
return sc, 0.0, t
for i in range(0, len(ref) - c + 1, s):
seg = ref[i:i + c]
if np.sqrt(np.mean(seg ** 2)) < 1e-3:
continue
sc, t, _ = detect(capture, seg, rate)
if sc > best[0]:
best = (sc, i / rate, t)
return best

93
ackit/verify/selfcheck.py Normal file
View File

@@ -0,0 +1,93 @@
"""Offline self-check against the installed game (no game launch, nothing is modified).
1. every audio archive parses and follows the WAD layout rules;
2. a sample of game WEMs is rebuilt byte-for-byte from the derived format rules;
3. a test signal is encoded for a speech and a music sound and decoded again (correlation check);
4. the originals store is consistent (blobs intact, recorded states).
"""
import random
import numpy as np
from .. import audio
from ..builder import build_wem, setup_header_values
from ..convert import library
from ..inject import is_pack
from ..wad import Wad
from ..wem import Wem, split_concatenated
from ..wwvorbis import transcode_setup, W2V
def rebuild_identical(r):
"""True if a game WEM is reproduced byte-for-byte by the builder."""
m = Wem.parse(r)
v = m.vorb
s = m.setup_packet()
_, info = transcode_setup(s, W2V, m.channels, library())
pk, _ = m.audio_packets()
mb = info.mode_bits
if v["bs0_exp"] != v["bs1_exp"]:
flags = [info.mode_blockflags[p[0] & ((1 << mb) - 1)] if mb else info.mode_blockflags[0] for _, p in pk]
else:
flags = [info.mode_blockflags[(p[0] >> 1) & ((1 << mb) - 1)] if mb else info.mode_blockflags[0] for _, p in pk]
hv = setup_header_values(s, m.channels)
extra = [c for c in m.chunks if c.tag not in (b"fmt ", b"data")]
for interval in (8192, 1024, 2048, 4096, 16384, 32768, 512, 256):
out = build_wem(m.channels, m.rate, v["bs0_exp"], v["bs1_exp"], s, [p for _, p in pk], flags,
v["total_frames"], hv["hash"], hv["alloc"], hv["alloc64"], m.channel_mask,
seek=v["seek_table_size"] > 0, seek_interval=interval, valid_bits=m.valid_bits,
extra_chunks=extra)
if out == r:
return True
if v["seek_table_size"] == 0:
break
return False
def run(ws, sample=300, log=print):
ok = True
# 1 + 2
for arch, path in ws.game.archives.items():
w = Wad(path)
probs = w.check()
rnd = random.Random(1)
ents = rnd.sample(w.entries, min(len(w.entries), 60))
n = good = 0
for e in ents:
if is_pack(path, e.hash): # stream packs are checked by the tests (they are large)
continue
for _, r in split_concatenated(w.read(e.hash))[0][:5]:
n += 1
try:
good += rebuild_identical(r)
except Exception:
pass
if n >= sample // max(1, len(ws.game.archives)):
break
log("%-4s layout %s, rebuilt %d/%d sample WEMs byte-identical" % (arch, "OK" if not probs else probs[:2], good, n))
ok &= not probs and good >= n - 2 # 4 looping music tracks are known exceptions
# 3
t = np.arange(48000 * 2) / 48000.0
sig = (0.3 * np.sin(2 * np.pi * 330 * t) * np.sin(2 * np.pi * 1.5 * t)).astype(np.float32)[:, None]
for s in [x for x in ws.catalog.sounds if x.group == "Speech"][:1] + \
[x for x in ws.catalog.sounds if x.group == "Music"][:1]:
wem, info = ws.prepare(s, sig, 48000)
y, sr = audio.decode_wem(wem)
ref = audio.to_channels(audio.resample(sig, 48000, sr), y.shape[1])
n = min(len(ref), len(y))
c = float(np.corrcoef(ref[:n, 0], y[:n, 0])[0, 1])
log("encode/decode %-30s %d Hz q%.1f corr %.3f" % (s.name[:30], info["rate"], info["quality"], c))
ok &= c > 0.95
# 4
bad = 0
for k, e in ws.store.entries.items():
try:
ws.store.get_blob(e["orig"])
if e.get("applied"):
ws.store.get_blob(e["applied"])
except (OSError, IOError):
bad += 1
log("originals store: %d record(s), %d damaged" % (len(ws.store.entries), bad))
ok &= bad == 0
log("SELF-CHECK %s" % ("PASS" if ok else "FAIL"))
return ok

122
ackit/wad.py Normal file
View File

@@ -0,0 +1,122 @@
"""Arkham City 'WWAD' container (SFX and *_LOC_*.wad).
Layout (little endian):
0x00 'WWAD'
0x04 u32 version (0x01020000)
0x08 u32 entry count N
0x0C u32 table offset (0x1C)
0x10 u32 table size (N*12)
0x14 u32 data start (header area size, 0x1000 or 0x8000; aligned)
0x18 u32 data size (file size - data start)
0x1C N x {u32 hash, u32 size, u32 offset} sorted by hash as *signed* int32
Entries are stored in table order, each at a 0x1000-aligned offset; 'size' is the
entry payload size padded to 0x800. Padding bytes are zero; the file ends 0x1000-aligned.
See docs/wad_format.md.
"""
import struct
from dataclasses import dataclass
MAGIC = b"WWAD"
ENTRY_ALIGN = 0x1000
SIZE_ALIGN = 0x800
def _s32(x):
return struct.unpack("<i", struct.pack("<I", x))[0]
def align(x, a):
return (x + a - 1) // a * a
@dataclass
class Entry:
hash: int
size: int
offset: int
class Wad:
def __init__(self, path):
self.path = path
with open(path, "rb") as f:
hdr = f.read(0x1C)
(magic, self.version, n, self.table_off, self.table_size,
self.data_start, self.data_size) = struct.unpack("<4s6I", hdr)
if magic != MAGIC:
raise ValueError("not a WWAD file")
f.seek(self.table_off)
tbl = f.read(12 * n)
f.seek(0)
self.header_area = f.read(self.data_start)
self.entries = [Entry(*struct.unpack_from("<III", tbl, 12 * i)) for i in range(n)]
self.index = {e.hash: e for e in self.entries}
def read(self, h) -> bytes:
e = self.index[h]
with open(self.path, "rb") as f:
f.seek(e.offset)
return f.read(e.size)
def iter_entries(self):
with open(self.path, "rb") as f:
for e in self.entries:
f.seek(e.offset)
yield e, f.read(e.size)
def check(self):
"""Validate structural invariants; returns list of problems (empty = OK)."""
probs = []
hs = [_s32(e.hash) for e in self.entries]
if hs != sorted(hs):
probs.append("table not sorted by signed hash")
if self.table_size != 12 * len(self.entries):
probs.append("table size mismatch")
pos = self.data_start
for e in self.entries:
if e.offset != align(pos, ENTRY_ALIGN):
probs.append("entry %08X at %x, expected %x" % (e.hash, e.offset, align(pos, ENTRY_ALIGN)))
if e.size % SIZE_ALIGN:
probs.append("entry %08X size not 0x800-aligned" % e.hash)
pos = e.offset + e.size
return probs
def write_wad(src: Wad, out_path: str, replacements: dict):
"""Rebuild a WAD, replacing entry payloads (hash -> bytes). Layout rules are reproduced
exactly so an unmodified rebuild is byte-identical to the original."""
n = len(src.entries)
payloads = {}
for h, data in replacements.items():
if h not in src.index:
raise KeyError("hash %08X not in WAD" % h)
payloads[h] = data + b"\0" * (align(len(data), SIZE_ALIGN) - len(data))
new_entries = []
pos = src.data_start
for e in src.entries:
size = len(payloads[e.hash]) if e.hash in payloads else e.size
off = align(pos, ENTRY_ALIGN)
new_entries.append(Entry(e.hash, size, off))
pos = off + size
end = pos
total = align(end, ENTRY_ALIGN)
hdr = bytearray(src.header_area)
struct.pack_into("<4s6I", hdr, 0, MAGIC, src.version, n, src.table_off, 12 * n, src.data_start,
total - src.data_start)
for i, e in enumerate(new_entries):
struct.pack_into("<III", hdr, src.table_off + 12 * i, e.hash, e.size, e.offset)
with open(src.path, "rb") as fi, open(out_path, "wb") as fo:
fo.write(hdr)
for old, new in zip(src.entries, new_entries):
fo.write(b"\0" * (new.offset - fo.tell()))
if old.hash in payloads:
fo.write(payloads[old.hash])
else:
fi.seek(old.offset)
remaining = old.size
while remaining:
chunk = fi.read(min(remaining, 1 << 22))
fo.write(chunk)
remaining -= len(chunk)
fo.write(b"\0" * (total - fo.tell()))
return new_entries

133
ackit/wem.py Normal file
View File

@@ -0,0 +1,133 @@
"""Wwise RIFF (.wem) container for Arkham City (Wwise 2011.1, fmt size 0x42, Vorbis 0xFFFF)."""
import re
import struct
from dataclasses import dataclass, field
VORB_FMT = "<IIIHHIIHHIII2B" # AkVorbisInfo as stored at fmt+0x18 (42 bytes)
VORB_FIELDS = ["total_frames", "loop_start_pkt", "loop_end_pkt", "loop_begin_extra", "loop_end_extra",
"seek_table_size", "vorbis_data_offset", "max_packet_size", "last_granule_extra",
"decode_alloc", "decode_alloc_x64", "codebook_hash", "bs0_exp", "bs1_exp"]
@dataclass
class Chunk:
tag: bytes
data: bytes
@dataclass
class Wem:
chunks: list
channels: int = 0
rate: int = 0
avg_bps: int = 0
valid_bits: int = 0
channel_mask: int = 0
vorb: dict = field(default_factory=dict)
# ---- parsing ----
@classmethod
def parse(cls, b: bytes) -> "Wem":
if b[:4] != b"RIFF" or b[8:12] != b"WAVE":
raise ValueError("not RIFF/WAVE")
riff_end = 8 + struct.unpack_from("<I", b, 4)[0]
pos = 12
chunks = []
while pos + 8 <= min(riff_end, len(b)):
tag, ln = struct.unpack_from("<4sI", b, pos)
chunks.append(Chunk(tag, b[pos + 8:pos + 8 + ln]))
pos += 8 + ln + (ln & 1)
w = cls(chunks)
fmt = w.get(b"fmt ")
if fmt is None or len(fmt) != 0x42:
raise ValueError("not a Wwise Vorbis WEM (fmt size %s)" % (None if fmt is None else hex(len(fmt))))
fmt_tag, w.channels, w.rate, w.avg_bps, balign, bps, cb = struct.unpack_from("<HHIIHHH", fmt, 0)
if fmt_tag != 0xFFFF or len(fmt) != 0x42 or cb != 0x30:
raise ValueError("unsupported fmt (tag=%x size=%x)" % (fmt_tag, len(fmt)))
w.valid_bits, w.channel_mask = struct.unpack_from("<HI", fmt, 0x12)
w.vorb = dict(zip(VORB_FIELDS, struct.unpack_from(VORB_FMT, fmt, 0x18)))
return w
def get(self, tag: bytes):
for c in self.chunks:
if c.tag == tag:
return c.data
return None
def set(self, tag: bytes, data: bytes):
for c in self.chunks:
if c.tag == tag:
c.data = data
return
self.chunks.append(Chunk(tag, data))
# ---- data chunk layout ----
@property
def data(self) -> bytes:
return self.get(b"data")
def seek_table(self):
n = self.vorb["seek_table_size"] // 4
return [struct.unpack_from("<HH", self.data, 4 * i) for i in range(n)]
def setup_packet(self) -> bytes:
d = self.data
off = self.vorb["seek_table_size"]
ln = struct.unpack_from("<H", d, off)[0]
return d[off + 2: off + 2 + ln]
def audio_packets(self):
"""List of (offset_in_data, payload) for audio packets (2-byte size headers)."""
d = self.data
pos = self.vorb["vorbis_data_offset"]
out = []
while pos + 2 <= len(d):
ln = struct.unpack_from("<H", d, pos)[0]
if ln == 0 or pos + 2 + ln > len(d):
break
out.append((pos, d[pos + 2: pos + 2 + ln]))
pos += 2 + ln
return out, pos
# ---- serialization ----
def build_fmt(self) -> bytes:
v = self.vorb
return (struct.pack("<HHIIHHH", 0xFFFF, self.channels, self.rate, self.avg_bps, 0, 0, 0x30)
+ struct.pack("<HI", self.valid_bits, self.channel_mask)
+ struct.pack(VORB_FMT, *[v[k] for k in VORB_FIELDS]))
def to_bytes(self) -> bytes:
self.set(b"fmt ", self.build_fmt())
body = b"WAVE"
for c in self.chunks:
# the game's WEMs never pad odd-sized chunks (all chunks before 'data' are even)
body += struct.pack("<4sI", c.tag, len(c.data)) + c.data
return b"RIFF" + struct.pack("<I", len(body)) + body
SUB_ALIGN = 0x800
_NONZERO = re.compile(rb"[^\x00]")
def split_concatenated(b: bytes):
"""All WEMs of a WAD entry, as [(offset, riff_bytes)], plus the end of the last one.
LOC entries group variations of a line (e.g. CHR_RadioScanner = 63 WEMs): each WEM starts at the
next 0x800 boundary after the previous one (zero padding); a few entries pack them back to back."""
out = []
pos = 0
end = 0
while pos + 12 <= len(b) and b[pos:pos + 4] == b"RIFF" and b[pos + 8:pos + 12] == b"WAVE":
end = pos + 8 + struct.unpack_from("<I", b, pos + 4)[0]
out.append((pos, b[pos:end]))
nxt = (end + SUB_ALIGN - 1) // SUB_ALIGN * SUB_ALIGN
if b[end:end + 4] == b"RIFF":
pos = end
elif b[nxt:nxt + 4] == b"RIFF":
pos = nxt
else: # zero padding of arbitrary length (e.g. after an in-place replacement)
m = _NONZERO.search(b, end)
if not m:
break
pos = m.start()
return out, end

197
ackit/workspace.py Normal file
View File

@@ -0,0 +1,197 @@
"""High-level operations shared by the CLI and the editor: apply / revert sounds safely.
Safety rules (docs/decisions.md):
* before the first change to a sound its original bytes go to the originals store;
* optionally a full copy of each WAD (and pack index) is taken once, as a fallback;
* each WAD is rewritten atomically; several sounds of one WAD are written in one pass;
* if a sound in the game is neither its original nor what we applied (Steam update, "verify
integrity", another tool), we refuse to write it unless forced, and report it.
"""
import os
from collections import defaultdict
from . import audio
from .catalog import Catalog
from .encoder import encode_fit
from .game import Game, GameError
from .inject import inject, read_target, parse_target, pack_bnk_path
from .originals import OriginalsStore, sha256, sha1_16
from .pack import Pack
from .paths import user_dir
from .wad import Wad
# sound states
ORIGINAL = "original" # untouched
MODIFIED = "modified" # contains what we applied
LOST = "lost" # we applied something, but the game has the original again (e.g. Steam verify)
CHANGED = "changed" # neither original nor ours: changed underneath (update / other tool)
FOREIGN = "foreign" # never touched by us, but differs from the catalog's original checksum
class ConflictError(GameError):
def __init__(self, sounds):
self.sounds = sounds
super().__init__("These sounds were changed outside this tool (Steam update or 'verify', or "
"another mod tool): %s.\nRevert them or confirm to overwrite."
% ", ".join(s.name for s in sounds[:5]) + (" ..." if len(sounds) > 5 else ""))
def default_store_root(game):
return os.path.join(user_dir("data"), "stores", game.store_id)
class Workspace:
def __init__(self, game_dir, store_root=None, archives=None, full_backup=True, progress=None):
self.game = game_dir if isinstance(game_dir, Game) else Game(game_dir)
self.store = OriginalsStore(store_root or default_store_root(self.game), self.game.root)
self.catalog = Catalog(self.game, archives, progress)
self.full_backup = full_backup
# reading -----------------------------------------------------------------------------------
def sound(self, name_or_key):
return self.catalog.resolve(name_or_key)
def wad_path(self, s):
return self.game.wad(s.archive)
def current_bytes(self, s):
return read_target(self.wad_path(s), s.target)
def original_bytes(self, s):
return self.store.original(s.key) or self.current_bytes(s)
def state(self, s, current=None):
cur = current if current is not None else self.current_bytes(s)
e = self.store.get(s.key)
if e:
h = sha256(cur)
if e.get("applied") and h == e["applied"]:
return MODIFIED
if h == e["orig"]:
return LOST if e.get("applied") else ORIGINAL
return CHANGED
if s.sha1 and sha1_16(cur) != s.sha1:
return FOREIGN
return ORIGINAL
def modified_keys(self):
return [k for k, e in self.store.entries.items() if e.get("applied")]
def limit(self, s):
"""Max size in bytes for a replacement (None = any size)."""
if s.is_pack_media:
return None
e = self.store.get(s.key)
if e:
return e["orig_size"]
if s.sha1 and s.size:
return s.size
return len(self.current_bytes(s))
def original_audio(self, s):
return audio.decode_wem(self.original_bytes(s))
def current_audio(self, s):
return audio.decode_wem(self.current_bytes(s))
# encoding ----------------------------------------------------------------------------------
def prepare(self, s, x, src_rate, quality="auto", progress=None):
"""Encode PCM for sound `s` (its rate/channels, fitted to its size limit) -> (wem, info)."""
return encode_fit(x, src_rate, s.rate or src_rate, s.channels or x.shape[1], self.limit(s),
quality, progress)
def prepare_file(self, s, path, quality="auto", progress=None):
x, sr = audio.load(path)
return self.prepare(s, x, sr, quality, progress)
# writing -----------------------------------------------------------------------------------
def _backup(self, wad, pack_hashes):
if not self.full_backup:
return
self.store.full_backup(wad, self.game.rel(wad))
for h in pack_hashes:
b = pack_bnk_path(wad, h)
self.store.full_backup(b, self.game.rel(b))
def _write(self, archive, items, progress=None):
"""items: [(sound, new_bytes, meta, is_revert)] all in one archive."""
wad = self.game.wad(archive)
self.game.check_writable(wad)
packs = {parse_target(s.target)[0] for s, _, _, _ in items if s.is_pack_media}
self._backup(wad, packs)
w = Wad(wad)
for h in packs:
if self.store.pack_gaps(h) is None:
self.store.remember_pack_gaps(h, Pack(open(pack_bnk_path(wad, h), "rb").read()).gaps(w.read(h)))
repl, limits = {}, {}
for s, data, meta, is_revert in items:
if not is_revert:
self.store.remember_original(s.key, s.archive, s.target, self.game.rel(wad),
read_target(wad, s.target, w))
repl[s.target] = data
limits[s.target] = self.limit(s)
self.store.save() # originals are safe on disk before the game file changes
if progress:
progress("writing %s" % os.path.basename(wad))
inject(wad, repl, limits, pack_gaps={h: self.store.pack_gaps(h) for h in packs})
for s, data, meta, is_revert in items:
if is_revert:
self.store.forget(s.key)
else:
self.store.record_applied(s.key, data, meta)
self.store.save()
def apply(self, changes, force=False, progress=None):
"""changes: [(sound, wem_bytes, meta dict)]. One atomic rewrite per WAD."""
conflicts = []
for s, _, _ in changes:
if self.state(s) in (CHANGED, FOREIGN):
conflicts.append(s)
if conflicts and not force:
raise ConflictError(conflicts)
by_arch = defaultdict(list)
for s, data, meta in changes:
by_arch[s.archive].append((s, data, meta, False))
for a, items in by_arch.items():
self._write(a, items, progress)
def revert(self, sounds, progress=None):
"""Put the stored originals back (sounds without a record are skipped). Returns reverted."""
by_arch = defaultdict(list)
done = []
for s in sounds:
orig = self.store.original(s.key)
if orig is None:
continue
by_arch[s.archive].append((s, orig, None, True))
done.append(s)
for a, items in by_arch.items():
self._write(a, items, progress)
self.store.gc()
return done
def revert_all(self, progress=None):
sounds = [self.catalog.by_key[k] for k in list(self.store.entries) if k in self.catalog.by_key]
missing = [k for k in self.store.entries if k not in self.catalog.by_key]
if missing:
# records of archives not loaded in this catalog (e.g. another language)
other = Catalog(self.game, sorted({k.split(":")[0] for k in missing}))
sounds += [other.by_key[k] for k in missing if k in other.by_key]
return self.revert(sounds, progress)
def restore_from_full_backup(self):
"""Last-resort fallback: copy back every full backup taken by this store."""
out = []
for rel in self.store.data.get("full", {}):
src = self.store.full_copy(rel)
dst = os.path.join(self.game.root, rel)
self.game.check_writable(dst)
tmp = dst + ".ackit-tmp"
import shutil
shutil.copy2(src, tmp)
os.replace(tmp, dst)
out.append(rel)
self.store.data["entries"] = {}
self.store.save()
self.store.gc()
return out

221
ackit/wwvorbis.py Normal file
View File

@@ -0,0 +1,221 @@
"""Wwise 2011 ("v56"-style) Vorbis <-> standard Vorbis conversion.
Wwise stores a stripped setup header (codebooks referenced by 10-bit IDs into the
built-in library, narrowed fields) and "modified" audio packets (no packet-type bit,
no prev/next window flags). See docs/wem_format.md.
"""
from .bits import BitReader, BitWriter, ilog
from .codebooks import CodebookLibrary, wwise_book_to_vorbis, skip_vorbis_book, book_bits_key
W2V = "w2v" # Wwise -> Vorbis
V2W = "v2w" # Vorbis -> Wwise
class SetupInfo:
def __init__(self):
self.codebook_ids = []
self.codebook_dims_entries = []
self.mode_blockflags = []
self.floor_count = 0
self.residue_count = 0
self.mapping_count = 0
@property
def mode_bits(self):
return ilog(len(self.mode_blockflags) - 1)
def transcode_setup(src: bytes, direction: str, channels: int, lib: CodebookLibrary) -> (bytes, SetupInfo):
"""Convert a setup packet between the Wwise stripped form and the standard Vorbis form."""
info = SetupInfo()
w = BitWriter()
if direction == W2V:
r = BitReader(src)
w.put(8, 5)
for c in b"vorbis":
w.put(8, c)
else:
if src[:7] != b"\x05vorbis":
raise ValueError("not a vorbis setup packet")
r = BitReader(src, 7 * 8)
def f(ww_bits, vb_bits=None):
"""Transcode one field; returns its value."""
if vb_bits is None:
vb_bits = ww_bits
if direction == W2V:
v = r.get(ww_bits); w.put(vb_bits, v)
else:
v = r.get(vb_bits); w.put(ww_bits, v)
if v >> ww_bits:
raise ValueError("value %d does not fit in %d Wwise bits" % (v, ww_bits))
return v
def const(vb_bits, value, what):
"""Field present only in Vorbis form, with a fixed value."""
if direction == W2V:
w.put(vb_bits, value)
else:
v = r.get(vb_bits)
if v != value:
raise ValueError("%s must be %d for Wwise, got %d" % (what, value, v))
# codebooks
nbooks = f(8) + 1
for _ in range(nbooks):
if direction == W2V:
cid = r.get(10)
if cid >= len(lib.books):
raise ValueError("codebook id %d out of range" % cid)
wwise_book_to_vorbis(lib.books[cid], w)
else:
start = r.pos
de = skip_vorbis_book(r)
info.codebook_dims_entries.append(de)
cid = lib.lookup(book_bits_key(src, start, r.pos))
if cid is None:
raise ValueError("codebook #%d not in game library (dims=%d entries=%d)" % (len(info.codebook_ids), de[0], de[1]))
w.put(10, cid)
info.codebook_ids.append(cid)
# time domain transforms (Vorbis only: count-1=0, one 16-bit zero)
if direction == W2V:
w.put(6, 0); w.put(16, 0)
else:
tc = r.get(6) + 1
for _ in range(tc):
if r.get(16) != 0:
raise ValueError("nonzero time domain value")
# floors
nfloors = f(6) + 1
info.floor_count = nfloors
for _ in range(nfloors):
const(16, 1, "floor type")
parts = f(5)
classes = [f(4) for _ in range(parts)]
maxc = max(classes) if classes else -1
dims = []
for _ in range(maxc + 1):
dims.append(f(3) + 1)
sub = f(2)
if sub:
f(8)
for _ in range(1 << sub):
f(8)
f(2)
rb = f(4)
for c in classes:
for _ in range(dims[c]):
f(rb)
# residues
nres = f(6) + 1
info.residue_count = nres
for _ in range(nres):
rtype = f(2, 16)
if rtype > 2:
raise ValueError("bad residue type")
f(24); f(24); f(24)
ncls = f(6) + 1
f(8)
cascade = []
for _ in range(ncls):
lo = f(3)
hi = f(5) if f(1) else 0
cascade.append(hi * 8 + lo)
for c in cascade:
for k in range(8):
if c & (1 << k):
f(8)
# mappings
nmap = f(6) + 1
info.mapping_count = nmap
for _ in range(nmap):
const(16, 0, "mapping type")
submaps = f(4) + 1 if f(1) else 1
if f(1):
steps = f(8) + 1
b = ilog(channels - 1)
for _ in range(steps):
f(b); f(b)
if f(2) != 0:
raise ValueError("mapping reserved != 0")
if submaps > 1:
for _ in range(channels):
f(4)
for _ in range(submaps):
f(8); f(8); f(8)
# modes
nmodes = f(6) + 1
for _ in range(nmodes):
bf = f(1)
const(16, 0, "window type")
const(16, 0, "transform type")
f(8)
info.mode_blockflags.append(bf)
if direction == W2V:
w.put(1, 1) # framing
else:
if r.get(1) != 1:
raise ValueError("framing bit missing")
return w.bytes(), info
def build_id_header(channels: int, rate: int, bs0_exp: int, bs1_exp: int) -> bytes:
import struct
return (b"\x01vorbis" + struct.pack("<IBIiii", 0, channels, rate, 0, 0, 0)
+ bytes([(bs1_exp << 4) | bs0_exp, 1]))
def build_comment_header(vendor: bytes = b"ackit") -> bytes:
import struct
return b"\x03vorbis" + struct.pack("<I", len(vendor)) + vendor + struct.pack("<I", 0) + b"\x01"
def ww_packet_to_vorbis(pkt: bytes, next_first: int, info: SetupInfo, prev_bf: int,
modified: bool = True) -> (bytes, int):
"""Rebuild a standard audio packet from a Wwise modified packet. Returns (packet, blockflag).
When blocksizes are equal Wwise keeps standard packets (modified=False)."""
mb = info.mode_bits
if not modified:
mode = (pkt[0] >> 1) & ((1 << mb) - 1)
return pkt, info.mode_blockflags[mode]
v = int.from_bytes(pkt, "little")
nbits = len(pkt) * 8
mode = v & ((1 << mb) - 1)
bf = info.mode_blockflags[mode]
out = 0 # packet type bit 0
pos = 1
out |= mode << pos; pos += mb
if bf:
nbf = 0 if next_first is None else info.mode_blockflags[next_first & ((1 << mb) - 1)]
out |= prev_bf << pos; pos += 1
out |= nbf << pos; pos += 1
rest = v >> mb
out |= rest << pos
pos += nbits - mb
return out.to_bytes((pos + 7) // 8, "little"), bf
def vorbis_packet_to_ww(pkt: bytes, info: SetupInfo, modified: bool = True) -> (bytes, int):
"""Strip packet-type bit and window flags. Returns (packet, blockflag)."""
mb = info.mode_bits
if not modified:
if pkt[0] & 1:
raise ValueError("not an audio packet")
return pkt, info.mode_blockflags[(pkt[0] >> 1) & ((1 << mb) - 1)]
v = int.from_bytes(pkt, "little")
nbits = len(pkt) * 8
if v & 1:
raise ValueError("not an audio packet")
mode = (v >> 1) & ((1 << mb) - 1)
bf = info.mode_blockflags[mode]
drop = 1 + mb + (2 if bf else 0)
rest = v >> drop
out = mode | (rest << mb)
obits = nbits - (drop - mb)
return out.to_bytes((obits + 7) // 8, "little"), bf

1
app/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Unheard City (PySide6 GUI on top of ackit)."""

250
app/audioio.py Normal file
View File

@@ -0,0 +1,250 @@
"""Playback and recording with Qt Multimedia (bundled with PySide6; WASAPI on Windows,
PulseAudio/PipeWire on Linux). Audio is exchanged as float32 numpy arrays (frames, channels).
Set ACKIT_NO_AUDIO=1 to disable devices (headless tests); `Recorder.start_simulated` feeds a recording from
an array in real time, so the recording logic is testable without a microphone.
"""
import os
import numpy as np
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QObject, QTimer, Signal
from PySide6.QtMultimedia import QAudio, QAudioFormat, QAudioSink, QAudioSource, QMediaDevices
from ackit import audio
def audio_enabled():
return os.environ.get("ACKIT_NO_AUDIO") != "1"
def output_devices():
return list(QMediaDevices.audioOutputs()) if audio_enabled() else []
def input_devices():
return list(QMediaDevices.audioInputs()) if audio_enabled() else []
def find_device(devices, description):
return next((d for d in devices if d.description() == description), None)
def _to_int16(x):
return (np.clip(x, -1.0, 1.0) * 32767.0).astype("<i2").tobytes()
class Player(QObject):
"""Plays one buffer at a time. Signals: position(seconds), playing(bool), error(str)."""
position = Signal(float)
playing = Signal(bool)
error = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.device = None
self.sink = None
self.buffer = None
self.data = QByteArray()
self.fmt = None
self.base = 0.0
self.duration = 0.0
self.loop = False
self.volume = 1.0
self.label = None
self.timer = QTimer(self)
self.timer.setInterval(40)
self.timer.timeout.connect(self._tick)
def set_device(self, device):
self.stop()
self.device = device
def _device(self):
if self.device is not None and not self.device.isNull():
return self.device
d = QMediaDevices.defaultAudioOutput() if audio_enabled() else None
return d if d is not None and not d.isNull() else None
def play(self, x, rate, start=0.0, loop=False, label=None):
"""x: float32 (n, ch). Converted to the device's format (rate, channels, 16-bit)."""
self.stop()
dev = self._device()
if dev is None:
self.error.emit("No audio output device available.")
return False
pref = dev.preferredFormat()
fmt = QAudioFormat()
fmt.setSampleRate(pref.sampleRate() or 48000)
fmt.setChannelCount(min(2, pref.channelCount() or 2))
fmt.setSampleFormat(QAudioFormat.SampleFormat.Int16)
if not dev.isFormatSupported(fmt):
fmt.setChannelCount(2)
fmt.setSampleRate(48000)
y = audio.to_channels(audio.resample(x, rate, fmt.sampleRate()), fmt.channelCount())
self.fmt, self.loop, self.label = fmt, loop, label
self.data = QByteArray(_to_int16(y))
self.duration = len(y) / float(fmt.sampleRate())
self.sink = QAudioSink(dev, fmt, self)
self.sink.setVolume(self.volume)
self.sink.stateChanged.connect(self._state)
self._start_at(start)
return True
def _start_at(self, seconds):
seconds = max(0.0, min(seconds, self.duration))
if self.buffer is not None:
self.buffer.close()
self.buffer = QBuffer(self)
self.buffer.setData(self.data)
self.buffer.open(QIODevice.OpenModeFlag.ReadOnly)
frame = self.fmt.bytesPerFrame()
self.buffer.seek(int(seconds * self.fmt.sampleRate()) * frame)
self.base = seconds
self.sink.start(self.buffer)
self.timer.start()
self.playing.emit(True)
def seek(self, seconds):
if self.sink is not None:
self.sink.stop()
self._start_at(seconds)
def set_volume(self, v):
self.volume = float(v)
if self.sink is not None:
self.sink.setVolume(self.volume)
def is_playing(self):
return self.sink is not None and self.sink.state() == QAudio.State.ActiveState
def current_time(self):
if self.sink is None:
return 0.0
return self.base + self.sink.processedUSecs() / 1e6
def _tick(self):
self.position.emit(min(self.current_time(), self.duration))
def _state(self, st):
if st == QAudio.State.IdleState: # reached the end of the buffer
if self.loop:
self.sink.stop()
self._start_at(0.0)
else:
self.stop()
def stop(self):
self.timer.stop()
if self.sink is not None:
try:
self.sink.stateChanged.disconnect(self._state)
except (RuntimeError, TypeError):
pass
self.sink.stop()
self.sink.deleteLater()
self.sink = None
self.playing.emit(False)
if self.buffer is not None:
self.buffer.close()
self.buffer = None
class Recorder(QObject):
"""Records from an input device. Signals: level(peak 0..1), elapsed(seconds), error(str)."""
level = Signal(float)
elapsed = Signal(float)
error = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.device = None
self.source = None
self.io = None
self.chunks = []
self.rate = 48000
self.channels = 1
self.fmt_float = False
self._sim = None
def set_device(self, device):
self.device = device
def is_recording(self):
return self.source is not None or self._sim is not None
def start(self):
self.chunks = []
dev = self.device if (self.device is not None and not self.device.isNull()) else (
QMediaDevices.defaultAudioInput() if audio_enabled() else None)
if dev is None or dev.isNull():
self.error.emit("No audio input device available.")
return False
fmt = QAudioFormat()
fmt.setSampleRate(48000)
fmt.setChannelCount(1)
fmt.setSampleFormat(QAudioFormat.SampleFormat.Int16)
if not dev.isFormatSupported(fmt):
fmt = dev.preferredFormat()
self.rate, self.channels = fmt.sampleRate(), fmt.channelCount()
self.fmt_float = fmt.sampleFormat() == QAudioFormat.SampleFormat.Float
self.source = QAudioSource(dev, fmt, self)
self.io = self.source.start()
if self.io is None:
self.error.emit("Could not open the input device.")
self.source = None
return False
self.io.readyRead.connect(self._read)
return True
def _read(self):
if self.io is None:
return
self._ingest(bytes(self.io.readAll()))
def _ingest(self, raw):
if not raw:
return
if self.fmt_float:
x = np.frombuffer(raw, dtype="<f4")
else:
x = np.frombuffer(raw[: len(raw) // 2 * 2], dtype="<i2").astype(np.float32) / 32768.0
x = x[: len(x) // self.channels * self.channels].reshape(-1, self.channels)
self.chunks.append(x)
self.level.emit(float(np.max(np.abs(x))) if len(x) else 0.0)
self.elapsed.emit(sum(len(c) for c in self.chunks) / float(self.rate))
# simulated input (tests / no microphone) ----------------------------------------------------
def start_simulated(self, x, rate, realtime=True):
"""Feed `x` as if it came from a microphone (20 ms blocks)."""
self.chunks = []
self.rate, self.channels, self.fmt_float = rate, x.shape[1], True
blocks = [x[i:i + rate // 50] for i in range(0, len(x), rate // 50)]
if not realtime:
for b in blocks:
self._ingest(np.ascontiguousarray(b, dtype="<f4").tobytes())
return True
self._sim = (QTimer(self), iter(blocks))
def step():
b = next(self._sim[1], None) if self._sim else None
if b is None:
if self._sim:
self._sim[0].stop()
return
self._ingest(np.ascontiguousarray(b, dtype="<f4").tobytes())
self._sim[0].timeout.connect(step)
self._sim[0].start(20)
return True
def stop(self):
"""-> (float32 array (n, ch), rate) of everything recorded."""
if self.source is not None:
self._read()
self.source.stop()
self.source.deleteLater()
self.source = None
self.io = None
if self._sim is not None:
self._sim[0].stop()
self._sim = None
x = np.concatenate(self.chunks) if self.chunks else np.zeros((0, self.channels), np.float32)
return x, self.rate

163
app/dialogs.py Normal file
View File

@@ -0,0 +1,163 @@
"""Settings, mod export and mod import dialogs."""
import os
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QHBoxLayout,
QLabel, QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton, QSlider,
QTreeWidget, QTreeWidgetItem, QVBoxLayout)
from ackit.game import find_installs, is_game_dir
from .audioio import output_devices
def pick_game_dir(parent, start=""):
"""Folder chooser that explains what to pick (also Proton/Linux paths)."""
d = QFileDialog.getExistingDirectory(
parent, "Select the Batman: Arkham City folder (contains BmGame and Binaries)", start or os.path.expanduser("~"))
return d or None
class SettingsDialog(QDialog):
def __init__(self, settings, parent=None):
super().__init__(parent)
self.setWindowTitle("Settings")
self.settings = dict(settings)
f = QFormLayout(self)
self.game = QLineEdit(settings.get("game_dir") or "")
b = QPushButton("Browse…")
b.clicked.connect(self._browse_game)
row = QHBoxLayout()
row.addWidget(self.game, 1)
row.addWidget(b)
found = find_installs()
hint = QLabel("Found via Steam: " + ("; ".join(found) if found else "none (Linux/Proton: "
"~/.steam/steam/steamapps/common/Batman Arkham City GOTY)"))
hint.setWordWrap(True)
f.addRow("Game folder", row)
f.addRow("", hint)
self.store = QLineEdit(settings.get("store_root") or "")
self.store.setPlaceholderText("default: in your user data folder")
b2 = QPushButton("Browse…")
b2.clicked.connect(lambda: self._browse(self.store))
row2 = QHBoxLayout()
row2.addWidget(self.store, 1)
row2.addWidget(b2)
f.addRow("Originals store", row2)
self.full = QCheckBox("Keep one full copy of each audio archive before its first change (safest)")
self.full.setChecked(settings.get("full_backup", True))
f.addRow("", self.full)
self.out = QComboBox()
self.out.addItem("Default output")
for d in output_devices():
self.out.addItem(d.description())
if settings.get("output_device"):
i = self.out.findText(settings["output_device"])
if i >= 0:
self.out.setCurrentIndex(i)
f.addRow("Playback device", self.out)
# appearance
self.bg_on = QCheckBox("Show the Arkham City wallpaper behind the filters")
self.bg_on.setChecked(settings.get("background_enabled", True))
f.addRow("Background", self.bg_on)
self.opacity = QSlider(Qt.Orientation.Horizontal)
self.opacity.setRange(0, 100)
self.opacity.setValue(int(round(float(settings.get("background_opacity", 0.1)) * 100)))
f.addRow("Background strength", self.opacity)
self.bg_on.toggled.connect(self.opacity.setEnabled)
self.opacity.setEnabled(self.bg_on.isChecked())
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
f.addRow(bb)
self.resize(640, 0)
def _browse_game(self):
d = pick_game_dir(self, self.game.text())
if d:
self.game.setText(d)
def _browse(self, edit):
d = QFileDialog.getExistingDirectory(self, "Choose folder", edit.text())
if d:
edit.setText(d)
def values(self):
s = dict(self.settings)
gd = self.game.text().strip()
s["game_dir"] = gd if is_game_dir(gd) else s.get("game_dir")
s["store_root"] = self.store.text().strip() or None
s["full_backup"] = self.full.isChecked()
s["output_device"] = self.out.currentText() if self.out.currentIndex() > 0 else None
s["background_enabled"] = self.bg_on.isChecked()
s["background_opacity"] = self.opacity.value() / 100.0
return s
class ExportModDialog(QDialog):
"""Pick which modified sounds go into a .acmod package."""
def __init__(self, items, parent=None):
"""items: [(key, name)]"""
super().__init__(parent)
self.setWindowTitle("Export mod package")
lay = QVBoxLayout(self)
f = QFormLayout()
self.title = QLineEdit()
self.author = QLineEdit()
self.desc = QPlainTextEdit()
self.desc.setMaximumHeight(70)
f.addRow("Title", self.title)
f.addRow("Author", self.author)
f.addRow("Description", self.desc)
lay.addLayout(f)
lay.addWidget(QLabel("Sounds to include:"))
self.list = QListWidget()
for key, name in items:
it = QListWidgetItem(name)
it.setData(Qt.ItemDataRole.UserRole, key)
it.setFlags(it.flags() | Qt.ItemFlag.ItemIsUserCheckable)
it.setCheckState(Qt.CheckState.Checked)
self.list.addItem(it)
lay.addWidget(self.list)
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
lay.addWidget(bb)
self.resize(520, 480)
def selected_keys(self):
return [self.list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.list.count())
if self.list.item(i).checkState() == Qt.CheckState.Checked]
class ImportModDialog(QDialog):
"""Shows how each item of a package matches this game before installing."""
def __init__(self, manifest, items, parent=None):
super().__init__(parent)
self.setWindowTitle("Import mod package")
lay = QVBoxLayout(self)
head = "<b>%s</b>" % (manifest.get("name") or "Mod package")
if manifest.get("author"):
head += " by %s" % manifest["author"]
lab = QLabel(head + ("<br>" + manifest["description"] if manifest.get("description") else ""))
lab.setWordWrap(True)
lay.addWidget(lab)
self.tree = QTreeWidget()
self.tree.setHeaderLabels(["Sound", "Match", "Note"])
n_ok = 0
for im in items:
note = {"exact": "same sound", "by name": "matched by name (other language) - will be re-encoded",
"missing": im.note}[im.how]
QTreeWidgetItem(self.tree, [im.item.get("name") or im.item["target"], im.how, note])
n_ok += im.how != "missing"
self.tree.resizeColumnToContents(0)
lay.addWidget(self.tree)
lay.addWidget(QLabel("%d of %d sounds will be installed." % (n_ok, len(items))))
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
bb.button(QDialogButtonBox.StandardButton.Ok).setText("Install")
bb.button(QDialogButtonBox.StandardButton.Ok).setEnabled(n_ok > 0)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
lay.addWidget(bb)
self.resize(720, 420)

526
app/editor.py Normal file
View File

@@ -0,0 +1,526 @@
"""Single-sound editor: listen, substitute (file or recording), process, apply / revert."""
import os
import numpy as np
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox,
QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QSlider,
QVBoxLayout, QWidget)
from ackit import audio, workspace as W
from . import icons, theme
from .audioio import Recorder, input_devices, find_device
from .spinner import Spinner
from .waveform import Waveform
AUDIO_FILTER = "Audio files (*.wav *.flac *.ogg *.oga *.opus *.mp3 *.aif *.aiff *.m4a *.aac *.wma);;All files (*)"
STATE_TEXT = {W.ORIGINAL: "Original", W.MODIFIED: "Modified (your replacement is installed)",
W.LOST: "Your change was undone outside the tool (e.g. Steam 'verify files')",
W.CHANGED: "Changed outside this tool (Steam update or another mod tool)",
W.FOREIGN: "Differs from the original game file (modified by another tool?)"}
def fmt_s(t):
return "%d:%05.2f" % (int(t // 60), t % 60)
class EditorPanel(QWidget):
applied = Signal(str) # sound key
reverted = Signal(str)
message = Signal(str)
def __init__(self, ctx, parent=None):
super().__init__(parent)
self.ctx = ctx # main window: .ws, .runner, .player, .settings
self.sound = None
self.orig = None # (x, rate)
self.installed = None
self.src = None # replacement source (x, rate) before processing
self.src_name = ""
self.playing_what = None
self.recorder = Recorder(self)
self._build()
self.set_sound(None)
# --- UI --------------------------------------------------------------------------------------
def _btn(self, text, icon=None, slot=None, tip=None, color=None):
b = QPushButton(text)
if icon is not None:
b.setIcon(icons.icon(icon, color or icons.TEXT))
if slot:
b.clicked.connect(slot)
if tip:
b.setToolTip(tip)
return b
def _wave(self, kind, selectable=False):
w = Waveform(selectable=selectable)
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
return w
def _build(self):
self.setObjectName("EditorPanel")
lay = QVBoxLayout(self)
lay.setContentsMargins(10, 8, 10, 8)
self.title = QLabel()
self.title.setObjectName("EditorTitle")
self.title.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
f = self.title.font()
f.setPointSizeF(f.pointSizeF() * 1.35)
f.setBold(True)
f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 104)
self.title.setFont(f)
lay.addWidget(self.title)
self.meta = QLabel()
self.meta.setWordWrap(True)
self.meta.setObjectName("Muted")
self.state = QLabel()
self.state.setWordWrap(True)
lay.addWidget(self.meta)
lay.addWidget(self.state)
# listen ----------------------------------------------------------------------------------
g = QGroupBox("LISTEN")
gl = QGridLayout(g)
self.wf_orig = self._wave("orig")
self.wf_inst = self._wave("inst")
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
self.b_ab = self._btn("A/B", "ab", self.ab_switch, "Switch between original and replacement at the same position",
icons.ACCENT)
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
self.cb_loop.setCheckable(True)
self.vol_icon = QLabel()
self.vol_icon.setPixmap(icons.icon("play", icons.MUTED).pixmap(14, 14))
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
self.sl_vol.setRange(0, 100)
self.sl_vol.setValue(80)
self.sl_vol.setMaximumWidth(120)
self.sl_vol.setToolTip("Volume")
self.sl_vol.valueChanged.connect(lambda v: self.ctx.player.set_volume(v / 100.0))
self.lbl_time = QLabel("0:00.00")
self.lbl_time.setObjectName("Muted")
gl.addWidget(QLabel("Original"), 0, 0)
gl.addWidget(self.wf_orig, 0, 1, 1, 6)
self.lbl_inst = QLabel("Installed")
gl.addWidget(self.lbl_inst, 1, 0)
gl.addWidget(self.wf_inst, 1, 1, 1, 6)
row = QHBoxLayout()
for w in (self.b_play_orig, self.b_play_inst, self.b_stop, self.b_ab, self.cb_loop, QLabel(" Volume"),
self.sl_vol, self.lbl_time):
row.addWidget(w)
row.addStretch(1)
gl.addLayout(row, 2, 0, 1, 7)
gl.setColumnStretch(1, 1)
lay.addWidget(g)
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
# replace ---------------------------------------------------------------------------------
g = QGroupBox("REPLACE")
rl = QVBoxLayout(g)
src = QHBoxLayout()
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
self.cmb_in = QComboBox()
self.cmb_in.setMinimumWidth(180)
self.cmb_in.setToolTip("Recording device")
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
self.meter = QProgressBar()
self.meter.setRange(0, 100)
self.meter.setTextVisible(False)
self.meter.setMaximumWidth(120)
self.meter.setToolTip("Input level")
self.lbl_rec = QLabel("")
self.lbl_rec.setObjectName("Muted")
for w in (self.b_load, QLabel(" or"), self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
src.addWidget(w)
src.addStretch(1)
rl.addLayout(src)
self.wf_new = self._wave("new", selectable=True)
self.wf_new.setToolTip("Click: play from here. Right-drag (or Shift+drag): select the part to keep.")
self.wf_new.seekRequested.connect(lambda t: self.play("new", t))
self.wf_new.selectionChanged.connect(self._selection)
rl.addWidget(self.wf_new)
proc = QHBoxLayout()
self.sp_a = QDoubleSpinBox()
self.sp_b = QDoubleSpinBox()
for s_ in (self.sp_a, self.sp_b):
s_.setDecimals(2)
s_.setSuffix(" s")
s_.setRange(0, 36000)
s_.valueChanged.connect(self._changed)
self.sp_gain = QDoubleSpinBox()
self.sp_gain.setRange(-40, 40)
self.sp_gain.setSuffix(" dB")
self.sp_gain.valueChanged.connect(self._changed)
self.cb_norm = QCheckBox("Normalize")
self.cb_match = QCheckBox("Match original loudness")
self.cb_norm.toggled.connect(self._changed)
self.cb_match.toggled.connect(self._changed)
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
icons.ACCENT)
for w in (QLabel("Keep from"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, QLabel(" Gain"),
self.sp_gain, self.cb_norm, self.cb_match, self.b_play_new):
proc.addWidget(w)
proc.addStretch(1)
rl.addLayout(proc)
info = QHBoxLayout()
self.lbl_new = QLabel()
self.lbl_new.setWordWrap(True)
self.lbl_new.setObjectName("Muted")
self.warn_icon = QLabel()
self.warn_icon.setPixmap(icons.icon("warning", icons.ACCENT).pixmap(18, 18))
self.warn_icon.hide()
self.lbl_warn = QLabel()
self.lbl_warn.setWordWrap(True)
self.lbl_warn.setStyleSheet("color: %s;" % icons.ACCENT)
self.lbl_new.setWordWrap(False)
info.addWidget(self.lbl_new, 0, Qt.AlignmentFlag.AlignTop)
info.addSpacing(12)
info.addWidget(self.warn_icon, 0, Qt.AlignmentFlag.AlignTop)
info.addWidget(self.lbl_warn, 1)
rl.addLayout(info)
lay.addWidget(g)
# apply -----------------------------------------------------------------------------------
act = QHBoxLayout()
self.b_apply = self._btn("Apply to game", "apply", self.apply,
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
self.b_apply.setObjectName("Primary")
self.b_revert = self._btn("Revert to original", "revert", self.revert, "Put the game's original back")
self.b_export = self._btn("Save as WAV…", "save", self.export_wav,
"Save the original (or the replacement) as a WAV file")
self.busy = Spinner(16)
self.lbl_busy = QLabel()
for w in (self.b_apply, self.b_revert, self.b_export, self.busy, self.lbl_busy):
act.addWidget(w)
act.addStretch(1)
lay.addLayout(act)
lay.addStretch(1)
self.recorder.level.connect(lambda v: self.meter.setValue(int(min(1.0, v) * 100)))
self.recorder.elapsed.connect(lambda t: self.lbl_rec.setText("recording %s" % fmt_s(t)))
self.recorder.error.connect(lambda m: self.inform("Recording", m))
self.ctx.player.position.connect(self._pos)
self.ctx.player.playing.connect(lambda on: None if on else self._clear_playheads())
self._reproc = QTimer(self)
self._reproc.setSingleShot(True)
self._reproc.setInterval(150)
self._reproc.timeout.connect(self._update_new)
def refresh_devices(self):
cur = self.ctx.settings.get("input_device")
self.cmb_in.clear()
self.cmb_in.addItem("Default input")
for d in input_devices():
self.cmb_in.addItem(d.description())
if cur:
i = self.cmb_in.findText(cur)
if i >= 0:
self.cmb_in.setCurrentIndex(i)
# --- dialogs (overridable in tests) -----------------------------------------------------------
def confirm(self, title, text):
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
def inform(self, title, text):
QMessageBox.information(self, title, text)
def ask_open(self):
p, _ = QFileDialog.getOpenFileName(self, "Choose replacement audio", self.ctx.settings.get("last_dir") or "",
AUDIO_FILTER)
return p
def ask_save(self, default):
p, _ = QFileDialog.getSaveFileName(self, "Save as WAV", default, "WAV (*.wav)")
return p
# --- loading ---------------------------------------------------------------------------------
def set_sound(self, s):
self.stop()
if self.recorder.is_recording():
self.recorder.stop()
self.sound, self.orig, self.installed, self.src = s, None, None, None
enabled = s is not None
for w in self.findChildren(QPushButton) + [self.sp_a, self.sp_b, self.sp_gain, self.cb_norm, self.cb_match]:
w.setEnabled(enabled)
self.wf_orig.set_audio(None, 1, "select a sound" if s is None else "loading…", busy=s is not None)
self.wf_inst.set_audio(None, 1, "")
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
self.lbl_new.setText("")
if s is None:
self.title.setText("No sound selected")
self.meta.setText("Search and select a sound in the list.")
self.state.setText("")
return
self.title.setText(s.name)
who = s.character or (("mentions " + s.characters_inferred) if s.characters_inferred else "—")
lim = self.ctx.ws.limit(s)
self.meta.setText("%s / %s • %s • %.2f s, %d Hz, %s • max size: %s • %s"
% (s.group, s.category, who, s.seconds, s.rate,
{1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels),
"any" if lim is None else "%.1f KB" % (lim / 1024.0), s.key))
key = s.key
ws = self.ctx.ws
def work(job):
cur = ws.current_bytes(s)
st = ws.state(s, cur)
o = audio.decode_wem(ws.original_bytes(s))
inst = audio.decode_wem(cur) if st != W.ORIGINAL else None
return key, st, o, inst
self.ctx.runner.start(work, self._loaded, lambda e, tb: self._failed("Loading the sound failed", e))
def _loaded(self, r):
key, st, o, inst = r
if not self.sound or self.sound.key != key:
return
self.orig, self.installed = o, inst
self.wf_orig.set_audio(*o)
self.wf_inst.set_audio(*(inst or (None, 1)), placeholder="same as original")
self.b_play_inst.setEnabled(inst is not None)
self.b_revert.setEnabled(st != W.ORIGINAL or bool(self.ctx.ws.store.get(key)))
self.state.setText("State: " + STATE_TEXT.get(st, st))
self._changed()
# --- replacement source ----------------------------------------------------------------------
def load_file_dialog(self):
p = self.ask_open()
if p:
self.ctx.settings["last_dir"] = os.path.dirname(p)
self.load_file(p)
def load_file(self, path):
"""Decode in the background (spinner in the replacement waveform); False if it already failed."""
name = os.path.basename(path)
self.wf_new.set_audio(None, 1, "loading %s…" % name, busy=True)
result = []
def done(r):
result.append(True)
self.set_source(r[0], r[1], name)
def failed(e, tb):
result.append(False)
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
if isinstance(e, audio.AudioError):
self.inform("Unsupported audio", str(e))
else:
self._failed("Loading the file failed", e)
self.ctx.runner.start(lambda job: audio.load(path), done, failed)
return result[0] if result else True
def set_source(self, x, sr, name):
self.src, self.src_name = (np.asarray(x, np.float32), sr), name
dur = audio.duration(x, sr)
self.sp_a.blockSignals(True)
self.sp_b.blockSignals(True)
self.sp_a.setMaximum(dur)
self.sp_b.setMaximum(dur)
self.sp_a.setValue(0)
self.sp_b.setValue(dur)
self.sp_a.blockSignals(False)
self.sp_b.blockSignals(False)
self._update_new()
def toggle_record(self):
if self.recorder.is_recording():
x, sr = self.recorder.stop()
self.b_rec.setText("Record")
self.b_rec.setIcon(icons.icon("record"))
self.meter.setValue(0)
if len(x) < sr // 10:
self.lbl_rec.setText("nothing recorded")
return
self.lbl_rec.setText("recorded %s" % fmt_s(len(x) / sr))
self.set_source(x, sr, "recording")
return
name = self.cmb_in.currentText()
self.recorder.set_device(find_device(input_devices(), name) if self.cmb_in.currentIndex() > 0 else None)
self.ctx.settings["input_device"] = name if self.cmb_in.currentIndex() > 0 else None
if self.recorder.start():
self.stop()
self.b_rec.setText("Stop recording")
self.b_rec.setIcon(icons.icon("stop", icons.DANGER))
def _selection(self, a, b):
if b > a:
self.sp_a.setValue(a)
self.sp_b.setValue(b)
def trim_silence(self):
if not self.src:
return
x, sr = self.src
y = audio.trim_silence(x, rate=sr)
self.set_source(y, sr, self.src_name)
def _changed(self, *_):
self._reproc.start()
def processed(self):
"""Replacement audio after trim / gain / normalize / loudness matching -> (x, rate) or None."""
if not self.src:
return None
x, sr = self.src
a, b = self.sp_a.value(), self.sp_b.value()
if b > a:
x = audio.trim(x, sr, a, b)
if self.cb_match.isChecked() and self.orig is not None:
x = audio.match_loudness(x, self.orig[0])
if self.cb_norm.isChecked():
x = audio.normalize(x)
if self.sp_gain.value():
x = audio.gain(x, self.sp_gain.value())
return x, sr
def _update_new(self):
p = self.processed()
if p is None:
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
self.lbl_new.setText("")
self._warn([])
return
x, sr = p
self.wf_new.set_audio(x, sr)
dur = audio.duration(x, sr)
s = self.sound
self.lbl_new.setText("%s: %.2f s, peak %.1f dBFS" % (self.src_name, dur, audio.peak_db(x)))
warnings = []
if s and not s.is_pack_media and dur > s.seconds * 1.15 + 0.25:
warnings.append("Longer than the original (%.2f s): it will be squeezed into the original's size "
"(lower quality) or refused, and the game may cut it off." % s.seconds)
if len(x) and np.max(np.abs(x)) >= 0.999:
warnings.append("Clipping: lower the gain or use Normalize.")
self._warn(warnings)
def _warn(self, warnings):
self.warn_icon.setVisible(bool(warnings))
self.lbl_warn.setText(" ".join(warnings))
# --- playback --------------------------------------------------------------------------------
def _audio_for(self, what):
return {"orig": self.orig, "inst": self.installed, "new": self.processed()}.get(what)
def play(self, what, start=0.0):
a = self._audio_for(what)
if not a:
return
self.playing_what = what
self.ctx.player.play(a[0], a[1], start, loop=self.cb_loop.isChecked(), label=what)
def toggle_play(self):
if self.ctx.player.is_playing():
self.stop()
else:
self.play("new" if self.src else "orig")
def ab_switch(self):
t = self.ctx.player.current_time() if self.ctx.player.is_playing() else 0.0
nxt = "orig" if self.playing_what in ("new", "inst") else ("new" if self.src else "inst")
if self._audio_for(nxt):
self.play(nxt, t)
def stop(self):
self.ctx.player.stop()
self._clear_playheads()
def _clear_playheads(self):
for w in (self.wf_orig, self.wf_inst, self.wf_new):
w.set_playhead(None)
def _pos(self, t):
self.lbl_time.setText(fmt_s(t))
wf = {"orig": self.wf_orig, "inst": self.wf_inst, "new": self.wf_new}.get(self.playing_what)
if wf:
wf.set_playhead(t)
# --- apply / revert --------------------------------------------------------------------------
def _busy(self, msg):
self.busy.setVisible(bool(msg))
self.lbl_busy.setText(msg or "")
self.b_apply.setEnabled(not msg and self.sound is not None)
self.b_revert.setEnabled(not msg and self.sound is not None)
def _failed(self, what, e):
self._busy(None)
if isinstance(e, Exception) and str(e) == "cancelled":
return
self.inform(what, str(e))
def apply(self, force=False):
s = self.sound
p = self.processed()
if s is None:
return
if p is None:
self.inform("Nothing to apply", "Load an audio file or record a replacement first.")
return
x, sr = p
if not s.is_pack_media and audio.duration(x, sr) > s.seconds * 1.15 + 0.25 and not force:
if not self.confirm("Longer than the original",
"Your audio is %.2f s, the original %.2f s.\nIt must fit in the original's size, "
"so it will be encoded at lower quality (or refused), and the game may cut it "
"off.\n\nApply anyway?" % (audio.duration(x, sr), s.seconds)):
return
ws = self.ctx.ws
meta = {"source": self.src_name}
def work(job):
job.report("encoding…")
wem, info = ws.prepare(s, x, sr, progress=job.report)
job.report("writing game files…")
ws.apply([(s, wem, meta)], force=force, progress=job.report)
return info
def done(info):
self._busy(None)
note = " (shrunk to fit: %d Hz, quality %.1f)" % (info["rate"], info["quality"]) if "shrunk_from" in info else \
" (%d Hz, quality %.1f)" % (info["rate"], info["quality"])
self.message.emit("Applied %s%s" % (s.name, note))
self.applied.emit(s.key)
self.set_sound(s)
def failed(e, tb):
if isinstance(e, W.ConflictError):
self._busy(None)
if self.confirm("Changed outside this tool", str(e) + "\n\nOverwrite it with your replacement?"):
self.apply(force=True)
return
self._failed("Could not apply", e)
self._busy("encoding…")
self.ctx.runner.start(work, done, failed, lambda m: self.lbl_busy.setText(m))
def revert(self):
s = self.sound
if s is None:
return
ws = self.ctx.ws
def work(job):
return ws.revert([s], progress=job.report)
def done(r):
self._busy(None)
self.message.emit("Reverted %s to the original" % s.name if r else "%s is already original" % s.name)
self.reverted.emit(s.key)
self.set_sound(s)
self._busy("reverting…")
self.ctx.runner.start(work, done, lambda e, tb: self._failed("Could not revert", e))
def export_wav(self):
if not self.orig:
return
which = self.processed() if self.src else self.orig
p = self.ask_save(os.path.join(self.ctx.settings.get("last_dir") or "", self.sound.name + ".wav"))
if p:
audio.save_wav(p, which[0], which[1])
self.message.emit("Saved %s" % p)

187
app/icons.py Normal file
View File

@@ -0,0 +1,187 @@
"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji).
`icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert,
save, export, import, settings, star, star-off, warning, ab, loop, search, wave.
`app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images).
"""
import math
import os
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
ACCENT = "#b0242c"
STEEL = "#c4c4c4"
TEXT = "#e6e6e6"
DANGER = "#e0533d"
MUTED = "#9a9a9a"
_cache = {}
def _arrow_arc(p, r, start, span, head=True):
rect = QRectF(-r, -r, 2 * r, 2 * r)
path = QPainterPath()
path.arcMoveTo(rect, start)
path.arcTo(rect, start, span)
p.drawPath(path)
if head:
a = math.radians(start + span)
tip = QPointF(r * math.cos(a), -r * math.sin(a))
d = 1 if span > 0 else -1
tang = math.radians(start + span + 90 * d)
back = QPointF(math.cos(tang), -math.sin(tang))
nrm = QPointF(math.cos(a), -math.sin(a))
L = r * 0.55
p.drawLine(tip, tip - back * L + nrm * L * 0.6)
p.drawLine(tip, tip - back * L - nrm * L * 0.6)
def _draw(p, name, c, s):
"""Draw icon `name` in a box of size s (origin top-left)."""
pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
p.setPen(pen)
p.setBrush(Qt.BrushStyle.NoBrush)
m = s * 0.18
if name == "play":
path = QPainterPath()
path.moveTo(s * 0.30, s * 0.20)
path.lineTo(s * 0.80, s * 0.50)
path.lineTo(s * 0.30, s * 0.80)
path.closeSubpath()
p.setBrush(QColor(c))
p.drawPath(path)
elif name == "stop":
p.setBrush(QColor(c))
p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06)
elif name == "record":
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor(DANGER))
p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56))
elif name == "open":
path = QPainterPath()
path.moveTo(m, s * 0.30)
path.lineTo(s * 0.40, s * 0.30)
path.lineTo(s * 0.48, s * 0.38)
path.lineTo(s - m, s * 0.38)
path.lineTo(s - m, s * 0.78)
path.lineTo(m, s * 0.78)
path.closeSubpath()
p.drawPath(path)
elif name == "apply":
path = QPainterPath()
path.moveTo(s * 0.20, s * 0.52)
path.lineTo(s * 0.42, s * 0.74)
path.lineTo(s * 0.82, s * 0.28)
p.drawPath(path)
elif name in ("revert", "loop"):
p.translate(s / 2, s / 2)
if name == "revert":
_arrow_arc(p, s * 0.30, 60, 270)
else:
_arrow_arc(p, s * 0.30, 20, 150)
_arrow_arc(p, s * 0.30, 200, 150)
p.translate(-s / 2, -s / 2)
elif name == "save": # disk
path = QPainterPath()
path.moveTo(m, m)
path.lineTo(s * 0.70, m)
path.lineTo(s - m, s * 0.30)
path.lineTo(s - m, s - m)
path.lineTo(m, s - m)
path.closeSubpath()
p.drawPath(path)
p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20))
p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26))
elif name in ("export", "import"):
p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m))
p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m))
p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62))
up = name == "export"
a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66)
p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b))
d = -1 if up else 1
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16))
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16))
elif name == "settings":
p.translate(s / 2, s / 2)
path = QPainterPath()
teeth = 8
for i in range(teeth * 2):
a = math.pi * i / teeth
r = s * (0.36 if i % 2 == 0 else 0.27)
pt = QPointF(r * math.cos(a), r * math.sin(a))
path.moveTo(pt) if i == 0 else path.lineTo(pt)
path.closeSubpath()
p.drawPath(path)
p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11)
p.translate(-s / 2, -s / 2)
elif name in ("star", "star-off"):
path = QPainterPath()
for i in range(10):
a = -math.pi / 2 + math.pi * i / 5
r = s * (0.40 if i % 2 == 0 else 0.17)
pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a))
path.moveTo(pt) if i == 0 else path.lineTo(pt)
path.closeSubpath()
if name == "star":
p.setBrush(QColor(c))
p.drawPath(path)
elif name == "warning":
path = QPainterPath()
path.moveTo(s / 2, s * 0.14)
path.lineTo(s * 0.88, s * 0.84)
path.lineTo(s * 0.12, s * 0.84)
path.closeSubpath()
p.drawPath(path)
p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60))
p.drawPoint(QPointF(s / 2, s * 0.72))
elif name == "ab": # compare: half-filled circle
rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64)
half = QPainterPath()
half.moveTo(s / 2, s * 0.18)
half.arcTo(rect, 90, 180)
half.closeSubpath()
p.setBrush(QColor(c))
p.drawPath(half)
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(rect)
elif name == "search":
p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46))
p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84))
elif name == "wave": # fallback app glyph: waveform bars
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor(c))
for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)):
p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03)
def icon(name, color=TEXT):
key = (name, color)
if key not in _cache:
ic = QIcon()
for sz in (16, 20, 24, 32, 48, 64, 128, 256):
pm = QPixmap(sz, sz)
pm.fill(Qt.GlobalColor.transparent)
p = QPainter(pm)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
_draw(p, name, color, sz)
p.end()
ic.addPixmap(pm)
_cache[key] = ic
return _cache[key]
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
def asset(name):
p = os.path.join(ASSETS, name)
return p if os.path.exists(p) else None
def app_icon():
"""The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing."""
p = asset("app_icon.ico") or asset("app_icon.png")
return QIcon(p) if p else icon("wave", ACCENT)

91
app/main.py Normal file
View File

@@ -0,0 +1,91 @@
"""Unheard City - entry point.
UnheardCity [--game DIR] start the editor
UnheardCity --smoke-test [--game DIR] headless self-test of the bundled pipeline (no window, never
writes the game: injects into a temporary copy of a WAD)
UnheardCity --quit-after 5 --screenshot shot.png (start, wait, screenshot, quit; for build checks)
"""
import argparse
import os
import sys
def smoke_test(game_dir=None, report=None):
"""Prove the packaged app can decode, encode and inject without any external program."""
lines = []
def print(*a): # noqa: A001 - also captured into the report (windowed builds have no console)
lines.append(" ".join(str(x) for x in a))
if sys.stdout:
sys.stdout.write(lines[-1] + "\n")
if report:
with open(report, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
import shutil
import tempfile
import numpy as np
from ackit import audio, settings
from ackit.game import Game, default_game_dir
from ackit.inject import inject, read_target
from ackit.workspace import Workspace
gd = game_dir or default_game_dir(settings.load())
print("ffmpeg on PATH:", shutil.which("ffmpeg"), "| vgmstream on PATH:", shutil.which("vgmstream-cli"))
if not gd:
print("no game found; testing the codec only")
from ackit.encoder import encode
t = np.arange(48000) / 48000.0
x = (0.3 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)[:, None]
wem, info = encode(x, 48000, 24000, 1)
y, sr = audio.decode_wem(wem)
print("codec OK: %d bytes, decoded %.2f s at %d Hz" % (len(wem), len(y) / sr, sr))
return 0
with tempfile.TemporaryDirectory() as td:
ws = Workspace(Game(gd), store_root=os.path.join(td, "store"))
s = next(x for x in ws.catalog.sounds if x.group == "Speech" and 1.0 < x.seconds < 4.0)
x, sr = audio.decode_wem(ws.original_bytes(s))
wem, info = ws.prepare(s, x[::-1].copy(), sr)
out = os.path.join(td, os.path.basename(ws.wad_path(s)))
inject(ws.wad_path(s), {s.target: wem}, {s.target: ws.limit(s)}, out_path=out)
assert read_target(out, s.target) == wem
print("pipeline OK: %s (%s), %d sounds loaded, re-encoded %d bytes (limit %d), injected into a temp copy"
% (s.name, s.archive, len(ws.catalog.sounds), len(wem), ws.limit(s)))
return 0
def main(argv=None):
ap = argparse.ArgumentParser(prog="UnheardCity")
ap.add_argument("--game", help="Arkham City folder")
ap.add_argument("--smoke-test", action="store_true")
ap.add_argument("--quit-after", type=float)
ap.add_argument("--screenshot")
ap.add_argument("--report", help="write the smoke-test output to this file")
a, rest = ap.parse_known_args(argv)
if a.smoke_test:
return smoke_test(a.game, a.report)
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication
from ackit import settings as settings_mod
from . import icons, theme
from .mainwindow import MainWindow
app = QApplication([sys.argv[0]] + rest)
app.setApplicationName("Unheard City")
app.setOrganizationName("UnheardCity")
app.setWindowIcon(icons.app_icon())
st = settings_mod.load()
theme.apply(app)
w = MainWindow(settings=st, game_dir=a.game)
w.show()
if a.quit_after:
def finish():
if a.screenshot:
w.grab().save(a.screenshot)
app.quit()
QTimer.singleShot(int(a.quit_after * 1000), finish)
return app.exec()
if __name__ == "__main__":
sys.exit(main())

640
app/mainwindow.py Normal file
View File

@@ -0,0 +1,640 @@
"""Main window: filters + sound table (browser) on top, single-sound editor below."""
import os
from PySide6.QtCore import QByteArray, QEvent, QItemSelectionModel, QSize, Qt, QTimer
from PySide6.QtGui import QFont, QKeySequence, QShortcut
from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QFileDialog, QFormLayout,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow, QMenu, QMessageBox,
QPushButton, QScrollArea, QSplitter, QTableView, QToolBar, QVBoxLayout, QWidget)
from ackit import settings as settings_mod, workspace as W
from ackit.game import Game, GameError, default_game_dir
from ackit.modpack import export_mod, match_mod, prepare_import
from . import icons, theme
from .audioio import Player, find_device, output_devices
from .dialogs import ExportModDialog, ImportModDialog, SettingsDialog, pick_game_dir
from .editor import EditorPanel
from .models import C_FAV, C_NAME, COLS, SoundFilter, SoundModel
from .spinner import Spinner
from .userdata import UserData
from .workers import Runner
SIDEBAR_MIN, SIDEBAR_MAX = 220, 320
TABLE_MIN = 160 # the sound list never gets smaller than this when fitting the editor
ANY = "Any"
class MainWindow(QMainWindow):
def __init__(self, settings=None, runner=None, game_dir=None):
super().__init__()
self.setWindowTitle("Unheard City")
self.settings = settings if settings is not None else settings_mod.load()
self.runner = runner or Runner()
self.player = Player(self)
self.player.error.connect(lambda m: self.statusBar().showMessage(m, 5000))
self.userdata = UserData()
self.ws = None
self.loading = False
self._build()
self._apply_output_device()
geo = self.settings.get("window_geometry")
if geo:
self.restoreGeometry(QByteArray.fromHex(geo.encode()))
else:
self.resize(1400, 900)
gd = game_dir or default_game_dir(self.settings)
if gd:
QTimer.singleShot(0, lambda: self.load_game(gd, self.settings.get("language")))
else:
self.show_no_game()
# --- UI ---------------------------------------------------------------------------------------
def _build(self):
self.setWindowIcon(icons.app_icon())
tb = QToolBar("Main")
tb.setMovable(False)
tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
tb.setIconSize(QSize(18, 18))
self.addToolBar(tb)
logo = QLabel()
logo.setPixmap(icons.app_icon().pixmap(28, 28))
tb.addWidget(logo)
title = QLabel("UNHEARD CITY")
title.setObjectName("AppTitle")
f = title.font()
f.setBold(True)
f.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 1.6)
title.setFont(f)
tb.addWidget(title)
tb.addSeparator()
self.cmb_lang = QComboBox()
self.cmb_lang.setToolTip("Speech language archive to browse")
self.cmb_lang.activated.connect(lambda _: self.load_game(self.ws.game.root, self.cmb_lang.currentText())
if self.ws else None)
tb.addWidget(QLabel(" Language "))
tb.addWidget(self.cmb_lang)
tb.addSeparator()
self.act_revert_all = tb.addAction(icons.icon("revert"), "Revert all", self.revert_all)
self.act_revert_all.setToolTip("Put every original sound back")
tb.addAction(icons.icon("export"), "Export mod…", self.export_mod_dialog)
tb.addAction(icons.icon("import"), "Import mod…", self.import_mod_dialog)
tb.addSeparator()
tb.addAction(icons.icon("settings"), "Settings…", self.settings_dialog)
# banner for problems (changed files, missing game)
self.banner = QWidget()
self.banner.setObjectName("Banner")
self.banner.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
bl = QHBoxLayout(self.banner)
bl.setContentsMargins(8, 4, 8, 4)
warn = QLabel()
warn.setPixmap(icons.icon("warning", icons.ACCENT).pixmap(20, 20))
bl.addWidget(warn, 0, Qt.AlignmentFlag.AlignTop)
self.banner_text = QLabel()
self.banner_text.setWordWrap(True)
self.b_reapply = QPushButton("Re-apply my changes")
self.b_forget = QPushButton("Forget them")
self.b_choose = QPushButton("Choose game folder…")
self.b_reapply.clicked.connect(self.reapply_lost)
self.b_forget.clicked.connect(self.forget_problems)
self.b_choose.clicked.connect(self.choose_game)
bl.addWidget(self.banner_text, 1)
for b in (self.b_reapply, self.b_forget, self.b_choose):
bl.addWidget(b)
self.banner.hide()
# filters
side = theme.Sidebar()
side.configure(self.settings)
self.sidebar = side
side.setMinimumWidth(SIDEBAR_MIN)
side.setMaximumWidth(SIDEBAR_MAX) # opens at the maximum (wallpaper fully visible); can only shrink
fl = QFormLayout(side)
self.search = QLineEdit()
self.search.setPlaceholderText("Search names, characters, notes… (Ctrl+F)")
self.search.setClearButtonEnabled(True)
self.search.addAction(icons.icon("search", icons.MUTED), QLineEdit.ActionPosition.LeadingPosition)
self._search_timer = QTimer(self)
self._search_timer.setSingleShot(True)
self._search_timer.setInterval(120)
self._search_timer.timeout.connect(lambda: self._filter_text())
self.search.textChanged.connect(lambda _: self._search_timer.start())
self.cmb_group = QComboBox()
self.cmb_cat = QComboBox()
self.cmb_char = QComboBox()
self.cmb_char.setMaxVisibleItems(25)
self.cb_inferred = QCheckBox("Include characters mentioned in names")
self.cmb_arch = QComboBox()
self.cb_mod = QCheckBox("Modified only")
self.cb_fav = QCheckBox("Favorites only")
self.cmb_tag = QComboBox()
clear = QPushButton("Clear filters")
clear.setIcon(icons.icon("revert", icons.MUTED))
clear.clicked.connect(self.clear_filters)
fl.addRow(self.search)
fl.addRow("Group", self.cmb_group)
fl.addRow("Category", self.cmb_cat)
fl.addRow("Character", self.cmb_char)
fl.addRow(self.cb_inferred)
fl.addRow("Archive", self.cmb_arch)
fl.addRow("Tag", self.cmb_tag)
fl.addRow(self.cb_mod)
fl.addRow(self.cb_fav)
fl.addRow(clear)
self.lbl_count = QLabel()
fl.addRow(self.lbl_count)
self.cmb_group.currentTextChanged.connect(self._group_changed)
for w in (self.cmb_cat, self.cmb_arch, self.cmb_tag):
w.currentTextChanged.connect(lambda _: self._filters_changed())
self.cmb_char.currentTextChanged.connect(lambda _: self._filters_changed())
for w in (self.cb_inferred, self.cb_mod, self.cb_fav):
w.toggled.connect(lambda _: self._filters_changed())
# table
self.model = SoundModel(self)
self.proxy = SoundFilter(self)
self.proxy.setSourceModel(self.model)
self.table = QTableView()
self.table.setModel(self.proxy)
self.table.setSortingEnabled(True)
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.table.verticalHeader().hide()
self.table.verticalHeader().setDefaultSectionSize(22)
self.table.setAlternatingRowColors(True)
self.table.setWordWrap(False)
h = self.table.horizontalHeader()
h.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
h.setStretchLastSection(True)
for c, w in zip(range(len(COLS)), (28, 360, 140, 200, 70, 55, 110, 75, 70, 200)):
self.table.setColumnWidth(c, w)
self.table.sortByColumn(C_NAME, Qt.SortOrder.AscendingOrder)
self.table.selectionModel().currentRowChanged.connect(self._current_changed)
self.table.clicked.connect(self._clicked)
self.table.doubleClicked.connect(lambda _: self.editor.play("orig"))
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._context_menu)
# editor
self.editor = EditorPanel(self)
self.editor.applied.connect(self._after_change)
self.editor.reverted.connect(self._after_change)
self.editor.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
scroll = QScrollArea()
scroll.setWidget(self.editor)
scroll.setWidgetResizable(True)
self.vsplit = QSplitter(Qt.Orientation.Vertical)
self.vsplit.addWidget(self.table)
self.vsplit.addWidget(scroll)
self.vsplit.setStretchFactor(0, 1) # extra height goes to the list; the editor keeps its fitted height
self.vsplit.setStretchFactor(1, 0)
self.scroll = scroll
self._fitted = False
self._user_sized = False # once the divider is dragged, keep the user's choice
self.vsplit.splitterMoved.connect(lambda *_: setattr(self, "_user_sized", True))
self.editor.installEventFilter(self)
hs = QSplitter(Qt.Orientation.Horizontal)
hs.addWidget(side)
hs.addWidget(self.vsplit)
hs.setStretchFactor(1, 1)
hs.setSizes([SIDEBAR_MAX, 100000])
central = QWidget()
cl = QVBoxLayout(central)
cl.setContentsMargins(0, 0, 0, 0)
cl.setSpacing(0)
cl.addWidget(self.banner)
cl.addWidget(hs, 1)
self.setCentralWidget(central)
self.spinner = Spinner(14)
self.statusBar().addWidget(self.spinner)
self.lbl_game = QLabel()
self.statusBar().addPermanentWidget(self.lbl_game)
sc = Qt.ShortcutContext.WindowShortcut
QShortcut(QKeySequence("Ctrl+F"), self, self.search.setFocus, context=sc)
QShortcut(QKeySequence("Ctrl+S"), self, lambda: self.editor.apply(), context=sc)
QShortcut(QKeySequence("Ctrl+O"), self, lambda: self.editor.load_file_dialog(), context=sc)
QShortcut(QKeySequence("Escape"), self, self.editor.stop, context=sc)
QShortcut(QKeySequence(Qt.Key.Key_Space), self.table, self.editor.toggle_play,
context=Qt.ShortcutContext.WidgetShortcut)
QShortcut(QKeySequence(Qt.Key.Key_F2), self.table, self.edit_notes, context=Qt.ShortcutContext.WidgetShortcut)
# --- dialogs (overridable in tests) -----------------------------------------------------------
def confirm(self, title, text):
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
def inform(self, title, text):
QMessageBox.information(self, title, text)
# --- game loading -----------------------------------------------------------------------------
def show_no_game(self, msg=None):
self.banner_text.setText(msg or "Batman: Arkham City was not found automatically. Choose the game folder "
"(it contains BmGame and Binaries; on Linux usually ~/.steam/steam/steamapps/"
"common/Batman Arkham City GOTY).")
self.b_reapply.hide()
self.b_forget.hide()
self.b_choose.show()
self.banner.show()
def choose_game(self):
d = pick_game_dir(self, self.settings.get("game_dir") or "")
if d:
self.load_game(d)
def load_game(self, game_dir, language=None):
try:
game = Game(game_dir)
except GameError as e:
self.show_no_game(str(e))
return
lang = language if language in game.languages else game.language()
self.loading = True
self.statusBar().showMessage("Loading sound list…")
self.spinner.show()
store = self.settings.get("store_root")
full = self.settings.get("full_backup", True)
def work(job):
return W.Workspace(game, store, [lang, "SFX"] if "SFX" in game.archives else [lang], full_backup=full,
progress=lambda n, t: job.report("scanning %s: %d / %d" % (lang, n, t)))
def done(ws):
self.loading = False
self.spinner.hide()
self.ws = ws
self.settings["game_dir"] = game.root
self.settings["language"] = lang
self.save_settings()
self.cmb_lang.blockSignals(True)
self.cmb_lang.clear()
self.cmb_lang.addItems(game.languages)
self.cmb_lang.setCurrentText(lang)
self.cmb_lang.blockSignals(False)
self.populate()
self.banner.hide()
self.lbl_game.setText("%s • store: %s" % (game.root, ws.store.root))
self.statusBar().showMessage("Loaded %d sounds (%s)." % (len(ws.catalog.sounds), ", ".join(ws.catalog.archives)), 6000)
self.editor.refresh_devices()
self.check_problems()
def failed(e, tb):
self.loading = False
self.spinner.hide()
self.show_no_game("Could not load the game audio: %s" % e)
self.runner.start(work, done, failed, lambda m: self.statusBar().showMessage(m))
def populate(self):
ws = self.ws
self.model.reset(ws.catalog.sounds, ws.modified_keys(), self.userdata)
snd = ws.catalog.sounds
def fill(cmb, items):
cur = cmb.currentText()
cmb.blockSignals(True)
cmb.clear()
cmb.addItems([ANY] + items)
i = cmb.findText(cur)
cmb.setCurrentIndex(i if i >= 0 else 0)
cmb.blockSignals(False)
fill(self.cmb_group, [g for g in ("Speech", "Music", "Ambience", "SFX", "Unknown") if any(s.group == g for s in snd)])
self._fill_categories()
chars = sorted({c.strip() for s in snd for c in s.character.split(";") if c.strip()})
fill(self.cmb_char, chars)
fill(self.cmb_arch, list(ws.catalog.archives))
fill(self.cmb_tag, self.userdata.all_tags())
self._filters_changed()
def _fill_categories(self):
g = self.cmb_group.currentText()
cats = sorted({s.category for s in (self.ws.catalog.sounds if self.ws else []) if g == ANY or s.group == g})
cur = self.cmb_cat.currentText()
self.cmb_cat.blockSignals(True)
self.cmb_cat.clear()
self.cmb_cat.addItems([ANY] + cats)
i = self.cmb_cat.findText(cur)
self.cmb_cat.setCurrentIndex(i if i >= 0 else 0)
self.cmb_cat.blockSignals(False)
# --- filtering --------------------------------------------------------------------------------
def _val(self, cmb):
t = cmb.currentText().strip()
return None if not t or t == ANY else t
def _group_changed(self, _):
self._fill_categories()
self._filters_changed()
def _filters_changed(self):
self.proxy.set(group=self._val(self.cmb_group), category=self._val(self.cmb_cat),
character=self._val(self.cmb_char), include_inferred=self.cb_inferred.isChecked(),
archive=self._val(self.cmb_arch), modified_only=self.cb_mod.isChecked(),
favorites_only=self.cb_fav.isChecked(), tag=self._val(self.cmb_tag))
self._update_count()
def _filter_text(self):
self.proxy.set_text(self.search.text())
self._update_count()
def _update_count(self):
self.lbl_count.setText("%d of %d sounds" % (self.proxy.rowCount(), self.model.rowCount()))
def clear_filters(self):
self.search.clear()
for c in (self.cmb_group, self.cmb_cat, self.cmb_char, self.cmb_arch, self.cmb_tag):
c.setCurrentIndex(0)
for c in (self.cb_inferred, self.cb_mod, self.cb_fav):
c.setChecked(False)
self._filter_text()
# --- selection --------------------------------------------------------------------------------
def selected_sounds(self):
rows = self.table.selectionModel().selectedRows()
return [self.proxy.sound_at(i) for i in rows]
def _current_changed(self, cur, prev):
if cur.isValid():
self.editor.set_sound(self.proxy.sound_at(cur))
def _clicked(self, index):
if index.column() == C_FAV:
s = self.proxy.sound_at(index)
self.userdata.update(s.key, fav=not self.userdata.favorite(s.key))
self.model.refresh_keys()
def select_name(self, name):
"""Select a sound by name (used by tests and 'jump to')."""
for r in range(self.proxy.rowCount()):
idx = self.proxy.index(r, C_NAME)
if self.proxy.sound_at(idx).name == name:
self.table.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
self.table.scrollTo(idx)
return True
return False
def _context_menu(self, pos):
sel = self.selected_sounds()
if not sel:
return
m = QMenu(self)
m.addAction("Play original", lambda: self.editor.play("orig"))
m.addAction("Notes && tags… (F2)", self.edit_notes)
m.addAction("Toggle favorite", lambda: self._toggle_fav(sel))
m.addSeparator()
m.addAction("Revert selected (%d)" % len(sel), lambda: self.revert_sounds(sel))
m.addAction("Copy name", lambda: self._copy("\n".join(s.name for s in sel)))
m.exec(self.table.viewport().mapToGlobal(pos))
def _copy(self, text):
from PySide6.QtWidgets import QApplication
QApplication.clipboard().setText(text)
def _toggle_fav(self, sounds):
on = not all(self.userdata.favorite(s.key) for s in sounds)
for s in sounds:
self.userdata.update(s.key, fav=on)
self.model.refresh_keys()
def edit_notes(self, sounds=None, note=None, tags=None):
sounds = sounds or self.selected_sounds()
if not sounds:
return
s0 = sounds[0]
if note is None:
dlg = QDialog(self)
dlg.setWindowTitle("Notes & tags - %s" % (s0.name if len(sounds) == 1 else "%d sounds" % len(sounds)))
f = QFormLayout(dlg)
en = QLineEdit(self.userdata.note(s0.key))
et = QLineEdit(", ".join(self.userdata.tags(s0.key)))
et.setPlaceholderText("comma separated, e.g. funny, todo")
f.addRow("Note", en)
f.addRow("Tags", et)
from PySide6.QtWidgets import QDialogButtonBox
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
bb.accepted.connect(dlg.accept)
bb.rejected.connect(dlg.reject)
f.addRow(bb)
dlg.resize(460, 0)
if dlg.exec() != QDialog.DialogCode.Accepted:
return
note, tags = en.text(), [t for t in et.text().split(",")]
for s in sounds:
self.userdata.update(s.key, note=note, tags=tags)
self.model.refresh_keys()
cur = self.cmb_tag.currentText()
self.cmb_tag.blockSignals(True)
self.cmb_tag.clear()
self.cmb_tag.addItems([ANY] + self.userdata.all_tags())
self.cmb_tag.setCurrentText(cur)
self.cmb_tag.blockSignals(False)
# --- changes ----------------------------------------------------------------------------------
def _after_change(self, key=None):
self.model.refresh_keys(modified=self.ws.modified_keys())
self.proxy.refilter()
self._update_count()
def revert_sounds(self, sounds):
sounds = [s for s in sounds if self.ws.store.get(s.key)]
if not sounds:
self.statusBar().showMessage("Nothing to revert: the selected sounds are original.", 5000)
return
self._run_change("Reverting %d sound(s)…" % len(sounds), lambda job: self.ws.revert(sounds, job.report),
"Reverted %d sound(s)." % len(sounds))
def revert_all(self):
if not self.ws:
return
n = len(self.ws.store.entries)
if not n:
self.inform("Revert all", "All sounds are already original.")
return
if not self.confirm("Revert all", "Put the original back for all %d changed sound(s)?" % n):
return
self._run_change("Reverting everything…", lambda job: self.ws.revert_all(job.report),
"All sounds are original again.")
def _run_change(self, busy, fn, ok_msg):
self.statusBar().showMessage(busy)
self.spinner.show()
def done(_):
self.spinner.hide()
self.statusBar().showMessage(ok_msg, 8000)
self._after_change()
if self.editor.sound:
self.editor.set_sound(self.editor.sound)
self.check_problems()
def failed(e, tb):
self.spinner.hide()
self.inform("Error", str(e))
self.runner.start(fn, done, failed, lambda m: self.statusBar().showMessage(m))
# --- changed underneath -----------------------------------------------------------------------
def check_problems(self):
ws = self.ws
if not ws:
return
def work(job):
out = []
for key in list(ws.store.entries):
s = ws.catalog.by_key.get(key)
if s is None:
continue
st = ws.state(s)
if st in (W.LOST, W.CHANGED):
out.append((s, st))
return out
def done(problems):
self.problems = problems
if not problems:
if self.banner.isVisible() and not self.b_choose.isVisible():
self.banner.hide()
return
lost = [s.name for s, st in problems if st == W.LOST]
changed = [s.name for s, st in problems if st == W.CHANGED]
txt = []
if lost:
txt.append("%d of your changes were undone outside this editor (Steam 'verify files' or an update?): %s"
% (len(lost), ", ".join(lost[:4]) + (" …" if len(lost) > 4 else "")))
if changed:
txt.append("%d sound(s) were changed by something else since you modified them: %s. "
"Revert puts the original back." % (len(changed), ", ".join(changed[:4])))
self.banner_text.setText("\n".join(txt))
self.b_reapply.show()
self.b_forget.show()
self.b_choose.hide()
self.banner.show()
self.runner.start(work, done)
def reapply_lost(self):
ws = self.ws
items = [(s, ws.store.get_blob(ws.store.get(s.key)["applied"]), ws.store.get(s.key).get("meta", {}))
for s, st in getattr(self, "problems", []) if ws.store.get(s.key) and ws.store.get(s.key).get("applied")]
if items:
self._run_change("Re-applying…", lambda job: ws.apply(items, force=True, progress=job.report),
"Re-applied %d change(s)." % len(items))
def forget_problems(self):
if not self.confirm("Forget", "Stop tracking these sounds? Their current game files are kept as they are."):
return
for s, _ in getattr(self, "problems", []):
self.ws.store.forget(s.key)
self.ws.store.save()
self.ws.store.gc()
self.banner.hide()
self._after_change()
# --- mods -------------------------------------------------------------------------------------
def export_mod_dialog(self, path=None, keys=None, title="", author="", description=""):
if not self.ws:
return
mods = self.ws.modified_keys()
if not mods:
self.inform("Export mod", "You have not changed any sound yet.")
return
if keys is None:
names = [(k, self.ws.catalog.by_key[k].name if k in self.ws.catalog.by_key else k) for k in mods]
dlg = ExportModDialog(names, self)
if dlg.exec() != QDialog.DialogCode.Accepted:
return
keys, title, author, description = (dlg.selected_keys(), dlg.title.text(), dlg.author.text(),
dlg.desc.toPlainText())
path, _ = QFileDialog.getSaveFileName(self, "Save mod package", os.path.join(
self.settings.get("last_dir") or "", (title or "my_mod") + ".acmod"), "Unheard City mod (*.acmod)")
if not path:
return
self._run_change("Exporting…", lambda job: export_mod(self.ws, path, keys, title, author, description),
"Saved %s" % path)
def import_mod_dialog(self, path=None, confirm=True):
if not self.ws:
return
if path is None:
path, _ = QFileDialog.getOpenFileName(self, "Install mod package", self.settings.get("last_dir") or "",
"Unheard City mod (*.acmod)")
if not path:
return
try:
man, items = match_mod(self.ws, path)
except Exception as e: # noqa: BLE001
self.inform("Import mod", "Cannot read this package: %s" % e)
return
if confirm and ImportModDialog(man, items, self).exec() != QDialog.DialogCode.Accepted:
return
ws = self.ws
def work(job):
prepare_import(ws, path, items, progress=job.report)
changes = [(im.sound, im.wem, {"source": "mod:" + os.path.basename(path)}) for im in items if im.wem]
try:
ws.apply(changes, progress=job.report)
except W.ConflictError:
ws.apply(changes, force=True, progress=job.report)
return len(changes)
self._run_change("Installing mod…", work, "Installed %s" % os.path.basename(path))
# --- settings ---------------------------------------------------------------------------------
def settings_dialog(self):
dlg = SettingsDialog(self.settings, self)
if dlg.exec() != QDialog.DialogCode.Accepted:
return
old = dict(self.settings)
self.settings.update(dlg.values())
self.save_settings()
self._apply_output_device()
self.sidebar.configure(self.settings)
if (self.settings.get("game_dir") != old.get("game_dir") or self.settings.get("store_root") != old.get("store_root")
or self.settings.get("full_backup") != old.get("full_backup")):
if self.settings.get("game_dir"):
self.load_game(self.settings["game_dir"], self.settings.get("language"))
def _apply_output_device(self):
name = self.settings.get("output_device")
self.player.set_device(find_device(output_devices(), name) if name else None)
def save_settings(self):
self.settings["window_geometry"] = bytes(self.saveGeometry().toHex()).decode()
try:
settings_mod.save(self.settings)
except OSError:
pass
def fit_editor(self):
"""Give the editor panel the height of its contents (everything visible), the list gets the rest."""
total = sum(self.vsplit.sizes()) or self.vsplit.height()
lay = self.editor.layout()
need = self.editor.sizeHint().height()
if lay.hasHeightForWidth(): # wrapped labels: height at the actual width
need = max(need, lay.heightForWidth(self.scroll.viewport().width()))
need += 2 * self.scroll.frameWidth()
if self.editor.minimumSizeHint().width() > self.scroll.viewport().width(): # narrow window: room for the h-bar
need += self.scroll.horizontalScrollBar().sizeHint().height()
editor_h = min(need, max(0, total - TABLE_MIN))
self.vsplit.setSizes([total - editor_h, editor_h])
def eventFilter(self, obj, e):
if obj is self.editor and e.type() == QEvent.Type.LayoutRequest and self._fitted and not self._user_sized:
QTimer.singleShot(0, self.fit_editor)
return super().eventFilter(obj, e)
def showEvent(self, e):
super().showEvent(e)
if not self._fitted:
self._fitted = True
QTimer.singleShot(0, self.fit_editor)
def closeEvent(self, e):
self.player.stop()
self.save_settings()
super().closeEvent(e)

184
app/models.py Normal file
View File

@@ -0,0 +1,184 @@
"""Table model for the sound browser + a combined filter (search text and facet filters)."""
from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel, Qt
from PySide6.QtGui import QFont, QPalette
from PySide6.QtWidgets import QApplication
from . import icons
COLS = ["", "Name", "Category", "Character", "Length", "Archive", "Format", "Max size", "Status", "Notes / tags"]
C_FAV, C_NAME, C_CAT, C_CHAR, C_LEN, C_ARCH, C_FMT, C_SIZE, C_STATUS, C_NOTES = range(len(COLS))
SORT_ROLE = Qt.ItemDataRole.UserRole + 1
SOUND_ROLE = Qt.ItemDataRole.UserRole + 2
class SoundModel(QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
self.sounds = []
self.modified = set()
self.userdata = None
self.hay = []
def reset(self, sounds, modified, userdata):
self.beginResetModel()
self.sounds, self.modified, self.userdata = list(sounds), set(modified), userdata
self._build_hay()
self.endResetModel()
def _build_hay(self):
ud = self.userdata
self.hay = ["\n".join((s.name, s.category, s.character, s.characters_inferred,
ud.note(s.key) if ud else "", " ".join(ud.tags(s.key)) if ud else "")).lower()
for s in self.sounds]
self.ids = [s.target.replace(":", " ").replace("/", " ").lower() for s in self.sounds]
def refresh_keys(self, keys=None, modified=None):
"""Update status / notes of some rows (all if keys is None)."""
if modified is not None:
self.modified = set(modified)
self._build_hay()
if self.sounds:
self.dataChanged.emit(self.index(0, 0), self.index(len(self.sounds) - 1, len(COLS) - 1))
def rowCount(self, parent=QModelIndex()):
return 0 if parent.isValid() else len(self.sounds)
def columnCount(self, parent=QModelIndex()):
return len(COLS)
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return COLS[section]
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DecorationRole and section == C_FAV:
return icons.icon("star", icons.ACCENT)
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.ToolTipRole:
return {C_FAV: "Favorite", C_CHAR: "Verified speaker; ~grey = characters mentioned by the name",
C_SIZE: "Largest replacement that fits (speech/effects keep the original's size)",
C_STATUS: "Modified = contains your replacement"}.get(section)
return None
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return None
s = self.sounds[index.row()]
c = index.column()
ud = self.userdata
if role == SOUND_ROLE:
return s
if role == Qt.ItemDataRole.DisplayRole:
if c == C_FAV:
return ""
if c == C_NAME:
return s.name
if c == C_CAT:
return s.category
if c == C_CHAR:
return s.character or (("~ " + s.characters_inferred) if s.characters_inferred else "")
if c == C_LEN:
return "%.2f s" % s.seconds
if c == C_ARCH:
return s.archive
if c == C_FMT:
return "%d Hz %s" % (s.rate, {1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels))
if c == C_SIZE:
return "any" if s.is_pack_media else "%.1f KB" % (s.size / 1024.0)
if c == C_STATUS:
return "Modified" if s.key in self.modified else ""
if c == C_NOTES:
if not ud:
return ""
t = ud.tags(s.key)
return " ".join("#" + x for x in t) + (" " if t else "") + ud.note(s.key)
if role == SORT_ROLE:
if c == C_FAV:
return 0 if ud and ud.favorite(s.key) else 1
if c == C_LEN:
return s.seconds
if c == C_SIZE:
return 1e12 if s.is_pack_media else s.size
if c == C_FMT:
return (s.rate, s.channels)
if c == C_STATUS:
return 0 if s.key in self.modified else 1
return (self.data(index, Qt.ItemDataRole.DisplayRole) or "").lower()
if role == Qt.ItemDataRole.DecorationRole and c == C_FAV:
fav = bool(ud and ud.favorite(s.key))
return icons.icon("star" if fav else "star-off", icons.ACCENT if fav else "#4a4a4a")
if role == Qt.ItemDataRole.DecorationRole and c == C_STATUS and s.key in self.modified:
return icons.icon("apply", icons.ACCENT)
if role == Qt.ItemDataRole.ForegroundRole and c == C_CHAR and not s.character:
return QApplication.palette().color(QPalette.ColorRole.PlaceholderText)
if role == Qt.ItemDataRole.FontRole and s.key in self.modified and c in (C_NAME, C_STATUS):
f = QFont()
f.setBold(True)
return f
if role == Qt.ItemDataRole.ToolTipRole and c in (C_NAME, C_CHAR):
tip = "%s\n%s %s" % (s.name, s.key, s.group + " / " + s.category)
if s.character:
tip += "\nSpeaker: " + s.character
if s.characters_inferred:
tip += "\nMentioned in the name: " + s.characters_inferred
return tip
return None
class SoundFilter(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self.words = []
self.group = None
self.category = None
self.character = None
self.include_inferred = False
self.archive = None
self.modified_only = False
self.favorites_only = False
self.tag = None
self.setSortRole(SORT_ROLE)
self.setDynamicSortFilter(False)
def refilter(self):
if hasattr(self, "beginFilterChange"): # Qt >= 6.10
self.beginFilterChange()
self.endFilterChange(QSortFilterProxyModel.Direction.Rows)
else:
self.invalidateRowsFilter()
def set(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
self.refilter()
def set_text(self, text):
self.words = [w for w in text.lower().replace("_", " ").split() if w]
self.refilter()
def filterAcceptsRow(self, row, parent):
m = self.sourceModel()
s = m.sounds[row]
if self.group and s.group != self.group:
return False
if self.category and s.category != self.category:
return False
if self.archive and s.archive != self.archive:
return False
if self.character:
c = self.character.lower()
if c not in s.character.lower() and not (self.include_inferred and c in s.characters_inferred.lower()):
return False
if self.modified_only and s.key not in m.modified:
return False
ud = m.userdata
if self.favorites_only and not (ud and ud.favorite(s.key)):
return False
if self.tag and not (ud and self.tag in ud.tags(s.key)):
return False
if self.words:
h = m.hay[row]
for w in self.words:
if w not in h and not (len(w) >= 6 and w in m.ids[row]): # IDs like b49cdf48 also work
return False
return True
def sound_at(self, proxy_index):
return self.sourceModel().sounds[self.mapToSource(proxy_index).row()]

53
app/spinner.py Normal file
View File

@@ -0,0 +1,53 @@
"""Small activity spinner: a rotating accent-coloured arc. Runs only while visible."""
from PySide6.QtCore import QRectF, QTimer, Qt
from PySide6.QtGui import QColor, QPainter, QPen
from PySide6.QtWidgets import QWidget
from . import icons
STEP_MS = 16
STEP_DEG = 9
def paint_arc(p, rect, angle, color=icons.ACCENT):
"""Draw the spinner (faint ring + 100 degree arc) into rect; used by Spinner and the waveform overlay."""
w = max(1.5, rect.width() / 8)
r = rect.adjusted(w / 2, w / 2, -w / 2, -w / 2)
p.save()
p.setRenderHint(QPainter.RenderHint.Antialiasing)
ring = QColor(color)
ring.setAlpha(45)
p.setPen(QPen(ring, w))
p.drawEllipse(r)
pen = QPen(QColor(color), w)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
p.setPen(pen)
p.drawArc(r, int(-angle * 16), 100 * 16)
p.restore()
class Spinner(QWidget):
def __init__(self, size=16, parent=None):
super().__init__(parent)
self.setFixedSize(size, size)
self.angle = 0
self.timer = QTimer(self)
self.timer.setInterval(STEP_MS)
self.timer.timeout.connect(self._step)
self.hide()
def _step(self):
self.angle = (self.angle + STEP_DEG) % 360
self.update()
def showEvent(self, e):
self.timer.start()
super().showEvent(e)
def hideEvent(self, e):
self.timer.stop()
super().hideEvent(e)
def paintEvent(self, _):
p = QPainter(self)
paint_arc(p, QRectF(self.rect()), self.angle)

140
app/theme.py Normal file
View File

@@ -0,0 +1,140 @@
"""The app look: neutral dark greys, one dark red accent, the wallpaper behind the filter panel.
`apply(app)` sets the style for the whole application; `Sidebar` is the filter panel, painted over the wallpaper
(app/assets/wallpaper.jpg) when the background is on.
"""
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QImage, QPainter, QPalette
from PySide6.QtWidgets import QWidget
from . import icons
ACCENT = "#b0242c"
ACCENT_HI = "#cf3a42"
STEEL = "#c4c4c4" # the original waveform: neutral light grey
TEXT = "#e6e6e6"
MUTED = "#9a9a9a"
BG = "#141414" # dialogs
PANEL_BG = "#0c0c0c" # main window: behind the filter and editor panels (darker for contrast)
SURFACE = "#1b1b1b"
BORDER = "#2c2c2c"
CONTROL = "#3a3a3a"
QSS = """
QWidget { color: %(TEXT)s; }
QMainWindow { background: %(PANEL_BG)s; }
QDialog { background: %(BG)s; }
QToolTip { background: #202020; color: %(TEXT)s; border: 1px solid %(CONTROL)s; padding: 4px; }
QToolBar { background: #0f0f0f; border: none; border-bottom: 1px solid %(BORDER)s; spacing: 6px; padding: 4px 6px; }
QToolBar QToolButton { color: %(TEXT)s; padding: 4px 9px; border-radius: 4px; }
QToolBar QToolButton:hover { background: #262626; }
QLabel#AppTitle { color: %(TEXT)s; font-weight: bold; padding: 0 10px 0 2px; }
QStatusBar { background: #0f0f0f; color: %(MUTED)s; border-top: 1px solid %(BORDER)s; }
QStatusBar QLabel { color: %(MUTED)s; }
QWidget#Sidebar { background: transparent; border-right: 1px solid %(BORDER)s; }
QWidget#EditorPanel, QScrollArea, QScrollArea > QWidget > QWidget { background: transparent; }
QWidget#Banner { background: #3a1416; border-bottom: 1px solid %(ACCENT)s; }
QLabel#EditorTitle { color: %(TEXT)s; }
QLabel#Muted { color: %(MUTED)s; }
QTableView { background: %(SURFACE)s; alternate-background-color: #202020; gridline-color: %(BORDER)s;
border: 1px solid %(BORDER)s; selection-background-color: rgba(176,36,44,70); selection-color: #ffffff; }
QTableView::item { padding: 0 4px; }
QHeaderView::section { background: #171717; color: %(MUTED)s; border: none; border-right: 1px solid %(BORDER)s;
border-bottom: 1px solid %(CONTROL)s; padding: 4px 6px; font-weight: bold; }
QTableCornerButton::section { background: #171717; border: none; }
QGroupBox { background: %(SURFACE)s; border: 1px solid %(BORDER)s; border-radius: 6px;
margin-top: 16px; padding: 10px 6px 6px 6px; }
QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; color: %(MUTED)s; font-weight: bold; }
QPushButton { background: #242424; border: 1px solid %(CONTROL)s; border-radius: 4px; padding: 5px 12px; }
QPushButton:hover { background: #2c2c2c; border-color: #4a4a4a; }
QPushButton:pressed, QPushButton:checked { background: rgba(176,36,44,45); border-color: %(ACCENT)s; }
QPushButton:disabled { color: #5c5c5c; border-color: #2a2a2a; background: #1c1c1c; }
QPushButton#Primary { background: %(ACCENT)s; color: #ffffff; font-weight: bold; border: 1px solid %(ACCENT)s; }
QPushButton#Primary:hover { background: %(ACCENT_HI)s; border-color: %(ACCENT_HI)s; }
QPushButton#Primary:disabled { background: #3a1c1e; color: #8a6466; border-color: #3a1c1e; }
QLineEdit, QComboBox, QDoubleSpinBox, QPlainTextEdit, QListWidget, QTreeWidget {
background: #101010; border: 1px solid %(CONTROL)s; border-radius: 4px; padding: 3px 6px;
selection-background-color: rgba(176,36,44,120); }
QLineEdit:focus, QComboBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus { border-color: %(ACCENT)s; }
QComboBox QAbstractItemView { background: #1b1b1b; border: 1px solid %(CONTROL)s;
selection-background-color: rgba(176,36,44,90); }
QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid #555555; border-radius: 3px; background: #101010; }
QCheckBox::indicator:checked { background: %(ACCENT)s; border-color: %(ACCENT)s; }
QProgressBar { background: #101010; border: 1px solid %(CONTROL)s; border-radius: 3px; height: 10px; }
QProgressBar::chunk { background: %(ACCENT)s; border-radius: 2px; }
QSlider::groove:horizontal { height: 4px; background: #333333; border-radius: 2px; }
QSlider::sub-page:horizontal { background: %(ACCENT)s; border-radius: 2px; }
QSlider::handle:horizontal { background: %(TEXT)s; width: 12px; margin: -5px 0; border-radius: 6px; }
QSplitter::handle { background: %(BORDER)s; }
QScrollBar:vertical, QScrollBar:horizontal { background: transparent; border: none; margin: 0; }
QScrollBar:vertical { width: 10px; } QScrollBar:horizontal { height: 10px; }
QScrollBar::handle { background: #3c3c3c; border-radius: 4px; min-height: 24px; min-width: 24px; }
QScrollBar::handle:hover { background: #555555; }
QScrollBar::add-line, QScrollBar::sub-line { width: 0; height: 0; }
QMenu { background: #1b1b1b; border: 1px solid %(CONTROL)s; }
QMenu::item { padding: 5px 22px 5px 10px; }
QMenu::item:selected { background: rgba(176,36,44,70); }
""" % dict(TEXT=TEXT, BG=BG, PANEL_BG=PANEL_BG, ACCENT=ACCENT, ACCENT_HI=ACCENT_HI, MUTED=MUTED, BORDER=BORDER, SURFACE=SURFACE,
CONTROL=CONTROL)
# waveform colours: original / installed / replacement
WAVE = {"orig": QColor(STEEL), "inst": QColor("#8f4a4e"), "new": QColor(ACCENT)}
WAVE_BG = QColor("#111111")
SIDEBAR_VEIL = QColor(16, 16, 16) # alpha follows the background strength
def apply(app):
app.setStyle("Fusion")
pal = QPalette()
for role, col in [(QPalette.ColorRole.Window, BG), (QPalette.ColorRole.WindowText, TEXT),
(QPalette.ColorRole.Base, "#101010"), (QPalette.ColorRole.AlternateBase, "#202020"),
(QPalette.ColorRole.Text, TEXT), (QPalette.ColorRole.Button, "#242424"),
(QPalette.ColorRole.ButtonText, TEXT), (QPalette.ColorRole.Highlight, ACCENT),
(QPalette.ColorRole.HighlightedText, "#ffffff"), (QPalette.ColorRole.ToolTipBase, "#202020"),
(QPalette.ColorRole.ToolTipText, TEXT), (QPalette.ColorRole.PlaceholderText, MUTED),
(QPalette.ColorRole.Mid, "#333333"), (QPalette.ColorRole.Link, ACCENT)]:
pal.setColor(role, QColor(col))
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor("#5c5c5c"))
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor("#5c5c5c"))
app.setPalette(pal)
app.setStyleSheet(QSS)
def wallpaper():
"""The Arkham City wallpaper (app/assets/wallpaper.jpg) as a QImage, or None if the assets are missing."""
p = icons.asset("wallpaper.jpg")
img = QImage(p) if p else QImage()
return None if img.isNull() else img
class Sidebar(QWidget):
"""Filter panel with the wallpaper behind it: scaled to the panel height, its left part fills the panel."""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("Sidebar")
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.image = None
self.opacity = 0.1
self._scaled = None
def configure(self, settings):
st = settings or {}
self.image = wallpaper() if st.get("background_enabled", True) else None
self.opacity = max(0.0, min(1.0, float(st.get("background_opacity", 0.1))))
self._scaled = None
self.update()
def paintEvent(self, e):
if self.image is not None:
if self._scaled is None or self._scaled.height() != self.height():
self._scaled = self.image.scaledToHeight(self.height(), Qt.TransformationMode.SmoothTransformation)
p = QPainter(self)
p.setOpacity(self.opacity)
p.drawImage(0, 0, self._scaled)
p.setOpacity(1.0)
veil = QColor(SIDEBAR_VEIL)
veil.setAlpha(int(180 * self.opacity)) # stronger image, stronger veil: labels stay readable
p.fillRect(self.rect(), veil)
p.end()
super().paintEvent(e)

50
app/userdata.py Normal file
View File

@@ -0,0 +1,50 @@
"""Per-user notes, tags and favorites for sounds (user data folder, never the game folder)."""
import json
import os
from ackit.paths import user_dir
class UserData:
def __init__(self, path=None):
self.path = path or os.path.join(user_dir("data"), "sound_notes.json")
try:
self.data = json.load(open(self.path, encoding="utf-8"))
except (OSError, ValueError):
self.data = {}
def save(self):
tmp = self.path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(self.data, f, indent=1, ensure_ascii=False)
os.replace(tmp, self.path)
def _get(self, key):
return self.data.get(key, {})
def note(self, key):
return self._get(key).get("note", "")
def tags(self, key):
return self._get(key).get("tags", [])
def favorite(self, key):
return bool(self._get(key).get("fav"))
def update(self, key, note=None, tags=None, fav=None):
d = dict(self._get(key))
if note is not None:
d["note"] = note.strip()
if tags is not None:
d["tags"] = sorted({t.strip() for t in tags if t.strip()})
if fav is not None:
d["fav"] = bool(fav)
d = {k: v for k, v in d.items() if v}
if d:
self.data[key] = d
else:
self.data.pop(key, None)
self.save()
def all_tags(self):
return sorted({t for d in self.data.values() for t in d.get("tags", [])})

123
app/waveform.py Normal file
View File

@@ -0,0 +1,123 @@
"""Waveform display: envelope, playhead, click-to-seek, drag-to-select (trim region)."""
import numpy as np
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
from PySide6.QtWidgets import QSizePolicy, QWidget
from . import spinner
class Waveform(QWidget):
seekRequested = Signal(float) # seconds
selectionChanged = Signal(float, float) # seconds (start, end); (0, 0) = none
def __init__(self, parent=None, selectable=False):
super().__init__(parent)
self.setMinimumHeight(70)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.selectable = selectable
self.env = None # (mins, maxs) at a fixed resolution
self.duration = 0.0
self.playhead = None
self.sel = None
self._drag = None
self.placeholder = "no audio"
self.accent = None
self.bg = None # background colour (theme), else the palette's Base
self.busy = False # spinner beside the placeholder (audio being loaded)
self._angle = 0
self._timer = QTimer(self)
self._timer.setInterval(spinner.STEP_MS)
self._timer.timeout.connect(self._spin)
def _spin(self):
self._angle = (self._angle + spinner.STEP_DEG) % 360
self.update()
def set_audio(self, x, rate, placeholder="no audio", busy=False):
self.placeholder = placeholder
self.busy = busy and (x is None or len(x) == 0)
self._timer.start() if self.busy else self._timer.stop()
self.sel = None
self.playhead = None
if x is None or len(x) == 0:
self.env, self.duration = None, 0.0
else:
m = x.mean(axis=1) if x.ndim == 2 else x
cols = 2000
n = len(m)
edges = np.linspace(0, n, cols + 1).astype(int)
mins = np.array([m[a:b].min() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
maxs = np.array([m[a:b].max() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
self.env = (mins, maxs)
self.duration = n / float(rate)
self.update()
def set_playhead(self, seconds):
self.playhead = seconds
self.update()
def set_selection(self, a, b):
self.sel = (min(a, b), max(a, b)) if b != a else None
self.update()
def _x2t(self, x):
return max(0.0, min(self.duration, x / max(1, self.width()) * self.duration))
def _t2x(self, t):
return t / self.duration * self.width() if self.duration else 0
def mousePressEvent(self, e):
if not self.duration:
return
t = self._x2t(e.position().x())
if self.selectable and e.button() == Qt.MouseButton.LeftButton and e.modifiers() & Qt.KeyboardModifier.ShiftModifier:
self._drag = t
elif self.selectable and e.button() == Qt.MouseButton.RightButton:
self._drag = t
else:
self.seekRequested.emit(t)
def mouseMoveEvent(self, e):
if self._drag is not None:
self.set_selection(self._drag, self._x2t(e.position().x()))
def mouseReleaseEvent(self, e):
if self._drag is not None:
self._drag = None
a, b = self.sel if self.sel else (0.0, 0.0)
self.selectionChanged.emit(a, b)
def paintEvent(self, _):
p = QPainter(self)
pal = self.palette()
r = self.rect()
p.fillRect(r, self.bg or pal.color(QPalette.ColorRole.Base))
mid = r.height() / 2
fg = self.accent or pal.color(QPalette.ColorRole.Highlight)
if self.env is None:
p.setPen(pal.color(QPalette.ColorRole.PlaceholderText))
p.drawText(r, Qt.AlignmentFlag.AlignCenter, self.placeholder)
if self.busy:
tw = p.fontMetrics().horizontalAdvance(self.placeholder)
d = 14
spinner.paint_arc(p, QRectF(r.center().x() - tw / 2 - d - 8, mid - d / 2, d, d), self._angle)
return
if self.sel:
sc = QColor(fg)
sc.setAlpha(50)
p.fillRect(QRectF(self._t2x(self.sel[0]), 0, self._t2x(self.sel[1]) - self._t2x(self.sel[0]), r.height()), sc)
mins, maxs = self.env
w = max(1, r.width())
idx = (np.arange(w) * len(mins) / w).astype(int)
p.setPen(QPen(fg, 1))
for x, i in enumerate(idx):
j = min(len(mins), int((x + 1) * len(mins) / w) + 1)
lo, hi = mins[i:j].min(), maxs[i:j].max()
p.drawLine(QPointF(x, mid - hi * (mid - 2)), QPointF(x, mid - lo * (mid - 2)))
p.setPen(QPen(pal.color(QPalette.ColorRole.Mid), 1))
p.drawLine(0, int(mid), w, int(mid))
if self.playhead is not None:
p.setPen(QPen(pal.color(QPalette.ColorRole.Text), 1))
x = self._t2x(self.playhead)
p.drawLine(QPointF(x, 0), QPointF(x, r.height()))

72
app/workers.py Normal file
View File

@@ -0,0 +1,72 @@
"""Run long operations off the GUI thread, with progress messages and cancel."""
import traceback
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
class Cancelled(Exception):
pass
class _Signals(QObject):
done = Signal(object)
failed = Signal(object, str)
progress = Signal(str)
class Job(QRunnable):
"""fn(job) -> result. Inside fn call job.report(msg) and job.check() (raises Cancelled)."""
def __init__(self, fn):
super().__init__()
self.fn = fn
self.signals = _Signals()
self.cancelled = False
def report(self, msg):
self.check()
self.signals.progress.emit(str(msg))
def check(self):
if self.cancelled:
raise Cancelled()
def cancel(self):
self.cancelled = True
def run(self):
try:
r = self.fn(self)
except Cancelled:
self.signals.failed.emit(Cancelled(), "cancelled")
except Exception as e: # noqa: BLE001 - shown to the user
self.signals.failed.emit(e, traceback.format_exc())
else:
self.signals.done.emit(r)
class Runner:
"""Starts jobs on a thread pool (or inline when `sync`, used by the GUI tests)."""
def __init__(self, sync=False):
self.sync = sync
self.pool = QThreadPool.globalInstance()
self.active = set() # keep Python references until the queued result signal is delivered
def start(self, fn, on_done=None, on_failed=None, on_progress=None):
job = Job(fn)
job.setAutoDelete(False)
self.active.add(job)
job.signals.done.connect(lambda *_: self.active.discard(job))
job.signals.failed.connect(lambda *_: self.active.discard(job))
if on_done:
job.signals.done.connect(on_done)
if on_failed:
job.signals.failed.connect(on_failed)
if on_progress:
job.signals.progress.connect(on_progress)
if self.sync:
job.run()
else:
self.pool.start(job)
return job

10817
data/catalog/DEU.csv Normal file

File diff suppressed because it is too large Load Diff

10817
data/catalog/ESN.csv Normal file

File diff suppressed because it is too large Load Diff

10817
data/catalog/FRA.csv Normal file

File diff suppressed because it is too large Load Diff

10817
data/catalog/INT.csv Normal file

File diff suppressed because it is too large Load Diff

10817
data/catalog/ITA.csv Normal file

File diff suppressed because it is too large Load Diff

37
data/catalog/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Sound catalog
| file | content |
|------|---------|
| `<ARCHIVE>.csv` | every sound of one audio archive: `ITA`, `INT`, `FRA`, `DEU`, `ESN` (speech, one per language) and `SFX` (music, ambience, effects) |
| `characters.csv` | `name,character`: verified speaker per sound name (language independent) |
| `index.json` | fingerprint of the game WAD each CSV was built from (`ackit/game.py: wad_fingerprint`) and counts |
| `source/<lang>/*.txt` | **the curated original lists** (one file per character, one wemscan-style file name per line, e.g. `Speech_TAPE_JOKER_1_4.wem`). `Files.txt` = all files, `Stats.txt` = counts, `Checkpoint.txt` = progress marker. Never edited by tools. |
All CSVs are UTF-8 with a header row. They contain only names and metadata (no game audio).
## `<ARCHIVE>.csv` columns
| column | meaning |
|--------|---------|
| `archive` | `ITA`, `INT`, ... or `SFX` |
| `target` | location inside the archive: `HASH` (entry with one WEM), `HASH:i` (i-th variation, 0-based), `PACK/MEDIA` (music stream pack) |
| `name` | display name (`NAME`, `NAME_1..n` for variations, see `ackit/catalog.py`) |
| `aliases` | other accepted names, `\|`-separated (`NAME_1` for single entries, `NAME#i` for variations) |
| `group` | Speech, Music, Ambience, SFX, Unknown (`docs/categories.md`) |
| `category` | finer category (`docs/categories.md`) |
| `character` | verified speaker(s) from `characters.csv`, `; `-separated, empty if unknown |
| `characters_inferred` | characters/roles mentioned by the name (hint only) |
| `seconds`, `rate`, `channels` | of the original sound |
| `size` | size of the original WEM in bytes = maximum size of a replacement (except music packs: any size) |
| `sha1` | first 16 hex digits of the original WEM's SHA-1 (detects files changed outside the tool) |
| `notes` | free text (empty; for future curation, e.g. transcripts) |
The editor uses these files directly when the installed WAD matches `index.json`; for another game build it
scans the WAD itself and applies the same rules (no checksums then).
## Rebuilding / adding a language
1. Make sure the game files are original (`ackit status`; `ackit revert --all`).
2. Optional: add listening-based lists for another language in `source/<lang>/<Character>.txt` (same format;
names are the same in every language, so the Italian lists already label all languages).
3. Run `python tools/build_catalog.py` - it scans every archive of the installed game (all languages present)
and rewrites the CSVs, `characters.csv` and `index.json`.
New character labels need a display name in `ackit/classify.py: USER_LABELS` (otherwise the file name is used).

652
data/catalog/SFX.csv Normal file
View File

@@ -0,0 +1,652 @@
archive,target,name,aliases,group,category,character,characters_inferred,seconds,rate,channels,size,sha1,notes
SFX,8301A1DA:0,AMB_OW_E8_CH0_1,AMB_OW_E8_CH0#1,Ambience,Ambience,,,26.465,32000,2,347255,d4f1950c2ccb9cbb,
SFX,8301A1DA:1,AMB_OW_E8_CH0_2,AMB_OW_E8_CH0#2,Ambience,Ambience,,,26.465,32000,2,341343,f6b9f4d0d90bfbfd,
SFX,84CC9D20:0,AMB_Meatpacking_B1_1,AMB_Meatpacking_B1#1,Ambience,Ambience,,,13.819,32000,4,368622,045daf15846206fa,
SFX,84CC9D20:1,AMB_Meatpacking_B1_2,AMB_Meatpacking_B1#2,Ambience,Ambience,,,21.382,32000,4,633549,5ba0fbc1f14ad4c3,
SFX,84CC9D20:2,AMB_Meatpacking_B1_3,AMB_Meatpacking_B1#3,Ambience,Ambience,,,9.316,32000,4,255018,1b3f4a087588fa8e,
SFX,84CC9D20:3,AMB_Meatpacking_B1_4,AMB_Meatpacking_B1#4,Ambience,Ambience,,,15.027,32000,4,398709,9fe235e189ae929c,
SFX,84CC9D20:4,AMB_Meatpacking_B1_5,AMB_Meatpacking_B1#5,Ambience,Ambience,,,9.447,32000,4,265375,7b20faec40c234e8,
SFX,84CC9D20:5,AMB_Meatpacking_B1_6,AMB_Meatpacking_B1#6,Ambience,Ambience,,,11.576,32000,4,313386,fa901e65bda499a6,
SFX,84CC9D20:6,AMB_Meatpacking_B1_7,AMB_Meatpacking_B1#7,Ambience,Ambience,,,25.747,32000,4,689430,b63a0028528493b2,
SFX,85F1F884:0,AMB_OW_C257_1,AMB_OW_C257#1,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
SFX,85F1F884:1,AMB_OW_C257_2,AMB_OW_C257#2,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
SFX,85F1F884:2,AMB_OW_C257_3,AMB_OW_C257#3,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
SFX,85F1F884:3,AMB_OW_C257_4,AMB_OW_C257#4,Ambience,Ambience,,,89.000,32000,4,2587371,4b2939c4c48d3a37,
SFX,898441C1:0,AMB_OW_Warzone_1,AMB_OW_Warzone#1,Ambience,Ambience,,,30.411,32000,2,393285,c22d04e66ef95442,
SFX,898441C1:1,AMB_OW_Warzone_2,AMB_OW_Warzone#2,Ambience,Ambience,,,30.411,32000,2,395301,78fe4e98c9dd26f9,
SFX,898441C1:2,AMB_OW_Warzone_3,AMB_OW_Warzone#3,Ambience,Ambience,,,34.441,32000,1,200811,010b705597bb7ecb,
SFX,8D6D9BB9:0,AMB_Museum_C1_1,AMB_Museum_C1#1,Ambience,Ambience,,,20.845,32000,4,479172,a32802f6e677bcbf,
SFX,8D6D9BB9:1,AMB_Museum_C1_2,AMB_Museum_C1#2,Ambience,Ambience,,,35.837,32000,4,976154,2ff67a7bb43c64e0,
SFX,8D6D9BBA,AMB_Museum_C2,AMB_Museum_C2_1,Ambience,Ambience,,,48.049,32000,4,1316154,1b24fce48be812f7,
SFX,8E6D9D29,AMB_Museum_B2,AMB_Museum_B2_1,Ambience,Ambience,,,34.557,32000,4,923709,f1ac94349c012b69,
SFX,8E6D9D2A,AMB_Museum_B1,AMB_Museum_B1_1,Ambience,Ambience,,,49.087,32000,4,1220981,6e9cf2ec033e68bf,
SFX,8F6D9E9F,AMB_Museum_A1,AMB_Museum_A1_1,Ambience,Ambience,,,34.499,32000,4,953199,6eb679248891bd1a,
SFX,98A82EFD:0,AMB_Steel_B1_1,AMB_Steel_B1#1,Ambience,Ambience,,,27.477,32000,4,743201,1ed8f1a2af43d06a,
SFX,98A82EFD:1,AMB_Steel_B1_2,AMB_Steel_B1#2,Ambience,Ambience,,,6.981,48000,4,258105,82ce38a05064a82a,
SFX,98A82EFE,AMB_Steel_B2,AMB_Steel_B2_1,Ambience,Ambience,,,23.790,32000,4,607771,cab42ed9b44112b7,
SFX,98A82EFF:0,AMB_Steel_B3_1,AMB_Steel_B3#1,Ambience,Ambience,,,18.323,32000,2,200224,a5366b02aa271e25,
SFX,98A82EFF:1,AMB_Steel_B3_2,AMB_Steel_B3#2,Ambience,Ambience,,,28.002,32000,4,693850,4ebb25c954313e04,
SFX,98A82EFF:2,AMB_Steel_B3_3,AMB_Steel_B3#3,Ambience,Ambience,,,18.985,32000,2,200757,0ab8c0a668b21772,
SFX,998A5D81:0,PSA_Underworld_Groans_1,PSA_Underworld_Groans#1,Ambience,Ambience,,,4.977,24000,1,14907,2abb33cf9e8b4b4f,
SFX,998A5D81:1,PSA_Underworld_Groans_2,PSA_Underworld_Groans#2,Ambience,Ambience,,,6.875,24000,1,19276,ecd69d2dce5e37a7,
SFX,998A5D81:2,PSA_Underworld_Groans_3,PSA_Underworld_Groans#3,Ambience,Ambience,,,6.591,24000,1,18148,7e09b2d25c2eea3a,
SFX,998A5D81:3,PSA_Underworld_Groans_4,PSA_Underworld_Groans#4,Ambience,Ambience,,,4.287,24000,1,13144,bf602fa72cf41dfe,
SFX,998A5D81:4,PSA_Underworld_Groans_5,PSA_Underworld_Groans#5,Ambience,Ambience,,,7.211,24000,1,21936,c7a506f8097c8015,
SFX,998A5D81:5,PSA_Underworld_Groans_6,PSA_Underworld_Groans#6,Ambience,Ambience,,,3.002,24000,1,9132,09901bea0b7aeb67,
SFX,998A5D81:6,PSA_Underworld_Groans_7,PSA_Underworld_Groans#7,Ambience,Ambience,,,4.678,24000,1,14423,de40e62612dad2fd,
SFX,998A5D81:7,PSA_Underworld_Groans_8,PSA_Underworld_Groans#8,Ambience,Ambience,,,5.649,24000,1,15919,7161ab2c29c0ad02,
SFX,998A5D81:8,PSA_Underworld_Groans_9,PSA_Underworld_Groans#9,Ambience,Ambience,,,7.804,24000,1,22303,e97522fb2216e189,
SFX,998A5D81:9,PSA_Underworld_Groans_10,PSA_Underworld_Groans#10,Ambience,Ambience,,,2.753,24000,1,8077,43a3682b07706ee1,
SFX,99A8306A:0,AMB_Steel_C5_1,AMB_Steel_C5#1,Ambience,Ambience,,,11.293,32000,4,305185,d41ddb1bea9221b2,
SFX,99A8306A:1,AMB_Steel_C5_2,AMB_Steel_C5#2,Ambience,Ambience,,,11.677,32000,4,318256,ab7d96341730b0fb,
SFX,99A8306B,AMB_Steel_C4,AMB_Steel_C4_1,Ambience,Ambience,,,14.436,32000,4,402822,f43b23089b800ec3,
SFX,99A8306C,AMB_Steel_C3,AMB_Steel_C3_1,Ambience,Ambience,,,7.346,32000,2,79462,deb1ccc91d8b0f26,
SFX,99A8306D,AMB_Steel_C2,AMB_Steel_C2_1,Ambience,Ambience,,,25.600,32000,4,684810,706448449f2b4842,
SFX,99A8306E,AMB_Steel_C1,AMB_Steel_C1_1,Ambience,Ambience,,,21.419,32000,4,609944,6c7606342cf158d7,
SFX,99F8DB33:0,AMB_CW03_1,AMB_CW03#1,Ambience,Ambience,,Catwoman,89.348,32000,4,2424261,77fef44e5c5a8d08,
SFX,99F8DB33:1,AMB_CW03_2,AMB_CW03#2,Ambience,Ambience,,Catwoman,9.461,32000,4,220339,50fa995c758b6ea6,
SFX,99F8DB33:2,AMB_CW03_3,AMB_CW03#3,Ambience,Ambience,,Catwoman,49.740,32000,4,1349759,78af61a718af1b0b,
SFX,99F8DB33:3,AMB_CW03_4,AMB_CW03#4,Ambience,Ambience,,Catwoman,30.336,48000,4,902577,aaa3b73a68f71eb7,
SFX,99F8DB33:4,AMB_CW03_5,AMB_CW03#5,Ambience,Ambience,,Catwoman,14.012,32000,4,381734,d9e66d2f055f9dbd,
SFX,99F8DB33:5,AMB_CW03_6,AMB_CW03#6,Ambience,Ambience,,Catwoman,48.736,32000,4,1377117,d6084ed25e4af0d7,
SFX,9BA833B4:0,AMB_Steel_A1_1,AMB_Steel_A1#1,Ambience,Ambience,,,6.981,32000,4,188172,e3353e1a1a24de74,
SFX,9BA833B4:1,AMB_Steel_A1_2,AMB_Steel_A1#2,Ambience,Ambience,,,24.742,32000,4,675277,2bc9fd4e72b321ac,
SFX,9BA833B4:2,AMB_Steel_A1_3,AMB_Steel_A1#3,Ambience,Ambience,,,24.742,32000,4,678133,a22748615a708356,
SFX,9C8A3A75:0,AMB_RiddlerPuzzle07_1,AMB_RiddlerPuzzle07#1,Ambience,Ambience,,,35.000,32000,4,886167,d0c28304e6b065a8,
SFX,9C8A3A75:1,AMB_RiddlerPuzzle07_2,AMB_RiddlerPuzzle07#2,Ambience,Ambience,,,42.000,32000,4,1157261,31b6ec6c6c4d55c1,
SFX,9C8A3A7A:0,AMB_RiddlerPuzzle08_1,AMB_RiddlerPuzzle08#1,Ambience,Ambience,,,35.000,32000,4,886167,d0c28304e6b065a8,
SFX,9C8A3A7A:1,AMB_RiddlerPuzzle08_2,AMB_RiddlerPuzzle08#2,Ambience,Ambience,,,42.000,32000,4,1157261,31b6ec6c6c4d55c1,
SFX,9C903381,NPC_CourtCrowd,NPC_CourtCrowd_1,Ambience,Crowds,,,15.644,32000,4,429416,1ee9589220e2a052,
SFX,9DA624C5,AMB_RiddlerChallenge04,AMB_RiddlerChallenge04_1,Ambience,Ambience,,,35.157,32000,4,828243,506937146f3bb8bd,
SFX,A9AE706C:0,AMB_Train_1,AMB_Train#1,Ambience,Ambience,,,21.409,32000,4,596226,341a9d16311d8991,
SFX,A9AE706C:1,AMB_Train_2,AMB_Train#2,Ambience,Ambience,,,18.693,32000,4,546851,d3b35cb3327cad64,
SFX,A9AE706C:2,AMB_Train_3,AMB_Train#3,Ambience,Ambience,,,13.563,32000,4,398654,ef7074176a9b5675,
SFX,AA9E3CD8:0,AMB_TheatreB1_1,AMB_TheatreB1#1,Ambience,Ambience,,,36.693,32000,4,914155,fc81b138f8962af4,
SFX,AA9E3CD8:1,AMB_TheatreB1_2,AMB_TheatreB1#2,Ambience,Ambience,,,35.072,32000,4,903712,3e175597e848055c,
SFX,BA9FCA68,AMB_DemonTrials,AMB_DemonTrials_1,Ambience,Ambience,,,42.411,32000,4,1287623,822e39e9fdb73c7d,
SFX,BE349608:0,AMB_BatmanCollapse_1,AMB_BatmanCollapse#1,Ambience,Ambience,,,4.350,32000,5,132523,355a6907a49ba4f7,
SFX,BE349608:1,AMB_BatmanCollapse_2,AMB_BatmanCollapse#2,Ambience,Ambience,,,3.838,32000,5,101089,37309cd7d8f681d2,
SFX,C4445D54:0,AMB_Underworld_C2_V1_1,AMB_Underworld_C2_V1#1,Ambience,Ambience,,,15.616,32000,4,395281,328d3c9176d28670,
SFX,C4445D54:1,AMB_Underworld_C2_V1_2,AMB_Underworld_C2_V1#2,Ambience,Ambience,,,24.064,32000,4,583180,3d41248514f09cbb,
SFX,C4445D54:2,AMB_Underworld_C2_V1_3,AMB_Underworld_C2_V1#3,Ambience,Ambience,,,30.207,48000,4,815718,2d95a14358e05838,
SFX,D6C65F1E,AMB_PDLC_A1,AMB_PDLC_A1_1,Ambience,Ambience,,,41.643,32000,4,1225237,2386c71bc251bdab,
SFX,D6EB8083,AMB_NoraB1,AMB_NoraB1_1,Ambience,Ambience,,,9.461,32000,4,220339,50fa995c758b6ea6,
SFX,D7C660F3:0,AMB_PDLC_B1_1,AMB_PDLC_B1#1,Ambience,Ambience,,,29.325,32000,4,766195,7d4546670403542c,
SFX,D7C660F3:1,AMB_PDLC_B1_2,AMB_PDLC_B1#2,Ambience,Ambience,,,29.579,32000,4,834813,5785d1e4bb6cde9a,
SFX,D7C660F3:2,AMB_PDLC_B1_3,AMB_PDLC_B1#3,Ambience,Ambience,,,32.376,32000,4,865428,a0a0d2b4b83499f7,
SFX,DDAFD6E2:0,DDAFD6E2_1,DDAFD6E2#1,Unknown,Unknown,,,1.827,48000,1,10677,a7893dbe34239855,
SFX,DDAFD6E2:1,DDAFD6E2_2,DDAFD6E2#2,Unknown,Unknown,,,1.827,48000,1,10677,a7893dbe34239855,
SFX,DEA4B422:0,CHR_BM_MadHatterScream_1,CHR_BM_MadHatterScream#1,SFX,Character foley,,Batman,2.531,48000,1,20481,861dcc285c25ad64,
SFX,DEA4B422:1,CHR_BM_MadHatterScream_2,CHR_BM_MadHatterScream#2,SFX,Character foley,,Batman,1.465,48000,1,11958,7c711a9df472e6a5,
SFX,DEA4B422:2,CHR_BM_MadHatterScream_3,CHR_BM_MadHatterScream#3,SFX,Character foley,,Batman,1.910,48000,1,15540,19a30c9788222076,
SFX,DEA4B422:3,CHR_BM_MadHatterScream_4,CHR_BM_MadHatterScream#4,SFX,Character foley,,Batman,1.215,48000,1,9942,1c298f52e501f97e,
SFX,DEA4B422:4,CHR_BM_MadHatterScream_5,CHR_BM_MadHatterScream#5,SFX,Character foley,,Batman,1.584,48000,1,13284,538645f2b4c6d26a,
SFX,DEA4B422:5,CHR_BM_MadHatterScream_6,CHR_BM_MadHatterScream#6,SFX,Character foley,,Batman,1.577,48000,1,12839,3754080de3dad89a,
SFX,E8F980BE/003ACD60,Music_PDLC_003ACD60,,Music,Music (DLC),,,9.227,48000,2,140179,8908f1b982728124,
SFX,E8F980BE/004B7F03,Music_PDLC_004B7F03,,Music,Music (DLC),,,6.750,48000,2,101871,044ff907852c49e6,
SFX,E8F980BE/0070B665,Music_PDLC_0070B665,,Music,Music (DLC),,,136.744,48000,2,2147137,690c3d62410d9109,
SFX,E8F980BE/00768D8F,Music_PDLC_00768D8F,,Music,Music (DLC),,,5.167,48000,2,73843,bed749e799d3f9fc,
SFX,E8F980BE/00973366,Music_PDLC_00973366,,Music,Music (DLC),,,7.303,48000,2,102108,d798af7f50edb775,
SFX,E8F980BE/00DEE385,Music_PDLC_00DEE385,,Music,Music (DLC),,,136.744,48000,2,1671248,e526b81447e2a0b3,
SFX,E8F980BE/00EC1DE2,Music_PDLC_00EC1DE2,,Music,Music (DLC),,,7.782,48000,2,114038,7df7abee227771a6,
SFX,E8F980BE/00FF49B2,Music_PDLC_00FF49B2,,Music,Music (DLC),,,15.002,48000,2,246159,d4f4799b844e4972,
SFX,E8F980BE/0103E540,Music_PDLC_0103E540,,Music,Music (DLC),,,5.896,48000,2,93093,4c79542c96d2bc82,
SFX,E8F980BE/0143D239,Music_PDLC_0143D239,,Music,Music (DLC),,,11.245,48000,2,169362,8e1428544928412d,
SFX,E8F980BE/01B5E923,Music_PDLC_01B5E923,,Music,Music (DLC),,,7.066,48000,2,108422,10af8989c0539907,
SFX,E8F980BE/01C03EEC,Music_PDLC_01C03EEC,,Music,Music (DLC),,,5.455,48000,2,100514,ff1bed6cc4f9aac3,
SFX,E8F980BE/01C4D313,Music_PDLC_01C4D313,,Music,Music (DLC),,,11.667,48000,2,196226,30f7225e239c7468,
SFX,E8F980BE/01DF861C,Music_PDLC_01DF861C,,Music,Music (DLC),,,13.752,48000,2,208569,19ce186edc1ab5c0,
SFX,E8F980BE/022A88A0,Music_PDLC_022A88A0,,Music,Music (DLC),,,124.043,48000,2,2079213,2ab1c9712fda7b1e,
SFX,E8F980BE/023B63DB,Music_PDLC_023B63DB,,Music,Music (DLC),,,143.721,48000,2,1932671,7bc502c5161189e6,
SFX,E8F980BE/0299E758,Music_PDLC_0299E758,,Music,Music (DLC),,,120.674,48000,2,2042297,6ed6b11c34f654e7,
SFX,E8F980BE/029B900E,Music_PDLC_029B900E,,Music,Music (DLC),,,116.883,48000,2,1881727,34d02b7c101e4ef1,
SFX,E8F980BE/02D9C509,Music_PDLC_02D9C509,,Music,Music (DLC),,,124.473,48000,2,1980220,a6c69c660b4d7cbc,
SFX,E8F980BE/033EA489,Music_PDLC_033EA489,,Music,Music (DLC),,,118.667,48000,2,1863889,8c691dbb6f681bfd,
SFX,E8F980BE/033FCBD2,Music_PDLC_033FCBD2,,Music,Music (DLC),,,4.243,48000,2,61878,08bab5850ea1325b,
SFX,E8F980BE/03448FF3,Music_PDLC_03448FF3,,Music,Music (DLC),,,150.000,48000,2,2411615,297784dcc1dc7c92,
SFX,E8F980BE/034DAABC,Music_PDLC_034DAABC,,Music,Music (DLC),,,37.091,48000,2,676458,80f6ec4ed1929c82,
SFX,E8F980BE/034E5611,Music_PDLC_034E5611,,Music,Music (DLC),,,8.900,48000,2,125034,0d1788a6c0ca8bf3,
SFX,E8F980BE/036A3074,Music_PDLC_036A3074,,Music,Music (DLC),,,120.667,48000,2,1874152,3b44666cbaab231c,
SFX,E8F980BE/03AE07AB,Music_PDLC_03AE07AB,,Music,Music (DLC),,,121.411,48000,2,1888443,a2413bd7645e6d81,
SFX,E8F980BE/03BB3DC6,Music_PDLC_03BB3DC6,,Music,Music (DLC),,,118.667,48000,2,2089926,54828927e669da60,
SFX,E8F980BE/03E46F5E,Music_PDLC_03E46F5E,,Music,Music (DLC),,,121.411,48000,2,1946516,8072a4c11cd900b4,
SFX,E8F980BE/03E7A461,Music_PDLC_03E7A461,,Music,Music (DLC),,,146.512,48000,2,2031291,5da7b39e1cd1ff0f,
SFX,E8F980BE/0482586F,Music_PDLC_0482586F,,Music,Music (DLC),,,13.483,48000,2,193233,33c03a8ec1e8dcb4,
SFX,E8F980BE/0484CDE4,Music_PDLC_0484CDE4,,Music,Music (DLC),,,132.600,48000,2,2072532,d093157052677ded,
SFX,E8F980BE/050DBEEE,Music_PDLC_050DBEEE,,Music,Music (DLC),,,6.234,48000,2,94644,1bfa0843267bc39f,
SFX,E8F980BE/05333001,Music_PDLC_05333001,,Music,Music (DLC),,,120.667,48000,2,1553715,109f7693bcd39fd4,
SFX,E8F980BE/0552DAA5,Music_PDLC_0552DAA5,,Music,Music (DLC),,,99.736,48000,2,959593,22139a0192c71238,
SFX,E8F980BE/062ABA96,Music_PDLC_062ABA96,,Music,Music (DLC),,,150.000,48000,2,2309451,80a70cbe8f929197,
SFX,E8F980BE/064B6476,Music_PDLC_064B6476,,Music,Music (DLC),,,11.056,48000,2,165665,557b7c7ed6489ee0,
SFX,E8F980BE/06988828,Music_PDLC_06988828,,Music,Music (DLC),,,10.655,48000,2,153388,b8ff7b3407abcdef,
SFX,E8F980BE/06D66515,Music_PDLC_06D66515,,Music,Music (DLC),,,124.160,48000,2,2086304,c878089fa7505965,
SFX,E8F980BE/07F084BD,Music_PDLC_07F084BD,,Music,Music (DLC),,,150.000,48000,2,2276849,701309519ea2a457,
SFX,E8F980BE/0882D3F9,Music_PDLC_0882D3F9,,Music,Music (DLC),,,174.084,48000,2,2510757,3cef66645f7ac162,
SFX,E8F980BE/09899473,Music_PDLC_09899473,,Music,Music (DLC),,,118.667,48000,2,1896684,f0063f284e9c0b92,
SFX,E8F980BE/0A398DE7,Music_PDLC_0A398DE7,,Music,Music (DLC),,,37.091,48000,2,591400,b3351a5ac99fadff,
SFX,E8F980BE/0AB639E8,Music_PDLC_0AB639E8,,Music,Music (DLC),,,3.870,48000,2,53364,99876e8ee09afd87,
SFX,E8F980BE/0AC66BD5,Music_PDLC_0AC66BD5,,Music,Music (DLC),,,4.985,48000,2,78043,3ea7d2f72028d4f7,
SFX,E8F980BE/0AE6CF11,Music_PDLC_0AE6CF11,,Music,Music (DLC),,,99.736,48000,2,1496790,5a615508319e4ff0,
SFX,E8F980BE/0AEB92B2,Music_PDLC_0AEB92B2,,Music,Music (DLC),,,132.600,48000,2,2070514,5b46ec6590d1fb68,
SFX,E8F980BE/0B0A64EC,Music_PDLC_0B0A64EC,,Music,Music (DLC),,,124.043,48000,2,1808300,86e2667fc2cdfaf3,
SFX,E8F980BE/0C190AF5,Music_PDLC_0C190AF5,,Music,Music (DLC),,,116.883,48000,2,1873236,2c5b319d5f100636,
SFX,E8F980BE/0CF5DF5A,Music_PDLC_0CF5DF5A,,Music,Music (DLC),,,116.883,48000,2,1941276,2463cd547fcdd70a,
SFX,E8F980BE/0E0B47BF,Music_PDLC_0E0B47BF,,Music,Music (DLC),,,18.302,48000,2,271552,96ae88c63ce6f0b3,
SFX,E8F980BE/0FA4D723,Music_PDLC_0FA4D723,,Music,Music (DLC),,,140.930,48000,2,1786777,e45ac4b09ae534ec,
SFX,E8F980BE/1141F701,Music_PDLC_1141F701,,Music,Music (DLC),,,6.159,48000,2,91737,4d51998417d9cd7e,
SFX,E8F980BE/114EA5BA,Music_PDLC_114EA5BA,,Music,Music (DLC),,,27.051,48000,2,435229,3c6f42829f0395d7,
SFX,E8F980BE/1153EB16,Music_PDLC_1153EB16,,Music,Music (DLC),,,112.514,48000,2,1941069,90c37a7953826bff,
SFX,E8F980BE/1306E46D,Music_PDLC_1306E46D,,Music,Music (DLC),,,65.130,48000,2,1058911,9957a92e97ce244f,
SFX,E8F980BE/130E1AD0,Music_PDLC_130E1AD0,,Music,Music (DLC),,,9.714,48000,2,138900,16db4fc946de5120,
SFX,E8F980BE/136D5FB5,Music_PDLC_136D5FB5,,Music,Music (DLC),,,26.764,48000,2,383894,edbae66d79d37c71,
SFX,E8F980BE/13C55A65,Music_PDLC_13C55A65,,Music,Music (DLC),,,32.891,48000,2,515906,916faaffe6c5803d,
SFX,E8F980BE/141164AF,Music_PDLC_141164AF,,Music,Music (DLC),,,114.167,48000,2,1722804,789d705f27ac48ff,
SFX,E8F980BE/14A4A6BF,Music_PDLC_14A4A6BF,,Music,Music (DLC),,,114.167,48000,2,1925579,d0e77781234a58f8,
SFX,E8F980BE/14BE3D5F,Music_PDLC_14BE3D5F,,Music,Music (DLC),,,5.280,48000,2,75630,e17368cc20c49925,
SFX,E8F980BE/15561AC7,Music_PDLC_15561AC7,,Music,Music (DLC),,,132.600,48000,2,2266323,964e899a0c6cf057,
SFX,E8F980BE/15F943C2,Music_PDLC_15F943C2,,Music,Music (DLC),,,148.310,48000,2,2326714,9002748400da728e,
SFX,E8F980BE/19434924,Music_PDLC_19434924,,Music,Music (DLC),,,109.981,48000,2,1635385,1cfde52c13e04959,
SFX,E8F980BE/1C80531B,Music_PDLC_1C80531B,,Music,Music (DLC),,,114.167,48000,2,1823209,fa38deac3aa77635,
SFX,E8F980BE/1D0EE8DC,Music_PDLC_1D0EE8DC,,Music,Music (DLC),,,119.221,48000,2,1463918,e69ddb4be1b7b84b,
SFX,E8F980BE/1F44186D,Music_PDLC_1F44186D,,Music,Music (DLC),,,99.736,48000,2,1514907,c21bde31f78fd34a,
SFX,E8F980BE/1F889215,Music_PDLC_1F889215,,Music,Music (DLC),,,120.674,48000,2,1962204,d582d0342bc42104,
SFX,E8F980BE/1FFD945F,Music_PDLC_1FFD945F,,Music,Music (DLC),,,18.045,48000,2,259821,4aeb30f7098adbbf,
SFX,E8F980BE/24CF1409,Music_PDLC_24CF1409,,Music,Music (DLC),,,26.182,48000,2,456132,e1e55cfe4e98a17b,
SFX,E8F980BE/2714A216,Music_PDLC_2714A216,,Music,Music (DLC),,,124.043,48000,2,1981814,289af901611c4269,
SFX,E8F980BE/2AA3CEA0,Music_PDLC_2AA3CEA0,,Music,Music (DLC),,,26.182,48000,2,418694,ed57512cccf5c2be,
SFX,E8F980BE/2B07A62F,Music_PDLC_2B07A62F,,Music,Music (DLC),,,5.429,48000,2,82642,f7158a1afd44959f,
SFX,E8F980BE/3C106E75,Music_PDLC_3C106E75,,Music,Music (DLC),,,6.527,48000,2,95703,7732a64a42bac899,
SFX,E8F980BE/3C961A0D,Music_PDLC_3C961A0D,,Music,Music (DLC),,,96.021,48000,2,1405544,5b6c24b48087e2b3,
SFX,E9044EB4:0,WND_Wind_Med_1,WND_Wind_Med#1,Ambience,Wind,,,20.000,24000,2,98547,faab78aed8025611,
SFX,E9044EB4:1,WND_Wind_Med_2,WND_Wind_Med#2,Ambience,Wind,,,20.000,24000,2,96838,e882b2fe710e4515,
SFX,E9044EB4:2,WND_Wind_Med_3,WND_Wind_Med#3,Ambience,Wind,,,20.000,24000,2,95005,fb7e49936a93286f,
SFX,E9044EB4:3,WND_Wind_Med_4,WND_Wind_Med#4,Ambience,Wind,,,20.000,24000,2,98328,cafcf14b9664292d,
SFX,E9044EB4:4,WND_Wind_Med_5,WND_Wind_Med#5,Ambience,Wind,,,20.000,24000,2,98362,a69db077ae3c396d,
SFX,E9044EB4:5,WND_Wind_Med_6,WND_Wind_Med#6,Ambience,Wind,,,20.000,24000,2,98183,2e882fa0c1ccdae3,
SFX,E9044EB4:6,WND_Wind_Med_7,WND_Wind_Med#7,Ambience,Wind,,,20.000,24000,2,101052,935c5248c7aa37d8,
SFX,E9044EB4:7,WND_Wind_Med_8,WND_Wind_Med#8,Ambience,Wind,,,20.000,24000,2,101958,538515b225fac347,
SFX,E9044EB4:8,WND_Wind_Med_9,WND_Wind_Med#9,Ambience,Wind,,,20.000,24000,2,101329,5fba010811aadbb3,
SFX,E9044EB4:9,WND_Wind_Med_10,WND_Wind_Med#10,Ambience,Wind,,,20.000,24000,2,102192,65b6a2dfde8c4057,
SFX,E9044EB4:10,WND_Wind_Med_11,WND_Wind_Med#11,Ambience,Wind,,,20.000,24000,2,104947,d070102cc6bae5a6,
SFX,E9044EB4:11,WND_Wind_Med_12,WND_Wind_Med#12,Ambience,Wind,,,20.000,24000,2,98166,4299280754fe97e5,
SFX,E9044EB4:12,WND_Wind_Med_13,WND_Wind_Med#13,Ambience,Wind,,,20.000,24000,2,101738,7d6b5cb8b260d45d,
SFX,E9044EB4:13,WND_Wind_Med_14,WND_Wind_Med#14,Ambience,Wind,,,20.000,24000,2,97973,67cbca7dcb8a93bd,
SFX,E9044EB4:14,WND_Wind_Med_15,WND_Wind_Med#15,Ambience,Wind,,,20.000,24000,2,95465,fa6f6fbf194be5d9,
SFX,E9044EB4:15,WND_Wind_Med_16,WND_Wind_Med#16,Ambience,Wind,,,20.000,24000,2,94809,4a5abcc79b1e0b80,
SFX,E9044EB4:16,WND_Wind_Med_17,WND_Wind_Med#17,Ambience,Wind,,,20.000,24000,2,99949,d539e0c11f8f6ab7,
SFX,E9044EB4:17,WND_Wind_Med_18,WND_Wind_Med#18,Ambience,Wind,,,20.000,24000,2,100418,6d1eaf75c4de53ac,
SFX,E9044EB4:18,WND_Wind_Med_19,WND_Wind_Med#19,Ambience,Wind,,,20.000,24000,2,99048,2dc97369646e6958,
SFX,E9044EB4:19,WND_Wind_Med_20,WND_Wind_Med#20,Ambience,Wind,,,20.000,24000,2,97280,36dcf4ccdebdf010,
SFX,E9044EB4:20,WND_Wind_Med_21,WND_Wind_Med#21,Ambience,Wind,,,20.000,24000,2,101748,e8d6d2edc55c2cf2,
SFX,E9044EB4:21,WND_Wind_Med_22,WND_Wind_Med#22,Ambience,Wind,,,20.000,24000,2,98163,3fa3cb70185cab36,
SFX,E9044EB4:22,WND_Wind_Med_23,WND_Wind_Med#23,Ambience,Wind,,,20.000,24000,2,98468,9abafc3360d4dde0,
SFX,EBF95C27,AMB_CWB1,AMB_CWB1_1,Ambience,Ambience,,,16.717,32000,4,482955,b5ce51f30e01cc11,
SFX,EDF036D6/003CB311,Music_003CB311,,Music,Music,,,29.760,48000,2,451510,eb4cdddd0ebce65d,
SFX,EDF036D6/004B7F03,Music_004B7F03,,Music,Music,,,6.750,48000,2,101871,044ff907852c49e6,
SFX,EDF036D6/006AC1CF,Music_006AC1CF,,Music,Music,,,8.000,48000,2,121670,ba0f4a31a96bbf0a,
SFX,EDF036D6/00768D8F,Music_00768D8F,,Music,Music,,,5.167,48000,2,73843,bed749e799d3f9fc,
SFX,EDF036D6/0077966A,Music_0077966A,,Music,Music,,,139.862,48000,2,2240069,0aa0ab711de88e99,
SFX,EDF036D6/0091757B,Music_0091757B,,Music,Music,,,136.358,48000,2,2079414,036ba4f5985870f5,
SFX,EDF036D6/0094ECE0,Music_0094ECE0,,Music,Music,,,28.000,48000,2,427330,c809d53c9ac3a4ff,
SFX,EDF036D6/00973366,Music_00973366,,Music,Music,,,7.303,48000,2,102108,d798af7f50edb775,
SFX,EDF036D6/00CF19F6,Music_00CF19F6,,Music,Music,,,36.651,48000,2,652015,21fd45e0ab2f46a3,
SFX,EDF036D6/00EC1DE2,Music_00EC1DE2,,Music,Music,,,7.782,48000,2,114038,7df7abee227771a6,
SFX,EDF036D6/00FA6232,Music_00FA6232,,Music,Music,,,136.744,48000,2,2147137,690c3d62410d9109,
SFX,EDF036D6/00FF49B2,Music_00FF49B2,,Music,Music,,,15.002,48000,2,246159,d4f4799b844e4972,
SFX,EDF036D6/0103E540,Music_0103E540,,Music,Music,,,5.896,48000,2,93093,4c79542c96d2bc82,
SFX,EDF036D6/0143D239,Music_0143D239,,Music,Music,,,11.245,48000,2,169362,8e1428544928412d,
SFX,EDF036D6/01714272,Music_01714272,,Music,Music,,,134.234,48000,2,1903999,ac187b1ed9b3e297,
SFX,EDF036D6/0187CAC4,Music_0187CAC4,,Music,Music,,,5.969,48000,2,85367,fcca428404e1ee90,
SFX,EDF036D6/018F4EE2,Music_018F4EE2,,Music,Music,,,15.360,48000,2,267100,ff8bebf954391f4f,
SFX,EDF036D6/01916433,Music_01916433,,Music,Music,,,18.100,48000,2,262068,cdee9195621576d5,
SFX,EDF036D6/01B5E923,Music_01B5E923,,Music,Music,,,7.066,48000,2,108422,10af8989c0539907,
SFX,EDF036D6/01C03EEC,Music_01C03EEC,,Music,Music,,,5.455,48000,2,100514,ff1bed6cc4f9aac3,
SFX,EDF036D6/01C4D313,Music_01C4D313,,Music,Music,,,11.667,48000,2,196226,30f7225e239c7468,
SFX,EDF036D6/01C73384,Music_01C73384,,Music,Music,,,21.404,48000,2,307229,4b1412e1904010f6,
SFX,EDF036D6/01DF861C,Music_01DF861C,,Music,Music,,,13.752,48000,2,208569,19ce186edc1ab5c0,
SFX,EDF036D6/022A88A0,Music_022A88A0,,Music,Music,,,124.043,48000,2,2079213,2ab1c9712fda7b1e,
SFX,EDF036D6/0236F058,Music_0236F058,,Music,Music,,,10.339,48000,2,157180,f69af5f5fa7089cc,
SFX,EDF036D6/023B63DB,Music_023B63DB,,Music,Music,,,143.721,48000,2,1932671,7bc502c5161189e6,
SFX,EDF036D6/0299E758,Music_0299E758,,Music,Music,,,120.674,48000,2,2042297,6ed6b11c34f654e7,
SFX,EDF036D6/02BD77FA,Music_02BD77FA,,Music,Music,,,4.177,48000,2,67717,eb9870b55a8edf72,
SFX,EDF036D6/030666A2,Music_030666A2,,Music,Music,,,2.118,48000,2,30627,9c49ed214407c3f7,
SFX,EDF036D6/030EAAF4,Music_030EAAF4,,Music,Music,,,27.001,48000,2,417402,a8aae159b0b70e0f,
SFX,EDF036D6/033EA489,Music_033EA489,,Music,Music,,,118.667,48000,2,1863889,8c691dbb6f681bfd,
SFX,EDF036D6/033FCBD2,Music_033FCBD2,,Music,Music,,,4.243,48000,2,61878,08bab5850ea1325b,
SFX,EDF036D6/03448FF3,Music_03448FF3,,Music,Music,,,150.000,48000,2,2411615,297784dcc1dc7c92,
SFX,EDF036D6/036A3074,Music_036A3074,,Music,Music,,,120.667,48000,2,1874152,3b44666cbaab231c,
SFX,EDF036D6/03A77EDB,Music_03A77EDB,,Music,Music,,,5.056,48000,2,74536,0358b137e007ef69,
SFX,EDF036D6/03AE07AB,Music_03AE07AB,,Music,Music,,,121.411,48000,2,1888443,a2413bd7645e6d81,
SFX,EDF036D6/03BB3DC6,Music_03BB3DC6,,Music,Music,,,118.667,48000,2,2089926,54828927e669da60,
SFX,EDF036D6/03CF5072,Music_03CF5072,,Music,Music,,,5.355,48000,2,77471,8c8e11b1b6730eee,
SFX,EDF036D6/03E46F5E,Music_03E46F5E,,Music,Music,,,121.411,48000,2,1946516,8072a4c11cd900b4,
SFX,EDF036D6/03E7A461,Music_03E7A461,,Music,Music,,,146.512,48000,2,2031291,5da7b39e1cd1ff0f,
SFX,EDF036D6/03EF5052,Music_03EF5052,,Music,Music,,,10.933,48000,2,165614,0a81b7aed4e19666,
SFX,EDF036D6/03F43679,Music_03F43679,,Music,Music,,,65.479,48000,2,910455,136a1d2be974aca4,
SFX,EDF036D6/03F81E23,Music_03F81E23,,Music,Music,,,12.827,48000,2,220428,346c196b48cc25f1,
SFX,EDF036D6/041B0207,Music_041B0207,,Music,Music,,,7.273,48000,2,115780,3b54e829d62e2cdd,
SFX,EDF036D6/041B3ED1,Music_041B3ED1,,Music,Music,,,136.571,48000,2,2156010,55794b221524244e,
SFX,EDF036D6/045740E7,Music_045740E7,,Music,Music,,,124.473,48000,2,1980220,a6c69c660b4d7cbc,
SFX,EDF036D6/04626D74,Music_04626D74,,Music,Music,,,33.231,48000,2,497870,7c8229f48538d37f,
SFX,EDF036D6/046D8256,Music_046D8256,,Music,Music,,,116.883,48000,2,1881727,34d02b7c101e4ef1,
SFX,EDF036D6/0482586F,Music_0482586F,,Music,Music,,,13.483,48000,2,193233,33c03a8ec1e8dcb4,
SFX,EDF036D6/0484CDE4,Music_0484CDE4,,Music,Music,,,132.600,48000,2,2072532,d093157052677ded,
SFX,EDF036D6/04A296D5,Music_04A296D5,,Music,Music,,,139.862,48000,2,2050965,ed4d836843d773e2,
SFX,EDF036D6/04C378D7,Music_04C378D7,,Music,Music,,,8.048,48000,2,121070,ddb4f685b987aa77,
SFX,EDF036D6/04DBE3AB,Music_04DBE3AB,,Music,Music,,,59.521,48000,2,818632,1fc309fc1ad2e944,
SFX,EDF036D6/0518B1B7,Music_0518B1B7,,Music,Music,,,11.427,48000,2,169289,2c178bbdbf1925b9,
SFX,EDF036D6/05333001,Music_05333001,,Music,Music,,,120.667,48000,2,1553715,109f7693bcd39fd4,
SFX,EDF036D6/05470280,Music_05470280,,Music,Music,,,128.764,48000,2,1746550,b33dd74c1bb0a7f1,
SFX,EDF036D6/05513772,Music_05513772,,Music,Music,,,120.674,48000,2,1815939,ef2aa623f394818d,
SFX,EDF036D6/0552DAA5,Music_0552DAA5,,Music,Music,,,99.736,48000,2,959593,22139a0192c71238,
SFX,EDF036D6/058231ED,Music_058231ED,,Music,Music,,,68.211,48000,2,1138245,86df0acdfdfc1df0,
SFX,EDF036D6/0587577E,Music_0587577E,,Music,Music,,,139.862,48000,2,2198296,560ce46975841a7e,
SFX,EDF036D6/05C8D32A,Music_05C8D32A,,Music,Music,,,26.526,48000,2,343088,73bef0da1727bba3,
SFX,EDF036D6/060E415F,Music_060E415F,,Music,Music,,,11.654,48000,2,161817,8f0c7e25073d4374,
SFX,EDF036D6/062ABA96,Music_062ABA96,,Music,Music,,,150.000,48000,2,2309451,80a70cbe8f929197,
SFX,EDF036D6/064B6476,Music_064B6476,,Music,Music,,,11.056,48000,2,165665,557b7c7ed6489ee0,
SFX,EDF036D6/06503115,Music_06503115,,Music,Music,,,57.931,48000,2,1145563,979b62622e8d74dd,
SFX,EDF036D6/067C3EF1,Music_067C3EF1,,Music,Music,,,10.288,48000,2,144367,4b61f96005d0861e,
SFX,EDF036D6/06963560,Music_06963560,,Music,Music,,,10.541,48000,2,169361,4684815a1a026ef9,
SFX,EDF036D6/06988828,Music_06988828,,Music,Music,,,10.655,48000,2,153388,b8ff7b3407abcdef,
SFX,EDF036D6/06AD24B7,Music_06AD24B7,,Music,Music,,,13.664,48000,2,216979,2e9d08baf192bc27,
SFX,EDF036D6/06D66515,Music_06D66515,,Music,Music,,,124.160,48000,2,2086304,c878089fa7505965,
SFX,EDF036D6/06E7FD0F,Music_06E7FD0F,,Music,Music,,,220.337,48000,2,3283587,97a25b3b1e86da70,
SFX,EDF036D6/06F1B02B,Music_06F1B02B,,Music,Music,,,10.909,48000,2,175394,906d1ab34e0676c5,
SFX,EDF036D6/0748B510,Music_0748B510,,Music,Music,,,4.637,48000,2,65408,c19c38f670f623da,
SFX,EDF036D6/0792E524,Music_0792E524,,Music,Music,,,63.140,48000,2,953656,401d370d42bcacf2,
SFX,EDF036D6/0793DE7C,Music_0793DE7C,,Music,Music,,,34.415,48000,2,490514,da62323a54973ef5,
SFX,EDF036D6/07C1C384,Music_07C1C384,,Music,Music,,,7.579,48000,2,127218,624b4083a829d00a,
SFX,EDF036D6/07D74727,Music_07D74727,,Music,Music,,,5.455,48000,2,83321,e7586180df9e15d2,
SFX,EDF036D6/07D7BB90,Music_07D7BB90,,Music,Music,,,128.764,48000,2,2069659,b28e2b67e237e1d4,
SFX,EDF036D6/07ED9BED,Music_07ED9BED,,Music,Music,,,5.000,48000,2,83513,3ac88d66cc7e2af4,
SFX,EDF036D6/07F084BD,Music_07F084BD,,Music,Music,,,150.000,48000,2,2276849,701309519ea2a457,
SFX,EDF036D6/08028AF6,Music_08028AF6,,Music,Music,,,27.032,48000,2,447037,3ea40013c34c5bfb,
SFX,EDF036D6/08350005,Music_08350005,,Music,Music,,,10.163,48000,2,163179,53b8f892cec18939,
SFX,EDF036D6/0879D4B8,Music_0879D4B8,,Music,Music,,,31.054,48000,2,432075,b0eae4067e80b8a4,
SFX,EDF036D6/08B01ABF,Music_08B01ABF,,Music,Music,,,5.920,48000,2,104192,fe272d172651fd13,
SFX,EDF036D6/08B289F1,Music_08B289F1,,Music,Music,,,59.951,48000,2,968149,3c93984f24d71dcf,
SFX,EDF036D6/08DEC601,Music_08DEC601,,Music,Music,,,7.984,48000,2,118158,4dc163d490d6b082,
SFX,EDF036D6/08FDF59F,Music_08FDF59F,,Music,Music,,,4.027,48000,2,59853,d642287171dac178,
SFX,EDF036D6/08FEB583,Music_08FEB583,,Music,Music,,,111.019,48000,2,1957859,e51988fee897cf9a,
SFX,EDF036D6/09268C8B,Music_09268C8B,,Music,Music,,,51.796,48000,2,792757,5ed884a66cbd99f4,
SFX,EDF036D6/09475E4C,Music_09475E4C,,Music,Music,,,59.200,48000,3,1046008,744aa70e18aeb5bd,
SFX,EDF036D6/0963C63E,Music_0963C63E,,Music,Music,,,136.744,48000,2,1671248,e526b81447e2a0b3,
SFX,EDF036D6/09662D97,Music_09662D97,,Music,Music,,,51.393,48000,2,705497,a886aa3314db3230,
SFX,EDF036D6/0966308B,Music_0966308B,,Music,Music,,,145.655,48000,2,2252265,c226fa9ab4947989,
SFX,EDF036D6/09899473,Music_09899473,,Music,Music,,,118.667,48000,2,1896684,f0063f284e9c0b92,
SFX,EDF036D6/09B90B1F,Music_09B90B1F,,Music,Music,,,12.957,48000,2,191397,4f07c23037242a86,
SFX,EDF036D6/09BCB885,Music_09BCB885,,Music,Music,,,27.183,48000,2,355145,452006f7d55b7542,
SFX,EDF036D6/09DF28BC,Music_09DF28BC,,Music,Music,,,8.900,48000,2,125034,0d1788a6c0ca8bf3,
SFX,EDF036D6/0A270868,Music_0A270868,,Music,Music,,,56.114,48000,2,829545,9b3d70dfd1d0bf7d,
SFX,EDF036D6/0A3391C4,Music_0A3391C4,,Music,Music,,,56.034,48000,2,862686,f56d0c0a35170987,
SFX,EDF036D6/0A35D517,Music_0A35D517,,Music,Music,,,49.232,48000,2,695149,6156c833d17fa9d5,
SFX,EDF036D6/0A398DE7,Music_0A398DE7,,Music,Music,,,37.091,48000,2,591407,ac67b2eaff5065d5,
SFX,EDF036D6/0A45BC45,Music_0A45BC45,,Music,Music,,,7.026,48000,2,110650,a81eeb8497dc6087,
SFX,EDF036D6/0A514BC5,Music_0A514BC5,,Music,Music,,,5.031,48000,2,69166,27a2d8a8524eabdc,
SFX,EDF036D6/0A6A4BEA,Music_0A6A4BEA,,Music,Music,,,120.674,48000,2,1998826,63ec01f5a15e3625,
SFX,EDF036D6/0AB639E8,Music_0AB639E8,,Music,Music,,,3.870,48000,2,53364,99876e8ee09afd87,
SFX,EDF036D6/0AE2EB37,Music_0AE2EB37,,Music,Music,,,9.227,48000,2,140179,8908f1b982728124,
SFX,EDF036D6/0AE6CF11,Music_0AE6CF11,,Music,Music,,,99.736,48000,2,1496790,5a615508319e4ff0,
SFX,EDF036D6/0AE6E55A,Music_0AE6E55A,,Music,Music,,,5.732,48000,2,78011,f1ce12bc172ee924,
SFX,EDF036D6/0B074B6F,Music_0B074B6F,,Music,Music,,,15.401,48000,2,236110,164b12aebce490d7,
SFX,EDF036D6/0B0A64EC,Music_0B0A64EC,,Music,Music,,,124.043,48000,2,1808300,86e2667fc2cdfaf3,
SFX,EDF036D6/0B591BEA,Music_0B591BEA,,Music,Music,,,9.600,48000,2,137803,8742c5db432792e2,
SFX,EDF036D6/0B8D3FA7,Music_0B8D3FA7,,Music,Music,,,37.091,48000,2,676472,c80ab19d29f54db2,
SFX,EDF036D6/0B934098,Music_0B934098,,Music,Music,,,130.386,48000,6,5433905,542486c69ab21c73,
SFX,EDF036D6/0BAFE185,Music_0BAFE185,,Music,Music,,,120.000,48000,2,1870515,af2b9390c746d55a,
SFX,EDF036D6/0BCB4DFB,Music_0BCB4DFB,,Music,Music,,,11.194,48000,2,157866,1893d0875bc0324c,
SFX,EDF036D6/0BD2F54C,Music_0BD2F54C,,Music,Music,,,4.000,48000,2,62058,c1364139c5ab6796,
SFX,EDF036D6/0BE54B8B,Music_0BE54B8B,,Music,Music,,,82.587,48000,2,1154038,b91f2a7731e80d93,
SFX,EDF036D6/0C190AF5,Music_0C190AF5,,Music,Music,,,116.883,48000,2,1873236,2c5b319d5f100636,
SFX,EDF036D6/0C786C68,Music_0C786C68,,Music,Music,,,25.455,48000,2,374798,9f32e428d0415a75,
SFX,EDF036D6/0CF5DF5A,Music_0CF5DF5A,,Music,Music,,,116.883,48000,2,1941276,2463cd547fcdd70a,
SFX,EDF036D6/0D080AF2,Music_0D080AF2,,Music,Music,,,24.841,48000,2,227956,3f0d0f61677ac580,
SFX,EDF036D6/0D116946,Music_0D116946,,Music,Music,,,136.571,48000,2,2261132,683f0bf6dc70b451,
SFX,EDF036D6/0D1D673E,Music_0D1D673E,,Music,Music,,,12.809,48000,2,217995,78bab68bfcaa5d43,
SFX,EDF036D6/0D395255,Music_0D395255,,Music,Music,,,114.880,48000,2,1771509,e2cdde27731d21d9,
SFX,EDF036D6/0D5E8A4E,Music_0D5E8A4E,,Music,Music,,,34.172,48000,6,620536,63c45a778bf37bf5,
SFX,EDF036D6/0D9F4730,Music_0D9F4730,,Music,Music,,,114.884,48000,2,1706066,42cfd6f97989241c,
SFX,EDF036D6/0DA55A9A,Music_0DA55A9A,,Music,Music,,,67.830,24000,2,349704,a4a8b9249bdc87ef,
SFX,EDF036D6/0DCDB7EA,Music_0DCDB7EA,,Music,Music,,,61.519,48000,6,1138993,4f2c30017b90638f,
SFX,EDF036D6/0DCF4A58,Music_0DCF4A58,,Music,Music,,,25.703,48000,2,365874,3cbd811d75dec15e,
SFX,EDF036D6/0DD4CB01,Music_0DD4CB01,,Music,Music,,,9.230,48000,2,132559,3b88efd559836afd,
SFX,EDF036D6/0DD67C32,Music_0DD67C32,,Music,Music,,,132.600,48000,2,2070514,5b46ec6590d1fb68,
SFX,EDF036D6/0DD82CDF,Music_0DD82CDF,,Music,Music,,,66.801,48000,2,973802,5632edb072828857,
SFX,EDF036D6/0E0B47BF,Music_0E0B47BF,,Music,Music,,,18.302,48000,2,271552,96ae88c63ce6f0b3,
SFX,EDF036D6/0E0D5646,Music_0E0D5646,,Music,Music,,,10.909,48000,2,179745,740b7105d6b42ff6,
SFX,EDF036D6/0E0F52CB,Music_0E0F52CB,,Music,Music,,,7.579,48000,2,102397,73ef4f5e53d2b4b1,
SFX,EDF036D6/0E6EA120,Music_0E6EA120,,Music,Music,,,4.985,48000,2,78043,3ea7d2f72028d4f7,
SFX,EDF036D6/0E8284B0,Music_0E8284B0,,Music,Music,,,13.633,48000,2,200731,0fb0d5eaea927482,
SFX,EDF036D6/0EC73393,Music_0EC73393,,Music,Music,,,56.689,48000,2,843627,8bce3f68a65ae479,
SFX,EDF036D6/0F1BB771,Music_0F1BB771,,Music,Music,,,5.357,48000,2,79456,98a6143e52c6fe9c,
SFX,EDF036D6/0F2E5BE7,Music_0F2E5BE7,,Music,Music,,,115.955,48000,2,1780926,7f34763c2bac1cbc,
SFX,EDF036D6/0F927604,Music_0F927604,,Music,Music,,,4.100,48000,2,60427,341074b2bcd2cc3e,
SFX,EDF036D6/0FA4D723,Music_0FA4D723,,Music,Music,,,140.930,48000,2,1786777,e45ac4b09ae534ec,
SFX,EDF036D6/100725CE,Music_100725CE,,Music,Music,,,138.805,48000,2,1988903,aede3b9c498119bd,
SFX,EDF036D6/103350C6,Music_103350C6,,Music,Music,,,10.706,48000,2,163488,6cd9394131897d8f,
SFX,EDF036D6/1045B2D0,Music_1045B2D0,,Music,Music,,,26.318,48000,2,439396,49ad8f1567a94ab2,
SFX,EDF036D6/107651F0,Music_107651F0,,Music,Music,,,11.031,48000,2,154397,dc018d1059ac9132,
SFX,EDF036D6/10CEC694,Music_10CEC694,,Music,Music,,,21.783,48000,2,337478,838175d3a11af025,
SFX,EDF036D6/10D95181,Music_10D95181,,Music,Music,,,62.161,48000,2,943143,4ec6adc0ff80df6f,
SFX,EDF036D6/10DD7E39,Music_10DD7E39,,Music,Music,,,41.038,48000,2,574465,c073bbdbf9162a17,
SFX,EDF036D6/10EF999B,Music_10EF999B,,Music,Music,,,7.836,48000,2,54377,30f28b938ba61758,
SFX,EDF036D6/11011607,Music_11011607,,Music,Music,,,64.928,48000,2,1008393,2f84bf32973e615f,
SFX,EDF036D6/11213CD9,Music_11213CD9,,Music,Music,,,5.472,48000,2,74798,5636cd7483facacc,
SFX,EDF036D6/1141F701,Music_1141F701,,Music,Music,,,6.159,48000,2,91737,4d51998417d9cd7e,
SFX,EDF036D6/114C2C42,Music_114C2C42,,Music,Music,,,57.931,48000,2,1210901,d7ecc9ca26aca3d3,
SFX,EDF036D6/114EA5BA,Music_114EA5BA,,Music,Music,,,27.051,48000,2,435215,cb7c21d127a57bda,
SFX,EDF036D6/114F6595,Music_114F6595,,Music,Music,,,137.553,48000,2,2138964,6ea746351dcabf9c,
SFX,EDF036D6/11749351,Music_11749351,,Music,Music,,,110.909,48000,2,1700949,cc8c7f8fe193a36d,
SFX,EDF036D6/1184538D,Music_1184538D,,Music,Music,,,57.931,48000,2,836829,7ae3a034a1a14842,
SFX,EDF036D6/11B3505F,Music_11B3505F,,Music,Music,,,119.040,48000,2,1804042,7ea5b6c29fc28a96,
SFX,EDF036D6/11D06D0C,Music_11D06D0C,,Music,Music,,,64.669,48000,2,960537,28e584eeced2b6ce,
SFX,EDF036D6/11D477D1,Music_11D477D1,,Music,Music,,,69.629,48000,2,976260,12a1237d1346aa35,
SFX,EDF036D6/1231904E,Music_1231904E,,Music,Music,,,5.472,48000,2,79207,828e7962c313978d,
SFX,EDF036D6/12CB7EA2,Music_12CB7EA2,,Music,Music,,,21.063,48000,2,306437,d13a3193ca5a8541,
SFX,EDF036D6/12DFC5A3,Music_12DFC5A3,,Music,Music,,,120.674,48000,2,1905166,3722d236172a35dd,
SFX,EDF036D6/12F0D318,Music_12F0D318,,Music,Music,,,25.576,48000,2,460855,970213287c121e61,
SFX,EDF036D6/130425A1,Music_130425A1,,Music,Music,,,4.638,48000,2,69039,39d636a6c20cb4a9,
SFX,EDF036D6/1304DD30,Music_1304DD30,,Music,Music,,,6.124,48000,2,79539,485a9a0e2a46c58a,
SFX,EDF036D6/1306E46D,Music_1306E46D,,Music,Music,,,65.130,48000,2,1058911,9957a92e97ce244f,
SFX,EDF036D6/130E1AD0,Music_130E1AD0,,Music,Music,,,9.714,48000,2,138900,16db4fc946de5120,
SFX,EDF036D6/134A4A91,Music_134A4A91,,Music,Music,,,12.488,48000,2,176307,69bfef90f2dacf24,
SFX,EDF036D6/136D5FB5,Music_136D5FB5,,Music,Music,,,26.764,48000,2,383894,edbae66d79d37c71,
SFX,EDF036D6/1388391B,Music_1388391B,,Music,Music,,,92.559,48000,2,1237011,7d966e508d118f35,
SFX,EDF036D6/138BDCCF,Music_138BDCCF,,Music,Music,,,7.680,48000,2,109178,65e4ba1a2297baa1,
SFX,EDF036D6/139E54D8,Music_139E54D8,,Music,Music,,,7.321,48000,2,119246,672ff00e9ef04c26,
SFX,EDF036D6/13A0FCF1,Music_13A0FCF1,,Music,Music,,,33.822,48000,6,620811,113344a096e024c9,
SFX,EDF036D6/13D03B5C,Music_13D03B5C,,Music,Music,,,124.148,48000,2,1917760,243ae8dcce359a64,
SFX,EDF036D6/13FC57BF,Music_13FC57BF,,Music,Music,,,11.864,48000,2,175369,b9851e6d3b65b196,
SFX,EDF036D6/141164AF,Music_141164AF,,Music,Music,,,114.167,48000,2,1722804,789d705f27ac48ff,
SFX,EDF036D6/1417F135,Music_1417F135,,Music,Music,,,18.416,48000,2,252360,502fb3a6747f1edc,
SFX,EDF036D6/142BD546,Music_142BD546,,Music,Music,,,33.600,48000,2,469861,72b38f1a2d26dd8f,
SFX,EDF036D6/145794F0,Music_145794F0,,Music,Music,,,4.667,48000,2,65468,3b6e4b9c36e9809b,
SFX,EDF036D6/149379DA,Music_149379DA,,Music,Music,,,11.324,48000,2,155217,42e0d077e4514dce,
SFX,EDF036D6/14949A24,Music_14949A24,,Music,Music,,,68.211,48000,2,1014148,c63a08eb40b2c73c,
SFX,EDF036D6/14BE3D5F,Music_14BE3D5F,,Music,Music,,,5.280,48000,2,75630,e17368cc20c49925,
SFX,EDF036D6/14C7314B,Music_14C7314B,,Music,Music,,,7.083,48000,2,110811,c4cc9738a29f1c64,
SFX,EDF036D6/14E1DF85,Music_14E1DF85,,Music,Music,,,6.998,48000,2,102881,6aa334d345da108a,
SFX,EDF036D6/15166641,Music_15166641,,Music,Music,,,9.931,48000,2,120665,66da27b2a11d71bb,
SFX,EDF036D6/1549DDB2,Music_1549DDB2,,Music,Music,,,37.091,48000,2,595527,01355f355c0b2853,
SFX,EDF036D6/15561AC7,Music_15561AC7,,Music,Music,,,132.600,48000,2,2266323,964e899a0c6cf057,
SFX,EDF036D6/15F24B15,Music_15F24B15,,Music,Music,,,7.788,48000,2,122403,4017b5d94a68cd74,
SFX,EDF036D6/163C8641,Music_163C8641,,Music,Music,,,30.583,48000,2,418441,602775842cc2eea1,
SFX,EDF036D6/166847B7,Music_166847B7,,Music,Music,,,11.272,48000,2,156467,86814d3b24ff9b7e,
SFX,EDF036D6/168E0EDC,Music_168E0EDC,,Music,Music,,,64.527,48000,2,899368,3cf382b0b081a7ba,
SFX,EDF036D6/16AEE4FD,Music_16AEE4FD,,Music,Music,,,4.739,48000,2,75711,5e466c8bf9a4d172,
SFX,EDF036D6/16C59E4D,Music_16C59E4D,,Music,Music,,,27.006,48000,2,479283,4171a30575d9123e,
SFX,EDF036D6/17A65BC8,Music_17A65BC8,,Music,Music,,,51.855,48000,2,735688,93e09affa01d21b5,
SFX,EDF036D6/17B8E786,Music_17B8E786,,Music,Music,,,6.234,48000,2,94644,1bfa0843267bc39f,
SFX,EDF036D6/18177B73,Music_18177B73,,Music,Music,,,49.880,48000,2,737499,888ab9710ee6bc1d,
SFX,EDF036D6/187BD87F,Music_187BD87F,,Music,Music,,,27.580,48000,2,407557,a0d79446b1b33012,
SFX,EDF036D6/18CE2848,Music_18CE2848,,Music,Music,,,16.726,48000,2,188870,60ab2664a3f64afd,
SFX,EDF036D6/190E481E,Music_190E481E,,Music,Music,,,56.486,48000,2,804642,5d6b1680a58b085a,
SFX,EDF036D6/1951CABD,Music_1951CABD,,Music,Music,,,20.105,48000,2,331402,4530a35f0898041a,
SFX,EDF036D6/1A2A956A,Music_1A2A956A,,Music,Music,,,120.674,48000,2,1773755,c5b6ce6781144d37,
SFX,EDF036D6/1B55A21D,Music_1B55A21D,,Music,Music,,,148.310,48000,2,2326714,9002748400da728e,
SFX,EDF036D6/1B995440,Music_1B995440,,Music,Music,,,82.719,48000,2,1230786,4eea50935d8861bb,
SFX,EDF036D6/1BB0C4AD,Music_1BB0C4AD,,Music,Music,,,136.568,48000,2,1695285,537e4507c59cb4dd,
SFX,EDF036D6/1C0025EC,Music_1C0025EC,,Music,Music,,,55.204,48000,2,851331,ae51ad07eadff2a0,
SFX,EDF036D6/1C0067DB,Music_1C0067DB,,Music,Music,,,174.084,48000,2,2510757,3cef66645f7ac162,
SFX,EDF036D6/1C070DC7,Music_1C070DC7,,Music,Music,,,15.158,48000,2,243328,7c2eb9df93a9dba7,
SFX,EDF036D6/1D5BDC25,Music_1D5BDC25,,Music,Music,,,30.005,48000,2,499008,d8217f8ccde1a7c6,
SFX,EDF036D6/1D9BCFEA,Music_1D9BCFEA,,Music,Music,,,27.051,48000,2,346917,4f5341cdf41e3dfe,
SFX,EDF036D6/1D9C5132,Music_1D9C5132,,Music,Music,,,18.876,48000,2,334388,c3d9f7b58e388452,
SFX,EDF036D6/1DBF5552,Music_1DBF5552,,Music,Music,,,124.212,48000,2,1883930,905e1ec95b5de2c1,
SFX,EDF036D6/1DC5D065,Music_1DC5D065,,Music,Music,,,49.865,48000,6,909285,0c96d8d8a0e119c7,
SFX,EDF036D6/1DE02287,Music_1DE02287,,Music,Music,,,128.764,48000,2,1957556,b3e0906c43c188af,
SFX,EDF036D6/1E326B83,Music_1E326B83,,Music,Music,,,13.653,48000,2,215358,a5334288e71969f0,
SFX,EDF036D6/1E7F8C2D,Music_1E7F8C2D,,Music,Music,,,10.645,48000,2,163632,61797286fd511a82,
SFX,EDF036D6/1ED075D1,Music_1ED075D1,,Music,Music,,,10.909,48000,2,116934,c6116d638313c3e5,
SFX,EDF036D6/1ED19140,Music_1ED19140,,Music,Music,,,118.144,48000,2,1977830,cd18434c96a33371,
SFX,EDF036D6/1EDB523D,Music_1EDB523D,,Music,Music,,,57.989,48000,2,978464,a9303a561db1736c,
SFX,EDF036D6/1F44015A,Music_1F44015A,,Music,Music,,,150.000,48000,2,2235862,ff86dc19c70f97be,
SFX,EDF036D6/1F889215,Music_1F889215,,Music,Music,,,120.674,48000,2,1962204,d582d0342bc42104,
SFX,EDF036D6/1FFD945F,Music_1FFD945F,,Music,Music,,,18.045,48000,2,259821,4aeb30f7098adbbf,
SFX,EDF036D6/204449FE,Music_204449FE,,Music,Music,,,49.892,48000,2,717441,b1db8172723811e4,
SFX,EDF036D6/2071230E,Music_2071230E,,Music,Music,,,99.736,48000,2,1514907,c21bde31f78fd34a,
SFX,EDF036D6/20857A8E,Music_20857A8E,,Music,Music,,,109.981,48000,2,1635385,1cfde52c13e04959,
SFX,EDF036D6/2105D597,Music_2105D597,,Music,Music,,,9.600,48000,2,98961,557f70acbbcfc2fe,
SFX,EDF036D6/2113CD65,Music_2113CD65,,Music,Music,,,112.514,48000,2,1941069,90c37a7953826bff,
SFX,EDF036D6/2199A947,Music_2199A947,,Music,Music,,,124.212,48000,2,1946256,b94df883b92bf6c7,
SFX,EDF036D6/2239696F,Music_2239696F,,Music,Music,,,14.659,48000,2,222484,899cd11416591458,
SFX,EDF036D6/22C833C6,Music_22C833C6,,Music,Music,,,41.468,48000,2,586179,8e5ece33b943d4de,
SFX,EDF036D6/23207A9C,Music_23207A9C,,Music,Music,,,150.000,48000,2,2261291,4f45d4cb0fe64351,
SFX,EDF036D6/23F9F43D,Music_23F9F43D,,Music,Music,,,100.231,48000,2,1638853,f9e28ef5ed84a60a,
SFX,EDF036D6/242766B2,Music_242766B2,,Music,Music,,,32.891,48000,2,515906,916faaffe6c5803d,
SFX,EDF036D6/2448BD91,Music_2448BD91,,Music,Music,,,5.724,48000,2,83185,d115a598c998aef9,
SFX,EDF036D6/246625C9,Music_246625C9,,Music,Music,,,114.167,48000,2,1823209,fa38deac3aa77635,
SFX,EDF036D6/24FEAAA6,Music_24FEAAA6,,Music,Music,,,4.999,48000,2,68925,fdccc549d81b3b54,
SFX,EDF036D6/25A00909,Music_25A00909,,Music,Music,,,4.007,48000,2,56999,30c67fa1ef9c0000,
SFX,EDF036D6/25A7D9DB,Music_25A7D9DB,,Music,Music,,,49.876,48000,2,719556,7b4ef5b74c5fcc22,
SFX,EDF036D6/25D08F49,Music_25D08F49,,Music,Music,,,5.241,48000,2,79549,1e6a7c7129cb3843,
SFX,EDF036D6/25F4F497,Music_25F4F497,,Music,Music,,,8.971,48000,2,147262,6e20fc042dd125cf,
SFX,EDF036D6/263E3B2E,Music_263E3B2E,,Music,Music,,,120.674,48000,2,1962177,0f512c818b122acf,
SFX,EDF036D6/26F6662B,Music_26F6662B,,Music,Music,,,84.268,48000,2,1252553,937b034854575c25,
SFX,EDF036D6/2714A216,Music_2714A216,,Music,Music,,,124.043,48000,2,1981814,289af901611c4269,
SFX,EDF036D6/27B481B5,Music_27B481B5,,Music,Music,,,42.180,48000,2,590926,6aee62ad4d7686de,
SFX,EDF036D6/286EAFBC,Music_286EAFBC,,Music,Music,,,25.576,48000,2,464876,01f9a32a5c85562c,
SFX,EDF036D6/291F6742,Music_291F6742,,Music,Music,,,52.323,48000,2,804372,425f49258782e7ac,
SFX,EDF036D6/29390786,Music_29390786,,Music,Music,,,6.661,48000,2,101067,7c84fa8ca1da620d,
SFX,EDF036D6/294C967B,Music_294C967B,,Music,Music,,,11.322,48000,2,161017,ca393e47bc8ecbd1,
SFX,EDF036D6/296B28EE,Music_296B28EE,,Music,Music,,,71.581,48000,2,1018253,511aba3e78e85eee,
SFX,EDF036D6/2987BACD,Music_2987BACD,,Music,Music,,,83.742,48000,2,1246073,9c48274110f1a12b,
SFX,EDF036D6/29AB41BA,Music_29AB41BA,,Music,Music,,,60.123,48000,2,1018528,6f56ef771f3434f6,
SFX,EDF036D6/29D2F637,Music_29D2F637,,Music,Music,,,203.312,48000,2,2971838,ec89b9e769b35868,
SFX,EDF036D6/2A1AD885,Music_2A1AD885,,Music,Music,,,34.675,48000,2,520729,0d0c912a47f702c1,
SFX,EDF036D6/2A24CEA6,Music_2A24CEA6,,Music,Music,,,1.409,48000,2,22216,677f3a8d58205de5,
SFX,EDF036D6/2AA3CEA0,Music_2AA3CEA0,,Music,Music,,,26.182,48000,2,418694,ed57512cccf5c2be,
SFX,EDF036D6/2AD309E6,Music_2AD309E6,,Music,Music,,,25.576,48000,2,369294,bda56bef71032f88,
SFX,EDF036D6/2B07A62F,Music_2B07A62F,,Music,Music,,,5.429,48000,2,82642,f7158a1afd44959f,
SFX,EDF036D6/2B65303C,Music_2B65303C,,Music,Music,,,119.221,48000,2,1463918,e69ddb4be1b7b84b,
SFX,EDF036D6/2BFCA0A1,Music_2BFCA0A1,,Music,Music,,,117.000,48000,2,1375538,fe76f756eab10a0d,
SFX,EDF036D6/2C8CEB90,Music_2C8CEB90,,Music,Music,,,53.066,48000,2,310850,ba12cb8c0ce2c213,
SFX,EDF036D6/2CC1F4E1,Music_2CC1F4E1,,Music,Music,,,18.177,48000,2,259134,d5a00a1674c95380,
SFX,EDF036D6/2CDA63DE,Music_2CDA63DE,,Music,Music,,,119.035,48000,2,1815581,bf1a62d940e5ca88,
SFX,EDF036D6/2CE4147F,Music_2CE4147F,,Music,Music,,,19.963,48000,2,282315,81f2136e11b85bdd,
SFX,EDF036D6/2D31EA09,Music_2D31EA09,,Music,Music,,,25.672,48000,2,380558,a69d5fa1bade8753,
SFX,EDF036D6/2D321DF5,Music_2D321DF5,,Music,Music,,,9.600,48000,2,143544,f93fca0d047cd2fe,
SFX,EDF036D6/2DB6F423,Music_2DB6F423,,Music,Music,,,136.744,48000,2,1552591,5a5ff2c4f953aeac,
SFX,EDF036D6/2DF40EC5,Music_2DF40EC5,,Music,Music,,,124.212,48000,2,2081712,72d855be03a60acf,
SFX,EDF036D6/2E49AEFC,Music_2E49AEFC,,Music,Music,,,43.254,48000,2,643737,a2199aa674d1a491,
SFX,EDF036D6/2E885FD0,Music_2E885FD0,,Music,Music,,,9.351,48000,2,147447,2eaf9dadf364eae3,
SFX,EDF036D6/2F0BF544,Music_2F0BF544,,Music,Music,,,10.712,48000,2,163834,14e6e64f1f87b86c,
SFX,EDF036D6/2FBB03E0,Music_2FBB03E0,,Music,Music,,,8.196,48000,2,116776,d9bc1e573ee6d1a3,
SFX,EDF036D6/301C76CA,Music_301C76CA,,Music,Music,,,57.931,48000,2,990419,02d9807cc745d4f2,
SFX,EDF036D6/3057FE36,Music_3057FE36,,Music,Music,,,9.790,48000,2,136096,9c32781ab97fe445,
SFX,EDF036D6/3085A581,Music_3085A581,,Music,Music,,,20.631,48000,2,357823,f5f4386a4f22b986,
SFX,EDF036D6/319FC00C,Music_319FC00C,,Music,Music,,,76.736,48000,2,1300493,d2288e6324088412,
SFX,EDF036D6/31AB3252,Music_31AB3252,,Music,Music,,,1.439,48000,2,24279,9e1452b253113e97,
SFX,EDF036D6/31C2F642,Music_31C2F642,,Music,Music,,,13.165,48000,2,185823,1bc15699e00200fe,
SFX,EDF036D6/322DC5AF,Music_322DC5AF,,Music,Music,,,4.388,48000,2,62342,7bf7872bc73c9174,
SFX,EDF036D6/32DCAEE7,Music_32DCAEE7,,Music,Music,,,9.306,48000,2,131764,9e9a2794e9fe45a1,
SFX,EDF036D6/32EB3DA7,Music_32EB3DA7,,Music,Music,,,171.524,48000,2,2600951,2066f38866c5d960,
SFX,EDF036D6/33057EFC,Music_33057EFC,,Music,Music,,,68.386,48000,2,972781,d7c24aa3f26ceca9,
SFX,EDF036D6/33695FB1,Music_33695FB1,,Music,Music,,,4.000,48000,2,64991,1808f37aef2d95cb,
SFX,EDF036D6/33828946,Music_33828946,,Music,Music,,,6.005,48000,2,98962,7dc7fbfc08e0f669,
SFX,EDF036D6/3418F5CF,Music_3418F5CF,,Music,Music,,,6.045,48000,2,90863,cc9cf509df1c7a96,
SFX,EDF036D6/3452B01B,Music_3452B01B,,Music,Music,,,114.167,48000,2,1925579,d0e77781234a58f8,
SFX,EDF036D6/3473C68E,Music_3473C68E,,Music,Music,,,21.231,48000,2,329799,1556993ad3defc8e,
SFX,EDF036D6/350357ED,Music_350357ED,,Music,Music,,,12.850,48000,2,152754,effedf711799f9fb,
SFX,EDF036D6/356CBA43,Music_356CBA43,,Music,Music,,,7.836,48000,2,59556,47e3f32bc4d4a5cc,
SFX,EDF036D6/3584EC39,Music_3584EC39,,Music,Music,,,51.176,48000,2,768089,0cf2d9590cbbcdd6,
SFX,EDF036D6/36FBFB5B,Music_36FBFB5B,,Music,Music,,,14.560,48000,6,264912,800fdb94db6dfeec,
SFX,EDF036D6/372964C4,Music_372964C4,,Music,Music,,,5.887,48000,2,86948,f6d4d75ab8445bbd,
SFX,EDF036D6/38597057,Music_38597057,,Music,Music,,,27.239,44100,2,355958,9d0730c4a1c127da,
SFX,EDF036D6/392269E5,Music_392269E5,,Music,Music,,,9.166,48000,2,136727,28667f4a68ee8e66,
SFX,EDF036D6/393BC53A,Music_393BC53A,,Music,Music,,,171.524,48000,6,7192714,eba537acd74009ba,
SFX,EDF036D6/39EF16A1,Music_39EF16A1,,Music,Music,,,20.571,48000,2,322708,cbc57a438236bfcd,
SFX,EDF036D6/3A0F34AA,Music_3A0F34AA,,Music,Music,,,20.842,48000,2,327714,4feb9c5e467509b3,
SFX,EDF036D6/3A6E0069,Music_3A6E0069,,Music,Music,,,30.607,48000,2,440966,23f0f02258eb7c3f,
SFX,EDF036D6/3A96A66A,Music_3A96A66A,,Music,Music,,,5.426,48000,2,78334,1a7e9e3fd65324a8,
SFX,EDF036D6/3AB86854,Music_3AB86854,,Music,Music,,,124.212,48000,2,2186737,956cd355f2baec4d,
SFX,EDF036D6/3B19926C,Music_3B19926C,,Music,Music,,,32.360,48000,2,571396,f8cfcdc11c88aff9,
SFX,EDF036D6/3B1C4571,Music_3B1C4571,,Music,Music,,,26.182,48000,2,456132,e1e55cfe4e98a17b,
SFX,EDF036D6/3B276E4D,Music_3B276E4D,,Music,Music,,,125.626,48000,2,1911354,0f17594e2df36276,
SFX,EDF036D6/3B30E8B5,Music_3B30E8B5,,Music,Music,,,4.422,48000,2,72779,b73c25399a8c66c7,
SFX,EDF036D6/3BA5398D,Music_3BA5398D,,Music,Music,,,6.028,48000,2,85229,f7d80b8c2a5d23ec,
SFX,EDF036D6/3C106E75,Music_3C106E75,,Music,Music,,,6.527,48000,2,95702,8f0ccbaa0deb002e,
SFX,EDF036D6/3C961A0D,Music_3C961A0D,,Music,Music,,,96.021,48000,2,1405544,5b6c24b48087e2b3,
SFX,EDF036D6/3CBF4761,Music_3CBF4761,,Music,Music,,,4.856,48000,2,70027,d408c7bb17835303,
SFX,EDF036D6/3CE33965,Music_3CE33965,,Music,Music,,,4.382,48000,2,64257,1fd2e7f5eecfeecb,
SFX,EDF036D6/3D54599E,Music_3D54599E,,Music,Music,,,13.390,48000,2,187301,27fed970b91d3ca8,
SFX,EDF036D6/3E47446A,Music_3E47446A,,Music,Music,,,7.806,48000,2,118924,c8fd851d3d5e5ea8,
SFX,EDF036D6/3F04E423,Music_3F04E423,,Music,Music,,,128.764,48000,2,2039407,4ed2d6eaa4c10c65,
SFX,EDF036D6/3F312E20,Music_3F312E20,,Music,Music,,,57.040,48000,2,809641,cb602f293c127d15,
SFX,EDF036D6/3FFB5B14,Music_3FFB5B14,,Music,Music,,,9.547,48000,2,142043,f859153f7497528d,
SFX,F16E832D:0,SEQ_DemonTrials_1,SEQ_DemonTrials#1,SFX,Scripted sequences,,,17.280,48000,1,105109,316e989c34a90718,
SFX,F16E832D:1,SEQ_DemonTrials_2,SEQ_DemonTrials#2,SFX,Scripted sequences,,,13.525,48000,1,84855,629e4ae11d2af39c,
SFX,F16E832D:2,SEQ_DemonTrials_3,SEQ_DemonTrials#3,SFX,Scripted sequences,,,10.837,48000,1,65652,56430a3bb8bcadfd,
SFX,F16E832D:3,SEQ_DemonTrials_4,SEQ_DemonTrials#4,SFX,Scripted sequences,,,14.635,48000,1,91112,44b6dd9a637cfdc7,
SFX,F16E832D:4,SEQ_DemonTrials_5,SEQ_DemonTrials#5,SFX,Scripted sequences,,,12.032,48000,1,74765,554d0d717858a5a6,
SFX,F16E832D:5,SEQ_DemonTrials_6,SEQ_DemonTrials#6,SFX,Scripted sequences,,,17.707,48000,1,107955,7462b3589a40f0e4,
SFX,F16E832D:6,SEQ_DemonTrials_7,SEQ_DemonTrials#7,SFX,Scripted sequences,,,7.723,48000,1,47185,fbc1fea07a56b18e,
SFX,F16E832D:7,SEQ_DemonTrials_8,SEQ_DemonTrials#8,SFX,Scripted sequences,,,15.232,48000,1,91634,7f84cdc41d59c8ed,
SFX,F16E832D:8,SEQ_DemonTrials_9,SEQ_DemonTrials#9,SFX,Scripted sequences,,,14.635,48000,1,89392,d3771bede86e3233,
SFX,F1B81FC7,PVOL_Water_Exterior,PVOL_Water_Exterior_1,Ambience,Room tone,,,25.722,32000,2,261962,55e9f9cd4bd498f2,
SFX,F9A51D18:0,AMB_OW_C46_1,AMB_OW_C46#1,Ambience,Ambience,,,84.593,32000,4,2210553,3b7926711dcd2996,
SFX,F9A51D18:1,AMB_OW_C46_2,AMB_OW_C46#2,Ambience,Ambience,,,59.741,32000,4,1546334,c9cb4e7868f6a813,
SFX,F9A51D18:2,AMB_OW_C46_3,AMB_OW_C46#3,Ambience,Ambience,,,97.923,32000,4,2728789,e6be03d0fe2c6639,
SFX,FCA4F778/0EC73393,FCA4F778_0EC73393,,Unknown,Unknown,,,56.689,48000,2,843627,8bce3f68a65ae479,
SFX,FD4461EA:0,AMB_RiddlerChallenge1_1,AMB_RiddlerChallenge1#1,Ambience,Ambience,,,35.157,32000,4,828243,506937146f3bb8bd,
SFX,FD4461EA:1,AMB_RiddlerChallenge1_2,AMB_RiddlerChallenge1#2,Ambience,Ambience,,,55.551,32000,4,1462792,1239f45c45e2f82a,
SFX,FD4461EA:2,AMB_RiddlerChallenge1_3,AMB_RiddlerChallenge1#3,Ambience,Ambience,,,16.384,32000,4,377945,d42730fbd1ecdcc3,
SFX,FDB108B0,SEQ_Steel_C5_WaterDoors,SEQ_Steel_C5_WaterDoors_1,SFX,Scripted sequences,,,17.285,48000,2,228831,bea79a6d7fe41df0,
SFX,FFF56412,AMB_UnderworldC2_V2,AMB_UnderworldC2_V2_1,Ambience,Ambience,,,28.160,32000,4,580489,ee66e8d0f24876e6,
SFX,01EB5DDC:0,AMB_OW_B_37_1,AMB_OW_B_37#1,Ambience,Ambience,,,126.880,32000,4,3753364,079885b8f4146578,
SFX,01EB5DDC:1,AMB_OW_B_37_2,AMB_OW_B_37#2,Ambience,Ambience,,,95.915,32000,4,2825273,f2feb7d5c975cb29,
SFX,01EB5DDC:2,AMB_OW_B_37_3,AMB_OW_B_37#3,Ambience,Ambience,,,71.680,32000,4,2093574,4cb79bfe20e6e46e,
SFX,01EB5DDC:3,AMB_OW_B_37_4,AMB_OW_B_37#4,Ambience,Ambience,,,73.720,32000,4,2176579,c671caa71010cc41,
SFX,01EB5DDC:4,AMB_OW_B_37_5,AMB_OW_B_37#5,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
SFX,01EB5DDC:5,AMB_OW_B_37_6,AMB_OW_B_37#6,Ambience,Ambience,,,77.999,32000,4,2269598,72ca05c3ec831537,
SFX,01EB5DDC:6,AMB_OW_B_37_7,AMB_OW_B_37#7,Ambience,Ambience,,,103.764,32000,4,3180437,e25931054a39debb,
SFX,062748BA:0,AMB_GCPD_HQ_1,AMB_GCPD_HQ#1,Ambience,Ambience,,Cop,40.852,32000,4,1204406,a0a7812ae604c8e2,
SFX,062748BA:1,AMB_GCPD_HQ_2,AMB_GCPD_HQ#2,Ambience,Ambience,,Cop,9.714,32000,4,218213,375fb85c4d413d51,
SFX,0B18F44C:0,CHR_RasBossFight_1,CHR_RasBossFight#1,SFX,Character foley,,,17.280,48000,1,105109,316e989c34a90718,
SFX,0B18F44C:1,CHR_RasBossFight_2,CHR_RasBossFight#2,SFX,Character foley,,,13.525,48000,1,84855,629e4ae11d2af39c,
SFX,0B18F44C:2,CHR_RasBossFight_3,CHR_RasBossFight#3,SFX,Character foley,,,14.635,48000,1,91112,44b6dd9a637cfdc7,
SFX,0B18F44C:3,CHR_RasBossFight_4,CHR_RasBossFight#4,SFX,Character foley,,,12.032,48000,1,74765,554d0d717858a5a6,
SFX,0B18F44C:4,CHR_RasBossFight_5,CHR_RasBossFight#5,SFX,Character foley,,,7.486,48000,4,223830,e8915027e3ce55d0,
SFX,0E398B80:0,SEQ_OW_Pay_Your_Respects_1,SEQ_OW_Pay_Your_Respects#1,SFX,Scripted sequences,,,17.158,24000,2,105596,ca9d7921fc5d1235,
SFX,0E398B80:1,SEQ_OW_Pay_Your_Respects_2,SEQ_OW_Pay_Your_Respects#2,SFX,Scripted sequences,,,17.158,24000,2,113215,0efb16b6e65dfb1f,
SFX,10CF081C:0,AMB_OW_A_ALL_1,AMB_OW_A_ALL#1,Ambience,Ambience,,,100.000,32000,4,2881899,6eda738d26302232,
SFX,10CF081C:1,AMB_OW_A_ALL_2,AMB_OW_A_ALL#2,Ambience,Ambience,,,93.757,32000,4,2767748,ec137126ce5315c3,
SFX,10CF081C:2,AMB_OW_A_ALL_3,AMB_OW_A_ALL#3,Ambience,Ambience,,,49.740,32000,4,1473273,0af778e258914efc,
SFX,10CF081C:3,AMB_OW_A_ALL_4,AMB_OW_A_ALL#4,Ambience,Ambience,,,90.494,32000,4,2647321,df324637be55b522,
SFX,10CF081C:4,AMB_OW_A_ALL_5,AMB_OW_A_ALL#5,Ambience,Ambience,,,92.101,32000,4,2655287,87c065e2406e711a,
SFX,10CF081C:5,AMB_OW_A_ALL_6,AMB_OW_A_ALL#6,Ambience,Ambience,,,89.000,32000,4,2587371,4b2939c4c48d3a37,
SFX,10CF081C:6,AMB_OW_A_ALL_7,AMB_OW_A_ALL#7,Ambience,Ambience,,,87.963,32000,4,2452402,c48eac4d33968c11,
SFX,10CF081C:7,AMB_OW_A_ALL_8,AMB_OW_A_ALL#8,Ambience,Ambience,,,48.736,32000,4,1510563,371103729d371d2f,
SFX,10CF081C:8,AMB_OW_A_ALL_9,AMB_OW_A_ALL#9,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
SFX,10CF081C:9,AMB_OW_A_ALL_10,AMB_OW_A_ALL#10,Ambience,Ambience,,,76.219,32000,4,2208864,b223019277b088c7,
SFX,10CF081C:10,AMB_OW_A_ALL_11,AMB_OW_A_ALL#11,Ambience,Ambience,,,89.348,32000,4,2648238,780fcdfc367ef989,
SFX,10CF081C:11,AMB_OW_A_ALL_12,AMB_OW_A_ALL#12,Ambience,Ambience,,,84.039,32000,4,2445442,d6ec35703e9f8a90,
SFX,17B983F5:0,AMB_OW_E123478_1,AMB_OW_E123478#1,Ambience,Ambience,,,100.000,32000,4,2881899,6eda738d26302232,
SFX,17B983F5:1,AMB_OW_E123478_2,AMB_OW_E123478#2,Ambience,Ambience,,,93.757,32000,4,2767748,ec137126ce5315c3,
SFX,17B983F5:2,AMB_OW_E123478_3,AMB_OW_E123478#3,Ambience,Ambience,,,60.828,32000,4,1813753,1a1753a70c841dec,
SFX,17B983F5:3,AMB_OW_E123478_4,AMB_OW_E123478#4,Ambience,Ambience,,,49.740,32000,4,1473273,0af778e258914efc,
SFX,17B983F5:4,AMB_OW_E123478_5,AMB_OW_E123478#5,Ambience,Ambience,,,3.204,32000,2,38134,8cf522d67fc03fe7,
SFX,17B983F5:5,AMB_OW_E123478_6,AMB_OW_E123478#6,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
SFX,17B983F5:6,AMB_OW_E123478_7,AMB_OW_E123478#7,Ambience,Ambience,,,90.494,32000,4,2647321,df324637be55b522,
SFX,17B983F5:7,AMB_OW_E123478_8,AMB_OW_E123478#8,Ambience,Ambience,,,92.101,32000,4,2655287,87c065e2406e711a,
SFX,17B983F5:8,AMB_OW_E123478_9,AMB_OW_E123478#9,Ambience,Ambience,,,87.963,32000,4,2452402,c48eac4d33968c11,
SFX,17B983F5:9,AMB_OW_E123478_10,AMB_OW_E123478#10,Ambience,Ambience,,,48.736,32000,4,1510563,371103729d371d2f,
SFX,17B983F5:10,AMB_OW_E123478_11,AMB_OW_E123478#11,Ambience,Ambience,,,4.205,32000,2,49274,acec2dcd750d3f1a,
SFX,17B983F5:11,AMB_OW_E123478_12,AMB_OW_E123478#12,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
SFX,17B983F5:12,AMB_OW_E123478_13,AMB_OW_E123478#13,Ambience,Ambience,,,76.219,32000,4,2208864,b223019277b088c7,
SFX,17B983F5:13,AMB_OW_E123478_14,AMB_OW_E123478#14,Ambience,Ambience,,,89.348,32000,4,2648238,780fcdfc367ef989,
SFX,17B983F5:14,AMB_OW_E123478_15,AMB_OW_E123478#15,Ambience,Ambience,,,84.039,32000,4,2445442,d6ec35703e9f8a90,
SFX,1C305431,AMB_MadHatter_PostFight,AMB_MadHatter_PostFight_1,Ambience,Ambience,,Mad Hatter,6.003,32000,4,168722,1842bba5f1fb4194,
SFX,1F3ABBB4:0,AMB_Scarecrow_Boat_1,AMB_Scarecrow_Boat#1,Ambience,Ambience,,,30.358,32000,2,347585,34b4d9a05154fa63,
SFX,1F3ABBB4:1,AMB_Scarecrow_Boat_2,AMB_Scarecrow_Boat#2,Ambience,Ambience,,,29.326,32000,2,316292,52819d7ad292a6a6,
SFX,23F6A8F8:0,AMB_CourtB1_1,AMB_CourtB1#1,Ambience,Ambience,,,14.368,32000,4,392497,923b77449efcd99e,
SFX,23F6A8F8:1,AMB_CourtB1_2,AMB_CourtB1#2,Ambience,Ambience,,,19.371,32000,4,527286,51acfa6500a7486d,
SFX,23F6A8F8:2,AMB_CourtB1_3,AMB_CourtB1#3,Ambience,Ambience,,,27.000,32000,4,755899,af2ec78c4279be78,
SFX,247E31CA,CHR_Grundy,CHR_Grundy_1,SFX,Character foley,,Solomon Grundy,3.284,48000,2,34305,2271da11e324c823,
SFX,2DE53B3F:0,AMB_OW_Tower_Exterior_1,AMB_OW_Tower_Exterior#1,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
SFX,2DE53B3F:1,AMB_OW_Tower_Exterior_2,AMB_OW_Tower_Exterior#2,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
SFX,2DE53B3F:2,AMB_OW_Tower_Exterior_3,AMB_OW_Tower_Exterior#3,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
SFX,2DE53B3F:3,AMB_OW_Tower_Exterior_4,AMB_OW_Tower_Exterior#4,Ambience,Ambience,,,8.319,24000,1,21247,fe0c508a6d2a341c,
SFX,2DE53B3F:4,AMB_OW_Tower_Exterior_5,AMB_OW_Tower_Exterior#5,Ambience,Ambience,,,98.450,32000,4,2836823,1f2b7810a3c875a0,
SFX,2DE53B3F:5,AMB_OW_Tower_Exterior_6,AMB_OW_Tower_Exterior#6,Ambience,Ambience,,,6.254,24000,1,20450,03df69ec6485a152,
SFX,2DE53B3F:6,AMB_OW_Tower_Exterior_7,AMB_OW_Tower_Exterior#7,Ambience,Ambience,,,5.436,24000,1,13147,419e24dbccf1e7e5,
SFX,2DE53B3F:7,AMB_OW_Tower_Exterior_8,AMB_OW_Tower_Exterior#8,Ambience,Ambience,,,6.594,24000,1,18166,15bc9f238253efa2,
SFX,2DE53B3F:8,AMB_OW_Tower_Exterior_9,AMB_OW_Tower_Exterior#9,Ambience,Ambience,,,7.198,24000,1,17394,ad8713b562f1714b,
SFX,2DE53B3F:9,AMB_OW_Tower_Exterior_10,AMB_OW_Tower_Exterior#10,Ambience,Ambience,,,7.060,24000,1,22630,d8465ba3a42b9387,
SFX,2DE53B3F:10,AMB_OW_Tower_Exterior_11,AMB_OW_Tower_Exterior#11,Ambience,Ambience,,,8.421,24000,1,24860,ee66e3e09bf1dc79,
SFX,396E6EBD:0,AMB_Church_B1_1,AMB_Church_B1#1,Ambience,Ambience,,,18.324,32000,4,496213,5eb76e08e5346ab4,
SFX,396E6EBD:1,AMB_Church_B1_2,AMB_Church_B1#2,Ambience,Ambience,,,11.720,32000,4,299346,7576251df0911437,
SFX,3CB673E7,AMB_HushRoom,AMB_HushRoom_1,Ambience,Ambience,,,24.000,32000,4,671357,1279af0238e7afdc,
SFX,3F4B608F,AMB_Underworld_S1,AMB_Underworld_S1_1,Ambience,Ambience,,,15.536,32000,4,455276,9fb735c477db3804,
SFX,45A19974:0,AMB_RumbleDebris_1,AMB_RumbleDebris#1,Ambience,Ambience,,,15.495,32000,4,404444,646660bf0b7207b1,
SFX,45A19974:1,AMB_RumbleDebris_2,AMB_RumbleDebris#2,Ambience,Ambience,,,11.602,32000,4,321943,54474dbc608a1506,
SFX,45A19974:2,AMB_RumbleDebris_3,AMB_RumbleDebris#3,Ambience,Ambience,,,9.056,32000,4,239594,7fc588270d062fc5,
SFX,45A19974:3,AMB_RumbleDebris_4,AMB_RumbleDebris#4,Ambience,Ambience,,,8.942,32000,4,255627,8ebabec798972470,
SFX,4D4B76FB:0,AMB_Underworld_A3_1,AMB_Underworld_A3#1,Ambience,Ambience,,,15.691,28000,1,98560,19c86c48a0681287,
SFX,4D4B76FB:1,AMB_Underworld_A3_2,AMB_Underworld_A3#2,Ambience,Ambience,,,15.263,28000,1,98065,b7f3b6ef76594a50,
SFX,4D4B76FB:2,AMB_Underworld_A3_3,AMB_Underworld_A3#3,Ambience,Ambience,,,10.682,28000,1,69865,d2ef62f37b65f62f,
SFX,4D4B76FB:3,AMB_Underworld_A3_4,AMB_Underworld_A3#4,Ambience,Ambience,,,10.139,28000,1,61110,b3bbb946619eb519,
SFX,4D4B76FB:4,AMB_Underworld_A3_5,AMB_Underworld_A3#5,Ambience,Ambience,,,9.415,28000,1,58433,7d6e7e3978db3156,
SFX,4D4B76FB:5,AMB_Underworld_A3_6,AMB_Underworld_A3#6,Ambience,Ambience,,,15.716,28000,1,91342,4f47198c1322c19a,
SFX,4D4B76FB:6,AMB_Underworld_A3_7,AMB_Underworld_A3#7,Ambience,Ambience,,,8.383,28000,1,54706,7cf7332a829e5065,
SFX,4D4B76FB:7,AMB_Underworld_A3_8,AMB_Underworld_A3#8,Ambience,Ambience,,,13.995,32000,4,420389,44e2d6cb6bc05ecd,
SFX,4DB8A342,OBJ_Tannoy,OBJ_Tannoy_1,SFX,Objects,,,2.229,32000,5,26334,d5aaba538d661493,
SFX,4DF92D97:0,OBJ_GunCrates_1,OBJ_GunCrates#1,SFX,Objects,,,1.249,48000,2,17932,5fcb6961ce873316,
SFX,4DF92D97:1,OBJ_GunCrates_2,OBJ_GunCrates#2,SFX,Objects,,,1.388,48000,2,19892,eb5c4c8018dcb3b2,
SFX,4DF92D97:2,OBJ_GunCrates_3,OBJ_GunCrates#3,SFX,Objects,,,1.388,48000,2,19708,64e7be26e0731db0,
SFX,4DF92D97:3,OBJ_GunCrates_4,OBJ_GunCrates#4,SFX,Objects,,,1.388,48000,2,23070,0314231fb31ebd12,
SFX,4EE4DEAC,VOL_IceFreeze_Interior,VOL_IceFreeze_Interior_1,Ambience,Room tone,,,24.000,48000,2,519820,93388507ef36519c,
SFX,4F4B79DF:0,AMB_Underworld_C1_1,AMB_Underworld_C1#1,Ambience,Ambience,,,10.062,32000,4,340307,1108533c7f5600f7,
SFX,4F4B79DF:1,AMB_Underworld_C1_2,AMB_Underworld_C1#2,Ambience,Ambience,,,16.746,48000,4,497401,835dc96ff6b7a1f9,
SFX,504B7BB4:0,AMB_Underworld_B5_1,AMB_Underworld_B5#1,Ambience,Ambience,,,4.977,24000,1,14907,2abb33cf9e8b4b4f,
SFX,504B7BB4:1,AMB_Underworld_B5_2,AMB_Underworld_B5#2,Ambience,Ambience,,,6.875,24000,1,19276,ecd69d2dce5e37a7,
SFX,504B7BB4:2,AMB_Underworld_B5_3,AMB_Underworld_B5#3,Ambience,Ambience,,,6.591,24000,1,18148,7e09b2d25c2eea3a,
SFX,504B7BB4:3,AMB_Underworld_B5_4,AMB_Underworld_B5#4,Ambience,Ambience,,,4.287,24000,1,13144,bf602fa72cf41dfe,
SFX,504B7BB4:4,AMB_Underworld_B5_5,AMB_Underworld_B5#5,Ambience,Ambience,,,7.211,24000,1,21936,c7a506f8097c8015,
SFX,504B7BB4:5,AMB_Underworld_B5_6,AMB_Underworld_B5#6,Ambience,Ambience,,,3.002,24000,1,9132,09901bea0b7aeb67,
SFX,504B7BB4:6,AMB_Underworld_B5_7,AMB_Underworld_B5#7,Ambience,Ambience,,,4.678,24000,1,14423,de40e62612dad2fd,
SFX,504B7BB4:7,AMB_Underworld_B5_8,AMB_Underworld_B5#8,Ambience,Ambience,,,15.691,28000,1,98560,19c86c48a0681287,
SFX,504B7BB4:8,AMB_Underworld_B5_9,AMB_Underworld_B5#9,Ambience,Ambience,,,15.263,28000,1,98065,b7f3b6ef76594a50,
SFX,504B7BB4:9,AMB_Underworld_B5_10,AMB_Underworld_B5#10,Ambience,Ambience,,,10.682,28000,1,69865,d2ef62f37b65f62f,
SFX,504B7BB4:10,AMB_Underworld_B5_11,AMB_Underworld_B5#11,Ambience,Ambience,,,5.649,24000,1,15919,7161ab2c29c0ad02,
SFX,504B7BB4:11,AMB_Underworld_B5_12,AMB_Underworld_B5#12,Ambience,Ambience,,,10.139,28000,1,61110,b3bbb946619eb519,
SFX,504B7BB4:12,AMB_Underworld_B5_13,AMB_Underworld_B5#13,Ambience,Ambience,,,9.415,28000,1,58433,7d6e7e3978db3156,
SFX,504B7BB4:13,AMB_Underworld_B5_14,AMB_Underworld_B5#14,Ambience,Ambience,,,7.804,24000,1,22303,e97522fb2216e189,
SFX,504B7BB4:14,AMB_Underworld_B5_15,AMB_Underworld_B5#15,Ambience,Ambience,,,2.753,24000,1,8077,43a3682b07706ee1,
SFX,504B7BB4:15,AMB_Underworld_B5_16,AMB_Underworld_B5#16,Ambience,Ambience,,,15.536,32000,4,455276,9fb735c477db3804,
SFX,504B7BB4:16,AMB_Underworld_B5_17,AMB_Underworld_B5#17,Ambience,Ambience,,,15.716,28000,1,91342,4f47198c1322c19a,
SFX,504B7BB4:17,AMB_Underworld_B5_18,AMB_Underworld_B5#18,Ambience,Ambience,,,8.383,28000,1,54706,7cf7332a829e5065,
SFX,504B7BB4:18,AMB_Underworld_B5_19,AMB_Underworld_B5#19,Ambience,Ambience,,,14.012,32000,4,442369,714ad5534983e096,
SFX,504B7BB7,AMB_Underworld_B6,AMB_Underworld_B6_1,Ambience,Ambience,,,30.336,48000,4,902577,aaa3b73a68f71eb7,
SFX,508590A0,AMB_HiddenAreas,AMB_HiddenAreas_1,Ambience,Ambience,,,18.666,32000,4,511603,10aefd8ea1557596,
SFX,53C0E792,AMB_OW_D5,AMB_OW_D5_1,Ambience,Ambience,,,14.012,32000,4,419982,3bdaee4ebf08faad,
SFX,53C0E793,AMB_OW_D4,AMB_OW_D4_1,Ambience,Ambience,,,14.012,32000,4,419982,3bdaee4ebf08faad,
SFX,53C0E794,AMB_OW_D3,AMB_OW_D3_1,Ambience,Ambience,,,60.043,32000,4,1717445,16f8f80c393a2544,
SFX,53C0E795,AMB_OW_D2,AMB_OW_D2_1,Ambience,Ambience,,,69.740,32000,4,2062109,17e7d4833c7dfae4,
SFX,53C0E796:0,AMB_OW_D1_1,AMB_OW_D1#1,Ambience,Ambience,,,60.043,32000,4,1717445,16f8f80c393a2544,
SFX,53C0E796:1,AMB_OW_D1_2,AMB_OW_D1#2,Ambience,Ambience,,,69.740,32000,4,2062109,17e7d4833c7dfae4,
SFX,5445778E,AMB_Museum_Corridors,AMB_Museum_Corridors_1,Ambience,Ambience,,,28.571,32000,4,750171,f5549719c82a2af2,
SFX,57572DAC/267F1DB3,Global_267F1DB3,,SFX,Global,,,10.092,24000,4,216184,c1f1c7385d15b611,
SFX,65845381:0,AMB_MadHatter_Party_1,AMB_MadHatter_Party#1,Ambience,Ambience,,Mad Hatter,20.595,32000,4,582875,573c8558ebc5de5d,
SFX,65845381:1,AMB_MadHatter_Party_2,AMB_MadHatter_Party#2,Ambience,Ambience,,Mad Hatter,35.144,32000,4,1047109,1bea95a6a07f9150,
SFX,6866CE3A,WPN_Grundy,WPN_Grundy_1,SFX,Weapons,,Solomon Grundy,5.325,48000,1,44570,6de71f59fab751c1,
SFX,6C151539:0,6C151539_1,6C151539#1,Unknown,Unknown,,,1.000,48000,1,10356,e9e3007ebc41458b,
SFX,6C151539:1,6C151539_2,6C151539#2,Unknown,Unknown,,,1.000,48000,1,14149,f36ba9b3afa4f6b3,
SFX,6C151539:2,6C151539_3,6C151539#3,Unknown,Unknown,,,1.000,48000,1,13537,452d9e2784cb5c41,
SFX,7506D11C,AMB_ZsaszB1,AMB_ZsaszB1_1,Ambience,Ambience,,,20.000,32000,4,545599,fbb73780df30e80c,
SFX,7D6D8269,AMB_Museum_S1,AMB_Museum_S1_1,Ambience,Ambience,,,20.468,32000,4,574954,4ce1cea0c3c854f4,
SFX,7D6D826A,AMB_Museum_S2,AMB_Museum_S2_1,Ambience,Ambience,,,20.468,32000,4,574954,4ce1cea0c3c854f4,
SFX,7D71D951,AMB_HushSpooky,AMB_HushSpooky_1,Ambience,Ambience,,,24.000,32000,4,680215,823670f2a118d1af,
SFX,7D848FF0:0,AMB_OW_B_12456_1,AMB_OW_B_12456#1,Ambience,Ambience,,,126.880,32000,4,3753364,079885b8f4146578,
SFX,7D848FF0:1,AMB_OW_B_12456_2,AMB_OW_B_12456#2,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
SFX,7D848FF0:2,AMB_OW_B_12456_3,AMB_OW_B_12456#3,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
SFX,7D848FF0:3,AMB_OW_B_12456_4,AMB_OW_B_12456#4,Ambience,Ambience,,,60.828,32000,4,1813753,1a1753a70c841dec,
SFX,7D848FF0:4,AMB_OW_B_12456_5,AMB_OW_B_12456#5,Ambience,Ambience,,,84.593,32000,4,2210553,3b7926711dcd2996,
SFX,7D848FF0:5,AMB_OW_B_12456_6,AMB_OW_B_12456#6,Ambience,Ambience,,,68.204,32000,4,2019040,e3b9a81b5d764030,
SFX,7D848FF0:6,AMB_OW_B_12456_7,AMB_OW_B_12456#7,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
SFX,7D848FF0:7,AMB_OW_B_12456_8,AMB_OW_B_12456#8,Ambience,Ambience,,,98.450,32000,4,2836823,1f2b7810a3c875a0,
SFX,7D848FF0:8,AMB_OW_B_12456_9,AMB_OW_B_12456#9,Ambience,Ambience,,,59.741,32000,4,1546334,c9cb4e7868f6a813,
SFX,7D848FF0:9,AMB_OW_B_12456_10,AMB_OW_B_12456#10,Ambience,Ambience,,,97.923,32000,4,2728789,e6be03d0fe2c6639,
SFX,7D848FF0:10,AMB_OW_B_12456_11,AMB_OW_B_12456#11,Ambience,Ambience,,,80.661,32000,4,2399422,f349c20ba690c651,
SFX,7D848FF0:11,AMB_OW_B_12456_12,AMB_OW_B_12456#12,Ambience,Ambience,,,77.999,32000,4,2269598,72ca05c3ec831537,
1 archive target name aliases group category character characters_inferred seconds rate channels size sha1 notes
2 SFX 8301A1DA:0 AMB_OW_E8_CH0_1 AMB_OW_E8_CH0#1 Ambience Ambience 26.465 32000 2 347255 d4f1950c2ccb9cbb
3 SFX 8301A1DA:1 AMB_OW_E8_CH0_2 AMB_OW_E8_CH0#2 Ambience Ambience 26.465 32000 2 341343 f6b9f4d0d90bfbfd
4 SFX 84CC9D20:0 AMB_Meatpacking_B1_1 AMB_Meatpacking_B1#1 Ambience Ambience 13.819 32000 4 368622 045daf15846206fa
5 SFX 84CC9D20:1 AMB_Meatpacking_B1_2 AMB_Meatpacking_B1#2 Ambience Ambience 21.382 32000 4 633549 5ba0fbc1f14ad4c3
6 SFX 84CC9D20:2 AMB_Meatpacking_B1_3 AMB_Meatpacking_B1#3 Ambience Ambience 9.316 32000 4 255018 1b3f4a087588fa8e
7 SFX 84CC9D20:3 AMB_Meatpacking_B1_4 AMB_Meatpacking_B1#4 Ambience Ambience 15.027 32000 4 398709 9fe235e189ae929c
8 SFX 84CC9D20:4 AMB_Meatpacking_B1_5 AMB_Meatpacking_B1#5 Ambience Ambience 9.447 32000 4 265375 7b20faec40c234e8
9 SFX 84CC9D20:5 AMB_Meatpacking_B1_6 AMB_Meatpacking_B1#6 Ambience Ambience 11.576 32000 4 313386 fa901e65bda499a6
10 SFX 84CC9D20:6 AMB_Meatpacking_B1_7 AMB_Meatpacking_B1#7 Ambience Ambience 25.747 32000 4 689430 b63a0028528493b2
11 SFX 85F1F884:0 AMB_OW_C257_1 AMB_OW_C257#1 Ambience Ambience 57.597 32000 4 1730923 a0b3071f0df4d576
12 SFX 85F1F884:1 AMB_OW_C257_2 AMB_OW_C257#2 Ambience Ambience 42.956 32000 4 1283371 fcc511bca155e988
13 SFX 85F1F884:2 AMB_OW_C257_3 AMB_OW_C257#3 Ambience Ambience 59.559 32000 4 1734273 ace4cf02968e016a
14 SFX 85F1F884:3 AMB_OW_C257_4 AMB_OW_C257#4 Ambience Ambience 89.000 32000 4 2587371 4b2939c4c48d3a37
15 SFX 898441C1:0 AMB_OW_Warzone_1 AMB_OW_Warzone#1 Ambience Ambience 30.411 32000 2 393285 c22d04e66ef95442
16 SFX 898441C1:1 AMB_OW_Warzone_2 AMB_OW_Warzone#2 Ambience Ambience 30.411 32000 2 395301 78fe4e98c9dd26f9
17 SFX 898441C1:2 AMB_OW_Warzone_3 AMB_OW_Warzone#3 Ambience Ambience 34.441 32000 1 200811 010b705597bb7ecb
18 SFX 8D6D9BB9:0 AMB_Museum_C1_1 AMB_Museum_C1#1 Ambience Ambience 20.845 32000 4 479172 a32802f6e677bcbf
19 SFX 8D6D9BB9:1 AMB_Museum_C1_2 AMB_Museum_C1#2 Ambience Ambience 35.837 32000 4 976154 2ff67a7bb43c64e0
20 SFX 8D6D9BBA AMB_Museum_C2 AMB_Museum_C2_1 Ambience Ambience 48.049 32000 4 1316154 1b24fce48be812f7
21 SFX 8E6D9D29 AMB_Museum_B2 AMB_Museum_B2_1 Ambience Ambience 34.557 32000 4 923709 f1ac94349c012b69
22 SFX 8E6D9D2A AMB_Museum_B1 AMB_Museum_B1_1 Ambience Ambience 49.087 32000 4 1220981 6e9cf2ec033e68bf
23 SFX 8F6D9E9F AMB_Museum_A1 AMB_Museum_A1_1 Ambience Ambience 34.499 32000 4 953199 6eb679248891bd1a
24 SFX 98A82EFD:0 AMB_Steel_B1_1 AMB_Steel_B1#1 Ambience Ambience 27.477 32000 4 743201 1ed8f1a2af43d06a
25 SFX 98A82EFD:1 AMB_Steel_B1_2 AMB_Steel_B1#2 Ambience Ambience 6.981 48000 4 258105 82ce38a05064a82a
26 SFX 98A82EFE AMB_Steel_B2 AMB_Steel_B2_1 Ambience Ambience 23.790 32000 4 607771 cab42ed9b44112b7
27 SFX 98A82EFF:0 AMB_Steel_B3_1 AMB_Steel_B3#1 Ambience Ambience 18.323 32000 2 200224 a5366b02aa271e25
28 SFX 98A82EFF:1 AMB_Steel_B3_2 AMB_Steel_B3#2 Ambience Ambience 28.002 32000 4 693850 4ebb25c954313e04
29 SFX 98A82EFF:2 AMB_Steel_B3_3 AMB_Steel_B3#3 Ambience Ambience 18.985 32000 2 200757 0ab8c0a668b21772
30 SFX 998A5D81:0 PSA_Underworld_Groans_1 PSA_Underworld_Groans#1 Ambience Ambience 4.977 24000 1 14907 2abb33cf9e8b4b4f
31 SFX 998A5D81:1 PSA_Underworld_Groans_2 PSA_Underworld_Groans#2 Ambience Ambience 6.875 24000 1 19276 ecd69d2dce5e37a7
32 SFX 998A5D81:2 PSA_Underworld_Groans_3 PSA_Underworld_Groans#3 Ambience Ambience 6.591 24000 1 18148 7e09b2d25c2eea3a
33 SFX 998A5D81:3 PSA_Underworld_Groans_4 PSA_Underworld_Groans#4 Ambience Ambience 4.287 24000 1 13144 bf602fa72cf41dfe
34 SFX 998A5D81:4 PSA_Underworld_Groans_5 PSA_Underworld_Groans#5 Ambience Ambience 7.211 24000 1 21936 c7a506f8097c8015
35 SFX 998A5D81:5 PSA_Underworld_Groans_6 PSA_Underworld_Groans#6 Ambience Ambience 3.002 24000 1 9132 09901bea0b7aeb67
36 SFX 998A5D81:6 PSA_Underworld_Groans_7 PSA_Underworld_Groans#7 Ambience Ambience 4.678 24000 1 14423 de40e62612dad2fd
37 SFX 998A5D81:7 PSA_Underworld_Groans_8 PSA_Underworld_Groans#8 Ambience Ambience 5.649 24000 1 15919 7161ab2c29c0ad02
38 SFX 998A5D81:8 PSA_Underworld_Groans_9 PSA_Underworld_Groans#9 Ambience Ambience 7.804 24000 1 22303 e97522fb2216e189
39 SFX 998A5D81:9 PSA_Underworld_Groans_10 PSA_Underworld_Groans#10 Ambience Ambience 2.753 24000 1 8077 43a3682b07706ee1
40 SFX 99A8306A:0 AMB_Steel_C5_1 AMB_Steel_C5#1 Ambience Ambience 11.293 32000 4 305185 d41ddb1bea9221b2
41 SFX 99A8306A:1 AMB_Steel_C5_2 AMB_Steel_C5#2 Ambience Ambience 11.677 32000 4 318256 ab7d96341730b0fb
42 SFX 99A8306B AMB_Steel_C4 AMB_Steel_C4_1 Ambience Ambience 14.436 32000 4 402822 f43b23089b800ec3
43 SFX 99A8306C AMB_Steel_C3 AMB_Steel_C3_1 Ambience Ambience 7.346 32000 2 79462 deb1ccc91d8b0f26
44 SFX 99A8306D AMB_Steel_C2 AMB_Steel_C2_1 Ambience Ambience 25.600 32000 4 684810 706448449f2b4842
45 SFX 99A8306E AMB_Steel_C1 AMB_Steel_C1_1 Ambience Ambience 21.419 32000 4 609944 6c7606342cf158d7
46 SFX 99F8DB33:0 AMB_CW03_1 AMB_CW03#1 Ambience Ambience Catwoman 89.348 32000 4 2424261 77fef44e5c5a8d08
47 SFX 99F8DB33:1 AMB_CW03_2 AMB_CW03#2 Ambience Ambience Catwoman 9.461 32000 4 220339 50fa995c758b6ea6
48 SFX 99F8DB33:2 AMB_CW03_3 AMB_CW03#3 Ambience Ambience Catwoman 49.740 32000 4 1349759 78af61a718af1b0b
49 SFX 99F8DB33:3 AMB_CW03_4 AMB_CW03#4 Ambience Ambience Catwoman 30.336 48000 4 902577 aaa3b73a68f71eb7
50 SFX 99F8DB33:4 AMB_CW03_5 AMB_CW03#5 Ambience Ambience Catwoman 14.012 32000 4 381734 d9e66d2f055f9dbd
51 SFX 99F8DB33:5 AMB_CW03_6 AMB_CW03#6 Ambience Ambience Catwoman 48.736 32000 4 1377117 d6084ed25e4af0d7
52 SFX 9BA833B4:0 AMB_Steel_A1_1 AMB_Steel_A1#1 Ambience Ambience 6.981 32000 4 188172 e3353e1a1a24de74
53 SFX 9BA833B4:1 AMB_Steel_A1_2 AMB_Steel_A1#2 Ambience Ambience 24.742 32000 4 675277 2bc9fd4e72b321ac
54 SFX 9BA833B4:2 AMB_Steel_A1_3 AMB_Steel_A1#3 Ambience Ambience 24.742 32000 4 678133 a22748615a708356
55 SFX 9C8A3A75:0 AMB_RiddlerPuzzle07_1 AMB_RiddlerPuzzle07#1 Ambience Ambience 35.000 32000 4 886167 d0c28304e6b065a8
56 SFX 9C8A3A75:1 AMB_RiddlerPuzzle07_2 AMB_RiddlerPuzzle07#2 Ambience Ambience 42.000 32000 4 1157261 31b6ec6c6c4d55c1
57 SFX 9C8A3A7A:0 AMB_RiddlerPuzzle08_1 AMB_RiddlerPuzzle08#1 Ambience Ambience 35.000 32000 4 886167 d0c28304e6b065a8
58 SFX 9C8A3A7A:1 AMB_RiddlerPuzzle08_2 AMB_RiddlerPuzzle08#2 Ambience Ambience 42.000 32000 4 1157261 31b6ec6c6c4d55c1
59 SFX 9C903381 NPC_CourtCrowd NPC_CourtCrowd_1 Ambience Crowds 15.644 32000 4 429416 1ee9589220e2a052
60 SFX 9DA624C5 AMB_RiddlerChallenge04 AMB_RiddlerChallenge04_1 Ambience Ambience 35.157 32000 4 828243 506937146f3bb8bd
61 SFX A9AE706C:0 AMB_Train_1 AMB_Train#1 Ambience Ambience 21.409 32000 4 596226 341a9d16311d8991
62 SFX A9AE706C:1 AMB_Train_2 AMB_Train#2 Ambience Ambience 18.693 32000 4 546851 d3b35cb3327cad64
63 SFX A9AE706C:2 AMB_Train_3 AMB_Train#3 Ambience Ambience 13.563 32000 4 398654 ef7074176a9b5675
64 SFX AA9E3CD8:0 AMB_TheatreB1_1 AMB_TheatreB1#1 Ambience Ambience 36.693 32000 4 914155 fc81b138f8962af4
65 SFX AA9E3CD8:1 AMB_TheatreB1_2 AMB_TheatreB1#2 Ambience Ambience 35.072 32000 4 903712 3e175597e848055c
66 SFX BA9FCA68 AMB_DemonTrials AMB_DemonTrials_1 Ambience Ambience 42.411 32000 4 1287623 822e39e9fdb73c7d
67 SFX BE349608:0 AMB_BatmanCollapse_1 AMB_BatmanCollapse#1 Ambience Ambience 4.350 32000 5 132523 355a6907a49ba4f7
68 SFX BE349608:1 AMB_BatmanCollapse_2 AMB_BatmanCollapse#2 Ambience Ambience 3.838 32000 5 101089 37309cd7d8f681d2
69 SFX C4445D54:0 AMB_Underworld_C2_V1_1 AMB_Underworld_C2_V1#1 Ambience Ambience 15.616 32000 4 395281 328d3c9176d28670
70 SFX C4445D54:1 AMB_Underworld_C2_V1_2 AMB_Underworld_C2_V1#2 Ambience Ambience 24.064 32000 4 583180 3d41248514f09cbb
71 SFX C4445D54:2 AMB_Underworld_C2_V1_3 AMB_Underworld_C2_V1#3 Ambience Ambience 30.207 48000 4 815718 2d95a14358e05838
72 SFX D6C65F1E AMB_PDLC_A1 AMB_PDLC_A1_1 Ambience Ambience 41.643 32000 4 1225237 2386c71bc251bdab
73 SFX D6EB8083 AMB_NoraB1 AMB_NoraB1_1 Ambience Ambience 9.461 32000 4 220339 50fa995c758b6ea6
74 SFX D7C660F3:0 AMB_PDLC_B1_1 AMB_PDLC_B1#1 Ambience Ambience 29.325 32000 4 766195 7d4546670403542c
75 SFX D7C660F3:1 AMB_PDLC_B1_2 AMB_PDLC_B1#2 Ambience Ambience 29.579 32000 4 834813 5785d1e4bb6cde9a
76 SFX D7C660F3:2 AMB_PDLC_B1_3 AMB_PDLC_B1#3 Ambience Ambience 32.376 32000 4 865428 a0a0d2b4b83499f7
77 SFX DDAFD6E2:0 DDAFD6E2_1 DDAFD6E2#1 Unknown Unknown 1.827 48000 1 10677 a7893dbe34239855
78 SFX DDAFD6E2:1 DDAFD6E2_2 DDAFD6E2#2 Unknown Unknown 1.827 48000 1 10677 a7893dbe34239855
79 SFX DEA4B422:0 CHR_BM_MadHatterScream_1 CHR_BM_MadHatterScream#1 SFX Character foley Batman 2.531 48000 1 20481 861dcc285c25ad64
80 SFX DEA4B422:1 CHR_BM_MadHatterScream_2 CHR_BM_MadHatterScream#2 SFX Character foley Batman 1.465 48000 1 11958 7c711a9df472e6a5
81 SFX DEA4B422:2 CHR_BM_MadHatterScream_3 CHR_BM_MadHatterScream#3 SFX Character foley Batman 1.910 48000 1 15540 19a30c9788222076
82 SFX DEA4B422:3 CHR_BM_MadHatterScream_4 CHR_BM_MadHatterScream#4 SFX Character foley Batman 1.215 48000 1 9942 1c298f52e501f97e
83 SFX DEA4B422:4 CHR_BM_MadHatterScream_5 CHR_BM_MadHatterScream#5 SFX Character foley Batman 1.584 48000 1 13284 538645f2b4c6d26a
84 SFX DEA4B422:5 CHR_BM_MadHatterScream_6 CHR_BM_MadHatterScream#6 SFX Character foley Batman 1.577 48000 1 12839 3754080de3dad89a
85 SFX E8F980BE/003ACD60 Music_PDLC_003ACD60 Music Music (DLC) 9.227 48000 2 140179 8908f1b982728124
86 SFX E8F980BE/004B7F03 Music_PDLC_004B7F03 Music Music (DLC) 6.750 48000 2 101871 044ff907852c49e6
87 SFX E8F980BE/0070B665 Music_PDLC_0070B665 Music Music (DLC) 136.744 48000 2 2147137 690c3d62410d9109
88 SFX E8F980BE/00768D8F Music_PDLC_00768D8F Music Music (DLC) 5.167 48000 2 73843 bed749e799d3f9fc
89 SFX E8F980BE/00973366 Music_PDLC_00973366 Music Music (DLC) 7.303 48000 2 102108 d798af7f50edb775
90 SFX E8F980BE/00DEE385 Music_PDLC_00DEE385 Music Music (DLC) 136.744 48000 2 1671248 e526b81447e2a0b3
91 SFX E8F980BE/00EC1DE2 Music_PDLC_00EC1DE2 Music Music (DLC) 7.782 48000 2 114038 7df7abee227771a6
92 SFX E8F980BE/00FF49B2 Music_PDLC_00FF49B2 Music Music (DLC) 15.002 48000 2 246159 d4f4799b844e4972
93 SFX E8F980BE/0103E540 Music_PDLC_0103E540 Music Music (DLC) 5.896 48000 2 93093 4c79542c96d2bc82
94 SFX E8F980BE/0143D239 Music_PDLC_0143D239 Music Music (DLC) 11.245 48000 2 169362 8e1428544928412d
95 SFX E8F980BE/01B5E923 Music_PDLC_01B5E923 Music Music (DLC) 7.066 48000 2 108422 10af8989c0539907
96 SFX E8F980BE/01C03EEC Music_PDLC_01C03EEC Music Music (DLC) 5.455 48000 2 100514 ff1bed6cc4f9aac3
97 SFX E8F980BE/01C4D313 Music_PDLC_01C4D313 Music Music (DLC) 11.667 48000 2 196226 30f7225e239c7468
98 SFX E8F980BE/01DF861C Music_PDLC_01DF861C Music Music (DLC) 13.752 48000 2 208569 19ce186edc1ab5c0
99 SFX E8F980BE/022A88A0 Music_PDLC_022A88A0 Music Music (DLC) 124.043 48000 2 2079213 2ab1c9712fda7b1e
100 SFX E8F980BE/023B63DB Music_PDLC_023B63DB Music Music (DLC) 143.721 48000 2 1932671 7bc502c5161189e6
101 SFX E8F980BE/0299E758 Music_PDLC_0299E758 Music Music (DLC) 120.674 48000 2 2042297 6ed6b11c34f654e7
102 SFX E8F980BE/029B900E Music_PDLC_029B900E Music Music (DLC) 116.883 48000 2 1881727 34d02b7c101e4ef1
103 SFX E8F980BE/02D9C509 Music_PDLC_02D9C509 Music Music (DLC) 124.473 48000 2 1980220 a6c69c660b4d7cbc
104 SFX E8F980BE/033EA489 Music_PDLC_033EA489 Music Music (DLC) 118.667 48000 2 1863889 8c691dbb6f681bfd
105 SFX E8F980BE/033FCBD2 Music_PDLC_033FCBD2 Music Music (DLC) 4.243 48000 2 61878 08bab5850ea1325b
106 SFX E8F980BE/03448FF3 Music_PDLC_03448FF3 Music Music (DLC) 150.000 48000 2 2411615 297784dcc1dc7c92
107 SFX E8F980BE/034DAABC Music_PDLC_034DAABC Music Music (DLC) 37.091 48000 2 676458 80f6ec4ed1929c82
108 SFX E8F980BE/034E5611 Music_PDLC_034E5611 Music Music (DLC) 8.900 48000 2 125034 0d1788a6c0ca8bf3
109 SFX E8F980BE/036A3074 Music_PDLC_036A3074 Music Music (DLC) 120.667 48000 2 1874152 3b44666cbaab231c
110 SFX E8F980BE/03AE07AB Music_PDLC_03AE07AB Music Music (DLC) 121.411 48000 2 1888443 a2413bd7645e6d81
111 SFX E8F980BE/03BB3DC6 Music_PDLC_03BB3DC6 Music Music (DLC) 118.667 48000 2 2089926 54828927e669da60
112 SFX E8F980BE/03E46F5E Music_PDLC_03E46F5E Music Music (DLC) 121.411 48000 2 1946516 8072a4c11cd900b4
113 SFX E8F980BE/03E7A461 Music_PDLC_03E7A461 Music Music (DLC) 146.512 48000 2 2031291 5da7b39e1cd1ff0f
114 SFX E8F980BE/0482586F Music_PDLC_0482586F Music Music (DLC) 13.483 48000 2 193233 33c03a8ec1e8dcb4
115 SFX E8F980BE/0484CDE4 Music_PDLC_0484CDE4 Music Music (DLC) 132.600 48000 2 2072532 d093157052677ded
116 SFX E8F980BE/050DBEEE Music_PDLC_050DBEEE Music Music (DLC) 6.234 48000 2 94644 1bfa0843267bc39f
117 SFX E8F980BE/05333001 Music_PDLC_05333001 Music Music (DLC) 120.667 48000 2 1553715 109f7693bcd39fd4
118 SFX E8F980BE/0552DAA5 Music_PDLC_0552DAA5 Music Music (DLC) 99.736 48000 2 959593 22139a0192c71238
119 SFX E8F980BE/062ABA96 Music_PDLC_062ABA96 Music Music (DLC) 150.000 48000 2 2309451 80a70cbe8f929197
120 SFX E8F980BE/064B6476 Music_PDLC_064B6476 Music Music (DLC) 11.056 48000 2 165665 557b7c7ed6489ee0
121 SFX E8F980BE/06988828 Music_PDLC_06988828 Music Music (DLC) 10.655 48000 2 153388 b8ff7b3407abcdef
122 SFX E8F980BE/06D66515 Music_PDLC_06D66515 Music Music (DLC) 124.160 48000 2 2086304 c878089fa7505965
123 SFX E8F980BE/07F084BD Music_PDLC_07F084BD Music Music (DLC) 150.000 48000 2 2276849 701309519ea2a457
124 SFX E8F980BE/0882D3F9 Music_PDLC_0882D3F9 Music Music (DLC) 174.084 48000 2 2510757 3cef66645f7ac162
125 SFX E8F980BE/09899473 Music_PDLC_09899473 Music Music (DLC) 118.667 48000 2 1896684 f0063f284e9c0b92
126 SFX E8F980BE/0A398DE7 Music_PDLC_0A398DE7 Music Music (DLC) 37.091 48000 2 591400 b3351a5ac99fadff
127 SFX E8F980BE/0AB639E8 Music_PDLC_0AB639E8 Music Music (DLC) 3.870 48000 2 53364 99876e8ee09afd87
128 SFX E8F980BE/0AC66BD5 Music_PDLC_0AC66BD5 Music Music (DLC) 4.985 48000 2 78043 3ea7d2f72028d4f7
129 SFX E8F980BE/0AE6CF11 Music_PDLC_0AE6CF11 Music Music (DLC) 99.736 48000 2 1496790 5a615508319e4ff0
130 SFX E8F980BE/0AEB92B2 Music_PDLC_0AEB92B2 Music Music (DLC) 132.600 48000 2 2070514 5b46ec6590d1fb68
131 SFX E8F980BE/0B0A64EC Music_PDLC_0B0A64EC Music Music (DLC) 124.043 48000 2 1808300 86e2667fc2cdfaf3
132 SFX E8F980BE/0C190AF5 Music_PDLC_0C190AF5 Music Music (DLC) 116.883 48000 2 1873236 2c5b319d5f100636
133 SFX E8F980BE/0CF5DF5A Music_PDLC_0CF5DF5A Music Music (DLC) 116.883 48000 2 1941276 2463cd547fcdd70a
134 SFX E8F980BE/0E0B47BF Music_PDLC_0E0B47BF Music Music (DLC) 18.302 48000 2 271552 96ae88c63ce6f0b3
135 SFX E8F980BE/0FA4D723 Music_PDLC_0FA4D723 Music Music (DLC) 140.930 48000 2 1786777 e45ac4b09ae534ec
136 SFX E8F980BE/1141F701 Music_PDLC_1141F701 Music Music (DLC) 6.159 48000 2 91737 4d51998417d9cd7e
137 SFX E8F980BE/114EA5BA Music_PDLC_114EA5BA Music Music (DLC) 27.051 48000 2 435229 3c6f42829f0395d7
138 SFX E8F980BE/1153EB16 Music_PDLC_1153EB16 Music Music (DLC) 112.514 48000 2 1941069 90c37a7953826bff
139 SFX E8F980BE/1306E46D Music_PDLC_1306E46D Music Music (DLC) 65.130 48000 2 1058911 9957a92e97ce244f
140 SFX E8F980BE/130E1AD0 Music_PDLC_130E1AD0 Music Music (DLC) 9.714 48000 2 138900 16db4fc946de5120
141 SFX E8F980BE/136D5FB5 Music_PDLC_136D5FB5 Music Music (DLC) 26.764 48000 2 383894 edbae66d79d37c71
142 SFX E8F980BE/13C55A65 Music_PDLC_13C55A65 Music Music (DLC) 32.891 48000 2 515906 916faaffe6c5803d
143 SFX E8F980BE/141164AF Music_PDLC_141164AF Music Music (DLC) 114.167 48000 2 1722804 789d705f27ac48ff
144 SFX E8F980BE/14A4A6BF Music_PDLC_14A4A6BF Music Music (DLC) 114.167 48000 2 1925579 d0e77781234a58f8
145 SFX E8F980BE/14BE3D5F Music_PDLC_14BE3D5F Music Music (DLC) 5.280 48000 2 75630 e17368cc20c49925
146 SFX E8F980BE/15561AC7 Music_PDLC_15561AC7 Music Music (DLC) 132.600 48000 2 2266323 964e899a0c6cf057
147 SFX E8F980BE/15F943C2 Music_PDLC_15F943C2 Music Music (DLC) 148.310 48000 2 2326714 9002748400da728e
148 SFX E8F980BE/19434924 Music_PDLC_19434924 Music Music (DLC) 109.981 48000 2 1635385 1cfde52c13e04959
149 SFX E8F980BE/1C80531B Music_PDLC_1C80531B Music Music (DLC) 114.167 48000 2 1823209 fa38deac3aa77635
150 SFX E8F980BE/1D0EE8DC Music_PDLC_1D0EE8DC Music Music (DLC) 119.221 48000 2 1463918 e69ddb4be1b7b84b
151 SFX E8F980BE/1F44186D Music_PDLC_1F44186D Music Music (DLC) 99.736 48000 2 1514907 c21bde31f78fd34a
152 SFX E8F980BE/1F889215 Music_PDLC_1F889215 Music Music (DLC) 120.674 48000 2 1962204 d582d0342bc42104
153 SFX E8F980BE/1FFD945F Music_PDLC_1FFD945F Music Music (DLC) 18.045 48000 2 259821 4aeb30f7098adbbf
154 SFX E8F980BE/24CF1409 Music_PDLC_24CF1409 Music Music (DLC) 26.182 48000 2 456132 e1e55cfe4e98a17b
155 SFX E8F980BE/2714A216 Music_PDLC_2714A216 Music Music (DLC) 124.043 48000 2 1981814 289af901611c4269
156 SFX E8F980BE/2AA3CEA0 Music_PDLC_2AA3CEA0 Music Music (DLC) 26.182 48000 2 418694 ed57512cccf5c2be
157 SFX E8F980BE/2B07A62F Music_PDLC_2B07A62F Music Music (DLC) 5.429 48000 2 82642 f7158a1afd44959f
158 SFX E8F980BE/3C106E75 Music_PDLC_3C106E75 Music Music (DLC) 6.527 48000 2 95703 7732a64a42bac899
159 SFX E8F980BE/3C961A0D Music_PDLC_3C961A0D Music Music (DLC) 96.021 48000 2 1405544 5b6c24b48087e2b3
160 SFX E9044EB4:0 WND_Wind_Med_1 WND_Wind_Med#1 Ambience Wind 20.000 24000 2 98547 faab78aed8025611
161 SFX E9044EB4:1 WND_Wind_Med_2 WND_Wind_Med#2 Ambience Wind 20.000 24000 2 96838 e882b2fe710e4515
162 SFX E9044EB4:2 WND_Wind_Med_3 WND_Wind_Med#3 Ambience Wind 20.000 24000 2 95005 fb7e49936a93286f
163 SFX E9044EB4:3 WND_Wind_Med_4 WND_Wind_Med#4 Ambience Wind 20.000 24000 2 98328 cafcf14b9664292d
164 SFX E9044EB4:4 WND_Wind_Med_5 WND_Wind_Med#5 Ambience Wind 20.000 24000 2 98362 a69db077ae3c396d
165 SFX E9044EB4:5 WND_Wind_Med_6 WND_Wind_Med#6 Ambience Wind 20.000 24000 2 98183 2e882fa0c1ccdae3
166 SFX E9044EB4:6 WND_Wind_Med_7 WND_Wind_Med#7 Ambience Wind 20.000 24000 2 101052 935c5248c7aa37d8
167 SFX E9044EB4:7 WND_Wind_Med_8 WND_Wind_Med#8 Ambience Wind 20.000 24000 2 101958 538515b225fac347
168 SFX E9044EB4:8 WND_Wind_Med_9 WND_Wind_Med#9 Ambience Wind 20.000 24000 2 101329 5fba010811aadbb3
169 SFX E9044EB4:9 WND_Wind_Med_10 WND_Wind_Med#10 Ambience Wind 20.000 24000 2 102192 65b6a2dfde8c4057
170 SFX E9044EB4:10 WND_Wind_Med_11 WND_Wind_Med#11 Ambience Wind 20.000 24000 2 104947 d070102cc6bae5a6
171 SFX E9044EB4:11 WND_Wind_Med_12 WND_Wind_Med#12 Ambience Wind 20.000 24000 2 98166 4299280754fe97e5
172 SFX E9044EB4:12 WND_Wind_Med_13 WND_Wind_Med#13 Ambience Wind 20.000 24000 2 101738 7d6b5cb8b260d45d
173 SFX E9044EB4:13 WND_Wind_Med_14 WND_Wind_Med#14 Ambience Wind 20.000 24000 2 97973 67cbca7dcb8a93bd
174 SFX E9044EB4:14 WND_Wind_Med_15 WND_Wind_Med#15 Ambience Wind 20.000 24000 2 95465 fa6f6fbf194be5d9
175 SFX E9044EB4:15 WND_Wind_Med_16 WND_Wind_Med#16 Ambience Wind 20.000 24000 2 94809 4a5abcc79b1e0b80
176 SFX E9044EB4:16 WND_Wind_Med_17 WND_Wind_Med#17 Ambience Wind 20.000 24000 2 99949 d539e0c11f8f6ab7
177 SFX E9044EB4:17 WND_Wind_Med_18 WND_Wind_Med#18 Ambience Wind 20.000 24000 2 100418 6d1eaf75c4de53ac
178 SFX E9044EB4:18 WND_Wind_Med_19 WND_Wind_Med#19 Ambience Wind 20.000 24000 2 99048 2dc97369646e6958
179 SFX E9044EB4:19 WND_Wind_Med_20 WND_Wind_Med#20 Ambience Wind 20.000 24000 2 97280 36dcf4ccdebdf010
180 SFX E9044EB4:20 WND_Wind_Med_21 WND_Wind_Med#21 Ambience Wind 20.000 24000 2 101748 e8d6d2edc55c2cf2
181 SFX E9044EB4:21 WND_Wind_Med_22 WND_Wind_Med#22 Ambience Wind 20.000 24000 2 98163 3fa3cb70185cab36
182 SFX E9044EB4:22 WND_Wind_Med_23 WND_Wind_Med#23 Ambience Wind 20.000 24000 2 98468 9abafc3360d4dde0
183 SFX EBF95C27 AMB_CWB1 AMB_CWB1_1 Ambience Ambience 16.717 32000 4 482955 b5ce51f30e01cc11
184 SFX EDF036D6/003CB311 Music_003CB311 Music Music 29.760 48000 2 451510 eb4cdddd0ebce65d
185 SFX EDF036D6/004B7F03 Music_004B7F03 Music Music 6.750 48000 2 101871 044ff907852c49e6
186 SFX EDF036D6/006AC1CF Music_006AC1CF Music Music 8.000 48000 2 121670 ba0f4a31a96bbf0a
187 SFX EDF036D6/00768D8F Music_00768D8F Music Music 5.167 48000 2 73843 bed749e799d3f9fc
188 SFX EDF036D6/0077966A Music_0077966A Music Music 139.862 48000 2 2240069 0aa0ab711de88e99
189 SFX EDF036D6/0091757B Music_0091757B Music Music 136.358 48000 2 2079414 036ba4f5985870f5
190 SFX EDF036D6/0094ECE0 Music_0094ECE0 Music Music 28.000 48000 2 427330 c809d53c9ac3a4ff
191 SFX EDF036D6/00973366 Music_00973366 Music Music 7.303 48000 2 102108 d798af7f50edb775
192 SFX EDF036D6/00CF19F6 Music_00CF19F6 Music Music 36.651 48000 2 652015 21fd45e0ab2f46a3
193 SFX EDF036D6/00EC1DE2 Music_00EC1DE2 Music Music 7.782 48000 2 114038 7df7abee227771a6
194 SFX EDF036D6/00FA6232 Music_00FA6232 Music Music 136.744 48000 2 2147137 690c3d62410d9109
195 SFX EDF036D6/00FF49B2 Music_00FF49B2 Music Music 15.002 48000 2 246159 d4f4799b844e4972
196 SFX EDF036D6/0103E540 Music_0103E540 Music Music 5.896 48000 2 93093 4c79542c96d2bc82
197 SFX EDF036D6/0143D239 Music_0143D239 Music Music 11.245 48000 2 169362 8e1428544928412d
198 SFX EDF036D6/01714272 Music_01714272 Music Music 134.234 48000 2 1903999 ac187b1ed9b3e297
199 SFX EDF036D6/0187CAC4 Music_0187CAC4 Music Music 5.969 48000 2 85367 fcca428404e1ee90
200 SFX EDF036D6/018F4EE2 Music_018F4EE2 Music Music 15.360 48000 2 267100 ff8bebf954391f4f
201 SFX EDF036D6/01916433 Music_01916433 Music Music 18.100 48000 2 262068 cdee9195621576d5
202 SFX EDF036D6/01B5E923 Music_01B5E923 Music Music 7.066 48000 2 108422 10af8989c0539907
203 SFX EDF036D6/01C03EEC Music_01C03EEC Music Music 5.455 48000 2 100514 ff1bed6cc4f9aac3
204 SFX EDF036D6/01C4D313 Music_01C4D313 Music Music 11.667 48000 2 196226 30f7225e239c7468
205 SFX EDF036D6/01C73384 Music_01C73384 Music Music 21.404 48000 2 307229 4b1412e1904010f6
206 SFX EDF036D6/01DF861C Music_01DF861C Music Music 13.752 48000 2 208569 19ce186edc1ab5c0
207 SFX EDF036D6/022A88A0 Music_022A88A0 Music Music 124.043 48000 2 2079213 2ab1c9712fda7b1e
208 SFX EDF036D6/0236F058 Music_0236F058 Music Music 10.339 48000 2 157180 f69af5f5fa7089cc
209 SFX EDF036D6/023B63DB Music_023B63DB Music Music 143.721 48000 2 1932671 7bc502c5161189e6
210 SFX EDF036D6/0299E758 Music_0299E758 Music Music 120.674 48000 2 2042297 6ed6b11c34f654e7
211 SFX EDF036D6/02BD77FA Music_02BD77FA Music Music 4.177 48000 2 67717 eb9870b55a8edf72
212 SFX EDF036D6/030666A2 Music_030666A2 Music Music 2.118 48000 2 30627 9c49ed214407c3f7
213 SFX EDF036D6/030EAAF4 Music_030EAAF4 Music Music 27.001 48000 2 417402 a8aae159b0b70e0f
214 SFX EDF036D6/033EA489 Music_033EA489 Music Music 118.667 48000 2 1863889 8c691dbb6f681bfd
215 SFX EDF036D6/033FCBD2 Music_033FCBD2 Music Music 4.243 48000 2 61878 08bab5850ea1325b
216 SFX EDF036D6/03448FF3 Music_03448FF3 Music Music 150.000 48000 2 2411615 297784dcc1dc7c92
217 SFX EDF036D6/036A3074 Music_036A3074 Music Music 120.667 48000 2 1874152 3b44666cbaab231c
218 SFX EDF036D6/03A77EDB Music_03A77EDB Music Music 5.056 48000 2 74536 0358b137e007ef69
219 SFX EDF036D6/03AE07AB Music_03AE07AB Music Music 121.411 48000 2 1888443 a2413bd7645e6d81
220 SFX EDF036D6/03BB3DC6 Music_03BB3DC6 Music Music 118.667 48000 2 2089926 54828927e669da60
221 SFX EDF036D6/03CF5072 Music_03CF5072 Music Music 5.355 48000 2 77471 8c8e11b1b6730eee
222 SFX EDF036D6/03E46F5E Music_03E46F5E Music Music 121.411 48000 2 1946516 8072a4c11cd900b4
223 SFX EDF036D6/03E7A461 Music_03E7A461 Music Music 146.512 48000 2 2031291 5da7b39e1cd1ff0f
224 SFX EDF036D6/03EF5052 Music_03EF5052 Music Music 10.933 48000 2 165614 0a81b7aed4e19666
225 SFX EDF036D6/03F43679 Music_03F43679 Music Music 65.479 48000 2 910455 136a1d2be974aca4
226 SFX EDF036D6/03F81E23 Music_03F81E23 Music Music 12.827 48000 2 220428 346c196b48cc25f1
227 SFX EDF036D6/041B0207 Music_041B0207 Music Music 7.273 48000 2 115780 3b54e829d62e2cdd
228 SFX EDF036D6/041B3ED1 Music_041B3ED1 Music Music 136.571 48000 2 2156010 55794b221524244e
229 SFX EDF036D6/045740E7 Music_045740E7 Music Music 124.473 48000 2 1980220 a6c69c660b4d7cbc
230 SFX EDF036D6/04626D74 Music_04626D74 Music Music 33.231 48000 2 497870 7c8229f48538d37f
231 SFX EDF036D6/046D8256 Music_046D8256 Music Music 116.883 48000 2 1881727 34d02b7c101e4ef1
232 SFX EDF036D6/0482586F Music_0482586F Music Music 13.483 48000 2 193233 33c03a8ec1e8dcb4
233 SFX EDF036D6/0484CDE4 Music_0484CDE4 Music Music 132.600 48000 2 2072532 d093157052677ded
234 SFX EDF036D6/04A296D5 Music_04A296D5 Music Music 139.862 48000 2 2050965 ed4d836843d773e2
235 SFX EDF036D6/04C378D7 Music_04C378D7 Music Music 8.048 48000 2 121070 ddb4f685b987aa77
236 SFX EDF036D6/04DBE3AB Music_04DBE3AB Music Music 59.521 48000 2 818632 1fc309fc1ad2e944
237 SFX EDF036D6/0518B1B7 Music_0518B1B7 Music Music 11.427 48000 2 169289 2c178bbdbf1925b9
238 SFX EDF036D6/05333001 Music_05333001 Music Music 120.667 48000 2 1553715 109f7693bcd39fd4
239 SFX EDF036D6/05470280 Music_05470280 Music Music 128.764 48000 2 1746550 b33dd74c1bb0a7f1
240 SFX EDF036D6/05513772 Music_05513772 Music Music 120.674 48000 2 1815939 ef2aa623f394818d
241 SFX EDF036D6/0552DAA5 Music_0552DAA5 Music Music 99.736 48000 2 959593 22139a0192c71238
242 SFX EDF036D6/058231ED Music_058231ED Music Music 68.211 48000 2 1138245 86df0acdfdfc1df0
243 SFX EDF036D6/0587577E Music_0587577E Music Music 139.862 48000 2 2198296 560ce46975841a7e
244 SFX EDF036D6/05C8D32A Music_05C8D32A Music Music 26.526 48000 2 343088 73bef0da1727bba3
245 SFX EDF036D6/060E415F Music_060E415F Music Music 11.654 48000 2 161817 8f0c7e25073d4374
246 SFX EDF036D6/062ABA96 Music_062ABA96 Music Music 150.000 48000 2 2309451 80a70cbe8f929197
247 SFX EDF036D6/064B6476 Music_064B6476 Music Music 11.056 48000 2 165665 557b7c7ed6489ee0
248 SFX EDF036D6/06503115 Music_06503115 Music Music 57.931 48000 2 1145563 979b62622e8d74dd
249 SFX EDF036D6/067C3EF1 Music_067C3EF1 Music Music 10.288 48000 2 144367 4b61f96005d0861e
250 SFX EDF036D6/06963560 Music_06963560 Music Music 10.541 48000 2 169361 4684815a1a026ef9
251 SFX EDF036D6/06988828 Music_06988828 Music Music 10.655 48000 2 153388 b8ff7b3407abcdef
252 SFX EDF036D6/06AD24B7 Music_06AD24B7 Music Music 13.664 48000 2 216979 2e9d08baf192bc27
253 SFX EDF036D6/06D66515 Music_06D66515 Music Music 124.160 48000 2 2086304 c878089fa7505965
254 SFX EDF036D6/06E7FD0F Music_06E7FD0F Music Music 220.337 48000 2 3283587 97a25b3b1e86da70
255 SFX EDF036D6/06F1B02B Music_06F1B02B Music Music 10.909 48000 2 175394 906d1ab34e0676c5
256 SFX EDF036D6/0748B510 Music_0748B510 Music Music 4.637 48000 2 65408 c19c38f670f623da
257 SFX EDF036D6/0792E524 Music_0792E524 Music Music 63.140 48000 2 953656 401d370d42bcacf2
258 SFX EDF036D6/0793DE7C Music_0793DE7C Music Music 34.415 48000 2 490514 da62323a54973ef5
259 SFX EDF036D6/07C1C384 Music_07C1C384 Music Music 7.579 48000 2 127218 624b4083a829d00a
260 SFX EDF036D6/07D74727 Music_07D74727 Music Music 5.455 48000 2 83321 e7586180df9e15d2
261 SFX EDF036D6/07D7BB90 Music_07D7BB90 Music Music 128.764 48000 2 2069659 b28e2b67e237e1d4
262 SFX EDF036D6/07ED9BED Music_07ED9BED Music Music 5.000 48000 2 83513 3ac88d66cc7e2af4
263 SFX EDF036D6/07F084BD Music_07F084BD Music Music 150.000 48000 2 2276849 701309519ea2a457
264 SFX EDF036D6/08028AF6 Music_08028AF6 Music Music 27.032 48000 2 447037 3ea40013c34c5bfb
265 SFX EDF036D6/08350005 Music_08350005 Music Music 10.163 48000 2 163179 53b8f892cec18939
266 SFX EDF036D6/0879D4B8 Music_0879D4B8 Music Music 31.054 48000 2 432075 b0eae4067e80b8a4
267 SFX EDF036D6/08B01ABF Music_08B01ABF Music Music 5.920 48000 2 104192 fe272d172651fd13
268 SFX EDF036D6/08B289F1 Music_08B289F1 Music Music 59.951 48000 2 968149 3c93984f24d71dcf
269 SFX EDF036D6/08DEC601 Music_08DEC601 Music Music 7.984 48000 2 118158 4dc163d490d6b082
270 SFX EDF036D6/08FDF59F Music_08FDF59F Music Music 4.027 48000 2 59853 d642287171dac178
271 SFX EDF036D6/08FEB583 Music_08FEB583 Music Music 111.019 48000 2 1957859 e51988fee897cf9a
272 SFX EDF036D6/09268C8B Music_09268C8B Music Music 51.796 48000 2 792757 5ed884a66cbd99f4
273 SFX EDF036D6/09475E4C Music_09475E4C Music Music 59.200 48000 3 1046008 744aa70e18aeb5bd
274 SFX EDF036D6/0963C63E Music_0963C63E Music Music 136.744 48000 2 1671248 e526b81447e2a0b3
275 SFX EDF036D6/09662D97 Music_09662D97 Music Music 51.393 48000 2 705497 a886aa3314db3230
276 SFX EDF036D6/0966308B Music_0966308B Music Music 145.655 48000 2 2252265 c226fa9ab4947989
277 SFX EDF036D6/09899473 Music_09899473 Music Music 118.667 48000 2 1896684 f0063f284e9c0b92
278 SFX EDF036D6/09B90B1F Music_09B90B1F Music Music 12.957 48000 2 191397 4f07c23037242a86
279 SFX EDF036D6/09BCB885 Music_09BCB885 Music Music 27.183 48000 2 355145 452006f7d55b7542
280 SFX EDF036D6/09DF28BC Music_09DF28BC Music Music 8.900 48000 2 125034 0d1788a6c0ca8bf3
281 SFX EDF036D6/0A270868 Music_0A270868 Music Music 56.114 48000 2 829545 9b3d70dfd1d0bf7d
282 SFX EDF036D6/0A3391C4 Music_0A3391C4 Music Music 56.034 48000 2 862686 f56d0c0a35170987
283 SFX EDF036D6/0A35D517 Music_0A35D517 Music Music 49.232 48000 2 695149 6156c833d17fa9d5
284 SFX EDF036D6/0A398DE7 Music_0A398DE7 Music Music 37.091 48000 2 591407 ac67b2eaff5065d5
285 SFX EDF036D6/0A45BC45 Music_0A45BC45 Music Music 7.026 48000 2 110650 a81eeb8497dc6087
286 SFX EDF036D6/0A514BC5 Music_0A514BC5 Music Music 5.031 48000 2 69166 27a2d8a8524eabdc
287 SFX EDF036D6/0A6A4BEA Music_0A6A4BEA Music Music 120.674 48000 2 1998826 63ec01f5a15e3625
288 SFX EDF036D6/0AB639E8 Music_0AB639E8 Music Music 3.870 48000 2 53364 99876e8ee09afd87
289 SFX EDF036D6/0AE2EB37 Music_0AE2EB37 Music Music 9.227 48000 2 140179 8908f1b982728124
290 SFX EDF036D6/0AE6CF11 Music_0AE6CF11 Music Music 99.736 48000 2 1496790 5a615508319e4ff0
291 SFX EDF036D6/0AE6E55A Music_0AE6E55A Music Music 5.732 48000 2 78011 f1ce12bc172ee924
292 SFX EDF036D6/0B074B6F Music_0B074B6F Music Music 15.401 48000 2 236110 164b12aebce490d7
293 SFX EDF036D6/0B0A64EC Music_0B0A64EC Music Music 124.043 48000 2 1808300 86e2667fc2cdfaf3
294 SFX EDF036D6/0B591BEA Music_0B591BEA Music Music 9.600 48000 2 137803 8742c5db432792e2
295 SFX EDF036D6/0B8D3FA7 Music_0B8D3FA7 Music Music 37.091 48000 2 676472 c80ab19d29f54db2
296 SFX EDF036D6/0B934098 Music_0B934098 Music Music 130.386 48000 6 5433905 542486c69ab21c73
297 SFX EDF036D6/0BAFE185 Music_0BAFE185 Music Music 120.000 48000 2 1870515 af2b9390c746d55a
298 SFX EDF036D6/0BCB4DFB Music_0BCB4DFB Music Music 11.194 48000 2 157866 1893d0875bc0324c
299 SFX EDF036D6/0BD2F54C Music_0BD2F54C Music Music 4.000 48000 2 62058 c1364139c5ab6796
300 SFX EDF036D6/0BE54B8B Music_0BE54B8B Music Music 82.587 48000 2 1154038 b91f2a7731e80d93
301 SFX EDF036D6/0C190AF5 Music_0C190AF5 Music Music 116.883 48000 2 1873236 2c5b319d5f100636
302 SFX EDF036D6/0C786C68 Music_0C786C68 Music Music 25.455 48000 2 374798 9f32e428d0415a75
303 SFX EDF036D6/0CF5DF5A Music_0CF5DF5A Music Music 116.883 48000 2 1941276 2463cd547fcdd70a
304 SFX EDF036D6/0D080AF2 Music_0D080AF2 Music Music 24.841 48000 2 227956 3f0d0f61677ac580
305 SFX EDF036D6/0D116946 Music_0D116946 Music Music 136.571 48000 2 2261132 683f0bf6dc70b451
306 SFX EDF036D6/0D1D673E Music_0D1D673E Music Music 12.809 48000 2 217995 78bab68bfcaa5d43
307 SFX EDF036D6/0D395255 Music_0D395255 Music Music 114.880 48000 2 1771509 e2cdde27731d21d9
308 SFX EDF036D6/0D5E8A4E Music_0D5E8A4E Music Music 34.172 48000 6 620536 63c45a778bf37bf5
309 SFX EDF036D6/0D9F4730 Music_0D9F4730 Music Music 114.884 48000 2 1706066 42cfd6f97989241c
310 SFX EDF036D6/0DA55A9A Music_0DA55A9A Music Music 67.830 24000 2 349704 a4a8b9249bdc87ef
311 SFX EDF036D6/0DCDB7EA Music_0DCDB7EA Music Music 61.519 48000 6 1138993 4f2c30017b90638f
312 SFX EDF036D6/0DCF4A58 Music_0DCF4A58 Music Music 25.703 48000 2 365874 3cbd811d75dec15e
313 SFX EDF036D6/0DD4CB01 Music_0DD4CB01 Music Music 9.230 48000 2 132559 3b88efd559836afd
314 SFX EDF036D6/0DD67C32 Music_0DD67C32 Music Music 132.600 48000 2 2070514 5b46ec6590d1fb68
315 SFX EDF036D6/0DD82CDF Music_0DD82CDF Music Music 66.801 48000 2 973802 5632edb072828857
316 SFX EDF036D6/0E0B47BF Music_0E0B47BF Music Music 18.302 48000 2 271552 96ae88c63ce6f0b3
317 SFX EDF036D6/0E0D5646 Music_0E0D5646 Music Music 10.909 48000 2 179745 740b7105d6b42ff6
318 SFX EDF036D6/0E0F52CB Music_0E0F52CB Music Music 7.579 48000 2 102397 73ef4f5e53d2b4b1
319 SFX EDF036D6/0E6EA120 Music_0E6EA120 Music Music 4.985 48000 2 78043 3ea7d2f72028d4f7
320 SFX EDF036D6/0E8284B0 Music_0E8284B0 Music Music 13.633 48000 2 200731 0fb0d5eaea927482
321 SFX EDF036D6/0EC73393 Music_0EC73393 Music Music 56.689 48000 2 843627 8bce3f68a65ae479
322 SFX EDF036D6/0F1BB771 Music_0F1BB771 Music Music 5.357 48000 2 79456 98a6143e52c6fe9c
323 SFX EDF036D6/0F2E5BE7 Music_0F2E5BE7 Music Music 115.955 48000 2 1780926 7f34763c2bac1cbc
324 SFX EDF036D6/0F927604 Music_0F927604 Music Music 4.100 48000 2 60427 341074b2bcd2cc3e
325 SFX EDF036D6/0FA4D723 Music_0FA4D723 Music Music 140.930 48000 2 1786777 e45ac4b09ae534ec
326 SFX EDF036D6/100725CE Music_100725CE Music Music 138.805 48000 2 1988903 aede3b9c498119bd
327 SFX EDF036D6/103350C6 Music_103350C6 Music Music 10.706 48000 2 163488 6cd9394131897d8f
328 SFX EDF036D6/1045B2D0 Music_1045B2D0 Music Music 26.318 48000 2 439396 49ad8f1567a94ab2
329 SFX EDF036D6/107651F0 Music_107651F0 Music Music 11.031 48000 2 154397 dc018d1059ac9132
330 SFX EDF036D6/10CEC694 Music_10CEC694 Music Music 21.783 48000 2 337478 838175d3a11af025
331 SFX EDF036D6/10D95181 Music_10D95181 Music Music 62.161 48000 2 943143 4ec6adc0ff80df6f
332 SFX EDF036D6/10DD7E39 Music_10DD7E39 Music Music 41.038 48000 2 574465 c073bbdbf9162a17
333 SFX EDF036D6/10EF999B Music_10EF999B Music Music 7.836 48000 2 54377 30f28b938ba61758
334 SFX EDF036D6/11011607 Music_11011607 Music Music 64.928 48000 2 1008393 2f84bf32973e615f
335 SFX EDF036D6/11213CD9 Music_11213CD9 Music Music 5.472 48000 2 74798 5636cd7483facacc
336 SFX EDF036D6/1141F701 Music_1141F701 Music Music 6.159 48000 2 91737 4d51998417d9cd7e
337 SFX EDF036D6/114C2C42 Music_114C2C42 Music Music 57.931 48000 2 1210901 d7ecc9ca26aca3d3
338 SFX EDF036D6/114EA5BA Music_114EA5BA Music Music 27.051 48000 2 435215 cb7c21d127a57bda
339 SFX EDF036D6/114F6595 Music_114F6595 Music Music 137.553 48000 2 2138964 6ea746351dcabf9c
340 SFX EDF036D6/11749351 Music_11749351 Music Music 110.909 48000 2 1700949 cc8c7f8fe193a36d
341 SFX EDF036D6/1184538D Music_1184538D Music Music 57.931 48000 2 836829 7ae3a034a1a14842
342 SFX EDF036D6/11B3505F Music_11B3505F Music Music 119.040 48000 2 1804042 7ea5b6c29fc28a96
343 SFX EDF036D6/11D06D0C Music_11D06D0C Music Music 64.669 48000 2 960537 28e584eeced2b6ce
344 SFX EDF036D6/11D477D1 Music_11D477D1 Music Music 69.629 48000 2 976260 12a1237d1346aa35
345 SFX EDF036D6/1231904E Music_1231904E Music Music 5.472 48000 2 79207 828e7962c313978d
346 SFX EDF036D6/12CB7EA2 Music_12CB7EA2 Music Music 21.063 48000 2 306437 d13a3193ca5a8541
347 SFX EDF036D6/12DFC5A3 Music_12DFC5A3 Music Music 120.674 48000 2 1905166 3722d236172a35dd
348 SFX EDF036D6/12F0D318 Music_12F0D318 Music Music 25.576 48000 2 460855 970213287c121e61
349 SFX EDF036D6/130425A1 Music_130425A1 Music Music 4.638 48000 2 69039 39d636a6c20cb4a9
350 SFX EDF036D6/1304DD30 Music_1304DD30 Music Music 6.124 48000 2 79539 485a9a0e2a46c58a
351 SFX EDF036D6/1306E46D Music_1306E46D Music Music 65.130 48000 2 1058911 9957a92e97ce244f
352 SFX EDF036D6/130E1AD0 Music_130E1AD0 Music Music 9.714 48000 2 138900 16db4fc946de5120
353 SFX EDF036D6/134A4A91 Music_134A4A91 Music Music 12.488 48000 2 176307 69bfef90f2dacf24
354 SFX EDF036D6/136D5FB5 Music_136D5FB5 Music Music 26.764 48000 2 383894 edbae66d79d37c71
355 SFX EDF036D6/1388391B Music_1388391B Music Music 92.559 48000 2 1237011 7d966e508d118f35
356 SFX EDF036D6/138BDCCF Music_138BDCCF Music Music 7.680 48000 2 109178 65e4ba1a2297baa1
357 SFX EDF036D6/139E54D8 Music_139E54D8 Music Music 7.321 48000 2 119246 672ff00e9ef04c26
358 SFX EDF036D6/13A0FCF1 Music_13A0FCF1 Music Music 33.822 48000 6 620811 113344a096e024c9
359 SFX EDF036D6/13D03B5C Music_13D03B5C Music Music 124.148 48000 2 1917760 243ae8dcce359a64
360 SFX EDF036D6/13FC57BF Music_13FC57BF Music Music 11.864 48000 2 175369 b9851e6d3b65b196
361 SFX EDF036D6/141164AF Music_141164AF Music Music 114.167 48000 2 1722804 789d705f27ac48ff
362 SFX EDF036D6/1417F135 Music_1417F135 Music Music 18.416 48000 2 252360 502fb3a6747f1edc
363 SFX EDF036D6/142BD546 Music_142BD546 Music Music 33.600 48000 2 469861 72b38f1a2d26dd8f
364 SFX EDF036D6/145794F0 Music_145794F0 Music Music 4.667 48000 2 65468 3b6e4b9c36e9809b
365 SFX EDF036D6/149379DA Music_149379DA Music Music 11.324 48000 2 155217 42e0d077e4514dce
366 SFX EDF036D6/14949A24 Music_14949A24 Music Music 68.211 48000 2 1014148 c63a08eb40b2c73c
367 SFX EDF036D6/14BE3D5F Music_14BE3D5F Music Music 5.280 48000 2 75630 e17368cc20c49925
368 SFX EDF036D6/14C7314B Music_14C7314B Music Music 7.083 48000 2 110811 c4cc9738a29f1c64
369 SFX EDF036D6/14E1DF85 Music_14E1DF85 Music Music 6.998 48000 2 102881 6aa334d345da108a
370 SFX EDF036D6/15166641 Music_15166641 Music Music 9.931 48000 2 120665 66da27b2a11d71bb
371 SFX EDF036D6/1549DDB2 Music_1549DDB2 Music Music 37.091 48000 2 595527 01355f355c0b2853
372 SFX EDF036D6/15561AC7 Music_15561AC7 Music Music 132.600 48000 2 2266323 964e899a0c6cf057
373 SFX EDF036D6/15F24B15 Music_15F24B15 Music Music 7.788 48000 2 122403 4017b5d94a68cd74
374 SFX EDF036D6/163C8641 Music_163C8641 Music Music 30.583 48000 2 418441 602775842cc2eea1
375 SFX EDF036D6/166847B7 Music_166847B7 Music Music 11.272 48000 2 156467 86814d3b24ff9b7e
376 SFX EDF036D6/168E0EDC Music_168E0EDC Music Music 64.527 48000 2 899368 3cf382b0b081a7ba
377 SFX EDF036D6/16AEE4FD Music_16AEE4FD Music Music 4.739 48000 2 75711 5e466c8bf9a4d172
378 SFX EDF036D6/16C59E4D Music_16C59E4D Music Music 27.006 48000 2 479283 4171a30575d9123e
379 SFX EDF036D6/17A65BC8 Music_17A65BC8 Music Music 51.855 48000 2 735688 93e09affa01d21b5
380 SFX EDF036D6/17B8E786 Music_17B8E786 Music Music 6.234 48000 2 94644 1bfa0843267bc39f
381 SFX EDF036D6/18177B73 Music_18177B73 Music Music 49.880 48000 2 737499 888ab9710ee6bc1d
382 SFX EDF036D6/187BD87F Music_187BD87F Music Music 27.580 48000 2 407557 a0d79446b1b33012
383 SFX EDF036D6/18CE2848 Music_18CE2848 Music Music 16.726 48000 2 188870 60ab2664a3f64afd
384 SFX EDF036D6/190E481E Music_190E481E Music Music 56.486 48000 2 804642 5d6b1680a58b085a
385 SFX EDF036D6/1951CABD Music_1951CABD Music Music 20.105 48000 2 331402 4530a35f0898041a
386 SFX EDF036D6/1A2A956A Music_1A2A956A Music Music 120.674 48000 2 1773755 c5b6ce6781144d37
387 SFX EDF036D6/1B55A21D Music_1B55A21D Music Music 148.310 48000 2 2326714 9002748400da728e
388 SFX EDF036D6/1B995440 Music_1B995440 Music Music 82.719 48000 2 1230786 4eea50935d8861bb
389 SFX EDF036D6/1BB0C4AD Music_1BB0C4AD Music Music 136.568 48000 2 1695285 537e4507c59cb4dd
390 SFX EDF036D6/1C0025EC Music_1C0025EC Music Music 55.204 48000 2 851331 ae51ad07eadff2a0
391 SFX EDF036D6/1C0067DB Music_1C0067DB Music Music 174.084 48000 2 2510757 3cef66645f7ac162
392 SFX EDF036D6/1C070DC7 Music_1C070DC7 Music Music 15.158 48000 2 243328 7c2eb9df93a9dba7
393 SFX EDF036D6/1D5BDC25 Music_1D5BDC25 Music Music 30.005 48000 2 499008 d8217f8ccde1a7c6
394 SFX EDF036D6/1D9BCFEA Music_1D9BCFEA Music Music 27.051 48000 2 346917 4f5341cdf41e3dfe
395 SFX EDF036D6/1D9C5132 Music_1D9C5132 Music Music 18.876 48000 2 334388 c3d9f7b58e388452
396 SFX EDF036D6/1DBF5552 Music_1DBF5552 Music Music 124.212 48000 2 1883930 905e1ec95b5de2c1
397 SFX EDF036D6/1DC5D065 Music_1DC5D065 Music Music 49.865 48000 6 909285 0c96d8d8a0e119c7
398 SFX EDF036D6/1DE02287 Music_1DE02287 Music Music 128.764 48000 2 1957556 b3e0906c43c188af
399 SFX EDF036D6/1E326B83 Music_1E326B83 Music Music 13.653 48000 2 215358 a5334288e71969f0
400 SFX EDF036D6/1E7F8C2D Music_1E7F8C2D Music Music 10.645 48000 2 163632 61797286fd511a82
401 SFX EDF036D6/1ED075D1 Music_1ED075D1 Music Music 10.909 48000 2 116934 c6116d638313c3e5
402 SFX EDF036D6/1ED19140 Music_1ED19140 Music Music 118.144 48000 2 1977830 cd18434c96a33371
403 SFX EDF036D6/1EDB523D Music_1EDB523D Music Music 57.989 48000 2 978464 a9303a561db1736c
404 SFX EDF036D6/1F44015A Music_1F44015A Music Music 150.000 48000 2 2235862 ff86dc19c70f97be
405 SFX EDF036D6/1F889215 Music_1F889215 Music Music 120.674 48000 2 1962204 d582d0342bc42104
406 SFX EDF036D6/1FFD945F Music_1FFD945F Music Music 18.045 48000 2 259821 4aeb30f7098adbbf
407 SFX EDF036D6/204449FE Music_204449FE Music Music 49.892 48000 2 717441 b1db8172723811e4
408 SFX EDF036D6/2071230E Music_2071230E Music Music 99.736 48000 2 1514907 c21bde31f78fd34a
409 SFX EDF036D6/20857A8E Music_20857A8E Music Music 109.981 48000 2 1635385 1cfde52c13e04959
410 SFX EDF036D6/2105D597 Music_2105D597 Music Music 9.600 48000 2 98961 557f70acbbcfc2fe
411 SFX EDF036D6/2113CD65 Music_2113CD65 Music Music 112.514 48000 2 1941069 90c37a7953826bff
412 SFX EDF036D6/2199A947 Music_2199A947 Music Music 124.212 48000 2 1946256 b94df883b92bf6c7
413 SFX EDF036D6/2239696F Music_2239696F Music Music 14.659 48000 2 222484 899cd11416591458
414 SFX EDF036D6/22C833C6 Music_22C833C6 Music Music 41.468 48000 2 586179 8e5ece33b943d4de
415 SFX EDF036D6/23207A9C Music_23207A9C Music Music 150.000 48000 2 2261291 4f45d4cb0fe64351
416 SFX EDF036D6/23F9F43D Music_23F9F43D Music Music 100.231 48000 2 1638853 f9e28ef5ed84a60a
417 SFX EDF036D6/242766B2 Music_242766B2 Music Music 32.891 48000 2 515906 916faaffe6c5803d
418 SFX EDF036D6/2448BD91 Music_2448BD91 Music Music 5.724 48000 2 83185 d115a598c998aef9
419 SFX EDF036D6/246625C9 Music_246625C9 Music Music 114.167 48000 2 1823209 fa38deac3aa77635
420 SFX EDF036D6/24FEAAA6 Music_24FEAAA6 Music Music 4.999 48000 2 68925 fdccc549d81b3b54
421 SFX EDF036D6/25A00909 Music_25A00909 Music Music 4.007 48000 2 56999 30c67fa1ef9c0000
422 SFX EDF036D6/25A7D9DB Music_25A7D9DB Music Music 49.876 48000 2 719556 7b4ef5b74c5fcc22
423 SFX EDF036D6/25D08F49 Music_25D08F49 Music Music 5.241 48000 2 79549 1e6a7c7129cb3843
424 SFX EDF036D6/25F4F497 Music_25F4F497 Music Music 8.971 48000 2 147262 6e20fc042dd125cf
425 SFX EDF036D6/263E3B2E Music_263E3B2E Music Music 120.674 48000 2 1962177 0f512c818b122acf
426 SFX EDF036D6/26F6662B Music_26F6662B Music Music 84.268 48000 2 1252553 937b034854575c25
427 SFX EDF036D6/2714A216 Music_2714A216 Music Music 124.043 48000 2 1981814 289af901611c4269
428 SFX EDF036D6/27B481B5 Music_27B481B5 Music Music 42.180 48000 2 590926 6aee62ad4d7686de
429 SFX EDF036D6/286EAFBC Music_286EAFBC Music Music 25.576 48000 2 464876 01f9a32a5c85562c
430 SFX EDF036D6/291F6742 Music_291F6742 Music Music 52.323 48000 2 804372 425f49258782e7ac
431 SFX EDF036D6/29390786 Music_29390786 Music Music 6.661 48000 2 101067 7c84fa8ca1da620d
432 SFX EDF036D6/294C967B Music_294C967B Music Music 11.322 48000 2 161017 ca393e47bc8ecbd1
433 SFX EDF036D6/296B28EE Music_296B28EE Music Music 71.581 48000 2 1018253 511aba3e78e85eee
434 SFX EDF036D6/2987BACD Music_2987BACD Music Music 83.742 48000 2 1246073 9c48274110f1a12b
435 SFX EDF036D6/29AB41BA Music_29AB41BA Music Music 60.123 48000 2 1018528 6f56ef771f3434f6
436 SFX EDF036D6/29D2F637 Music_29D2F637 Music Music 203.312 48000 2 2971838 ec89b9e769b35868
437 SFX EDF036D6/2A1AD885 Music_2A1AD885 Music Music 34.675 48000 2 520729 0d0c912a47f702c1
438 SFX EDF036D6/2A24CEA6 Music_2A24CEA6 Music Music 1.409 48000 2 22216 677f3a8d58205de5
439 SFX EDF036D6/2AA3CEA0 Music_2AA3CEA0 Music Music 26.182 48000 2 418694 ed57512cccf5c2be
440 SFX EDF036D6/2AD309E6 Music_2AD309E6 Music Music 25.576 48000 2 369294 bda56bef71032f88
441 SFX EDF036D6/2B07A62F Music_2B07A62F Music Music 5.429 48000 2 82642 f7158a1afd44959f
442 SFX EDF036D6/2B65303C Music_2B65303C Music Music 119.221 48000 2 1463918 e69ddb4be1b7b84b
443 SFX EDF036D6/2BFCA0A1 Music_2BFCA0A1 Music Music 117.000 48000 2 1375538 fe76f756eab10a0d
444 SFX EDF036D6/2C8CEB90 Music_2C8CEB90 Music Music 53.066 48000 2 310850 ba12cb8c0ce2c213
445 SFX EDF036D6/2CC1F4E1 Music_2CC1F4E1 Music Music 18.177 48000 2 259134 d5a00a1674c95380
446 SFX EDF036D6/2CDA63DE Music_2CDA63DE Music Music 119.035 48000 2 1815581 bf1a62d940e5ca88
447 SFX EDF036D6/2CE4147F Music_2CE4147F Music Music 19.963 48000 2 282315 81f2136e11b85bdd
448 SFX EDF036D6/2D31EA09 Music_2D31EA09 Music Music 25.672 48000 2 380558 a69d5fa1bade8753
449 SFX EDF036D6/2D321DF5 Music_2D321DF5 Music Music 9.600 48000 2 143544 f93fca0d047cd2fe
450 SFX EDF036D6/2DB6F423 Music_2DB6F423 Music Music 136.744 48000 2 1552591 5a5ff2c4f953aeac
451 SFX EDF036D6/2DF40EC5 Music_2DF40EC5 Music Music 124.212 48000 2 2081712 72d855be03a60acf
452 SFX EDF036D6/2E49AEFC Music_2E49AEFC Music Music 43.254 48000 2 643737 a2199aa674d1a491
453 SFX EDF036D6/2E885FD0 Music_2E885FD0 Music Music 9.351 48000 2 147447 2eaf9dadf364eae3
454 SFX EDF036D6/2F0BF544 Music_2F0BF544 Music Music 10.712 48000 2 163834 14e6e64f1f87b86c
455 SFX EDF036D6/2FBB03E0 Music_2FBB03E0 Music Music 8.196 48000 2 116776 d9bc1e573ee6d1a3
456 SFX EDF036D6/301C76CA Music_301C76CA Music Music 57.931 48000 2 990419 02d9807cc745d4f2
457 SFX EDF036D6/3057FE36 Music_3057FE36 Music Music 9.790 48000 2 136096 9c32781ab97fe445
458 SFX EDF036D6/3085A581 Music_3085A581 Music Music 20.631 48000 2 357823 f5f4386a4f22b986
459 SFX EDF036D6/319FC00C Music_319FC00C Music Music 76.736 48000 2 1300493 d2288e6324088412
460 SFX EDF036D6/31AB3252 Music_31AB3252 Music Music 1.439 48000 2 24279 9e1452b253113e97
461 SFX EDF036D6/31C2F642 Music_31C2F642 Music Music 13.165 48000 2 185823 1bc15699e00200fe
462 SFX EDF036D6/322DC5AF Music_322DC5AF Music Music 4.388 48000 2 62342 7bf7872bc73c9174
463 SFX EDF036D6/32DCAEE7 Music_32DCAEE7 Music Music 9.306 48000 2 131764 9e9a2794e9fe45a1
464 SFX EDF036D6/32EB3DA7 Music_32EB3DA7 Music Music 171.524 48000 2 2600951 2066f38866c5d960
465 SFX EDF036D6/33057EFC Music_33057EFC Music Music 68.386 48000 2 972781 d7c24aa3f26ceca9
466 SFX EDF036D6/33695FB1 Music_33695FB1 Music Music 4.000 48000 2 64991 1808f37aef2d95cb
467 SFX EDF036D6/33828946 Music_33828946 Music Music 6.005 48000 2 98962 7dc7fbfc08e0f669
468 SFX EDF036D6/3418F5CF Music_3418F5CF Music Music 6.045 48000 2 90863 cc9cf509df1c7a96
469 SFX EDF036D6/3452B01B Music_3452B01B Music Music 114.167 48000 2 1925579 d0e77781234a58f8
470 SFX EDF036D6/3473C68E Music_3473C68E Music Music 21.231 48000 2 329799 1556993ad3defc8e
471 SFX EDF036D6/350357ED Music_350357ED Music Music 12.850 48000 2 152754 effedf711799f9fb
472 SFX EDF036D6/356CBA43 Music_356CBA43 Music Music 7.836 48000 2 59556 47e3f32bc4d4a5cc
473 SFX EDF036D6/3584EC39 Music_3584EC39 Music Music 51.176 48000 2 768089 0cf2d9590cbbcdd6
474 SFX EDF036D6/36FBFB5B Music_36FBFB5B Music Music 14.560 48000 6 264912 800fdb94db6dfeec
475 SFX EDF036D6/372964C4 Music_372964C4 Music Music 5.887 48000 2 86948 f6d4d75ab8445bbd
476 SFX EDF036D6/38597057 Music_38597057 Music Music 27.239 44100 2 355958 9d0730c4a1c127da
477 SFX EDF036D6/392269E5 Music_392269E5 Music Music 9.166 48000 2 136727 28667f4a68ee8e66
478 SFX EDF036D6/393BC53A Music_393BC53A Music Music 171.524 48000 6 7192714 eba537acd74009ba
479 SFX EDF036D6/39EF16A1 Music_39EF16A1 Music Music 20.571 48000 2 322708 cbc57a438236bfcd
480 SFX EDF036D6/3A0F34AA Music_3A0F34AA Music Music 20.842 48000 2 327714 4feb9c5e467509b3
481 SFX EDF036D6/3A6E0069 Music_3A6E0069 Music Music 30.607 48000 2 440966 23f0f02258eb7c3f
482 SFX EDF036D6/3A96A66A Music_3A96A66A Music Music 5.426 48000 2 78334 1a7e9e3fd65324a8
483 SFX EDF036D6/3AB86854 Music_3AB86854 Music Music 124.212 48000 2 2186737 956cd355f2baec4d
484 SFX EDF036D6/3B19926C Music_3B19926C Music Music 32.360 48000 2 571396 f8cfcdc11c88aff9
485 SFX EDF036D6/3B1C4571 Music_3B1C4571 Music Music 26.182 48000 2 456132 e1e55cfe4e98a17b
486 SFX EDF036D6/3B276E4D Music_3B276E4D Music Music 125.626 48000 2 1911354 0f17594e2df36276
487 SFX EDF036D6/3B30E8B5 Music_3B30E8B5 Music Music 4.422 48000 2 72779 b73c25399a8c66c7
488 SFX EDF036D6/3BA5398D Music_3BA5398D Music Music 6.028 48000 2 85229 f7d80b8c2a5d23ec
489 SFX EDF036D6/3C106E75 Music_3C106E75 Music Music 6.527 48000 2 95702 8f0ccbaa0deb002e
490 SFX EDF036D6/3C961A0D Music_3C961A0D Music Music 96.021 48000 2 1405544 5b6c24b48087e2b3
491 SFX EDF036D6/3CBF4761 Music_3CBF4761 Music Music 4.856 48000 2 70027 d408c7bb17835303
492 SFX EDF036D6/3CE33965 Music_3CE33965 Music Music 4.382 48000 2 64257 1fd2e7f5eecfeecb
493 SFX EDF036D6/3D54599E Music_3D54599E Music Music 13.390 48000 2 187301 27fed970b91d3ca8
494 SFX EDF036D6/3E47446A Music_3E47446A Music Music 7.806 48000 2 118924 c8fd851d3d5e5ea8
495 SFX EDF036D6/3F04E423 Music_3F04E423 Music Music 128.764 48000 2 2039407 4ed2d6eaa4c10c65
496 SFX EDF036D6/3F312E20 Music_3F312E20 Music Music 57.040 48000 2 809641 cb602f293c127d15
497 SFX EDF036D6/3FFB5B14 Music_3FFB5B14 Music Music 9.547 48000 2 142043 f859153f7497528d
498 SFX F16E832D:0 SEQ_DemonTrials_1 SEQ_DemonTrials#1 SFX Scripted sequences 17.280 48000 1 105109 316e989c34a90718
499 SFX F16E832D:1 SEQ_DemonTrials_2 SEQ_DemonTrials#2 SFX Scripted sequences 13.525 48000 1 84855 629e4ae11d2af39c
500 SFX F16E832D:2 SEQ_DemonTrials_3 SEQ_DemonTrials#3 SFX Scripted sequences 10.837 48000 1 65652 56430a3bb8bcadfd
501 SFX F16E832D:3 SEQ_DemonTrials_4 SEQ_DemonTrials#4 SFX Scripted sequences 14.635 48000 1 91112 44b6dd9a637cfdc7
502 SFX F16E832D:4 SEQ_DemonTrials_5 SEQ_DemonTrials#5 SFX Scripted sequences 12.032 48000 1 74765 554d0d717858a5a6
503 SFX F16E832D:5 SEQ_DemonTrials_6 SEQ_DemonTrials#6 SFX Scripted sequences 17.707 48000 1 107955 7462b3589a40f0e4
504 SFX F16E832D:6 SEQ_DemonTrials_7 SEQ_DemonTrials#7 SFX Scripted sequences 7.723 48000 1 47185 fbc1fea07a56b18e
505 SFX F16E832D:7 SEQ_DemonTrials_8 SEQ_DemonTrials#8 SFX Scripted sequences 15.232 48000 1 91634 7f84cdc41d59c8ed
506 SFX F16E832D:8 SEQ_DemonTrials_9 SEQ_DemonTrials#9 SFX Scripted sequences 14.635 48000 1 89392 d3771bede86e3233
507 SFX F1B81FC7 PVOL_Water_Exterior PVOL_Water_Exterior_1 Ambience Room tone 25.722 32000 2 261962 55e9f9cd4bd498f2
508 SFX F9A51D18:0 AMB_OW_C46_1 AMB_OW_C46#1 Ambience Ambience 84.593 32000 4 2210553 3b7926711dcd2996
509 SFX F9A51D18:1 AMB_OW_C46_2 AMB_OW_C46#2 Ambience Ambience 59.741 32000 4 1546334 c9cb4e7868f6a813
510 SFX F9A51D18:2 AMB_OW_C46_3 AMB_OW_C46#3 Ambience Ambience 97.923 32000 4 2728789 e6be03d0fe2c6639
511 SFX FCA4F778/0EC73393 FCA4F778_0EC73393 Unknown Unknown 56.689 48000 2 843627 8bce3f68a65ae479
512 SFX FD4461EA:0 AMB_RiddlerChallenge1_1 AMB_RiddlerChallenge1#1 Ambience Ambience 35.157 32000 4 828243 506937146f3bb8bd
513 SFX FD4461EA:1 AMB_RiddlerChallenge1_2 AMB_RiddlerChallenge1#2 Ambience Ambience 55.551 32000 4 1462792 1239f45c45e2f82a
514 SFX FD4461EA:2 AMB_RiddlerChallenge1_3 AMB_RiddlerChallenge1#3 Ambience Ambience 16.384 32000 4 377945 d42730fbd1ecdcc3
515 SFX FDB108B0 SEQ_Steel_C5_WaterDoors SEQ_Steel_C5_WaterDoors_1 SFX Scripted sequences 17.285 48000 2 228831 bea79a6d7fe41df0
516 SFX FFF56412 AMB_UnderworldC2_V2 AMB_UnderworldC2_V2_1 Ambience Ambience 28.160 32000 4 580489 ee66e8d0f24876e6
517 SFX 01EB5DDC:0 AMB_OW_B_37_1 AMB_OW_B_37#1 Ambience Ambience 126.880 32000 4 3753364 079885b8f4146578
518 SFX 01EB5DDC:1 AMB_OW_B_37_2 AMB_OW_B_37#2 Ambience Ambience 95.915 32000 4 2825273 f2feb7d5c975cb29
519 SFX 01EB5DDC:2 AMB_OW_B_37_3 AMB_OW_B_37#3 Ambience Ambience 71.680 32000 4 2093574 4cb79bfe20e6e46e
520 SFX 01EB5DDC:3 AMB_OW_B_37_4 AMB_OW_B_37#4 Ambience Ambience 73.720 32000 4 2176579 c671caa71010cc41
521 SFX 01EB5DDC:4 AMB_OW_B_37_5 AMB_OW_B_37#5 Ambience Ambience 112.224 32000 4 3257825 5e6d73dc0efee7ac
522 SFX 01EB5DDC:5 AMB_OW_B_37_6 AMB_OW_B_37#6 Ambience Ambience 77.999 32000 4 2269598 72ca05c3ec831537
523 SFX 01EB5DDC:6 AMB_OW_B_37_7 AMB_OW_B_37#7 Ambience Ambience 103.764 32000 4 3180437 e25931054a39debb
524 SFX 062748BA:0 AMB_GCPD_HQ_1 AMB_GCPD_HQ#1 Ambience Ambience Cop 40.852 32000 4 1204406 a0a7812ae604c8e2
525 SFX 062748BA:1 AMB_GCPD_HQ_2 AMB_GCPD_HQ#2 Ambience Ambience Cop 9.714 32000 4 218213 375fb85c4d413d51
526 SFX 0B18F44C:0 CHR_RasBossFight_1 CHR_RasBossFight#1 SFX Character foley 17.280 48000 1 105109 316e989c34a90718
527 SFX 0B18F44C:1 CHR_RasBossFight_2 CHR_RasBossFight#2 SFX Character foley 13.525 48000 1 84855 629e4ae11d2af39c
528 SFX 0B18F44C:2 CHR_RasBossFight_3 CHR_RasBossFight#3 SFX Character foley 14.635 48000 1 91112 44b6dd9a637cfdc7
529 SFX 0B18F44C:3 CHR_RasBossFight_4 CHR_RasBossFight#4 SFX Character foley 12.032 48000 1 74765 554d0d717858a5a6
530 SFX 0B18F44C:4 CHR_RasBossFight_5 CHR_RasBossFight#5 SFX Character foley 7.486 48000 4 223830 e8915027e3ce55d0
531 SFX 0E398B80:0 SEQ_OW_Pay_Your_Respects_1 SEQ_OW_Pay_Your_Respects#1 SFX Scripted sequences 17.158 24000 2 105596 ca9d7921fc5d1235
532 SFX 0E398B80:1 SEQ_OW_Pay_Your_Respects_2 SEQ_OW_Pay_Your_Respects#2 SFX Scripted sequences 17.158 24000 2 113215 0efb16b6e65dfb1f
533 SFX 10CF081C:0 AMB_OW_A_ALL_1 AMB_OW_A_ALL#1 Ambience Ambience 100.000 32000 4 2881899 6eda738d26302232
534 SFX 10CF081C:1 AMB_OW_A_ALL_2 AMB_OW_A_ALL#2 Ambience Ambience 93.757 32000 4 2767748 ec137126ce5315c3
535 SFX 10CF081C:2 AMB_OW_A_ALL_3 AMB_OW_A_ALL#3 Ambience Ambience 49.740 32000 4 1473273 0af778e258914efc
536 SFX 10CF081C:3 AMB_OW_A_ALL_4 AMB_OW_A_ALL#4 Ambience Ambience 90.494 32000 4 2647321 df324637be55b522
537 SFX 10CF081C:4 AMB_OW_A_ALL_5 AMB_OW_A_ALL#5 Ambience Ambience 92.101 32000 4 2655287 87c065e2406e711a
538 SFX 10CF081C:5 AMB_OW_A_ALL_6 AMB_OW_A_ALL#6 Ambience Ambience 89.000 32000 4 2587371 4b2939c4c48d3a37
539 SFX 10CF081C:6 AMB_OW_A_ALL_7 AMB_OW_A_ALL#7 Ambience Ambience 87.963 32000 4 2452402 c48eac4d33968c11
540 SFX 10CF081C:7 AMB_OW_A_ALL_8 AMB_OW_A_ALL#8 Ambience Ambience 48.736 32000 4 1510563 371103729d371d2f
541 SFX 10CF081C:8 AMB_OW_A_ALL_9 AMB_OW_A_ALL#9 Ambience Ambience 112.224 32000 4 3257825 5e6d73dc0efee7ac
542 SFX 10CF081C:9 AMB_OW_A_ALL_10 AMB_OW_A_ALL#10 Ambience Ambience 76.219 32000 4 2208864 b223019277b088c7
543 SFX 10CF081C:10 AMB_OW_A_ALL_11 AMB_OW_A_ALL#11 Ambience Ambience 89.348 32000 4 2648238 780fcdfc367ef989
544 SFX 10CF081C:11 AMB_OW_A_ALL_12 AMB_OW_A_ALL#12 Ambience Ambience 84.039 32000 4 2445442 d6ec35703e9f8a90
545 SFX 17B983F5:0 AMB_OW_E123478_1 AMB_OW_E123478#1 Ambience Ambience 100.000 32000 4 2881899 6eda738d26302232
546 SFX 17B983F5:1 AMB_OW_E123478_2 AMB_OW_E123478#2 Ambience Ambience 93.757 32000 4 2767748 ec137126ce5315c3
547 SFX 17B983F5:2 AMB_OW_E123478_3 AMB_OW_E123478#3 Ambience Ambience 60.828 32000 4 1813753 1a1753a70c841dec
548 SFX 17B983F5:3 AMB_OW_E123478_4 AMB_OW_E123478#4 Ambience Ambience 49.740 32000 4 1473273 0af778e258914efc
549 SFX 17B983F5:4 AMB_OW_E123478_5 AMB_OW_E123478#5 Ambience Ambience 3.204 32000 2 38134 8cf522d67fc03fe7
550 SFX 17B983F5:5 AMB_OW_E123478_6 AMB_OW_E123478#6 Ambience Ambience 59.559 32000 4 1734273 ace4cf02968e016a
551 SFX 17B983F5:6 AMB_OW_E123478_7 AMB_OW_E123478#7 Ambience Ambience 90.494 32000 4 2647321 df324637be55b522
552 SFX 17B983F5:7 AMB_OW_E123478_8 AMB_OW_E123478#8 Ambience Ambience 92.101 32000 4 2655287 87c065e2406e711a
553 SFX 17B983F5:8 AMB_OW_E123478_9 AMB_OW_E123478#9 Ambience Ambience 87.963 32000 4 2452402 c48eac4d33968c11
554 SFX 17B983F5:9 AMB_OW_E123478_10 AMB_OW_E123478#10 Ambience Ambience 48.736 32000 4 1510563 371103729d371d2f
555 SFX 17B983F5:10 AMB_OW_E123478_11 AMB_OW_E123478#11 Ambience Ambience 4.205 32000 2 49274 acec2dcd750d3f1a
556 SFX 17B983F5:11 AMB_OW_E123478_12 AMB_OW_E123478#12 Ambience Ambience 112.224 32000 4 3257825 5e6d73dc0efee7ac
557 SFX 17B983F5:12 AMB_OW_E123478_13 AMB_OW_E123478#13 Ambience Ambience 76.219 32000 4 2208864 b223019277b088c7
558 SFX 17B983F5:13 AMB_OW_E123478_14 AMB_OW_E123478#14 Ambience Ambience 89.348 32000 4 2648238 780fcdfc367ef989
559 SFX 17B983F5:14 AMB_OW_E123478_15 AMB_OW_E123478#15 Ambience Ambience 84.039 32000 4 2445442 d6ec35703e9f8a90
560 SFX 1C305431 AMB_MadHatter_PostFight AMB_MadHatter_PostFight_1 Ambience Ambience Mad Hatter 6.003 32000 4 168722 1842bba5f1fb4194
561 SFX 1F3ABBB4:0 AMB_Scarecrow_Boat_1 AMB_Scarecrow_Boat#1 Ambience Ambience 30.358 32000 2 347585 34b4d9a05154fa63
562 SFX 1F3ABBB4:1 AMB_Scarecrow_Boat_2 AMB_Scarecrow_Boat#2 Ambience Ambience 29.326 32000 2 316292 52819d7ad292a6a6
563 SFX 23F6A8F8:0 AMB_CourtB1_1 AMB_CourtB1#1 Ambience Ambience 14.368 32000 4 392497 923b77449efcd99e
564 SFX 23F6A8F8:1 AMB_CourtB1_2 AMB_CourtB1#2 Ambience Ambience 19.371 32000 4 527286 51acfa6500a7486d
565 SFX 23F6A8F8:2 AMB_CourtB1_3 AMB_CourtB1#3 Ambience Ambience 27.000 32000 4 755899 af2ec78c4279be78
566 SFX 247E31CA CHR_Grundy CHR_Grundy_1 SFX Character foley Solomon Grundy 3.284 48000 2 34305 2271da11e324c823
567 SFX 2DE53B3F:0 AMB_OW_Tower_Exterior_1 AMB_OW_Tower_Exterior#1 Ambience Ambience 57.597 32000 4 1730923 a0b3071f0df4d576
568 SFX 2DE53B3F:1 AMB_OW_Tower_Exterior_2 AMB_OW_Tower_Exterior#2 Ambience Ambience 42.956 32000 4 1283371 fcc511bca155e988
569 SFX 2DE53B3F:2 AMB_OW_Tower_Exterior_3 AMB_OW_Tower_Exterior#3 Ambience Ambience 59.559 32000 4 1734273 ace4cf02968e016a
570 SFX 2DE53B3F:3 AMB_OW_Tower_Exterior_4 AMB_OW_Tower_Exterior#4 Ambience Ambience 8.319 24000 1 21247 fe0c508a6d2a341c
571 SFX 2DE53B3F:4 AMB_OW_Tower_Exterior_5 AMB_OW_Tower_Exterior#5 Ambience Ambience 98.450 32000 4 2836823 1f2b7810a3c875a0
572 SFX 2DE53B3F:5 AMB_OW_Tower_Exterior_6 AMB_OW_Tower_Exterior#6 Ambience Ambience 6.254 24000 1 20450 03df69ec6485a152
573 SFX 2DE53B3F:6 AMB_OW_Tower_Exterior_7 AMB_OW_Tower_Exterior#7 Ambience Ambience 5.436 24000 1 13147 419e24dbccf1e7e5
574 SFX 2DE53B3F:7 AMB_OW_Tower_Exterior_8 AMB_OW_Tower_Exterior#8 Ambience Ambience 6.594 24000 1 18166 15bc9f238253efa2
575 SFX 2DE53B3F:8 AMB_OW_Tower_Exterior_9 AMB_OW_Tower_Exterior#9 Ambience Ambience 7.198 24000 1 17394 ad8713b562f1714b
576 SFX 2DE53B3F:9 AMB_OW_Tower_Exterior_10 AMB_OW_Tower_Exterior#10 Ambience Ambience 7.060 24000 1 22630 d8465ba3a42b9387
577 SFX 2DE53B3F:10 AMB_OW_Tower_Exterior_11 AMB_OW_Tower_Exterior#11 Ambience Ambience 8.421 24000 1 24860 ee66e3e09bf1dc79
578 SFX 396E6EBD:0 AMB_Church_B1_1 AMB_Church_B1#1 Ambience Ambience 18.324 32000 4 496213 5eb76e08e5346ab4
579 SFX 396E6EBD:1 AMB_Church_B1_2 AMB_Church_B1#2 Ambience Ambience 11.720 32000 4 299346 7576251df0911437
580 SFX 3CB673E7 AMB_HushRoom AMB_HushRoom_1 Ambience Ambience 24.000 32000 4 671357 1279af0238e7afdc
581 SFX 3F4B608F AMB_Underworld_S1 AMB_Underworld_S1_1 Ambience Ambience 15.536 32000 4 455276 9fb735c477db3804
582 SFX 45A19974:0 AMB_RumbleDebris_1 AMB_RumbleDebris#1 Ambience Ambience 15.495 32000 4 404444 646660bf0b7207b1
583 SFX 45A19974:1 AMB_RumbleDebris_2 AMB_RumbleDebris#2 Ambience Ambience 11.602 32000 4 321943 54474dbc608a1506
584 SFX 45A19974:2 AMB_RumbleDebris_3 AMB_RumbleDebris#3 Ambience Ambience 9.056 32000 4 239594 7fc588270d062fc5
585 SFX 45A19974:3 AMB_RumbleDebris_4 AMB_RumbleDebris#4 Ambience Ambience 8.942 32000 4 255627 8ebabec798972470
586 SFX 4D4B76FB:0 AMB_Underworld_A3_1 AMB_Underworld_A3#1 Ambience Ambience 15.691 28000 1 98560 19c86c48a0681287
587 SFX 4D4B76FB:1 AMB_Underworld_A3_2 AMB_Underworld_A3#2 Ambience Ambience 15.263 28000 1 98065 b7f3b6ef76594a50
588 SFX 4D4B76FB:2 AMB_Underworld_A3_3 AMB_Underworld_A3#3 Ambience Ambience 10.682 28000 1 69865 d2ef62f37b65f62f
589 SFX 4D4B76FB:3 AMB_Underworld_A3_4 AMB_Underworld_A3#4 Ambience Ambience 10.139 28000 1 61110 b3bbb946619eb519
590 SFX 4D4B76FB:4 AMB_Underworld_A3_5 AMB_Underworld_A3#5 Ambience Ambience 9.415 28000 1 58433 7d6e7e3978db3156
591 SFX 4D4B76FB:5 AMB_Underworld_A3_6 AMB_Underworld_A3#6 Ambience Ambience 15.716 28000 1 91342 4f47198c1322c19a
592 SFX 4D4B76FB:6 AMB_Underworld_A3_7 AMB_Underworld_A3#7 Ambience Ambience 8.383 28000 1 54706 7cf7332a829e5065
593 SFX 4D4B76FB:7 AMB_Underworld_A3_8 AMB_Underworld_A3#8 Ambience Ambience 13.995 32000 4 420389 44e2d6cb6bc05ecd
594 SFX 4DB8A342 OBJ_Tannoy OBJ_Tannoy_1 SFX Objects 2.229 32000 5 26334 d5aaba538d661493
595 SFX 4DF92D97:0 OBJ_GunCrates_1 OBJ_GunCrates#1 SFX Objects 1.249 48000 2 17932 5fcb6961ce873316
596 SFX 4DF92D97:1 OBJ_GunCrates_2 OBJ_GunCrates#2 SFX Objects 1.388 48000 2 19892 eb5c4c8018dcb3b2
597 SFX 4DF92D97:2 OBJ_GunCrates_3 OBJ_GunCrates#3 SFX Objects 1.388 48000 2 19708 64e7be26e0731db0
598 SFX 4DF92D97:3 OBJ_GunCrates_4 OBJ_GunCrates#4 SFX Objects 1.388 48000 2 23070 0314231fb31ebd12
599 SFX 4EE4DEAC VOL_IceFreeze_Interior VOL_IceFreeze_Interior_1 Ambience Room tone 24.000 48000 2 519820 93388507ef36519c
600 SFX 4F4B79DF:0 AMB_Underworld_C1_1 AMB_Underworld_C1#1 Ambience Ambience 10.062 32000 4 340307 1108533c7f5600f7
601 SFX 4F4B79DF:1 AMB_Underworld_C1_2 AMB_Underworld_C1#2 Ambience Ambience 16.746 48000 4 497401 835dc96ff6b7a1f9
602 SFX 504B7BB4:0 AMB_Underworld_B5_1 AMB_Underworld_B5#1 Ambience Ambience 4.977 24000 1 14907 2abb33cf9e8b4b4f
603 SFX 504B7BB4:1 AMB_Underworld_B5_2 AMB_Underworld_B5#2 Ambience Ambience 6.875 24000 1 19276 ecd69d2dce5e37a7
604 SFX 504B7BB4:2 AMB_Underworld_B5_3 AMB_Underworld_B5#3 Ambience Ambience 6.591 24000 1 18148 7e09b2d25c2eea3a
605 SFX 504B7BB4:3 AMB_Underworld_B5_4 AMB_Underworld_B5#4 Ambience Ambience 4.287 24000 1 13144 bf602fa72cf41dfe
606 SFX 504B7BB4:4 AMB_Underworld_B5_5 AMB_Underworld_B5#5 Ambience Ambience 7.211 24000 1 21936 c7a506f8097c8015
607 SFX 504B7BB4:5 AMB_Underworld_B5_6 AMB_Underworld_B5#6 Ambience Ambience 3.002 24000 1 9132 09901bea0b7aeb67
608 SFX 504B7BB4:6 AMB_Underworld_B5_7 AMB_Underworld_B5#7 Ambience Ambience 4.678 24000 1 14423 de40e62612dad2fd
609 SFX 504B7BB4:7 AMB_Underworld_B5_8 AMB_Underworld_B5#8 Ambience Ambience 15.691 28000 1 98560 19c86c48a0681287
610 SFX 504B7BB4:8 AMB_Underworld_B5_9 AMB_Underworld_B5#9 Ambience Ambience 15.263 28000 1 98065 b7f3b6ef76594a50
611 SFX 504B7BB4:9 AMB_Underworld_B5_10 AMB_Underworld_B5#10 Ambience Ambience 10.682 28000 1 69865 d2ef62f37b65f62f
612 SFX 504B7BB4:10 AMB_Underworld_B5_11 AMB_Underworld_B5#11 Ambience Ambience 5.649 24000 1 15919 7161ab2c29c0ad02
613 SFX 504B7BB4:11 AMB_Underworld_B5_12 AMB_Underworld_B5#12 Ambience Ambience 10.139 28000 1 61110 b3bbb946619eb519
614 SFX 504B7BB4:12 AMB_Underworld_B5_13 AMB_Underworld_B5#13 Ambience Ambience 9.415 28000 1 58433 7d6e7e3978db3156
615 SFX 504B7BB4:13 AMB_Underworld_B5_14 AMB_Underworld_B5#14 Ambience Ambience 7.804 24000 1 22303 e97522fb2216e189
616 SFX 504B7BB4:14 AMB_Underworld_B5_15 AMB_Underworld_B5#15 Ambience Ambience 2.753 24000 1 8077 43a3682b07706ee1
617 SFX 504B7BB4:15 AMB_Underworld_B5_16 AMB_Underworld_B5#16 Ambience Ambience 15.536 32000 4 455276 9fb735c477db3804
618 SFX 504B7BB4:16 AMB_Underworld_B5_17 AMB_Underworld_B5#17 Ambience Ambience 15.716 28000 1 91342 4f47198c1322c19a
619 SFX 504B7BB4:17 AMB_Underworld_B5_18 AMB_Underworld_B5#18 Ambience Ambience 8.383 28000 1 54706 7cf7332a829e5065
620 SFX 504B7BB4:18 AMB_Underworld_B5_19 AMB_Underworld_B5#19 Ambience Ambience 14.012 32000 4 442369 714ad5534983e096
621 SFX 504B7BB7 AMB_Underworld_B6 AMB_Underworld_B6_1 Ambience Ambience 30.336 48000 4 902577 aaa3b73a68f71eb7
622 SFX 508590A0 AMB_HiddenAreas AMB_HiddenAreas_1 Ambience Ambience 18.666 32000 4 511603 10aefd8ea1557596
623 SFX 53C0E792 AMB_OW_D5 AMB_OW_D5_1 Ambience Ambience 14.012 32000 4 419982 3bdaee4ebf08faad
624 SFX 53C0E793 AMB_OW_D4 AMB_OW_D4_1 Ambience Ambience 14.012 32000 4 419982 3bdaee4ebf08faad
625 SFX 53C0E794 AMB_OW_D3 AMB_OW_D3_1 Ambience Ambience 60.043 32000 4 1717445 16f8f80c393a2544
626 SFX 53C0E795 AMB_OW_D2 AMB_OW_D2_1 Ambience Ambience 69.740 32000 4 2062109 17e7d4833c7dfae4
627 SFX 53C0E796:0 AMB_OW_D1_1 AMB_OW_D1#1 Ambience Ambience 60.043 32000 4 1717445 16f8f80c393a2544
628 SFX 53C0E796:1 AMB_OW_D1_2 AMB_OW_D1#2 Ambience Ambience 69.740 32000 4 2062109 17e7d4833c7dfae4
629 SFX 5445778E AMB_Museum_Corridors AMB_Museum_Corridors_1 Ambience Ambience 28.571 32000 4 750171 f5549719c82a2af2
630 SFX 57572DAC/267F1DB3 Global_267F1DB3 SFX Global 10.092 24000 4 216184 c1f1c7385d15b611
631 SFX 65845381:0 AMB_MadHatter_Party_1 AMB_MadHatter_Party#1 Ambience Ambience Mad Hatter 20.595 32000 4 582875 573c8558ebc5de5d
632 SFX 65845381:1 AMB_MadHatter_Party_2 AMB_MadHatter_Party#2 Ambience Ambience Mad Hatter 35.144 32000 4 1047109 1bea95a6a07f9150
633 SFX 6866CE3A WPN_Grundy WPN_Grundy_1 SFX Weapons Solomon Grundy 5.325 48000 1 44570 6de71f59fab751c1
634 SFX 6C151539:0 6C151539_1 6C151539#1 Unknown Unknown 1.000 48000 1 10356 e9e3007ebc41458b
635 SFX 6C151539:1 6C151539_2 6C151539#2 Unknown Unknown 1.000 48000 1 14149 f36ba9b3afa4f6b3
636 SFX 6C151539:2 6C151539_3 6C151539#3 Unknown Unknown 1.000 48000 1 13537 452d9e2784cb5c41
637 SFX 7506D11C AMB_ZsaszB1 AMB_ZsaszB1_1 Ambience Ambience 20.000 32000 4 545599 fbb73780df30e80c
638 SFX 7D6D8269 AMB_Museum_S1 AMB_Museum_S1_1 Ambience Ambience 20.468 32000 4 574954 4ce1cea0c3c854f4
639 SFX 7D6D826A AMB_Museum_S2 AMB_Museum_S2_1 Ambience Ambience 20.468 32000 4 574954 4ce1cea0c3c854f4
640 SFX 7D71D951 AMB_HushSpooky AMB_HushSpooky_1 Ambience Ambience 24.000 32000 4 680215 823670f2a118d1af
641 SFX 7D848FF0:0 AMB_OW_B_12456_1 AMB_OW_B_12456#1 Ambience Ambience 126.880 32000 4 3753364 079885b8f4146578
642 SFX 7D848FF0:1 AMB_OW_B_12456_2 AMB_OW_B_12456#2 Ambience Ambience 57.597 32000 4 1730923 a0b3071f0df4d576
643 SFX 7D848FF0:2 AMB_OW_B_12456_3 AMB_OW_B_12456#3 Ambience Ambience 42.956 32000 4 1283371 fcc511bca155e988
644 SFX 7D848FF0:3 AMB_OW_B_12456_4 AMB_OW_B_12456#4 Ambience Ambience 60.828 32000 4 1813753 1a1753a70c841dec
645 SFX 7D848FF0:4 AMB_OW_B_12456_5 AMB_OW_B_12456#5 Ambience Ambience 84.593 32000 4 2210553 3b7926711dcd2996
646 SFX 7D848FF0:5 AMB_OW_B_12456_6 AMB_OW_B_12456#6 Ambience Ambience 68.204 32000 4 2019040 e3b9a81b5d764030
647 SFX 7D848FF0:6 AMB_OW_B_12456_7 AMB_OW_B_12456#7 Ambience Ambience 59.559 32000 4 1734273 ace4cf02968e016a
648 SFX 7D848FF0:7 AMB_OW_B_12456_8 AMB_OW_B_12456#8 Ambience Ambience 98.450 32000 4 2836823 1f2b7810a3c875a0
649 SFX 7D848FF0:8 AMB_OW_B_12456_9 AMB_OW_B_12456#9 Ambience Ambience 59.741 32000 4 1546334 c9cb4e7868f6a813
650 SFX 7D848FF0:9 AMB_OW_B_12456_10 AMB_OW_B_12456#10 Ambience Ambience 97.923 32000 4 2728789 e6be03d0fe2c6639
651 SFX 7D848FF0:10 AMB_OW_B_12456_11 AMB_OW_B_12456#11 Ambience Ambience 80.661 32000 4 2399422 f349c20ba690c651
652 SFX 7D848FF0:11 AMB_OW_B_12456_12 AMB_OW_B_12456#12 Ambience Ambience 77.999 32000 4 2269598 72ca05c3ec831537

3365
data/catalog/characters.csv Normal file

File diff suppressed because it is too large Load Diff

65
data/catalog/index.json Normal file
View File

@@ -0,0 +1,65 @@
{
"DEU": {
"wad": "BmGame/CookedPCConsole/German/FFB9E71B_LOC_DEU.wad",
"fingerprint": "2464-93ff67901d8eb43b",
"sounds": 10816,
"groups": {
"Speech": 10815,
"Ambience": 1
},
"with_character": 3366
},
"ESN": {
"wad": "BmGame/CookedPCConsole/Spanish(Spain)/0E07A43D_LOC_ESN.wad",
"fingerprint": "2464-90591332cf02ddad",
"sounds": 10816,
"groups": {
"Speech": 10815,
"Ambience": 1
},
"with_character": 3366
},
"FRA": {
"wad": "BmGame/CookedPCConsole/French(France)/134795B3_LOC_FRA.wad",
"fingerprint": "2464-b4b42ed9b0c025cf",
"sounds": 10816,
"groups": {
"Speech": 10815,
"Ambience": 1
},
"with_character": 3366
},
"INT": {
"wad": "BmGame/CookedPCConsole/English(US)/28CCF006_LOC_INT.wad",
"fingerprint": "2464-09e6656cc3fe5209",
"sounds": 10816,
"groups": {
"Speech": 10815,
"Ambience": 1
},
"with_character": 3366
},
"ITA": {
"wad": "BmGame/CookedPCConsole/Italian/49D84887_LOC_ITA.wad",
"fingerprint": "2464-f667c0ac06942f00",
"sounds": 10816,
"groups": {
"Speech": 10815,
"Ambience": 1
},
"with_character": 3366
},
"SFX": {
"wad": "BmGame/CookedPCConsole/SFX/17705D3E.wad",
"fingerprint": "83-a700d4ee8d10ae1f",
"sounds": 651,
"groups": {
"Ambience": 225,
"Unknown": 6,
"SFX": 31,
"Music": 389
},
"with_character": 0
},
"_info": "unmodified game files"
}

View File

@@ -0,0 +1,46 @@
Speech_AUGMENTED_Challenge_Alf_1.wem
Speech_AUGMENTED_Challenge_Alf_2.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb1_1.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb1_3.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb2_1.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb2_2.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb3_2.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD2_1.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_3.wem
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_3.wem
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_4.wem
Speech_GCPD_CH3_BM_ALF_Codes_2.wem
Speech_GCPD_CH3_BM_ALF_Codes_5.wem
Speech_MAP_BM_ALF_CURE_INTRO_1.wem
Speech_MAP_BM_ALF_CURE_INTRO_2.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_2.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_3.wem
Speech_New_game_Alfred_1.wem
Speech_New_help_Enter_court_1.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_2.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_5.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_6.wem
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_1.wem
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_2.wem
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_5.wem
Speech_STEEL_B1_CH2_Goto_Doc_ALF_1.wem
Speech_STEEL_C3_CH3_post_seq_BM_ALF_1.wem
Speech_STEEL_C3_CH3_post_seq_BM_ALF_5.wem
Speech_STREETS_A1_CH0_BM_ALF_1.wem
Speech_STREETS_A1_CH0_BM_ALF_2.wem
Speech_STREETS_A1_CH2_BM_ALF_Church_2.wem
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_2.wem
Speech_STREETS_A3_CH2_BM_ALF_findroute_1.wem
Speech_STREETS_A3_CH2_BM_ALF_findroute_3.wem
Speech_STREETS_A3_CH3_BM_Wake_Intro_1.wem
Speech_STREETS_A3_CH3_BM_Wake_Intro_5.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_2.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_5.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_8.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_10.wem
Speech_STREETS_A3_CollectGrapnelBoost_2.wem
Speech_STREETS_A3_CollectGrapnelBoost_3.wem
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_1.wem
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_2.wem
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_2.wem
Speech_STREETS_A5_CH4_BM_Line_Launch_req_2.wem

View File

@@ -0,0 +1,10 @@
Speech_SIDE_AZRAEL_Final_1.wem
Speech_SIDE_AZRAEL_Final_3.wem
Speech_SIDE_AZRAEL_Final_4.wem
Speech_SIDE_AZRAEL_Final_6.wem
Speech_SIDE_AZRAEL_Final_7.wem
Speech_SIDE_AZRAEL_Final_8.wem
Speech_SIDE_AZRAEL_Meeting_1_1.wem
Speech_SIDE_AZRAEL_Meeting_2_1.wem
Speech_SIDE_AZRAEL_Meeting_3_1.wem
Speech_SIDE_AZRAEL_Meeting_4_1.wem

View File

@@ -0,0 +1,45 @@
Speech_COMBAT_BARKS_BANE_Hit_batman_1.wem
Speech_COMBAT_BARKS_BANE_Hit_batman_2.wem
Speech_COMBAT_BARKS_BANE_Hit_batman_3.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_1.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_2.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_3.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_4.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_5.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_6.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_7.wem
Speech_COMBAT_BARKS_BANE_Hit_tyger_8.wem
Speech_SIDE_BANE_Bane_look_at_map_1.wem
Speech_SIDE_BANE_Bane_look_at_map_2.wem
Speech_SIDE_BANE_Bane_look_at_map_3.wem
Speech_SIDE_BANE_Bane_look_at_map_4.wem
Speech_SIDE_BANE_CAUGHT_CONV_1.wem
Speech_SIDE_BANE_CAUGHT_CONV_4.wem
Speech_SIDE_BANE_END_1.wem
Speech_SIDE_BANE_END_2.wem
Speech_SIDE_BANE_END_3.wem
Speech_SIDE_BANE_END_5.wem
Speech_SIDE_BANE_END_7.wem
Speech_SIDE_BANE_END_9.wem
Speech_SIDE_BANE_IDLES_1.wem
Speech_SIDE_BANE_IDLES_2.wem
Speech_SIDE_BANE_IDLES_3.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_1.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_3.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_4.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_7.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_9.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_4.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_5.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_6.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_9.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_10.wem
Speech_SIDE_BANE_Pre_crate_captured_idles_1.wem
Speech_SIDE_BANE_Pre_crate_captured_idles_2.wem
Speech_SIDE_BANE_Pre_tyger_conv_1.wem
Speech_SIDE_BANE_Pre_tyger_conv_3.wem
Speech_SIDE_BANE_STOP_1.wem
Speech_SIDE_BANE_STOP_3.wem
Speech_SIDE_BANE_STOP_4.wem
Speech_SIDE_CAT_MEET_BANE_2.wem
Speech_SIDE_CAT_MEET_BANE_3.wem

View File

@@ -0,0 +1,789 @@
Speech_CH_ZERO_CH0_Airlock1_2.wem
Speech_CH_ZERO_CH0_Airlock2_2.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb1_2.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb1_4.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb2_3.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb2_4.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb2_5.wem
Speech_CH_ZERO_CH0_BW_ALF_Climb3_1.wem
Speech_CH_ZERO_CH0_BW_Breathing_1.wem
Speech_CH_ZERO_CH0_BW_Breathing_2.wem
Speech_CH_ZERO_CH0_BW_Breathing_3.wem
Speech_CH_ZERO_CH0_BW_JR_save_1.wem
Speech_CH_ZERO_CH0_BW_JR_save_2.wem
Speech_CH_ZERO_CH0_CLIMB_THOUGHTS_1.wem
Speech_CH_ZERO_CH0_CLIMB_THOUGHTS_2.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD1_1.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_1.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_2.wem
Speech_CH_ZERO_CH0_fight_intro_3.wem
Speech_CH_ZERO_CH0_FindCatwoman_1.wem
Speech_CHAR_BATMAN_AfterCroc_1.wem
Speech_CHAR_BATMAN_AfterCroc_2.wem
Speech_CHAR_BATMAN_Batarang_1.wem
Speech_CHAR_BATMAN_Cant_leave_mayor_1.wem
Speech_CHAR_BATMAN_Cant_leave_ryder_1.wem
Speech_CHAR_BATMAN_Cant_leave_ryder_2.wem
Speech_CHAR_BATMAN_Cant_leave_taser_1.wem
Speech_CHAR_BATMAN_Cant_leave_Vale_1.wem
Speech_CHAR_BATMAN_Cops_1.wem
Speech_CHAR_BATMAN_Cops_2.wem
Speech_CHAR_BATMAN_Cops_3.wem
Speech_CHAR_BATMAN_Cops_ice_1.wem
Speech_CHAR_BATMAN_Cops_ice_2.wem
Speech_CHAR_BATMAN_Crane_1.wem
Speech_CHAR_BATMAN_Crane_2.wem
Speech_CHAR_BATMAN_Crane_3.wem
Speech_CHAR_BATMAN_Crane_4.wem
Speech_CHAR_BATMAN_Crane_5.wem
Speech_CHAR_BATMAN_Deadshot_Can_wait_1.wem
Speech_CHAR_BATMAN_Destroy_last_crate_1.wem
Speech_CHAR_BATMAN_Dont_leave_burlow_1.wem
Speech_CHAR_BATMAN_Dont_leave_burlow_2.wem
Speech_CHAR_BATMAN_Dont_leave_GCPD_1.wem
Speech_CHAR_BATMAN_Dont_leave_GCPD_2.wem
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_1.wem
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_2.wem
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_3.wem
Speech_CHAR_BATMAN_End_blood_trail_1.wem
Speech_CHAR_BATMAN_Explosive_hostage_1.wem
Speech_CHAR_BATMAN_Explosive_hostage_2.wem
Speech_CHAR_BATMAN_Fail_mag_1.wem
Speech_CHAR_BATMAN_Fail_mag_2.wem
Speech_CHAR_BATMAN_Fail_Ninja_1.wem
Speech_CHAR_BATMAN_Fail_Ninja_2.wem
Speech_CHAR_BATMAN_Find_Jammers_first_1.wem
Speech_CHAR_BATMAN_Find_Jammers_first_2.wem
Speech_CHAR_BATMAN_Find_new_route_through_chimney_1.wem
Speech_CHAR_BATMAN_Find_new_route_through_chimney_2.wem
Speech_CHAR_BATMAN_Find_secret_door_1.wem
Speech_CHAR_BATMAN_First_jamming_tower_1.wem
Speech_CHAR_BATMAN_Freeze_jammer_fail_1.wem
Speech_CHAR_BATMAN_Freeze_jammer_fail_2.wem
Speech_CHAR_BATMAN_Freeze_jammer_not_used_1.wem
Speech_CHAR_BATMAN_Frontend_taunt_1.wem
Speech_CHAR_BATMAN_Frontend_taunt_2.wem
Speech_CHAR_BATMAN_Frontend_taunt_3.wem
Speech_CHAR_BATMAN_Frontend_taunt_4.wem
Speech_CHAR_BATMAN_Frontend_taunt_5.wem
Speech_CHAR_BATMAN_Frozen_air_1.wem
Speech_CHAR_BATMAN_Frozen_air_2.wem
Speech_CHAR_BATMAN_Gadget_Hostage_1.wem
Speech_CHAR_BATMAN_GCPD_Safe_1.wem
Speech_CHAR_BATMAN_Get_penguin_radio_1.wem
Speech_CHAR_BATMAN_Get_penguin_radio_2.wem
Speech_CHAR_BATMAN_Go_to_Subway_1.wem
Speech_CHAR_BATMAN_Grapnel_Boost_1.wem
Speech_CHAR_BATMAN_Hint_switch_1.wem
Speech_CHAR_BATMAN_Hint_switch_2.wem
Speech_CHAR_BATMAN_Hints_1.wem
Speech_CHAR_BATMAN_Hints_2.wem
Speech_CHAR_BATMAN_Hold_tight_1.wem
Speech_CHAR_BATMAN_Hold_tight_2.wem
Speech_CHAR_BATMAN_Hostages_Hostage_Taken_1.wem
Speech_CHAR_BATMAN_Hostages_Hostage_Taken_2.wem
Speech_CHAR_BATMAN_Hostages_quiet_1.wem
Speech_CHAR_BATMAN_Hostages_quiet_2.wem
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_1.wem
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_2.wem
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_3.wem
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_4.wem
Speech_CHAR_BATMAN_Jammed_frequencies_1.wem
Speech_CHAR_BATMAN_Jammer_fail_deadshot_1.wem
Speech_CHAR_BATMAN_Jammers_destroyed_1.wem
Speech_CHAR_BATMAN_Jammers_destroyed_2.wem
Speech_CHAR_BATMAN_Jammers_destroyed_3.wem
Speech_CHAR_BATMAN_Jammers_destroyed_4.wem
Speech_CHAR_BATMAN_Jammers_destroyed_5.wem
Speech_CHAR_BATMAN_lookout_for_hush_victims_1.wem
Speech_CHAR_BATMAN_Municipal_req_1.wem
Speech_CHAR_BATMAN_Municipal_req_2.wem
Speech_CHAR_BATMAN_MUS_no_Trail_1.wem
Speech_CHAR_BATMAN_MUS_no_Trail_2.wem
Speech_CHAR_BATMAN_Nav_Door_Locked_1.wem
Speech_CHAR_BATMAN_Nav_Door_Locked_2.wem
Speech_CHAR_BATMAN_Nav_Door_Locked_3.wem
Speech_CHAR_BATMAN_Nav_Door_Locked_4.wem
Speech_CHAR_BATMAN_No_entry_chimney_2.wem
Speech_CHAR_BATMAN_No_entry_chimney_3.wem
Speech_CHAR_BATMAN_No_entry_chimney_4.wem
Speech_CHAR_BATMAN_Platform_grenade_1.wem
Speech_CHAR_BATMAN_Riddle_1.wem
Speech_CHAR_BATMAN_Rubble_1.wem
Speech_CHAR_BATMAN_Safe_later_1.wem
Speech_CHAR_BATMAN_Safe_later_2.wem
Speech_CHAR_BATMAN_Save_doctor_in_steel_1.wem
Speech_CHAR_BATMAN_Save_generic_1.wem
Speech_CHAR_BATMAN_Save_generic_2.wem
Speech_CHAR_BATMAN_Save_generic_3.wem
Speech_CHAR_BATMAN_Save_generic_4.wem
Speech_CHAR_BATMAN_Save_generic_5.wem
Speech_CHAR_BATMAN_Save_generic_6.wem
Speech_CHAR_BATMAN_Save_Talia_1.wem
Speech_CHAR_BATMAN_Save_Talia_2.wem
Speech_CHAR_BATMAN_Save_Talia_Gliding_1.wem
Speech_CHAR_BATMAN_Save_vv_1.wem
Speech_CHAR_BATMAN_Secure_Area_1.wem
Speech_CHAR_BATMAN_Secure_Area_2.wem
Speech_CHAR_BATMAN_Steam_1.wem
Speech_CHAR_BATMAN_Steam_2.wem
Speech_CHAR_BATMAN_TAUNT_1.wem
Speech_CHAR_BATMAN_TAUNT_2.wem
Speech_CHAR_BATMAN_TAUNT_3.wem
Speech_CHAR_BATMAN_TAUNT_4.wem
Speech_CHAR_BATMAN_TAUNT_5.wem
Speech_CHAR_BATMAN_TAUNT_6.wem
Speech_CHAR_BATMAN_TAUNT_7.wem
Speech_CHAR_BATMAN_TAUNT_8.wem
Speech_CHAR_BATMAN_TAUNT_9.wem
Speech_CHAR_BATMAN_TAUNT_10.wem
Speech_CHAR_BATMAN_Thawing_ice_1.wem
Speech_CHAR_BATMAN_Tut_hostage_in_church1_1.wem
Speech_CHAR_BATMAN_TYGER_Req_1.wem
Speech_CHAR_BATMAN_UnderC2_Sword_1.wem
Speech_CHAR_BATMAN_Water_too_hot_1.wem
Speech_CHAR_BATMAN_Water_too_hot_2.wem
Speech_CHAR_BATMAN_Zsasz_no_door_1.wem
Speech_CHAR_BATMAN_Zsasz_no_door_2.wem
Speech_CHAR_FREEZE_FreezeHeadOn_1.wem
Speech_CHAR_FREEZE_FreezeRepeatTactic_1.wem
Speech_CHAR_FREEZE_FreezeRepeatTactic_2.wem
Speech_CHAR_FREEZE_FreezeSightedWarning_1.wem
Speech_CHAR_ZSASZ_Batman_located_Zsasz_1.wem
Speech_CHAR_ZSASZ_Batman_located_Zsasz_2.wem
Speech_CHAR_ZSASZ_Batman_located_Zsasz_3.wem
Speech_CHAR_ZSASZ_CALL1GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL1GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL2GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL8GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL8GLOAT_3.wem
Speech_CHAR_ZSASZ_Find_me_first_1.wem
Speech_CHAR_ZSASZ_Find_me_first_2.wem
Speech_CHAR_ZSASZ_Find_me_first_3.wem
Speech_CHAR_ZSASZ_Tease_2.wem
Speech_CHURCH_B1_BM_LETMEIN_1.wem
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_1.wem
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_2.wem
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_4.wem
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_6.wem
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_7.wem
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_9.wem
Speech_CHURCH_B1_CH2_BM_free_1.wem
Speech_CHURCH_B1_CH2_BM_GOTO_TOWER_1.wem
Speech_CHURCH_B1_CH2_BM_GOTO_TOWER_2.wem
Speech_CHURCH_B1_CH2_BM_Sniper_1.wem
Speech_CHURCH_B1_CH2_BM_Sniper_2.wem
Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_1.wem
Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_3.wem
Speech_CHURCH_B1_CH2_post_G3toBM_1.wem
Speech_CHURCH_B1_CH2_post_G3toBM_2.wem
Speech_CHURCH_B1_CH2_post_MD2_comfort_FD4_BM_2.wem
Speech_CHURCH_B1_CH2_post_MD3_tend_OC_BM_int_1.wem
Speech_CHURCH_B1_Pred_tutorial_1.wem
Speech_CHURCH_B1_Pred_tutorial_2.wem
Speech_CHURCH_B1_Pred_tutorial_3.wem
Speech_CHURCH_B1_Pred_tutorial_4.wem
Speech_CHURCH_B1_Pred_tutorial_5.wem
Speech_CHURCH_B1_Pred_tutorial_6.wem
Speech_CHURCH_B1_Pred_tutorial_7.wem
Speech_CHURCH_B1_Pred_tutorial_8.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_3.wem
Speech_CHURCH_B1_Riddler_guard_ENIGMA_2.wem
Speech_CHURCH_B1_Riddler_guard_ENIGMA_4.wem
Speech_CHURCH_B1_Riddlerguard_intro_2.wem
Speech_CHURCH_B1_Riddlerguard_intro_4.wem
Speech_CHURCH_B1_Riddlerguard_intro_6.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_3.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_5.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_9.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_22.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_29.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_11.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_13.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_14.wem
Speech_CINEMATIC_CH4_MEET_TALIA_1.wem
Speech_CINEMATIC_CH4_MEET_TALIA_2.wem
Speech_CINEMATIC_CH4_MEET_TALIA_6.wem
Speech_CINEMATIC_CH4_MEET_TALIA_8.wem
Speech_CINEMATIC_CH4_MEET_TALIA_13.wem
Speech_CINEMATIC_CH4_MEET_TALIA_14.wem
Speech_CINEMATIC_CH4_MEET_TALIA_17.wem
Speech_CINEMATIC_CH4_MEET_TALIA_23.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_5.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_10.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_13.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_14.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_15.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_21.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_31.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_32.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_35.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_2.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_4.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_6.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_13.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_14.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_17.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_24.wem
Speech_COURT_B1_CH1_BM_Catwoman_1.wem
Speech_COURT_B1_CH1_BM_Observeroom_1.wem
Speech_COURT_B1_CH2_BM_Bullet_1.wem
Speech_COURT_B1_CH2_BM_NotScanned_1.wem
Speech_COURT_B1_CH2_BM_Scanned_1.wem
Speech_COURT_B1_CH2_Bullet_floor_1.wem
Speech_COURT_B1_CH2_Bullet_floor_2.wem
Speech_COURT_B1_CH2_Bullet_window_1.wem
Speech_COURT_B1_CH2_Bullet_window_2.wem
Speech_COURT_B1_CH2_TF_Hang_conv_4.wem
Speech_EMOTE_Batman_1.wem
Speech_EMOTE_Batman_2.wem
Speech_EMOTE_Batman_3.wem
Speech_EMOTE_Batman_4.wem
Speech_EMOTE_Batman_5.wem
Speech_EMOTE_Batman_6.wem
Speech_EMOTE_Batman_7.wem
Speech_EMOTE_Batman_8.wem
Speech_EMOTE_Batman_9.wem
Speech_EMOTE_Batman_10.wem
Speech_EMOTE_Batman_11.wem
Speech_EMOTE_Batman_12.wem
Speech_EMOTE_Batman_13.wem
Speech_EMOTE_Batman_14.wem
Speech_EMOTE_Batman_15.wem
Speech_EMOTE_Batman_16.wem
Speech_EMOTE_Batman_17.wem
Speech_EMOTE_Batman_18.wem
Speech_EMOTE_Batman_19.wem
Speech_EMOTE_Batman_20.wem
Speech_EMOTE_Batman_21.wem
Speech_EMOTE_Batman_22.wem
Speech_EMOTE_Batman_23.wem
Speech_EMOTE_Batman_24.wem
Speech_EMOTE_Batman_25.wem
Speech_EMOTE_Batman_26.wem
Speech_EMOTE_Batman_27.wem
Speech_EMOTE_Batman_28.wem
Speech_EMOTE_Batman_29.wem
Speech_EMOTE_Batman_30.wem
Speech_EMOTE_Batman_31.wem
Speech_EMOTE_Batman_32.wem
Speech_EMOTE_Batman_33.wem
Speech_EMOTE_Batman_34.wem
Speech_EMOTE_Batman_35.wem
Speech_EMOTE_Batman_36.wem
Speech_EMOTE_Batman_37.wem
Speech_EMOTE_Batman_38.wem
Speech_EMOTE_Batman_39.wem
Speech_EMOTE_Batman_40.wem
Speech_EMOTE_Batman_41.wem
Speech_FORENSICS_Cold_1.wem
Speech_FORENSICS_Cold_2.wem
Speech_FORENSICS_Cold_3.wem
Speech_FORENSICS_Cold_4.wem
Speech_FORENSICS_Cold_5.wem
Speech_GCPD_BM_Bombs_1.wem
Speech_GCPD_BM_hackradio_thought_1.wem
Speech_GCPD_BM_need_to_find_new_route_1.wem
Speech_GCPD_Boss_intro_2.wem
Speech_GCPD_CH3_BM_ALF_Codes_1.wem
Speech_GCPD_CH3_BM_ALF_Codes_3.wem
Speech_GCPD_CH3_BM_ALF_Codes_4.wem
Speech_GCPD_Heartbeat_1.wem
Speech_GCPD_Heartbeat_2.wem
Speech_INTERROGATION_CH5_MAYOR_01_4.wem
Speech_INTERROGATION_CH5_MAYOR_01_5.wem
Speech_INTERROGATION_CH5_MAYOR_01_6.wem
Speech_INTERROGATION_GCPD_A1_01_2.wem
Speech_INTERROGATION_Hush_SS_01_1.wem
Speech_INTERROGATION_Hush_SS_01_2.wem
Speech_INTERROGATION_Hush_SS_01_6.wem
Speech_INTERROGATION_Riddler_01_2.wem
Speech_INTERROGATION_Riddler_01_3.wem
Speech_INTERROGATION_Riddler_02_2.wem
Speech_INTERROGATION_Riddler_02_3.wem
Speech_INTERROGATION_Riddler_03_1.wem
Speech_INTERROGATION_Riddler_03_3.wem
Speech_INTERROGATION_Riddler_04_2.wem
Speech_INTERROGATION_Riddler_04_3.wem
Speech_INTERROGATION_Riddler_05_2.wem
Speech_INTERROGATION_Riddler_05_3.wem
Speech_INTERROGATION_Riddler_06_1.wem
Speech_INTERROGATION_Riddler_07_1.wem
Speech_INTERROGATION_Riddler_07_3.wem
Speech_INTERROGATION_Riddler_08_2.wem
Speech_INTERROGATION_Riddler_08_3.wem
Speech_INTERROGATION_Riddler_09_1.wem
Speech_INTERROGATION_Riddler_09_3.wem
Speech_INTERROGATION_Riddler_10_1.wem
Speech_INTERROGATION_Riddler_10_3.wem
Speech_INTERROGATION_Riddler_11_2.wem
Speech_INTERROGATION_Riddler_11_3.wem
Speech_INTERROGATION_Riddler_12_1.wem
Speech_INTERROGATION_Riddler_12_2.wem
Speech_INTERROGATION_Riddler_13_1.wem
Speech_INTERROGATION_Riddler_13_3.wem
Speech_INTERROGATION_Riddler_14_2.wem
Speech_INTERROGATION_Riddler_14_3.wem
Speech_INTERROGATION_Riddler_15_2.wem
Speech_INTERROGATION_Riddler_15_3.wem
Speech_INTERROGATION_Riddler_16_1.wem
Speech_INTERROGATION_Riddler_16_3.wem
Speech_INTERROGATION_Riddler_17_2.wem
Speech_INTERROGATION_Riddler_17_3.wem
Speech_INTERROGATION_Riddler_18_1.wem
Speech_INTERROGATION_Riddler_18_3.wem
Speech_INTERROGATION_Riddler_19_2.wem
Speech_INTERROGATION_Riddler_19_3.wem
Speech_INTERROGATION_Riddler_20_1.wem
Speech_INTERROGATION_Riddler_20_2.wem
Speech_INTERROGATION_Riddler_21_2.wem
Speech_INTERROGATION_Riddler_21_3.wem
Speech_INTERROGATION_Riddler_22_1.wem
Speech_INTERROGATION_Riddler_22_3.wem
Speech_INTERROGATION_Riddler_23_1.wem
Speech_INTERROGATION_Riddler_23_3.wem
Speech_INTERROGATION_Riddler_24_1.wem
Speech_INTERROGATION_Riddler_24_2.wem
Speech_INTERROGATION_Riddler_25_2.wem
Speech_INTERROGATION_Riddler_25_3.wem
Speech_MAP_Final_map_1.wem
Speech_MAP_Find_marker_1.wem
Speech_MUSEUM_A1_CH3_Cop_C4_door_conv_1.wem
Speech_MUSEUM_A1_CH3_Cop_C5_bridge_conv_2.wem
Speech_MUSEUM_A1_CH3_Cop_C5_bridge_conv_4.wem
Speech_MUSEUM_A1_CH3_Cop_C6_Patrol_conv_3.wem
Speech_MUSEUM_A1_Ch3_dont_leave_cops_1.wem
Speech_MUSEUM_A1_Ch3_dont_leave_cops_2.wem
Speech_MUSEUM_A1_CH3_Find_suit_Early_1.wem
Speech_MUSEUM_A1_Spot_thermal_1.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_1.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_5.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_7.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_8.wem
Speech_MUSEUM_B2_CH3_BM_Ice_1.wem
Speech_MUSEUM_B2_CH3_BM_Ice_2.wem
Speech_MUSEUM_B2_CH3_BM_Ice_nobomb_1.wem
Speech_MUSEUM_B2_CH3_Host2_Resc_1.wem
Speech_MUSEUM_B2_CH3_Host2and3_Resc_BM_1.wem
Speech_MUSEUM_B2_CH3_Host2and3_Resc_BM_2.wem
Speech_MUSEUM_C1_CH3_BM_TalktoCop_1.wem
Speech_MUSEUM_C1_CH3_Find_Jammers_outside_1.wem
Speech_MUSEUM_C1_CH3_Museum_no_entry_1.wem
Speech_MUSEUM_C1_CH3_Museum_no_entry_2.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_1.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_2.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_3.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_4.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_7.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_9.wem
Speech_MUSEUM_C1_CH3_POST_THUG_COP_11.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_4.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_5.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_6.wem
Speech_MUSEUM_C1_CH4_Penguin_2.wem
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_3.wem
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_2.wem
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_4.wem
Speech_MUSEUM_C2_CH4_Freeze_revisit_2.wem
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_2.wem
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_1.wem
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_2.wem
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_3.wem
Speech_MUSEUM_S1_CH3_BM_Get_Jammer_1.wem
Speech_MUSEUM_S1_CH3_BM_Jammer_1.wem
Speech_MUSEUM_S1_CH3_BM_Jammer_2.wem
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_1.wem
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_2.wem
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_3.wem
Speech_MUSEUM_S1_CH5_plus_post_door_chat_2.wem
Speech_MUSEUM_S1_CH5_plus_post_door_chat_3.wem
Speech_MUSEUM_S1_CH9_post_door_chat_2.wem
Speech_MUSEUM_S1_CH9_post_door_chat_4.wem
Speech_New_game_Quick_1.wem
Speech_New_game_Quick_2.wem
Speech_New_game_Quick_3.wem
Speech_New_help_Enter_court_2.wem
Speech_New_help_Spot_amored_1.wem
Speech_New_help_Spot_jammer_1.wem
Speech_New_help_Spot_minelayer_1.wem
Speech_New_help_Spot_minelayer_2.wem
Speech_New_help_Spot_shield_1.wem
Speech_New_help_Spot_sniper_1.wem
Speech_New_help_Spot_taser_1.wem
Speech_RIDDLER_R1_Guard_Rescue_2.wem
Speech_RIDDLER_R1_intro_1.wem
Speech_RIDDLER_R1_intro_6.wem
Speech_RIDDLER_R2_Saved_conv_1.wem
Speech_RIDDLER_R2_Saved_conv_2.wem
Speech_RIDDLER_R2_Saved_conv_5.wem
Speech_RIDDLER_R3_new_BM_sit_tight_1.wem
Speech_RIDDLER_R3_new_Hostage_rescue_1.wem
Speech_RIDDLER_R3_new_Hostage_rescue_2.wem
Speech_RIDDLER_R3_new_Hostage_rescue_3.wem
Speech_RIDDLER_R4_Hostage_rescue_2.wem
Speech_RIDDLER_R4_Hostage_rescue_3.wem
Speech_RIDDLER_R4_Hostage_rescue_4.wem
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_2.wem
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_3.wem
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_4.wem
Speech_RIDDLER_R5_new_finished_oracle_give_location_2.wem
Speech_RIDDLER_R5_new_finished_oracle_give_location_3.wem
Speech_RIDDLER_R5_new_hostage_rescue_conv_1.wem
Speech_RIDDLER_R5_new_hostage_rescue_conv_5.wem
Speech_RIDDLER_R5_new_hostage_rescue_conv_6.wem
Speech_RIDDLER_R6_BM_thoughts_about_vest_1.wem
Speech_SEQUENCE_CATmeetBAT_2.wem
Speech_SEQUENCE_CATmeetBAT_5.wem
Speech_SEQUENCE_CATsaveBAT_3.wem
Speech_SEQUENCE_CATsaveBAT_5.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_1.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_4.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_5.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_10.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_11.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_16.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_18.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_2.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_3.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_4.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_5.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_7.wem
Speech_SEQUENCE_CH3_MUSEUM_A1_BM_FREEZESUIT_1.wem
Speech_SEQUENCE_CH3_MUSEUM_A1_BM_FREEZESUIT_2.wem
Speech_SEQUENCE_CH3_MUSEUM_A1_GUARD_SAVED_1.wem
Speech_SEQUENCE_CH3_MUSEUM_A1_GUARD_SAVED_3.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_1.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_4.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_5.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_6.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_1.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_2.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_6.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_10.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_13.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_14.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_18.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_19.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_4.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_6.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_8.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_2.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_3.wem
Speech_SIDE_AZRAEL_Final_2.wem
Speech_SIDE_AZRAEL_Final_5.wem
Speech_SIDE_AZRAEL_Final_9.wem
Speech_SIDE_BANE_All_Crates_1.wem
Speech_SIDE_BANE_Bat_Symbol_1.wem
Speech_SIDE_BANE_BM_comments_crate_before_Bane_1.wem
Speech_SIDE_BANE_CAUGHT_CONV_2.wem
Speech_SIDE_BANE_CAUGHT_CONV_3.wem
Speech_SIDE_BANE_Crate_destroy_hint_1.wem
Speech_SIDE_BANE_Crate_destroyed_hint_1.wem
Speech_SIDE_BANE_END_4.wem
Speech_SIDE_BANE_END_6.wem
Speech_SIDE_BANE_END_8.wem
Speech_SIDE_BANE_INTRO_ANSWERS_FIVE_1.wem
Speech_SIDE_BANE_INTRO_ANSWERS_FOUR_1.wem
Speech_SIDE_BANE_INTRO_ANSWERS_THREEANDLESS_1.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_2.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_5.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_6.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_8.wem
Speech_SIDE_BANE_INTRO_POST_MUSEUM_10.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_1.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_2.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_3.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_7.wem
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_8.wem
Speech_SIDE_BANE_Pre_tyger_conv_4.wem
Speech_SIDE_BANE_STOP_2.wem
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_1.wem
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_2.wem
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_3.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_1.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_2.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_3.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_4.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_5.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_6.wem
Speech_SIDE_DEADSHOT_BM_Crimescene_7.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_1.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_2.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_3.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_4.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_5.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_6.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_7.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_8.wem
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_9.wem
Speech_SIDE_DEADSHOT_BM_FindStash_1.wem
Speech_SIDE_DEADSHOT_BM_FindStash_2.wem
Speech_SIDE_DEADSHOT_BM_Leaves_area_1.wem
Speech_SIDE_DEADSHOT_BM_Leaves_area_2.wem
Speech_SIDE_DEADSHOT_BM_No_prints_1.wem
Speech_SIDE_DEADSHOT_BM_No_prints_2.wem
Speech_SIDE_DEADSHOT_BM_No_prints_3.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot2_2.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_1.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_4.wem
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_1.wem
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_2.wem
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_3.wem
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_4.wem
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_5.wem
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_1.wem
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_2.wem
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_3.wem
Speech_SIDE_DEADSHOT_BM_See_sub_1.wem
Speech_SIDE_DEADSHOT_BM_See_sub_2.wem
Speech_SIDE_DEADSHOT_BM_See_sub_3.wem
Speech_SIDE_DEADSHOT_BM_Substations_1.wem
Speech_SIDE_DEADSHOT_BM_Substations_2.wem
Speech_SIDE_DEADSHOT_BM_Substations_3.wem
Speech_SIDE_DEADSHOT_END_seq_1.wem
Speech_SIDE_DEADSHOT_RYDER_post_saved_1.wem
Speech_SIDE_DEADSHOT_RYDER_post_saved_4.wem
Speech_SIDE_DEADSHOT_Save_Ryder_3.wem
Speech_SIDE_DEADSHOT_VIC1_BM_find_case_1.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_1.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_3.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_4.wem
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_7.wem
Speech_SIDE_DEADSHOT_VIC1_Conv_1.wem
Speech_SIDE_DEADSHOT_VIC1_Conv_5.wem
Speech_SIDE_DEADSHOT_VIC1_Conv_6.wem
Speech_SIDE_DEADSHOT_VIC2_BM_Find_Tripod_1.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_1.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_3.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_4.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_1.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_2.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_5.wem
Speech_SIDE_DEADSHOT_VIC2_Watertower_1.wem
Speech_SIDE_DEADSHOT_VIC3_BM_comments_snow_1.wem
Speech_SIDE_DEADSHOT_VIC3_BM_comments_snow_2.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_1.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_3.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_4.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_1.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_2.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_5.wem
Speech_SIDE_DEADSHOT_VIC3_Ricochet_1.wem
Speech_SIDE_HUSH_BM_post_hush_OR_3.wem
Speech_SIDE_HUSH_BM_post_hush_OR_4.wem
Speech_SIDE_HUSH_BM_post_hush_OR_5.wem
Speech_SIDE_HUSH_BM_post_hush_OR_7.wem
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_1.wem
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_5.wem
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_6.wem
Speech_SIDE_HUSH_hobo_conv_1.wem
Speech_SIDE_HUSH_hobo_conv_2.wem
Speech_SIDE_HUSH_hobo_conv_4.wem
Speech_SIDE_HUSH_Hush_speech_1.wem
Speech_SIDE_HUSH_Hush_speech_4.wem
Speech_SIDE_HUSH_Hush_speech_6.wem
Speech_SIDE_HUSH_Hush_speech_7.wem
Speech_SIDE_HUSH_Hush_speech_9.wem
Speech_SIDE_HUSH_SEE_VIC1_1.wem
Speech_SIDE_HUSH_SEE_VIC2_1.wem
Speech_SIDE_HUSH_SEE_VIC3_1.wem
Speech_SIDE_HUSH_VIC1_BOTH_SCANNED_1.wem
Speech_SIDE_HUSH_VIC1_SCANBLOOD_1.wem
Speech_SIDE_HUSH_VIC1_SCANFACE_1.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_3.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_4.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_1.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_2.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_6.wem
Speech_SIDE_HUSH_VIC2_SCANFACE_1.wem
Speech_SIDE_HUSH_VIC2_SCANPRINT_1.wem
Speech_SIDE_HUSH_VIC3_BOTHSCANNED_1.wem
Speech_SIDE_HUSH_VIC3_SCANBLEACH_1.wem
Speech_SIDE_HUSH_VIC3_SCANFACE_1.wem
Speech_SIDE_MADHATTER_INTRO1_2.wem
Speech_SIDE_MADHATTER_INTRO1_4.wem
Speech_SIDE_MADHATTER_INTRO2_2.wem
Speech_SIDE_MADHATTER_INTRO3_2.wem
Speech_SIDE_MADHATTER_INTRO4_2.wem
Speech_SIDE_MADHATTER_POST_FIGHT_2.wem
Speech_SIDE_MADHATTER_POST_FIGHT_3.wem
Speech_SIDE_NORA_BM_door_check_1.wem
Speech_SIDE_STORY_Killer_Croc_1.wem
Speech_SIDE_STORY_Nora_BM_Safe_1.wem
Speech_SIDE_STORY_Nora_BM_Safe_2.wem
Speech_SIDE_STORY_Nora_Freeze_BM_2.wem
Speech_SIDE_STORY_Nora_Freeze_BM_3.wem
Speech_SIDE_STORY_Nora_GCPD_Nora_1.wem
Speech_SIDE_STORY_Nora_GCPD_Nora_3.wem
Speech_SIDE_STORY_VV_pre_rescue_snipers_1.wem
Speech_SIDE_STORY_VV_saved_conv_2.wem
Speech_SIDE_STORY_VV_saved_conv_3.wem
Speech_SIDE_STORY_VV_saved_conv_4.wem
Speech_SIDE_STORY_VV_saved_conv_7.wem
Speech_SIDE_STORY_VV_saved_conv_10.wem
Speech_SIDE_STORY_Zsasz_BM_Save_hobo1_1.wem
Speech_SIDE_STORY_Zsasz_BM_Save_hobo2_1.wem
Speech_STEEL_A1_CH2_BM_Doc_Trap_1.wem
Speech_STEEL_A1_CH2_BM_Doc_Trap_3.wem
Speech_STEEL_A1_CH2_BM_Go_J_1.wem
Speech_STEEL_A1_CH2_BM_Go_J_2.wem
Speech_STEEL_A1_CH2_Rescue_early_1.wem
Speech_STEEL_A1_CH2_Rescue_early_2.wem
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_3.wem
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_4.wem
Speech_STEEL_B1_CH2_Goto_Doc_ALF_2.wem
Speech_STEEL_B1_CH2_Goto_Doc_ALF_3.wem
Speech_STEEL_B2_CH2_BM_find_MCR_1.wem
Speech_STEEL_B2_CH2_BM_Observe_1.wem
Speech_STEEL_B2_CH2_BM_Observe_2.wem
Speech_STEEL_B2_CH2_BM_Observe_3.wem
Speech_STEEL_B2_CH2_GoBack_LoadingBay_1.wem
Speech_STEEL_C1_CH2_BM_Steam_1.wem
Speech_STEEL_C1_CH2_BM_Steam_2.wem
Speech_STEEL_C1_CH2_BM_UnstableFurnace_1.wem
Speech_STEEL_C2_CH2_Batman_glass_1.wem
Speech_STEEL_C2_CH2_BM_Ambush_1.wem
Speech_STEEL_C2_CH2_check_door_1.wem
Speech_STEEL_C2_CH2_check_door_hint_1.wem
Speech_STEEL_C2_CH2_Smashglass_1.wem
Speech_STEEL_C3_CH3_post_seq_BM_ALF_2.wem
Speech_STEEL_C3_CH3_post_seq_BM_ALF_3.wem
Speech_STEEL_C3_CH3_post_seq_BM_ALF_4.wem
Speech_STEEL_C4_CH2_BM_Water_1.wem
Speech_STEEL_C4_CH2_BM_Water_2.wem
Speech_STEEL_C4_CH6_BM_Platform_1.wem
Speech_STEEL_C4_CH6_BM_Platform_2.wem
Speech_STEEL_C4_OR_INT_COUNCIL_1_2.wem
Speech_STEEL_C4_OR_INT_COUNCIL_1_5.wem
Speech_STEEL_C5_CH6_Doc_Sorry_1.wem
Speech_STEEL_C5_CH6_Doc_Sorry_4.wem
Speech_STEEL_C5_CH6_Doc_Sorry_6.wem
Speech_STEEL_C5_CH6_Doc_Sorry_8.wem
Speech_STEEL_C5_CH6_Doc_Sorry_9.wem
Speech_STEEL_C5_OR_INT_COUNCIL_2_2.wem
Speech_STREETS_A1_CH0_BM_ALF_3.wem
Speech_STREETS_A1_CH0_BM_ALF_4.wem
Speech_STREETS_A1_CH1_BM_CourtLocked_1.wem
Speech_STREETS_A1_CH1_BM_Sneak_1.wem
Speech_STREETS_A1_CH2_BM_ALF_Church_1.wem
Speech_STREETS_A1_CH2_BM_ALF_Church_3.wem
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_1.wem
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_3.wem
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_4.wem
Speech_STREETS_A1_CH2_BM_ChurchSniper_1.wem
Speech_STREETS_A1_CH2_BM_ChurchSniper_2.wem
Speech_STREETS_A1_SZ2_CH1_JR_CONV_1.wem
Speech_STREETS_A1_SZ2_CH1_JR_CONV_4.wem
Speech_STREETS_A1_SZ2_CH1_JR_CONV_5.wem
Speech_STREETS_A1_SZ2_CH1_JR_CONV_8.wem
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_1.wem
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_3.wem
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_5.wem
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_7.wem
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_8.wem
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_2.wem
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_3.wem
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_4.wem
Speech_STREETS_A2_CH2_BM_Defended_1.wem
Speech_STREETS_A2_CH2_BM_Defended_2.wem
Speech_STREETS_A3_CH2_BM_ALF_findroute_2.wem
Speech_STREETS_A3_CH2_BM_ALF_findroute_4.wem
Speech_STREETS_A3_CH2_BM_ALF_findroute_5.wem
Speech_STREETS_A3_CH3_BM_Wake_Intro_2.wem
Speech_STREETS_A3_CH3_BM_Wake_Intro_3.wem
Speech_STREETS_A3_CH3_BM_Wake_Intro_4.wem
Speech_STREETS_A3_CH7_BM_ALF_Tower_2.wem
Speech_STREETS_A3_CH7_BM_ALF_Tower_3.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_1.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_3.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_6.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_7.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_9.wem
Speech_STREETS_A3_CollectGrapnelBoost_1.wem
Speech_STREETS_A3_CollectGrapnelBoost_4.wem
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_3.wem
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_4.wem
Speech_STREETS_A4_CH4_BM_Trail_underground_2.wem
Speech_STREETS_A4_CH4_D4_post_thought_1.wem
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_1.wem
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_3.wem
Speech_STREETS_A5_CH4_BM_Line_Launch_req_1.wem
Speech_STREETS_A5_CH4_Need_tracker_1.wem
Speech_STREETS_A5_CH4_Need_tracker_2.wem
Speech_STREETS_A5_CH5_BM_OR_Mayor_2.wem
Speech_STREETS_A5_CH5_BM_OR_Mayor_4.wem
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_2.wem
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_4.wem
Speech_STREETS_A5_CH7_post_fight_conversation_1.wem
Speech_STREETS_A5_CH7_post_fight_conversation_2.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_1.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_2.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_3.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_7.wem
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_1.wem
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_2.wem
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_3.wem
Speech_UNDER_A1_CH4_Doc_post_rescue_3.wem
Speech_UNDER_A1_CH4_Doc_post_rescue_5.wem
Speech_UNDER_A1_CH4_Doc_post_rescue_6.wem
Speech_UNDER_A1_CH4_Doc_post_rescue_8.wem
Speech_UNDER_A1_CH4_Post_BM_OR_3.wem
Speech_UNDER_A1_CH4_Post_BM_OR_4.wem
Speech_UNDER_A1_CH4_Post_BM_OR_5.wem
Speech_UNDER_A1_CH4_Post_BM_OR_6.wem
Speech_UNDER_A1_CH5_BM_NW_4.wem
Speech_UNDER_A1_CH5_BM_NW_5.wem
Speech_UNDER_A1_CH5_BM_NW_7.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_1.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_2.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_5.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_6.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_7.wem
Speech_UNDER_A1_CH7_Doc_Execute_2.wem
Speech_UNDER_A1_CH7_Doc_Execute_5.wem
Speech_UNDER_A1_CH7_Doc_Execute_6.wem
Speech_UNDER_B1_CH4_TALIA_WALK_3.wem
Speech_UNDER_B1_CH4_TALIA_WALK_6.wem
Speech_UNDER_B4_CH7_Elevator_stuck_1.wem
Speech_UNDER_B4_CH7_Elevator_stuck_2.wem
Speech_UNDER_B4_CH7_ORACLE_near_top_3.wem
Speech_UNDER_B4_CH7_ORACLE_near_top_4.wem
Speech_UNDER_C1_CH4_BM_OR_Collapse1_1.wem
Speech_UNDER_C1_CH4_BM_OR_stealguns_1.wem
Speech_UNDER_C1_CH4_BM_OR_stealguns_3.wem
Speech_UNDER_C1_Ch4_BM_Weapons_1.wem
Speech_UNDER_C2_CH4_BM_DoorSealed_1.wem
Speech_UNDER_C2_CH4_BM_DoorSealed_2.wem
Speech_UNDER_C2_CH4_BM_DoorSealed_3.wem
Speech_UNDER_C2_CH4_BM_DoorSealed_4.wem
Speech_UNDER_C2_CH4_BM_DoorSealed_5.wem
Speech_UNDER_C2_CH4_BM_watches_video_1.wem
Speech_UNDER_C2_CH4_BM_watches_video_2.wem
Speech_UNDER_C2_CH4_MG_corrupt_1.wem
Speech_UNDER_C2_CH4_MG_corrupt_2.wem
Speech_UNDER_S1_Batarang_1.wem
Speech_UNDER_S1_Batman_sword_raised_1.wem
Speech_UNDER_S1_Batman_sword_raised_2.wem
Speech_UNDER_S1_Batman_sword_raised_3.wem
Speech_UNDER_S1_Batman_sword_raised_4.wem
Speech_UNDER_S1_Boss_conv1B_1.wem
Speech_UNDER_S1_Sick_dream_post_1.wem
Speech_UNDER_S1_Sick_dream_post_3.wem
Speech_UNDER_S1_SwordStruggle1_2.wem
Speech_UNDER_S1_Talia_TrialsDone_1.wem
Speech_UNDER_S1_Talia_TrialsDone_2.wem
Speech_Vertical_slice_JackRyderGDC_2.wem
Speech_Vertical_slice_JackRyderGDC_7.wem
Speech_Vertical_slice_JackRyderGDC_8.wem
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo7_1.wem
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo7_2.wem
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo8_2.wem
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo8_4.wem
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_1.wem
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_3.wem
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_4.wem
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_1.wem
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_2.wem
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_5.wem
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_7.wem
Speech_ZP1_BM_ace_jump_1.wem

View File

@@ -0,0 +1,119 @@
Speech_SIDE_CAT_MEET_CM_1.wem
Speech_SIDE_CAT_MEET_CM_2.wem
Speech_SIDE_CAT_MEET_CM_6.wem
Speech_SIDE_STORY_April_1.wem
Speech_SIDE_STORY_April_2.wem
Speech_SIDE_STORY_Aug_1.wem
Speech_SIDE_STORY_Aug_2.wem
Speech_SIDE_STORY_CM_APR_1.wem
Speech_SIDE_STORY_CM_APR_2.wem
Speech_SIDE_STORY_CM_APR_3.wem
Speech_SIDE_STORY_CM_APR_4.wem
Speech_SIDE_STORY_CM_AUG_1.wem
Speech_SIDE_STORY_CM_AUG_2.wem
Speech_SIDE_STORY_CM_AUG_3.wem
Speech_SIDE_STORY_CM_AUG_4.wem
Speech_SIDE_STORY_CM_AUG_5.wem
Speech_SIDE_STORY_CM_AUG_6.wem
Speech_SIDE_STORY_CM_DEC_1.wem
Speech_SIDE_STORY_CM_DEC_2.wem
Speech_SIDE_STORY_CM_DEC_3.wem
Speech_SIDE_STORY_CM_DEC_4.wem
Speech_SIDE_STORY_CM_DEC_5.wem
Speech_SIDE_STORY_CM_DEC_6.wem
Speech_SIDE_STORY_CM_FEB_1.wem
Speech_SIDE_STORY_CM_FEB_2.wem
Speech_SIDE_STORY_CM_FEB_3.wem
Speech_SIDE_STORY_CM_FEB_4.wem
Speech_SIDE_STORY_CM_FEB_5.wem
Speech_SIDE_STORY_CM_FEB_6.wem
Speech_SIDE_STORY_CM_Intro_loop1_1.wem
Speech_SIDE_STORY_CM_Intro_loop1_2.wem
Speech_SIDE_STORY_CM_Intro_loop1_3.wem
Speech_SIDE_STORY_CM_Intro_loop1_4.wem
Speech_SIDE_STORY_CM_Intro_loop1_5.wem
Speech_SIDE_STORY_CM_Intro_loop1_6.wem
Speech_SIDE_STORY_CM_Intro_loop2_1.wem
Speech_SIDE_STORY_CM_Intro_loop2_2.wem
Speech_SIDE_STORY_CM_Intro_loop2_3.wem
Speech_SIDE_STORY_CM_Intro_loop2_4.wem
Speech_SIDE_STORY_CM_Intro_loop2_5.wem
Speech_SIDE_STORY_CM_Intro_loop2_6.wem
Speech_SIDE_STORY_CM_Intro_loop3_1.wem
Speech_SIDE_STORY_CM_Intro_loop3_2.wem
Speech_SIDE_STORY_CM_Intro_loop3_3.wem
Speech_SIDE_STORY_CM_Intro_loop3_4.wem
Speech_SIDE_STORY_CM_Intro_loop3_5.wem
Speech_SIDE_STORY_CM_Intro_loop3_6.wem
Speech_SIDE_STORY_CM_Intro_loop3_7.wem
Speech_SIDE_STORY_CM_JAN_1.wem
Speech_SIDE_STORY_CM_JAN_2.wem
Speech_SIDE_STORY_CM_JAN_3.wem
Speech_SIDE_STORY_CM_JAN_4.wem
Speech_SIDE_STORY_CM_JAN_5.wem
Speech_SIDE_STORY_CM_JUL_1.wem
Speech_SIDE_STORY_CM_JUL_2.wem
Speech_SIDE_STORY_CM_JUL_3.wem
Speech_SIDE_STORY_CM_JUL_4.wem
Speech_SIDE_STORY_CM_JUL_5.wem
Speech_SIDE_STORY_CM_JUL_6.wem
Speech_SIDE_STORY_CM_JUN_1.wem
Speech_SIDE_STORY_CM_JUN_2.wem
Speech_SIDE_STORY_CM_JUN_3.wem
Speech_SIDE_STORY_CM_JUN_4.wem
Speech_SIDE_STORY_CM_JUN_5.wem
Speech_SIDE_STORY_CM_MAR_1.wem
Speech_SIDE_STORY_CM_MAR_2.wem
Speech_SIDE_STORY_CM_MAR_3.wem
Speech_SIDE_STORY_CM_MAR_4.wem
Speech_SIDE_STORY_CM_MAR_5.wem
Speech_SIDE_STORY_CM_MAR_6.wem
Speech_SIDE_STORY_CM_MAR_7.wem
Speech_SIDE_STORY_CM_MAY_1.wem
Speech_SIDE_STORY_CM_MAY_2.wem
Speech_SIDE_STORY_CM_MAY_3.wem
Speech_SIDE_STORY_CM_MAY_4.wem
Speech_SIDE_STORY_CM_MAY_5.wem
Speech_SIDE_STORY_CM_NOV_1.wem
Speech_SIDE_STORY_CM_NOV_2.wem
Speech_SIDE_STORY_CM_NOV_3.wem
Speech_SIDE_STORY_CM_NOV_4.wem
Speech_SIDE_STORY_CM_NOV_5.wem
Speech_SIDE_STORY_CM_NOV_6.wem
Speech_SIDE_STORY_CM_NOV_7.wem
Speech_SIDE_STORY_CM_NOV_8.wem
Speech_SIDE_STORY_CM_OCT_1.wem
Speech_SIDE_STORY_CM_OCT_2.wem
Speech_SIDE_STORY_CM_OCT_3.wem
Speech_SIDE_STORY_CM_OCT_4.wem
Speech_SIDE_STORY_CM_OCT_5.wem
Speech_SIDE_STORY_CM_OCT_6.wem
Speech_SIDE_STORY_CM_SEP_1.wem
Speech_SIDE_STORY_CM_SEP_2.wem
Speech_SIDE_STORY_CM_SEP_3.wem
Speech_SIDE_STORY_CM_SEP_4.wem
Speech_SIDE_STORY_CM_SEP_5.wem
Speech_SIDE_STORY_CM_SEP_6.wem
Speech_SIDE_STORY_CM_special_1.wem
Speech_SIDE_STORY_CM_special_2.wem
Speech_SIDE_STORY_CM_special_3.wem
Speech_SIDE_STORY_Dec_1.wem
Speech_SIDE_STORY_Dec_2.wem
Speech_SIDE_STORY_Feb_1.wem
Speech_SIDE_STORY_Feb_2.wem
Speech_SIDE_STORY_Jan_1.wem
Speech_SIDE_STORY_Jan_2.wem
Speech_SIDE_STORY_July_1.wem
Speech_SIDE_STORY_July_2.wem
Speech_SIDE_STORY_June_1.wem
Speech_SIDE_STORY_June_2.wem
Speech_SIDE_STORY_March_1.wem
Speech_SIDE_STORY_March_2.wem
Speech_SIDE_STORY_May_1.wem
Speech_SIDE_STORY_May_2.wem
Speech_SIDE_STORY_Nov_1.wem
Speech_SIDE_STORY_Nov_2.wem
Speech_SIDE_STORY_October_1.wem
Speech_SIDE_STORY_October_2.wem
Speech_SIDE_STORY_Sept_1.wem
Speech_SIDE_STORY_Sept_2.wem

View File

@@ -0,0 +1,223 @@
Speech_CHAR_CATWOMAN_CW2_intro_mono_1.wem
Speech_CHAR_CATWOMAN_CW2_intro_mono_2.wem
Speech_CHAR_CATWOMAN_CW2_intro_mono_3.wem
Speech_CHAR_CATWOMAN_CW2_intro_mono_4.wem
Speech_CHAR_CATWOMAN_CW2_intro_mono_5.wem
Speech_CHAR_CATWOMAN_CW2_intro_mono_6.wem
Speech_CHAR_CATWOMAN_CW2_Round1_over_1.wem
Speech_CHAR_CATWOMAN_CW2_Round1_over_2.wem
Speech_CHAR_CATWOMAN_CW2_Round2_over_2.wem
Speech_CHAR_CATWOMAN_CW2_Round2_over_3.wem
Speech_CHAR_CATWOMAN_CW3_be_quick_1.wem
Speech_CHAR_CATWOMAN_CW3_clear_room_first_1.wem
Speech_CHAR_CATWOMAN_CW3_clear_room_first_2.wem
Speech_CHAR_CATWOMAN_CW3_destroy_plant_intro_1.wem
Speech_CHAR_CATWOMAN_CW3_destroy_plants_1.wem
Speech_CHAR_CATWOMAN_CW3_destroy_plants_2.wem
Speech_CHAR_CATWOMAN_CW3_dont_leave_goto_vault_1.wem
Speech_CHAR_CATWOMAN_CW3_Getcards_1.wem
Speech_CHAR_CATWOMAN_CW3_Getcards_2.wem
Speech_CHAR_CATWOMAN_CW3_Goto_SecRoom_1.wem
Speech_CHAR_CATWOMAN_CW3_Key_reminder_1.wem
Speech_CHAR_CATWOMAN_CW3_Key_reminder_2.wem
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_1.wem
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_2.wem
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_3.wem
Speech_CHAR_CATWOMAN_CW3_save_Bat_1.wem
Speech_CHAR_CATWOMAN_CW3_save_Bat_2.wem
Speech_CHAR_CATWOMAN_CW3_vault_mono_1.wem
Speech_CHAR_CATWOMAN_CW3_vault_mono_2.wem
Speech_CHAR_CATWOMAN_CW3_vault_mono_3.wem
Speech_CHAR_CATWOMAN_CW3_vault_mono_4.wem
Speech_CHAR_CATWOMAN_CW_all_loot_1.wem
Speech_CHAR_CATWOMAN_CW_all_loot_2.wem
Speech_CHAR_CATWOMAN_CW_find_safe_1.wem
Speech_CHAR_CATWOMAN_CW_find_safe_2.wem
Speech_CHAR_CATWOMAN_CW_find_safe_3.wem
Speech_CHAR_CATWOMAN_CW_FindIvy_1.wem
Speech_CHAR_CATWOMAN_CW_Home_1.wem
Speech_CHAR_CATWOMAN_CW_Home_2.wem
Speech_CHAR_CATWOMAN_CW_see_vine_1.wem
Speech_CHAR_CATWOMAN_CW_see_vine_2.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_1.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_2.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_3.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_4.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_5.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_6.wem
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_7.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_1.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_2.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_3.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_4.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_5.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_6.wem
Speech_CHAR_CATWOMAN_Frontend_taunt_7.wem
Speech_CHAR_CATWOMAN_Goto_Apart_1.wem
Speech_CHAR_CATWOMAN_Goto_Apart_2.wem
Speech_CHAR_CATWOMAN_Goto_Apart_3.wem
Speech_CHAR_CATWOMAN_Grates_1.wem
Speech_CHAR_CATWOMAN_Grates_2.wem
Speech_CHAR_CATWOMAN_Grates_3.wem
Speech_CHAR_CATWOMAN_New_plant_destroy_1.wem
Speech_CHAR_CATWOMAN_New_plant_destroy_2.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_1.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_8.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_11.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_12.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_13.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_14.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_15.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_17.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_18.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_19.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_20.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_24.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_25.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_26.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_27.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_28.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_31.wem
Speech_COURT_B1_CH1_CW_Fight_idles_1.wem
Speech_COURT_B1_CH1_CW_Fight_idles_2.wem
Speech_COURT_B1_CH1_CW_Fight_idles_3.wem
Speech_COURT_B1_CH1_CW_Fight_idles_4.wem
Speech_COURT_B1_CH1_CW_rope_idles_1.wem
Speech_COURT_B1_CH1_CW_rope_idles_2.wem
Speech_COURT_B1_CH1_TwoFace_1_1.wem
Speech_COURT_B1_CH1_TwoFace_2_4.wem
Speech_EMOTE_CAT_1.wem
Speech_EMOTE_CAT_2.wem
Speech_EMOTE_CAT_3.wem
Speech_EMOTE_CAT_4.wem
Speech_EMOTE_CAT_5.wem
Speech_EMOTE_CAT_6.wem
Speech_EMOTE_CAT_7.wem
Speech_EMOTE_CAT_8.wem
Speech_EMOTE_CAT_9.wem
Speech_EMOTE_CAT_10.wem
Speech_EMOTE_CAT_11.wem
Speech_EMOTE_CAT_12.wem
Speech_EMOTE_CAT_13.wem
Speech_EMOTE_CAT_14.wem
Speech_EMOTE_CAT_15.wem
Speech_EMOTE_CAT_16.wem
Speech_EMOTE_CAT_17.wem
Speech_EMOTE_CAT_18.wem
Speech_EMOTE_CAT_19.wem
Speech_EMOTE_CAT_20.wem
Speech_EMOTE_CAT_21.wem
Speech_EMOTE_CAT_22.wem
Speech_EMOTE_CAT_23.wem
Speech_EMOTE_CAT_24.wem
Speech_EMOTE_CAT_25.wem
Speech_EMOTE_CAT_26.wem
Speech_EMOTE_CAT_27.wem
Speech_EMOTE_CAT_28.wem
Speech_EMOTE_CAT_29.wem
Speech_EMOTE_CAT_30.wem
Speech_EMOTE_CAT_31.wem
Speech_EMOTE_CAT_32.wem
Speech_EMOTE_CAT_33.wem
Speech_EMOTE_CAT_34.wem
Speech_EMOTE_CAT_35.wem
Speech_EMOTE_CAT_36.wem
Speech_EMOTE_CAT_37.wem
Speech_EMOTE_CAT_38.wem
Speech_EMOTE_CAT_39.wem
Speech_EMOTE_CAT_40.wem
Speech_EMOTE_CAT_41.wem
Speech_EMOTE_CAT_42.wem
Speech_EMOTE_CAT_43.wem
Speech_EMOTE_CAT_44.wem
Speech_EMOTE_CAT_45.wem
Speech_EMOTE_CAT_46.wem
Speech_EMOTE_CAT_47.wem
Speech_EMOTE_CAT_48.wem
Speech_EMOTE_CAT_49.wem
Speech_EMOTE_CAT_50.wem
Speech_EMOTE_CAT_51.wem
Speech_EMOTE_CAT_52.wem
Speech_EMOTE_CAT_53.wem
Speech_EMOTE_CAT_54.wem
Speech_EMOTE_CAT_55.wem
Speech_EMOTE_CAT_56.wem
Speech_EMOTE_CAT_57.wem
Speech_EMOTE_CAT_58.wem
Speech_EMOTE_CAT_59.wem
Speech_INTERROGATION_Cat_01_2.wem
Speech_INTERROGATION_Cat_01_3.wem
Speech_INTERROGATION_Cat_01_3_1.wem
Speech_SEQUENCE_CAT_GOTO_IVY_1.wem
Speech_SEQUENCE_CAT_GOTO_IVY_2.wem
Speech_SEQUENCE_CAT_GOTO_IVY_5.wem
Speech_SEQUENCE_CAT_GOTO_IVY_6.wem
Speech_SEQUENCE_CAT_GOTO_IVY_7.wem
Speech_SEQUENCE_CAT_GOTO_IVY_9.wem
Speech_SEQUENCE_CAT_IVY_1_1.wem
Speech_SEQUENCE_CAT_IVY_1_3.wem
Speech_SEQUENCE_CAT_IVY_2_1.wem
Speech_SEQUENCE_CAT_IVY_2_3.wem
Speech_SEQUENCE_CAT_IVY_2_7.wem
Speech_SEQUENCE_CAT_IVY_2_8.wem
Speech_SEQUENCE_CATmeetBAT_1.wem
Speech_SEQUENCE_CATmeetBAT_3.wem
Speech_SEQUENCE_CATmeetBAT_4.wem
Speech_SEQUENCE_CATmeetBAT_6.wem
Speech_SEQUENCE_CATmeetBAT_7.wem
Speech_SEQUENCE_CATmeetBAT_8.wem
Speech_SEQUENCE_CATmeetBAT_9.wem
Speech_SEQUENCE_CATmeetBAT_10.wem
Speech_SEQUENCE_CATsaveBAT_1.wem
Speech_SEQUENCE_CATsaveBAT_2.wem
Speech_SEQUENCE_CATsaveBAT_4.wem
Speech_SEQUENCE_CATsaveBAT_6.wem
Speech_SEQUENCE_CATsaveBAT_7.wem
Speech_SEQUENCE_CH0_CATWOMAN_INTRO_1.wem
Speech_SEQUENCE_CH0_CATWOMAN_INTRO_2.wem
Speech_SEQUENCE_CH0_CATWOMAN_OUTRO_3.wem
Speech_SEQUENCE_TWOFACE_END2_1.wem
Speech_SEQUENCE_TWOFACE_END2_2.wem
Speech_SEQUENCE_TWOFACE_END2_5.wem
Speech_SEQUENCE_TWOFACE_END2_7.wem
Speech_SEQUENCE_TWOFACE_END2_10.wem
Speech_SEQUENCE_TWOFACE_END_1.wem
Speech_SEQUENCE_TWOFACE_END_3.wem
Speech_SEQUENCE_TWOFACE_END_4.wem
Speech_SEQUENCE_TWOFACE_END_5.wem
Speech_SIDE_CAT_CH6_cw_ivy_chat_3.wem
Speech_SIDE_CAT_CH6_Dilemma_1.wem
Speech_SIDE_CAT_CH6_Dilemma_2.wem
Speech_SIDE_CAT_CH6_Dilemma_3.wem
Speech_SIDE_CAT_CH6_Dilemma_4.wem
Speech_SIDE_CAT_CH6_vault_1.wem
Speech_SIDE_CAT_CH8b_ivy_chat_1.wem
Speech_SIDE_CAT_CH8b_ivy_chat_2.wem
Speech_SIDE_CAT_CH8b_ivy_chat_3.wem
Speech_SIDE_CAT_CHURCH_LETMEIN_1.wem
Speech_SIDE_CAT_CHURCH_LETMEIN_2.wem
Speech_SIDE_CAT_Get_kit_at_IVY_1.wem
Speech_SIDE_CAT_Get_kit_at_IVY_2.wem
Speech_SIDE_CAT_Get_stuff_1.wem
Speech_SIDE_CAT_Get_thugs_first_1.wem
Speech_SIDE_CAT_MEET_BANE_1.wem
Speech_SIDE_CAT_MEET_BANE_4.wem
Speech_SIDE_CAT_MEET_CM_3.wem
Speech_SIDE_CAT_MEET_CM_4.wem
Speech_SIDE_CAT_MEET_CM_5.wem
Speech_SIDE_CAT_MEET_Freeze_3.wem
Speech_SIDE_CAT_MEET_Freeze_4.wem
Speech_SIDE_CAT_MEET_Freeze_5.wem
Speech_SIDE_CAT_MEET_Hatter_2.wem
Speech_SIDE_CAT_MEET_Hatter_3.wem
Speech_SIDE_CAT_MEET_PENGUIN_1.wem
Speech_SIDE_CAT_MEET_PENGUIN_2.wem
Speech_SIDE_CAT_MEET_PENGUIN_5.wem
Speech_SIDE_CAT_MEET_PENGUIN_6.wem
Speech_SIDE_CAT_Outside_lair_1.wem
Speech_SIDE_CAT_Outside_lair_2.wem
Speech_SIDE_CAT_RIDDLERS_games_1.wem
Speech_SIDE_CAT_RIDDLERS_games_2.wem
Speech_SIDE_CAT_Tunnel_collapse_1.wem
Speech_SIDE_CAT_Underground_shake_1.wem
Speech_SIDE_CAT_Zsasz_room_1.wem

View File

@@ -0,0 +1 @@
10816

View File

@@ -0,0 +1,3 @@
Speech_SIDE_STORY_Killer_Croc_2.wem
Speech_SIDE_STORY_Killer_Croc_4_1.wem
Speech_SIDE_STORY_Killer_Croc_intro_1.wem

View File

@@ -0,0 +1,34 @@
Speech_CHAR_DEADSHOT_BM_under_1.wem
Speech_CHAR_DEADSHOT_BM_under_2.wem
Speech_CHAR_DEADSHOT_BM_under_3.wem
Speech_CHAR_DEADSHOT_Extra_cocky_1.wem
Speech_CHAR_DEADSHOT_Extra_cocky_2.wem
Speech_CHAR_DEADSHOT_Extra_cocky_3.wem
Speech_CHAR_DEADSHOT_In_cover_1.wem
Speech_CHAR_DEADSHOT_In_cover_2.wem
Speech_CHAR_DEADSHOT_In_cover_3.wem
Speech_CHAR_DEADSHOT_In_cover_4.wem
Speech_CHAR_DEADSHOT_In_cover_5.wem
Speech_CHAR_DEADSHOT_In_cover_6.wem
Speech_CHAR_DEADSHOT_In_cover_7.wem
Speech_CHAR_DEADSHOT_Shooting_idles_1.wem
Speech_CHAR_DEADSHOT_Shooting_idles_2.wem
Speech_CHAR_DEADSHOT_Shooting_idles_3.wem
Speech_CHAR_DEADSHOT_Shooting_idles_4.wem
Speech_CHAR_DEADSHOT_Shooting_idles_5.wem
Speech_CHAR_DEADSHOT_Shooting_idles_6.wem
Speech_CHAR_DEADSHOT_Shooting_idles_7.wem
Speech_CHAR_DEADSHOT_Shooting_idles_8.wem
Speech_GAME_OVER_Deadshot_1.wem
Speech_GAME_OVER_Deadshot_2.wem
Speech_GAME_OVER_Deadshot_3.wem
Speech_SIDE_CAT_MEET_Deadshot_1.wem
Speech_SIDE_CAT_MEET_Deadshot_2.wem
Speech_SIDE_CAT_MEET_Deadshot_3.wem
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_1.wem
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_2.wem
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_retreat_1.wem
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_retreat_2.wem
Speech_SIDE_DEADSHOT_DS_TO_BM_Post_capture_1.wem
Speech_SIDE_DEADSHOT_DS_TO_BM_Post_capture_2.wem
Speech_SIDE_DEADSHOT_END_seq_3.wem

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
Speech_CHAR_FREEZE_BMsmashSculpt_1.wem
Speech_CHAR_FREEZE_BMsmashSculpt_2.wem
Speech_CHAR_FREEZE_BMsmashSculpt_3.wem
Speech_CHAR_FREEZE_BMsmashSculpt_4.wem
Speech_CHAR_FREEZE_BMsmashSculpt_5.wem
Speech_CHAR_FREEZE_BMsmashSculpt_6.wem
Speech_CHAR_FREEZE_BMsmashSculpt_7.wem
Speech_CHAR_FREEZE_BMsmashSculpt_8.wem
Speech_CHAR_FREEZE_Destroy_Generic_1.wem
Speech_CHAR_FREEZE_Destroy_Generic_2.wem
Speech_CHAR_FREEZE_Destroy_Generic_3.wem
Speech_CHAR_FREEZE_Destroy_Generic_4.wem
Speech_CHAR_FREEZE_Destroy_Generic_5.wem
Speech_CHAR_FREEZE_Destroy_Generic_6.wem
Speech_CHAR_FREEZE_Destroy_Generic_7.wem
Speech_CHAR_FREEZE_Destroy_Generic_8.wem
Speech_CHAR_FREEZE_Destroy_Generic_9.wem
Speech_CHAR_FREEZE_Destroy_Generic_10.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_1.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_2.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_3.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_4.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_5.wem
Speech_CHAR_FREEZE_DisableDetectiveMode_6.wem
Speech_CHAR_FREEZE_DisableGlideKick_1.wem
Speech_CHAR_FREEZE_DisableGlideKick_2.wem
Speech_CHAR_FREEZE_DisableGlideKick_3.wem
Speech_CHAR_FREEZE_DisableGlideKick_4.wem
Speech_CHAR_FREEZE_FailedSilentTakedown_1.wem
Speech_CHAR_FREEZE_FailedSilentTakedown_2.wem
Speech_CHAR_FREEZE_FailedSilentTakedown_3.wem
Speech_CHAR_FREEZE_FailedSilentTakedown_4.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_1.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_2.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_3.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_4.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_5.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_6.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_7.wem
Speech_CHAR_FREEZE_FailedWeaponJamming_8.wem
Speech_CHAR_FREEZE_FightIntro_1.wem
Speech_CHAR_FREEZE_FightIntro_2.wem
Speech_CHAR_FREEZE_FightIntro_3.wem
Speech_CHAR_FREEZE_FightIntro_4.wem
Speech_CHAR_FREEZE_FindFootprintTrail_1.wem
Speech_CHAR_FREEZE_FindFootprintTrail_2.wem
Speech_CHAR_FREEZE_FindFootprintTrail_3.wem
Speech_CHAR_FREEZE_FindFootprintTrail_4.wem
Speech_CHAR_FREEZE_FindFootprintTrail_5.wem
Speech_CHAR_FREEZE_FindFootprintTrail_6.wem
Speech_CHAR_FREEZE_FindFootprintTrail_7.wem
Speech_CHAR_FREEZE_FindFootprintTrail_8.wem
Speech_CHAR_FREEZE_FireFreezeBeam_1.wem
Speech_CHAR_FREEZE_FireFreezeBeam_2.wem
Speech_CHAR_FREEZE_FireFreezeBeam_3.wem
Speech_CHAR_FREEZE_FireFreezeBeam_4.wem
Speech_CHAR_FREEZE_FireFreezeBeam_5.wem
Speech_CHAR_FREEZE_FireFreezeBeam_6.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_1.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_2.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_3.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_4.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_5.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_6.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_7.wem
Speech_CHAR_FREEZE_FireGrenadeBlind_8.wem
Speech_CHAR_FREEZE_FireGrenadeDirect_1.wem
Speech_CHAR_FREEZE_FireGrenadeDirect_2.wem
Speech_CHAR_FREEZE_FireGrenadeDirect_3.wem
Speech_CHAR_FREEZE_FireGrenadeDirect_4.wem
Speech_CHAR_FREEZE_FireSeekers_1.wem
Speech_CHAR_FREEZE_FireSeekers_2.wem
Speech_CHAR_FREEZE_FireSeekers_3.wem
Speech_CHAR_FREEZE_FireSeekers_4.wem
Speech_CHAR_FREEZE_FreezeSmashedSculpture_1.wem
Speech_CHAR_FREEZE_FreezeSmashedSculpture_2.wem
Speech_CHAR_FREEZE_HearBM_1.wem
Speech_CHAR_FREEZE_HearBM_2.wem
Speech_CHAR_FREEZE_HearBM_3.wem
Speech_CHAR_FREEZE_HearBM_4.wem
Speech_CHAR_FREEZE_HearBM_5.wem
Speech_CHAR_FREEZE_HearBM_6.wem
Speech_CHAR_FREEZE_HearBM_7.wem
Speech_CHAR_FREEZE_HearBM_8.wem
Speech_CHAR_FREEZE_HearLineLauncher_1.wem
Speech_CHAR_FREEZE_HearLineLauncher_2.wem
Speech_CHAR_FREEZE_HearLineLauncher_3.wem
Speech_CHAR_FREEZE_HearLineLauncher_4.wem
Speech_CHAR_FREEZE_HearLineLauncher_5.wem
Speech_CHAR_FREEZE_HearLineLauncher_6.wem
Speech_CHAR_FREEZE_NeutraliseWindow_1.wem
Speech_CHAR_FREEZE_NeutraliseWindow_2.wem
Speech_CHAR_FREEZE_NeutraliseWindow_3.wem
Speech_CHAR_FREEZE_NeutraliseWindow_4.wem
Speech_CHAR_FREEZE_Neutralize_Generic_1.wem
Speech_CHAR_FREEZE_Neutralize_Generic_2.wem
Speech_CHAR_FREEZE_Neutralize_Generic_3.wem
Speech_CHAR_FREEZE_Neutralize_Generic_4.wem
Speech_CHAR_FREEZE_Neutralize_Generic_5.wem
Speech_CHAR_FREEZE_Neutralize_Generic_6.wem
Speech_CHAR_FREEZE_Neutralize_Generic_7.wem
Speech_CHAR_FREEZE_Neutralize_Generic_8.wem
Speech_CHAR_FREEZE_Neutralize_Generic_9.wem
Speech_CHAR_FREEZE_Neutralize_Generic_10.wem
Speech_CHAR_FREEZE_Neutralize_Generic_11.wem
Speech_CHAR_FREEZE_Neutralize_Generic_12.wem
Speech_CHAR_FREEZE_Neutralize_Generic_13.wem
Speech_CHAR_FREEZE_Neutralize_Generic_14.wem
Speech_CHAR_FREEZE_Neutralize_Generic_15.wem
Speech_CHAR_FREEZE_Neutralize_Generic_16.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_1.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_2.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_3.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_4.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_5.wem
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_6.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_1.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_2.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_3.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_4.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_5.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_6.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_7.wem
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_8.wem
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_1.wem
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_2.wem
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_3.wem
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_4.wem
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_1.wem
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_2.wem
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_3.wem
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_4.wem
Speech_CHAR_FREEZE_NoticedWalkingInCircles_1.wem
Speech_CHAR_FREEZE_NoticedWalkingInCircles_2.wem
Speech_CHAR_FREEZE_ReachedEndOfTrail_1.wem
Speech_CHAR_FREEZE_ReachedEndOfTrail_2.wem
Speech_CHAR_FREEZE_ReachedEndOfTrail_3.wem
Speech_CHAR_FREEZE_ReachedEndOfTrail_4.wem
Speech_CHAR_FREEZE_RecogniseSound_1.wem
Speech_CHAR_FREEZE_RecogniseSound_2.wem
Speech_CHAR_FREEZE_RecogniseSound_3.wem
Speech_CHAR_FREEZE_RecogniseSound_4.wem
Speech_CHAR_FREEZE_SeekerDestroyed_1.wem
Speech_CHAR_FREEZE_SeekerDestroyed_2.wem
Speech_CHAR_FREEZE_SeekerDestroyed_3.wem
Speech_CHAR_FREEZE_SeekerDestroyed_4.wem
Speech_CHAR_FREEZE_SeekerDestroyed_5.wem
Speech_CHAR_FREEZE_SeekerDestroyed_6.wem
Speech_CHAR_FREEZE_SeekerDestroyed_7.wem
Speech_CHAR_FREEZE_SeekerDestroyed_8.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_1.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_2.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_3.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_4.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_5.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_6.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_7.wem
Speech_CHAR_FREEZE_SeekerFindsBatman_8.wem
Speech_CHAR_FREEZE_SpotBM_1.wem
Speech_CHAR_FREEZE_SpotBM_2.wem
Speech_CHAR_FREEZE_SpotBM_3.wem
Speech_CHAR_FREEZE_SpotBM_4.wem
Speech_CHAR_FREEZE_SpotBM_5.wem
Speech_CHAR_FREEZE_SpotBM_6.wem
Speech_CHAR_FREEZE_SpotBM_7.wem
Speech_CHAR_FREEZE_SpotBM_8.wem
Speech_CHAR_FREEZE_SpotBM_9.wem
Speech_CHAR_FREEZE_SpotBM_10.wem
Speech_CHAR_FREEZE_SpotBM_11.wem
Speech_CHAR_FREEZE_SpotBM_12.wem
Speech_CHAR_FREEZE_TakeDownReaction_1.wem
Speech_CHAR_FREEZE_TakeDownReaction_2.wem
Speech_CHAR_FREEZE_TakeDownReaction_3.wem
Speech_CHAR_FREEZE_TakeDownReaction_4.wem
Speech_CHAR_FREEZE_TakeDownReaction_5.wem
Speech_CHAR_FREEZE_TakeDownReaction_6.wem
Speech_CHAR_FREEZE_TakeDownReaction_7.wem
Speech_CHAR_FREEZE_TakeDownReaction_8.wem
Speech_CHAR_FREEZE_TakeDownReaction_9.wem
Speech_CHAR_FREEZE_TakeDownReaction_10.wem
Speech_CHAR_FREEZE_TakeDownReaction_11.wem
Speech_CHAR_FREEZE_TakeDownReaction_12.wem
Speech_CHAR_FREEZE_TakeDownReaction_13.wem
Speech_CHAR_FREEZE_TakeDownReaction_14.wem
Speech_CHAR_FREEZE_TakeDownReaction_15.wem
Speech_CHAR_FREEZE_TakeDownReaction_16.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_1.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_2.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_3.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_4.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_5.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_6.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_7.wem
Speech_CHAR_FREEZE_TauntWithoutVisual_8.wem
Speech_CHAR_FREEZE_TauntWithVisual_1.wem
Speech_CHAR_FREEZE_TauntWithVisual_2.wem
Speech_CHAR_FREEZE_TauntWithVisual_3.wem
Speech_CHAR_FREEZE_TauntWithVisual_4.wem
Speech_CHAR_FREEZE_TauntWithVisual_5.wem
Speech_CHAR_FREEZE_TauntWithVisual_6.wem
Speech_CHAR_FREEZE_TauntWithVisual_7.wem
Speech_CHAR_FREEZE_TauntWithVisual_8.wem
Speech_CHAR_FREEZE_WeaponJammingReaction_1.wem
Speech_CHAR_FREEZE_WeaponJammingReaction_2.wem
Speech_GAME_OVER_Mr_Freeze_1.wem
Speech_GAME_OVER_Mr_Freeze_2.wem
Speech_GCPD_Freeze_working_1.wem
Speech_GCPD_Freeze_working_2.wem
Speech_GCPD_Freeze_working_3.wem
Speech_GCPD_Freeze_working_4.wem
Speech_MAP_Freeze_Nora_Map_1.wem
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_1.wem
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_2.wem
Speech_MUSEUM_C1_CH4_Post_Freeze_idle_1.wem
Speech_MUSEUM_C1_CH4_Post_Freeze_idle_2.wem
Speech_MUSEUM_C2_CH4_Find_suit_idles_1.wem
Speech_MUSEUM_C2_CH4_Find_suit_idles_2.wem
Speech_MUSEUM_C2_CH4_Freeze_revisit_1.wem
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_1.wem
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_3.wem
Speech_MUSEUM_C2_CH4_Freeze_revisit_gun_idles_1.wem
Speech_MUSEUM_C2_CH4_Freeze_revisit_gun_idles_2.wem
Speech_SIDE_CAT_MEET_Freeze_1.wem
Speech_SIDE_CAT_MEET_Freeze_2.wem
Speech_SIDE_STORY_Nora_Freeze_1.wem
Speech_SIDE_STORY_Nora_Freeze_2.wem
Speech_SIDE_STORY_Nora_Freeze_3.wem
Speech_SIDE_STORY_Nora_Freeze_BM_1.wem
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_1.wem
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_2.wem
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_3.wem
Speech_SIDE_STORY_Nora_Freeze_idles_1.wem
Speech_SIDE_STORY_Nora_Freeze_idles_2.wem
Speech_SIDE_STORY_NORA_Freeze_post_fight_1.wem
Speech_SIDE_STORY_Nora_GCPD_NoNora_1.wem
Speech_SIDE_STORY_Nora_GCPD_NoNora_2.wem
Speech_SIDE_STORY_Nora_GCPD_Nora_2.wem
Speech_SIDE_STORY_Nora_GCPD_Nora_4.wem

View File

@@ -0,0 +1,2 @@
CHR_RadioScanner_24.wem
CHR_RadioScanner_87.wem

View File

@@ -0,0 +1,54 @@
Speech_CHAR_CATWOMAN_CW2_Round2_over_1.wem
Speech_CHAR_IVY_CW2_PI_Gas_idles_1.wem
Speech_CHAR_IVY_CW2_PI_Gas_idles_2.wem
Speech_CHAR_IVY_CW2_PI_Gas_idles_3.wem
Speech_CHAR_IVY_CW2_PI_Gas_idles_4.wem
Speech_CHAR_IVY_Order_guards_1.wem
Speech_CHAR_IVY_Order_guards_2.wem
Speech_CHAR_IVY_Order_guards_3.wem
Speech_CHAR_IVY_Order_guards_4.wem
Speech_CHAR_IVY_Order_guards_5.wem
Speech_CHAR_IVY_Order_guards_6.wem
Speech_CHAR_IVY_Order_guards_7.wem
Speech_CHAR_IVY_Order_guards_8.wem
Speech_CHAR_IVY_Order_guards_9.wem
Speech_CHAR_IVY_Order_guards_10.wem
Speech_CHAR_IVY_Order_guards_11.wem
Speech_CHAR_IVY_Order_guards_12.wem
Speech_CHAR_IVY_Order_guards_13.wem
Speech_CHAR_IVY_Plant_grabs_1.wem
Speech_CHAR_IVY_Plant_grabs_2.wem
Speech_CHAR_IVY_Plant_grabs_3.wem
Speech_CHAR_IVY_Plant_grabs_4.wem
Speech_CHAR_IVY_Plant_grabs_5.wem
Speech_CHAR_IVY_Plant_grabs_6.wem
Speech_CHAR_IVY_Plant_grabs_7.wem
Speech_GAME_OVER_Poison_Ivy_1.wem
Speech_GAME_OVER_Poison_Ivy_2.wem
Speech_GAME_OVER_Poison_Ivy_3.wem
Speech_GAME_OVER_Poison_Ivy_4.wem
Speech_SEQUENCE_CAT_GOTO_IVY_4.wem
Speech_SEQUENCE_CAT_GOTO_IVY_8.wem
Speech_SEQUENCE_CAT_IVY_1_2.wem
Speech_SEQUENCE_CAT_IVY_1_4.wem
Speech_SEQUENCE_CAT_IVY_2_2.wem
Speech_SEQUENCE_CAT_IVY_2_4.wem
Speech_SEQUENCE_CAT_IVY_2_5.wem
Speech_SEQUENCE_CAT_IVY_2_6.wem
Speech_SIDE_CAT_CH6_cw_ivy_chat_1.wem
Speech_SIDE_CAT_CH6_cw_ivy_chat_2.wem
Speech_SIDE_CAT_CH6_ivy_idles_1.wem
Speech_SIDE_CAT_CH6_ivy_idles_2.wem
Speech_SIDE_CAT_CH8_Ivy_Idles_1.wem
Speech_SIDE_CAT_CH8_Ivy_Idles_2.wem
Speech_SIDE_CAT_CH8_Ivy_Idles_3.wem
Speech_SIDE_CAT_CH8b_ivy_chat_4.wem
Speech_SIDE_CAT_CH8b_ivy_chat_5.wem
Speech_SIDE_CAT_CH8b_ivy_chat_6.wem
Speech_SIDE_CAT_CH8b_ivy_chat_7.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_3.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_4.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_5.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_9.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_11.wem
Speech_SIDE_CAT_Post_CW_IVY_IDLES_12.wem

View File

@@ -0,0 +1,289 @@
Speech_Boss_Clayface_JOKER_1.wem
Speech_Boss_Clayface_JOKER_2.wem
Speech_Boss_Clayface_JOKER_3.wem
Speech_Boss_Clayface_JOKER_4.wem
Speech_Boss_Clayface_JOKER_5.wem
Speech_Boss_Clayface_JOKER_6.wem
Speech_Boss_Clayface_JOKER_BM_gets_sword_1.wem
Speech_Boss_Clayface_JOKER_BM_gets_sword_2.wem
Speech_Boss_Clayface_JOKER_Cutting_CF_1.wem
Speech_Boss_Clayface_JOKER_Detonate_1.wem
Speech_Boss_Clayface_JOKER_Detonate_2.wem
Speech_Boss_Clayface_JOKER_Detonate_3.wem
Speech_Boss_Clayface_JOKER_Detonate_full_1.wem
Speech_CHAR_JOKER_APPROACH_STEEL_PA_1.wem
Speech_CHAR_JOKER_APPROACH_STEEL_PA_2.wem
Speech_CHAR_JOKER_APPROACH_STEEL_PA_3.wem
Speech_CHAR_JOKER_APPROACH_STEEL_PA_4.wem
Speech_CHAR_JOKER_CH5_working_with_freeze_1.wem
Speech_CHAR_JOKER_CH5_working_with_freeze_2.wem
Speech_CHAR_JOKER_CH5_working_with_freeze_3.wem
Speech_CHAR_JOKER_CH6_SteelMill_noentry_1.wem
Speech_CHAR_JOKER_CH6_SteelMill_noentry_2.wem
Speech_CHAR_JOKER_CH6_SteelMill_noentry_3.wem
Speech_CHAR_JOKER_Come_to_theatre_1.wem
Speech_CHAR_JOKER_Come_to_theatre_2.wem
Speech_CHAR_JOKER_Come_to_theatre_3.wem
Speech_CHAR_JOKER_Come_to_theatre_4.wem
Speech_CHAR_JOKER_Come_to_theatre_5.wem
Speech_CHAR_JOKER_Come_to_theatre_6.wem
Speech_CHAR_JOKER_Come_to_theatre_7.wem
Speech_CHAR_JOKER_Leaving_to_Movie_1.wem
Speech_CHAR_JOKER_Prank_Killing_1.wem
Speech_CHAR_JOKER_Prank_Killing_2.wem
Speech_CHAR_JOKER_Prank_Killing_3.wem
Speech_CHURCH_B1_CH2_Joker_countdown_1.wem
Speech_CHURCH_B1_CH2_Joker_countdown_2.wem
Speech_CHURCH_B1_CH2_Joker_countdown_3.wem
Speech_CHURCH_B1_CH2_Joker_countdown_4.wem
Speech_CHURCH_B1_CH2_Joker_countdown_5.wem
Speech_CHURCH_B1_CH2_Joker_countdown_6.wem
Speech_CHURCH_B1_Post_zero_joker_message_1.wem
Speech_CHURCH_B1_Post_zero_joker_message_2.wem
Speech_CHURCH_B1_Post_zero_joker_message_3.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_6.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_16.wem
Speech_CINEMATIC_CH6_BATMAN_CONFRONTS_JOKER_1.wem
Speech_CINEMATIC_CH6_BATMAN_CONFRONTS_JOKER_2.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_2.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_3.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_6.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_8.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_9.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_11.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_12.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_16.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_17.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_19.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_20.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_22.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_23.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_24.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_25.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_29.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_36.wem
Speech_DLC_DLCComb_1_1.wem
Speech_DLC_DLCComb_1_2.wem
Speech_DLC_DLCComb_1_3.wem
Speech_DLC_DLCComb_1_4.wem
Speech_DLC_DLCComb_1_5.wem
Speech_DLC_DLCComb_1_6.wem
Speech_DLC_DLCComb_1_7.wem
Speech_DLC_DLCComb_1_8.wem
Speech_DLC_DLCComb_1_9.wem
Speech_DLC_DLCComb_1_10.wem
Speech_DLC_DLCComb_1_11.wem
Speech_DLC_DLCComb_1_12.wem
Speech_DLC_DLCComb_1_13.wem
Speech_DLC_DLCComb_1_14.wem
Speech_DLC_DLCComb_1_15.wem
Speech_DLC_DLCComb_1_16.wem
Speech_DLC_DLCComb_1_17.wem
Speech_DLC_DLCComb_1_18.wem
Speech_DLC_DLCComb_1_19.wem
Speech_DLC_DLCComb_1_20.wem
Speech_DLC_DLCComb_1_21.wem
Speech_DLC_DLCComb_1_22.wem
Speech_DLC_DLCComb_1_23.wem
Speech_DLC_DLCComb_1_24.wem
Speech_DLC_DLCComb_1_25.wem
Speech_DLC_DLCComb_1_26.wem
Speech_DLC_DLCComb_1_27.wem
Speech_DLC_DLCComb_1_28.wem
Speech_DLC_DLCComb_1_29.wem
Speech_DLC_DLCComb_1_30.wem
Speech_DLC_DLCComb_1_31.wem
Speech_DLC_DLCComb_1_32.wem
Speech_DLC_DLCComb_1_33.wem
Speech_DLC_DLCComb_1_34.wem
Speech_DLC_DLCComb_1_35.wem
Speech_DLC_DLCComb_1_36.wem
Speech_DLC_DLCComb_1_37.wem
Speech_DLC_DLCComb_1_38.wem
Speech_DLC_DLCComb_1_39.wem
Speech_DLC_DLCComb_1_40.wem
Speech_DLC_DLCComb_1_41.wem
Speech_DLC_DLCComb_1_42.wem
Speech_DLC_DLCComb_1_43.wem
Speech_DLC_DLCComb_1_44.wem
Speech_DLC_DLCComb_1_45.wem
Speech_DLC_DLCComb_1_46.wem
Speech_DLC_DLCComb_1_47.wem
Speech_DLC_DLCComb_1_48.wem
Speech_DLC_DLCComb_1_49.wem
Speech_DLC_DLCComb_1_50.wem
Speech_DLC_DLCComb_1_51.wem
Speech_DLC_DLCComb_1_52.wem
Speech_DLC_DLCComb_1_53.wem
Speech_DLC_DLCComb_1_54.wem
Speech_DLC_DLCComb_1_55.wem
Speech_DLC_DLCComb_1_56.wem
Speech_DLC_DLCComb_1_57.wem
Speech_DLC_DLCComb_1_58.wem
Speech_DLC_DLCComb_1_59.wem
Speech_DLC_DLCComb_1_60.wem
Speech_DLC_DLCComb_1_61.wem
Speech_DLC_DLCComb_1_62.wem
Speech_DLC_DLCComb_1_63.wem
Speech_DLC_DLCComb_1_64.wem
Speech_DLC_DLCComb_1_65.wem
Speech_DLC_DLCComb_1_66.wem
Speech_DLC_DLCComb_1_67.wem
Speech_DLC_DLCComb_1_68.wem
Speech_DLC_DLCComb_1_69.wem
Speech_DLC_DLCComb_1_70.wem
Speech_DLC_DLCComb_1_71.wem
Speech_DLC_DLCComb_1_72.wem
Speech_DLC_DLCComb_1_73.wem
Speech_DLC_DLCComb_1_74.wem
Speech_DLC_DLCComb_1_75.wem
Speech_DLC_DLCComb_1_76.wem
Speech_DLC_DLCComb_1_77.wem
Speech_DLC_DLCComb_1_78.wem
Speech_DLC_DLCComb_1_79.wem
Speech_DLC_DLCComb_1_80.wem
Speech_DLC_DLCComb_1_81.wem
Speech_DLC_DLCComb_1_82.wem
Speech_DLC_DLCComb_1_83.wem
Speech_DLC_DLCComb_1_84.wem
Speech_DLC_DLCComb_1_85.wem
Speech_DLC_DLCComb_1_86.wem
Speech_DLC_DLCComb_1_87.wem
Speech_DLC_DLCComb_1_88.wem
Speech_DLC_DLCComb_1_89.wem
Speech_DLC_DLCComb_1_90.wem
Speech_DLC_DLCComb_1_91.wem
Speech_DLC_DLCComb_1_92.wem
Speech_DLC_DLCComb_1_93.wem
Speech_DLC_DLCComb_1_94.wem
Speech_DLC_DLCComb_1_95.wem
Speech_DLC_DLCComb_1_96.wem
Speech_DLC_DLCComb_1_97.wem
Speech_DLC_DLCComb_1_98.wem
Speech_DLC_DLCComb_1_99.wem
Speech_DLC_DLCComb_1_100.wem
Speech_DLC_DLCComb_1_101.wem
Speech_DLC_DLCComb_1_102.wem
Speech_DLC_DLCComb_1_103.wem
Speech_DLC_DLCComb_1_104.wem
Speech_DLC_DLCComb_1_105.wem
Speech_DLC_DLCComb_1_106.wem
Speech_DLC_DLCComb_1_107.wem
Speech_DLC_DLCComb_1_INTRO_cast_1.wem
Speech_DLC_DLCComb_1_INTRO_cast_2.wem
Speech_DLC_DLCComb_1_INTRO_cast_3.wem
Speech_DLC_DLCComb_1_INTRO_cast_4.wem
Speech_DLC_DLCComb_1_INTRO_cast_5.wem
Speech_DLC_DLCComb_1_INTRO_cast_6.wem
Speech_DLC_DLCComb_1_INTRO_cast_7.wem
Speech_DLC_DLCComb_1_INTRO_cast_8.wem
Speech_DLC_DLCComb_1_INTRO_cast_9.wem
Speech_DLC_DLCComb_1_INTRO_cast_10.wem
Speech_DLC_DLCComb_1_INTRO_cast_11.wem
Speech_DLC_DLCComb_1_INTRO_cast_12.wem
Speech_DLC_DLCComb_1_INTRO_cast_13.wem
Speech_DLC_DLCComb_1_INTRO_cast_14.wem
Speech_GAME_OVER_Joker_1.wem
Speech_GAME_OVER_Joker_2.wem
Speech_GAME_OVER_Joker_3.wem
Speech_GAME_OVER_Joker_4.wem
Speech_GAME_OVER_Joker_5.wem
Speech_GAME_OVER_Joker_6.wem
Speech_GAME_OVER_Joker_7.wem
Speech_GAME_OVER_Joker_8.wem
Speech_GAME_OVER_Joker_9.wem
Speech_GAME_OVER_Joker_10.wem
Speech_GAME_OVER_Joker_11.wem
Speech_GAME_OVER_Joker_12.wem
Speech_GAME_OVER_Joker_13.wem
Speech_GAME_OVER_Joker_14.wem
Speech_GAME_OVER_Joker_15.wem
Speech_GAME_OVER_Joker_16.wem
Speech_GAME_OVER_Joker_17.wem
Speech_GCPD_Joker_laugh_1.wem
Speech_GCPD_Joker_laugh_2.wem
Speech_PRED_J_Automated_1.wem
Speech_PRED_J_Automated_2.wem
Speech_PRED_J_Automated_3.wem
Speech_PRED_J_Automated_4.wem
Speech_PRED_J_Automated_5.wem
Speech_PRED_J_Automated_6.wem
Speech_PRED_J_Automated_7.wem
Speech_PRED_J_Automated_8.wem
Speech_PRED_J_Automated_9.wem
Speech_PRED_J_Automated_10.wem
Speech_PRED_J_Automated_11.wem
Speech_PRED_J_Automated_12.wem
Speech_PRED_J_Automated_13.wem
Speech_PRED_J_Automated_14.wem
Speech_PRED_J_Automated_15.wem
Speech_PRED_J_Automated_16.wem
Speech_PRED_J_Automated_17.wem
Speech_PRED_J_Automated_18.wem
Speech_PRED_J_Automated_19.wem
Speech_PRED_J_Automated_20.wem
Speech_PRED_J_Automated_21.wem
Speech_PRED_J_Automated_22.wem
Speech_PRED_J_Automated_23.wem
Speech_PRED_J_Automated_24.wem
Speech_PRED_J_Automated_25.wem
Speech_PRED_J_Automated_26.wem
Speech_PRED_J_Automated_27.wem
Speech_PRED_J_Automated_28.wem
Speech_PRED_J_Automated_29.wem
Speech_PRED_J_Automated_30.wem
Speech_PRED_J_Automated_31.wem
Speech_PRED_J_Automated_32.wem
Speech_PRED_J_Automated_33.wem
Speech_PRED_J_Automated_34.wem
Speech_PRED_J_Automated_35.wem
Speech_PRED_J_Automated_36.wem
Speech_PRED_J_Automated_37.wem
Speech_PRED_J_Automated_38.wem
Speech_PRED_J_Automated_39.wem
Speech_PRED_J_Automated_40.wem
Speech_PRED_J_Automated_41.wem
Speech_PRED_J_Automated_42.wem
Speech_PRED_J_Automated_43.wem
Speech_PRED_J_Automated_44.wem
Speech_PRED_J_Automated_45.wem
Speech_PRED_J_Automated_46.wem
Speech_PRED_J_Automated_47.wem
Speech_PRED_J_Automated_48.wem
Speech_PRED_J_Automated_49.wem
Speech_PRED_J_Automated_50.wem
Speech_PRED_J_CH4_Ending_1.wem
Speech_PRED_J_CH4_Ending_2.wem
Speech_PRED_J_CH4_Ending_3.wem
Speech_PRED_J_CH5_GCPD_END_2.wem
Speech_PRED_J_CH5_GCPD_END_3.wem
Speech_PRED_J_CH6_ending_1.wem
Speech_PRED_J_CH6_ending_2.wem
Speech_PRED_J_CH6_ending_3.wem
Speech_PRED_J_CH6_Steel_ext_end_1.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_1.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_6.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_8.wem
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_9.wem
Speech_SIDE_STORY_VV_saved_conv_1.wem
Speech_SIDE_STORY_VV_saved_conv_8.wem
Speech_SIDE_STORY_VV_saved_conv_9.wem
Speech_SIDE_STORY_VV_Shoot_copter_3.wem
Speech_SIDE_STORY_VV_Shoot_copter_4.wem
Speech_STEEL_B1_CH2_HQ_Intro3_1.wem
Speech_STEEL_B1_CH2_HQ_Muffled_Conversation_2.wem
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_1.wem
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_interrupt_2.wem
Speech_STEEL_B3_Ch6_fight_intro2_1.wem
Speech_STEEL_B3_Ch6_fight_intro2_2.wem
Speech_STEEL_B3_Ch6_fight_intro_1.wem
Speech_STEEL_B3_Ch6_fight_intro_2.wem
Speech_STEEL_B3_CH6_Joker_Lieut_1.wem
Speech_STEEL_B3_CH6_Joker_Lieut_2.wem
Speech_STEEL_B3_CH6_Joker_Titan_1.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_5.wem
Speech_TV_CH2_JK_belfry_1.wem
Speech_TV_CH2_JK_belfry_2.wem
Speech_TV_CH2_JK_belfry_3.wem
Speech_TV_CH2_JK_belfry_4.wem
Speech_TV_CH2_JK_belfry_5.wem

View File

@@ -0,0 +1,40 @@
Speech_GAME_OVER_MadHatter_1.wem
Speech_GAME_OVER_MadHatter_2.wem
Speech_SIDE_CAT_MEET_Hatter_1.wem
Speech_SIDE_CAT_MEET_Hatter_4.wem
Speech_SIDE_MADHATTER_Break_control_1.wem
Speech_SIDE_MADHATTER_Break_control_2.wem
Speech_SIDE_MADHATTER_BREAKHAT_1.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_1.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_2.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_3.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_4.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_5.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_6.wem
Speech_SIDE_MADHATTER_FIGHT_IDLES_7.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_1.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_2.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_3.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_4.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_5.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_6.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_7.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_8.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_9.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_10.wem
Speech_SIDE_MADHATTER_HYNO_IDLES_11.wem
Speech_SIDE_MADHATTER_INTRO1_1.wem
Speech_SIDE_MADHATTER_INTRO1_3.wem
Speech_SIDE_MADHATTER_INTRO1_5.wem
Speech_SIDE_MADHATTER_INTRO1_6.wem
Speech_SIDE_MADHATTER_INTRO2_1.wem
Speech_SIDE_MADHATTER_INTRO3_1.wem
Speech_SIDE_MADHATTER_INTRO4_1.wem
Speech_SIDE_MADHATTER_MY_SOLDIER_1.wem
Speech_SIDE_MADHATTER_MY_SOLDIER_2.wem
Speech_SIDE_MADHATTER_MY_SOLDIER_3.wem
Speech_SIDE_MADHATTER_POST_FIGHT_1.wem
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_1.wem
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_2.wem
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_3.wem
Speech_SIDE_MADHATTER_SOBBING_1.wem

View File

@@ -0,0 +1,83 @@
Speech_CHAR_BATMAN_No_entry_chimney_1.wem
Speech_CHAR_BATMAN_No_entry_chimney_5.wem
Speech_CHAR_BATMAN_No_entry_chimney_6.wem
Speech_GCPD_Boss_intro_1.wem
Speech_MAP_BM_OR_recogniseshot2_map_1.wem
Speech_MAP_BM_OR_recogniseshot2_map_2.wem
Speech_MAP_BM_OR_recogniseshot2_map_3.wem
Speech_MAP_BM_OR_recogniseshot2_map_4.wem
Speech_MAP_BM_OR_recogniseshot_map_1.wem
Speech_MAP_BM_OR_recogniseshot_map_2.wem
Speech_MAP_BM_OR_recogniseshot_map_3.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_4.wem
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_6.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_1.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_2.wem
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_3.wem
Speech_RADIO_SCAN_Secret1_1.wem
Speech_RADIO_SCAN_Secret1_2.wem
Speech_RADIO_SCAN_Secret1_3.wem
Speech_RADIO_SCAN_Secret1_4.wem
Speech_RADIO_SCAN_Secret1_5.wem
Speech_RADIO_SCAN_Secret1FULL_1.wem
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_1.wem
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_5.wem
Speech_RIDDLER_R5_new_finished_oracle_give_location_1.wem
Speech_RIDDLER_R5_new_finished_oracle_give_location_4.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot2_1.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_2.wem
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_3.wem
Speech_SIDE_DEADSHOT_END_seq_2.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_2.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_5.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_3.wem
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_4.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_2.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_5.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_6.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_3.wem
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_4.wem
Speech_SIDE_HUSH_BM_post_hush_OR_1.wem
Speech_SIDE_HUSH_BM_post_hush_OR_2.wem
Speech_SIDE_HUSH_BM_post_hush_OR_6.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_1.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_2.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_3.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_4.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_5.wem
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_7.wem
Speech_SIDE_HUSH_VIC2_SCANPRINT_2.wem
Speech_STEEL_C4_OR_INT_COUNCIL_1_1.wem
Speech_STEEL_C4_OR_INT_COUNCIL_1_3.wem
Speech_STEEL_C4_OR_INT_COUNCIL_1_4.wem
Speech_STEEL_C5_OR_INT_COUNCIL_2_1.wem
Speech_STEEL_C5_OR_INT_COUNCIL_2_3.wem
Speech_STEEL_C5_OR_INT_COUNCIL_2_4.wem
Speech_STREETS_A3_CH7_BM_ALF_Tower_1.wem
Speech_STREETS_A3_CH7_NEW_DILEMMA_4.wem
Speech_STREETS_A4_CH4_BM_Trail_underground_1.wem
Speech_STREETS_A5_CH5_BM_OR_Mayor_1.wem
Speech_STREETS_A5_CH5_BM_OR_Mayor_3.wem
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_1.wem
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_3.wem
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_5.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_4.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_6.wem
Speech_STREETS_A5_CH8_BM_OR_Catchup_8.wem
Speech_UNDER_A1_CH4_Post_BM_OR_1.wem
Speech_UNDER_A1_CH4_Post_BM_OR_2.wem
Speech_UNDER_A1_CH5_BM_NW_1.wem
Speech_UNDER_A1_CH5_BM_NW_2.wem
Speech_UNDER_A1_CH5_BM_NW_6.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_3.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_4.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_8.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_9.wem
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_10.wem
Speech_UNDER_B4_CH7_ORACLE_near_top_1.wem
Speech_UNDER_B4_CH7_ORACLE_near_top_2.wem
Speech_UNDER_C1_CH4_BM_OR_Collapse1_2.wem
Speech_UNDER_C1_CH4_BM_OR_Collapse1_3.wem
Speech_UNDER_C1_CH4_BM_OR_stealguns_2.wem
Speech_UNDER_S1_Sick_Dream_4.wem
Speech_UNDER_S1_Sick_dream_post_2.wem

View File

@@ -0,0 +1,296 @@
CHR_RadioScanner_18.wem
CHR_RadioScanner_55.wem
CHR_RadioScanner_90.wem
Speech_BOSS_GRUNDY_PENGUIN_1.wem
Speech_BOSS_GRUNDY_PENGUIN_2.wem
Speech_BOSS_GRUNDY_PENGUIN_3.wem
Speech_BOSS_GRUNDY_PENGUIN_4.wem
Speech_BOSS_GRUNDY_PENGUIN_5.wem
Speech_BOSS_GRUNDY_PENGUIN_6.wem
Speech_BOSS_GRUNDY_PENGUIN_7.wem
Speech_BOSS_GRUNDY_PENGUIN_8.wem
Speech_BOSS_GRUNDY_PENGUIN_9.wem
Speech_BOSS_GRUNDY_PENGUIN_10.wem
Speech_BOSS_GRUNDY_PENGUIN_11.wem
Speech_BOSS_GRUNDY_PENGUIN_12.wem
Speech_BOSS_GRUNDY_PENGUIN_13.wem
Speech_BOSS_GRUNDY_PENGUIN_14.wem
Speech_BOSS_GRUNDY_PENGUIN_15.wem
Speech_BOSS_GRUNDY_PENGUIN_16.wem
Speech_BOSS_GRUNDY_PENGUIN_17.wem
Speech_BOSS_GRUNDY_PENGUIN_18.wem
Speech_BOSS_GRUNDY_PENGUIN_Flaming_Grundy_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Grundy_is_dead_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Grundy_is_dead_2.wem
Speech_BOSS_GRUNDY_PENGUIN_Grundy_knees_1_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Oi_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Over_charge_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Penguin_rocket_launcher_1.wem
Speech_BOSS_GRUNDY_PENGUIN_Rec_1.wem
Speech_CH_ZERO_CH0_fight_intro2_1.wem
Speech_CH_ZERO_CH0_fight_intro_1.wem
Speech_CH_ZERO_CH0_fight_intro_2.wem
Speech_CH_ZERO_CH0_fight_intro_4.wem
Speech_CH_ZERO_CH0_Peng_scream_1.wem
Speech_CH_ZERO_CH0_Peng_scream_2.wem
Speech_CH_ZERO_CH0_Peng_scream_3.wem
Speech_CH_ZERO_CH0_Penguin_reveal_1.wem
Speech_CH_ZERO_CH0_Penguin_reveal_2.wem
Speech_CH_ZERO_CH0_Thug_Knockout_1.wem
Speech_CH_ZERO_CH0_Thug_Knockout_5.wem
Speech_CH_ZERO_PENG_fightcomments_1.wem
Speech_CH_ZERO_PENG_fightcomments_2.wem
Speech_CH_ZERO_PENG_fightcomments_3.wem
Speech_CH_ZERO_PENG_GATEHELP_1.wem
Speech_CH_ZERO_PENG_GATEHELP_2.wem
Speech_CH_ZERO_PENG_GATEHELP_3.wem
Speech_CH_ZERO_PENG_GATEHELP_4.wem
Speech_CH_ZERO_PENG_GATEHELP_5.wem
Speech_CH_ZERO_PENG_TRASHTALK2_1.wem
Speech_CH_ZERO_PENG_TRASHTALK2_2.wem
Speech_CH_ZERO_PENG_TRASHTALK2_3.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_1.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_2.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_3.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_4.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_5.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_6.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_7.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_8.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_9.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_10.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_12.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_15.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_16.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_17.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_18.wem
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_19.wem
Speech_DLC_DLCComb_2_1.wem
Speech_DLC_DLCComb_2_2.wem
Speech_DLC_DLCComb_2_2.wem
Speech_DLC_DLCComb_2_3.wem
Speech_DLC_DLCComb_2_4.wem
Speech_DLC_DLCComb_2_5.wem
Speech_DLC_DLCComb_2_6.wem
Speech_DLC_DLCComb_2_7.wem
Speech_DLC_DLCComb_2_8.wem
Speech_DLC_DLCComb_2_9.wem
Speech_DLC_DLCComb_2_10.wem
Speech_DLC_DLCComb_2_11.wem
Speech_DLC_DLCComb_2_12.wem
Speech_DLC_DLCComb_2_13.wem
Speech_DLC_DLCComb_2_14.wem
Speech_DLC_DLCComb_2_INTRO_cast_1.wem
Speech_DLC_DLCComb_2_INTRO_cast_2.wem
Speech_DLC_DLCComb_2_INTRO_cast_3.wem
Speech_DLC_DLCComb_2_INTRO_cast_4.wem
Speech_DLC_DLCComb_2_INTRO_cast_5.wem
Speech_DLC_DLCComb_2_INTRO_cast_6.wem
Speech_DLC_DLCComb_2_INTRO_cast_7.wem
Speech_DLC_DLCComb_2_INTRO_cast_8.wem
Speech_DLC_DLCComb_2_INTRO_cast_9.wem
Speech_DLC_DLCComb_2_INTRO_cast_10.wem
Speech_DLC_DLCComb_2_INTRO_cast_11.wem
Speech_DLC_DLCComb_2_INTRO_cast_12.wem
Speech_DLC_DLCComb_2_INTRO_cast_13.wem
Speech_DLC_DLCComb_2_INTRO_cast_14.wem
Speech_GAME_OVER_Penguin_1.wem
Speech_GAME_OVER_Penguin_2.wem
Speech_GAME_OVER_Penguin_3.wem
Speech_GAME_OVER_Penguin_4.wem
Speech_GAME_OVER_Penguin_5.wem
Speech_GCPD_Cackle_1.wem
Speech_GCPD_Cackle_2.wem
Speech_GCPD_Cackle_3.wem
Speech_GCPD_Cackle_4.wem
Speech_GCPD_CH3_PENG_post_int_1.wem
Speech_GCPD_CH3_Penguin_Radio_2.wem
Speech_GCPD_CH3_Penguin_Radio_3.wem
Speech_GCPD_CH3_Penguin_Radio_4.wem
Speech_GCPD_CH3_Thug_intro_radio_3.wem
Speech_GCPD_CH3_thug_P_Lock_1.wem
Speech_GCPD_Radio_intro_1.wem
Speech_GCPD_Radio_intro_2.wem
Speech_GCPD_Radio_intro_3.wem
Speech_INTERROGATION_GCPD_A1_01_1.wem
Speech_MUSEUM_A1_CH3_Intro_2.wem
Speech_MUSEUM_A1_CH3_Intro_4.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_1.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_2.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_3.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_4.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_5.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_6.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_7.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_8.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_1.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_2.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_3.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_4.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_5.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_6.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_7.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_8.wem
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_9.wem
Speech_MUSEUM_B2_CH3_Penguin_Shark_1.wem
Speech_MUSEUM_B2_CH3_Penguin_Shark_2.wem
Speech_MUSEUM_B2_CH3_Penguin_Shark_3.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_1.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_2.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_3.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_4.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_5.wem
Speech_MUSEUM_B2_CH3_Penguin_tort_6.wem
Speech_MUSEUM_C1_CH3_Museum_entry_1.wem
Speech_MUSEUM_C1_CH4_Penguin_1.wem
Speech_MUSEUM_C1_CH4_Penguin_3.wem
Speech_MUSEUM_C1_CH4_Penguin_idle_1.wem
Speech_MUSEUM_C1_CH4_Penguin_idle_2.wem
Speech_MUSEUM_C1_CH4_Penguin_idle_3.wem
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_5.wem
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_6.wem
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_8.wem
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_9.wem
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_1.wem
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_3.wem
Speech_MUSEUM_EXHIBIT_A1_BodyArmor_1.wem
Speech_MUSEUM_EXHIBIT_A1_Freeze_suit_1.wem
Speech_MUSEUM_EXHIBIT_A1_Goggles_1.wem
Speech_MUSEUM_EXHIBIT_A1_Grenades_1.wem
Speech_MUSEUM_EXHIBIT_A1_Jammers_1.wem
Speech_MUSEUM_EXHIBIT_A1_MGuns_1.wem
Speech_MUSEUM_EXHIBIT_A1_Mines_1.wem
Speech_MUSEUM_EXHIBIT_A1_Rockets_1.wem
Speech_MUSEUM_EXHIBIT_A1_SHOTGUN_1.wem
Speech_MUSEUM_EXHIBIT_A1_SNIPER_1.wem
Speech_MUSEUM_EXHIBIT_C1_ASSASSIN_1.wem
Speech_MUSEUM_EXHIBIT_C1_ASSASSIN_2.wem
Speech_MUSEUM_EXHIBIT_C1_BATMAN_1.wem
Speech_MUSEUM_EXHIBIT_C1_BATMAN_2.wem
Speech_MUSEUM_EXHIBIT_C1_Bruce_Wayne_1.wem
Speech_MUSEUM_EXHIBIT_C1_Bruce_Wayne_2.wem
Speech_MUSEUM_EXHIBIT_C1_Joker_1.wem
Speech_MUSEUM_EXHIBIT_C1_scar_1.wem
Speech_MUSEUM_EXHIBIT_C1_SCARFACE_1.wem
Speech_MUSEUM_EXHIBIT_C1_SCARFACE_2.wem
Speech_MUSEUM_EXHIBIT_C1_TWOFACE_1.wem
Speech_MUSEUM_EXHIBIT_C1_TYGER_1.wem
Speech_MUSEUM_EXHIBIT_C1_Zsasz_1.wem
Speech_MUSEUM_EXHIBIT_C1_Zsasz_2.wem
Speech_MUSEUM_EXHIBIT_C2_Freeze_1.wem
Speech_MUSEUM_EXHIBIT_C2_Freeze_2.wem
Speech_MUSEUM_EXHIBIT_C2_Joker_1.wem
Speech_MUSEUM_EXHIBIT_C2_Joker_HQ_1.wem
Speech_MUSEUM_EXHIBIT_C2_Joker_Hyena_1.wem
Speech_MUSEUM_EXHIBIT_C2_Penguin2_1.wem
Speech_MUSEUM_EXHIBIT_C2_Penguin3_1.wem
Speech_MUSEUM_EXHIBIT_C2_Victim_1.wem
Speech_MUSEUM_EXHIBIT_C2_Victim_2.wem
Speech_MUSEUM_S1_CH3_BOSS_PENG_rocketKILL_1.wem
Speech_MUSEUM_S1_CH3_PENG_Enter_1.wem
Speech_MUSEUM_S1_CH3_PENG_Enter_2.wem
Speech_MUSEUM_S1_CH3_PENG_Enter_3.wem
Speech_MUSEUM_S1_CH3_PENG_Enter_4.wem
Speech_MUSEUM_S1_CH3_PENG_Enter_5.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_1.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_2.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_3.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_4.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_5.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_6.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_7.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_8.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_1_9.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_gadget_1.wem
Speech_MUSEUM_S1_CH3_PENG_Firing_gadget_2.wem
Speech_MUSEUM_S1_CH3_PENG_first_enter_1.wem
Speech_MUSEUM_S1_CH3_PENG_first_enter_2.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_1.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_2.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_3.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_4.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_5.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_6.wem
Speech_MUSEUM_S1_CH3_Peng_hits_BM_7.wem
Speech_MUSEUM_S1_CH3_PENG_huh_1.wem
Speech_MUSEUM_S1_CH3_PENG_huh_2.wem
Speech_MUSEUM_S1_CH3_PENG_huh_3.wem
Speech_MUSEUM_S1_CH3_PENG_huh_4.wem
Speech_MUSEUM_S1_CH3_PENG_huh_intro_1.wem
Speech_MUSEUM_S1_CH3_PENG_huh_intro_2.wem
Speech_MUSEUM_S1_CH3_PENG_huh_intro_3.wem
Speech_MUSEUM_S1_CH3_PENG_huh_intro_4.wem
Speech_MUSEUM_S1_CH3_PENG_STOP_1.wem
Speech_MUSEUM_S1_CH3_PENG_STOP_2.wem
Speech_PRED_PENG_Automated_1.wem
Speech_PRED_PENG_Automated_2.wem
Speech_PRED_PENG_Automated_3.wem
Speech_PRED_PENG_Automated_4.wem
Speech_PRED_PENG_Automated_5.wem
Speech_PRED_PENG_Automated_6.wem
Speech_PRED_PENG_Automated_7.wem
Speech_PRED_PENG_Automated_8.wem
Speech_PRED_PENG_Automated_9.wem
Speech_PRED_PENG_Automated_10.wem
Speech_PRED_PENG_Automated_11.wem
Speech_PRED_PENG_Automated_12.wem
Speech_PRED_PENG_Automated_13.wem
Speech_PRED_PENG_Automated_14.wem
Speech_PRED_PENG_Automated_15.wem
Speech_PRED_PENG_Automated_16.wem
Speech_PRED_PENG_Automated_17.wem
Speech_PRED_PENG_Automated_18.wem
Speech_PRED_PENG_Automated_19.wem
Speech_PRED_PENG_Automated_20.wem
Speech_PRED_PENG_Automated_21.wem
Speech_PRED_PENG_Automated_22.wem
Speech_PRED_PENG_Automated_23.wem
Speech_PRED_PENG_Automated_24.wem
Speech_PRED_PENG_Automated_25.wem
Speech_PRED_PENG_Automated_26.wem
Speech_PRED_PENG_Automated_27.wem
Speech_PRED_PENG_Automated_28.wem
Speech_PRED_PENG_Automated_29.wem
Speech_PRED_PENG_Automated_30.wem
Speech_PRED_PENG_Automated_31.wem
Speech_PRED_PENG_Automated_32.wem
Speech_PRED_PENG_Automated_33.wem
Speech_PRED_PENG_Automated_34.wem
Speech_PRED_PENG_Automated_35.wem
Speech_PRED_PENG_Automated_36.wem
Speech_PRED_PENG_Automated_37.wem
Speech_PRED_PENG_Automated_38.wem
Speech_PRED_PENG_Automated_39.wem
Speech_PRED_PENG_Automated_40.wem
Speech_PRED_PENG_Automated_41.wem
Speech_PRED_PENG_Automated_42.wem
Speech_PRED_PENG_Automated_43.wem
Speech_PRED_PENG_Automated_44.wem
Speech_PRED_PENG_CH3_museum_ending_1.wem
Speech_RADIO_SCAN_CH3_PENG_1Jammer_down_1.wem
Speech_RADIO_SCAN_CH3_PENG_1Jammer_down_2.wem
Speech_RADIO_SCAN_CH3_Peng_to_thugs_3.wem
Speech_RADIO_SCAN_CH3_Peng_to_thugs_4.wem
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_1.wem
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_2.wem
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_3.wem
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_1.wem
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_3.wem
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_4.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_2.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_3.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_4.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_5.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_6.wem
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_8.wem
Speech_SIDE_CAT_MEET_PENGUIN_3.wem
Speech_SIDE_CAT_MEET_PENGUIN_4.wem
Speech_STREETS_A2_CH3_Penguin_gloat_1.wem
Speech_STREETS_A2_CH3_Penguin_gloat_2.wem
Speech_STREETS_A2_CH3_Penguin_gloat_3.wem
Speech_STREETS_A2_CH3_Penguin_gloat_4.wem
Speech_STREETS_A2_CH3_Penguin_gloat_5.wem
Speech_STREETS_A2_CH3_Penguin_gloat_6.wem
Speech_STREETS_A2_CH3_Penguin_gloat_7.wem
Speech_STREETS_A2_CH3_Penguin_gloat_8.wem
Speech_STREETS_A5_CH3_PenguinTaunt_1.wem

View File

@@ -0,0 +1,121 @@
Speech_CHAR_HARLEY_End_1.wem
Speech_CHAR_HARLEY_End_2.wem
Speech_CHAR_HARLEY_Harley_heads_1.wem
Speech_CHAR_HARLEY_Harley_heads_2.wem
Speech_CHAR_HARLEY_Harley_heads_3.wem
Speech_CHAR_HARLEY_Harley_heads_4.wem
Speech_CHAR_HARLEY_Harley_heads_5.wem
Speech_CHAR_HARLEY_Harley_not_there_1.wem
Speech_CHAR_HARLEY_Harley_not_there_2.wem
Speech_CHAR_HARLEY_Insult_BM_1.wem
Speech_CHAR_HARLEY_Insult_BM_2.wem
Speech_CHAR_HARLEY_Insult_BM_3.wem
Speech_CHAR_HARLEY_Insult_BM_4.wem
Speech_CHAR_HARLEY_Insult_BM_5.wem
Speech_CHAR_HARLEY_Insult_BM_6.wem
Speech_CHAR_HARLEY_Insult_BM_7.wem
Speech_CHAR_HARLEY_Insult_BM_8.wem
Speech_CHAR_HARLEY_Insult_BM_9.wem
Speech_CHAR_HARLEY_Insult_BM_10.wem
Speech_CHAR_HARLEY_Insult_BM_11.wem
Speech_CHURCH_B1_CH2_HQ_INTRO2_1.wem
Speech_CHURCH_B1_CH2_HQ_INTRO2_2.wem
Speech_CHURCH_B1_CH2_HQ_INTRO2_4.wem
Speech_CHURCH_B1_CH2_HQ_INTRO2_5.wem
Speech_CHURCH_B1_CH2_HQ_intro_1.wem
Speech_CHURCH_B1_Ch2_HQ_scream_1.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_28.wem
Speech_GAME_OVER_Harley_1.wem
Speech_GAME_OVER_Harley_2.wem
Speech_GAME_OVER_Harley_3.wem
Speech_GAME_OVER_Harley_4.wem
Speech_GAME_OVER_Harley_5.wem
Speech_PRED_HQ_Automated_1.wem
Speech_PRED_HQ_Automated_2.wem
Speech_PRED_HQ_Automated_3.wem
Speech_PRED_HQ_Automated_4.wem
Speech_PRED_HQ_Automated_5.wem
Speech_PRED_HQ_Automated_6.wem
Speech_PRED_HQ_Automated_7.wem
Speech_PRED_HQ_Automated_8.wem
Speech_PRED_HQ_Automated_9.wem
Speech_PRED_HQ_Automated_10.wem
Speech_PRED_HQ_Automated_11.wem
Speech_PRED_HQ_Automated_12.wem
Speech_PRED_HQ_Automated_13.wem
Speech_PRED_HQ_Automated_14.wem
Speech_PRED_HQ_Automated_15.wem
Speech_PRED_HQ_Automated_16.wem
Speech_PRED_HQ_Automated_17.wem
Speech_PRED_HQ_Automated_18.wem
Speech_PRED_HQ_Automated_19.wem
Speech_PRED_HQ_Automated_20.wem
Speech_PRED_HQ_Automated_21.wem
Speech_PRED_HQ_Automated_22.wem
Speech_PRED_HQ_Automated_23.wem
Speech_PRED_HQ_Automated_24.wem
Speech_PRED_HQ_Automated_25.wem
Speech_PRED_HQ_Automated_26.wem
Speech_PRED_HQ_Automated_27.wem
Speech_PRED_HQ_Automated_28.wem
Speech_PRED_HQ_Automated_29.wem
Speech_PRED_HQ_Automated_30.wem
Speech_PRED_HQ_Automated_31.wem
Speech_PRED_HQ_Automated_32.wem
Speech_PRED_HQ_Automated_33.wem
Speech_PRED_HQ_Automated_34.wem
Speech_PRED_HQ_Automated_35.wem
Speech_PRED_HQ_Automated_36.wem
Speech_PRED_HQ_Automated_37.wem
Speech_PRED_HQ_CH2_End_1.wem
Speech_PRED_J_CH5_GCPD_END_1.wem
Speech_SEQUENCE_CH2_HARLEY_OFFICE_1.wem
Speech_SEQUENCE_CH2_HARLEY_OFFICE_2.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_7.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_8.wem
Speech_SEQUENCE_CH2_RESCUE_DOC_9.wem
Speech_STEEL_A1_CH2_HQ_DoorWeld_1.wem
Speech_STEEL_A1_CH2_HQ_DoorWeld_2.wem
Speech_STEEL_B1_CH2_FinalFight_HQ_tears_1.wem
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_1.wem
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_2.wem
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_3.wem
Speech_STEEL_B1_CH2_HQ_Dismiss_Crowd_1.wem
Speech_STEEL_B1_CH2_HQ_Dismiss_Crowd_2.wem
Speech_STEEL_B1_CH2_HQ_Intro2_1.wem
Speech_STEEL_B1_CH2_HQ_Intro2_2.wem
Speech_STEEL_B1_CH2_HQ_Intro3_2.wem
Speech_STEEL_B1_CH2_HQ_Intro_1.wem
Speech_STEEL_B1_CH2_HQ_Intro_3.wem
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_1.wem
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_2.wem
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_3.wem
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_4.wem
Speech_STEEL_B1_CH2_HQ_Muffled_Conversation_1.wem
Speech_STEEL_B1_CH2_HQ_Stop_Execution_1.wem
Speech_STEEL_B1_CH2_HQ_Stop_Execution_3.wem
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_2.wem
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_3.wem
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_interrupt_1.wem
Speech_STEEL_C2_CH6_harley_gag1_1.wem
Speech_STEEL_C2_CH6_harley_gag_idle_1.wem
Speech_STEEL_C2_CH6_harley_gag_idle_2.wem
Speech_STEEL_C2_CH6_harley_gag_idle_3.wem
Speech_STEEL_C2_CH6_harley_gag_idle_4.wem
Speech_STEEL_C2_CH6_harley_gag_idle_5.wem
Speech_STEEL_C2_CH6_harley_gag_idle_6.wem
Speech_STEEL_C2_CH6_harley_gag_idle_7.wem
Speech_STEEL_C2_CH6_Harleyregag_1.wem
Speech_STEEL_C2_CH6_Harleyregag_2.wem
Speech_STEEL_C2_CH6_harleyungagged_1.wem
Speech_STEEL_C2_CH6_harleyungagged_2.wem
Speech_STEEL_C2_CH6_harleyungagged_3.wem
Speech_STEEL_C2_CH6_HQ_end_gag_1.wem
Speech_STEEL_C3_CH3_HQ_Power_Off_3.wem
Speech_STEEL_C3_CH3_HQ_Power_Off_4.wem
Speech_STEEL_C3_CH3_HQ_Power_Off_5.wem
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_INTRO_1.wem
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_1.wem
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_2.wem
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_3.wem
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_4.wem

View File

@@ -0,0 +1,100 @@
Speech_CHAR_RAS_Demon_trials_1.wem
Speech_CHAR_RAS_Demon_trials_2.wem
Speech_CHAR_RAS_Demon_trials_3.wem
Speech_CHAR_RAS_Demon_trials_4.wem
Speech_CHAR_RAS_Demon_trials_5.wem
Speech_CHAR_RAS_Demon_trials_6.wem
Speech_CHAR_RAS_Demon_trials_7.wem
Speech_CHAR_RAS_Demon_trials_8.wem
Speech_CHAR_RAS_Demon_trials_9.wem
Speech_CHAR_RAS_Laugh_1.wem
Speech_CHAR_RAS_No_detective_1.wem
Speech_CHAR_RAS_No_detective_2.wem
Speech_CHAR_RAS_No_detective_3.wem
Speech_GAME_OVER_Ras_al_Ghul_1.wem
Speech_GAME_OVER_Ras_al_Ghul_2.wem
Speech_GAME_OVER_Ras_al_Ghul_3.wem
Speech_GAME_OVER_Ras_al_Ghul_4.wem
Speech_GAME_OVER_Ras_al_Ghul_5.wem
Speech_UNDER_C2_CH4_Wonder1_1.wem
Speech_UNDER_C2_CH4_Wonder1_2.wem
Speech_UNDER_C2_CH4_Wonder1_3.wem
Speech_UNDER_C2_CH4_Wonder1_4.wem
Speech_UNDER_C2_CH4_Wonder1_5.wem
Speech_UNDER_C2_CH4_Wonder2_1.wem
Speech_UNDER_C2_CH4_Wonder2_2.wem
Speech_UNDER_C2_CH4_Wonder2_3.wem
Speech_UNDER_C2_CH4_Wonder3_1.wem
Speech_UNDER_C2_CH4_Wonder3_2.wem
Speech_UNDER_C2_CH4_Wonder3_3.wem
Speech_UNDER_C2_CH4_Wonder3_4.wem
Speech_UNDER_S1_Army_of_Ras_1.wem
Speech_UNDER_S1_Army_of_Ras_2.wem
Speech_UNDER_S1_Army_of_Ras_3.wem
Speech_UNDER_S1_Army_of_Ras_4.wem
Speech_UNDER_S1_Army_of_Ras_5.wem
Speech_UNDER_S1_Army_of_Ras_6.wem
Speech_UNDER_S1_Army_of_Ras_7.wem
Speech_UNDER_S1_Army_of_Ras_8.wem
Speech_UNDER_S1_Army_of_Ras_9.wem
Speech_UNDER_S1_Army_of_Ras_10.wem
Speech_UNDER_S1_Army_of_Ras_11.wem
Speech_UNDER_S1_Army_of_Ras_12.wem
Speech_UNDER_S1_Boss_conv2_1.wem
Speech_UNDER_S1_Boss_conv2_4.wem
Speech_UNDER_S1_Boss_intro_1.wem
Speech_UNDER_S1_EMOTE_1.wem
Speech_UNDER_S1_EMOTE_2.wem
Speech_UNDER_S1_EMOTE_3.wem
Speech_UNDER_S1_EMOTE_4.wem
Speech_UNDER_S1_EMOTE_5.wem
Speech_UNDER_S1_EMOTE_6.wem
Speech_UNDER_S1_EMOTE_7.wem
Speech_UNDER_S1_EMOTE_8.wem
Speech_UNDER_S1_EMOTE_9.wem
Speech_UNDER_S1_EMOTE_10.wem
Speech_UNDER_S1_EMOTE_11.wem
Speech_UNDER_S1_EMOTE_12.wem
Speech_UNDER_S1_EMOTE_13.wem
Speech_UNDER_S1_EMOTE_14.wem
Speech_UNDER_S1_EMOTE_15.wem
Speech_UNDER_S1_EMOTE_16.wem
Speech_UNDER_S1_EMOTE_17.wem
Speech_UNDER_S1_EMOTE_18.wem
Speech_UNDER_S1_EMOTE_19.wem
Speech_UNDER_S1_EMOTE_20.wem
Speech_UNDER_S1_EMOTE_21.wem
Speech_UNDER_S1_EMOTE_22.wem
Speech_UNDER_S1_EMOTE_23.wem
Speech_UNDER_S1_EMOTE_24.wem
Speech_UNDER_S1_EMOTE_25.wem
Speech_UNDER_S1_EMOTE_26.wem
Speech_UNDER_S1_EMOTE_27.wem
Speech_UNDER_S1_EMOTE_28.wem
Speech_UNDER_S1_EMOTE_29.wem
Speech_UNDER_S1_EMOTE_30.wem
Speech_UNDER_S1_EMOTE_31.wem
Speech_UNDER_S1_EMOTE_Hit_light_1.wem
Speech_UNDER_S1_EMOTE_Hit_light_2.wem
Speech_UNDER_S1_EMOTE_Hit_light_3.wem
Speech_UNDER_S1_EMOTE_Hit_light_4.wem
Speech_UNDER_S1_EMOTE_Hit_light_5.wem
Speech_UNDER_S1_EMOTE_Hit_light_6.wem
Speech_UNDER_S1_EMOTE_Hit_light_7.wem
Speech_UNDER_S1_EMOTE_Hit_light_8.wem
Speech_UNDER_S1_EMOTE_Hit_light_9.wem
Speech_UNDER_S1_EMOTE_Hit_light_10.wem
Speech_UNDER_S1_GAINT_RAS_Intro_1.wem
Speech_UNDER_S1_Giant_Ras_laugh_1.wem
Speech_UNDER_S1_Giant_Ras_laugh_2.wem
Speech_UNDER_S1_Giant_Ras_laugh_3.wem
Speech_UNDER_S1_Giant_Ras_laugh_4.wem
Speech_UNDER_S1_Giant_Ras_laugh_5.wem
Speech_UNDER_S1_Giant_Ras_laugh_6.wem
Speech_UNDER_S1_Giant_Ras_laugh_7.wem
Speech_UNDER_S1_Giant_Ras_laugh_8.wem
Speech_UNDER_S1_Giant_Ras_taunt1_1.wem
Speech_UNDER_S1_Giant_Ras_taunt2_1.wem
Speech_UNDER_S1_Post_electric_sky_attack_1.wem
Speech_UNDER_S1_Ras_Trials_Complete_1.wem
Speech_UNDER_S1_SwordStruggle1_1.wem

View File

@@ -0,0 +1,353 @@
CHR_RadioScanner_5.wem
CHR_RadioScanner_19.wem
CHR_RadioScanner_42.wem
CHR_RadioScanner_43.wem
CHR_RadioScanner_45.wem
CHR_RadioScanner_48.wem
CHR_RadioScanner_81.wem
CHR_RadioScanner_89.wem
Speech_CHURCH_B1_Informant_outside_1.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_1.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_2.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_4.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_5.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_1.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_2.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_3.wem
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_4.wem
Speech_GAME_OVER_Riddler_1.wem
Speech_GAME_OVER_Riddler_2.wem
Speech_GAME_OVER_Riddler_3.wem
Speech_GAME_OVER_Riddler_4.wem
Speech_GAME_OVER_Riddler_5.wem
Speech_GAME_OVER_Riddler_6.wem
Speech_GAME_OVER_Riddler_7.wem
Speech_GAME_OVER_Riddler_8.wem
Speech_GAME_OVER_Riddler_9.wem
Speech_GAME_OVER_Riddler_10.wem
Speech_GAME_OVER_Riddler_11.wem
Speech_GAME_OVER_Riddler_12.wem
Speech_PRED_RIDDLER_Automated_1.wem
Speech_PRED_RIDDLER_Automated_2.wem
Speech_PRED_RIDDLER_Automated_3.wem
Speech_PRED_RIDDLER_Automated_4.wem
Speech_PRED_RIDDLER_Automated_5.wem
Speech_PRED_RIDDLER_Automated_6.wem
Speech_PRED_RIDDLER_Automated_7.wem
Speech_PRED_RIDDLER_Automated_8.wem
Speech_PRED_RIDDLER_Automated_9.wem
Speech_PRED_RIDDLER_Automated_10.wem
Speech_PRED_RIDDLER_Automated_11.wem
Speech_PRED_RIDDLER_Automated_12.wem
Speech_PRED_RIDDLER_Automated_13.wem
Speech_PRED_RIDDLER_Automated_14.wem
Speech_PRED_RIDDLER_Automated_15.wem
Speech_PRED_RIDDLER_Automated_16.wem
Speech_PRED_RIDDLER_Automated_17.wem
Speech_PRED_RIDDLER_Automated_18.wem
Speech_PRED_RIDDLER_Automated_19.wem
Speech_PRED_RIDDLER_Automated_20.wem
Speech_PRED_RIDDLER_Automated_21.wem
Speech_PRED_RIDDLER_Automated_22.wem
Speech_PRED_RIDDLER_Automated_23.wem
Speech_PRED_RIDDLER_Automated_24.wem
Speech_PRED_RIDDLER_Automated_25.wem
Speech_PRED_RIDDLER_Automated_26.wem
Speech_PRED_RIDDLER_Automated_27.wem
Speech_PRED_RIDDLER_Automated_28.wem
Speech_PRED_RIDDLER_Automated_29.wem
Speech_PRED_RIDDLER_Automated_30.wem
Speech_PRED_RIDDLER_Automated_31.wem
Speech_PRED_RIDDLER_Automated_32.wem
Speech_PRED_RIDDLER_Automated_33.wem
Speech_PRED_RIDDLER_Automated_34.wem
Speech_PRED_RIDDLER_Automated_35.wem
Speech_PRED_RIDDLER_Automated_36.wem
Speech_PRED_RIDDLER_Automated_37.wem
Speech_PRED_RIDDLER_Automated_38.wem
Speech_PRED_RIDDLER_Automated_39.wem
Speech_PRED_RIDDLER_Automated_40.wem
Speech_PRED_RIDDLER_Automated_41.wem
Speech_PRED_RIDDLER_Automated_42.wem
Speech_PRED_RIDDLER_Automated_43.wem
Speech_PRED_RIDDLER_Automated_44.wem
Speech_PRED_RIDDLER_Automated_45.wem
Speech_PRED_RIDDLER_Automated_46.wem
Speech_PRED_RIDDLER_Automated_47.wem
Speech_PRED_RIDDLER_Automated_48.wem
Speech_PRED_RIDDLER_Automated_49.wem
Speech_PRED_RIDDLER_Automated_50.wem
Speech_PRED_RIDDLER_Automated_51.wem
Speech_PRED_RIDDLER_Automated_52.wem
Speech_PRED_RIDDLER_Automated_53.wem
Speech_PRED_RIDDLER_Automated_54.wem
Speech_PRED_RIDDLER_Automated_55.wem
Speech_PRED_RIDDLER_Automated_56.wem
Speech_PRED_RIDDLER_Automated_57.wem
Speech_PRED_RIDDLER_Automated_58.wem
Speech_PRED_RIDDLER_Automated_59.wem
Speech_PRED_RIDDLER_Automated_60.wem
Speech_PRED_RIDDLER_Automated_61.wem
Speech_PRED_RIDDLER_Automated_62.wem
Speech_PRED_RIDDLER_Automated_63.wem
Speech_PRED_RIDDLER_Automated_64.wem
Speech_PRED_RIDDLER_Automated_65.wem
Speech_PRED_RIDDLER_Automated_66.wem
Speech_PRED_RIDDLER_Automated_67.wem
Speech_PRED_RIDDLER_Automated_68.wem
Speech_PRED_RIDDLER_Automated_69.wem
Speech_PRED_RIDDLER_Automated_70.wem
Speech_PRED_RIDDLER_Automated_71.wem
Speech_PRED_RIDDLER_Automated_72.wem
Speech_PRED_RIDDLER_Automated_73.wem
Speech_PRED_RIDDLER_Automated_74.wem
Speech_PRED_RIDDLER_Automated_75.wem
Speech_RIDDLER_Enigma_Cat_trophy_1.wem
Speech_RIDDLER_Enigma_Cat_trophy_2.wem
Speech_RIDDLER_Enigma_Cat_trophy_3.wem
Speech_RIDDLER_Enigma_Control_box_goto_church_1.wem
Speech_RIDDLER_Enigma_Control_box_goto_church_2.wem
Speech_RIDDLER_Enigma_Control_box_goto_church_3.wem
Speech_RIDDLER_Enigma_Control_box_too_soon_1.wem
Speech_RIDDLER_Enigma_Control_box_too_soon_2.wem
Speech_RIDDLER_Enigma_Control_box_too_soon_3.wem
Speech_RIDDLER_Enigma_Fail_1.wem
Speech_RIDDLER_Enigma_Fail_1_1.wem
Speech_RIDDLER_Enigma_Fail_2.wem
Speech_RIDDLER_Enigma_Fail_3.wem
Speech_RIDDLER_Enigma_Fail_4.wem
Speech_RIDDLER_Enigma_Fail_5.wem
Speech_RIDDLER_Enigma_Fail_6.wem
Speech_RIDDLER_Enigma_Fail_7.wem
Speech_RIDDLER_Enigma_Fail_8.wem
Speech_RIDDLER_Enigma_Fail_9.wem
Speech_RIDDLER_Enigma_Fail_10.wem
Speech_RIDDLER_Enigma_Fail_11.wem
Speech_RIDDLER_Enigma_Fail_12.wem
Speech_RIDDLER_Enigma_Fail_13.wem
Speech_RIDDLER_Enigma_Fail_first_1.wem
Speech_RIDDLER_Enigma_Fail_first_2.wem
Speech_RIDDLER_Enigma_Fail_first_3.wem
Speech_RIDDLER_Enigma_Fail_first_4.wem
Speech_RIDDLER_Enigma_Fail_first_5.wem
Speech_RIDDLER_Enigma_Fail_first_6.wem
Speech_RIDDLER_Enigma_Fail_first_7.wem
Speech_RIDDLER_Enigma_Fail_first_playonce_1.wem
Speech_RIDDLER_Enigma_Fail_second_playonce_1.wem
Speech_RIDDLER_Enigma_Fail_second_playonce_2.wem
Speech_RIDDLER_Enigma_Got_enough_trophies_1.wem
Speech_RIDDLER_Enigma_Got_enough_trophies_2.wem
Speech_RIDDLER_Enigma_Got_enough_trophies_3.wem
Speech_RIDDLER_Enigma_Got_enough_trophies_4.wem
Speech_RIDDLER_Enigma_Got_enough_trophies_5.wem
Speech_RIDDLER_Enigma_Hostage_Riddles_1.wem
Speech_RIDDLER_Enigma_Hostage_Riddles_2.wem
Speech_RIDDLER_Enigma_Hostage_Riddles_3.wem
Speech_RIDDLER_Enigma_Hostage_Riddles_4.wem
Speech_RIDDLER_Enigma_Hostage_Riddles_5.wem
Speech_RIDDLER_Enigma_Hostage_taunt_1.wem
Speech_RIDDLER_Enigma_Hostage_taunt_2.wem
Speech_RIDDLER_Enigma_Hostage_taunt_3.wem
Speech_RIDDLER_Enigma_Hostage_taunt_4.wem
Speech_RIDDLER_Enigma_Hostage_taunt_5.wem
Speech_RIDDLER_Enigma_Map_explain_rule_1.wem
Speech_RIDDLER_Enigma_No_go_first_room_remind_1.wem
Speech_RIDDLER_Enigma_No_go_first_room_remind_2.wem
Speech_RIDDLER_Enigma_No_go_first_room_remind_3.wem
Speech_RIDDLER_Enigma_No_go_first_room_remind_4.wem
Speech_RIDDLER_Enigma_Not_enough_1.wem
Speech_RIDDLER_Enigma_Not_enough_2.wem
Speech_RIDDLER_Enigma_Not_enough_3.wem
Speech_RIDDLER_Enigma_Solved_1_1.wem
Speech_RIDDLER_Enigma_Solved_2_1.wem
Speech_RIDDLER_Enigma_Solved_3_1.wem
Speech_RIDDLER_Enigma_Solved_4_1.wem
Speech_RIDDLER_Enigma_Solved_5_1.wem
Speech_RIDDLER_Keith_1.wem
Speech_RIDDLER_Keith_2.wem
Speech_RIDDLER_Poz_rec_temp_1.wem
Speech_RIDDLER_Poz_rec_temp_2.wem
Speech_RIDDLER_Poz_rec_temp_3.wem
Speech_RIDDLER_Poz_rec_temp_4.wem
Speech_RIDDLER_Poz_rec_temp_5.wem
Speech_RIDDLER_R1_BM_enter_room_1.wem
Speech_RIDDLER_R1_Fail_door_1.wem
Speech_RIDDLER_R1_Fail_door_2.wem
Speech_RIDDLER_R1_Hit_switch1_1.wem
Speech_RIDDLER_R1_Hit_switch2_1.wem
Speech_RIDDLER_R1_IDLES_1.wem
Speech_RIDDLER_R1_IDLES_2.wem
Speech_RIDDLER_R1_IDLES_3.wem
Speech_RIDDLER_R1_IDLES_4.wem
Speech_RIDDLER_R1_intro_2.wem
Speech_RIDDLER_R1_intro_3.wem
Speech_RIDDLER_R1_intro_4.wem
Speech_RIDDLER_R1_intro_5.wem
Speech_RIDDLER_R1_post_complete_taunt_1.wem
Speech_RIDDLER_R1_Shocking_BM_1.wem
Speech_RIDDLER_R1_Shocking_BM_2.wem
Speech_RIDDLER_R1_switch_taunts_1.wem
Speech_RIDDLER_R1_switch_taunts_2.wem
Speech_RIDDLER_R1_switch_taunts_3.wem
Speech_RIDDLER_R2_hit_correct_switch_1.wem
Speech_RIDDLER_R2_intro_1.wem
Speech_RIDDLER_R2_intro_2.wem
Speech_RIDDLER_R2_intro_3.wem
Speech_RIDDLER_R2_intro_4.wem
Speech_RIDDLER_R2_post_complete_taunt_1.wem
Speech_RIDDLER_R2_Spin_idles_1.wem
Speech_RIDDLER_R2_Spin_idles_2.wem
Speech_RIDDLER_R2_wait_too_long_1.wem
Speech_RIDDLER_R2_wait_too_long_2.wem
Speech_RIDDLER_R2_wait_too_long_3.wem
Speech_RIDDLER_R2_wrong_1_riddler_1.wem
Speech_RIDDLER_R2_wrong_1_riddler_2.wem
Speech_RIDDLER_R3_new_Intro_1.wem
Speech_RIDDLER_R3_new_Intro_2.wem
Speech_RIDDLER_R3_new_Outro_1.wem
Speech_RIDDLER_R3_new_pressure_1.wem
Speech_RIDDLER_R3_new_pressure_2.wem
Speech_RIDDLER_R3_new_pressure_3.wem
Speech_RIDDLER_R3_new_use_ice_1.wem
Speech_RIDDLER_R4_IDLES_1.wem
Speech_RIDDLER_R4_IDLES_2.wem
Speech_RIDDLER_R4_intro_1.wem
Speech_RIDDLER_R4_intro_2.wem
Speech_RIDDLER_R4_intro_3.wem
Speech_RIDDLER_R4_new_outro_1.wem
Speech_RIDDLER_R4_Scared_idle_1.wem
Speech_RIDDLER_R4_Scared_idle_2.wem
Speech_RIDDLER_R5_new_BM_shocked_1.wem
Speech_RIDDLER_R5_new_BM_shocked_2.wem
Speech_RIDDLER_R5_new_Cannon_Smash_Wall_1.wem
Speech_RIDDLER_R5_new_Cannon_Torment_1.wem
Speech_RIDDLER_R5_new_Cannon_Torment_2.wem
Speech_RIDDLER_R5_new_Cannon_Torment_3.wem
Speech_RIDDLER_R5_new_Hostage_intro_1.wem
Speech_RIDDLER_R5_new_IDLES_1.wem
Speech_RIDDLER_R5_new_IDLES_2.wem
Speech_RIDDLER_R5_new_IDLES_3.wem
Speech_RIDDLER_R5_new_IDLES_4.wem
Speech_RIDDLER_R5_new_IDLES_5.wem
Speech_RIDDLER_R5_new_Intro_1.wem
Speech_RIDDLER_R5_new_Intro_2.wem
Speech_RIDDLER_R5_new_Intro_3.wem
Speech_RIDDLER_R5_new_Shooting_Range_Intro_1.wem
Speech_RIDDLER_R5_new_Wait_For_BM_Solve_Muncher_1.wem
Speech_RIDDLER_R5_new_wait_too_long_1.wem
Speech_RIDDLER_R5_new_wait_too_long_2.wem
Speech_RIDDLER_R5_new_wait_too_long_3.wem
Speech_RIDDLER_R5_outro_1.wem
Speech_RIDDLER_R5_platform_descend_1.wem
Speech_RIDDLER_R6_buttonloops_1.wem
Speech_RIDDLER_R6_buttonloops_2.wem
Speech_RIDDLER_R6_buttonloops_3.wem
Speech_RIDDLER_R6_buttonloops_4.wem
Speech_RIDDLER_R6_buttonloops_5.wem
Speech_RIDDLER_R6_Intro_1.wem
Speech_RIDDLER_R6_Intro_2.wem
Speech_RIDDLER_R6_Intro_3.wem
Speech_RIDDLER_R6_Intro_7.wem
Speech_RIDDLER_R6_Riddler_blocking_1.wem
Speech_RIDDLER_R6_Riddler_blocking_2.wem
Speech_RIDDLER_R6_Riddler_blocking_3.wem
Speech_RIDDLER_R6_Riddler_blocking_4.wem
Speech_RIDDLER_R6_Riddler_button_1.wem
Speech_RIDDLER_R6_Riddler_button_2.wem
Speech_RIDDLER_R6_Riddler_button_3.wem
Speech_RIDDLER_R6_Riddler_button_4.wem
Speech_RIDDLER_R6_Riddler_button_5.wem
Speech_RIDDLER_R6_Riddler_button_6.wem
Speech_RIDDLER_R6_Riddler_idles_chain_1.wem
Speech_RIDDLER_R6_Riddler_idles_chain_2.wem
Speech_RIDDLER_R6_Riddler_idles_chain_3.wem
Speech_RIDDLER_R6_Riddler_idles_chain_4.wem
Speech_RIDDLER_R6_Riddler_idles_chain_5.wem
Speech_RIDDLER_R6_Riddler_idles_chain_6.wem
Speech_RIDDLER_R6_Riddler_idles_chain_7.wem
Speech_RIDDLER_R6_Riddler_mumbles_1.wem
Speech_RIDDLER_R6_Riddler_mumbles_2.wem
Speech_RIDDLER_R6_Riddler_mumbles_3.wem
Speech_RIDDLER_R6_Riddler_mumbles_4.wem
Speech_RIDDLER_R6_Riddler_mumbles_5.wem
Speech_RIDDLER_R6_Riddler_mumbles_6.wem
Speech_RIDDLER_R_Count_down2_1.wem
Speech_RIDDLER_R_Count_down2_2.wem
Speech_RIDDLER_R_Count_down2_3.wem
Speech_RIDDLER_R_Count_down2_4.wem
Speech_RIDDLER_R_Count_down_1.wem
Speech_RIDDLER_R_Count_down_2.wem
Speech_RIDDLER_R_Count_down_3.wem
Speech_RIDDLER_R_Count_down_4.wem
Speech_RIDDLER_R_Switch_1.wem
Speech_RIDDLER_R_Switch_2.wem
Speech_RIDDLER_R_Switch_3.wem
Speech_RIDDLER_R_Tick_1.wem
Speech_RIDDLER_R_Tick_2.wem
Speech_RIDDLER_Secrets_Found_first_1.wem
Speech_RIDDLES_CAMEO_DONOTUSE_1.wem
Speech_RIDDLES_CAMEO_DONOTUSE_2.wem
Speech_RIDDLES_CAMEO_DONOTUSE_3.wem
Speech_RIDDLES_CAMEO_DONOTUSE_4.wem
Speech_RIDDLES_CAMEO_DONOTUSE_5.wem
Speech_RIDDLES_CAMEO_DONOTUSE_6.wem
Speech_RIDDLES_CAMEO_DONOTUSE_7.wem
Speech_RIDDLES_CAMEO_DONOTUSE_8.wem
Speech_RIDDLES_CAMEO_DONOTUSE_9.wem
Speech_RIDDLES_CAMEO_DONOTUSE_10.wem
Speech_RIDDLES_CAMEO_DONOTUSE_11.wem
Speech_RIDDLES_CAMEO_DONOTUSE_12.wem
Speech_RIDDLES_CAMEO_DONOTUSE_13.wem
Speech_RIDDLES_CAMEO_DONOTUSE_14.wem
Speech_RIDDLES_CAMEO_DONOTUSE_15.wem
Speech_RIDDLES_CAMEO_DONOTUSE_16.wem
Speech_RIDDLES_CAMEO_DONOTUSE_17.wem
Speech_RIDDLES_CAMEO_DONOTUSE_18.wem
Speech_RIDDLES_CAMEO_DONOTUSE_19.wem
Speech_RIDDLES_CAMEO_DONOTUSE_20.wem
Speech_RIDDLES_CAMEO_DONOTUSE_21.wem
Speech_RIDDLES_CAMEO_DONOTUSE_22.wem
Speech_RIDDLES_CAMEO_DONOTUSE_23.wem
Speech_RIDDLES_CAMEO_DONOTUSE_24.wem
Speech_RIDDLES_CAMEO_DONOTUSE_25.wem
Speech_RIDDLES_CAMEO_DONOTUSE_26.wem
Speech_RIDDLES_CAMEO_DONOTUSE_27.wem
Speech_RIDDLES_CAMEO_DONOTUSE_28.wem
Speech_RIDDLES_CAMEO_DONOTUSE_29.wem
Speech_RIDDLES_CAMEO_DONOTUSE_30.wem
Speech_RIDDLES_CAMEO_DONOTUSE_31.wem
Speech_RIDDLES_CAMEO_DONOTUSE_32.wem
Speech_RIDDLES_CAMEO_DONOTUSE_33.wem
Speech_RIDDLES_CAMEO_DONOTUSE_34.wem
Speech_RIDDLES_CAMEO_DONOTUSE_35.wem
Speech_RIDDLES_CAMEO_DONOTUSE_36.wem
Speech_RIDDLES_CAMEO_DONOTUSE_37.wem
Speech_RIDDLES_CAMEO_DONOTUSE_38.wem
Speech_RIDDLES_CAMEO_DONOTUSE_39.wem
Speech_RIDDLES_CAMEO_DONOTUSE_40.wem
Speech_RIDDLES_CAMEO_DONOTUSE_41.wem
Speech_RIDDLES_CAMEO_DONOTUSE_42.wem
Speech_RIDDLES_CAMEO_DONOTUSE_43.wem
Speech_RIDDLES_CAMEO_DONOTUSE_44.wem
Speech_RIDDLES_CAMEO_DONOTUSE_45.wem
Speech_RIDDLES_CAMEO_DONOTUSE_46.wem
Speech_RIDDLES_CAMEO_DONOTUSE_47.wem
Speech_RIDDLES_CAMEO_DONOTUSE_48.wem
Speech_RIDDLES_CAMEO_DONOTUSE_49.wem
Speech_RIDDLES_CAMEO_DONOTUSE_50.wem
Speech_RIDDLES_CAMEO_DONOTUSE_51.wem
Speech_RIDDLES_CAMEO_DONOTUSE_52.wem
Speech_RIDDLES_CAMEO_DONOTUSE_53.wem
Speech_RIDDLES_CAMEO_DONOTUSE_54.wem
Speech_RIDDLES_CAMEO_DONOTUSE_55.wem
Speech_RIDDLES_CAMEO_DONOTUSE_56.wem
Speech_RIDDLES_CAMEO_DONOTUSE_57.wem
Speech_RIDDLES_CAMEO_DONOTUSE_58.wem
Speech_RIDDLES_CAMEO_DONOTUSE_59.wem
Speech_RIDDLES_CAMEO_DONOTUSE_60.wem
Speech_RIDDLES_CAMEO_DONOTUSE_61.wem
Speech_RIDDLES_CAMEO_DONOTUSE_62.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_1.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_5.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_6.wem
Speech_SEQUENCE_RIDDLER_CAPTURE_7.wem

View File

@@ -0,0 +1,10 @@
Speech_SEQUENCE_CH4_MEET_ROBIN_3.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_4.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_8.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_9.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_12.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_16.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_17.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_20.wem
Speech_UNDER_A1_CH5_BM_NW_3.wem
Speech_UNDER_A1_CH5_BM_NW_8.wem

View File

@@ -0,0 +1,27 @@
Alfred : 46
Azrael : 10
Bane : 45
Batman : 789
Calendar : 119
Catwoman : 223
Croc : 3
Deadshot : 34
Freeze : 238
Gordon : 2
Ivy : 54
Joker : 289
Madhatter: 40
Oracle : 83
Penguin : 296
Quinn : 121
Ras : 100
Riddler : 353
Robin : 10
Strange : 182
Talia : 50
Twoface : 112
Unknown : 33
Vicky : 51
Zsazz : 90
tot. 3373

View File

@@ -0,0 +1,182 @@
CHR_RadioScanner_2.wem
CHR_RadioScanner_3.wem
CHR_RadioScanner_71.wem
CHR_RadioScanner_79.wem
Speech_CH_ZERO_CH0_close_door_1.wem
Speech_CH_ZERO_CH0_ENCRYPTION_CARD2_2.wem
Speech_CH_ZERO_CH0_strange_guardstoRoom_1.wem
Speech_CH_ZERO_CH0_strange_picks_victim_3.wem
Speech_CH_ZERO_CH0_strange_picks_victim_6.wem
Speech_CH_ZERO_CH0_thug1_heckle_2.wem
Speech_CHAR_STRANGE_CH0_Curfew_1.wem
Speech_CHAR_STRANGE_CH0_Curfew_2.wem
Speech_CHAR_STRANGE_CH0_Curfew_3.wem
Speech_CHAR_STRANGE_CH0_Curfew_4.wem
Speech_CHAR_STRANGE_Ch5_Strange_to_ARK_1.wem
Speech_CHAR_STRANGE_Ch5_Strange_to_ARK_2.wem
Speech_CHAR_STRANGE_CH6_P10_1_1.wem
Speech_CHAR_STRANGE_CH6_P10_1_2.wem
Speech_CHAR_STRANGE_CH6_P10_2_1.wem
Speech_CHAR_STRANGE_CH6_P10_2_2.wem
Speech_CHAR_STRANGE_CH7_Spotted_1.wem
Speech_CHAR_STRANGE_CH7_Spotted_2.wem
Speech_CHAR_STRANGE_PA_looping_1.wem
Speech_CHAR_STRANGE_PA_looping_2.wem
Speech_CHAR_STRANGE_PA_looping_3.wem
Speech_CHAR_STRANGE_PA_looping_4.wem
Speech_CHAR_STRANGE_PA_looping_5.wem
Speech_CHAR_STRANGE_PA_looping_6.wem
Speech_CHAR_STRANGE_PA_looping_7.wem
Speech_CHAR_STRANGE_PA_looping_8.wem
Speech_CHAR_STRANGE_PA_looping_9.wem
Speech_CHAR_STRANGE_PA_looping_10.wem
Speech_CHAR_STRANGE_PA_looping_11.wem
Speech_CHAR_STRANGE_PROTOCOL_count_1.wem
Speech_CHAR_STRANGE_PROTOCOL_count_2.wem
Speech_CHAR_STRANGE_PROTOCOL_count_3.wem
Speech_CHAR_STRANGE_PROTOCOL_count_4.wem
Speech_CHAR_STRANGE_PROTOCOL_count_5.wem
Speech_CHAR_STRANGE_PROTOCOL_count_6.wem
Speech_CHAR_STRANGE_PROTOCOL_count_7.wem
Speech_CHAR_STRANGE_PROTOCOL_count_8.wem
Speech_CHAR_STRANGE_PROTOCOL_count_9.wem
Speech_CHAR_STRANGE_PROTOCOL_count_10.wem
Speech_CHAR_STRANGE_PROTOCOL_count_11.wem
Speech_CHAR_STRANGE_PROTOCOL_count_12.wem
Speech_CHAR_STRANGE_PROTOCOL_count_13.wem
Speech_CHAR_STRANGE_PROTOCOL_count_14.wem
Speech_CHAR_STRANGE_PROTOCOL_count_15.wem
Speech_CHAR_STRANGE_PROTOCOL_count_16.wem
Speech_CHAR_STRANGE_PROTOCOL_count_17.wem
Speech_CHAR_STRANGE_PROTOCOL_count_18.wem
Speech_CHAR_STRANGE_PROTOCOL_count_19.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_5.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_12.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_19.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_21.wem
Speech_GAME_OVER_Hugo_Strange_1.wem
Speech_GAME_OVER_Hugo_Strange_2.wem
Speech_GAME_OVER_Hugo_Strange_3.wem
Speech_GAME_OVER_Hugo_Strange_4.wem
Speech_GAME_OVER_Hugo_Strange_5.wem
Speech_GAME_OVER_Hugo_Strange_6.wem
Speech_GAME_OVER_Hugo_Strange_7.wem
Speech_GAME_OVER_Hugo_Strange_8.wem
Speech_MAP_CH2_TYGER_copter_spots_flare_2.wem
Speech_MAP_CH3_Lost_contact_church_2.wem
Speech_MAP_CH3_Lost_contact_church_4.wem
Speech_MAP_CH3_Tyger_Discovered_Hush_Victim_1_1.wem
Speech_PRED_MAYOR_AUTOMATED_1.wem
Speech_PRED_MAYOR_AUTOMATED_2.wem
Speech_PRED_MAYOR_AUTOMATED_3.wem
Speech_PRED_MAYOR_AUTOMATED_4.wem
Speech_PRED_MAYOR_AUTOMATED_5.wem
Speech_PRED_MAYOR_AUTOMATED_6.wem
Speech_PRED_MAYOR_AUTOMATED_7.wem
Speech_PRED_MAYOR_AUTOMATED_8.wem
Speech_PRED_MAYOR_AUTOMATED_9.wem
Speech_PRED_MAYOR_AUTOMATED_10.wem
Speech_PRED_MAYOR_AUTOMATED_11.wem
Speech_PRED_MAYOR_AUTOMATED_12.wem
Speech_PRED_MAYOR_AUTOMATED_13.wem
Speech_PRED_MAYOR_AUTOMATED_14.wem
Speech_PRED_MAYOR_AUTOMATED_15.wem
Speech_PRED_MAYOR_AUTOMATED_16.wem
Speech_PRED_MAYOR_AUTOMATED_17.wem
Speech_PRED_MAYOR_AUTOMATED_18.wem
Speech_PRED_MAYOR_AUTOMATED_19.wem
Speech_PRED_MAYOR_AUTOMATED_20.wem
Speech_PRED_MAYOR_AUTOMATED_21.wem
Speech_PRED_MAYOR_AUTOMATED_22.wem
Speech_PRED_MAYOR_AUTOMATED_23.wem
Speech_PRED_MAYOR_AUTOMATED_24.wem
Speech_PRED_MAYOR_AUTOMATED_25.wem
Speech_PRED_MAYOR_AUTOMATED_26.wem
Speech_PRED_MAYOR_AUTOMATED_27.wem
Speech_PRED_MAYOR_AUTOMATED_28.wem
Speech_PRED_MAYOR_AUTOMATED_29.wem
Speech_PRED_MAYOR_AUTOMATED_30.wem
Speech_PRED_MAYOR_AUTOMATED_31.wem
Speech_PRED_MAYOR_AUTOMATED_32.wem
Speech_PRED_MAYOR_AUTOMATED_33.wem
Speech_PRED_MAYOR_AUTOMATED_COMMS_back_first_1.wem
Speech_PRED_MAYOR_AUTOMATED_NumDownOne_1.wem
Speech_PRED_MAYOR_AUTOMATED_NumDownThree_1.wem
Speech_PRED_MAYOR_CH7_ending_1.wem
Speech_PRED_MAYOR_CH7_ending_2.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_1.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_2.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_3.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_4.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_5.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_6.wem
Speech_PRED_STRANGE_Batman_in_room_location_unknown_7.wem
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_1.wem
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_2.wem
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_3.wem
Speech_PRED_STRANGE_IDLES_1.wem
Speech_PRED_STRANGE_IDLES_2.wem
Speech_PRED_STRANGE_IDLES_3.wem
Speech_PRED_STRANGE_IDLES_4.wem
Speech_PRED_STRANGE_IDLES_5.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_1.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_2.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_3.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_4.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_5.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_6.wem
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_7.wem
Speech_PRED_STRANGE_See_Batman_1.wem
Speech_PRED_STRANGE_See_Batman_2.wem
Speech_PRED_STRANGE_See_Batman_3.wem
Speech_PRED_STRANGE_See_Batman_4.wem
Speech_PRED_STRANGE_See_Batman_5.wem
Speech_PRED_STRANGE_Strange_to_Tyger_1.wem
Speech_PRED_STRANGE_Strange_to_Tyger_2.wem
Speech_PRED_STRANGE_Strange_to_Tyger_3.wem
Speech_PRED_STRANGE_Strange_to_Tyger_4.wem
Speech_PRED_STRANGE_Strange_to_Tyger_5.wem
Speech_PRED_STRANGE_Strange_to_Tyger_6.wem
Speech_PRED_STRANGE_Strange_to_Tyger_7.wem
Speech_PRED_STRANGE_Strange_to_Tyger_8.wem
Speech_PRED_STRANGE_Taunts_all_taken_down_1.wem
Speech_PRED_STRANGE_Taunts_all_taken_down_2.wem
Speech_PRED_STRANGE_Taunts_all_taken_down_3.wem
Speech_PRED_STRANGE_Taunts_all_taken_down_4.wem
Speech_PRED_STRANGE_Taunts_all_taken_down_5.wem
Speech_RADIO_SCAN_CH3_road_explosion_3.wem
Speech_RADIO_SCAN_CH7_Attack_museum2_1.wem
Speech_RADIO_SCAN_CH7_End_search_2.wem
Speech_RADIO_SCAN_CH7_Fire_at_museum_1.wem
Speech_RADIO_SCAN_CH7_Fire_at_museum_4.wem
Speech_RADIO_SCAN_CH7_search_tower_3.wem
Speech_RADIO_SCAN_CH7_under_speech_1.wem
Speech_RADIO_SCAN_CH7_under_speech_2.wem
Speech_RADIO_SCAN_CH7_under_speech_3.wem
Speech_RADIO_SCAN_FIRST_copter_spot_BM_1.wem
Speech_SEQUENCE_CH7_UNDER_A3_INTRO_1.wem
Speech_SIDE_CAT_Batdown_2.wem
Speech_SIDE_CAT_Batman_Joker_spotted_4.wem
Speech_SIDE_CAT_Batman_Joker_spotted_8.wem
Speech_SIDE_CAT_Ch6_vault_intro_1.wem
Speech_SIDE_CAT_Ch6_vault_intro_2.wem
Speech_SIDE_CAT_Strange_lockdown_1.wem
Speech_SIDE_CAT_Strange_lockdown_2.wem
Speech_STEEL_C5_OR_INT_COUNCIL_2_5.wem
Speech_STREETS_A5_CH7_processing_intro_2.wem
Speech_STREETS_A5_CH7_processing_intro_5.wem
Speech_TV_CH7_Under_C4_fightover_1.wem
Speech_TV_CH7_Under_C4_fightover_2.wem
Speech_TV_CH7_UNDER_C4_STRANGE_1.wem
Speech_TV_CH7_UNDER_C4_STRANGE_2.wem
Speech_TV_CH7_UNDER_C4_STRANGE_3.wem
Speech_TV_CH7_UNDER_C4_STRANGE_4.wem
Speech_TV_CH7_UNDER_C4_STRANGE_5.wem
Speech_TV_CH7_UNDER_C4_STRANGE_6.wem
Speech_TV_CH7_UNDER_C4_STRANGE_7.wem
Speech_TV_CH7_UNDER_C4_STRANGE_8.wem
Speech_TV_CH7_UNDER_C4_STRANGE_9.wem
Speech_TV_CH7_UNDER_C4_STRANGE_10.wem
Speech_UNDER_A1_CH7_elev_STRANGE_1.wem
Speech_UNDER_A1_CH7_elev_STRANGE_2.wem
Speech_UNDER_A1_CH7_elev_STRANGE_3.wem

View File

@@ -0,0 +1,50 @@
Speech_CINEMATIC_CH4_MEET_TALIA_3.wem
Speech_CINEMATIC_CH4_MEET_TALIA_4.wem
Speech_CINEMATIC_CH4_MEET_TALIA_5.wem
Speech_CINEMATIC_CH4_MEET_TALIA_7.wem
Speech_CINEMATIC_CH4_MEET_TALIA_9.wem
Speech_CINEMATIC_CH4_MEET_TALIA_10.wem
Speech_CINEMATIC_CH4_MEET_TALIA_11.wem
Speech_CINEMATIC_CH4_MEET_TALIA_12.wem
Speech_CINEMATIC_CH4_MEET_TALIA_15.wem
Speech_CINEMATIC_CH4_MEET_TALIA_16.wem
Speech_CINEMATIC_CH4_MEET_TALIA_18.wem
Speech_CINEMATIC_CH4_MEET_TALIA_19.wem
Speech_CINEMATIC_CH4_MEET_TALIA_20.wem
Speech_CINEMATIC_CH4_MEET_TALIA_21.wem
Speech_CINEMATIC_CH4_MEET_TALIA_22.wem
Speech_CINEMATIC_CH4_MEET_TALIA_24.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_4.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_7.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_26.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_27.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_30.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_33.wem
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_34.wem
Speech_GAME_OVER_Talia_al_Ghul_1.wem
Speech_GAME_OVER_Talia_al_Ghul_2.wem
Speech_GAME_OVER_Talia_al_Ghul_3.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_1.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_2.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_3.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_5.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_7.wem
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_9.wem
Speech_UNDER_B1_CH4_TALIA_NONE_1.wem
Speech_UNDER_B1_CH4_TALIA_NONE_2.wem
Speech_UNDER_B1_CH4_TALIA_WALK_1.wem
Speech_UNDER_B1_CH4_TALIA_WALK_2.wem
Speech_UNDER_B1_CH4_TALIA_WALK_4.wem
Speech_UNDER_B1_CH4_TALIA_WALK_5.wem
Speech_UNDER_S1_Boss_conv1B_2.wem
Speech_UNDER_S1_Boss_conv1B_3.wem
Speech_UNDER_S1_Boss_conv1B_4.wem
Speech_UNDER_S1_Boss_conv2_2.wem
Speech_UNDER_S1_Boss_conv2_3.wem
Speech_UNDER_S1_Talia_idles_1.wem
Speech_UNDER_S1_Talia_idles_2.wem
Speech_UNDER_S1_Talia_idles_3.wem
Speech_UNDER_S1_Talia_idles_4.wem
Speech_UNDER_S1_Talia_TrialsDone_3.wem
Speech_UNDER_S1_Talia_TrialsDone_4.wem
Speech_UNDER_S1_Talia_TrialsDone_5.wem

View File

@@ -0,0 +1,112 @@
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_4.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_7.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_10.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_23.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_30.wem
Speech_COURT_B1_CH1_TwoFace_1_2.wem
Speech_COURT_B1_CH1_TwoFace_1_3.wem
Speech_COURT_B1_CH1_TwoFace_1_4.wem
Speech_COURT_B1_CH1_TwoFace_1_4.wem
Speech_COURT_B1_CH1_TwoFace_2_1.wem
Speech_COURT_B1_CH1_TwoFace_2_2.wem
Speech_COURT_B1_CH1_TwoFace_2_3.wem
Speech_COURT_B1_CH1_TwoFace_Court_1.wem
Speech_COURT_B1_CH1_TwoFace_Court_2.wem
Speech_COURT_B1_CH1_TwoFace_Court_3.wem
Speech_COURT_B1_CH1_TwoFace_Court_4.wem
Speech_COURT_B1_CH1_TwoFace_Court_5.wem
Speech_COURT_B1_CH1_TwoFace_Court_6.wem
Speech_COURT_B1_CH1_TwoFace_Court_7.wem
Speech_COURT_B1_CH1_TwoFace_Court_8.wem
Speech_COURT_B1_CH1_TwoFace_Court_9.wem
Speech_COURT_B1_CH1_TwoFace_Court_10.wem
Speech_COURT_B1_CH1_TwoFace_Court_11.wem
Speech_COURT_B1_CH1_TwoFace_Court_12.wem
Speech_COURT_B1_CH1_TwoFace_Court_13.wem
Speech_COURT_B1_CH1_TwoFace_Court_14.wem
Speech_COURT_B1_CH1_TwoFace_Court_15.wem
Speech_COURT_B1_CH1_TwoFace_Court_16.wem
Speech_COURT_B1_CH1_TwoFace_Court_17.wem
Speech_COURT_B1_CH1_TwoFace_Court_19.wem
Speech_COURT_B1_CH1_TwoFace_Court_21.wem
Speech_COURT_B1_CH1_TwoFace_Court_22.wem
Speech_COURT_B1_CH1_TwoFace_Court_24.wem
Speech_COURT_B1_CH1_TwoFace_Kill_her_1.wem
Speech_COURT_B1_CH1_TwoFace_Kill_her_2.wem
Speech_COURT_B1_CH1_TwoFace_Kill_her_3.wem
Speech_COURT_B1_Ch1_TwoFace_Order_1.wem
Speech_COURT_B1_Ch1_TwoFace_Order_2.wem
Speech_COURT_B1_Ch1_TwoFace_Order_3.wem
Speech_COURT_B1_Ch1_TwoFace_Order_4.wem
Speech_COURT_B1_Ch1_TwoFace_Order_5.wem
Speech_COURT_B1_Ch1_TwoFace_Order_6.wem
Speech_COURT_B1_Ch1_TwoFace_Order_7.wem
Speech_COURT_B1_Ch1_TwoFace_Order_8.wem
Speech_COURT_B1_Ch1_TwoFace_Order_9.wem
Speech_COURT_B1_Ch1_TwoFace_Order_10.wem
Speech_COURT_B1_Ch1_TwoFace_Order_11.wem
Speech_COURT_B1_Ch1_TwoFace_Order_12.wem
Speech_COURT_B1_CH1_TwoFace_Rand_1.wem
Speech_COURT_B1_CH1_TwoFace_Rand_2.wem
Speech_COURT_B1_CH1_TwoFace_Rand_3.wem
Speech_COURT_B1_CH1_TwoFace_Rand_4.wem
Speech_COURT_B1_CH1_TwoFace_Rand_5.wem
Speech_COURT_B1_Ch2_Batman_spotted_1.wem
Speech_COURT_B1_CH2_TF_Hang_conv_1.wem
Speech_COURT_B1_CH2_TF_Hang_conv_2.wem
Speech_COURT_B1_CH2_TF_Hang_conv_3.wem
Speech_COURT_B1_CH2_TF_HITBM_1.wem
Speech_COURT_B1_CH2_TF_HITBM_2.wem
Speech_COURT_B1_CH2_TF_HITBM_3.wem
Speech_COURT_B1_CH2_TF_HITTHUG_1.wem
Speech_COURT_B1_CH2_TF_HITTHUG_2.wem
Speech_COURT_B1_CH2_TF_HITTHUG_3.wem
Speech_COURT_B1_CH2_TF_Idles_1.wem
Speech_COURT_B1_CH2_TF_Idles_2.wem
Speech_COURT_B1_CH2_TF_Idles_3.wem
Speech_COURT_B1_CH2_TF_MissBM_1.wem
Speech_COURT_B1_CH2_TF_MissBM_2.wem
Speech_COURT_B1_CH2_TF_Shoot_1.wem
Speech_COURT_B1_CH2_TF_Shoot_2.wem
Speech_COURT_B1_CH2_TF_Shoot_3.wem
Speech_COURT_B1_CH2_TF_Shoot_4.wem
Speech_GAME_OVER_Two_Face_1.wem
Speech_GAME_OVER_Two_Face_2.wem
Speech_GAME_OVER_Two_Face_3.wem
Speech_GAME_OVER_Two_Face_4.wem
Speech_GAME_OVER_Two_Face_5.wem
Speech_GAME_OVER_Two_Face_6.wem
Speech_GAME_OVER_Two_Face_7.wem
Speech_GAME_OVER_Two_Face_8.wem
Speech_GAME_OVER_Two_Face_9.wem
Speech_SEQUENCE_TWOFACE_END2_3.wem
Speech_SEQUENCE_TWOFACE_END2_4.wem
Speech_SEQUENCE_TWOFACE_END2_8.wem
Speech_SEQUENCE_TWOFACE_END_2.wem
Speech_SEQUENCE_TWOFACE_END_6.wem
Speech_SIDE_CAT_TF_Backup_1.wem
Speech_SIDE_CAT_TF_Backup_2.wem
Speech_SIDE_CAT_TF_Backup_3.wem
Speech_SIDE_CAT_TF_fire_1.wem
Speech_SIDE_CAT_TF_fire_2.wem
Speech_SIDE_CAT_TF_fire_3.wem
Speech_SIDE_CAT_TF_fire_4.wem
Speech_SIDE_CAT_TF_fire_5.wem
Speech_SIDE_CAT_TF_fire_6.wem
Speech_SIDE_CAT_TF_intro1_1.wem
Speech_SIDE_CAT_TF_intro1_2.wem
Speech_SIDE_CAT_TF_intro1_3.wem
Speech_SIDE_CAT_TF_intro1_4.wem
Speech_SIDE_CAT_TF_intro1_5.wem
Speech_SIDE_CAT_TF_intro1_6.wem
Speech_SIDE_CAT_TF_intro1_7.wem
Speech_SIDE_CAT_TF_look_for_her_1.wem
Speech_SIDE_CAT_TF_look_for_her_2.wem
Speech_SIDE_CAT_TF_look_for_her_3.wem
Speech_SIDE_CAT_TF_look_for_her_4.wem
Speech_SIDE_CAT_TF_look_for_her_5.wem
Speech_SIDE_CAT_TF_shes_here_1.wem
Speech_SIDE_CAT_TF_shes_here_2.wem
Speech_SIDE_CAT_TF_shes_here_3.wem
Speech_SIDE_CAT_TF_shes_here_4.wem
Speech_SIDE_CAT_TF_shes_here_5.wem

View File

@@ -0,0 +1,33 @@
Speech_CH_ZERO_CH0_thug1_heckle_1.wem
Speech_CH_ZERO_CH0_thug1_heckle_3.wem
Speech_CH_ZERO_CH0_thug_ryder_1.wem
Speech_CH_ZERO_CH0_thug_ryder_2.wem
Speech_CH_ZERO_CH0_thug_ryder_3.wem
Speech_CH_ZERO_CH0_thug_ryder_4.wem
Speech_CH_ZERO_CH0_thug_ryder_5.wem
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_21.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_1.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_3.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_8.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_9.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_10.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_11.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_20.wem
Speech_COURT_B1_CH1_TwoFace_1_3.wem
Speech_COURT_B1_CH1_TwoFace_1_4.wem
Speech_COURT_B1_CH1_TwoFace_2_1.wem
Speech_COURT_B1_CH1_TwoFace_2_2.wem
Speech_COURT_B1_CH1_TwoFace_2_3.wem
Speech_COURT_B1_CH1_TwoFace_Court_18.wem
Speech_INTERROGATION_Riddler_06_2.wem
Speech_SEQUENCE_CH4_MEET_ROBIN_5.wem
Speech_SEQUENCE_TWOFACE_END2_9.wem
Speech_SIDE_BANE_Post_intro_idles_1.wem
Speech_SIDE_BANE_Post_intro_idles_2.wem
Speech_SIDE_BANE_Post_intro_idles_3.wem
Speech_UNDER_C4_CH7_Batman_lose_1.wem
Speech_UNDER_C4_CH7_Batman_lose_2.wem
Speech_UNDER_C4_CH7_Batman_lose_3.wem
Speech_UNDER_C4_CH7_Batman_Wins_1.wem
Speech_UNDER_C4_CH7_Batman_Wins_2.wem
Speech_UNDER_C4_CH7_Batman_Wins_3.wem

View File

@@ -0,0 +1,51 @@
CHR_RadioScanner_4.wem
CHR_RadioScanner_13.wem
CHR_RadioScanner_17.wem
CHR_RadioScanner_20.wem
CHR_RadioScanner_27.wem
CHR_RadioScanner_37.wem
CHR_RadioScanner_39.wem
CHR_RadioScanner_51.wem
CHR_RadioScanner_58.wem
CHR_RadioScanner_60.wem
CHR_RadioScanner_65.wem
CHR_RadioScanner_76.wem
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_3.wem
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_5.wem
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_6.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_7.wem
Speech_CINEMATIC_SPECIAL_NEW_INTRO_15.wem
Speech_RADIO_SCAN_CH1_VV_1.wem
Speech_RADIO_SCAN_CH1_VV_2.wem
Speech_RADIO_SCAN_CH1_VV_3.wem
Speech_RADIO_SCAN_CH1_VV_4.wem
Speech_RADIO_SCAN_CH6_VV_BM_Comment_1.wem
Speech_RADIO_SCAN_CH6_VV_BM_Comment_2.wem
Speech_RADIO_SCAN_CH6_VV_BM_Comment_3.wem
Speech_RADIO_SCAN_CH6_VV_BM_Comment_4.wem
Speech_RADIO_SCAN_CH6_VV_BM_Losing_1.wem
Speech_RADIO_SCAN_CH6_VV_BM_Losing_2.wem
Speech_RADIO_SCAN_CH6_VV_BM_Losing_3.wem
Speech_RADIO_SCAN_CH6_VV_BM_Winning_1.wem
Speech_RADIO_SCAN_CH6_VV_BM_Winning_2.wem
Speech_RADIO_SCAN_CH6_VV_BM_Winning_3.wem
Speech_RADIO_SCAN_CH6_VV_SniperIdles_1.wem
Speech_RADIO_SCAN_CH6_VV_SniperIdles_2.wem
Speech_RADIO_SCAN_CH6_VV_SniperIdles_3.wem
Speech_RADIO_SCAN_CH6_VV_SniperIdles_4.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_1.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_2.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_3.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_4.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_5.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_6.wem
Speech_SIDE_STORY_VV_post_rescued_big_speech_7.wem
Speech_SIDE_STORY_VV_pre_rescue_snipers_2.wem
Speech_SIDE_STORY_VV_saved_conv_5.wem
Speech_SIDE_STORY_VV_saved_conv_6.wem
Speech_SIDE_STORY_VV_Saved_idles_1.wem
Speech_SIDE_STORY_VV_Saved_idles_2.wem
Speech_SIDE_STORY_VV_Saved_idles_3.wem
Speech_SIDE_STORY_VV_Saved_idles_4.wem
Speech_SIDE_STORY_VV_Shoot_copter_1.wem
Speech_SIDE_STORY_VV_Shoot_copter_2.wem

View File

@@ -0,0 +1,90 @@
Speech_CHAR_ZSASZ_CALL1GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL1GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL2GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL2GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL2GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL2GLOAT_5.wem
Speech_CHAR_ZSASZ_CALL3GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL3GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL3GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL3GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL4GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL4GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL4GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL4GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL5GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL5GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL5GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL6GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL6GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL6GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL6GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL7GLOAT_1.wem
Speech_CHAR_ZSASZ_CALL7GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL7GLOAT_3.wem
Speech_CHAR_ZSASZ_CALL8GLOAT_2.wem
Speech_CHAR_ZSASZ_CALL8GLOAT_4.wem
Speech_CHAR_ZSASZ_CALL8GLOAT_5.wem
Speech_CHAR_ZSASZ_Find_me_1.wem
Speech_CHAR_ZSASZ_Find_me_2.wem
Speech_CHAR_ZSASZ_Find_me_3.wem
Speech_CHAR_ZSASZ_Find_me_4.wem
Speech_CHAR_ZSASZ_Find_me_5.wem
Speech_CHAR_ZSASZ_Find_me_6.wem
Speech_CHAR_ZSASZ_Find_me_7.wem
Speech_CHAR_ZSASZ_Find_me_8.wem
Speech_CHAR_ZSASZ_Find_me_9.wem
Speech_CHAR_ZSASZ_Find_me_10.wem
Speech_CHAR_ZSASZ_Find_me_first_4.wem
Speech_CHAR_ZSASZ_Find_me_first_5.wem
Speech_CHAR_ZSASZ_Find_me_first_6.wem
Speech_CHAR_ZSASZ_Find_me_first_7.wem
Speech_CHAR_ZSASZ_Find_me_first_8.wem
Speech_CHAR_ZSASZ_Tease_1.wem
Speech_CHAR_ZSASZ_Tease_3.wem
Speech_GAME_OVER_Zsasz_1.wem
Speech_GAME_OVER_Zsasz_2.wem
Speech_GAME_OVER_Zsasz_3.wem
Speech_GAME_OVER_Zsasz_4.wem
Speech_GAME_OVER_Zsasz_5.wem
Speech_SIDE_STORY_Zsasz_BM_fail_1.wem
Speech_SIDE_STORY_Zsasz_BM_fail_2.wem
Speech_SIDE_STORY_Zsasz_BM_fail_3.wem
Speech_SIDE_STORY_Zsasz_BM_fail_4.wem
Speech_SIDE_STORY_Zsasz_cage_idles_1.wem
Speech_SIDE_STORY_Zsasz_cage_idles_2.wem
Speech_SIDE_STORY_Zsasz_cage_idles_3.wem
Speech_SIDE_STORY_Zsasz_call_BM_1.wem
Speech_SIDE_STORY_Zsasz_call_BM_2.wem
Speech_SIDE_STORY_Zsasz_call_BM_3.wem
Speech_SIDE_STORY_Zsasz_call_BM_4.wem
Speech_SIDE_STORY_Zsasz_call_BM_5.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_1.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_2.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_3.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_4.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_5.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_6.wem
Speech_SIDE_STORY_Zsasz_call_BM_idles_7.wem
Speech_SIDE_STORY_Zsasz_idles_1.wem
Speech_SIDE_STORY_Zsasz_idles_2.wem
Speech_SIDE_STORY_Zsasz_idles_3.wem
Speech_SIDE_STORY_Zsasz_idles_4.wem
Speech_SIDE_STORY_Zsasz_idles_5.wem
Speech_SIDE_STORY_Zsasz_idles_6.wem
Speech_SIDE_STORY_Zsasz_idles_7.wem
Speech_SIDE_STORY_Zsasz_idles_8.wem
Speech_SIDE_STORY_Zsasz_idles_9.wem
Speech_SIDE_STORY_Zsasz_idles_10.wem
Speech_SIDE_STORY_Zsasz_idles_11.wem
Speech_SIDE_STORY_Zsasz_idles_12.wem
Speech_SIDE_STORY_Zsasz_idles_13.wem
Speech_SIDE_STORY_Zsasz_idles_14.wem
Speech_SIDE_STORY_Zsasz_idles_15.wem
Speech_SIDE_STORY_Zsasz_idles_16.wem
Speech_SIDE_STORY_Zsasz_idles_17.wem
Speech_SIDE_STORY_Zsasz_intro_1.wem
Speech_SIDE_STORY_Zsasz_intro_2.wem
Speech_SIDE_STORY_ZSASZ_SEEBM_1.wem
Speech_SIDE_STORY_Zsasz_Z_Caught_1.wem
Speech_SIDE_STORY_Zsasz_Z_Caught_2.wem

14
docs/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Documentation
| document | content |
|----------|---------|
| [user-guide.md](user-guide.md) | installing and using Unheard City (screenshots) |
| [decisions.md](decisions.md) | stack, dependencies/licenses, originals store, size rule, trade-offs |
| [categories.md](categories.md) | group/category scheme and character fields |
| [building.md](building.md) | building the editor and its installer (Windows, Linux) |
| [formats/wad.md](formats/wad.md) | WWAD archives, grouped entries, BKPK stream packs |
| [formats/wem.md](formats/wem.md) | WEM layout the game accepts, setup stripping, hash and decode-alloc formulas |
| [reverse-engineering/decoder.md](reverse-engineering/decoder.md) | BatmanAC.exe decoder functions and addresses |
| [reverse-engineering/findings.md](reverse-engineering/findings.md) | root cause, findings, in-game experiment log (incl. failed hypotheses) |
| [../data/catalog/README.md](../data/catalog/README.md) | catalog CSV schema, adding languages |
| [history/](history/) | the two mission briefs this project was built from |

55
docs/building.md Normal file
View File

@@ -0,0 +1,55 @@
# Building Unheard City
The build is a PyInstaller **one-folder** bundle: Python, Qt (incl. Qt Multimedia), libsndfile (libvorbis,
FLAC, opus, mpg123) and soxr are all inside; nothing else is installed or needed at runtime. Build on each
target OS (no cross-compiling).
## Windows (x86-64) - tested
```
py -3.14 -m venv .venv (Python 3.10-3.14)
.venv\Scripts\python -m pip install -e .[app,dev]
.venv\Scripts\python -m pytest -q
.venv\Scripts\python tools\build_app.py
```
That one command does everything, offline. If `app/assets/` (git-ignored icon and wallpaper) is missing, it is
made from `bac_logo_full.png` and `wallpaper.jpg` in `Downloads\Arkham City` (or `--art FOLDER`); without those
images the build still works, with a plain icon and no wallpaper.
Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity-<ver>-windows-amd64.zip`
(~78 MB zip, ~190 MB unpacked). The build also writes `THIRD_PARTY_LICENSES.txt` (license texts come from the
wheels and from `tools/licenses/`).
**Installer.** On Windows the folder (and zip) also contains `Install.cmd`, `install.ps1` and `uninstall.ps1`
(sources in `tools/installer/`, version filled in by the build). It is a plain PowerShell installer, so no
installer tool is needed to build it: per-user install to `%LOCALAPPDATA%\Programs\UnheardCity`, Start menu and
optional desktop shortcut, an uninstall entry in Settings > Apps (`HKCU`), upgrade in place. User data in
`%APPDATA%\UnheardCity` is kept on uninstall unless the user chooses otherwise. Unattended:
`install.ps1 -Quiet -Desktop no [-Target DIR]`, `uninstall.ps1 -Quiet`.
Verify the bundle is self-contained (PATH stripped to Windows folders):
```
set PATH=C:\Windows\System32;C:\Windows
dist\UnheardCity\UnheardCity.exe --smoke-test --report smoke.txt & type smoke.txt
dist\UnheardCity\UnheardCity.exe --quit-after 8 --screenshot shot.png
```
## Linux (x86-64) - procedure (not executed on the development machine)
Use the oldest distribution you want to support (glibc compatibility), e.g. Ubuntu 22.04:
```
sudo apt install python3 python3-venv libxcb-cursor0 libxkbcommon-x11-0 libegl1 libpulse0
python3 -m venv .venv && . .venv/bin/activate
pip install -e .[app,dev]
QT_QPA_PLATFORM=offscreen pytest -q
python tools/build_app.py # -> dist/UnheardCity-<ver>-linux-x86_64.tar.gz
QT_QPA_PLATFORM=offscreen dist/UnheardCity/UnheardCity --smoke-test --game /path/to/game
```
Notes:
- The `soundfile` and `soxr` wheels for manylinux bundle libsndfile/libsoxr; the PySide6 wheels bundle Qt and
Qt Multimedia's FFmpeg backend (PulseAudio/PipeWire output). The target desktop only needs the usual X11/
Wayland and PulseAudio/PipeWire client libraries that every desktop distribution has.
- Game folders under Proton are found automatically in `~/.steam/steam`, `~/.local/share/Steam`, Flatpak
(`~/.var/app/com.valvesoftware.Steam/...`) and Snap Steam libraries, including extra libraries from
`libraryfolders.vdf`. The game language is read from the Proton prefix
(`steamapps/compatdata/200260/pfx/drive_c/users/steamuser/Documents/WB Games/...`).
- The in-game test harness (`ackit verify --ingame`) is Windows-only; everything else is portable.
A GitHub Actions workflow that builds both platforms is in `.github/workflows/build.yml`.

64
docs/categories.md Normal file
View File

@@ -0,0 +1,64 @@
# Categories and characters
Implemented in `ackit/classify.py`; used both when building the shipped catalog (`tools/build_catalog.py`)
and when a game build is not in the catalog (runtime scan), so every language/build gets the same scheme.
## Groups and categories
Every sound has a **group** and a finer **category**, derived from its Wwise name (from the community hash
list; unnamed IDs show as hex). First matching rule wins:
| name starts with | group | category |
|------------------|-------|----------|
| `CHR_RadioScanner`, `PSO_CarRadio`, `Speech_RADIO`, `Speech_TV`, `Speech_CAMERAS`, `DIA_` | Speech | Radio & TV |
| `Speech_TAPE_` | Speech | Interview tapes |
| `Speech_INTERROGATION_` | Speech | Interrogations |
| `Speech_CINEMATIC_`, `Speech_New_` | Speech | Cinematics |
| `Speech_Boss_` / `Speech_BOSS_` | Speech | Boss fights |
| `Speech_RIDDLER_`, `Speech_RIDDLES_` | Speech | Riddler |
| `Speech_SIDE_` | Speech | Side missions |
| `Speech_DLC_` | Speech | DLC |
| `Speech_PRED_` | Speech | Predator chatter |
| `Speech_COMBAT_`, `Speech_Bully_` | Speech | Combat barks |
| `Speech_EMOTE_` | Speech | Efforts & emotes |
| `Speech_REACT_` | Speech | Reactions |
| `Speech_STREETS_`, `Speech_ZONES_`, `Speech_OUTCAST_`, `Speech_Vertical_` | Speech | Open-world chatter |
| `Speech_CHAR_` | Speech | Character lines |
| `Speech_CH_`, `Speech_SEQUENCE_`, `Speech_STEEL_`, `_MUSEUM_`, `_CHURCH_`, `_COURT_`, `_UNDER_`, `_GCPD_`, `_ZP1_` | Speech | Story missions |
| `Speech_GAME_`, `Speech_MAP_`, `Speech_AUGMENTED_`, `Speech_FORENSICS_`, `Speech_TEMP_` | Speech | Gameplay & hints |
| `NPC_` | Ambience | Crowds |
| other `Speech_` | Speech | Other speech |
| media of the `Music` / `Music_PDLC` stream packs, `Music*` | Music | Music / Music (DLC) |
| `AMB_`, `PSA_` | Ambience | Ambience |
| `WND_` | Ambience | Wind |
| `VOL_`, `PVOL_` | Ambience | Room tone |
| `SEQ_` | SFX | Scripted sequences |
| `OBJ_` | SFX | Objects |
| `WPN_` | SFX | Weapons |
| `CHR_` (in SFX) | SFX | Character foley |
| `Global` | SFX | Global |
| anything else (e.g. unnamed hex IDs) | Unknown | Unknown |
Counts in the ITA catalog: Story missions 1993, Predator chatter 1796, Character lines 1537, Open-world chatter
1295, Side missions 808, Combat barks 801, Efforts & emotes 616, Interview tapes 514, Riddler 360, Radio & TV 226,
Gameplay & hints 199, DLC 167, Boss fights 150, Cinematics 150, Reactions 108, Interrogations 95. SFX archive:
Music 389, Ambience 225, SFX 31, Unknown 6.
## Characters
Two separate fields, because Wwise names describe a *scene*, not the speaker of each variation (a conversation
alternates speakers between `NAME_1`, `NAME_2`, ...):
- **character** - the verified speaker, only from the curated lists in `data/catalog/source/<lang>/*.txt`
(one file per character, listening-based). Mapped by sound name, so labels made on the Italian files apply to
every language (the five language WADs have identical names and variation counts). Display names:
Calendar -> Calendar Man, Croc -> Killer Croc, Freeze -> Mr. Freeze, Ivy -> Poison Ivy, Madhatter -> Mad
Hatter, Quinn -> Harley Quinn, Ras -> Ra's al Ghul, Strange -> Hugo Strange, Talia -> Talia al Ghul,
Twoface -> Two-Face, Vicky -> Vicki Vale, Zsazz -> Victor Zsasz, Unknown -> Unknown (speaker checked but not
identified). A line in two lists gets both ("A; B").
- **characters_inferred** - characters and roles *mentioned by the name* (token rules in `classify._TOKENS`,
e.g. `BM`/`BATMAN` -> Batman, `PENG` -> Penguin, `TF` -> Two-Face, `THUG*`/`GENTHUG*` -> Thug, `TYGER` ->
TYGER guard; interview tapes add Hugo Strange). A hint for the ~7,500 lines without a verified speaker,
never shown as the speaker. In the editor the character filter matches verified speakers, and optionally
inferred ones.
Coverage (every language): 3,366 sounds with a verified speaker, 5,930 more with inferred characters, 1,520 with
neither (mostly predator/efforts from unnamed thugs).

107
docs/decisions.md Normal file
View File

@@ -0,0 +1,107 @@
# Decisions
## 1. Stack: Python + Qt (PySide6), packaged with PyInstaller
- **Reuse:** the toolkit (`ackit`) is Python and proven against the real game; the editor imports it unchanged.
- **Cross-platform GUI:** Qt Widgets via PySide6 (LGPL-3.0, official Qt for Python) looks native on
Windows and Linux, has a fast model/view table for 11,000+ rows, and runs offscreen for automated GUI tests.
- **Audio I/O:** Qt Multimedia (`QMediaDevices`, `QAudioSink`, `QAudioSource`) for playback, recording and
device selection. It ships inside the PySide6 wheels with its own backends (WASAPI on Windows,
PulseAudio/PipeWire on Linux), so unlike PortAudio/`sounddevice` nothing has to be installed on Linux.
- **Distribution:** PyInstaller one-folder build per platform. It bundles Python, Qt and all native libraries;
the user unzips and runs `UnheardCity(.exe)`, or on Windows double-clicks `Install.cmd` (see below). Nothing
else is installed. Alternatives considered:
Tauri/Rust or C# would mean rewriting the verified codec/injector (high risk, no benefit); a local web UI
would still need a bundled runtime and makes audio recording harder.
## 2. No external programs at runtime
The first toolkit used ffmpeg (decoding inputs and libvorbis encoding) and vgmstream (checks). Both are gone:
- **Encoding:** `libsndfile` (via the `soundfile` wheel) contains libvorbis 1.3.7. Tested: it yields exactly the
same codebook compatibility and the same game setups as ffmpeg's libvorbis (findings 12). Encoding is in-process.
- **Decoding game WEMs:** ackit's own WEM -> Ogg converter (the game's codebooks) + libsndfile.
- **Input formats:** libsndfile reads WAV, FLAC, OGG/Vorbis/Opus, MP3, AIFF and more. M4A/AAC/WMA would need
ffmpeg: an ffmpeg found on the system is used if present, otherwise the user gets a clear "convert to WAV/MP3"
message. Bundling ffmpeg (~80-100 MB, GPL/LGPL build care) for rare formats was judged not worth it.
- **Resampling:** `soxr` (LGPL-2.1).
So there are **no vendor binaries to fetch** (no `tools/fetch_vendor` script was needed): every native
dependency comes from version-pinned PyPI wheels (which embed libsndfile, libvorbis, libogg, FLAC, opus, mpg123,
Qt and Qt Multimedia's FFmpeg backend) that PyInstaller bundles, resolved relative to the app (never `PATH`).
`tools/collect_licenses.py` writes `THIRD_PARTY_LICENSES.txt` into each build from the wheels' own license files
plus official texts kept in `tools/licenses/` (sources in SOURCES.txt; no network at build time). Verified: the packaged Windows build runs
its decode/encode/inject smoke test with `PATH` reduced to `C:\Windows` (no ffmpeg, vgmstream or Python).
**Licenses:** PySide6/Qt LGPL-3.0 (dynamic linking, unmodified Qt shared libraries in the one-folder build,
source at https://code.qt.io), libsndfile LGPL-2.1, mpg123 LGPL-2.1, soxr LGPL-2.1, libvorbis/libogg/FLAC/opus
BSD, numpy BSD, Python PSF. One-folder (not one-file) builds keep the LGPL libraries replaceable.
The only Windows-only part is the in-game test harness (`ackit verify --ingame`, uses WinDbg/cdb and WASAPI
loopback); it is a developer tool and not part of the editor.
## 3. Where originals live (revert safety)
- **Originals store** per game folder: `<user data>/UnheardCity/stores/<id>/` (Windows `%APPDATA%`, Linux
`~/.local/share/unheardcity`), id = hash of the game path; configurable (`--store`, editor settings).
Before a sound is first changed, its original WEM bytes (SHA-256-named blob) and metadata are saved;
the applied replacement is saved too. **Revert** re-injects the stored original; **Revert all** does this for
every record. Stream-pack padding bytes are stored so pack reverts are byte-identical.
- **Full-file fallback:** before the first change to a WAD (and its pack index), one verified full copy is kept
in the store (`full/`), default on, can be disabled. Used by "Restore from full backup" if the store is damaged.
- **Why the user data folder:** it survives app updates and reinstalls, needs no write access next to the game,
and works the same on Linux/Proton. Steam never touches it.
- **Safe writes:** every WAD is rebuilt into a temp file and atomically renamed over the original, pack indexes
first. Several sounds of one WAD are written in one pass. A locked WAD (game running) gives a clear message.
- **Changed underneath:** each sound's state is computed from its bytes: original / modified (ours) / lost
(ours was replaced by the original, e.g. Steam "verify") / changed (neither: update or another tool) /
foreign (never touched by us, but not the catalog's original checksum). Writing over changed/foreign sounds
requires explicit confirmation; the catalog is used only when the WAD fingerprint matches.
## 4. Size rule
Speech/ambience variations are addressed by offset+size from BKPK tables inside compressed `.upk` packages,
which ackit does not rewrite. Replacements are written in place and must not exceed the original WEM's size;
the encoder lowers quality, then sample rate, to fit, and refuses otherwise. Music stream packs have a loose
index (`SFX\*.bnk`) that ackit rewrites, so music can be any length. (Rewriting `.upk` packages would lift the
limit but means LZO recompression of large engine packages: out of scope, high risk.)
## 5. Catalog
- Shipped CSVs per archive (names, categories, characters, original size + checksum) make loading instant and
enable change detection; any other build is scanned at runtime (header reads only, cached).
- Verified speakers come only from the user's lists; name-based character inference is kept in a separate field.
- Category scheme: `docs/categories.md`.
## 6. Repository cleanup choices
- Windows installer: a per-user PowerShell installer shipped in the zip (`Install.cmd`) instead of Inno Setup or
MSI, so building needs no extra tool; no admin rights, Start menu shortcut, Settings > Apps uninstall entry.
The zip still runs without installing.
- One entry point: `ackit` (CLI) and `UnheardCity` (GUI). The `.bat` launchers and the `mods/` folder
flow were removed (`ackit apply <folder>` and mod packages replace them).
- The user's third-party downloads that sat in `tools/` and `repositories/` (quickbms, Ravioli, AbdAshes' guide,
vgmstream/ww2ogg/revorb sources) were moved to the git-ignored `external/` folder, since `tools/` now holds
build scripts only.
- Experiment scripts and outputs were deleted; they are preserved in git tag `pre-cleanup`.
- The old `_backup/` (2.3 GB full copies) was deleted on 2026-10-08 with the game verified original; the originals
store does not need it.
## 7. Editor UX choices
- One window: filters (left), sound table (top), editor of the selected sound (bottom). The primary flow
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
window stays responsive. Errors are shown as plain sentences (game running/locked file, wrong folder,
unsupported audio, too long, changed outside the tool).
- No network access and no telemetry. User notes/tags/favorites live in the user data folder.
- One look: neutral dark greys (no blue tint), a single dark red accent (#b0242c, theme and icon), solid panels. Icons are vector drawings in
`app/icons.py` (no emoji, no icon fonts, no image files), so they look the same on every platform.
- **Artwork:** the app icon (the Arkham City logo bat in flat black over a red audio waveform of rounded bars, on a
white rounded tile; shown only in the window/taskbar and the toolbar corner) and the default background wallpaper come from images supplied by the user.
`tools/make_assets.py <folder>` turns `bac_logo_full.png` + `wallpaper.jpg` into `app/assets/` (app_icon.png,
app_icon.ico, wallpaper.jpg), which the build bundles. The folder is git-ignored because it is Warner Bros.
artwork: keep builds that contain it for personal use. Without the assets the app falls back to a plain
waveform icon and no wallpaper.
- Background: the wallpaper sits only behind the filter panel, scaled to the panel height so its left part fills
the panel, under a light veil. Settings -> Background toggles it and sets its strength (default 10 %).
- Loading feedback stays where the work is, never modal: a spinner inside the waveform being loaded (sound or
replacement file, decoded off the GUI thread), next to the Apply/Revert buttons while encoding/writing, and in
the status bar for game loading and bulk changes.
All filter selectors (including Character) are plain, non-editable drop-downs so they look and behave alike.
## 8. Testing limitations
- No Linux machine, WSL distribution or container runtime is available on the development PC. Linux support is
covered by portable code (no Windows-only calls outside `ackit/verify`), path handling for Steam/Flatpak/
Proton, unit tests that run on any OS, and a documented build procedure; the Linux package could not be
executed here.

70
docs/formats/wad.md Normal file
View File

@@ -0,0 +1,70 @@
# Arkham City WAD / BKPK container format
All values little-endian. Verified by byte-identical round trips of every shipped WAD
(INT/ITA/FRA/DEU/ESN LOC WADs and `SFX\17705D3E.wad`) and every BKPK pack.
## WWAD (`*.wad`)
| off | type | meaning |
|------|------|---------|
| 0x00 | 4s | `WWAD` |
| 0x04 | u32 | version `0x01020000` |
| 0x08 | u32 | entry count N |
| 0x0C | u32 | table offset (always `0x1C`) |
| 0x10 | u32 | table size (`12*N`) |
| 0x14 | u32 | data start (header area size; `0x8000` LOC, `0x1000` SFX) |
| 0x18 | u32 | data size (`file size - data start`) |
| 0x1C | N x {u32 hash, u32 size, u32 offset} | |
Rules:
- Table sorted by `hash` interpreted as **signed** int32. Payloads stored in table order.
- Each payload starts at the next **0x1000-aligned** offset after the previous one.
- `size` = payload length rounded up to **0x800**; padding is zero.
- File ends 0x1000-aligned; header area after the table is zero.
- `hash` = Wwise media/file ID (names: `hashlist.txt`, e.g. `CHR_RadioScanner`, `AMB_Steel_B3`, `Music`).
Payload kinds:
- **LOC WADs**: each entry is a group of WEMs, the variations of one line (ITA: 10816 WEMs in
2464 entries; e.g. `CHR_RadioScanner` holds 91). Each WEM starts at the next 0x800 boundary after
the previous one (zero padding); 3 entries pack two WEMs back to back. Addressed `HASH:i`.
- **SFX WAD**: 79 grouped ambience/effect entries (260 WEMs) plus 4 **stream packs** (below).
Where the game gets the inner offsets: the entry IDs appear next to `BKPK` headers inside the
LZO-compressed UE3 packages (e.g. `BmGame.upk`, `OW.upk`, `Church_B1_*.upk`). Those embedded BKPK
tables give each WEM's offset **and size**. ackit does not rewrite `.upk` files, so these WEMs are
replaced **in place, no larger than the original** (zero-filled to the original size), and the WAD
layout never changes. The user's earlier quickbms-reimport mod did the same and was loaded by the game.
Only the 4 SFX stream packs, whose BKPK index is a loose `.bnk`, can change size.
## BKPK stream packs (`SFX\<ID>.bnk` + SFX WAD entry `<ID>`)
`EDF036D6` (Music, 314 WEMs, incl. the intro/front-end music `393BC53A`), `E8F980BE` (Music_PDLC, 75),
`57572DAC` (Global, 1), `FCA4F778` (1). The `.bnk` file is an index + an embedded soundbank; the WEM
data lives in the SFX WAD entry with the same ID.
| off | meaning |
|------|---------|
| 0x00 | `BKPK` |
| 0x04 | version `0x01020000` |
| 0x08 | pack id (== WAD entry hash) |
| 0x10 | media count N |
| 0x18 | table size `12*N` |
| 0x1C | table offset `0x48` |
| 0x20 | total stream size (== WAD entry size) |
| 0x2C | end of table (`0x48 + 12*N`) |
| 0x38 | embedded bank size |
| 0x3C | embedded bank offset (0x800 or 0x1000; a normal BKHD/HIRC/STID bank) |
| 0x48 | N x {u32 media id, u32 size, u32 offset in the WAD entry} |
Rules: table sorted by media id; each media starts at the next **0x800-aligned** offset; total =
end rounded up to 0x800. Padding is mostly zero (a few non-zero bytes in `EDF036D6`; the tool keeps
original bytes where nothing moved).
Replacing a pack media (`ackit` target `EDF036D6/393BC53A`) rewrites the pack table (`.bnk`), the
WAD entry payload, and the WAD table/offsets. Verified in-game (NOTES.md, E2).
## Regular soundbanks (`SFX\*.bnk`, bank version 56)
BKHD / DIDX {id, offset, size} / DATA / HIRC / STID. In-memory WEMs (DATA) are 16-byte aligned.
`ackit/bnk.py` can read/rebuild them, but most front-end sounds come from banks embedded in UE3
packages (`.upk`), which this toolkit does not modify.

82
docs/formats/wem.md Normal file
View File

@@ -0,0 +1,82 @@
# WEM format accepted by Arkham City (Wwise 2011.1, Vorbis)
Derived from the game's WEMs; `ackit verify` / `tests/test_realgame.py` rebuild them byte-for-byte from these rules
alone (all 10816 ITA WEMs; in SFX only the 4 looping music tracks with loop points differ).
## RIFF layout
`RIFF` size `WAVE`, then `fmt ` (0x42 bytes), optional `cue `/`LIST` (37 files), `data`.
Odd-sized `data` chunks are **not** padded.
### `fmt ` (0x42)
| off | type | value |
|-----|------|-------|
| 0x00 | u16 | 0xFFFF (Wwise Vorbis) |
| 0x02 | u16 | channels (1,2,3,4,5,6 seen) |
| 0x04 | u32 | sample rate |
| 0x08 | u32 | avg bytes/s = `(data_len - seek_size) * rate // total_frames` |
| 0x0C | u16,u16 | 0, 0 |
| 0x10 | u16 | cbSize 0x30 |
| 0x12 | u16 | valid bits (16 or 24 = source bit depth, informational) |
| 0x14 | u32 | channel mask (mono 0x4, stereo 0x3, quad 0x33, 5.0 0x37, 5.1 0x3F) |
| 0x18 | 42 B | AkVorbisInfo (below) |
### AkVorbisInfo (fmt+0x18)
| off | type | field | rule |
|-----|------|-------|------|
| 0x00 | u32 | dwTotalPCMFrames | sample count |
| 0x04 | u32 | dwLoopStartPacketOffset | `2 + setup_len` (relative to end of seek table) |
| 0x08 | u32 | dwLoopEndPacketOffset | `data_len - seek_size` |
| 0x0C | u16 | uLoopBeginExtra | 0 |
| 0x0E | u16 | uLoopEndExtra | = uLastGranuleExtra |
| 0x10 | u32 | dwSeekTableSize | bytes (4 per entry) |
| 0x14 | u32 | dwVorbisDataOffset | `seek_size + 2 + setup_len` |
| 0x18 | u16 | uMaxPacketSize | max audio packet payload |
| 0x1A | u16 | uLastGranuleExtra | `sum(packet samples) - total_frames` (< blocksize) |
| 0x1C | u32 | dwDecodeAllocSize | see "decode alloc" — **must be right** (too small = NULL write crash) |
| 0x20 | u32 | dwDecodeX64AllocSize | `alloc + 8*nbooks + 40 + 52*nfloors` (fit; unused by 32-bit game) |
| 0x24 | u32 | uHashCodebook | **FNV-1 32** of the stripped setup bytes; codec-setup cache key |
| 0x28 | u8,u8 | uBlockSizes | log2 short, log2 long |
## `data` chunk
```
[seek table: N x {u16 granule delta, u16 byte delta}]
[u16 setup_len][stripped setup packet]
[u16 len][audio packet] ...
```
- Seek points: walk packets; point at the first packet whose *end* granule >= previous point + interval
(8192 for SFX/speech; music uses 1024 or other per-sound values). Byte offsets are relative to the
setup start (end of seek table).
- No identification/comment headers (rebuilt from fmt).
## Stripped setup ("external codebooks")
Standard Vorbis setup with: no `\x05vorbis` header; codebook count (8b), then each codebook as a
**10-bit ID** into the built-in library; no time-domain section; floor type field removed (always 1);
residue type 2 bits; mapping type removed; mode window/transform type removed. See
`ackit/wwvorbis.py:transcode_setup` (bidirectional).
Codebook library: the game's table (runtime `0x0158E748`, 598 books) is byte-identical to ww2ogg's
standard `packed_codebooks.bin` (`ackit/data/game_codebooks.bin`). **Wwise 2011.2.x uses the aoTuV 6.03
library instead** — same IDs, different books — which is why files from the newer encoder decode to
garbage/silence in this game.
Stock libvorbis (1.3.7, in ffmpeg and in the libsndfile wheels ackit uses) setups map onto this library for all tested rates with 1/2/4/5
channels, and for 6 channels up to 32 kHz (its 5.1 books at 44.1/48 kHz are newer than the game).
At 24 kHz q>=5 needs a book not in the library. The encoder probes and picks a compatible quality.
## Audio packets
- Blocksizes differ (e.g. 256/2048): "modified" packets — packet-type bit and the long-window
prev/next flags are removed (mode number stays first).
- Equal blocksizes (512/512, 1024/1024): standard Vorbis packets (type bit kept).
- 2-byte size header, no granule.
## Decode alloc (dwDecodeAllocSize)
`0x30 + parser allocations + 4*nbooks + 8`, every allocation rounded up to 4 bytes. Parser
allocations (Tremor-lowmem derived, `ackit/allocsize.py`):
- codebooks: `nbooks*0x3C` + per book: decode table (`used<2` or nodeb==4: `used*8+4`, else
`align4((used*(leafw+1)-2)*nodeb)`) + for dec_type 2 the quantized values (`align4(quantvals)` or
`align4(2*quantvals)`).
- floors: `nfloors*0x24` + per floor `a4(parts) + a4(11*(maxclass+1)) + a4(2*(posts+2)) + a4(posts+2) + 2*a4(posts)`.
- residues: `nres*0x1C` + per residue `a4(ncls) + 8*ncls`.
- mappings: `nmaps*0x14` + `a4(2*coupling_steps)` + `a4(channels)` if submaps>1 + `a4(2*submaps)`.
- modes: `a4(2*nmodes)`.
Exact on all 24 setups the game ships; in-game test E10 used a new setup with computed values.

View File

@@ -0,0 +1,92 @@
# MISSION: Arkham City audio modding toolchain (autonomous)
You are working fully autonomously. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption in `NOTES.md`, and continue. Never stop to ask me for permission or clarification. Stop only when a success criterion below is met, when a stop condition is hit, or when usage limits force a pause (see "Limits").
## Goal
Make it possible to replace audio in **Batman: Arkham City (PC)** with custom audio in a simple, repeatable way. Deliverables:
1. **Encoder tool**: converts an arbitrary WAV/OGG/MP3 into a WEM file the game actually accepts and plays (correct RIFF/Wwise chunks, Vorbis stream and codebooks as the game's Wwise version expects).
2. **Injector tool**: replaces a given WEM inside the game's WAD container, tracking file sizes and alignment, and updating all offset/size tables so the container stays valid. Must be reversible (restore from backup).
3. **A short README** documenting the WAD structure, WEM format findings, and usage, so a non-expert can replace a sound with one command.
Success = a custom sound I supply plays correctly in-game in place of the original, with no crash, verified by your harness (see "Verification").
## What is already known (do not redo this work)
- Game was built with **Audiokinetic Wwise 2011.1.0**, which is unavailable. The closest available is **2011.2.2** (Internet Archive). Audio is a custom Wwise RIFF (`RIFF/WAVE`, `fmt`, `data`, etc.) containing Vorbis with **stripped headers and packed codebooks**.
- Differences between 2011.1.0 and 2011.2.x exist, including **which Vorbis codebooks the encoder uses**. Header layout differences are also possible and not fully mapped.
- Existing public tools (unpackers/repackers) work for extracting and repacking, but the resulting audio is incompatible: the game refuses to play it or crashes. Alternative codecs (PCM, etc.) were already tried and do not work.
- I already made a **patch that stops the crash**: a code cave that patches the variable holding the number of bytes to allocate on a **bump allocator**. Re-derive details from my patch/notes in the repo if present; do not assume it is complete.
- The game executable has **DRM/packing**: many functions are missing when the original exe is analyzed statically. A **WinDbg memory dump of the running executable** exposes them. Use the dumped exe for Ghidra analysis; use the live process for dynamic analysis.
- Useful references: **vgmstream** and **ww2ogg** (Wwise RIFF parsing, packed codebook handling, codebook files such as `packed_codebooks.bin` / aoTuV variants), **revorb**, and Wwise 2011.2.2 itself as a reference encoder.
Search the filesystem first for my existing patches, dumps, notes, and tools. Treat anything you find as input data, not as ground truth; verify before relying on it.
## Two approaches (pursue in this order, switch when evidence says so)
**A. Make the files compatible with the game.** Reverse engineer the game's WEM loader/Vorbis decoder to find exactly what it requires (header fields, codebook set/IDs, setup-packet format, packet/granule layout, block sizes, seek table, etc.). Then build an encoder that produces that format, likely by encoding with Wwise 2011.2.2 or libvorbis and rewriting headers/codebooks to match the game's expectations. Compare original game WEMs against Wwise 2011.2.2 output to find the differences.
**B. Patch the game binary** to accept more encodings (for example, make the decoder accept the 2011.2.x codebooks or headers, or add a path that supports another codec). Use this if A proves infeasible, or as a complement if it is simpler.
Decide by evidence. Document the reasoning in `NOTES.md`.
## Environment and tools
- Native Windows. Work in a git repo; commit often with meaningful messages.
- Ghidra via MCP (static analysis, on the dumped exe).
- **Steam-only game.** The game is installed through **Steam** and can only be run through Steam (Steam DRM/ownership check). Steam must be running and logged in. Launching the game exe directly may fail or exit; do not rely on it. Consequences you must handle:
- Find the install folder (`steamapps\common\...`) and the game's `appmanifest_*.acf` to get the real App ID and exe name; do not guess them.
- Launch the game via Steam (for example `steam://rungameid/<appid>` or `steam.exe -applaunch <appid>`), or via a Steam **launch option** that wraps the game with the debugger (`<debugger command> %command%`), so the Steam environment is inherited. Test which approach works and record it in `NOTES.md`. Use whichever allows attaching early enough to break during startup and audio loading.
- **Never use Steam's "Verify integrity of game files"** and make sure automatic updates are off for this game, or Steam will overwrite the modified files. Your restore script, not Steam, is the way to restore.
- Keep the backup of the game folder outside the Steam library, and write the restore script to work while Steam is running.
- **Debugger (dynamic analysis): WinDbg is the baseline.** WinDbg is installed and is the default for dynamic analysis. First detect how it is installed (Store version `windbgx.exe`, install folder, any WinDbg MCP tool in your tool list) and record it in `NOTES.md`. Prefer a WinDbg MCP tool if one is connected; otherwise drive WinDbg from the command line with a startup command string, a log file, and debugger scripts (verify the switches actually work with the installed version before building the harness around them). It must run unattended: log to a file, set breakpoints/commands at startup, and terminate the debuggee and debugger on its own at the end. Attaching to the Steam-launched process (by name or PID) is a valid approach. Prefer scripted debugging with log files over interactive tool calls when it is cheaper for context.
- **You are free to improve the workflow.** Any tool, library, script, or technique that speeds up or de-risks the modding process is welcome: other debuggers or tracing tools, API/file-access monitors (for example Process Monitor or Frida-style hooks), in-process hooks or injected DLLs that log or call the game's audio functions directly, emulation or unit-testing of the decoder in isolation, custom Ghidra scripts, format-fuzzing harnesses, or anything else you judge useful. Install what you need, keep installs minimal, and record each addition and why in `NOTES.md`.
- Python for tooling. Available/installable: vgmstream-cli, ww2ogg, revorb, ffmpeg, libvorbis/oggenc, and Wwise 2011.2.2. Check what is installed; if a tool is missing and the network allows, install it; otherwise write a minimal replacement.
- You may debug the running game autonomously: launch it, attach debuggers, set breakpoints, read memory, and kill it. Always work on a copy-restorable game directory.
## Rules of engagement
- **Back up first.** Before modifying anything in the game directory or WAD, create a verified backup and a one-command restore script. Never edit the only copy of anything. Every test run starts from a clean restore plus exactly one change.
- Keep the game's original files byte-identical except for the intended injections.
- Change one variable at a time when testing hypotheses, and log the result.
- Prefer small, falsifiable experiments over long speculation. State a hypothesis, test it, record the outcome.
- Do not spend effort on unrelated goals (graphics, gameplay, other codecs) unless needed.
## Verification (build this first)
Before deep reverse engineering, build an **automated test harness** script that: restores a clean game copy, injects a given WEM, launches the game through Steam with WinDbg attached and logging, triggers or loads the target sound in a deterministic way (for example, a save or a scripted entry point that plays it early), detects crash / hang / clean exit, captures debugger logs and exception info, and prints a PASS/FAIL summary.
Make "does it play" machine-checkable without a human listening. For example: breakpoint on the decoder's init/decode functions and log return values or error codes, check that decoded sample counts match expectations, and optionally capture audio output (loopback) and compare against the source with a similarity metric. Choose whatever is most reliable and document it. Also use vgmstream/ffmpeg decoding as an offline sanity check, remembering that offline success does not imply the game accepts the file.
## Suggested phases
1. Inventory: find existing work, tools, dumps, and notes. Set up the repo, `CLAUDE.md`, `NOTES.md`, backups, and restore script.
2. Reverse the **WAD container**: header, file table, offsets, sizes, alignment, compression/encryption if any. Write a parser and a round-trip test (unpack, repack, byte-identical output).
3. Build the test harness.
4. Reverse the **WEM loading and Vorbis decoding path** in the dumped exe (and live process). Document exact format requirements.
5. Compare original game WEMs with Wwise 2011.2.2 output and map every difference. Test hypotheses with the harness.
6. Implement the encoder (approach A) or binary patch (approach B), whichever the evidence supports.
7. Implement the injector with size/alignment/offset handling, including the case where the new file is larger or smaller than the original.
8. Final end-to-end test with a custom sound, then write the README.
## Memory and context management
- `NOTES.md` is your persistent memory. After every meaningful finding or failed experiment, append: what you tried, the result, and the conclusion. Keep a short "Current status / Next steps" section at the top, rewritten as you go.
- Record addresses, struct layouts, function names, and format specs in a `docs/` folder as you discover them, so you never have to rediscover them after a context reset.
- At the start of every session or after a context reset, read `NOTES.md` first and continue from there.
- Use subagents for bulk or mechanical work (large searches, log triage, batch experiments) to keep the main context small.
## Limits and efficiency
I am on a limited Claude Pro quota. Be economical: avoid dumping huge outputs into context (filter, grep, summarize logs), do not re-read large files unnecessarily, and delegate mechanical work to cheaper subagents. If you hit a usage limit, write a clear status in `NOTES.md` so work can resume cleanly.
## Stop conditions
- **Success:** the end-to-end test passes and the deliverables exist.
- **Dead end:** if approach A and B have each had at least five distinct, well-documented failed hypotheses, stop and write a `REPORT.md` that lists what was proven, what was ruled out, the best remaining hypotheses, and the exact next experiments.
- If any action risks something irreversible outside the project directory and the game directory, do not do it.
## Output at the end
Summarize in a few lines: what works, how to use the tools, and what remains. Everything else lives in the repo.

View File

@@ -0,0 +1,109 @@
# MISSION: Clean up the repo, then build the Arkham City Audio Editor (autonomous)
You are working fully autonomously, exactly as in the previous mission. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption and reasoning in `NOTES.md`, and continue. Never stop to ask for permission or clarification. Stop only when the success criteria below are met, when a stop condition is hit, or when usage limits force a pause.
## Context
The previous mission succeeded: you reverse engineered the WAD container and the WEM format of **Batman: Arkham City (PC, Steam)**, built an encoder and an injector (the "ackit" tooling, in Python), and patched the game so modified audio plays. All of that is in this repository, together with many experiment scripts, notes, and session-resume markdown files.
Start by reading `NOTES.md`, `CLAUDE.md`, and the docs to re-learn the current state. Do not redo reverse engineering. The Steam rules still apply: the game runs only through Steam, never use "Verify integrity of game files", keep auto-updates off for this game, keep backups outside the Steam library, and keep game files byte-identical except for intended changes. WinDbg and your other tools remain available if you need to verify something.
## Part 1: Repository cleanup (do this first, then commit)
Goal: a small, clear repository where a newcomer can find everything.
1. **Safety net first.** Before deleting anything, commit the current state and create a git tag (for example `pre-cleanup`) so every deleted file is recoverable from history. Do not rewrite history.
2. **Delete** every script and file that is not needed any more: one-off experiments, scratch analysis scripts, dead ends, duplicated tools, temporary outputs, large generated artifacts. Judge by whether the file is part of the final working pipeline or documents it.
3. **Keep:**
- `ackit` (the final toolkit), with only the code that matters: encoding, injection, WAD/WEM parsing, patching, and testing/verification. Merge duplicates, remove leftover and unused code, give modules clear names, and add short docstrings. Keep the tests, make sure they pass, and add tests where core behavior is uncovered (WAD round-trip, WEM encode/decode check, inject/revert).
- **All documentation you wrote**: format specs, address maps, reverse-engineering notes, findings, failed hypotheses worth remembering. Move them into a tidy `docs/` structure, but do not delete or condense away information.
- **The markdown files used to resume sessions** (`NOTES.md`, `CLAUDE.md`, and similar). Keep them, update them to describe the new layout, and keep a short "Current status / Next steps" section.
4. **Simplify usage.** Provide one clear entry point for the toolkit, for example a single CLI (`ackit <command>`) with a few well-named subcommands (such as `info`, `list`, `extract`, `encode`, `inject`, `revert`, `verify`) and helpful `--help` text. Replace the multi-step manual procedures with one-command workflows where possible. Write a short, clear top-level `README.md`: what it is, install, quick start with 2-3 examples, and where things live.
5. **Reorganize the layout** so each kind of thing has an obvious place. Suggested (adapt if you find better):
```
ackit/ Python package (core library + CLI)
tests/ automated tests
data/catalog/ CSV catalogs (one per language/WAD, see below)
docs/ format specs, reverse-engineering notes, how-tos
app/ the editor application (Part 2)
tools/ build/packaging helper scripts only
NOTES.md, CLAUDE.md, README.md
```
6. **Catalog.** My curated catalog of audio files (by character, mainly for the ITA language, though the idea applies to other language WADs too) is the source for names/characters. You had started converting it into CSV files. Locate the original catalog files and your CSV work. Finish converting everything into well-structured CSVs under `data/catalog/`, with a documented schema (see Part 2) and a short `data/catalog/README.md` explaining columns and how to add other languages. Do not lose or overwrite my original catalog files; keep them in a clearly named `data/catalog/source/` folder (unless they are too large or clearly not redistributable, in that case document where they are).
7. Add a `.gitignore` so game assets, extracted audio, dumps, and build outputs never get committed. Do not commit any Arkham City game audio.
8. Run the test suite and one real end-to-end check (inject a custom sound, confirm it plays in game through the existing harness if still available, and revert) to prove the cleanup did not break anything. Commit.
## Part 2: The Audio Editor application
Build a simple yet powerful desktop editor for the game's audio, working on **Windows and Linux** (on Linux the game runs through Steam/Proton, so the editor must accept any game folder path and handle Proton library paths; the editor itself does not need to launch the game).
### Technology decision (you decide, document it in `docs/decisions.md`)
Existing tools are Python. Pick the stack that best balances: working on Windows and Linux, easy installation for non-technical users, good audio playback/recording, reuse of `ackit`, and reasonable effort. Candidates: Python with a Qt GUI (PySide6) packaged with PyInstaller/Nuitka (bundles its own Python, no separate install), a web UI served locally and shown in a window, or a rewrite in another language (for example Rust/Tauri or C#). Prefer reusing `ackit` unchanged over rewriting it, unless there is a strong reason. Requirements for the decision:
- Distribution must not require the user to install Python or other runtimes (bundle what is needed), or else provide a clearly simple one-step installer.
- The encoder must work **without Wwise** at runtime and on Linux as well as Windows. If any part of the encoding pipeline depends on a Windows-only tool, replace that part with portable code, or ship the required pre-generated data (such as codebook data) with the app. Verify on both platforms as far as you can (you can test Linux behavior under WSL or a Linux container if available).
- Audio I/O must be portable (for example `sounddevice`/PortAudio, `miniaudio`, or Qt Multimedia), with device selection.
- **Bundle external tooling in the distribution.** If the app or `ackit` needs any external tool or binary (for example ffmpeg/ffprobe for decoding and converting, vgmstream-cli for decoding game WEMs, revorb, ww2ogg, oggenc/libvorbis, PortAudio or other native libraries), include it in the final dist build for each platform, so the user installs nothing else. Rules:
- Prefer removing a dependency when a small portable library does the job (for example a Python decoder instead of a full ffmpeg), to keep the package small. Otherwise bundle the binary.
- Ship per-platform binaries (Windows and Linux, x86-64 at least) in a `vendor/` or `bin/` folder inside the package, and resolve them at runtime relative to the app (never rely on `PATH`). Fall back to a system copy only if the bundled one is missing.
- Download them from official/trusted sources in a build script (`tools/fetch_vendor.*`), pin versions, and verify checksums. Do not commit large binaries to git; fetch them at build time.
- **Licenses:** check each tool's license (for example ffmpeg LGPL/GPL build variants; prefer an LGPL build without GPL-only components if sufficient). Include a `THIRD_PARTY_LICENSES` file with each license text, version, and source URL, and satisfy redistribution terms (for example, point to the source). Record the decisions in `docs/decisions.md`.
- Test the packaged build on a machine/container **without** these tools on `PATH` to prove it is self-contained.
### Feature 1: Browser
- Load a game folder (and its WADs), remember the last path, and show a table of all audio entries with fast loading even for tens of thousands of entries (lazy loading, indexing/caching in a user cache folder; do not decode everything up front).
- **Search** by name/ID, plus **filters** for category, character, language/WAD, and any other useful field. Combine them freely, with instant results as you type.
- **Categories:** you decide a sensible category scheme (for example Speech, Music, SFX, Ambience, UI, Combat, Cinematic...). Derive them from the catalog and from the data itself (WAD/folder names, Wwise bank/event names, file size/duration heuristics, naming patterns), and document the scheme and the rules in `docs/`. Entries that cannot be classified go into an "Unknown" category and must remain searchable.
- **Characters** come from my catalog CSVs. Entries not in the catalog still appear, with empty character.
- Add other columns/filters you judge useful (duration, size, sample rate, channels, modified/original status, catalog notes or transcript text if present in the catalog, favorites/tags). Sorting on every column, multi-select, and a "modified only" filter.
- Quick preview playback directly from the list (play/stop, keyboard-driven navigation), with a visible indicator for entries modified by the user.
- Let the user add their own notes/tags per entry, stored in a user data file (not in the game folder).
### Feature 2: Single-file editor
Opening an entry shows an editor view with:
- **Listen**: play original and current (modified) audio, waveform display, seek, volume, loop, and A/B compare between original and replacement.
- **Substitute**: replace the audio from a file (WAV, OGG, MP3, FLAC, and whatever else ffmpeg or the chosen libs decode easily). Handle sample-rate/channel conversion, loudness/volume adjustment, and trimming. Warn clearly if the replacement is much longer/larger than the original, and make sure the injector handles any size correctly (offsets and alignment already solved in ackit; reuse it).
- **Record**: record in the moment from a user-selected **audio input device** (device list, level meter, record/stop/re-record, preview before accepting, optional trim and normalize). Test recording logic with a virtual or file-based input where no hardware exists.
- **Apply / Revert**: applying encodes with ackit and injects into the game WAD (with the safety rules from before: back up before first write, atomic/safe writes, never leave the WAD in a half-written state). **Revert** must restore the original audio of that entry, and there must also be "Revert all". Decide where originals live and justify it in `docs/decisions.md`. A good default: the first time an entry is modified, store its original bytes (with hash and location metadata) in an `originals` store; revert re-injects from it, so no full-WAD copy is required. Consider also keeping a pristine backup of the whole WAD as a fallback. Choose the location (user data folder vs. next to the game files) by what is most convenient and portable, for example a per-game-folder store with a configurable path, and make it survive app updates. Detect when the game files changed underneath (for example after a Steam update or verification) and warn the user safely instead of corrupting anything.
- **Export/import mods:** let the user export their changes as a portable package (replacement audio plus a manifest) and import it on another machine or share it, so mods are not tied to one WAD state. Keep it simple.
### UX and quality bar
- Simple first: the primary flow (search, select, listen, replace/record, apply) must be obvious without reading docs. Power features stay available but unobtrusive.
- Clear messages for errors (wrong game version, missing patch, unsupported audio, WAD locked by a running game, Steam path issues).
- Do not add telemetry or network access, except where strictly needed (none expected).
- Keep dependencies reasonable and pinned; keep the app responsive (long operations in background threads with progress and cancel).
## Verification (build it as you go)
- Unit/integration tests for the library; headless tests for the app logic (search/filter index, catalog loading, apply/revert, mod export/import). Use fixtures or synthetic WADs where real game data cannot be committed.
- Automated GUI smoke tests (for example headless/offscreen Qt or a browser driver) that open the app, search, open an entry, replace audio from a file, apply, and revert, checking byte-level restoration of the original.
- At least one **real end-to-end test against the actual game** using the existing harness: apply a replacement through the editor code path, confirm the game still starts and plays it, then revert and confirm the WAD is byte-identical to the original.
- Build the distributable packages for the platform you are on, and verify as far as possible that the Linux package starts and works (container or WSL). Document exactly how to build for the other platform.
## Deliverables
1. Cleaned repository with the structure above, passing tests, and updated `NOTES.md`/`CLAUDE.md`/docs.
2. Simplified `ackit` CLI and top-level `README.md`.
3. Catalog CSVs and schema documentation.
4. The editor app source in `app/`, with build scripts in `tools/`, plus a self-contained packaged build for the current platform (bundled external tools included, with `THIRD_PARTY_LICENSES`) and instructions to build for Windows and Linux.
5. `docs/decisions.md` (stack, category scheme, originals store, trade-offs) and a user guide with a few screenshots (headless screenshots are fine).
## Working rules (unchanged from before)
- `NOTES.md` is your persistent memory: log progress, decisions, and failed attempts; keep "Current status / Next steps" at the top. After a context reset or a usage pause, read it first and continue.
- Commit often with meaningful messages. Back up before touching game files, and test on restorable copies.
- Be economical with quota: filter and summarize outputs, avoid re-reading large files, delegate mechanical or bulk work to subagents, and work in stages (cleanup first, then library changes, then browser, then editor, then packaging) so partial progress is always usable.
- Order of priority if time or quota is short: (1) cleanup, (2) browser with search and filters, (3) single-file editor with substitute and revert, (4) recording, (5) mod export/import, (6) packaging polish. Never sacrifice the revert safety for features.
## Stop conditions
- **Success:** all deliverables exist and the verification tests pass, including the real game end-to-end test.
- **Blocked:** if a requirement cannot be met (for example, the encoder truly needs a Windows-only component), implement the best alternative, document the limitation clearly in `docs/decisions.md` and `README.md`, and continue with the rest. Write `REPORT.md` at the end with what works, what does not, and the next steps.
- Do not take irreversible actions outside the project and game folders.
## Output at the end
A few lines: what was cleaned up, what the app can do, how to run and build it, and known limitations. Everything else lives in the repo.

BIN
docs/img/browse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/img/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/img/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Some files were not shown because too many files have changed in this diff Show More