#!/usr/bin/env python3 """ 将 Wwise Vorbis .wem 文件转换为标准 Ogg Vorbis (.ogg) 文件。 纯 Python 实现,不依赖 ww2ogg 或 revorb 外部工具。 基于 Wwise RIFF/Vorbis 格式: - Codec ID: 0xFFFF - Vorbis 数据存储在 "vorb" chunk 中 - 数据包可直接封装为 Ogg 容器 用法: python wem2ogg.py python wem2ogg.py --batch """ import argparse import os import struct import sys import zlib from pathlib import Path # Ogg 页类型 OGG_HEADER = 0x02 OGG_FIRST_DATA = 0x00 OGG_CONTINUED = 0x00 OGG_LAST = 0x04 # CRC32 表 (预计算) _crc_table = None def _get_crc_table(): global _crc_table if _crc_table is None: _crc_table = [] for i in range(256): r = i << 24 for _ in range(8): if r & 0x80000000: r = (r << 1) ^ 0x04c11db7 else: r <<= 1 _crc_table.append(r & 0xffffffff) return _crc_table def ogg_crc32(data: bytes) -> int: table = _get_crc_table() crc = 0 for b in data: crc = (crc << 8) ^ table[((crc >> 24) & 0xff) ^ b] crc &= 0xffffffff return crc def make_ogg_page(segment_data: bytes, granule: int, header_type: int, stream_serial: int = 0, page_index: int = 0) -> bytes: """构造一个 Ogg 页.""" # 将数据分割成最多 255 字节的段 segments = [] pos = 0 while pos < len(segment_data): seg_len = min(255, len(segment_data) - pos) segments.append(seg_len) pos += seg_len num_segments = len(segments) page_header = bytearray(27 + num_segments) # OggS 签名 page_header[0:4] = b'OggS' # Version page_header[4] = 0 # Header type page_header[5] = header_type # Granule position (8 bytes, little-endian) struct.pack_into(' list[bytes]: """从 WEM 文件中提取 Vorbis 数据包.""" with open(wem_path, 'rb') as f: data = f.read() # 验证 RIFF 头 if data[:4] != b'RIFF': raise ValueError("不是有效的 RIFF 文件") # 查找 "vorb" chunk pos = 12 # 跳过 RIFF 头 vorb_data = None while pos < len(data) - 8: chunk_id = data[pos:pos + 4] chunk_size = struct.unpack_from(' 2: # 读取 packet 大小 (可能用 2 或 4 字节) header = struct.unpack_from(' len(remaining): break packet = remaining[offset:offset + pkt_size] packets.append(packet) remaining = remaining[offset + pkt_size:] return packets def wem_to_ogg(wem_path: str, ogg_path: str) -> bool: """转换单个 .wem 文件为 .ogg.""" try: packets = extract_vorbis_packets(wem_path) if len(packets) < 3: print(f" 警告: {os.path.basename(wem_path)} 只有 {len(packets)} 个数据包") return False # Vorbis 头三个数据包: # 1. Identification header # 2. Comment header # 3. Setup header # 后续: 音频数据包 ident_pkt = packets[0] comment_pkt = packets[1] setup_pkt = packets[2] audio_packets = packets[3:] # 构造 Ogg 文件 ogg_data = bytearray() # 第 0 页: Identification header ogg_data += make_ogg_page(ident_pkt, granule=0, header_type=OGG_HEADER, page_index=0) # 第 1 页: Comment + Setup headers header_data = comment_pkt + setup_pkt ogg_data += make_ogg_page(header_data, granule=0, header_type=OGG_FIRST_DATA, page_index=1) # 后续页: 音频数据 (每页放尽可能多的 packet) page_idx = 2 granule = 0 buf = bytearray() for pkt in audio_packets: # Vorbis granule = 累积样本数 # 粗略估计: 每个 packet 约 576 或 1024 samples granule += 576 if len(buf) + len(pkt) > 45000: # Ogg 页最大约 65KB ogg_data += make_ogg_page(bytes(buf), granule=granule, header_type=OGG_CONTINUED, page_index=page_idx) page_idx += 1 buf = bytearray() buf += pkt # 最后一页 if buf: ogg_data += make_ogg_page(bytes(buf), granule=granule, header_type=OGG_LAST, page_index=page_idx) with open(ogg_path, 'wb') as f: f.write(ogg_data) return os.path.getsize(ogg_path) > 100 except Exception as e: print(f" 错误 [{os.path.basename(wem_path)}]: {e}") return False def batch_convert(input_dir: str, output_dir: str) -> tuple[int, int]: """批量转换目录中所有 .wem 文件.""" os.makedirs(output_dir, exist_ok=True) wem_files = sorted(Path(input_dir).glob('*.wem')) if not wem_files: print(f"在 {input_dir} 中未找到 .wem 文件") return 0, 0 print(f"找到 {len(wem_files)} 个 .wem 文件") success = 0 failed = 0 for i, wem_path in enumerate(wem_files): ogg_name = wem_path.stem + '.ogg' ogg_path = os.path.join(output_dir, ogg_name) if wem_to_ogg(str(wem_path), ogg_path): success += 1 else: failed += 1 if (i + 1) % 50 == 0: print(f" 进度: {i+1}/{len(wem_files)} (成功: {success})") print(f"\n转换完成: {success} 成功, {failed} 失败") return success, failed def main(): parser = argparse.ArgumentParser(description="WEM → OGG 转换器 (纯 Python)") parser.add_argument('input', help='输入 .wem 文件或目录') parser.add_argument('output', help='输出 .ogg 文件或目录') parser.add_argument('--batch', action='store_true', help='批量模式: 转换目录中所有 .wem 文件') args = parser.parse_args() if args.batch: batch_convert(args.input, args.output) else: ok = wem_to_ogg(args.input, args.output) if ok: print(f"OK: {args.input} → {args.output}") else: print(f"FAILED: {args.input}") sys.exit(1) if __name__ == '__main__': main()