Important information: this site is currently scheduled to go offline indefinitely by end of the year.

Frostbite 2 sound extraction research

Get help on any and all audio formats, or chip in and help others!
User avatar
Vosvoy
veteran
Posts: 127
Joined: Fri Feb 18, 2011 4:58 pm
Has thanked: 15 times
Been thanked: 15 times

Re: Frostbite 2 sound extraction research

Post by Vosvoy »

Yeah, daemon1 and Frankelstner did a really really great job.

Thanks again guys for your work and patience. Useless post for most of readers, maybe, but kind of important for those who did help us (I guess).

FatalBulletHit: are you talking about extracting BF1 sounds? Because the oldest tools didn't work.
Vosvoy
User avatar
FatalBulletHit
beginner
Posts: 32
Joined: Sun Nov 06, 2016 7:29 pm
Has thanked: 12 times
Been thanked: 6 times
Contact:

Re: Frostbite 2 sound extraction research

Post by FatalBulletHit »

Vosvoy wrote:FatalBulletHit: are you talking about extracting BF1 sounds? Because the oldest tools didn't work.
Nope, sry:
FatalBulletHit wrote:However, if sb after me wants to have all the bf3 sounds and doesn't know how to...
:P
rpopulik
n00b
Posts: 14
Joined: Thu Nov 12, 2015 1:28 pm
Has thanked: 2 times
Been thanked: 1 time

Re: Frostbite 2 sound extraction research

Post by rpopulik »

Hi,
I am interested in grabbing handheld weapons sounds from bf3. I extracted everything and I noticed that gunshot ebx files are significantly bigger and different than reloads ebx files. They also doesn't convert to wave using fb2audio.py. The script reads them, but doesnt convert to wave just as it does with reloads ebx. Does anyone know how to convert gunshots ebxes ?
User avatar
FatalBulletHit
beginner
Posts: 32
Joined: Sun Nov 06, 2016 7:29 pm
Has thanked: 12 times
Been thanked: 6 times
Contact:

Re: Frostbite 2 sound extraction research

Post by FatalBulletHit »

rpopulik wrote:Does anyone know how to convert gunshots ebxes ?
I don't, but as far as I know, every single gun shot is made with the sounds in the "shared_content" folder and not with the files you are talking about. No guarantee, tho, and I don't have a clue why there are additional bigger files in each of these sub folders.
However, if you are trying to get a clean gun shot you will get the job done quite well with the sounds in "shared_content". It also gives you all the sounds you need for an immersive gun shot, e.g. a M82 shot in a forest with deploying the bipod, handling the safety, a case dropping on dirt, reflection of the shot, bolt action, etc.

Hope I was able to help, feel free to report back if you have more questions! :)
User avatar
Vosvoy
veteran
Posts: 127
Joined: Fri Feb 18, 2011 4:58 pm
Has thanked: 15 times
Been thanked: 15 times

Re: Frostbite 2 sound extraction research

Post by Vosvoy »

FatalBulletHit wrote:
rpopulik wrote:Does anyone know how to convert gunshots ebxes ?
I don't, but as far as I know, every single gun shot is made with the sounds in the "shared_content" folder and not with the files you are talking about. No guarantee, tho, and I don't have a clue why there are additional bigger files in each of these sub folders.
However, if you are trying to get a clean gun shot you will get the job done quite well with the sounds in "shared_content". It also gives you all the sounds you need for an immersive gun shot, e.g. a M82 shot in a forest with deploying the bipod, handling the safety, a case dropping on dirt, reflection of the shot, bolt action, etc.

Hope I was able to help, feel free to report back if you have more questions! :)
You can convert those .ebx files to .txt files via a python script made by Frankelstner. Those files doesn't contain any sounds but informations about them.

Imagine that the wave samples are culinary ingredients, the ebx files are the recipes and the engine is the cook.

For example, I'm gonna make a REX MP412 gunfire sound. Thanks to the ebx file, I know what I need:
  • - " CoreBassClose_OneShot_DoublePunch_Wave " for the punch/power (Shared_Content\CoreBassClose_OneShot\...).
    - " HiFi_Revolver_Wave " for the mechanical effect (Shared_Content\HiFi_OneShot\...) and put the speed of this sample at -60 in Audacity.
    - " Noise_Close_Rifle_Wave " to make the main stereo layer (Shared_Content\Noise_Layers\...).
Mix the whole thing with an additional impulse response sound of your choice (Shared_Content\Reflections\... or Shared_Content\Noise_Layers\...) and "Voilà, bon appétit".

It worked for BF3 and BF4 but I don't know if it works for BF1.

If you don't have it already, try this script:

Code: Select all

#Requires Python 2.7
import string
import sys
from binascii import hexlify
from struct import unpack
import os
from cStringIO import StringIO
import cProfile
import cPickle

#adjust input and output folders here
inputFolder=r"H:\BF3SNDS\DUMP\bundles\ebx\sound"
outputFolder=r"H:\BF3SNDS\EBX"
guidTableName="guidTable bf" #name of the guid table file

EXTENSION=".txt"
SEP="    "

#the script can use the actual filenames in the explorer for the guid table (fast)
#or it can parse almost the entire file to retrieve the filename (slow, but necessary when the explorer names are just hashes)
#in case #2, create a separate guidTable file, in case#1 do not create that file. 
#True/False
useExplorerNames=True


#ignore all instances and fields with these names when converting to text:
IGNOREINSTANCES=[]
IGNOREFIELDS=[]
##IGNOREINSTANCES=["ShaderAdjustmentData","SocketData","WeaponSkinnedSocketObjectData","WeaponRegularSocketObjectData"]
##IGNOREFIELDS=["Mesh3pTransforms","Mesh3pRigidMeshSocketObjectTransforms"]


#run createGuidTable or dumpText, or both (preferably in the right order)
#When using explorer names, do not change anything below.
#When not using explorer names you might want to make the guid table first, then restart the script to dump text only,
#though it should work fine without change too.
def main():
    createGuidTable()
    dumpText()




##############################################################
##############################################################
if useExplorerNames:
    def createGuidTable(): #guid vs filename
        for dir0, dirs, ff in os.walk(inputFolder):
            for fname in ff:
                path=os.path.join(dir0,fname)
                f=open(path,"rb")
                if f.read(4)!="\xCE\xD1\xB2\x0F":
                    f.close()
                    continue
                #grab the file guid directly, absolute offset 48 bytes
                f.seek(48)
                fileguid=f.read(16)
                f.close()
                filename=path[len(inputFolder):-4].replace("\\","/")
                guidTable[fileguid]=filename
else:
    def createGuidTable():
        for dir0, dirs, ff in os.walk(inputFolder):
            for fname in ff:
                f=open(dir0+"\\"+fname,"rb")
                if f.read(4)!="\xCE\xD1\xB2\x0F":
                    f.close()
                    continue
                dbx=Dbx(f)
                guidTable[dbx.fileGUID]=dbx.trueFilename
        f5=open(guidTableName,"wb") #write the table
        cPickle.dump(guidTable,f5)
        f5.close()

def dumpText():
    for dir0, dirs, ff in os.walk(inputFolder):
        for fname in ff:
            f=open(dir0+"\\"+fname,"rb")
            if f.read(4)!="\xCE\xD1\xB2\x0F":
                f.close()
                continue
            dbx=Dbx(f)
            dbx.dump(outputFolder)
            
try:
    from ctypes import *
    floatlib = cdll.LoadLibrary("floattostring")
    def formatfloat(num):
        bufType = c_char * 100
        buf = bufType()
        bufpointer = pointer(buf)
        floatlib.convertNum(c_double(num), bufpointer, 100)
        rawstring=(buf.raw)[:buf.raw.find("\x00")]
        if rawstring[:2]=="-.": return "-0."+rawstring[2:]
        elif rawstring[0]==".": return "0."+rawstring[1:]
        elif "e" not in rawstring and "." not in rawstring: return rawstring+".0"
        return rawstring
except:
    def formatfloat(num):
        return str(num)
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): ##all 4byte unsigned integers
        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.null                = varList[3]  ## 00000000
        self.numInstanceRepeater = varList[4]
        self.numComplex          = varList[5]  ## number of complex entries
        self.numField            = varList[6]  ## number of field entries
        self.lenName             = varList[7]  ## length of name section including padding
        self.lenString           = varList[8]  ## length of string section including padding
        self.numArrayRepeater    = varList[9]
        self.lenPayload          = varList[10] ## 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]
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.null            = varList[0] #called "internalCount", seems to be always null
        self.repetitions     = varList[1] #number of instance repetitions
        self.complexIndex    = varList[2] #index of complex used as the instance
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):
        self.desc=desc
class Field:
    def __init__(self,desc):
        self.desc=desc

numDict={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 Dbx:
    def __init__(self, f):
        #metadata
        self.trueFilename=""
        self.header=Header(unpack("11I",f.read(44)))
        self.arraySectionstart=self.header.absStringOffset+self.header.lenString+self.header.lenPayload
        self.fileGUID, self.primaryInstanceGUID = f.read(16), f.read(16)    
        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(unpack("IHHII",f.read(16)), self.keywordDict) for i in xrange(self.header.numField)]
        self.complexDescriptors=[ComplexDescriptor(unpack("IIBBHHH",f.read(16)), self.keywordDict) for i in xrange(self.header.numComplex)]
        self.instanceRepeaters=[InstanceRepeater(unpack("3I",f.read(12))) for i in xrange(self.header.numInstanceRepeater)] 
        while f.tell()%16!=0: f.seek(1,1) #padding
        self.arrayRepeaters=[arrayRepeater(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)
        for instanceRepeater in self.instanceRepeaters:
            for repetition in xrange(instanceRepeater.repetitions):
                instanceGUID=f.read(16)
                self.internalGUIDs.append(instanceGUID)
                if instanceGUID==self.primaryInstanceGUID: self.isPrimaryInstance=True
                else: self.isPrimaryInstance=False
                
                self.instances.append( (instanceGUID,self.readComplex(instanceRepeater.complexIndex,f)) )
        f.close()
        
        

    def readComplex(self, complexIndex,f):
        complexDesc=self.complexDescriptors[complexIndex]
        cmplx=Complex(complexDesc)
        
        startPos=f.tell()                 
        cmplx.fields=[]
        for fieldIndex in xrange(complexDesc.fieldStartIndex,complexDesc.fieldStartIndex+complexDesc.numField):
            f.seek(startPos+self.fieldDescriptors[fieldIndex].offset)
            cmplx.fields.append(self.readField(fieldIndex,f))
        
        f.seek(startPos+complexDesc.size)
        return cmplx


    def readField(self,fieldIndex,f):
        fieldDesc = self.fieldDescriptors[fieldIndex]
        field=Field(fieldDesc)
        
        if fieldDesc.type in (0x0029, 0xd029,0x0000):
            field.value=self.readComplex(fieldDesc.ref,f)
        elif fieldDesc.type==0x0041:
            arrayRepeater=self.arrayRepeaters[unpack("I",f.read(4))[0]]
            arrayComplexDesc=self.complexDescriptors[fieldDesc.ref]

##            if arrayRepeater.repetitions==0: field.value = "*nullArray*"
            f.seek(self.arraySectionstart+arrayRepeater.offset)
            arrayComplex=Complex(arrayComplexDesc)
            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()
            f.seek(self.header.absStringOffset+unpack("I",f.read(4))[0])
            string=""
            while 1:
                a=f.read(1)
                if a=="\x00": break
                else: string+=a
            f.seek(startPos+4)
            
            if len(string)==0: field.value="*nullString*" #actually the string is ""
            else: field.value=string
            
            if self.isPrimaryInstance and self.trueFilename=="" and fieldDesc.name=="Name": self.trueFilename=string
            
                   
        elif fieldDesc.type==0x0089: #incomplete implementation, only gives back the selected string
            compareValue=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)
        else:
            (typ,length)=numDict[fieldDesc.type]
            num=unpack(typ,f.read(length))[0]
            field.value=num
        
        return field
        

    def dump(self,outputFolder):
        dirName=os.path.dirname(outputFolder+self.trueFilename)
        if not os.path.isdir(dirName): os.makedirs(dirName)
        if not self.trueFilename: self.trueFilename=hexlify(self.fileGUID)
        f2=open(outputFolder+self.trueFilename+EXTENSION,"wb")
        print self.trueFilename
        
        for (guid,instance) in self.instances:
            if instance.desc.name not in IGNOREINSTANCES: #############
                if guid==self.primaryInstanceGUID: f2.write(instance.desc.name+" "+hexlify(guid)+ " #primary instance\r\n")
                else: f2.write(instance.desc.name+" "+hexlify(guid)+ "\r\n")
                self.recurse(instance.fields,f2,0)
        f2.close()

    def recurse(self, fields, f2, lvl): #over fields
        lvl+=1
        for field in fields:
            if field.desc.type in (0xc14d, 0xc0fd, 0xc10d, 0xc0ed, 0xc0dd, 0xc0bd, 0xc0ad, 0x407d, 0x409d, 0x0089):
                f2.write(lvl*SEP+field.desc.name+" "+str(field.value)+"\r\n")
            elif field.desc.type == 0xc13d:
                f2.write(lvl*SEP+field.desc.name+" "+formatfloat(field.value)+"\r\n")
            elif field.desc.type == 0xc15d:
                f2.write(lvl*SEP+field.desc.name+" "+hexlify(field.value).upper()+"\r\n") #upper case=> chunk guid
            elif field.desc.type == 0x0035:
                towrite=""
                if field.value>>31:
                    extguid=self.externalGUIDs[field.value&0x7fffffff]
                    try: towrite=guidTable[extguid[0]]+"/"+hexlify(extguid[1])
                    except: towrite=hexlify(extguid[0])+"/"+hexlify(extguid[1])
                elif field.value==0: towrite="*nullGuid*"
                else: towrite=hexlify(self.internalGUIDs[field.value-1])
                f2.write(lvl*SEP+field.desc.name+" "+towrite+"\r\n") 
            elif field.desc.type==0x0041 and len(field.value.fields)==0:
                f2.write(lvl*SEP+field.desc.name+" "+"*nullArray*"+"\r\n")
            else:
                if field.desc.name not in IGNOREFIELDS: #############
                    f2.write(lvl*SEP+field.desc.name+"::"+field.value.desc.name+"\r\n")
                    self.recurse(field.value.fields,f2,lvl)


if outputFolder[-1] not in ("/","\\"): outputFolder+="/"
if inputFolder[-1] not in ("/","\\"): inputFolder+="/"


#if there's a guid table already, use it
try:
    f5=open(guidTableName,"rb")
    guidTable=cPickle.load(f5)
    f5.close()
except:
    guidTable=dict()


main()
(I'm not at home now so I just copy/pasted this script from an ancient post on this thread so tell me if this script doesn't work)

Cordialy.
Vosvoy
User avatar
FatalBulletHit
beginner
Posts: 32
Joined: Sun Nov 06, 2016 7:29 pm
Has thanked: 12 times
Been thanked: 6 times
Contact:

Re: Frostbite 2 sound extraction research

Post by FatalBulletHit »

Vosvoy wrote:(I'm not at home now so I just copy/pasted this script from an ancient post on this thread so tell me if this script doesn't work)
Works totally fine, had the exact same script already on my HDD but apparently never used it.

Just one question:
"Weapon_SMG_UMP45_Silenced_02.txt" has 5498 lines of code... I understand the 56 paths, but what are the other 5442 lines about? :?
I mean, what is important and how do you know stuff like this:
Vosvoy wrote: [...] put the speed of this sample at -60 in Audacity.
? :oops:
JakeGreen
mega-veteran
mega-veteran
Posts: 164
Joined: Sun Aug 22, 2010 10:14 pm
Has thanked: 40 times
Been thanked: 11 times

Re: Frostbite 2 sound extraction research

Post by JakeGreen »

Any change someone could look at the audio stuff from battlefront 2 not sure if it's the sounds that's changed or the ebx format has changed but if anyone needs anything else like a chunk file to go along with that ebx file tell me the names...ect and i'll send it
You do not have the required permissions to view the files attached to this post.
User avatar
Vosvoy
veteran
Posts: 127
Joined: Fri Feb 18, 2011 4:58 pm
Has thanked: 15 times
Been thanked: 15 times

Re: Frostbite 2 sound extraction research

Post by Vosvoy »

FatalBulletHit wrote:
Vosvoy wrote: Just one question:
"Weapon_SMG_UMP45_Silenced_02.txt" has 5498 lines of code... I understand the 56 paths, but what are the other 5442 lines about? :?
I mean, what is important and how do you know stuff like this:
Vosvoy wrote: [...] put the speed of this sample at -60 in Audacity.
Here:

Wave ebx/sound/weapons/shared_content/hifi_oneshot/hifi_revolver_wave/8cac79083715480ad77264b7c25229c8
BasePitch 0.600000023842
Loop LtNone
ShuffleSegments False
Plugins::array*

I decided a long time ago to mess around with BF3 sounds and I found this conclusion:

+/- 0.10 Ebx BasePitch points = +/- 10 speed points in Audacity

E.g:
> BasePitch: 1.00 = default.
> BasePitch: 0.60 = -40 speed points in Audacity [0.60 (ebx base pitch) - 1.00 (default base pitch) = -0.40].
> BasePitch: 2.00 = +200 speed points in Audacity.
> BasePitch: 1.10 = +10 speed points in Audacity.
> BasePitch: 0.80 = -20 speed points in Audacity.
Etc...

I don't speak english so I don't even know how to explain all this thing, but I have one last advice for you.

Don't even try to understand the whole ebx mumbo jumbo. The most important things are "BasePitch" points and "Wave ebx/sound/..." directories. That's all.

EDIT: I corrected a few things.
Vosvoy
manavortex
ultra-n00b
Posts: 5
Joined: Mon Nov 25, 2019 11:36 pm
Been thanked: 1 time

Re: Frostbite 2 sound extraction research

Post by manavortex »

I've tweaked the python script a little. It will ask for game and extraction paths, if those have not been set, and check if the directories are valid.

https://1drv.ms/u/s!Ahb8fw3iR21phWlB2iI ... n?e=Y9qbT6
mono24
double-veteran
double-veteran
Posts: 840
Joined: Sat Nov 06, 2010 12:27 am
Has thanked: 435 times
Been thanked: 235 times

Re: Frostbite 2 sound extraction research

Post by mono24 »

manavortex wrote: Fri Dec 13, 2019 12:06 pm I've tweaked the python script a little.
Just out of curiosity, was modified to do what exactly? Anything more specific?
Also, what is wrong with the following, because it is more complete than anything else there is out there, above included, which have become fully obsolete.
https://github.com/NicknineTheEagle/Frostbite-Scripts
Will save you lots of headache.
Post Reply