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/sample
FM Harmonics/
.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 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
from enum import Enum
from riffwriter import AbstractRiffChunk
from typing import BinaryIO, List
from common.riffwriter import AbstractRiffChunk
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
from abc import abstractmethod
from enum import Enum
from typing import BinaryIO, List
from riffwriter import RiffFile, AbstractRiffChunk
from common.riffwriter import RiffFile, AbstractRiffChunk
class WaveSampleFormat(Enum):
@@ -46,6 +49,11 @@ class WaveAbstractFormatChunk(AbstractRiffChunk):
self.bitdepth()))
class WaveFile(RiffFile):
def __init__(self, format: WaveAbstractFormatChunk, chunks: List[AbstractRiffChunk]):
super().__init__(b"WAVE", [format] + chunks)
class WavePcmFormatChunk(WaveAbstractFormatChunk):
def sampleformat(self) -> WaveSampleFormat: return WaveSampleFormat.PCM
@@ -88,8 +96,3 @@ class WaveCommentChunk(AbstractRiffChunk):
def __init__(self, comment: bytes):
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
import sys
sys.path.append("../common")
from wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from wavesampler import WaveSamplerChunk, WaveSamplerLoop
sys.path.append("..")
from common.wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from common.wavesampler import WaveSamplerChunk, WaveSamplerLoop
# Directory constants
@@ -36,7 +36,6 @@ class Sample:
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...
@@ -77,7 +76,7 @@ def readsmp(f: BinaryIO, ofs: int, idx: int):
f.seek(ofs + 0x12)
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
loopBit = True if flags & 0b00010000 else False