mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-16 17:34:05 +09:00
midi2taud: --force-synth-loop
This commit is contained in:
58
midi2taud.py
58
midi2taud.py
@@ -1785,11 +1785,17 @@ def _effective_vol_env(z: SFZone, ms: 'MonoSample') -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def build_sample_inst_bin(sf: SF2, pool: list, layer_insts: list, meta_records: list,
|
def build_sample_inst_bin(sf: SF2, pool: list, layer_insts: list, meta_records: list,
|
||||||
fadeout_override, bpm0: int):
|
fadeout_override, bpm0: int, force_synth_loop: bool = False):
|
||||||
"""Render & pool every used MonoSample (with the 65535-byte per-sample
|
"""Render & pool every used MonoSample (with the 65535-byte per-sample
|
||||||
and 8 MB global caps), write the 256-byte normal-instrument records for every
|
and 8 MB global caps), write the 256-byte normal-instrument records for every
|
||||||
layer instrument, then the Metainstrument records. Returns the raw
|
layer instrument, then the Metainstrument records. Returns the raw
|
||||||
SAMPLEINST_SIZE image."""
|
SAMPLEINST_SIZE image.
|
||||||
|
|
||||||
|
`force_synth_loop`: when a looped sample's loop sits past the 65535-frame cap
|
||||||
|
even at 32 kHz (case 3c), replace its real loop with a synthesized one at the
|
||||||
|
32 kHz floor instead of muffling the whole sample down to fit (the default).
|
||||||
|
Trades the genuine sustain loop for full bandwidth + a 10 s decay — useful for
|
||||||
|
banks of multi-second far-loop instruments (e.g. Timbres of Heaven)."""
|
||||||
for ms in pool:
|
for ms in pool:
|
||||||
ms.render(sf)
|
ms.render(sf)
|
||||||
|
|
||||||
@@ -1817,15 +1823,13 @@ def build_sample_inst_bin(sf: SF2, pool: list, layer_insts: list, meta_records:
|
|||||||
ms.data = resample_linear(ms.data, r_fit)
|
ms.data = resample_linear(ms.data, r_fit)
|
||||||
ms.ratio *= len(ms.data) / native_len
|
ms.ratio *= len(ms.data) / native_len
|
||||||
|
|
||||||
if rate_fit >= SF2_RESAMPLE_FLOOR_HZ:
|
def _synth_path():
|
||||||
_fit_whole()
|
"""(3) resample to the 32 kHz floor (full bandwidth), keep the first 65535
|
||||||
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap; "
|
frames and synthesize a near-seamless sustain loop near the end, plus a
|
||||||
f"resampling by {r_fit:.4f} (rate {rate_fit:.0f} Hz)")
|
peak->0 decay vol-envelope that fades the looped note to silence from
|
||||||
elif ms.loop_native is None:
|
note-on. Used for unlooped long samples, and (with --force-synth-loop) for
|
||||||
# (3) No loop: resample to the 32 kHz floor (full bandwidth), keep the first
|
looped samples whose real loop won't fit at the floor. synth_loop takes
|
||||||
# 65535 frames and synthesize a near-seamless sustain loop near the end, plus
|
precedence over any real loop_native in the record/patch writers."""
|
||||||
# a peak->0 decay vol-envelope that fades the looped note to silence from
|
|
||||||
# note-on (the SF2 sample stops on its own otherwise; a loop would ring).
|
|
||||||
resampled = resample_linear(ms.data, r32)
|
resampled = resample_linear(ms.data, r32)
|
||||||
ms.ratio *= len(resampled) / native_len # effective rate -> 32 kHz
|
ms.ratio *= len(resampled) / native_len # effective rate -> 32 kHz
|
||||||
ms.data = resampled
|
ms.data = resampled
|
||||||
@@ -1833,8 +1837,17 @@ def build_sample_inst_bin(sf: SF2, pool: list, layer_insts: list, meta_records:
|
|||||||
ms.data = body
|
ms.data = body
|
||||||
ms.synth_loop = (ls, le)
|
ms.synth_loop = (ls, le)
|
||||||
ms.synth_decay = SF2_SYNTH_DECAY_SEC
|
ms.synth_decay = SF2_SYNTH_DECAY_SEC
|
||||||
|
return ls, le, len(body)
|
||||||
|
|
||||||
|
if rate_fit >= SF2_RESAMPLE_FLOOR_HZ:
|
||||||
|
_fit_whole()
|
||||||
|
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap; "
|
||||||
|
f"resampling by {r_fit:.4f} (rate {rate_fit:.0f} Hz)")
|
||||||
|
elif ms.loop_native is None:
|
||||||
|
# (3) No loop: synthesize one at the 32 kHz floor.
|
||||||
|
ls, le, n = _synth_path()
|
||||||
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long & unlooped; "
|
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long & unlooped; "
|
||||||
f"32 kHz, kept {len(body)} frames, synth loop [{ls}..{le}] "
|
f"32 kHz, kept {n} frames, synth loop [{ls}..{le}] "
|
||||||
f"+ {SF2_SYNTH_DECAY_SEC:.0f}s decay")
|
f"+ {SF2_SYNTH_DECAY_SEC:.0f}s decay")
|
||||||
elif le32 <= SAMPLE_LEN_LIMIT - 2:
|
elif le32 <= SAMPLE_LEN_LIMIT - 2:
|
||||||
# (3) Looped, and the loop fits at the 32 kHz floor: resample to 32 kHz and
|
# (3) Looped, and the loop fits at the 32 kHz floor: resample to 32 kHz and
|
||||||
@@ -1846,11 +1859,20 @@ def build_sample_inst_bin(sf: SF2, pool: list, layer_insts: list, meta_records:
|
|||||||
ms.data = resampled[:SAMPLE_LEN_LIMIT]
|
ms.data = resampled[:SAMPLE_LEN_LIMIT]
|
||||||
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long & looped; "
|
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long & looped; "
|
||||||
f"32 kHz, kept first {len(ms.data)} frames (loop_end {le32})")
|
f"32 kHz, kept first {len(ms.data)} frames (loop_end {le32})")
|
||||||
|
elif force_synth_loop:
|
||||||
|
# (3c, forced) Looped, far loop, but --force-synth-loop: drop the genuine
|
||||||
|
# loop and synthesize one at the 32 kHz floor rather than muffling the whole
|
||||||
|
# sample. Full bandwidth + a 10 s decay, at the cost of the real sustain.
|
||||||
|
ls, le, n = _synth_path()
|
||||||
|
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long, looped, far "
|
||||||
|
f"loop; FORCED synth: 32 kHz, kept {n} frames, synth loop [{ls}..{le}] "
|
||||||
|
f"+ {SF2_SYNTH_DECAY_SEC:.0f}s decay")
|
||||||
else:
|
else:
|
||||||
# (3) Looped but the loop sits past the 65535-frame cap at 32 kHz (a far-end
|
# (3) Looped but the loop sits past the 65535-frame cap at 32 kHz (a far-end
|
||||||
# sustain loop on a multi-second sample): the floor rate can't hold it, so
|
# sustain loop on a multi-second sample): the floor rate can't hold it, so
|
||||||
# downsample the whole sample to fit — the ratio-scaled loop stays valid,
|
# downsample the whole sample to fit — the ratio-scaled loop stays valid,
|
||||||
# at a sub-32 kHz rate. (This is the pre-existing fit-to-cap behaviour.)
|
# at a sub-32 kHz rate. (This is the pre-existing fit-to-cap behaviour;
|
||||||
|
# --force-synth-loop swaps it for the synth path above.)
|
||||||
_fit_whole()
|
_fit_whole()
|
||||||
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long, looped, "
|
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long, looped, "
|
||||||
f"far loop; fit-to-cap by {r_fit:.4f} (rate {ms.rate * r_fit:.0f} Hz)")
|
f"far loop; fit-to-cap by {r_fit:.4f} (rate {ms.rate * r_fit:.0f} Hz)")
|
||||||
@@ -2432,7 +2454,8 @@ def assemble_taud(sf: SF2, song: Song, layer_insts: list, meta_records: list,
|
|||||||
|
|
||||||
# ── Sample + instrument bin ──
|
# ── Sample + instrument bin ──
|
||||||
sampleinst_raw = build_sample_inst_bin(sf, pool, layer_insts, meta_records,
|
sampleinst_raw = build_sample_inst_bin(sf, pool, layer_insts, meta_records,
|
||||||
args.fadeout, bpm0)
|
args.fadeout, bpm0,
|
||||||
|
force_synth_loop=args.force_synth_loop)
|
||||||
assert len(sampleinst_raw) == SAMPLEINST_SIZE
|
assert len(sampleinst_raw) == SAMPLEINST_SIZE
|
||||||
compressed = compress_blob(sampleinst_raw, "sample+inst bin")
|
compressed = compress_blob(sampleinst_raw, "sample+inst bin")
|
||||||
comp_size = len(compressed)
|
comp_size = len(compressed)
|
||||||
@@ -2569,6 +2592,13 @@ def main():
|
|||||||
ap.add_argument('--drum-keyoff', action='store_true',
|
ap.add_argument('--drum-keyoff', action='store_true',
|
||||||
help='Emit KEY_OFF for percussion-channel notes too '
|
help='Emit KEY_OFF for percussion-channel notes too '
|
||||||
'(GM drums normally ignore note-off)')
|
'(GM drums normally ignore note-off)')
|
||||||
|
ap.add_argument('--force-synth-loop', action='store_true',
|
||||||
|
help='For looped samples whose loop sits past the 65535-frame '
|
||||||
|
'cap even at 32 kHz (multi-second far-loop instruments, '
|
||||||
|
'e.g. Timbres of Heaven): replace the real loop with a '
|
||||||
|
'synthesized one at 32 kHz + a 10 s decay, instead of '
|
||||||
|
'muffling the whole sample down to fit. Trades the genuine '
|
||||||
|
'sustain loop for full bandwidth')
|
||||||
ap.add_argument('--no-project-data', action='store_true',
|
ap.add_argument('--no-project-data', action='store_true',
|
||||||
help='Omit the Project Data section — NOTE: this also '
|
help='Omit the Project Data section — NOTE: this also '
|
||||||
'omits Ixmp, collapsing every instrument to its '
|
'omits Ixmp, collapsing every instrument to its '
|
||||||
|
|||||||
@@ -2780,6 +2780,8 @@ TODO:
|
|||||||
1. If length exceeds 65535 samples, calculate resampling.
|
1. If length exceeds 65535 samples, calculate resampling.
|
||||||
2. If calculated resampling >= 32000, use that.
|
2. If calculated resampling >= 32000, use that.
|
||||||
3. If not, resample at 32000. If there is no loop defined, then loop the last 8192 samples (converter SHOULD NOT take that number at face value; perform waveform analysis to derive a smoother loop; converter MAY use that number as a starting number) and modify the fade value such that it decays to zero after 10 or so seconds of firing.
|
3. If not, resample at 32000. If there is no loop defined, then loop the last 8192 samples (converter SHOULD NOT take that number at face value; perform waveform analysis to derive a smoother loop; converter MAY use that number as a starting number) and modify the fade value such that it decays to zero after 10 or so seconds of firing.
|
||||||
|
[x] Add option to force this behaviour for samples with loops that are far beyond the 64k limit (e.g. Timbres of Heaven)
|
||||||
|
* DONE 2026-06-15. Added `--force-synth-loop`
|
||||||
[x] Faithful .sf2 "release segment": Set NNA to 'Note Fade' (incl. drumkits), and make sure Volume Fadeout to have a correct number derived from the SF2 timecent unit (it seems SF2 defines envelope floor as 100 dB; needs check)
|
[x] Faithful .sf2 "release segment": Set NNA to 'Note Fade' (incl. drumkits), and make sure Volume Fadeout to have a correct number derived from the SF2 timecent unit (it seems SF2 defines envelope floor as 100 dB; needs check)
|
||||||
* DONE 2026-06-14. Floor CONFIRMED 100 dB (sfspec24.txt:1934-1941: releaseVolEnv ramps a constant 100 dB per its timecent value, "until 100dB attenuation were reached"). midi2taud.py now: (a) byte 186 NNA = Note Fade (0b11) for every instrument incl. drum kits (was melodic Key-Lift 0b100000 / drum Continue 0b10); (b) the vol-env no longer carries a release leg — it ENDS at the sustain node and the engine holds there on key-off (AudioAdapter.kt:1697-1701 holds a non-zero terminator, doesn't cut); (c) Volume Fadeout (base bytes 172-173 AND per-patch Ixmp 'x' block) = the release segment, fade_sec = releaseVolEnv·(1000−sus_cb)/1000 (the sustain-level → 100 dB-floor time), fadeStep = 2560/(fade_sec·bpm0) so the linear fade completes in that wall-clock time. Per-patch 'x' now also emits when only the release differs (faithful per-layer release). The engine's Key-Lift feature is unchanged (still used by KeyLiftTest); midi2taud simply stopped emitting it. See _zone_fadeout / _adsr_to_env in midi2taud.py.
|
* DONE 2026-06-14. Floor CONFIRMED 100 dB (sfspec24.txt:1934-1941: releaseVolEnv ramps a constant 100 dB per its timecent value, "until 100dB attenuation were reached"). midi2taud.py now: (a) byte 186 NNA = Note Fade (0b11) for every instrument incl. drum kits (was melodic Key-Lift 0b100000 / drum Continue 0b10); (b) the vol-env no longer carries a release leg — it ENDS at the sustain node and the engine holds there on key-off (AudioAdapter.kt:1697-1701 holds a non-zero terminator, doesn't cut); (c) Volume Fadeout (base bytes 172-173 AND per-patch Ixmp 'x' block) = the release segment, fade_sec = releaseVolEnv·(1000−sus_cb)/1000 (the sustain-level → 100 dB-floor time), fadeStep = 2560/(fade_sec·bpm0) so the linear fade completes in that wall-clock time. Per-patch 'x' now also emits when only the release differs (faithful per-layer release). The engine's Key-Lift feature is unchanged (still used by KeyLiftTest); midi2taud simply stopped emitting it. See _zone_fadeout / _adsr_to_env in midi2taud.py.
|
||||||
[x] SF2 filter still sounds way too muffled?
|
[x] SF2 filter still sounds way too muffled?
|
||||||
@@ -2796,9 +2798,6 @@ TODO:
|
|||||||
(startFastFade): a ~0.3 s note-fade while the sample keeps playing, distinct from
|
(startFastFade): a ~0.3 s note-fade while the sample keeps playing, distinct from
|
||||||
^^CUT's hard stop. Headless coverage: devtests/ixmp/FastFadeTest. Verified on
|
^^CUT's hard stop. Headless coverage: devtests/ixmp/FastFadeTest. Verified on
|
||||||
E2M1 (open hat no longer rings >0.3 s; crashes, not in a class, still ring out).
|
E2M1 (open hat no longer rings >0.3 s; crashes, not in a class, still ring out).
|
||||||
[ ] midi2taud: toggleable option for disabling filter for percussions [default: on]
|
|
||||||
- Anything on bank 127 and 128 (usually asso siated with ch 10)
|
|
||||||
- GeneralMIDI instruments 113..128
|
|
||||||
[x] midi2taud: instrument fadeout (release) is significantly longer than Fluidsynth
|
[x] midi2taud: instrument fadeout (release) is significantly longer than Fluidsynth
|
||||||
* DONE 2026-06-14. _zone_fadeout (midi2taud.py) now scales fade_sec by
|
* DONE 2026-06-14. _zone_fadeout (midi2taud.py) now scales fade_sec by
|
||||||
_RELEASE_PERCEPTUAL_SCALE = 0.25, bringing the fadeout in line with FluidSynth's perceived
|
_RELEASE_PERCEPTUAL_SCALE = 0.25, bringing the fadeout in line with FluidSynth's perceived
|
||||||
|
|||||||
Reference in New Issue
Block a user