Jump to content
Official BF Editor Forums
Frankelstner

Bf4 Audio Decoder

Recommended Posts

It seems to be working with virtually all of the SFX files now, getting a lot of decoded files, but still the same thing with the music ebx files.

Fairly sure I followed the process correctly, maybe the music files themselves are different?

Edited by dashner

Share this post


Link to post
Share on other sites

Something seems to be missing.

I just did a complete dump of BF4 using the latest script code with the dll from the other thread.

Resulted in a 22,8GB large dump folder.

No errors, all seems fine.

Then I ran the latest Audio Decoder. Again all went smooth, no errors.

But it resulted in a folder that is 966MB large.

Someone who extracted the sounds of the alpha, resulted in 5GB of wav files.

i.e. the following was present in the extracted alpha files

Sound/VO/Common/CH/Comrose/Trose/vo_ch_mp_trose_morale_boost

when I look at the dump, this file exists:

H:\bf4 dump\bundles\ebx\sound/vo/common/ch/comrose/trose/vo_ch_mp_trose_morale_boost.ebx

but there is no "Sound/VO/Common/CH/" in the audio decoder output dir.

Neither do I have the "/Warsaw/" folder inside "Sound/Music/" just like dashner.

I am trying to run the decoder a 2nd time now like he did.

here is the shell output from the dump and the decoder: https://www.dropbox.com/s/hzzhscc6popvptk/shell%20results.zip

Any idea what is going wrong on my end?

-------edit1-------

I think I know now, I did not run the dumper a 2nd time with the "tocRoot" changed to the "Data\Win32"

doing that now

-------edit2-------

I ran the dumper first with "tocRoot=bf4Directory+"\\"+r"Update\Patch\Data\Win32"

Then a 2nd time with "tocRoot=bf4Directory+"\\"+r"Data\Win32"

because thats how I understood

##Adjust paths here. The script doesn't overwrite existing files so set tocRoot to the patched files first,

##then run the script again with the unpatched ones to get all files at their most recent version.

If I run the audio decoder now, then it will not decode/place a single file into the outputfolder anymore. :S

-------edit3-------

deleted everything, restarted from scracth, and now the dump folder is nearly 2 times as large as before.

running the audio decoder now. =)

-------edit4-------

now it works!

this time i only ran the dumper with tocRoot=bf4Directory+"\\"+r"Data\Win32"

Edited by infinityloop

Share this post


Link to post
Share on other sites

I'm not able to download the fb2audio.zip from the gamefront, could somebody upload it to mediafire or something similar? I just want to get my hands on the loading screen themes...

Share this post


Link to post
Share on other sites

could you tell me what I did wrong because I get this error:

Speex segment not converted as dll is missing: dcb4720651bc2297a6172e43339f6298 Sound/VO/Common/CH/Autotriggers/Inform/Radio/vo_ch_mp_comment_driver_bail_r

Traceback (most recent call last):
 File "C:\Users\D\Desktop\fb3extractor\Xas decoder\fb3audio.py", line 609, in <module>
   decodeAudio()
 File "C:\Users\D\Desktop\fb3extractor\Xas decoder\fb3audio.py", line 78, in decodeAudio
   dbx.decode()
 File "C:\Users\D\Desktop\fb3extractor\Xas decoder\fb3audio.py", line 479, in decode
   speex.decode(currentChunkName, target, Segment.SamplesOffset)
NameError: global name 'speex' is not defined
>>>

I put the speex dll in the same folder as the script. By the way out of curiosity does the decoder script work on the dlc that is out? From what I've tried it doesn't look like it does I know you've been busy but do you have any plans on updating that?

Edited by Durandal-217

Share this post


Link to post
Share on other sites

It works fine when placed in the Python27 folder. Anyway, you must have 32bit Python to use the dll. Though wait a sec, that is required for xas too. I don't really have any ideas right now.

I haven't tried it myself, but I suppose the decoder should work with the DLC too. Is there any error message in particular or does it just not do anything?

Edited by Frankelstner

Share this post


Link to post
Share on other sites
It doesn't do anything.

I think we should elaborate a bit to prevent misunderstanding.

So you mean to say that you have used the bf4 audio decoder (the previous version without speex support) to decode the xpack audio? I see a couple of issues. The dumper script does not handle noncas sbtoc yet so maybe the chunks are hiding there. Also, how did you define the chunk folders? Did you merge the DLC files with the other files (which is what I recommend)? In that case however, have you used some tool to determine the difference in output before and after adding the DLC files?

About speex, try to replace the try/except at the top with a simple statement, i.e. substitute

try:
   speex = cdll.LoadLibrary("easpeex")
   isSpeex=True
except:
   isSpeex=False

with

speex = cdll.LoadLibrary("easpeex")

The error message should be a bit more descriptive as to what's going on.

Share this post


Link to post
Share on other sites

I think we should elaborate a bit to prevent misunderstanding.

So you mean to say that you have used the bf4 audio decoder (the previous version without speex support) to decode the xpack audio? I see a couple of issues. The dumper script does not handle noncas sbtoc yet so maybe the chunks are hiding there. Also, how did you define the chunk folders? Did you merge the DLC files with the other files (which is what I recommend)? In that case however, have you used some tool to determine the difference in output before and after adding the DLC files?

About speex, try to replace the try/except at the top with a simple statement, i.e. substitute

try:
   speex = cdll.LoadLibrary("easpeex")
   isSpeex=True
except:
   isSpeex=False

with

speex = cdll.LoadLibrary("easpeex")

The error message should be a bit more descriptive as to what's going on.

I'm sorry I should have elaborated earlier, my mistake. What I meant was the dumper script does not extract noncas sbtoc toc properly. For example I was trying to get the L85A2 reload sound but after running the script I cannot find any folders with the DLC filename (XP0-XP1 Xpack ect.) I ran The dumper script as you instructed, first I dumped the non patched files then ran it again to dump patched files. I don't know which tool to use in order to determine if the files are different or even how to tell if they are.

As for Speex I did as you said, however it didn't work, this is what I did.

##############################################################
##############################################################
speex = cdll.LoadLibrary("easpeex")

def unpackBE(typ,data): return unpack(">"+typ,data)

and got the following.

Traceback (most recent call last):
 File "C:\Users\Durandal-217\Desktop\fb3extractor\Xas decoder\fb3audio.py", line 34, in <module>
   speex = cdll.LoadLibrary("easpeex")
 File "C:\Python27\lib\ctypes\__init__.py", line 443, in LoadLibrary
   return self._dlltype(name)
 File "C:\Python27\lib\ctypes\__init__.py", line 365, in __init__
   self._handle = _dlopen(self._name, mode)
WindowsError: [Error 126] The specified module could not be found
>>> 

I probably did it wrong, oh and I also placed the Speex DLL in the python folder during all of this and tested both with the default XAS script and the change you wanted me to make. Nothing worked.

Share this post


Link to post
Share on other sites

I'm sorry I should have elaborated earlier, my mistake. What I meant was the dumper script does not extract noncas sbtoc toc properly. For example I was trying to get the L85A2 reload sound but after running the script I cannot find any folders with the DLC filename (XP0-XP1 Xpack ect.) I ran The dumper script as you instructed, first I dumped the non patched files then ran it again to dump patched files. I don't know which tool to use in order to determine if the files are different or even how to tell if they are.

Aha. The dumper doesn't support the xpacks at the moment as they are noncas (and of course different from the noncas from bf3). I've downloaded the xpack files just yesterday and managed to dump the unpatched noncas, plus I'm about 50% done with the patched noncas. Thus it shouldn't take too long until I finish this. Will post the script when I'm done.

As for Speex I did as you said, however it didn't work, this is what I did.

...

and got the following.

Traceback (most recent call last):
 File "C:\Users\Durandal-217\Desktop\fb3extractor\Xas decoder\fb3audio.py", line 34, in <module>
   speex = cdll.LoadLibrary("easpeex")
 File "C:\Python27\lib\ctypes\__init__.py", line 443, in LoadLibrary
   return self._dlltype(name)
 File "C:\Python27\lib\ctypes\__init__.py", line 365, in __init__
   self._handle = _dlopen(self._name, mode)
WindowsError: [Error 126] The specified module could not be found
>>> 

I probably did it wrong, oh and I also placed the Speex DLL in the python folder during all of this and tested both with the default XAS script and the change you wanted me to make. Nothing worked.

I've changed some settings when compiling now. Does this dll fix it? http://www.gamefront.com/files/23943162/easpeex.zip

Share this post


Link to post
Share on other sites

I've updated the script to handle those 6 EASpeex stereo files correctly. Additionally I've cut down the EASpeex volume in half to get rid of clipping. The codec gives me back floats in a range of about +-50000.0 so I just divide by 65536 (instead of 32768) to get decent samples.

Share this post


Link to post
Share on other sites

Refer to the instructions here: http://www.bfeditor.org/forums/index.php?showtopic=15780

I still haven't found the time to dump everything, so I only used a few sample files. Let me know if the script fails somewhere.

Additionally, I've shaved off a few silent bytes for the xas compression. There are compressed blocks of 76 bytes which become 128 samples. However, most uncompressed audio is not a multiple of 128 samples. As a result, the last compressed block

contains some silence at the end. The ebx contains the info to cut off the silence though, which I have now taken into account.

Update 27.12.2013: Added EASpeex support.

Update 12.01.2014: Fixed EASpeex multichannel. Moved all decoding into the dlls to improve performance. The script really just handles the ebx files now while the dlls handle the decoding. Grab it here: https://www.dropbox.com/s/ox6clmozrzzvr5e/fb3decoder.zip

Another GOOD work.

Nice one

thank you and good luck

Share this post


Link to post
Share on other sites

I've updated the script to handle those 6 EASpeex stereo files correctly. Additionally I've cut down the EASpeex volume in half to get rid of clipping. The codec gives me back floats in a range of about +-50000.0 so I just divide by 65536 (instead of 32768) to get decent samples.

Working like a charm! =)

Share this post


Link to post
Share on other sites

Hi!! Sorry to go a little off-topic here but can anyone reupload this file/s for me? http://www.gamefront.com/files/23423833/fb2audio.zip I tried to download it using a proxy(f*ck you gamefront) but it looks like it was already deleted Dx

I'm really interested into ripping BBC2's sounds and those seem to be the "core" files to the whole thing, so yeah xD

Thanks in advance!!

Edited by Guerriel

Share this post


Link to post
Share on other sites

 

What are you talking of? that website link works fine for the download, just tested.

 

Ah WTF... It does work LOL, no idea why I couldn't download it before...

Well... thanks for the heads up!! xD

Share this post


Link to post
Share on other sites

&amp;nbsp;

hello I dont know what to do Im confused. Is it still possible to extract sounds ? http://gyazo.com/0f36678624462c426d0f531452457d00 I do not know how I continue, thanks for any advice

&amp;nbsp;

Use this script, and only change dumpDirectory, targetDirectory and ealayer3Path, nothing else.

#This script runs through all ebx files and finds those which have an instance of type SoundWaveAsset as their primary instance (which in bf4 is always very first instance).

#The info in those instances is then used to grab and decode the audio chunks.

#The decoded files are named after the ebx files (that's the whole point of going through the ebx in the first place).

#In bf4, audio can be encoded in 4 ways (ignoring console stuff like XMA):

# 0x12) PCM: Raw audio without any compression (big endian shorts)

# 0x14) XAS1: ADPCM variant. This was the sole format used in bf3 and is still the most frequent in bf4.

# 0x16) EALayer3: MP3. It's easy to spot this type in the resulting files, just look out for the .mp3 file extension (the other formats become wav).

# 0x19) Speex: Lossy compression of speech, http://www.speex.org/

#

#There are three indices attached to a decoded file in the following order.

#1: Variation.ChunkIndex: Some ebx files import several completely independent audio chunk files. This index differentiates between them.

#2: Variation.Index: An ebx may use the same audio chunk for several sound variations, this index keeps them apart.

#3: Segment.Index: A variation may contain several segments, so there is another index.

#The script will overwrite existing files.

#

#The file formats have been changed between bf4 alpha and beta. So for decoding/dumping, bf4 alpha is basically Frostbite 2, whereas bf4 beta etc. is Frostbite 3.

#Thus use the Frostbite 2 decoder for bf4 alpha files.

#Choose where you dumped the files and where to put the decoded audio:

dumpDirectory = r"E:\hexing\bf4 dump"

targetDirectory = r"E:\hexing\bf4 decoded"

#Download Zench's tool so the script can handle EALayer3.

ealayer3Path=r"path to Ealayer3 here" #https://bitbucket.org/Zenchreal/ealayer3/downloads

#These paths are relative to the dumpDirectory. They don't need to be changed.

ebxFolder = r"bundles\ebx" #files are not restricted to the Sound folder in bf4. As a result it will seem that the script does nothing for a while.

chunkFolder = r"chunks"

chunkFolder2 = r"bundles\chunks" #if the chunk is not found in the first folder, use this one

#There was a console game for Frostbite 2 in which the byte order of the chunk ids given in the ebx was changed a bit

#so it did not match the ids in the chunk folders. See also http://www.bfeditor.org/forums/index.php?showtopic=15780&view=findpost&p=106500

#Unless you know for sure that the ids are obfuscated, don't set this to True.

ChunkIdObfuscation=False

obfuscationPermutation=[3,2,1,0,5,4,7,6,8,9,10,11,12,13,14,15]

#I'm at a loss as to what the fuck the devs were thinking. I wrote tools to decrypt the toc files (all credit to gibbed/Rick though for the algorithm),

#then read in the decrypted data to split the sb into separate bundles, then read in the bundles to get the individual entries.

#Then wrote like 6 different functions to retrieve the actual payload depending on whether it's patched or not, and using cas or not.

#Then wrote a tool to deal with the binary XML/ebx files. Then hacked something together to decode the audio.

#And they expected me to get that far, but stop at a simplistic permutation, which leaves the second half of the id intact?

#I'll just leave this in, just in case another dev has a brilliant idea again.

##############################################################

##############################################################

from binascii import hexlify

from struct import unpack,pack

import os

from cStringIO import StringIO

import subprocess

from ctypes import *

#let's see if XAS and Speex exist

try:

xas = cdll.LoadLibrary("xas")

def decodeXas(chunkPath, target, samplesOffset):

makeLongDirs(target)

xas.decode(chunkPath, target, samplesOffset)

def decodePcm(chunkPath, target, samplesOffset):

makeLongDirs(target)

xas.swapEndianPcm(chunkPath, target, samplesOffset)

print "XAS1 dll detected."

except:

def decodeXas(chunkPath, target, samplesOffset):

print "Skipping XAS1 due to missing dll."

def decodePcm(chunkPath, target, samplesOffset):

print "Skipping PCM due to missing dll."

print "XAS1 dll not detected."

try:

speex = cdll.LoadLibrary("easpeex")

def decodeSpeex(chunkPath, target, samplesOffset):

makeLongDirs(target)

speex.decode(chunkPath, target, samplesOffset)

print "EASpeex dll detected."

except:

def decodeSpeex(chunkPath, target, samplesOffset):

print "Skipping Speex due to missing dll."

print "EASpeex dll not detected."

#By default Python opens a new EALayer3 window for a split second and puts focus on it. This info makes no window show up at all.

startupinfo = subprocess.STARTUPINFO()

startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

startupinfo.wShowWindow = subprocess.SW_HIDE

try:

#make sure EALayer3 exists by calling it once without arguments

subprocess.Popen([ealayer3Path],startupinfo=startupinfo)

def decodeEaLayer(chunkPath, target, samplesOffset):

makeLongDirs(target)

process = subprocess.Popen([ealayer3Path,chunkPath,"-i",str(samplesOffset),"-o",target,"-s","all"], stderr=subprocess.PIPE,startupinfo=startupinfo)

process.communicate() #this should set the returncode

if process.returncode:

print process.stderr.readlines()

print "EALayer3 tool detected."

except:

def decodeEaLayer(chunkPath, target, samplesOffset):

print "Skipping EALayer3 due to missing tool."

print "EALayer3 tool not detected."

def unpackBE(typ,data): return unpack(">"+typ,data)

def makeLongDirs(path):

#create folders if necessary and return the file handle

#first of all, create one folder level manully because makedirs might fail

path=os.path.normpath(path)

pathParts=path.split("\\")

manualPart="\\".join(pathParts[:2])

if not os.path.isdir(manualPart): os.makedirs(manualPart)

#now handle the rest, including extra long path names

folderPath=lp(os.path.dirname(path))

if not os.path.isdir(folderPath): os.makedirs(folderPath)

def open2(path,mode="rb"):

if mode=="wb": makeLongDirs(path)

return open(lp(path),mode)

def lp(path): #long pathnames

if len(path)<=247 or path=="" or path[:4]=='\\\\?\\': return path

return unicode('\\\\?\\' + os.path.normpath(path))

def hasher(keyword): #32bit FNV-1 hash with FNV_offset_basis = 5381 and FNV_prime = 33

hash = 5381

for byte in keyword:

hash = (hash*33) ^ ord(byte)

return hash & 0xffffffff # use & because Python promotes the num instead of intended overflow

class Header:

def __init__(self,varList):

self.absStringOffset = varList[0] ## absolute offset for string section start

self.lenStringToEOF = varList[1] ## length from string section start to EOF

self.numGUID = varList[2] ## number of external GUIDs

self.numInstanceRepeater = varList[3] ## total number of instance repeaters

self.numGUIDRepeater = varList[4] ## instance repeaters with GUID

self.unknown = varList[5]

self.numComplex = varList[6] ## number of complex entries

self.numField = varList[7] ## number of field entries

self.lenName = varList[8] ## length of name section including padding

self.lenString = varList[9] ## length of string section including padding

self.numArrayRepeater = varList[10]

self.lenPayload = varList[11] ## length of normal payload section; the start of the array payload section is absStringOffset+lenString+lenPayload

class FieldDescriptor:

def __init__(self,varList,keywordDict):

self.name = keywordDict[varList[0]]

self.type = varList[1]

self.ref = varList[2] #the field may contain another complex

self.offset = varList[3] #offset in payload section; relative to the complex containing it

self.secondaryOffset = varList[4]

if self.name=="$": self.offset-=8

class ComplexDescriptor:

def __init__(self,varList,keywordDict):

self.name = keywordDict[varList[0]]

self.fieldStartIndex = varList[1] #the index of the first field belonging to the complex

self.numField = varList[2] #the total number of fields belonging to the complex

self.alignment = varList[3]

self.type = varList[4]

self.size = varList[5] #total length of the complex in the payload section

self.secondarySize = varList[6] #seems deprecated

class InstanceRepeater:

def __init__(self,varList):

self.complexIndex = varList[0] #index of complex used as the instance

self.repetitions = varList[1] #number of instance repetitions

class arrayRepeater:

def __init__(self,varList):

self.offset = varList[0] #offset in array payload section

self.repetitions = varList[1] #number of array repetitions

self.complexIndex = varList[2] #not necessary for extraction

class Complex:

def __init__(self,desc,dbxhandle):

self.desc=desc

self.dbx=dbxhandle #lazy

def get(self,name):

pathElems=name.split("/")

curPos=self

if pathElems[-1].find("::")!=-1: #grab a complex

for elem in pathElems:

try:

curPos=curPos.go1(elem)

except Exception,e:

raise Exception("Could not find complex with name: "+str(e)+"\nFull path: "+name+"\nFilename: "+self.dbx.trueFilename)

return curPos

#grab a field instead

for elem in pathElems[:-1]:

try:

curPos=curPos.go1(elem)

except Exception,e:

raise Exception("Could not find complex with name: "+str(e)+"\nFull path: "+name+"\nFilename: "+self.dbx.trueFilename)

for field in curPos.fields:

if field.desc.name==pathElems[-1]:

return field

raise Exception("Could not find field with name: "+name+"\nFilename: "+self.dbx.trueFilename)

def go1(self,name): #go once

for field in self.fields:

if field.desc.type in (0x0029, 0xd029,0x0000,0x0041):

if field.desc.name+"::"+field.value.desc.name == name:

return field.value

raise Exception(name)

class Field:

def __init__(self,desc,dbx):

self.desc=desc

self.dbx=dbx

def link(self):

if self.desc.type!=0x0035: raise Exception("Invalid link, wrong field type\nField name: "+self.desc.name+"\nField type: "+hex(self.desc.type)+"\nFile name: "+self.dbx.trueFilename)

if self.value>>31:

extguid=self.dbx.externalGUIDs[self.value&0x7fffffff]

for existingDbx in dbxArray:

if existingDbx.fileGUID==extguid[0]:

for guid, instance in existingDbx.instances:

if guid==extguid[1]:

return instance

f=valid(inputFolder+guidTable[extguid[0]]+".ebx")

## print guidTable[extguid[0]]

dbx=Dbx(f)

dbxArray.append(dbx)

for guid, instance in dbx.instances:

if guid==extguid[1]:

return instance

raise nullguid("Nullguid link.\nFilename: "+self.dbx.trueFilename)

elif self.value!=0:

for guid, instance in self.dbx.instances:

if guid==self.dbx.internalGUIDs[self.value-1]:

return instance

else:

raise nullguid("Nullguid link.\nFilename: "+self.dbx.trueFilename)

raise Exception("Invalid link, could not find target.")

def getlinkguid(self):

if self.desc.type!=0x0035: raise Exception("Invalid link, wrong field type\nField name: "+self.desc.name+"\nField type: "+hex(self.desc.type)+"\nFile name: "+self.dbx.trueFilename)

if self.value>>31:

return "".join(self.dbx.externalGUIDs[self.value&0x7fffffff])

elif self.value!=0:

return self.dbx.fileGUID+self.dbx.internalGUIDs[self.value-1]

else:

raise nullguid("Nullguid link.\nFilename: "+self.dbx.trueFilename)

def getlinkname(self):

if self.desc.type!=0x0035: raise Exception("Invalid link, wrong field type\nField name: "+self.desc.name+"\nField type: "+hex(self.desc.type)+"\nFile name: "+self.dbx.trueFilename)

if self.value>>31:

return guidTable[self.dbx.externalGUIDs[self.value&0x7fffffff][0]]+"/"+self.dbx.externalGUIDs[self.value&0x7fffffff][1]

elif self.value!=0:

return self.dbx.trueFilename+"/"+self.dbx.internalGUIDs[self.value-1]

else:

raise nullguid("Nullguid link.\nFilename: "+self.dbx.trueFilename)

def valid(fname):

f=open2(fname,"rb")

if f.read(4) not in ("\xCE\xD1\xB2\x0F","\x0F\xB2\xD1\xCE"):

f.close()

raise Exception("nope")

return f

class nullguid(Exception):

def __init__(self, value):

self.value = value

def __str__(self):

return repr(self.value)

numDict={0xC12D:("Q",8),0xc0cd:("B",1) ,0x0035:("I",4),0xc10d:("I",4),0xc14d:("d",8),0xc0ad:("?",1),0xc0fd:("i",4),0xc0bd:("b",1),0xc0ed:("h",2), 0xc0dd:("H",2), 0xc13d:("f",4)}

class Stub:

pass

class Dbx:

def __init__(self, f, relPath):

#metadata

magic=f.read(4)

if magic=="\xCE\xD1\xB2\x0F": self.unpack=unpack

elif magic=="\x0F\xB2\xD1\xCE": self.unpack=unpackBE

else: raise ValueError("The file is not ebx: "+relPath)

self.trueFilename=""

self.header=Header(self.unpack("3I6H3I",f.read(36)))

self.arraySectionstart=self.header.absStringOffset+self.header.lenString+self.header.lenPayload

self.fileGUID=f.read(16)

while f.tell()%16!=0: f.seek(1,1) #padding

self.externalGUIDs=[(f.read(16),f.read(16)) for i in xrange(self.header.numGUID)]

self.keywords=str.split(f.read(self.header.lenName),"\x00")

self.keywordDict=dict((hasher(keyword),keyword) for keyword in self.keywords)

self.fieldDescriptors=[FieldDescriptor(self.unpack("IHHii",f.read(16)), self.keywordDict) for i in xrange(self.header.numField)]

self.complexDescriptors=[ComplexDescriptor(self.unpack("IIBBHHH",f.read(16)), self.keywordDict) for i in xrange(self.header.numComplex)]

self.instanceRepeaters=[instanceRepeater(self.unpack("2H",f.read(4))) for i in xrange(self.header.numInstanceRepeater)]

while f.tell()%16!=0: f.seek(1,1) #padding

self.arrayRepeaters=[arrayRepeater(self.unpack("3I",f.read(12))) for i in xrange(self.header.numArrayRepeater)]

#payload

f.seek(self.header.absStringOffset+self.header.lenString)

self.internalGUIDs=[]

self.instances=[] # (guid, complex)

nonGUIDindex=0

self.isPrimaryInstance=True

for i, instanceRepeater in enumerate(self.instanceRepeaters):

for repetition in xrange(instanceRepeater.repetitions):

#obey alignment of the instance; peek into the complex for that

while f.tell()%self.complexDescriptors[instanceRepeater.complexIndex].alignment!=0: f.seek(1,1)

#all instances after numGUIDRepeater have no guid

if i<self.header.numGUIDRepeater:

instanceGUID=f.read(16)

else:

#just numerate those instances without guid and assign a big endian int to them.

instanceGUID=pack(">I",nonGUIDindex)

nonGUIDindex+=1

self.internalGUIDs.append(instanceGUID)

inst=self.readComplex(instanceRepeater.complexIndex,f,True)

inst.guid=instanceGUID

if self.isPrimaryInstance: self.prim=inst

self.instances.append( (instanceGUID,inst))

self.isPrimaryInstance=False #the readComplex function has used isPrimaryInstance by now

f.close()

#if no filename found, use the relative input path instead

#it's just as good though without capitalization

if self.trueFilename=="":

self.trueFilename=relPath

def readComplex(self, complexIndex, f, isInstance=False):

complexDesc=self.complexDescriptors[complexIndex]

cmplx=Complex(complexDesc,self)

cmplx.offset=f.tell()

cmplx.fields=[]

#alignment 4 instances require subtracting 8 for all field offsets and the complex size

obfuscationShift=8 if (isInstance and cmplx.desc.alignment==4) else 0

for fieldIndex in xrange(complexDesc.fieldStartIndex,complexDesc.fieldStartIndex+complexDesc.numField):

f.seek(cmplx.offset+self.fieldDescriptors[fieldIndex].offset-obfuscationShift)

cmplx.fields.append(self.readField(fieldIndex,f))

f.seek(cmplx.offset+complexDesc.size-obfuscationShift)

return cmplx

def readField(self,fieldIndex,f):

fieldDesc = self.fieldDescriptors[fieldIndex]

field=Field(fieldDesc,self)

if fieldDesc.type in (0x0029, 0xd029,0x0000,0x8029):

field.value=self.readComplex(fieldDesc.ref,f)

elif fieldDesc.type==0x0041:

arrayRepeater=self.arrayRepeaters[self.unpack("I",f.read(4))[0]]

arrayComplexDesc=self.complexDescriptors[fieldDesc.ref]

f.seek(self.arraySectionstart+arrayRepeater.offset)

arrayComplex=Complex(arrayComplexDesc,self)

arrayComplex.fields=[self.readField(arrayComplexDesc.fieldStartIndex,f) for repetition in xrange(arrayRepeater.repetitions)]

field.value=arrayComplex

elif fieldDesc.type in (0x407d, 0x409d):

startPos=f.tell()

stringOffset=self.unpack("i",f.read(4))[0]

if stringOffset==-1:

field.value="*nullString*"

else:

f.seek(self.header.absStringOffset+stringOffset)

field.value=""

while 1:

a=f.read(1)

if a=="\x00": break

else: field.value+=a

f.seek(startPos+4)

if self.isPrimaryInstance and fieldDesc.name=="Name" and self.trueFilename=="": self.trueFilename=field.value

elif fieldDesc.type in (0x0089,0xc089): #incomplete implementation, only gives back the selected string

compareValue=self.unpack("i",f.read(4))[0]

enumComplex=self.complexDescriptors[fieldDesc.ref]

if enumComplex.numField==0:

field.value="*nullEnum*"

for fieldIndex in xrange(enumComplex.fieldStartIndex,enumComplex.fieldStartIndex+enumComplex.numField):

if self.fieldDescriptors[fieldIndex].offset==compareValue:

field.value=self.fieldDescriptors[fieldIndex].name

break

elif fieldDesc.type==0xc15d:

field.value=f.read(16)

## elif fieldDesc.type == 0xc13d: ################################

## field.value=formatfloat(self.unpack("f",f.read(4))[0])

elif fieldDesc.type==0x417d:

field.value=f.read(8)

else:

(typ,length)=numDict[fieldDesc.type]

num=self.unpack(typ,f.read(length))[0]

field.value=num

return field

def decode(self):

if not self.prim.desc.name=="SoundWaveAsset": return

histogram=dict() #count the number of times each chunk is used by a variation to obtain the right index

Chunks=[]

for i in self.prim.get("$::SoundDataAsset/Chunks::array").fields:

chnk=Stub()

Chunks.append(chnk)

chnk.ChunkId=i.value.get("ChunkId").value

if ChunkIdObfuscation: chnk.ChunkId="".join([chnk.ChunkId[permute] for permute in obfuscationPermutation])

chnk.ChunkSize=i.value.get("ChunkSize").value

Variations=[]

Segments=[]

for seg in self.prim.get("Segments::array").fields:

Segment=Stub()

Segments.append(Segment)

Segment.SamplesOffset = seg.value.get("SamplesOffset").value

Segment.SeekTableOffset = seg.value.get("SeekTableOffset").value

Segment.SegmentLength = seg.value.get("SegmentLength").value

for var in self.prim.get("RuntimeVariations::array").fields:

Variation=Stub()

Variations.append(Variation)

Variation.ChunkIndex=var.value.get("ChunkIndex").value

Variation.FirstSegmentIndex=var.value.get("FirstSegmentIndex").value

Variation.SegmentCount=var.value.get("SegmentCount").value

Variation.Segments=Segments[Variation.FirstSegmentIndex:Variation.FirstSegmentIndex+Variation.SegmentCount]

Variation.ChunkId=hexlify(Chunks[Variation.ChunkIndex].ChunkId)

Variation.ChunkSize=Chunks[Variation.ChunkIndex].ChunkSize

#find the appropriate index

#the index from the Variations array can get large very fast

#instead, make my own index starting from 0 for every chunkIndex

if Variation.ChunkIndex in histogram: #has been used previously already

Variation.Index=histogram[Variation.ChunkIndex]

histogram[Variation.ChunkIndex]+=1

else:

Variation.Index=0

histogram[Variation.ChunkIndex]=1

#everything is laid out neatly now

#Variation fields: ChunkId, ChunkSize, Index, ChunkIndex, SeekTablesSize, FirstLoopSegmentIndex, LastLoopSegmentIndex, Segments

#Variation.Segments fields: SamplesOffset, SeekTableOffset, SegmentLength

ChunkHandles=dict() #for each ebx, keep track of all file handles

## chunkErrors=set() #

for Variation in Variations:

try:

f=ChunkHandles[Variation.ChunkId]

except:

try:

f=open2(chunkFolder+Variation.ChunkId+".chunk")

currentChunkName=chunkFolder+Variation.ChunkId+".chunk"

except IOError:

try:

f=open2(chunkFolder2+Variation.ChunkId+".chunk")

currentChunkName=chunkFolder2+Variation.ChunkId+".chunk"

except:

## print "Chunk does not exist: "+Variation.ChunkId+" "+self.trueFilename

## chunkErrors.add("Chunk does not exist: "+Variation.ChunkId+" "+self.trueFilename)

continue #do NOT return, instead print the messages at the very end

ChunkHandles[Variation.ChunkId]=f

for ijk in xrange(len(Variation.Segments)):

Segment=Variation.Segments[ijk]

f.seek(Segment.SamplesOffset)

magic=f.read(4)

if magic!="\x48\x00\x00\x0c":

continue

raise Exception("Wrong XAS magic.")

audioType=ord(f.read(1)) #0x14 is XAS, 0x16 is EALayer3, 0x13 is XMA, 0x12 is PCM

target=os.path.join(targetDirectory,self.trueFilename)+" "+str(Variation.ChunkIndex)+" "+str(Variation.Index)+" "+str(ijk)

if audioType==0x16: #EALayer3

target+=".mp3"

decodeEaLayer(currentChunkName, target, Segment.SamplesOffset)

elif audioType==0x19: #Speex

target+=".wav"

decodeSpeex(currentChunkName, target, Segment.SamplesOffset)

elif audioType==0x14: #XAS1

target+=".wav"

decodeXas(currentChunkName, target, Segment.SamplesOffset)

elif audioType==0x12: #16bit big endian PCM

target+=".wav"

decodePcm(currentChunkName, target, Segment.SamplesOffset)

else:

print "Unknown audio segment (type "+hex(audioType)+"): "+Variation.ChunkId+" "+self.trueFilename

for key in ChunkHandles:

ChunkHandles[key].close()

print self.trueFilename

## for message in chunkErrors:

## print message

def riffHeader(samplingRate, numChannels, isFloat):

header="RIFFabcdWAVEfmt (\x00\x00\x00" #replace abcd later

extended=0xfffe #always use the extended header to make things simpler

numFormat,numSize=(3,4) if isFloat else (1,2)

header+=pack("HHllHHHHlH", extended, numChannels, samplingRate, numChannels*samplingRate*numSize,

numChannels*numSize, numSize*8, 22, numSize*8, (1<<numChannels)-1, numFormat)

header+="\x00\x00\x00\x00\x10\x00\x80\x00\x00\xaa\x008\x9bqfact\x04\x00\x00\x00"

header+=pack("l",numSize)

header+="dataABCD"

return header

#make the paths absolute and normalize the slashes

ebxFolder,chunkFolder,chunkFolder2 = [os.path.normpath(dumpDirectory+"\\"+path)+"\\" for path in (ebxFolder, chunkFolder, chunkFolder2)]

targetDirectory=os.path.normpath(targetDirectory) #it's an absolute path already

def main():

for dir0, dirs, ff in os.walk(ebxFolder):

for fname in ff:

if fname[-4:]!=".ebx": continue

absPath=os.path.join(dir0,fname)

relPath=absPath[len(ebxFolder):-4]

f=open(lp(absPath),"rb")

try:

dbx=Dbx(f,relPath)

f.close()

except ValueError as msg:

f.close()

if str(msg).startswith("The file is not ebx: "):

continue

else: asdf

dbx.decode()

main()

Edited by TheRadGamerDan

Share this post


Link to post
Share on other sites

As for BattleField Hardline, this Script works up to the VO folder which is very close to the end, it misses all Weapon sounds, it throws this error

Traceback (most recent call last):

File "S:\BFH\Audio Dumper\fb3decoder.py", line 564, in

main()

File "S:\BFH\Audio Dumper\fb3decoder.py", line 561, in main

dbx.decode()

File "S:\BFH\Audio Dumper\fb3decoder.py", line 514, in decode

decodeXas(currentChunkName, target, Segment.SamplesOffset)

File "S:\BFH\Audio Dumper\fb3decoder.py", line 67, in decodeXas

xas.decode(chunkPath, target, samplesOffset)

WindowsError: [Error -529697949] Windows Error 0xE06D7363

Frank any Ideas on how to get a full dump

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×