aboutsummaryrefslogtreecommitdiff
path: root/examples/sound/spustream/convert_stream.py
diff options
context:
space:
mode:
authorspicyjpeg <88942473+spicyjpeg@users.noreply.github.com>2021-12-23 21:58:56 +0100
committerspicyjpeg <88942473+spicyjpeg@users.noreply.github.com>2021-12-23 21:58:56 +0100
commit6c7512ff42805e9399dfee8b67e2f70fa55909d3 (patch)
tree946aedf4b4bc1dc548ba82430274150c32ec6e07 /examples/sound/spustream/convert_stream.py
parent31fba94b8e8c15c1d127925f01a38efc68933fd1 (diff)
downloadpsn00bsdk-6c7512ff42805e9399dfee8b67e2f70fa55909d3.tar.gz
Add sound/spustream example
Diffstat (limited to 'examples/sound/spustream/convert_stream.py')
-rw-r--r--examples/sound/spustream/convert_stream.py112
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()