#!/usr/bin/env python3 # ripsamples.py -- a python script for mass extracting samples from SPC files. # (C) 2018, 2023 a dinosaur (zlib) import os import subprocess import pathlib import struct import hashlib from typing import BinaryIO from io import BytesIO 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/spc2it" class Sample: length = 0 loopBeg = 0 loopEnd = 0 rate = 0 data: bytes = None def writesmp(smp: Sample, path: str): print(path) with open(path, "wb") as wav: # 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") 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 WaveFile(fmtChunk, [dataChunk] if loopChunk is None else [loopChunk, dataChunk] ).write(wav) def readsmp(f: BinaryIO, ofs: int, idx: int): # List of assumptions made: # - Samples are 16 bit # - Samples are mono # - Samples are signed # - No compression # - No sustain loop # - Loops are forwards # - Global volume is ignored f.seek(ofs) if f.read(4) != b"IMPS": return None # 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 loopBit = True if flags & 0b00010000 else False smp = Sample() # 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 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 # 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 #FIXME: This actually generates a butt ton of collisions... # there's got to be a better way! h = hashlib.md5(struct.pack(" 1024: return if smpNum > 4000: return if insNum > 256: return if patNum > 256: return smpOfsTable = 0xC0 + ordNum + insNum * 4 for i in range(0, smpNum): f.seek(smpOfsTable + i * 4) smpOfs = int.from_bytes(f.read(4), byteorder="little", signed=False) smp = readsmp(f, smpOfs, i + 1) if smp != None: outwav = os.path.join(outpath, smp.hash + ".wav") if not os.path.isfile(outwav): pathlib.Path(outpath).mkdir(parents=True, exist_ok=True) writesmp(smp, outwav) def scanit(srcPath: str, dstPath: str): for directory, subdirectories, files in os.walk(srcPath): for file in files: if file.endswith(".it"): path = os.path.join(directory, file) outpath = dstPath + path[len(srcPath):-len(file)] readit(path, outpath) def scanspc(srcPath: str, dstPath: str): for directory, subdirectories, files in os.walk(srcPath): # 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 for file in files: if file.endswith(".spc"): # 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) subprocess.call([SPC2IT, path]) path = path[:-3] + "it" if os.path.isfile(path): os.rename(path, itpath) # Actual main stuff if __name__ == "__main__": scanspc(SPCDIR, ITDIR) scanit(ITDIR, SMPDIR)