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:
23
midi2taud.py
23
midi2taud.py
@@ -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
|
||||
|
||||
|
||||
|
||||
108
taud_common.py
108
taud_common.py
@@ -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`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user