Skip to content

Instantly share code, notes, and snippets.

@SamusAranX
Last active August 17, 2024 22:38
Show Gist options
  • Save SamusAranX/6eb8b6fd1777b17afc3107a979c2409a to your computer and use it in GitHub Desktop.
Save SamusAranX/6eb8b6fd1777b17afc3107a979c2409a to your computer and use it in GitHub Desktop.
BARS extractor (with improvised documentation)

bars_extractor.py

Extracts BFWAV files from BARS files found in games for Switch, Wii U (and possibly also 3DS, but this is untested)

Usage

$ bars_extractor.py [list of BARS files] or $ bars_extractor.py *.bars

Run bars_extractor.py -h for a list of options.

Wildcard syntax is only supported on non-Windows systems. Thank Microsoft for that one.

Information

  • bars_extractor.py always overwrites files when extracting.
  • File names will be trimmed to avoid issues with bad file systems. Check lines 134-135 if you want to change the trimming behavior.

Example

$ bars_extractor.py KorokPot.bars
KorokPot.bars: 8 Tracks found:
KorokPot.bars: Saved track 1 to KorokPot_KorokPot_Slide01.bfwav
KorokPot.bars: Saved track 2 to KorokPot_KorokPot_Break01.bfwav
KorokPot.bars: Saved track 3 to KorokPot_KorokPot_Land02.bfwav
KorokPot.bars: Saved track 4 to KorokPot_KorokPot_Slide00.bfwav
KorokPot.bars: Saved track 5 to KorokPot_KorokPot_Break00.bfwav
KorokPot.bars: Saved track 6 to KorokPot_KorokPot_Break02.bfwav
KorokPot.bars: Saved track 7 to KorokPot_KorokPot_Land00.bfwav
KorokPot.bars: Saved track 8 to KorokPot_KorokPot_Land01.bfwav

Changes

Version 1.1

  • Moved script logic to a separate function that's called from main()
  • Added a new FWAV header for experimental extraction
  • Changed all structs from signed to unsigned values
  • Now correctly jumps to AMTA offsets instead of assuming they exist back-to-back
  • Added better error handling

Version 1.2

  • Fixed a lot of bugs that prevented the script from running in the first place
  • Replaced sys.argv with argparse and added new options
  • General cleanups
IMPORTANT: All offsets are taken from KorokPot.bars! Actual offsets of dynamic-length data may vary in other files, but based
on the information here, it shouldn't be hard to find the real offsets. When in doubt, look at the code.
╒══════════╤══════════╤═════════╤════════════════════╤═══════════╤═════════════════════════╤════════════════════════════════════════════════════════════════════════╕
│ Offset │ -> Dec │ Size │ -> Dec │ Type │ Name │ Information │
╞══════════╪══════════╪═════════╪════════════════════╪═══════════╪═════════════════════════╪════════════════════════════════════════════════════════════════════════╡
│ 0x0 │ 0 │ 0x4 │ 4 │ │ BARS header │ Indicates that this is, in fact, a BARS file │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x4 │ 4 │ 0x4 │ 4 │ Int │ File length │ The file length in bytes, from 0x0 to whatever this is │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x8 │ 8 │ 0x2 │ 2 │ Short │ Endianness? │ Possibly a big endian BOM │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0xa │ 10 │ 0x2 │ 2 │ │ Unknown │ Not needed when merely extracting FWAV data │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0xc │ 12 │ 0x4 │ 4 │ Int │ Number of FWAV tracks │ │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x10 │ 16 │ 0x20 │ 32 │ Int[]? │ Unknown │ Most likely data relating to the tracks, not needed │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x30 │ 48 │ 0x40 │ 64 │ Int[] │ AMTA/FWAV chunk offsets │ Odd elements = AMTA chunk offsets / even elements = FWAV chunk offsets │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x70 │ 112 │ 0x5d4 │ 1492 │ See below │ AMTA chunks │ See below │
├──────────┼──────────┼─────────┼────────────────────┼───────────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0x680 │ 1664 │ 0x22338 │ 140088 (until EOF) │ See below │ FWAV chunks │ See below │
╘══════════╧══════════╧═════════╧════════════════════╧═══════════╧═════════════════════════╧════════════════════════════════════════════════════════════════════════╛
╒══════════╤══════════╤════════╤══════════╤════════╤══════════════╤═══════════════════════════════════════════════════════╕
│ Offset │ -> Dec │ Size │ -> Dec │ Type │ Name │ Information │
╞══════════╪══════════╪════════╪══════════╪════════╪══════════════╪═══════════════════════════════════════════════════════╡
│ 0x0 │ 0 │ 0x4 │ 4 │ │ AMTA header │ Start of an AMTA chunk │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x4 │ 4 │ 0x2 │ 2 │ Short │ Endianness? │ See above │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x6 │ 6 │ 0x2 │ 2 │ Short │ Unknown │ Is always 0x400 (1024) in BOTW's files │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x8 │ 8 │ 0x4 │ 4 │ Int │ Chunk length │ Length of the AMTA chunk │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0xc │ 12 │ 0x4 │ 4 │ Int │ DATA offset │ Offset of the DATA chunk, relative to the AMTA header │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x10 │ 16 │ 0x4 │ 4 │ Int │ MARK offset │ Offset of the MARK chunk, relative to the AMTA header │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x14 │ 20 │ 0x4 │ 4 │ Int │ EXT_ offset │ Offset of the EXT_ chunk, relative to the AMTA header │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x18 │ 24 │ 0x4 │ 4 │ Int │ STRG offset │ Offset of the STRG chunk, relative to the AMTA header │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x1c │ 28 │ 0x4 │ 4 │ │ DATA header │ │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x20 │ 32 │ 0x4 │ 4 │ Int │ DATA length │ Length of the DATA chunk, from here │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x24 │ 36 │ 0x64 │ 100 │ │ DATA content │ Unknown │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x88 │ 136 │ 0x4 │ 4 │ │ MARK header │ │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x8c │ 140 │ 0x4 │ 4 │ Int │ MARK length │ Length of the MARK chunk, from here │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x90 │ 144 │ 0x4 │ 4 │ Int? │ MARK content │ Unknown │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x94 │ 148 │ 0x4 │ 4 │ │ EXT_ header │ │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x98 │ 152 │ 0x4 │ 4 │ Int │ EXT_ length │ Length of the EXT_ chunk, from here │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0x9c │ 156 │ 0x4 │ 4 │ │ EXT_ content │ Unknown │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0xa0 │ 160 │ 0x4 │ 4 │ │ STRG header │ │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0xa4 │ 164 │ 0x4 │ 4 │ Int │ STRG length │ Length of the STRG chunk, from here │
├──────────┼──────────┼────────┼──────────┼────────┼──────────────┼───────────────────────────────────────────────────────┤
│ 0xa8 │ 168 │ 0x11 │ 17 │ String │ STRG content │ Null-terminated track name │
╘══════════╧══════════╧════════╧══════════╧════════╧══════════════╧═══════════════════════════════════════════════════════╛
╒══════════╤══════════╤════════╤══════════╤════════╤═════════════╤═══════════════════════════════════════════════════════╕
│ Offset │ -> Dec │ Size │ -> Dec │ Type │ Name │ Information │
╞══════════╪══════════╪════════╪══════════╪════════╪═════════════╪═══════════════════════════════════════════════════════╡
│ 0x0 │ 0 │ 0x4 │ 4 │ │ FWAV header │ Start of an FWAV chunk │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0x4 │ 4 │ 0x2 │ 2 │ Short │ Endianness? │ See above │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0x6 │ 6 │ 0x2 │ 2 │ Short │ Unknown │ Is always 0x40 (64) in BOTW's files │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0x8 │ 8 │ 0x2 │ 2 │ Short │ Unknown │ Is always 0x1 (1) in BOTW's files │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0xa │ 10 │ 0x2 │ 2 │ Short │ Unknown │ Is always 0x200 (512) in BOTW's files │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0xc │ 12 │ 0x4 │ 4 │ Int │ FWAV length │ Length of the FWAV chunk, starting at the FWAV header │
├──────────┼──────────┼────────┼──────────┼────────┼─────────────┼───────────────────────────────────────────────────────┤
│ 0x10 │ 16 │ 0xc334 │ 49972 │ │ Data │ Doesn't matter in this context │
╘══════════╧══════════╧════════╧══════════╧════════╧═════════════╧═══════════════════════════════════════════════════════╛
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""bars_extractor.py: Extracts BFWAV files from BARS files found in Switch, Wii U (and possibly also 3DS, but this is untested)"""
__author__ = "Emma Alyx Wunder (https://emmalyx.site)"
__license__ = "WTFPL"
__version__ = "1.2"
import argparse
import io
import os
import sys
import glob
import struct
# Magic numbers, commonly known as "headers"
BARS_HEADER = b"BARS"
AMTA_HEADER = b"AMTA"
DATA_HEADER = b"DATA"
MARK_HEADER = b"MARK"
EXT_HEADER = b"EXT_"
STRG_HEADER = b"STRG"
FWAV_HEADERS = [b"FWAV", b"FSTP"]
# The Python structs to go with the above headers
BARS_HEADER_STRUCT = struct.Struct("<4sIH2xI") # size: 16
BARS_OFFSET_STRUCT = struct.Struct("<II") # size: 8
AMTA_HEADER_STRUCT = struct.Struct("<4sH2x5I") # size: 28
DATA_HEADER_STRUCT = struct.Struct("<4sI") # size: 8
MARK_HEADER_STRUCT = struct.Struct("<4sI") # size: 8
EXT_HEADER_STRUCT = struct.Struct("<4sI") # size: 8
STRG_HEADER_STRUCT = struct.Struct("<4sI") # size: 8
FWAV_HEADER_STRUCT = struct.Struct("<4s8xI8x2I32x") # size: 64
def plural_s(n):
return "s" if n != 1 else ""
def extract_from_bars(fname: str, out_dir: str, export_bfstp: bool, debug: bool):
with open(fname, "rb") as f:
bars_header, bars_file_length, bars_endianness, bars_count = BARS_HEADER_STRUCT.unpack(f.read(BARS_HEADER_STRUCT.size))
# skip useless file identifiers
f.seek(bars_count*4, io.SEEK_CUR)
bars_track_offsets: list[tuple[int, int]] = []
for i in range(bars_count):
offsets = BARS_OFFSET_STRUCT.unpack(f.read(BARS_OFFSET_STRUCT.size))
bars_track_offsets.append(offsets)
if bars_header != BARS_HEADER:
raise RuntimeError(f"{f.name}: Not a valid BARS file.")
file_size = os.fstat(f.fileno()).st_size
if bars_file_length != file_size:
raise RuntimeError(f"{f.name}: File size mismatch (expected {bars_file_length} bytes, got {file_size} bytes)")
track_names = []
for i, (amta_offset, _) in enumerate(bars_track_offsets):
f.seek(amta_offset, io.SEEK_SET)
amta_bytes = f.read(AMTA_HEADER_STRUCT.size)
amta_header, amta_endianness, amta_length, data_offset, mark_offset, ext_offset, strg_offset = AMTA_HEADER_STRUCT.unpack(amta_bytes)
if amta_header != AMTA_HEADER:
raise RuntimeError(f"{f.name}: Track {i+1} has an invalid AMTA header")
data_bytes = f.read(DATA_HEADER_STRUCT.size)
data_header, data_length = DATA_HEADER_STRUCT.unpack(data_bytes)
if data_header != DATA_HEADER:
raise RuntimeError(f"{f.name}: Track {i+1} has an invalid DATA header")
data = f.read(data_length)
mark_bytes = f.read(MARK_HEADER_STRUCT.size)
mark_header, mark_length = MARK_HEADER_STRUCT.unpack(mark_bytes)
if mark_header != MARK_HEADER:
raise RuntimeError(f"{f.name}: Track {i+1} has an invalid MARK header")
mark = f.read(mark_length)
ext_bytes = f.read(EXT_HEADER_STRUCT.size)
ext_header, ext_length = EXT_HEADER_STRUCT.unpack(ext_bytes)
if ext_header != EXT_HEADER:
raise RuntimeError(f"{f.name}: Track {i+1} has an invalid EXT_ header")
ext = f.read(ext_length)
strg_bytes = f.read(STRG_HEADER_STRUCT.size)
strg_header, strg_length = STRG_HEADER_STRUCT.unpack(strg_bytes)
if strg_header != STRG_HEADER:
raise RuntimeError(f"{f.name}: Track {i+1} has an invalid STRG header")
# Read track name and convert the resulting byte sequence to an UTF8 string
strg = f.read(strg_length).decode("utf8")
track_names.append(strg)
print(f"{f.name}: {bars_count} track{plural_s(bars_count)} found!")
if f.tell() == bars_file_length: # We have now reached the end of the file despite the file telling us that there would be stuff here
raise RuntimeError(f"{f.name}: Reached EOF, this file probably doesn't actually contain any FWAVs despite containing the offsets for them")
base_dir = os.path.splitext(f.name)[0]
if out_dir:
base_dir = os.path.join(out_dir, base_dir)
os.makedirs(base_dir, exist_ok=True)
for i, (_, file_offset) in enumerate(bars_track_offsets):
if file_offset >= bars_file_length: # The offset the file is telling us to jump to can't exist because the file's too small
print(f"{f.name}: Track {i+1} probably doesn't exist, skipping it")
continue
f.seek(file_offset, io.SEEK_SET) # seek to the next FWAV header
fwav_header_bytes = f.read(FWAV_HEADER_STRUCT.size)
fwav_header, fwav_length, fwav_info_offset, fwav_data_offset = FWAV_HEADER_STRUCT.unpack(fwav_header_bytes)
if fwav_header not in FWAV_HEADERS:
print(f"{f.name}: Track {i+1} has an invalid FWAV header")
continue
# skip file export if this is a BFSTP and BFSTP export hasn't explicitly been enabled
if fwav_header == b"FSTP" and not export_bfstp:
continue
f.seek(-FWAV_HEADER_STRUCT.size, os.SEEK_CUR) # seek back to the start of the FWAV data...
fwav_data = f.read(fwav_length) # ...so that we can read it out in one big chunk
track_ext = [".bfwav", ".bfstp"][FWAV_HEADERS.index(fwav_header)] # Give the output file a different extension depending on content
track_name = track_names[i].rstrip("\0") # remove trailing null bytes
track_name, *track_name_remainder = track_name.split("\0", maxsplit=1)
if track_name_remainder and track_name_remainder[0]:
print(f"Extra name bit on track {i+1} ignored: {track_name_remainder[0]}")
# track_name = f"{track_name}_{track_name_remainder[0]}"
bfwav_name = os.path.join(base_dir, track_name + track_ext) # Construct output file name
if not debug:
with open(bfwav_name, "wb") as wf:
wf.write(fwav_data) # write the data to a BFWAV file
print(f"{f.name}: Saved track {i+1} to {bfwav_name}")
def main():
parser = argparse.ArgumentParser(description="BOTW BARS Extractor")
parser.add_argument("input", type=str, nargs="+", help="Input files")
parser.add_argument("-o", "--outdir", type=str, help="Output directory")
parser.add_argument("--bfstp", action="store_true", help="Export BFSTPs as well (they're ignored by default)")
parser.add_argument("--debug", action="store_true", help="Debug Mode (skips file extraction)")
args = parser.parse_args()
for f in args.input:
try:
extract_from_bars(f, args.outdir, args.bfstp, args.debug)
except RuntimeError as e:
print(e)
if __name__ == "__main__":
main()
@Minikea
Copy link

Minikea commented May 20, 2018

Seems it doesn't work wit .bars from switch gamecard dump
Traceback (most recent call last): File "bars.py", line 138, in <module> main() File "bars.py", line 133, in main extract_from_bars(_f) File "bars.py", line 40, in extract_from_bars bars_track_offsets = bars_track_struct.unpack(f.read(bars_track_struct.size)) MemoryError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment