Merge commit '57e889fb45a31abda48930fecf8006ef1db4ffcc' as 'python/fsb5'
This commit is contained in:
15
python/fsb5/.editorconfig
Normal file
15
python/fsb5/.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
# EditorConfig: http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
quote_type = double
|
||||
spaces_around_brackets = none
|
||||
spaces_around_operators = true
|
||||
trim_trailing_whitespace = true
|
||||
61
python/fsb5/.gitignore
vendored
Normal file
61
python/fsb5/.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Samples for testing
|
||||
samples/
|
||||
out/
|
||||
201
python/fsb5/FSB5.bt
Normal file
201
python/fsb5/FSB5.bt
Normal file
@@ -0,0 +1,201 @@
|
||||
//--------------------------------------
|
||||
//--- 010 Editor Binary Template
|
||||
//
|
||||
// File: FSB5.bt
|
||||
// Author: Simon Pinfold
|
||||
// Purpose: Parses the FSB5 (v0 and v1) audio container.
|
||||
//--------------------------------------
|
||||
|
||||
BitfieldDisablePadding();
|
||||
|
||||
string OffsetCalcComment(int offset){
|
||||
string s;
|
||||
SPrintf(s, "Offset = %d from sampleData start", offset * 16);
|
||||
return s;
|
||||
}
|
||||
|
||||
string FrequencyLookupComment(int value){
|
||||
switch (value){
|
||||
case 1: return "8000Hz";
|
||||
case 2: return "11000Hz";
|
||||
case 3: return "11025Hz";
|
||||
case 4: return "16000Hz";
|
||||
case 5: return "22050Hz";
|
||||
case 6: return "24000Hz";
|
||||
case 7: return "32000Hz";
|
||||
case 8: return "44100Hz";
|
||||
case 9: return "48000Hz";
|
||||
default: { Warning("Invalid value for frequency"); return "Unknown"; }
|
||||
}
|
||||
}
|
||||
|
||||
typedef enum<uint32> {
|
||||
NONE = 0,
|
||||
PCM8 = 1,
|
||||
PCM16 = 2,
|
||||
PCM24 = 3,
|
||||
PCM32 = 4,
|
||||
PCMFLOAT = 5,
|
||||
GCADPCM = 6,
|
||||
IMAADPCM = 7,
|
||||
VAG = 8,
|
||||
HEVAG = 9,
|
||||
XMA = 10,
|
||||
MPEG = 11,
|
||||
CELT = 12,
|
||||
AT9 = 13,
|
||||
XWMA = 14,
|
||||
VORBIS = 15
|
||||
} MODE;
|
||||
|
||||
typedef enum<uint32> {
|
||||
CHANNELS=1,
|
||||
FREQUENCY=2,
|
||||
LOOP=3,
|
||||
XMASEEK=6,
|
||||
DSPCOEFF=7,
|
||||
XWMADATA=10,
|
||||
VORBISDATA=11
|
||||
} CHUNK_TYPE;
|
||||
|
||||
typedef struct {
|
||||
char id[4];
|
||||
if (id != "FSB5"){
|
||||
Warning( "File is not FSB5. Template stopped." );
|
||||
return -1;
|
||||
}
|
||||
int32 version;
|
||||
int32 numSamples;
|
||||
int32 sampleHeaderSize;
|
||||
int32 nameTableSize;
|
||||
int32 dataSize;
|
||||
MODE mode;
|
||||
|
||||
byte zero[8];
|
||||
byte hash[16];
|
||||
byte dummy[8];
|
||||
if (version == 0) {
|
||||
uint32 unknown <format=hex, bgcolor=0x0000ff>;
|
||||
}
|
||||
} FSOUND_FSB_HEADER_FSB5;
|
||||
|
||||
typedef struct {
|
||||
|
||||
uint32 extraParams :1;
|
||||
uint32 frequency :4 <comment=FrequencyLookupComment>;
|
||||
uint32 twoChannels :1;
|
||||
uint32 dataOffset :28 <comment=OffsetCalcComment>;
|
||||
uint32 samples :30;
|
||||
|
||||
if (extraParams){ // has extra params
|
||||
|
||||
local int _next = 1;
|
||||
while (_next){
|
||||
struct {
|
||||
uint32 next :1;
|
||||
|
||||
uint32 size :24;
|
||||
CHUNK_TYPE type :7;
|
||||
|
||||
if (type == CHANNELS){
|
||||
Assert(size == 1, "Channels chunk should be 1 byte");
|
||||
byte channels;
|
||||
} else if (type == FREQUENCY){
|
||||
Assert(size == 4, "Frequency chunk should be 4 bytes");
|
||||
uint32 frequency;
|
||||
} else if (type == LOOP){
|
||||
Assert(size == 8, "Frequency chunk should be 8 bytes");
|
||||
struct {
|
||||
uint32 loopstart;
|
||||
uint32 loopend;
|
||||
} loop;
|
||||
} else if (type == XMASEEK){
|
||||
byte xmaSeek[size];
|
||||
} else if (type == DSPCOEFF){
|
||||
byte dspCoefficient[size];
|
||||
} else if (type == XWMADATA){
|
||||
byte xwmaData[size];
|
||||
} else if (type == VORBISDATA){
|
||||
struct {
|
||||
uint32 crc32;
|
||||
|
||||
local int _remain = size - 4;
|
||||
while (_remain > 0){
|
||||
struct {
|
||||
uint32 offset;
|
||||
if ( _remain > 4) uint32 granulePosition;
|
||||
} packetData;
|
||||
_remain -= 8;
|
||||
}
|
||||
|
||||
//byte unknownData[size-4] <format=hex>;
|
||||
} vorbis;
|
||||
} else {
|
||||
byte unknownData[size];
|
||||
}
|
||||
|
||||
} chunk;
|
||||
_next = chunk.next;
|
||||
}
|
||||
}
|
||||
} FSOUND_FSB_SAMPLE_HEADER;
|
||||
|
||||
|
||||
|
||||
|
||||
FSOUND_FSB_HEADER_FSB5 header <bgcolor=0xffbf00>;
|
||||
|
||||
struct {
|
||||
local int i;
|
||||
for (i = 0; i < header.numSamples; i++) {
|
||||
FSOUND_FSB_SAMPLE_HEADER sampleHeader;
|
||||
}
|
||||
} sampleHeaders <bgcolor=0x00ffff>;
|
||||
|
||||
if (header.nameTableSize) {
|
||||
local int nameTableStart = FTell();
|
||||
struct {
|
||||
local int i;
|
||||
for (i = 0; i < header.numSamples; i++) {
|
||||
uint32 nameStart;
|
||||
}
|
||||
for (i = 0; i < header.numSamples; i++) {
|
||||
struct {
|
||||
Assert(nameTableStart + nameTable.nameStart[i] == FTell(), "nameStart did not point to expected start of string");
|
||||
string name;
|
||||
} name;
|
||||
}
|
||||
} nameTable <bgcolor=0x33ff00>;
|
||||
} else {
|
||||
Printf("No name table\n");
|
||||
}
|
||||
|
||||
byte pad[(sizeof(header) + header.sampleHeaderSize + header.nameTableSize) - FTell()];
|
||||
|
||||
struct {
|
||||
local int dataStart = FTell();
|
||||
local int dataEnd = dataStart + header.dataSize;
|
||||
local int i, start, end;
|
||||
for (i = 0; i < header.numSamples; i++) {
|
||||
end = dataEnd;
|
||||
if (i+1 < header.numSamples) end = dataStart + sampleHeaders.sampleHeader[i+1].dataOffset * 16;
|
||||
start = dataStart + sampleHeaders.sampleHeader[i ].dataOffset * 16;
|
||||
Assert(start == FTell(), "Wrong length calculated for sample data");
|
||||
struct {
|
||||
if (header.mode == VORBIS){
|
||||
while (!FEof()){
|
||||
struct {
|
||||
ushort size;
|
||||
|
||||
if (!size)
|
||||
return 0;
|
||||
ubyte audio :1;
|
||||
ubyte r :7;
|
||||
ubyte data[size-1];
|
||||
} packet;
|
||||
}
|
||||
} else
|
||||
byte bytes[end - start] <bgcolor=cLtGray>;
|
||||
} sample;
|
||||
}
|
||||
} sampleData;
|
||||
21
python/fsb5/LICENSE
Normal file
21
python/fsb5/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Pinfold
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
129
python/fsb5/README.md
Normal file
129
python/fsb5/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# python-fsb5
|
||||
Python library and tool to extract FSB5 (FMOD Sample Bank) files.
|
||||
|
||||
### Supported formats
|
||||
|
||||
- MPEG
|
||||
- Vorbis (OGG)
|
||||
- WAVE (PCM8, PCM16, PCM32)
|
||||
|
||||
Other formats can be identified but will be extracted as `.dat` files and may not play as the headers may be missing.
|
||||
|
||||
## Tool Usage
|
||||
|
||||
```
|
||||
usage: extract.py [-h] [-o OUTPUT_DIRECTORY] [-p] [-q]
|
||||
[fsb_file [fsb_file ...]]
|
||||
|
||||
Extract audio samples from FSB5 files
|
||||
|
||||
positional arguments:
|
||||
fsb_file FSB5 container to extract audio from (defaults to
|
||||
stdin)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY
|
||||
output directory to write extracted samples into
|
||||
-q, --quiet suppress output of header and sample information
|
||||
(samples that failed to decode will still be printed)
|
||||
```
|
||||
|
||||
#### Resource files
|
||||
Unity3D packs multiple FSB5 files each containing a single sample into it's `.resource` files.
|
||||
python-fsb5 will automatically extract all samples if multiple FSB5s are found within one file.
|
||||
Output files will be prefixed with the (0 based) index of their FSB container within the resource file e.g. `out/sounds-15-track1.wav` is the path for a WAVE sample named track1 which is contained within the 16th FSB file within sounds.resource.
|
||||
|
||||
#### Unnamed samples
|
||||
FSB5 does not require samples to store a name. If samples are stored without a name they will use their index within the FSB e.g. `sounds-0000.mp3` is the first sample in sounds.fsb.
|
||||
|
||||
## Requirements
|
||||
|
||||
python-fsb5 should work with python3 from version 3.2 and up.
|
||||
|
||||
`libogg` and `libvorbis` are required to decode ogg samples. For linux simply install from your package manager. For windows ensure the dlls are avaliable (ie. in System32 or the directory you are running the script from). Known working dlls are avaliable as part of the [release](https://github.com/HearthSim/python-fsb5/releases/tag/b7bf605).
|
||||
|
||||
If ogg files are not required to be decoded then the libraries are not required.
|
||||
|
||||
## Library usage
|
||||
|
||||
```python
|
||||
import fsb5
|
||||
|
||||
# read the file into a FSB5 object
|
||||
with open('sample.fsb', 'rb') as f:
|
||||
fsb = fsb5.FSB5(f.read())
|
||||
|
||||
print(fsb.header)
|
||||
|
||||
# get the extension of samples based off the sound format specified in the header
|
||||
ext = fsb.get_sample_extension()
|
||||
|
||||
# iterate over samples
|
||||
for sample in fsb.samples:
|
||||
# print sample properties
|
||||
print('''\t{sample.name}.{extension}:
|
||||
Frequency: {sample.frequency}
|
||||
Channels: {sample.channels}
|
||||
Samples: {sample.samples}'''.format(sample=sample, extension=ext))
|
||||
|
||||
# rebuild the sample and save
|
||||
with open('{0}.{1}'.format(sample.name, ext), 'wb') as f:
|
||||
rebuilt_sample = fsb.rebuild_sample(sample)
|
||||
f.write(rebuilt_sample)
|
||||
```
|
||||
|
||||
#### Useful header properties
|
||||
|
||||
- `numSamples`: The number of samples contained in the file
|
||||
- `mode`: The audio format of all samples. Can be one of:
|
||||
* `fsb5.SoundFormat.NONE`
|
||||
* `fsb5.SoundFormat.PCM8`
|
||||
* `fsb5.SoundFormat.PCM16`
|
||||
* `fsb5.SoundFormat.PCM24`
|
||||
* `fsb5.SoundFormat.PCM32`
|
||||
* `fsb5.SoundFormat.PCMFLOAT`
|
||||
* `fsb5.SoundFormat.GCADPCM`
|
||||
* `fsb5.SoundFormat.IMAADPCM`
|
||||
* `fsb5.SoundFormat.VAG`
|
||||
* `fsb5.SoundFormat.HEVAG`
|
||||
* `fsb5.SoundFormat.XMA`
|
||||
* `fsb5.SoundFormat.MPEG`
|
||||
* `fsb5.SoundFormat.CELT`
|
||||
* `fsb5.SoundFormat.AT9`
|
||||
* `fsb5.SoundFormat.XWMA`
|
||||
* `fsb5.SoundFormat.VORBIS`
|
||||
|
||||
|
||||
#### Useful sample properties
|
||||
|
||||
- `name` : The name of the sample, or a 4 digit number if names are not provided.
|
||||
- `frequency` : The sample rate of the audio
|
||||
- `channels` : The number of channels of audio (either 1 or 2)
|
||||
- `samples` : The number of samples in the audio
|
||||
- `metadata` : A dictionary of `fsb5.MetadataChunkType` to tuple (sometimes namedtuple) or bytes.
|
||||
|
||||
All contents of sample.metadata is optional and often not provided. Several metadata types seem to override sample properties.
|
||||
|
||||
Supported `fsb5.MetadataChunkType`s are:
|
||||
* `CHANNELS`: A 1-tuple containing the number of channels
|
||||
* `FREQUENCY`: A 1-tuple containing the sample rate
|
||||
* `LOOP`: A 2-tuple of the loop start and end
|
||||
* `XMASEEK`: Raw bytes
|
||||
* `DSPCOEFF`: Raw bytes
|
||||
* `XWMADATA`: Raw bytes
|
||||
* `VORBISDATA`: A named tuple with properties `crc32` (int) and `unknown` (bytes)
|
||||
|
||||
If a metadata chunk is unrecognized it will be included in the dictionary as an interger mapping to a bytes.
|
||||
|
||||
#### Rebuilding samples
|
||||
|
||||
Samples also have the `data` property.
|
||||
This contains the raw, unprocessed audio data for that sample from the FSB file.
|
||||
To reconstruct a playable version of the audio use `rebuild_sample` on the FSB5 object passing the sample desired to be rebuilt.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
python-fsb5 is licensed under the terms of the MIT license.
|
||||
The full text of the license is available in the LICENSE file.
|
||||
131
python/fsb5/extract.py
Executable file
131
python/fsb5/extract.py
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import fsb5
|
||||
|
||||
|
||||
class FSBExtractor:
|
||||
description = 'Extract audio samples from FSB5 files'
|
||||
|
||||
def __init__(self):
|
||||
self.parser = self.init_parser()
|
||||
|
||||
def init_parser(self):
|
||||
parser = argparse.ArgumentParser(description=self.description)
|
||||
parser.add_argument('fsb_file', nargs='*', type=str,
|
||||
help='FSB5 container to extract audio from (defaults to stdin)'
|
||||
)
|
||||
parser.add_argument('-o', '--output-directory', default='out/',
|
||||
help='output directory to write extracted samples into'
|
||||
)
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
help='be more verbose during extraction'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def print(self, *args):
|
||||
print(*args)
|
||||
|
||||
def debug(self, *args):
|
||||
if self.args.verbose:
|
||||
self.print(*args)
|
||||
|
||||
def error(self, *args):
|
||||
print(*args, file=sys.stderr)
|
||||
|
||||
def write_to_file(self, filename_prefix, filename, extension, contents):
|
||||
directory = self.args.output_directory
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
if filename_prefix:
|
||||
path = os.path.join(directory, '{0}-{1}.{2}'.format(filename_prefix, filename, extension))
|
||||
else:
|
||||
path = os.path.join(directory, '{0}.{1}'.format(filename, extension))
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
written = f.write(contents)
|
||||
return path
|
||||
|
||||
def load_fsb(self, data):
|
||||
fsb = fsb5.load(data)
|
||||
ext = fsb.get_sample_extension()
|
||||
|
||||
self.debug('\nHeader:')
|
||||
self.debug('\tVersion: 5.%s' % (fsb.header.version))
|
||||
self.debug('\tSample count: %i' % (fsb.header.numSamples))
|
||||
self.debug('\tNamed samples: %s' % ('Yes' if fsb.header.nameTableSize else 'No'))
|
||||
self.debug('\tSound format: %s' % (fsb.header.mode.name.capitalize()))
|
||||
|
||||
return fsb, ext
|
||||
|
||||
def read_samples(self, fsb_name, fsb, ext):
|
||||
self.debug('Samples:')
|
||||
for sample in fsb.samples:
|
||||
self.debug('\t%s.%s' % (sample.name, ext))
|
||||
self.debug('\tFrequency: %iHz' % (sample.frequency))
|
||||
self.debug('\tChannels: %i' % (sample.channels))
|
||||
self.debug('\tSamples %r' % (sample.samples))
|
||||
|
||||
if sample.metadata and self.args.verbose:
|
||||
for meta_type, meta_value in sample.metadata.items():
|
||||
if type(meta_type) is fsb5.MetadataChunkType:
|
||||
contents = str(meta_value)
|
||||
if len(contents) > 45:
|
||||
contents = contents[:45] + '... )'
|
||||
self.debug('\t%s: %s' % (meta_type.name, contents))
|
||||
else:
|
||||
self.debug('\t<unknown metadata type: %r>' % (meta_type))
|
||||
|
||||
sample_fakepath = '{0}:{1}.{2}'.format(fsb_name, sample.name, ext)
|
||||
try:
|
||||
yield sample_fakepath, sample.name, fsb.rebuild_sample(sample)
|
||||
except ValueError as e:
|
||||
self.error('FAILED to extract %r: %s' % (sample_fakepath, e))
|
||||
|
||||
def handle_file(self, f):
|
||||
data = f.read()
|
||||
fsb_name = os.path.splitext(os.path.basename(f.name))[0]
|
||||
|
||||
self.debug('Reading FSB5 container: %s' % (f.name))
|
||||
|
||||
is_resource = False
|
||||
index = 0
|
||||
while data:
|
||||
fsb, ext = self.load_fsb(data)
|
||||
|
||||
data = data[fsb.raw_size:]
|
||||
if not is_resource and data:
|
||||
is_resource = True
|
||||
|
||||
sample_prefix = fsb_name
|
||||
fakepath_prefix = fsb_name
|
||||
if is_resource:
|
||||
sample_prefix += '-%d' % (index)
|
||||
fakepath_prefix += ':%d' % (index)
|
||||
for sample_fakepath, sample_name, sample_data in self.read_samples(fakepath_prefix, fsb, ext):
|
||||
outpath = self.write_to_file(sample_prefix, sample_name, ext, sample_data)
|
||||
self.print('%r -> %r' % (sample_fakepath, outpath))
|
||||
|
||||
index += 1
|
||||
|
||||
def run(self, args):
|
||||
self.args = self.parser.parse_args(args)
|
||||
|
||||
for fname in self.args.fsb_file:
|
||||
with open(fname, 'rb') as f:
|
||||
self.handle_file(f)
|
||||
|
||||
return 0
|
||||
|
||||
def main():
|
||||
app = FSBExtractor()
|
||||
exit(app.run(sys.argv[1:]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
230
python/fsb5/fsb5/__init__.py
Normal file
230
python/fsb5/fsb5/__init__.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from collections import namedtuple
|
||||
from enum import IntEnum
|
||||
from io import BytesIO
|
||||
|
||||
from .utils import BinaryReader
|
||||
|
||||
|
||||
__version__ = "1.0"
|
||||
__author__ = "Simon Pinfold"
|
||||
__email__ = "simon@uint8.me"
|
||||
|
||||
|
||||
class SoundFormat(IntEnum):
|
||||
NONE = 0
|
||||
PCM8 = 1
|
||||
PCM16 = 2
|
||||
PCM24 = 3
|
||||
PCM32 = 4
|
||||
PCMFLOAT = 5
|
||||
GCADPCM = 6
|
||||
IMAADPCM = 7
|
||||
VAG = 8
|
||||
HEVAG = 9
|
||||
XMA = 10
|
||||
MPEG = 11
|
||||
CELT = 12
|
||||
AT9 = 13
|
||||
XWMA = 14
|
||||
VORBIS = 15
|
||||
|
||||
@property
|
||||
def file_extension(self):
|
||||
if self == SoundFormat.MPEG:
|
||||
return "mp3"
|
||||
elif self == SoundFormat.VORBIS:
|
||||
return "ogg"
|
||||
elif self.is_pcm:
|
||||
return "wav"
|
||||
return "bin"
|
||||
|
||||
@property
|
||||
def is_pcm(self):
|
||||
return self in (SoundFormat.PCM8, SoundFormat.PCM16, SoundFormat.PCM32)
|
||||
|
||||
|
||||
FSB5Header = namedtuple("FSB5Header", [
|
||||
"id",
|
||||
"version",
|
||||
"numSamples",
|
||||
"sampleHeadersSize",
|
||||
"nameTableSize",
|
||||
"dataSize",
|
||||
"mode",
|
||||
|
||||
"zero",
|
||||
"hash",
|
||||
"dummy",
|
||||
|
||||
"unknown",
|
||||
|
||||
"size"
|
||||
])
|
||||
|
||||
Sample = namedtuple("Sample", [
|
||||
"name",
|
||||
"frequency",
|
||||
"channels",
|
||||
"dataOffset",
|
||||
"samples",
|
||||
|
||||
"metadata",
|
||||
|
||||
"data"
|
||||
])
|
||||
|
||||
frequency_values = {
|
||||
1: 8000,
|
||||
2: 11000,
|
||||
3: 11025,
|
||||
4: 16000,
|
||||
5: 22050,
|
||||
6: 24000,
|
||||
7: 32000,
|
||||
8: 44100,
|
||||
9: 48000
|
||||
}
|
||||
|
||||
class MetadataChunkType(IntEnum):
|
||||
CHANNELS = 1
|
||||
FREQUENCY = 2
|
||||
LOOP = 3
|
||||
XMASEEK = 6
|
||||
DSPCOEFF = 7
|
||||
XWMADATA = 10
|
||||
VORBISDATA = 11
|
||||
|
||||
chunk_data_format = {
|
||||
MetadataChunkType.CHANNELS : "B",
|
||||
MetadataChunkType.FREQUENCY: "I",
|
||||
MetadataChunkType.LOOP: "II"
|
||||
}
|
||||
|
||||
VorbisData = namedtuple("VorbisData", ["crc32", "unknown"])
|
||||
|
||||
|
||||
def bits(val, start, len):
|
||||
stop = start + len
|
||||
r = val & ((1<<stop)-1)
|
||||
return r >> start
|
||||
|
||||
|
||||
class FSB5:
|
||||
def __init__(self, data):
|
||||
buf = BinaryReader(BytesIO(data), endian="<")
|
||||
|
||||
magic = buf.read(4)
|
||||
if magic != b"FSB5":
|
||||
raise ValueError("Expected magic header 'FSB5' but got %r" % (magic))
|
||||
|
||||
buf.seek(0)
|
||||
self.header = buf.read_struct_into(FSB5Header, "4s I I I I I I 8s 16s 8s")
|
||||
if self.header.version == 0:
|
||||
self.header = self.header._replace(unknown=buf.read_type("I"))
|
||||
self.header = self.header._replace(mode=SoundFormat(self.header.mode), size=buf.tell())
|
||||
|
||||
self.raw_size = self.header.size + self.header.sampleHeadersSize + self.header.nameTableSize + self.header.dataSize
|
||||
|
||||
self.samples = []
|
||||
for i in range(self.header.numSamples):
|
||||
raw = buf.read_type("Q")
|
||||
next_chunk = bits(raw, 0, 1)
|
||||
frequency = bits(raw, 1, 4)
|
||||
channels = bits(raw, 1+4, 1) + 1
|
||||
dataOffset = bits(raw, 1+4+1, 28) * 16
|
||||
samples = bits(raw, 1+4+1+28, 30)
|
||||
|
||||
chunks = {}
|
||||
while next_chunk:
|
||||
raw = buf.read_type("I")
|
||||
next_chunk = bits(raw, 0, 1)
|
||||
chunk_size = bits(raw, 1, 24)
|
||||
chunk_type = bits(raw, 1+24, 7)
|
||||
|
||||
try:
|
||||
chunk_type = MetadataChunkType(chunk_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if chunk_type == MetadataChunkType.VORBISDATA:
|
||||
chunk_data = VorbisData(
|
||||
crc32 = buf.read_type("I"),
|
||||
unknown = buf.read(chunk_size-4)
|
||||
)
|
||||
elif chunk_type in chunk_data_format:
|
||||
fmt = chunk_data_format[chunk_type]
|
||||
if buf.struct_calcsize(fmt) != chunk_size:
|
||||
err = "Expected chunk %s of size %d, SampleHeader specified %d" % (
|
||||
chunk_type, buf.struct_calcsize(fmt), chunk_size
|
||||
)
|
||||
raise ValueError(err)
|
||||
chunk_data = buf.read_struct(fmt)
|
||||
else:
|
||||
chunk_data = buf.read(chunk_size)
|
||||
|
||||
chunks[chunk_type] = chunk_data
|
||||
|
||||
if MetadataChunkType.FREQUENCY in chunks:
|
||||
frequency = chunks[MetadataChunkType.FREQUENCY][0]
|
||||
elif frequency in frequency_values:
|
||||
frequency = frequency_values[frequency]
|
||||
else:
|
||||
raise ValueError("Frequency value %d is not valid and no FREQUENCY metadata chunk was provided")
|
||||
|
||||
self.samples.append(Sample(
|
||||
name="%04d" % (i),
|
||||
frequency=frequency,
|
||||
channels=channels,
|
||||
dataOffset=dataOffset,
|
||||
samples=samples,
|
||||
metadata=chunks,
|
||||
data=None
|
||||
))
|
||||
|
||||
if self.header.nameTableSize:
|
||||
nametable_start = buf.tell()
|
||||
|
||||
samplename_offsets = []
|
||||
for i in range(self.header.numSamples):
|
||||
samplename_offsets.append(buf.read_type("I"))
|
||||
|
||||
for i in range(self.header.numSamples):
|
||||
buf.seek(nametable_start + samplename_offsets[i])
|
||||
name = buf.read_string(maxlen=self.header.nameTableSize)
|
||||
self.samples[i] = self.samples[i]._replace(name=name.decode("utf-8"))
|
||||
|
||||
buf.seek(self.header.size + self.header.sampleHeadersSize + self.header.nameTableSize)
|
||||
for i in range(self.header.numSamples):
|
||||
data_start = self.samples[i].dataOffset
|
||||
data_end = data_start + self.header.dataSize
|
||||
if i < self.header.numSamples-1:
|
||||
data_end = self.samples[i+1].dataOffset
|
||||
self.samples[i] = self.samples[i]._replace(data=buf.read(data_end - data_start))
|
||||
|
||||
def rebuild_sample(self, sample):
|
||||
if sample not in self.samples:
|
||||
raise ValueError("Sample to decode did not originate from the FSB archive decoding it")
|
||||
if self.header.mode == SoundFormat.MPEG:
|
||||
return sample.data
|
||||
elif self.header.mode == SoundFormat.VORBIS:
|
||||
# import here as vorbis.py requires native libraries
|
||||
from . import vorbis
|
||||
return vorbis.rebuild(sample)
|
||||
elif self.header.mode.is_pcm:
|
||||
from .pcm import rebuild
|
||||
if self.header.mode == SoundFormat.PCM8:
|
||||
width = 1
|
||||
elif self.header.mode == SoundFormat.PCM16:
|
||||
width = 2
|
||||
else:
|
||||
width = 4
|
||||
return rebuild(sample, width)
|
||||
|
||||
raise NotImplementedError("Decoding samples of type %s is not supported" % (self.header.mode))
|
||||
|
||||
def get_sample_extension(self):
|
||||
return self.header.mode.file_extension
|
||||
|
||||
|
||||
def load(data):
|
||||
return FSB5(data)
|
||||
11
python/fsb5/fsb5/pcm.py
Normal file
11
python/fsb5/fsb5/pcm.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import wave
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def rebuild(sample, width):
|
||||
data = sample.data[:sample.samples * width]
|
||||
ret = BytesIO()
|
||||
with wave.open(ret, "wb") as wav:
|
||||
wav.setparams((sample.channels, width, sample.frequency, 0, "NONE", "NONE"))
|
||||
wav.writeframes(data)
|
||||
return ret.getvalue()
|
||||
76
python/fsb5/fsb5/utils.py
Normal file
76
python/fsb5/fsb5/utils.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import ctypes
|
||||
import struct
|
||||
|
||||
|
||||
class BinaryReader:
|
||||
def __init__(self, buf, endian="<"):
|
||||
self.buf = buf
|
||||
self.endian = endian
|
||||
self.seek(0, 2)
|
||||
self.size = self.tell()
|
||||
self.seek(0)
|
||||
|
||||
def read(self, *args):
|
||||
return self.buf.read(*args)
|
||||
|
||||
def seek(self, *args):
|
||||
return self.buf.seek(*args)
|
||||
|
||||
def tell(self):
|
||||
return self.buf.tell()
|
||||
|
||||
def finished(self):
|
||||
return self.tell() == self.size
|
||||
|
||||
def read_string(self, maxlen=0):
|
||||
r = []
|
||||
start = self.tell()
|
||||
while maxlen == 0 or len(r) <= maxlen:
|
||||
c = self.read(1)
|
||||
if not c:
|
||||
raise ValueError("Unterminated string starting at %d" % (start))
|
||||
if c == b"\0":
|
||||
break
|
||||
r.append(c)
|
||||
return b"".join(r)
|
||||
|
||||
def struct_calcsize(self, fmt):
|
||||
return struct.calcsize(fmt)
|
||||
|
||||
def read_struct(self, fmt, endian=None):
|
||||
fmt = (endian or self.endian) + fmt;
|
||||
fmtlen = struct.calcsize(fmt)
|
||||
data = self.read(fmtlen)
|
||||
if len(data) != fmtlen:
|
||||
raise ValueError("Not enough bytes left in buffer to read struct")
|
||||
return struct.unpack(fmt, data)
|
||||
|
||||
def read_struct_into(self, dest, fmt, endian=None):
|
||||
fields = self.read_struct(fmt, endian=endian)
|
||||
fields = list(fields) + [None] * (len(dest._fields) - len(fields))
|
||||
return dest._make(fields)
|
||||
|
||||
def read_type(self, type_fmt, endian=None):
|
||||
r = self.read_struct(type_fmt, endian=endian)
|
||||
if len(r) != 1:
|
||||
raise ValueError("Format %r did not describe a single type" % (type_fmt))
|
||||
return r[0]
|
||||
|
||||
|
||||
class LibraryNotFoundException(OSError):
|
||||
pass
|
||||
|
||||
|
||||
def load_lib(*names):
|
||||
for name in names:
|
||||
try:
|
||||
libname = ctypes.util.find_library(name)
|
||||
if libname:
|
||||
return ctypes.CDLL(libname)
|
||||
else:
|
||||
dll_path = os.path.join(os.getcwd(), "lib%s.dll" % (name))
|
||||
return ctypes.CDLL(dll_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise LibraryNotFoundException("Could not load the library %r" % (names[0]))
|
||||
379
python/fsb5/fsb5/vorbis.py
Normal file
379
python/fsb5/fsb5/vorbis.py
Normal file
@@ -0,0 +1,379 @@
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
from enum import IntEnum
|
||||
from io import BytesIO
|
||||
|
||||
from . import *
|
||||
from .utils import BinaryReader, load_lib
|
||||
from .vorbis_headers import lookup as vorbis_header_lookup
|
||||
|
||||
|
||||
vorbis = load_lib('vorbis')
|
||||
ogg = load_lib('ogg')
|
||||
|
||||
|
||||
class VorbisInfo(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/vorbis/doc/libvorbis/vorbis_info.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('version', ctypes.c_int),
|
||||
('channels', ctypes.c_int),
|
||||
('rate', ctypes.c_long),
|
||||
('bitrate_upper', ctypes.c_long),
|
||||
('bitrate_nominal', ctypes.c_long),
|
||||
('bitrate_lower', ctypes.c_long),
|
||||
('bitrate_window', ctypes.c_long),
|
||||
('codec_setup', ctypes.c_void_p),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
vorbis.vorbis_info_init(self)
|
||||
|
||||
def __del__(self):
|
||||
vorbis.vorbis_info_clear(self)
|
||||
|
||||
|
||||
class VorbisComment(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/vorbis/doc/libvorbis/vorbis_info.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('user_comments', ctypes.POINTER(ctypes.c_char_p)),
|
||||
('comment_lengths', ctypes.POINTER(ctypes.c_int)),
|
||||
('comments', ctypes.c_int),
|
||||
('vendor', ctypes.c_char_p)
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
vorbis.vorbis_comment_init(self)
|
||||
|
||||
def __del__(self):
|
||||
vorbis.vorbis_comment_clear(self)
|
||||
|
||||
|
||||
class VorbisDSPState(ctypes.Structure):
|
||||
"""
|
||||
https://svn.xiph.org/trunk/vorbis/include/vorbis/codec.h
|
||||
"""
|
||||
_fields_ = [
|
||||
('analysisp', ctypes.c_int),
|
||||
('vi', ctypes.c_void_p),
|
||||
('pcm', ctypes.POINTER(ctypes.POINTER(ctypes.c_float))),
|
||||
('pcmret', ctypes.POINTER(ctypes.POINTER(ctypes.c_float))),
|
||||
('pcm_storage', ctypes.c_int),
|
||||
('pcm_current', ctypes.c_int),
|
||||
('pcm_returned', ctypes.c_int),
|
||||
('preextrapolate', ctypes.c_int),
|
||||
('eofflag', ctypes.c_int),
|
||||
('lW', ctypes.c_long),
|
||||
('W', ctypes.c_long),
|
||||
('nW', ctypes.c_long),
|
||||
('centerW', ctypes.c_long),
|
||||
('granulepos', ctypes.c_longlong),
|
||||
('sequence', ctypes.c_longlong),
|
||||
('glue_bits', ctypes.c_longlong),
|
||||
('time_bits', ctypes.c_longlong),
|
||||
('floor_bits', ctypes.c_longlong),
|
||||
('res_bits', ctypes.c_longlong),
|
||||
('backend_state', ctypes.c_void_p)
|
||||
]
|
||||
|
||||
|
||||
class OggStreamState(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/ogg/doc/libogg/ogg_stream_state.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('body_data', ctypes.POINTER(ctypes.c_char)),
|
||||
('body_storage', ctypes.c_long),
|
||||
('body_fill', ctypes.c_long),
|
||||
('body_returned', ctypes.c_long),
|
||||
('lacing_vals', ctypes.POINTER(ctypes.c_int)),
|
||||
('granule_vals', ctypes.POINTER(ctypes.c_longlong)),
|
||||
('lacing_storage', ctypes.c_long),
|
||||
('lacing_fill', ctypes.c_long),
|
||||
('lacing_packet', ctypes.c_long),
|
||||
('lacing_returned', ctypes.c_long),
|
||||
('header', ctypes.c_char * 282),
|
||||
('header_fill', ctypes.c_int),
|
||||
('e_o_s', ctypes.c_int),
|
||||
('b_o_s', ctypes.c_int),
|
||||
('serialno', ctypes.c_long),
|
||||
('pageno', ctypes.c_int),
|
||||
('packetno', ctypes.c_longlong),
|
||||
('granulepos', ctypes.c_longlong)
|
||||
]
|
||||
|
||||
def __init__(self, serialno):
|
||||
super().__init__()
|
||||
ogg.ogg_stream_init(self, serialno)
|
||||
|
||||
def __del__(self):
|
||||
ogg.ogg_stream_clear(self)
|
||||
|
||||
|
||||
class OggPacket(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/ogg/doc/libogg/ogg_packet.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('packet', ctypes.POINTER(ctypes.c_char)),
|
||||
('bytes', ctypes.c_long),
|
||||
('b_o_s', ctypes.c_long),
|
||||
('e_o_s', ctypes.c_long),
|
||||
('granulepos', ctypes.c_longlong),
|
||||
('packetno', ctypes.c_longlong)
|
||||
]
|
||||
|
||||
class OggpackBuffer(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/ogg/doc/libogg/oggpack_buffer.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('endbyte', ctypes.c_long),
|
||||
('endbit', ctypes.c_int),
|
||||
('buffer', ctypes.POINTER(ctypes.c_char)),
|
||||
('ptr', ctypes.POINTER(ctypes.c_char)),
|
||||
('storage', ctypes.c_long)
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
ogg.oggpack_writeinit(self)
|
||||
|
||||
def __del__(self):
|
||||
ogg.oggpack_writeclear(self)
|
||||
|
||||
class OggPage(ctypes.Structure):
|
||||
"""
|
||||
https://xiph.org/ogg/doc/libogg/oggpack_buffer.html
|
||||
"""
|
||||
_fields_ = [
|
||||
('header', ctypes.POINTER(ctypes.c_char)),
|
||||
('header_len', ctypes.c_long),
|
||||
('body', ctypes.POINTER(ctypes.c_char)),
|
||||
('body_len', ctypes.c_long)
|
||||
]
|
||||
|
||||
def errcheck(result, func, arguments):
|
||||
if result != 0:
|
||||
raise OSError('Call to %s(%s) returned %d (error)' % (func.__name__, ', '.join(str(x) for x in arguments), result))
|
||||
return result == 0
|
||||
|
||||
######## libvorbis functions ########
|
||||
|
||||
vorbis.vorbis_info_init.argtypes = [ctypes.POINTER(VorbisInfo)]
|
||||
vorbis.vorbis_info_init.restype = None
|
||||
|
||||
vorbis.vorbis_info_clear.argtypes = [ctypes.POINTER(VorbisInfo)]
|
||||
vorbis.vorbis_info_clear.restype = None
|
||||
|
||||
vorbis.vorbis_comment_init.argtypes = [ctypes.POINTER(VorbisComment)]
|
||||
vorbis.vorbis_comment_init.restype = None
|
||||
|
||||
vorbis.vorbis_comment_clear.argtypes = [ctypes.POINTER(VorbisComment)]
|
||||
vorbis.vorbis_comment_clear.restype = None
|
||||
|
||||
vorbis.vorbis_analysis_init.argtypes = [ctypes.POINTER(VorbisDSPState), ctypes.POINTER(VorbisInfo)]
|
||||
vorbis.vorbis_analysis_init.errcheck = errcheck
|
||||
|
||||
vorbis.vorbis_analysis_headerout.argtypes = [
|
||||
ctypes.POINTER(VorbisDSPState),
|
||||
ctypes.POINTER(VorbisComment),
|
||||
ctypes.POINTER(OggPacket),
|
||||
ctypes.POINTER(OggPacket),
|
||||
ctypes.POINTER(OggPacket)
|
||||
]
|
||||
vorbis.vorbis_analysis_headerout.errcheck = errcheck
|
||||
|
||||
vorbis.vorbis_dsp_clear.argtypes = [ctypes.POINTER(VorbisDSPState)]
|
||||
vorbis.vorbis_dsp_clear.restype = None
|
||||
|
||||
vorbis.vorbis_commentheader_out.argtypes = [ctypes.POINTER(VorbisComment), ctypes.POINTER(OggPacket)]
|
||||
vorbis.vorbis_commentheader_out.errcheck = errcheck
|
||||
|
||||
vorbis.vorbis_synthesis_headerin.argtypes = [
|
||||
ctypes.POINTER(VorbisInfo),
|
||||
ctypes.POINTER(VorbisComment),
|
||||
ctypes.POINTER(OggPacket)
|
||||
]
|
||||
vorbis.vorbis_synthesis_headerin.errcheck = errcheck
|
||||
|
||||
|
||||
def vorbis_packet_blocksize_errcheck(result, func, arguments):
|
||||
if result < 0:
|
||||
errcheck(result, func, arguments)
|
||||
return result
|
||||
|
||||
vorbis.vorbis_packet_blocksize.argtypes = [ctypes.POINTER(VorbisInfo), ctypes.POINTER(OggPacket)]
|
||||
vorbis.vorbis_packet_blocksize.errcheck = vorbis_packet_blocksize_errcheck
|
||||
|
||||
|
||||
######## libogg functions ########
|
||||
|
||||
ogg.ogg_stream_init.argtypes = [ctypes.POINTER(OggStreamState), ctypes.c_int]
|
||||
ogg.ogg_stream_init.errcheck = errcheck
|
||||
|
||||
ogg.ogg_stream_clear.argtypes = [ctypes.POINTER(OggStreamState)]
|
||||
ogg.ogg_stream_clear.restype = ctypes.c_int
|
||||
|
||||
ogg.oggpack_writeinit.argtypes = [ctypes.POINTER(OggpackBuffer)]
|
||||
ogg.oggpack_writeinit.restype = None
|
||||
|
||||
ogg.oggpack_write.argtypes = [ctypes.POINTER(OggpackBuffer), ctypes.c_ulong, ctypes.c_int]
|
||||
ogg.oggpack_write.restype = None
|
||||
|
||||
ogg.oggpack_writeclear.argtypes = [ctypes.POINTER(OggpackBuffer)]
|
||||
ogg.oggpack_writeclear.restype = None
|
||||
|
||||
ogg.oggpack_bytes.argtypes = [ctypes.POINTER(OggpackBuffer)]
|
||||
ogg.oggpack_bytes.restype = ctypes.c_int
|
||||
|
||||
ogg.oggpack_writeclear.argtypes = [ctypes.POINTER(OggpackBuffer)]
|
||||
ogg.oggpack_writeclear.restype = None
|
||||
|
||||
ogg.ogg_packet_clear.argtypes = [ctypes.POINTER(OggPacket)]
|
||||
ogg.ogg_packet_clear.restype = None
|
||||
|
||||
ogg.ogg_stream_packetin.argtypes = [ctypes.POINTER(OggStreamState), ctypes.POINTER(OggPacket)]
|
||||
ogg.ogg_stream_packetin.errcheck = errcheck
|
||||
|
||||
ogg.ogg_stream_pageout.argtypes = [ctypes.POINTER(OggStreamState), ctypes.POINTER(OggPage)]
|
||||
ogg.ogg_stream_pageout.restype = ctypes.c_int
|
||||
|
||||
ogg.ogg_stream_flush.argtypes = [ctypes.POINTER(OggStreamState), ctypes.POINTER(OggPage)]
|
||||
ogg.ogg_stream_flush.restype = ctypes.c_int
|
||||
|
||||
if hasattr(ogg, 'oggpack_writecheck'):
|
||||
ogg.oggpack_writecheck.argtypes = [ctypes.POINTER(OggpackBuffer)]
|
||||
ogg.oggpack_writecheck.errcheck = errcheck
|
||||
|
||||
|
||||
def rebuild(sample):
|
||||
if MetadataChunkType.VORBISDATA not in sample.metadata:
|
||||
raise ValueError('Expected sample header to contain a VORBISDATA chunk but none was found')
|
||||
|
||||
crc32 = sample.metadata[MetadataChunkType.VORBISDATA].crc32
|
||||
try:
|
||||
setup_packet_buff = vorbis_header_lookup[crc32]
|
||||
except KeyError as e:
|
||||
raise ValueError('Could not find header info for crc32=%d' % crc32) from e
|
||||
|
||||
info = VorbisInfo()
|
||||
comment = VorbisComment()
|
||||
state = OggStreamState(1)
|
||||
outbuf = BytesIO()
|
||||
|
||||
id_header = rebuild_id_header(sample.channels, sample.frequency, 0x100, 0x800)
|
||||
comment_header = rebuild_comment_header()
|
||||
setup_header = rebuild_setup_header(setup_packet_buff)
|
||||
|
||||
vorbis.vorbis_synthesis_headerin(info, comment, id_header)
|
||||
vorbis.vorbis_synthesis_headerin(info, comment, comment_header)
|
||||
vorbis.vorbis_synthesis_headerin(info, comment, setup_header)
|
||||
|
||||
ogg.ogg_stream_packetin(state, id_header)
|
||||
write_packets(state, outbuf)
|
||||
ogg.ogg_stream_packetin(state, comment_header)
|
||||
write_packets(state, outbuf)
|
||||
ogg.ogg_stream_packetin(state, setup_header)
|
||||
write_packets(state, outbuf)
|
||||
write_packets(state, outbuf, func=ogg.ogg_stream_flush)
|
||||
|
||||
packetno = setup_header.packetno
|
||||
granulepos = 0
|
||||
prev_blocksize = 0
|
||||
|
||||
inbuf = BinaryReader(BytesIO(sample.data))
|
||||
packet_size = inbuf.read_type('H')
|
||||
while packet_size:
|
||||
packetno += 1
|
||||
|
||||
packet = OggPacket()
|
||||
buf = ctypes.create_string_buffer(inbuf.read(packet_size), packet_size)
|
||||
packet.packet = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))
|
||||
packet.bytes = packet_size
|
||||
packet.packetno = packetno
|
||||
|
||||
try:
|
||||
packet_size = inbuf.read_type('H')
|
||||
except ValueError:
|
||||
packet_size = 0
|
||||
packet.e_o_s = 1 if not packet_size else 0
|
||||
|
||||
blocksize = vorbis.vorbis_packet_blocksize(info, packet)
|
||||
assert blocksize
|
||||
|
||||
granulepos = int(granulepos + (blocksize + prev_blocksize) / 4) if prev_blocksize else 0
|
||||
packet.granulepos = granulepos
|
||||
prev_blocksize = blocksize
|
||||
|
||||
ogg.ogg_stream_packetin(state, packet)
|
||||
write_packets(state, outbuf)
|
||||
|
||||
return outbuf.getbuffer()
|
||||
|
||||
|
||||
def write_packets(state, buf, func=ogg.ogg_stream_pageout):
|
||||
page = OggPage()
|
||||
while func(state, page):
|
||||
buf.write(bytes(page.header[:page.header_len]))
|
||||
buf.write(bytes(page.body[:page.body_len]))
|
||||
|
||||
|
||||
def rebuild_id_header(channels, frequency, blocksize_short, blocksize_long):
|
||||
packet = OggPacket()
|
||||
|
||||
buf = OggpackBuffer()
|
||||
ogg.oggpack_write(buf, 0x01, 8)
|
||||
for c in 'vorbis':
|
||||
ogg.oggpack_write(buf, ord(c), 8)
|
||||
ogg.oggpack_write(buf, 0, 32)
|
||||
ogg.oggpack_write(buf, channels, 8)
|
||||
ogg.oggpack_write(buf, frequency, 32)
|
||||
ogg.oggpack_write(buf, 0, 32)
|
||||
ogg.oggpack_write(buf, 0, 32)
|
||||
ogg.oggpack_write(buf, 0, 32)
|
||||
ogg.oggpack_write(buf, len(bin(blocksize_short)) - 3, 4)
|
||||
ogg.oggpack_write(buf, len(bin(blocksize_long)) - 3, 4)
|
||||
ogg.oggpack_write(buf, 1, 1)
|
||||
|
||||
if hasattr(ogg, 'oggpack_writecheck'):
|
||||
ogg.oggpack_writecheck(buf)
|
||||
|
||||
packet.bytes = ogg.oggpack_bytes(buf)
|
||||
buf = ctypes.create_string_buffer(bytes(buf.buffer[:packet.bytes]), packet.bytes)
|
||||
packet.packet = ctypes.cast(ctypes.pointer(buf), ctypes.POINTER(ctypes.c_char))
|
||||
packet.b_o_s = 1
|
||||
packet.e_o_s = 0
|
||||
packet.granulepos = 0
|
||||
packet.packetno = 0
|
||||
|
||||
return packet
|
||||
|
||||
|
||||
def rebuild_comment_header():
|
||||
packet = OggPacket()
|
||||
ogg.ogg_packet_clear(packet)
|
||||
|
||||
comment = VorbisComment()
|
||||
vorbis.vorbis_commentheader_out(comment, packet)
|
||||
|
||||
return packet
|
||||
|
||||
|
||||
def rebuild_setup_header(setup_packet_buff):
|
||||
packet = OggPacket()
|
||||
|
||||
packet.packet = ctypes.cast(ctypes.pointer(ctypes.create_string_buffer(setup_packet_buff, len(setup_packet_buff))), ctypes.POINTER(ctypes.c_char))
|
||||
packet.bytes = len(setup_packet_buff)
|
||||
packet.b_o_s = 0
|
||||
packet.e_o_s = 0
|
||||
packet.granulepos = 0
|
||||
packet.packetno = 2
|
||||
|
||||
return packet
|
||||
30321
python/fsb5/fsb5/vorbis_headers.py
Normal file
30321
python/fsb5/fsb5/vorbis_headers.py
Normal file
File diff suppressed because it is too large
Load Diff
101
python/fsb5/scripts/generate_vorbis_header_lookup.py
Normal file
101
python/fsb5/scripts/generate_vorbis_header_lookup.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import pefile
|
||||
import struct
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
def main():
|
||||
pp = argparse.ArgumentParser(description='Dump FSB5 vorbis headers from a 32-bit Windows executable')
|
||||
pp.add_argument('file', help='path to executable file')
|
||||
args = pp.parse_args()
|
||||
pe = pefile.PE(args.file)
|
||||
lookup = {}
|
||||
base_addr = pe.OPTIONAL_HEADER.ImageBase
|
||||
|
||||
# reimplementation of get_memory_mapped_image because pefile did not survive
|
||||
# the port to python 3 fully in tact...
|
||||
mapped_data = pe.__data__[:]
|
||||
for section in pe.sections:
|
||||
|
||||
# Miscellaneous integrity tests.
|
||||
# Some packer will set these to bogus values to make tools go nuts.
|
||||
if section.Misc_VirtualSize == 0 or section.SizeOfRawData == 0:
|
||||
continue
|
||||
|
||||
if section.SizeOfRawData > len(pe.__data__):
|
||||
continue
|
||||
|
||||
if pe.adjust_FileAlignment( section.PointerToRawData,
|
||||
pe.OPTIONAL_HEADER.FileAlignment ) > len(pe.__data__):
|
||||
|
||||
continue
|
||||
|
||||
VirtualAddress_adj = pe.adjust_SectionAlignment( section.VirtualAddress,
|
||||
pe.OPTIONAL_HEADER.SectionAlignment, pe.OPTIONAL_HEADER.FileAlignment )
|
||||
|
||||
padding_length = VirtualAddress_adj - len(mapped_data)
|
||||
|
||||
if padding_length>0:
|
||||
mapped_data += b'\0'*padding_length
|
||||
elif padding_length<0:
|
||||
mapped_data = mapped_data[:padding_length]
|
||||
|
||||
mapped_data += section.get_data()
|
||||
|
||||
vas = []
|
||||
va = 0
|
||||
while True:
|
||||
va = mapped_data.find(b'\x05vorbis', va)
|
||||
if va > 0:
|
||||
vas.append(base_addr + va)
|
||||
va += 7
|
||||
else:
|
||||
break
|
||||
|
||||
refs = []
|
||||
for va in vas:
|
||||
word = struct.pack('<I', va)
|
||||
ref = 0
|
||||
while True:
|
||||
ref = mapped_data.find(word, ref)
|
||||
if ref > 0:
|
||||
refs.append(base_addr + ref)
|
||||
ref += 4
|
||||
else:
|
||||
break
|
||||
refs.sort()
|
||||
|
||||
table_addr = 0
|
||||
for ref in refs:
|
||||
if (ref - 24 not in refs and ref - 12 not in refs) and (ref + 24 in refs or ref + 36 in refs):
|
||||
table_addr = ref
|
||||
break
|
||||
|
||||
if table_addr == 0:
|
||||
raise
|
||||
|
||||
ea = table_addr
|
||||
while True:
|
||||
ea_ofs = ea - base_addr
|
||||
buf1, len1, crc, buf2, ofs, len2 = struct.unpack('<IIIIII', mapped_data[ea_ofs:ea_ofs+0x18])
|
||||
if crc == 0 or len1 == 0 or buf1 == 0 or len1 > 8192 or len2 > 8192:
|
||||
break
|
||||
hdr = bytearray(len1)
|
||||
base_buf = buf1
|
||||
if buf2 != 0:
|
||||
base_buf = buf2
|
||||
for j in range(len1):
|
||||
hdr[j] = mapped_data[base_buf + j - base_addr]
|
||||
if buf2 != 0:
|
||||
for j in range(len2):
|
||||
hdr[ofs+j] = mapped_data[buf1 + j - base_addr]
|
||||
lookup[crc] = bytes(hdr)
|
||||
ea += 24
|
||||
print('lookup = ', end='')
|
||||
pprint(lookup)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
31
python/fsb5/setup.py
Executable file
31
python/fsb5/setup.py
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import fsb5
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
CLASSIFIERS = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Topic :: Multimedia :: Sound/Audio",
|
||||
"Topic :: Multimedia :: Sound/Audio :: Conversion",
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name="fsb5",
|
||||
version=fsb5.__version__,
|
||||
author=fsb5.__author__,
|
||||
author_email=fsb5.__email__,
|
||||
description="Library and to extract audio from FSB5 (FMOD Sample Bank) files",
|
||||
download_url="https://github.com/HearthSim/python-fsb5/tarball/master",
|
||||
license="MIT",
|
||||
url="https://github.com/HearthSim/python-fsb5",
|
||||
classifiers=CLASSIFIERS,
|
||||
packages=find_packages(),
|
||||
)
|
||||
Reference in New Issue
Block a user