diff options
Diffstat (limited to 'examples/sound/spustream/convert_stream.py')
| -rw-r--r-- | examples/sound/spustream/convert_stream.py | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/examples/sound/spustream/convert_stream.py b/examples/sound/spustream/convert_stream.py new file mode 100644 index 0000000..1b1696f --- /dev/null +++ b/examples/sound/spustream/convert_stream.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Simple .VAG to STREAM.BIN interleaving tool +# (C) 2021 spicyjpeg - MPL licensed + +import sys +from warnings import warn +from struct import Struct +from itertools import zip_longest +from argparse import ArgumentParser, FileType + +VAG_HEADER = Struct("> 4s I 4x 2I 12x 16s") +VAG_MAGIC = b"VAGp" +SAMPLE_RATE = 44100 +BUFFER_SIZE = 26624 # (26624 / 16 * 28) / 44100 = 1.05 seconds +ALIGN_SIZE = 2048 + +## Helpers + +def align(data, size): + chunks = (len(data) + size - 1) // size + + return data.ljust(chunks * size, b"\x00") + +def set_loop_flag(data): + last_block = bytearray(data[-16:]) + last_block[1] = 0x03 # Jump to loop point + sustain + + return data[:-16] + last_block + +## .VAG file reader + +def read_vag(_file, chunk_size): + with _file: + header = _file.read(VAG_HEADER.size) + ( + magic, + version, + size, + sample_rate, + name + ) = VAG_HEADER.unpack(header) + + #if magic != VAG_MAGIC: + #raise RuntimeError(f"{_file.name} is not a valid .VAG file") + if sample_rate != SAMPLE_RATE: + warn(RuntimeWarning(f"{_file.name} sample rate is not {SAMPLE_RATE} Hz")) + + for i in range(0, size, chunk_size): + chunk = _file.read(chunk_size) + + if len(chunk) % 16: + warn(RuntimeWarning(f"{_file.name} is not 16-byte aligned, trimming")) + chunk = chunk[0:len(chunk) // 16 * 16] + + chunk = set_loop_flag(chunk) + + yield chunk.ljust(chunk_size, b"\x00") + +## Main + +def get_args(): + parser = ArgumentParser( + description = "Generates interleaved stream data from one or more .VAG files." + ) + parser.add_argument( + "input_file", + nargs = "+", + type = FileType("rb"), + help = f"mono input files for each channel (must be {SAMPLE_RATE} Hz .VAG)" + ) + parser.add_argument( + "-o", "--output", + type = FileType("wb"), + default = "stream.bin", + help = "where to output converted stream data (stream.bin by default)", + metavar = "file" + ) + parser.add_argument( + "-b", "--buffer-size", + type = int, + default = BUFFER_SIZE, + help = f"size of each interleaved chunk (one per channel, default {BUFFER_SIZE})", + metavar = "bytes" + ) + parser.add_argument( + "-a", "--align", + type = int, + default = ALIGN_SIZE, + help = f"align each group of chunks to N bytes (default {ALIGN_SIZE})", + metavar = "bytes" + ) + + return parser.parse_args() + +def main(): + args = get_args() + if args.buffer_size % 16: + raise ValueError("buffer size must be a multiple of 16 bytes") + + interleave = zip_longest( + *( read_vag(_file, args.buffer_size) for _file in args.input_file ), + fillvalue = b"\x00" * args.buffer_size + ) + + with args.output as _file: + for chunks in interleave: + data = b"".join(chunks) + + _file.write(align(data, args.align)) + +if __name__ == "__main__": + main() |
