mirror of
https://github.com/ScrelliCopter/VGM-Tools
synced 2025-02-21 04:09:25 +11:00
wavetable stuff
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,4 +14,6 @@ spctools/it
|
|||||||
spctools/spc
|
spctools/spc
|
||||||
spctools/sample
|
spctools/sample
|
||||||
|
|
||||||
|
FM Harmonics/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
18
common/waveserum.py
Normal 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"))
|
||||||
@@ -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
78
sinharmonicswt.py
Normal 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()
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user