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

wavetable stuff

This commit is contained in:
2023-11-07 02:43:28 +11:00
parent 111f800c49
commit 9c5e19264b
7 changed files with 121 additions and 15 deletions

2
.gitignore vendored
View File

@@ -14,4 +14,6 @@ spctools/it
spctools/spc spctools/spc
spctools/sample spctools/sample
FM Harmonics/
.DS_Store .DS_Store

View File

@@ -1,3 +1,6 @@
# riffwriter.py -- Generic RIFF writing framework
# (C) 2023 a dinosaur (zlib)
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import BinaryIO, List from typing import BinaryIO, List

View File

@@ -1,7 +1,10 @@
# wavesampler.py -- Support for the non-standard "Sampler" WAVE chunk
# (C) 2023 a dinosaur (zlib)
import struct import struct
from enum import Enum from enum import Enum
from riffwriter import AbstractRiffChunk
from typing import BinaryIO, List from typing import BinaryIO, List
from common.riffwriter import AbstractRiffChunk
class WaveSamplerSMPTEOffset: class WaveSamplerSMPTEOffset:

18
common/waveserum.py Normal file
View File

@@ -0,0 +1,18 @@
# waveserum.py -- Serum comment chunk support
# (C) 2023 a dinosaur (zlib)
from enum import Enum
from common.wavewriter import WaveCommentChunk
class SerumWavetableInterpolation(Enum):
NONE = 0
LINEAR_XFADE = 1
SPECTRAL_MORPH = 2
class WaveSerumCommentChunk(WaveCommentChunk):
def __init__(self, size: int, mode: SerumWavetableInterpolation, factory=False):
comment = f"<!>{size: <4} {mode.value}{'1' if factory else '0'}000000"
comment += " wavetable (www.xferrecords.com)"
super().__init__(comment.encode("ascii"))

View File

@@ -1,8 +1,11 @@
# wavewriter.py -- Extensible WAVE writing framework
# (C) 2023 a dinosaur (zlib)
import struct import struct
from abc import abstractmethod from abc import abstractmethod
from enum import Enum from enum import Enum
from typing import BinaryIO, List from typing import BinaryIO, List
from riffwriter import RiffFile, AbstractRiffChunk from common.riffwriter import RiffFile, AbstractRiffChunk
class WaveSampleFormat(Enum): class WaveSampleFormat(Enum):
@@ -46,6 +49,11 @@ class WaveAbstractFormatChunk(AbstractRiffChunk):
self.bitdepth())) self.bitdepth()))
class WaveFile(RiffFile):
def __init__(self, format: WaveAbstractFormatChunk, chunks: List[AbstractRiffChunk]):
super().__init__(b"WAVE", [format] + chunks)
class WavePcmFormatChunk(WaveAbstractFormatChunk): class WavePcmFormatChunk(WaveAbstractFormatChunk):
def sampleformat(self) -> WaveSampleFormat: return WaveSampleFormat.PCM def sampleformat(self) -> WaveSampleFormat: return WaveSampleFormat.PCM
@@ -88,8 +96,3 @@ class WaveCommentChunk(AbstractRiffChunk):
def __init__(self, comment: bytes): def __init__(self, comment: bytes):
self._comment = comment self._comment = comment
class WaveFile(RiffFile):
def __init__(self, format: WaveAbstractFormatChunk, chunks: List[AbstractRiffChunk]):
super().__init__(b"WAVE", [format] + chunks)

78
sinharmonicswt.py Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# Name: sinharmonicswt.py
# Copyright: © 2023 a dinosaur
# Homepage: https://github.com/ScrelliCopter/VGM-Tools
# License: Zlib (https://opensource.org/licenses/Zlib)
# Description: Generate Serum format wavetables of the harmonic series
# for a handful of common FM waveforms, intended for improving
# the workflow of creating FM sounds in Vital.
import math
from pathlib import Path
from typing import NamedTuple
from common.wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from common.waveserum import WaveSerumCommentChunk, SerumWavetableInterpolation
def write_wavetable(name: str, generator,
mode: SerumWavetableInterpolation = SerumWavetableInterpolation.NONE,
num: int = 64, size: int = 2048):
def sweep_table() -> bytes:
for i in range(num - 1):
yield b"".join(generator(size, i + 1))
with open(name, "wb") as f:
WaveFile(WavePcmFormatChunk(1, 44100, 16), [
WaveSerumCommentChunk(size, mode),
WaveDataChunk(b"".join(sweep_table()))
]).write(f)
def main():
clip = lambda x, a, b: max(a, min(b, x))
def clamp2short(a: float) -> int: return clip(int(a * 0x7FFF), -0x8000, 0x7FFF)
def sinetable16(size: int, harmonic: int):
# Generate a simple sine wave
for i in range(size):
sample = clamp2short(math.sin(i / size * math.tau * harmonic))
yield sample.to_bytes(2, byteorder="little", signed=True)
#TODO: probably make bandlimited versions of the nonlinear waves
def hsinetable16(size: int, harmonic: int):
# Generate one half of a sine wave with the negative pole hard clipped off
for i in range(size):
sample = clamp2short(max(0.0, math.sin(i / size * math.tau * harmonic)))
yield sample.to_bytes(2, byteorder="little", signed=True)
#TODO: probably make bandlimited versions of the nonlinear waves
def asinetable16(size: int, harmonic: int):
# Generate a sine wave with the negative pole mirrored positively
for i in range(size):
sample = clamp2short(math.fabs(math.sin(i / size * math.pi * harmonic)))
yield sample.to_bytes(2, byteorder="little", signed=True)
outfolder = Path("FM Harmonics")
outfolder.mkdir(exist_ok=True)
# Build queue of files to generate
GenItem = NamedTuple("GenItem", generator=any, steps=int, mode=SerumWavetableInterpolation, name=str)
genqueue: list[GenItem] = list()
# All waveform types with 64 harmonic steps in stepped and linear versions
for mode in [("", SerumWavetableInterpolation.NONE), (" (XFade)", SerumWavetableInterpolation.LINEAR_XFADE)]:
for generator in [("Sine", sinetable16), ("Half Sine", hsinetable16), ("Abs Sine", asinetable16)]:
genqueue.append(GenItem(generator[1], 64, mode[1], f"{generator[0]} Harmonics{mode[0]}"))
# Shorter linear versions of hsine and asine
for steps in [8, 16, 32]:
spec = SerumWavetableInterpolation.LINEAR_XFADE
for generator in [("Half Sine", hsinetable16), ("Abs Sine", asinetable16)]:
genqueue.append(GenItem(generator[1], steps, spec, f"{generator[0]} (XFade {steps})"))
# Generate & write wavetables
for i in genqueue:
write_wavetable(str(outfolder.joinpath(f"{i.name}.wav")), i.generator, i.mode, i.steps)
if __name__ == "__main__":
main()

View File

@@ -12,9 +12,9 @@ from typing import BinaryIO
from io import BytesIO from io import BytesIO
import sys import sys
sys.path.append("../common") sys.path.append("..")
from wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk from common.wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from wavesampler import WaveSamplerChunk, WaveSamplerLoop from common.wavesampler import WaveSamplerChunk, WaveSamplerLoop
# Directory constants # Directory constants
@@ -36,7 +36,6 @@ class Sample:
def writesmp(smp: Sample, path: str): def writesmp(smp: Sample, path: str):
print(path)
with open(path, "wb") as wav: 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... #TODO: figure out why this even happens...
@@ -77,8 +76,8 @@ def readsmp(f: BinaryIO, ofs: int, idx: int):
f.seek(ofs + 0x12) f.seek(ofs + 0x12)
flags = int.from_bytes(f.read(1), byteorder="little", signed=False) flags = int.from_bytes(f.read(1), byteorder="little", signed=False)
# Read flag values. # 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 loopBit = True if flags & 0b00010000 else False
smp = Sample() smp = Sample()
@@ -90,11 +89,11 @@ def readsmp(f: BinaryIO, ofs: int, idx: int):
smp.loopBeg = int.from_bytes(f.read(4), byteorder="little", signed=False) smp.loopBeg = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp.loopEnd = int.from_bytes(f.read(4), byteorder="little", signed=False) smp.loopEnd = int.from_bytes(f.read(4), byteorder="little", signed=False)
else: else:
f.seek(8, 1) # Skip over f.seek(8, 1) # Skip over
smp.loopBeg = 0 smp.loopBeg = 0
smp.loopEnd = 0 smp.loopEnd = 0
smp.rate = int.from_bytes(f.read(4), byteorder="little", signed=False) 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) dataOfs = int.from_bytes(f.read(4), byteorder="little", signed=False)