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/sample
|
||||
|
||||
FM Harmonics/
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
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
|
||||
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
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user