midi2taud: filter env override for 'I shall have no filter'

This commit is contained in:
minjaesong
2026-06-21 17:23:01 +09:00
parent 2209bf1031
commit eb42281a4e
2 changed files with 124 additions and 13 deletions

View File

@@ -1526,10 +1526,29 @@ class Patch:
'default_cutoff': cut_s,
'default_resonance': res_s,
'initial_attenuation': att_s}
if filt_s is not None and filt_differs:
if filt_differs:
if filt_s is not None:
d['filter_env'] = filt_s
if pit_s is not None and pit_s != pit_c:
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

View File

@@ -8,6 +8,7 @@ duplicate verbatim.
"""
import gzip as _gzip
import math
import struct
import sys
@@ -263,22 +264,113 @@ def d_arg_to_col(arg: int):
return (SEL_UP, hi)
def resample_linear(data: bytes, ratio: float) -> bytes:
"""Resample bytes by ratio (< 1 = downsample) using linear interpolation."""
if not data:
_SINC_TABLE_CACHE = {}
_KAISER_BETA = 8.0 # ~-70 dB stop-band, near-flat pass-band to the corner
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
n_out = max(1, int(len(data) * ratio))
n_in = len(data)
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):
src = i / ratio
src = i * inv_ratio
i0 = int(src)
frac = src - i0
i1 = min(i0 + 1, len(data) - 1)
v = data[i0] * (1.0 - frac) + data[i1] * frac
out[i] = int(v + 0.5) & 0xFF
row = table[int(frac * PHASES) & (PHASES - 1)]
base = i0 - (half_width - 1)
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)
# 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:
"""Scale TOP_O sample-offset args in raw pattern bytes by `ratio`.