1
0
mirror of https://github.com/ScrelliCopter/VGM-Tools synced 2025-02-21 04:09:25 +11:00

generalise python wave writer

This commit is contained in:
2023-11-07 01:34:29 +11:00
parent dbce8e5c29
commit 111f800c49
5 changed files with 297 additions and 70 deletions

View File

@@ -1,21 +1,29 @@
#!/usr/bin/env python3
# ripsamples.py -- a python script for mass extracting samples from SPC files.
# (C) 2018 neoadpcmextract.c (C) 2018 a dinosaur (zlib)
# (C) 2018, 2023 a dinosaur (zlib)
import os
import subprocess
import pathlib
import struct
import hashlib
from typing import BinaryIO
from io import BytesIO
# Directory constants.
import sys
sys.path.append("../common")
from wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from wavesampler import WaveSamplerChunk, WaveSamplerLoop
# Directory constants
SPCDIR = "./spc"
ITDIR = "./it"
SMPDIR = "./sample"
# External programs used by this script.
SPC2IT = "spc2it"
# External programs used by this script
SPC2IT = "spc2it/spc2it"
class Sample:
@@ -24,63 +32,35 @@ class Sample:
loopEnd = 0
rate = 0
data = None
data: bytes = None
def writesmp(smp, path):
def writesmp(smp: Sample, path: str):
print(path)
with open(path, "wb") as wav:
# Make sure sample rate is nonzero.
# Make sure sample rate is nonzero
#TODO: figure out why this even happens...
if smp.rate == 0:
smp.rate = 32000
#print(path + " may be corrupted...")
#print(path + " may be corrupted")
writeLoop = True if smp.loopEnd > smp.loopBeg else False
fmtChunk = WavePcmFormatChunk( # Audio format (uncompressed)
1, # Channel count (mono)
smp.rate, # Samplerate
16) # Bits per sample (16 bit)
dataChunk = WaveDataChunk(smp.data)
loopChunk = None
if smp.loopEnd > smp.loopBeg:
loopChunk = WaveSamplerChunk(loops=[WaveSamplerLoop(
start=smp.loopBeg, # Loop start
end=smp.loopEnd)]) # Loop end
# Write RIFF chunk.
wav.write(b"RIFF")
# Size of entire file following
riffSize = 104 if writeLoop else 36
wav.write(struct.pack("<I", riffSize + smp.length * 2))
wav.write(b"WAVE")
WaveFile(fmtChunk,
[dataChunk] if loopChunk is None else [loopChunk, dataChunk]
).write(wav)
# Write fmt chunk.
wav.write(b"fmt ")
wav.write(struct.pack("<I", 16)) # Subchunk size.
wav.write(struct.pack("<H", 1)) # Audio format (uncompressed)
wav.write(struct.pack("<H", 1)) # Channel count (mono)
wav.write(struct.pack("<I", smp.rate)) # Samplerate
wav.write(struct.pack("<I", smp.rate * 2 )) # Byte rate (16 bit mono)
wav.write(struct.pack("<H", 2)) # Bytes per sample (16 bit mono)
wav.write(struct.pack("<H", 16)) # Bits per sample (16 bit)
# Write sampler chunk (if looped).
if writeLoop:
wav.write(b"smpl")
wav.write(struct.pack("<I", 60)) # Chunk size (36 + loops * 24)
wav.write(b"\x00\x00\x00\x00") # Manufacturer
wav.write(b"\x00\x00\x00\x00") # Product
wav.write(b"\x00\x00\x00\x00") # Sample period
wav.write(b"\x00\x00\x00\x00") # MIDI unity note
wav.write(b"\x00\x00\x00\x00") # MIDI pitch fraction
wav.write(b"\x00\x00\x00\x00") # SMPTE format
wav.write(b"\x00\x00\x00\x00") # SMPTE offset
wav.write(struct.pack("<I", 1)) # Loop count
wav.write(struct.pack("<I", 24)) # Loop data length
wav.write(struct.pack("<I", 0)) # Cue point ID (none)
wav.write(struct.pack("<I", 0)) # Loop type (forward)
wav.write(struct.pack("<I", smp.loopBeg)) # Loop start
wav.write(struct.pack("<I", smp.loopEnd)) # Loop end
wav.write(struct.pack("<I", 0)) # Fraction (none)
wav.write(struct.pack("<I", 0)) # Loop count (infinite)
# Write data chunk.
wav.write(b"data")
wav.write(struct.pack("<I", smp.length * 2))
wav.write(smp.data)
def readsmp(f, ofs, idx):
def readsmp(f: BinaryIO, ofs: int, idx: int):
# List of assumptions made:
# - Samples are 16 bit
# - Samples are mono
@@ -93,34 +73,34 @@ def readsmp(f, ofs, idx):
f.seek(ofs)
if f.read(4) != b"IMPS": return None
# Skip fname to flags & read.
# Skip fname to flags & read
f.seek(ofs + 0x12)
flags = int.from_bytes(f.read(1), byteorder="little", signed=False)
# Read flag values.
if not flags & 0b00000001: return None # Check sample data bit.
if not flags & 0b00000001: return None # Check sample data bit
loopBit = True if flags & 0b00010000 else False
smp = Sample()
# Read the rest of the header.
# Read the rest of the header
f.seek(ofs + 0x30)
smp.length = int.from_bytes(f.read(4), byteorder="little", signed=False)
if loopBit:
smp.loopBeg = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp.loopEnd = int.from_bytes(f.read(4), byteorder="little", signed=False)
else:
f.seek(8, 1) # Skip over.
f.seek(8, 1) # Skip over
smp.loopBeg = 0
smp.loopEnd = 0
smp.rate = int.from_bytes(f.read(4), byteorder="little", signed=False)
f.seek(8, 1) # Skip over sustain shit.
f.seek(8, 1) # Skip over sustain shit
# Read sample data.
# Read sample data
dataOfs = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp.data = f.read(smp.length * 2)
# Compute hash of data.
# Compute hash of data
#FIXME: This actually generates a butt ton of collisions...
# there's got to be a better way!
h = hashlib.md5(struct.pack("<pII", smp.data, smp.loopBeg, smp.loopEnd))
@@ -128,15 +108,16 @@ def readsmp(f, ofs, idx):
return smp
def readit(path, outpath):
def readit(path: str, outpath: str):
with open(path, "r+b") as f:
# Don't bother scanning non IT files.
# Don't bother scanning non IT files
if f.read(4) != b"IMPM": return
#print("Song name: " + f.read(26).decode('utf-8'))
# Need order list size and num instruments to know how far to skip.
# Need order list size and num instruments to know how far to skip
f.seek(0x20)
ordNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
insNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
@@ -161,7 +142,8 @@ def readit(path, outpath):
pathlib.Path(outpath).mkdir(parents=True, exist_ok=True)
writesmp(smp, outwav)
def scanit(srcPath, dstPath):
def scanit(srcPath: str, dstPath: str):
for directory, subdirectories, files in os.walk(srcPath):
for file in files:
if file.endswith(".it"):
@@ -169,18 +151,19 @@ def scanit(srcPath, dstPath):
outpath = dstPath + path[len(srcPath):-len(file)]
readit(path, outpath)
def scanspc(srcPath, dstPath):
def scanspc(srcPath: str, dstPath: str):
for directory, subdirectories, files in os.walk(srcPath):
# Create output dir for each game.
# Create output dir for each game
for sub in subdirectories:
path = os.path.join(dstPath, sub)
pathlib.Path(path).mkdir(parents=True, exist_ok=True)
# Convert spc files.
# Convert spc files
for file in files:
if file.endswith(".spc"):
# Don't convert files that have already been converted.
# Don't convert files that have already been converted
itpath = os.path.join(dstPath + directory[len(srcPath):], file[:-3] + "it")
if not os.path.isfile(itpath):
path = os.path.join(directory, file)
@@ -190,6 +173,7 @@ def scanspc(srcPath, dstPath):
os.rename(path, itpath)
# Actual main stuff.
scanspc(SPCDIR, ITDIR)
scanit(ITDIR, SMPDIR)
# Actual main stuff
if __name__ == "__main__":
scanspc(SPCDIR, ITDIR)
scanit(ITDIR, SMPDIR)