Advertisement
Nicknine

Wwise Sound Extractor

Jan 2nd, 2019 (edited)
2,124
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.99 KB | None | 0 0
  1. # Wwise Sound Extractor (by Nicknine)
  2. # Requires Python 3
  3. # This script loads soundbanksinfo.xml and uses it to extract the sound files with proper names.
  4.  
  5. import os
  6. import struct
  7. import shutil
  8. import xml.etree.ElementTree
  9.  
  10. # This is the script config. Update these variables appropriately for your game.
  11. # Path to soundbanksinfo.xml
  12. gameRoot=r"D:\Games\Steam\steamapps\common\DOOM\base\sound\soundbanks\pc"
  13.  
  14. # Output path
  15. targetDirectory=r"E:\GameRips\doom"
  16.  
  17. # List PCK files used by the game here. You can skip PCKs for the languages you don't need.
  18. pckFiles = [
  19.     "initial.pck",
  20.     "streamed_sfx.pck",
  21.     "streamed_sfx_dlc1.pck",
  22.     "streamed_sfx_dlc2.pck",
  23.     "streamed_sfx_dlc3.pck",
  24.     "English(US)/streamed_vo.pck",
  25.     "English(US)/streamed_vo_dlc1.pck",
  26.     "English(US)/streamed_vo_dlc2.pck",
  27.     "English(US)/streamed_vo_dlc3.pck",
  28.     ]
  29.  
  30. # List banks that you want to extract in "name:language" format.
  31. # Language is always "SFX" for normal banks, check soundbanksinfo.xml for what languages are available for voice banks.
  32. targetBanks = [
  33.     "doom_music:SFX",
  34.     "doom_vo:English(US)",
  35.     ]
  36.  
  37. # NOTE: Each bank will be extracted into its own sub-folder.
  38.  
  39.  
  40.  
  41. # Actual script starts here.
  42.  
  43. def unpackLE(typ,data): return struct.unpack("<"+typ,data)
  44. def unpackBE(typ,data): return struct.unpack(">"+typ,data)
  45.  
  46. def detectEndian(f,pos):
  47.     prev=f.tell()
  48.     f.seek(pos)
  49.     data=f.read(4)
  50.     LETest=unpackLE("I",data)[0]
  51.     BETest=unpackBE("I",data)[0]
  52.     f.seek(prev)
  53.     return True if LETest > BETest else False
  54.  
  55. class Bank:
  56.     def __init__(self,path):
  57.         self.path=path
  58.         self.f=open(path,"rb")
  59.         self.bigEndian=detectEndian(self.f,0x04)
  60.         self.unpack=unpackBE if self.bigEndian else unpackLE
  61.         self.f.seek(0,2)
  62.         self.size=self.f.tell()
  63.         self.f.seek(0)
  64.        
  65.         magic, size = self.getSectionHeader()
  66.         if magic!=b"BKHD":
  67.             raise Exception("%s: Wrong BNK header magic." % self.path)
  68.  
  69.         tableOffset, tableSize = self.findSection(b"DIDX")
  70.         dataOffset, dataSize = self.findSection(b"DATA")
  71.  
  72.         # Parse sound entries from DIDX table.
  73.         self.f.seek(tableOffset)
  74.         self.numFiles=tableSize//0x0c
  75.         self.files=dict()
  76.  
  77.         for i in range(self.numFiles):
  78.             file=BankFile(self,dataOffset)
  79.             self.files[file.id]=file
  80.  
  81.     def getVar(self):
  82.         return self.unpack("I",self.f.read(4))[0]
  83.  
  84.     def findSection(self,targetSection):
  85.         self.f.seek(0)
  86.  
  87.         while self.f.tell()!=self.size:
  88.             magic,size=self.getSectionHeader()
  89.             offset=self.f.tell()
  90.             if magic==targetSection:
  91.                 return (offset,size)
  92.  
  93.             self.f.seek(size,1)
  94.  
  95.         raise Exception("%s: Did not find section %s." % (self.path,targetSection.decode()))
  96.  
  97.     def getSectionHeader(self):
  98.         magic=self.f.read(4)
  99.         size=self.unpack("I",self.f.read(4))[0]
  100.         return (magic, size)
  101.  
  102. class BankFile:
  103.     def __init__(self,bank,dataOffset):
  104.         self.id=bank.getVar()
  105.         self.offset=bank.getVar()
  106.         self.size=bank.getVar()
  107.  
  108.         self.offset+=dataOffset
  109.  
  110. class Package:
  111.     def __init__(self,path):
  112.         self.path=path
  113.         self.f=open(path,"rb")
  114.         self.magic=self.f.read(4)
  115.         if self.magic!=b"AKPK":
  116.             raise Exception("%s: Wrong PCK header magic." % self.path)
  117.  
  118.         self.bigEndian=detectEndian(self.f,0x08)
  119.         self.unpack=unpackBE if self.bigEndian else unpackLE
  120.  
  121.         self.headerSize=self.getVar()
  122.         self.flag=self.getVar()
  123.         self.langsSize=self.getVar()
  124.         self.banksSize=self.getVar()
  125.         self.soundsSize=self.getVar()
  126.  
  127.         if self.langsSize+self.banksSize+self.soundsSize+0x10==self.headerSize:
  128.             # Initial version.
  129.             self.version=1
  130.         else:
  131.             # 2012 revision which added the fourth section and changed file size from 64-bit to 32-bit.
  132.             self.version=2
  133.             self.unk=self.getVar()
  134.  
  135.         # Parse languages.
  136.         self.langsOffset=self.f.tell()
  137.         self.numLangs=self.getVar()
  138.         self.langs=dict()
  139.  
  140.         for i in range(self.numLangs):
  141.             lang=PackageLanguage(self)
  142.             self.langs[lang.id]=lang
  143.  
  144.         # Skip language names.
  145.         self.f.seek(self.langsOffset+self.langsSize)
  146.  
  147.         # Parse embedded banks.
  148.         self.numBanks=self.getVar()
  149.         self.banks=dict()
  150.         for i in range(self.numBanks):
  151.             file=PackageFile(self)
  152.             self.banks[file.id]=file
  153.  
  154.         # Parse sounds.
  155.         self.numFiles=self.getVar()
  156.         self.files=dict()
  157.         for i in range(self.numFiles):
  158.             file=PackageFile(self)
  159.             self.files[file.id]=file
  160.  
  161.             streamedFiles[file.id] = self, file
  162.  
  163.     def readLangName(self):
  164.         bytestring=b""
  165.         if self.version==1 or gamePlatform=="Windows":
  166.             while True:
  167.                 byte=self.f.read(2)
  168.                 if byte==b"\x00\x00": break
  169.                 bytestring+=byte
  170.  
  171.             return bytestring.decode("utf-16be") if self.bigEndian else bytestring.decode("utf-16le")
  172.         else:
  173.             while True:
  174.                 byte=self.f.read(1)
  175.                 if byte==b"\x00": break
  176.                 bytestring+=byte
  177.  
  178.             return bytestring.decode("utf-8")
  179.  
  180.     def getVar(self):
  181.         return self.unpack("I",self.f.read(4))[0]
  182.  
  183.     def getVar64(self):
  184.         return self.unpack("Q",self.f.read(8))[0]
  185.  
  186. class PackageLanguage:
  187.     def __init__(self,pck):
  188.         self.nameOffset=pck.getVar()
  189.         self.id=pck.getVar()
  190.         pos=pck.f.tell()
  191.         pck.f.seek(pck.langsOffset+self.nameOffset)
  192.         self.name=pck.readLangName()
  193.         pck.f.seek(pos)
  194.  
  195. class PackageFile:
  196.     def __init__(self,pck):
  197.         self.id=pck.getVar()
  198.         self.mult=pck.getVar()
  199.         self.size=pck.getVar64() if pck.version==1 else pck.getVar()
  200.         self.offset=pck.getVar()
  201.         self.lang=pck.getVar()
  202.  
  203.         if self.mult!=0: self.offset*=self.mult;
  204.  
  205. def extractFile(f,offset,size,name,outPath):
  206.     print(name)
  207.     soundPath=os.path.join(outPath,name)
  208.  
  209.     f.seek(offset)
  210.     folderPath=os.path.dirname(soundPath)
  211.     os.makedirs(folderPath,exist_ok=True)
  212.     f2=open(soundPath,"wb")
  213.     f2.write(f.read(sound.size))
  214.     f2.close()
  215.  
  216. def extractLooseFile(path,name,outPath):
  217.     print(name)
  218.     soundPath=os.path.join(outPath,name)
  219.  
  220.     folderPath=os.path.dirname(soundPath)
  221.     os.makedirs(folderPath,exist_ok=True)
  222.     shutil.copyfile(path,soundPath)
  223.  
  224. # Normalize paths.
  225. gameRoot=os.path.normpath(gameRoot)
  226. targetDirectory=os.path.normpath(targetDirectory)
  227.  
  228. tree=xml.etree.ElementTree.parse(os.path.join(gameRoot,"soundbanksinfo.xml"))
  229. root=tree.getroot()
  230. gamePlatform=root.get("Platform")
  231. schemaVersion=int(root.get("SchemaVersion"))
  232.  
  233. print("Loading package files...")
  234. packages=list()
  235. streamedFiles=dict()
  236. for pck in pckFiles:
  237.     pckPath=os.path.join(gameRoot,os.path.normpath(pck))
  238.     packages.append(Package(pckPath))
  239.  
  240. print("Loading streamed file names...")
  241. streamedFileNames=dict()
  242. for node in list(root.find("StreamedFiles")):
  243.     if node.get("UsingReferenceLanguageAsStandIn")=="true":
  244.         continue # Skip these, they're just duplicates.
  245.  
  246.     id=int(node.get("Id"))
  247.     lang=node.get("Language")
  248.     streamedFileNames[id] = os.path.normpath(node.find("Path").text), lang
  249.  
  250. for targetBank in targetBanks:
  251.     targetName, targetLang = targetBank.split(":")
  252.  
  253.     # Find bank with the specified name and language.
  254.     bankNode=None
  255.  
  256.     for node in list(root.find("SoundBanks")):
  257.         if node.get("Language")==targetLang and node.find("ShortName").text==targetName:
  258.             bankNode=node
  259.             break
  260.  
  261.     if not bankNode:
  262.         print("Bank %s was not found" % targetBank)
  263.         continue
  264.  
  265.     print(targetBank)
  266.     bankOutPath=os.path.join(targetDirectory,targetName)
  267.  
  268.     # Extract all included files.
  269.     if schemaVersion<10:
  270.         IncludedMemoryFiles=bankNode.find("IncludedFullFiles")
  271.         hasIncludedFiles=len(list(IncludedMemoryFiles))!=0
  272.     else:
  273.         IncludedMemoryFiles=bankNode.find("IncludedMemoryFiles")
  274.         hasIncludedFiles=IncludedMemoryFiles!=None
  275.  
  276.     if hasIncludedFiles:
  277.         # Only load BNK file if there are any files to extract, empty banks have no DIDX and DATA sections at all.
  278.         bankPath=os.path.join(gameRoot,bankNode.find("Path").text)
  279.         bank=Bank(bankPath)
  280.  
  281.         for node in list(IncludedMemoryFiles):
  282.             id=int(node.get("Id"))
  283.             name=os.path.normpath(node.find("Path").text)
  284.             if id in streamedFileNames:
  285.                 continue # Prefetched streamed sound, ignore.
  286.  
  287.             try:
  288.                 sound=bank.files[id]
  289.             except Exception:
  290.                 raise Exception("Internal file %u not found in bank %s. Check your files!" % (id,targetBank))
  291.            
  292.             extractFile(bank.f,sound.offset,sound.size,name,bankOutPath)
  293.  
  294.         bank.f.close()
  295.  
  296.     # Extract all streamed files.
  297.     ReferencedStreamedFiles=bankNode.find("ReferencedStreamedFiles")
  298.     if ReferencedStreamedFiles:
  299.         for node in list(ReferencedStreamedFiles):
  300.             id=int(node.get("Id"))
  301.             name, lang = streamedFileNames[id]
  302.             ext=os.path.splitext(name)[1]
  303.  
  304.             try:
  305.                 # Try looking for it in PCKs first.
  306.                 pck, sound = streamedFiles[id]
  307.                 extractFile(pck.f,sound.offset,sound.size,name,bankOutPath)
  308.             except:
  309.                 # Failing that, see if it's a loose file.
  310.                 path=os.path.join(gameRoot,lang,str(id)+ext) if lang!="SFX" else os.path.join(gameRoot,str(id)+ext)
  311.                 if os.path.isfile(path):
  312.                     extractLooseFile(path,name,bankOutPath)
  313.                 else:
  314.                     print("Streamed file %u not found! Have you included all the required package files?" % id)
  315.  
  316. for pck in packages:
  317.     pck.f.close()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement