Audio (AUDIO_FIDELITY F14 residue): bake the authored static resonant
low-pass into the zone WAVs 232 zones author initialFilterFc(8)/initialFilterQ(9) (SBK 0..127) -- the fixed resonant low-pass the EMU8000 applied in hardware; the port played them unfiltered (flagship: the LaserLoaded charge hum, fc=57 Q=87, much brighter/harsher than the arcade). The extractor now applies an RBJ 2-pole low-pass per zone: cutoff = the AWE NRPN curve (100 + fc*7900/127 Hz), resonance = SBK Q -> 0..+12 dB peak [T3 curve, endpoints exact], designed at the zone's baked rate so note-60 playback reproduces the hardware's absolute cutoff (pitch-shifted notes carry the filter -- same limitation as the rate-baked tuning). Synthetic sweep verified: flat lows, +5.4 dB at the authored 3.6 kHz cutoff, -22 dB @10 kHz, -46 dB @14 kHz. Whole bake pipeline (filter -> attenuation) now runs in float with ONE int16 conversion + peak normalization: an int-per-stage draft hard-clipped 165 of 232 zones (resonance overshoot, worst 3% of samples); now only pre-existing source-material clipping remains (14 files). 160 WAVs re-baked; the preset table is unchanged (no engine rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a11a697824
commit
c581553a6c
+62
-6
@@ -42,6 +42,7 @@ MAX_ZONES = 25 # PRESETINFO samples[] capacity (AllExplosion = 25 la
|
||||
|
||||
G_KEYRANGE, G_PAN, G_RELEASE, G_INST, G_KEYRANGE2 = 43, 17, 38, 41, 43
|
||||
G_ATTEN, G_COARSE, G_FINE, G_SAMPLEID, G_MODES, G_PITCH, G_ROOT = 48, 51, 52, 53, 54, 55, 58
|
||||
G_FILTER_FC, G_FILTER_Q = 8, 9
|
||||
SIGNED_GENS = {G_COARSE, G_FINE, G_RELEASE}
|
||||
|
||||
|
||||
@@ -138,16 +139,71 @@ def zone_fields(bank, g):
|
||||
release = min(8.0, 2.0 ** (rel_tc / 1200.0)) if rel_tc is not None else 0.0
|
||||
pan = g.get(G_PAN, 64)
|
||||
chan = "CHANNEL_LEFT" if pan <= 42 else ("CHANNEL_RIGHT" if pan >= 85 else "CHANNEL_CENTER")
|
||||
# (AUDIO_FIDELITY F14) the authored static resonant low-pass the EMU8000
|
||||
# applied in hardware: initialFilterFc(8) / initialFilterQ(9), SBK 0..127
|
||||
# scale. Absent fc = filter fully open.
|
||||
filter_fc = g.get(G_FILTER_FC, 127)
|
||||
filter_q = g.get(G_FILTER_Q, 0)
|
||||
return dict(pcm=pcm, rate=rate, keylo=keylo, keyhi=keyhi, looping=looping,
|
||||
lstart=lstart, lend=lend, gain=gain, release=release, chan=chan,
|
||||
sid=g[G_SAMPLEID], root_cents=root_cents, tune=tune)
|
||||
sid=g[G_SAMPLEID], root_cents=root_cents, tune=tune,
|
||||
fc=filter_fc, q=filter_q)
|
||||
|
||||
|
||||
def write_wav(fname, pcm, rate, gain):
|
||||
if gain < 0.999: # bake layer-balance attenuation
|
||||
def bake_lowpass(pcm, rate, fc127, q127):
|
||||
"""(AUDIO_FIDELITY F14) bake the zone's authored static resonant low-pass
|
||||
into the PCM. Cutoff uses the AWE NRPN curve (100 + fc*7900/127 Hz);
|
||||
resonance maps SBK Q 0..127 -> 0..+12 dB biquad peak [T3 -- curve shape
|
||||
approximate, off/open endpoints exact]. The filter is designed at the
|
||||
zone's BAKED rate, so note-60 playback (1 file second == 1 real second)
|
||||
reproduces the hardware's absolute cutoff exactly; pitch-shifted notes
|
||||
carry the filter with them (same limitation as the rate-baked tuning)."""
|
||||
cutoff_hz = 100.0 + fc127 * 7900.0 / 127.0
|
||||
if cutoff_hz >= 0.45 * rate: # at/above Nyquist headroom: no-op
|
||||
return None
|
||||
q_biquad = 0.7071 * 10.0 ** ((q127 / 127.0 * 12.0) / 20.0)
|
||||
# RBJ cookbook 2-pole low-pass, run in FLOAT (the caller converts once)
|
||||
w0 = 2.0 * math.pi * cutoff_hz / rate
|
||||
alpha = math.sin(w0) / (2.0 * q_biquad)
|
||||
cw = math.cos(w0)
|
||||
b0 = (1.0 - cw) / 2.0
|
||||
b1 = 1.0 - cw
|
||||
b2 = (1.0 - cw) / 2.0
|
||||
a0 = 1.0 + alpha
|
||||
a1 = -2.0 * cw
|
||||
a2 = 1.0 - alpha
|
||||
b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0
|
||||
n = len(pcm) // 2
|
||||
vals = struct.unpack("<%dh" % n, pcm[:n * 2])
|
||||
out = [0.0] * n
|
||||
x1 = x2 = y1 = y2 = 0.0
|
||||
for i, x0 in enumerate(vals):
|
||||
y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2
|
||||
x2 = x1; x1 = x0
|
||||
y2 = y1; y1 = y0
|
||||
out[i] = y0
|
||||
return out
|
||||
|
||||
|
||||
def write_wav(fname, pcm, rate, gain, fc127=127, q127=0):
|
||||
# Full bake pipeline in FLOAT, one int16 conversion at the end: the
|
||||
# resonance peak can overshoot full scale (165 of 232 filtered zones
|
||||
# hard-clipped in an int-per-stage draft) -- normalize the whole file
|
||||
# down to fit instead of clipping (level error <= the overshoot, wave
|
||||
# shape preserved; the EMU's filter stage had internal headroom).
|
||||
fvals = None
|
||||
if fc127 < 127: # authored static low-pass
|
||||
fvals = bake_lowpass(pcm, rate, fc127, q127)
|
||||
if fvals is None and gain < 0.999:
|
||||
n = len(pcm) // 2
|
||||
vals = struct.unpack("<%dh" % n, pcm[:n * 2])
|
||||
pcm = struct.pack("<%dh" % n, *[max(-32768, min(32767, int(v * gain))) for v in vals])
|
||||
fvals = [float(v) for v in struct.unpack("<%dh" % n, pcm[:n * 2])]
|
||||
if fvals is not None:
|
||||
if gain < 0.999: # layer-balance attenuation
|
||||
fvals = [v * gain for v in fvals]
|
||||
peak = max(1.0, max(abs(v) for v in fvals))
|
||||
norm = (32767.0 / peak) if peak > 32767.0 else 1.0
|
||||
pcm = struct.pack("<%dh" % len(fvals),
|
||||
*[int(v * norm) for v in fvals])
|
||||
with wave.open(os.path.join(AUDIO, fname), "wb") as w:
|
||||
w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate)
|
||||
w.writeframes(pcm)
|
||||
@@ -178,7 +234,7 @@ def main():
|
||||
continue
|
||||
for k, z in enumerate(zs):
|
||||
z["file"] = f"{name}.wav" if len(zs) == 1 else f"{name}_z{k}.wav"
|
||||
write_wav(z["file"], z["pcm"], z["rate"], z["gain"])
|
||||
write_wav(z["file"], z["pcm"], z["rate"], z["gain"], z["fc"], z["q"])
|
||||
cents = 1200.0 * math.log2(z["rate"] / 44100.0)
|
||||
if abs(cents - round(cents / 100.0) * 100) > 3 and not z["tune"] % 100:
|
||||
grid_outliers.append((name, k, z["rate"], round(cents, 1)))
|
||||
|
||||
Reference in New Issue
Block a user