Files
UnheardCity/ackit/wwvorbis.py

222 lines
7.0 KiB
Python

"""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/formats/wem.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