mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-22 04:04:04 +09:00
midi2taud: filter env override for 'I shall have no filter'
This commit is contained in:
27
midi2taud.py
27
midi2taud.py
@@ -1526,10 +1526,29 @@ class Patch:
|
|||||||
'default_cutoff': cut_s,
|
'default_cutoff': cut_s,
|
||||||
'default_resonance': res_s,
|
'default_resonance': res_s,
|
||||||
'initial_attenuation': att_s}
|
'initial_attenuation': att_s}
|
||||||
if filt_s is not None and filt_differs:
|
if filt_differs:
|
||||||
d['filter_env'] = filt_s
|
if filt_s is not None:
|
||||||
if pit_s is not None and pit_s != pit_c:
|
d['filter_env'] = filt_s
|
||||||
d['pitch_env'] = pit_s
|
elif filt_c is not None:
|
||||||
|
# This patch has NO filter env but the base/canonical zone DOES. Without an
|
||||||
|
# explicit override the voice INHERITS the base record's filter env (the
|
||||||
|
# engine's resolveActiveEnvelopes falls a patch through to the base pf-slots),
|
||||||
|
# and since the canonical of a shared layer is the most-HIT drum (e.g. the
|
||||||
|
# kick, ~6kHz me2filt sweep), an unfiltered patch like the open hi-hat gets
|
||||||
|
# that sweep applied → "incredibly lowpassed". Emit an absent filter-env block
|
||||||
|
# (PRESENT bit clear) so the engine sets hasFilterEnv=false and, with this
|
||||||
|
# patch's 'x' cutoff already = off, bypasses the filter entirely.
|
||||||
|
d['filter_env'] = {'loop': ENV_PF_FILTER, 'sustain': 0,
|
||||||
|
'nodes': [(0, 0)] * 25}
|
||||||
|
if pit_s != pit_c:
|
||||||
|
if pit_s is not None:
|
||||||
|
d['pitch_env'] = pit_s
|
||||||
|
elif pit_c is not None:
|
||||||
|
# Same inherited-env hazard as the filter leg above: a patch with no pitch
|
||||||
|
# env must explicitly override the base's, or it inherits the canonical's
|
||||||
|
# modEnvToPitch sweep (e.g. an 808 tom's pitch drop bleeding onto a co-layered
|
||||||
|
# drum). Absent pitch-env block: m-bit clear (pitch role) + PRESENT bit clear.
|
||||||
|
d['pitch_env'] = {'loop': 0, 'sustain': 0, 'nodes': [(0, 0)] * 25}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
110
taud_common.py
110
taud_common.py
@@ -8,6 +8,7 @@ duplicate verbatim.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import gzip as _gzip
|
import gzip as _gzip
|
||||||
|
import math
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -263,22 +264,113 @@ def d_arg_to_col(arg: int):
|
|||||||
return (SEL_UP, hi)
|
return (SEL_UP, hi)
|
||||||
|
|
||||||
|
|
||||||
def resample_linear(data: bytes, ratio: float) -> bytes:
|
_SINC_TABLE_CACHE = {}
|
||||||
"""Resample bytes by ratio (< 1 = downsample) using linear interpolation."""
|
_KAISER_BETA = 8.0 # ~-70 dB stop-band, near-flat pass-band to the corner
|
||||||
if not data:
|
|
||||||
|
def _bessel_i0(x: float) -> float:
|
||||||
|
"""Modified Bessel function of the first kind, order 0 (for the Kaiser window)."""
|
||||||
|
s = 1.0
|
||||||
|
t = 1.0
|
||||||
|
k = 1
|
||||||
|
while True:
|
||||||
|
t *= (x * x) / (4.0 * k * k)
|
||||||
|
s += t
|
||||||
|
if t < 1e-12 * s:
|
||||||
|
return s
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
def _windowed_sinc_table(cutoff: float, half_width: int, phases: int):
|
||||||
|
"""Precompute a polyphase Kaiser-windowed-sinc kernel.
|
||||||
|
|
||||||
|
`cutoff` is the low-pass corner in cycles/input-sample (<= 0.5); for a
|
||||||
|
downsample it is half the TARGET rate so the kernel doubles as the
|
||||||
|
anti-alias filter. Each of `phases` sub-sample positions gets a row of
|
||||||
|
`2*half_width` taps, DC-normalised to unity gain so a constant signal
|
||||||
|
(e.g. the u8 128 silence bias, or any DC offset) passes through unchanged.
|
||||||
|
Cached: the same (cutoff, width, phases) recurs across a whole sample pool.
|
||||||
|
"""
|
||||||
|
key = (round(cutoff, 6), half_width, phases)
|
||||||
|
cached = _SINC_TABLE_CACHE.get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
n_taps = 2 * half_width
|
||||||
|
inv_i0 = 1.0 / _bessel_i0(_KAISER_BETA)
|
||||||
|
table = []
|
||||||
|
for p in range(phases):
|
||||||
|
frac = p / phases
|
||||||
|
row = [0.0] * n_taps
|
||||||
|
s = 0.0
|
||||||
|
for k in range(n_taps):
|
||||||
|
t = k - (half_width - 1) # tap index relative to left sample
|
||||||
|
x = t - frac # distance from the output position
|
||||||
|
a = 2.0 * cutoff * x
|
||||||
|
sinc = 1.0 if a == 0.0 else math.sin(math.pi * a) / (math.pi * a)
|
||||||
|
# Kaiser window over [-half_width, +half_width]
|
||||||
|
r = x / half_width # -1..1
|
||||||
|
win = _bessel_i0(_KAISER_BETA * math.sqrt(max(0.0, 1.0 - r * r))) * inv_i0
|
||||||
|
c = sinc * win
|
||||||
|
row[k] = c
|
||||||
|
s += c
|
||||||
|
inv = 1.0 / s if s else 1.0
|
||||||
|
table.append([c * inv for c in row])
|
||||||
|
_SINC_TABLE_CACHE[key] = table
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def resample_bandlimited(data: bytes, ratio: float) -> bytes:
|
||||||
|
"""Resample u8 bytes by `ratio` (< 1 = downsample) with a band-limited
|
||||||
|
windowed-sinc kernel.
|
||||||
|
|
||||||
|
Replaces the old linear interpolator, whose triangular kernel has a sinc^2
|
||||||
|
response that rolled off the top octave (~3.4 dB @ 15 kHz, worse near the
|
||||||
|
source Nyquist) and audibly muffled bright percussion and any SF2 sample
|
||||||
|
downsampled to the 65535-frame / 32 kHz fit — e.g. the GeneralUser-GS open
|
||||||
|
hi-hat, ~3.6% spectral-centroid loss on a single pass, compounding on the
|
||||||
|
sub-32 kHz fit and 8 MB-pool re-resample paths. The windowed sinc stays
|
||||||
|
flat to the post-resample Nyquist and anti-aliases on the way down (cutoff
|
||||||
|
follows `ratio`). DC-normalised so the u8 128 bias is preserved; output is
|
||||||
|
CLAMPED (not wrapped like the old `& 0xFF`) because sinc ringing can
|
||||||
|
overshoot 0..255.
|
||||||
|
"""
|
||||||
|
if not data or ratio == 1.0:
|
||||||
return data
|
return data
|
||||||
n_out = max(1, int(len(data) * ratio))
|
n_in = len(data)
|
||||||
out = bytearray(n_out)
|
n_out = max(1, int(n_in * ratio))
|
||||||
|
cutoff = 0.5 * min(1.0, ratio) # anti-alias corner (== target Nyquist)
|
||||||
|
# More taps when downsampling so the (wider) sinc keeps the same lobe count and the
|
||||||
|
# stop-band stays steep at the lower corner. 16 half-taps at unity matches a scipy
|
||||||
|
# polyphase resample to within ~0.3% spectral centroid; capped for the rare 8 MB pass.
|
||||||
|
half_width = max(8, min(24, round(12.0 / min(1.0, ratio))))
|
||||||
|
PHASES = 512 # power of two → cheap phase index
|
||||||
|
table = _windowed_sinc_table(cutoff, half_width, PHASES)
|
||||||
|
out = bytearray(n_out)
|
||||||
|
inv_ratio = 1.0 / ratio
|
||||||
|
last = n_in - 1
|
||||||
|
n_taps = 2 * half_width
|
||||||
for i in range(n_out):
|
for i in range(n_out):
|
||||||
src = i / ratio
|
src = i * inv_ratio
|
||||||
i0 = int(src)
|
i0 = int(src)
|
||||||
frac = src - i0
|
frac = src - i0
|
||||||
i1 = min(i0 + 1, len(data) - 1)
|
row = table[int(frac * PHASES) & (PHASES - 1)]
|
||||||
v = data[i0] * (1.0 - frac) + data[i1] * frac
|
base = i0 - (half_width - 1)
|
||||||
out[i] = int(v + 0.5) & 0xFF
|
acc = 0.0
|
||||||
|
for k in range(n_taps):
|
||||||
|
idx = base + k
|
||||||
|
if idx < 0:
|
||||||
|
idx = 0
|
||||||
|
elif idx > last:
|
||||||
|
idx = last
|
||||||
|
acc += data[idx] * row[k]
|
||||||
|
v = int(acc + 0.5)
|
||||||
|
out[i] = 0 if v < 0 else (255 if v > 255 else v)
|
||||||
return bytes(out)
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# Back-compat alias: every *2taud converter imports `resample_linear` by name.
|
||||||
|
# It is now band-limited (the name is kept to avoid churn across six converters).
|
||||||
|
resample_linear = resample_bandlimited
|
||||||
|
|
||||||
|
|
||||||
def rescale_offset_effects(pat_bin: bytes, ratio: float) -> bytes:
|
def rescale_offset_effects(pat_bin: bytes, ratio: float) -> bytes:
|
||||||
"""Scale TOP_O sample-offset args in raw pattern bytes by `ratio`.
|
"""Scale TOP_O sample-offset args in raw pattern bytes by `ratio`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user