mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-19 02:44:04 +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,
|
||||
fadeout_override, bpm0: int):
|
||||
fadeout_override, bpm0: int, force_synth_loop: bool = False):
|
||||
"""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
|
||||
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:
|
||||
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.ratio *= len(ms.data) / native_len
|
||||
|
||||
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: resample to the 32 kHz floor (full bandwidth), keep the first
|
||||
# 65535 frames and synthesize a near-seamless sustain loop near the end, plus
|
||||
# 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).
|
||||
def _synth_path():
|
||||
"""(3) resample to the 32 kHz floor (full bandwidth), keep the first 65535
|
||||
frames and synthesize a near-seamless sustain loop near the end, plus a
|
||||
peak->0 decay vol-envelope that fades the looped note to silence from
|
||||
note-on. Used for unlooped long samples, and (with --force-synth-loop) for
|
||||
looped samples whose real loop won't fit at the floor. synth_loop takes
|
||||
precedence over any real loop_native in the record/patch writers."""
|
||||
resampled = resample_linear(ms.data, r32)
|
||||
ms.ratio *= len(resampled) / native_len # effective rate -> 32 kHz
|
||||
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.synth_loop = (ls, le)
|
||||
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; "
|
||||
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")
|
||||
elif le32 <= SAMPLE_LEN_LIMIT - 2:
|
||||
# (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]
|
||||
vprint(f" info: '{ms.name}' {native_len} frames > 64K cap, long & looped; "
|
||||
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:
|
||||
# (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
|
||||
# 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()
|
||||
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)")
|
||||
@@ -2432,7 +2454,8 @@ def assemble_taud(sf: SF2, song: Song, layer_insts: list, meta_records: list,
|
||||
|
||||
# ── Sample + instrument bin ──
|
||||
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
|
||||
compressed = compress_blob(sampleinst_raw, "sample+inst bin")
|
||||
comp_size = len(compressed)
|
||||
@@ -2569,6 +2592,13 @@ def main():
|
||||
ap.add_argument('--drum-keyoff', action='store_true',
|
||||
help='Emit KEY_OFF for percussion-channel notes too '
|
||||
'(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',
|
||||
help='Omit the Project Data section — NOTE: this also '
|
||||
'omits Ixmp, collapsing every instrument to its '
|
||||
|
||||
211
terranmon.txt
211
terranmon.txt
@@ -2775,112 +2775,111 @@ TODO:
|
||||
[x] Ixmp version 2, supporting per-patch ADSR
|
||||
For UI concerns, taut_instredit.js will take care of it (aka problem for later)
|
||||
[x] .sf2 import module (for generic use, including "Import instrument from soundfont" and midi2taud conversion)
|
||||
[x] Midi2Taud using .mid and .sf2 as input, trim unused samples and Ixmp patches
|
||||
[x] .sf2 specific resample handling
|
||||
1. If length exceeds 65535 samples, calculate resampling.
|
||||
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.
|
||||
[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.
|
||||
[x] SF2 filter still sounds way too muffled?
|
||||
[x] Drum notes get eaten (E2M1.mid)
|
||||
* DONE 2026-06-14. Root cause: midi2taud ignored SF2 exclusiveClass (gen 57). SGM's
|
||||
STANDARD 1 puts the closed/pedal/open hi-hats (keys 42/44/46) in class 1; without the
|
||||
choke the open hi-hat's multi-second tail (≈4.9 s) washes over the whole beat and
|
||||
buries the other hits. FluidSynth kills same-class voices on note-on
|
||||
(fluid_synth_kill_by_exclusive_class → fluid_voice_kill_excl, a fast −2000-timecent
|
||||
release ≈ 0.3 s). Fix in two parts: (a) midi2taud.py parses gen 57, and per drum
|
||||
channel chokes each note at the next same-class onset — emitting the new fast-fade
|
||||
note 0x0004 (apply_exclusive_class + emit_cells pass 2b; allocate_voices holds the
|
||||
choked voice foreground until the choke); (b) the engine implements note 0x0004
|
||||
(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
|
||||
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
|
||||
* 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. fadeStep comes out ~4× larger (faster fade). I kept the IT/FT2 engine path untouched
|
||||
— it's shared, byte-faithful tracker behaviour and must stay linear-amplitude; the
|
||||
compensation belongs on the encoder side. 0.25 targets the ~−22 dB "release ended" point.
|
||||
If you find sustained pads now cut a touch short (their long tails are more noticeable),
|
||||
nudging it toward 0.30–0.35 lengthens the tail without returning to the old over-long
|
||||
behaviour.
|
||||
[x] auto-set optimal-ish Tickspeed and RPB using MIDI Time Signature events and note analysis. Break pattern when Time Signature changes.
|
||||
* DONE 2026-06-14. midi2taud.py now parses the time-signature meta event (FF 58) and
|
||||
auto-sets the grid by DEFAULT (--rpb/--speed default to auto; passing one pins that
|
||||
axis and auto-fits the other, pass both to override). auto_timing() picks F = rpb·speed
|
||||
fine-ticks/beat to (a) represent the finest onset subdivision actually used
|
||||
(_detect_subdivision: smallest 1/D-of-quarter grid covering >=95% of note onsets,
|
||||
D in {1,2,3,4,6,8,12,16}), (b) keep every tempo inside the Taud BPM register [25,280],
|
||||
and (c) anchor at the proven 24-fts/beat grid (smallest multiple of D that is >=24), so
|
||||
plain material reproduces the old speed 6 / rpb 4. Lexicographic key: init-tempo-fits >
|
||||
fewest-clamped-tempos > prefer-rpb-4 (rows = beats×rpb, so rpb caps pattern count;
|
||||
speed is "free" sub-row + tempo precision) > closeness-to-F_want > exact-grid > near
|
||||
speed 6. Verified rpb4/speed6 on Onestop / E1M1 / E2M1 / flourish / keep_on_rolling /
|
||||
pokemon-theme. RPB bump (final step, both axes auto only): a bend-heavy (>=24 non-centre
|
||||
bend events AND >=0.25/note) OR many-polyphony (peak simultaneous notes >= 10) song with
|
||||
rpb < 8 gets rpb doubled / speed halved (F=rpb·speed, hence tempo, UNCHANGED) up to rpb 8,
|
||||
so more rows host key-offs / chokes / bend-G / channel-M and fewer are eaten by same-row &
|
||||
per-cell-slot collisions. Guarded by a cue/pattern-budget estimate so it can't flip a long
|
||||
dense song into a hard error (pin --rpb 4 to opt out). Measured on Onestop: key-offs
|
||||
absorbed 1856→1574, bend segments 266→552. All six demo MIDIs are dense → rpb8/speed3.
|
||||
Pattern breaking: plan_cues() breaks a cue at every time-sig change and
|
||||
packs each section into whole-bar cues (largest multiple of the bar length that fits in
|
||||
64 rows) via the LEN cue instruction (constant 4/4 still = 64-row cues; 7/8 flourish =
|
||||
56-row cues; mid-song change starts the new section on a fresh cue). The project-data
|
||||
sMet block gets Primary beat division = rows per NOTATED beat (the time-sig denominator,
|
||||
= round(rpb·4/2^dpow): 4 for x/4, 2 for x/8 — always a divisor of the bar so the highlight
|
||||
stays bar-aligned; rpb=rows-per-quarter would drift on 7/8 since 14%4!=0) and Secondary =
|
||||
rows per bar, so the tracker's bar/beat highlight lines up. sMet notation = 120 (12-TET;
|
||||
MIDI is 12-TET) — REQUIRED now that taut.js honours notation on load (the taud_common
|
||||
default 0 = "Raw format" would show hex note numbers). build_pattern_bin now takes
|
||||
(cue_starts, cue_lens) instead of a flat 64-row chunking.
|
||||
taut.js side: loadTaudSongList now reads beat_pri/beat_sec from sMet (were skipped) into
|
||||
songsMeta.songs[i]; applySongBeatDiv() + applySongPitchPreset() apply per-song beat
|
||||
divisions AND notation/pitch-preset on BOTH initial open and switchSong (initial open
|
||||
previously left both at the 4/16 + 12-TET defaults — the latent pitch-preset gap is now
|
||||
closed too). Only midi2taud emits sMet; the other 2taud converters omit it → 12-TET default.
|
||||
[x] Taut UI commit
|
||||
- Inst > Gen.1 > sample binding: ~~~....[two doubledots] et al. (n extra samples)
|
||||
- Inst > Gen.2 > filter: IT/SF mode toggle (which also need to redefine slider range and their writebacks as IT takes 8-bit and SF takes 16-bit values)
|
||||
- Samples playblobs: only active for actually playing samples
|
||||
- Samples playcursor: true cursors for actually playing samples
|
||||
[x] implement note-fade (0x0003) and wire it to it2taud
|
||||
* DONE 2026-06-14. Engine: note word 0x0003 sets voice.noteFading (IT CHN_NOTEFADE,
|
||||
Schism effects.c:1505-1509) — the instrument's own activeFadeoutStep drives
|
||||
fadeoutVolume to 0 in the line ~3676 fade path while sustain loop + vol envelope
|
||||
keep running; no applyKeyLift (sustain kept), no rate override (unlike fast fade
|
||||
0x0004); a zero fadeout rings on, matching IT. Honours sub-row S$Dx delay (delayed
|
||||
dispatch L3453). it2taud: IT_NOTE_FADE (246) now emits NOTE_NOTEFADE (0x0003)
|
||||
instead of collapsing to key-off; NOTE_NOTEFADE added to taud_common.py.
|
||||
[x] taut.js quit with TypeError: Cannot read property 'terminatorIdx' of undefined:
|
||||
TypeError: Cannot read property 'terminatorIdx' of undefined
|
||||
at drawEnvelopeCursor (<eval>:5312:22)
|
||||
at drawInstrumentsContents (<eval>:4919:41)
|
||||
at Object.onClick (<eval>:4984:17)
|
||||
at dispatchMouseEvent (<eval>:6275:53)
|
||||
at <eval>:6633:13
|
||||
at Object.withEvent (<eval>:1168:27)
|
||||
at _tvdosExec$microtone$tvdos$bin$taut$js (<eval>:6632:11)
|
||||
at execApp (<eval>:1457:16)
|
||||
at Object.execute (<eval>:893:38)
|
||||
at <eval>:867:31
|
||||
[x] Timbres of Heaven's Overdriven Gt decays EXTREMELY SLOWLY even after the fix
|
||||
* DONE 2026-06-15. Root cause (engine). Overdriven Gt becomes a 4-layer Metainstrument.
|
||||
Layer children inherit the parent's key-off only via the per-tick background-voice sync
|
||||
(AudioAdapter.kt:3782), which requires parent.active. With a fast fadeout (fo=1067 = 1-tick
|
||||
cut), the foreground voice deactivates in the same tick KEY_OFF fires — before the sync runs —
|
||||
so the children were detached as orphans that never picked up the release and kept looping at
|
||||
the ~0.70 sustain until the next note. That's the "extremely slow decay"/ringing tail, and it's
|
||||
ToH-specific because ToH leans on multi-layer presets with short releases.
|
||||
Fix (AudioAdapter.kt, inactive-parent branch): inherit parent.keyOff/noteFading before
|
||||
detaching, mirroring the existing active-branch. parent.keyOff survives deactivation and is
|
||||
reset on retrigger, so a true value means this note was released. A parent that ended naturally
|
||||
(no release) still leaves the child to finish on its own.
|
||||
[ ] Some ways to decouple Sample+Inst and patterns into separate files (tsvm-doom needs separate file access; samplepack can be uploaded once on init)
|
||||
[x] Midi2Taud using .mid and .sf2 as input, trim unused samples and Ixmp patches
|
||||
[x] .sf2 specific resample handling
|
||||
1. If length exceeds 65535 samples, calculate resampling.
|
||||
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.
|
||||
[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)
|
||||
* 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] Drum notes get eaten (E2M1.mid)
|
||||
* DONE 2026-06-14. Root cause: midi2taud ignored SF2 exclusiveClass (gen 57). SGM's
|
||||
STANDARD 1 puts the closed/pedal/open hi-hats (keys 42/44/46) in class 1; without the
|
||||
choke the open hi-hat's multi-second tail (≈4.9 s) washes over the whole beat and
|
||||
buries the other hits. FluidSynth kills same-class voices on note-on
|
||||
(fluid_synth_kill_by_exclusive_class → fluid_voice_kill_excl, a fast −2000-timecent
|
||||
release ≈ 0.3 s). Fix in two parts: (a) midi2taud.py parses gen 57, and per drum
|
||||
channel chokes each note at the next same-class onset — emitting the new fast-fade
|
||||
note 0x0004 (apply_exclusive_class + emit_cells pass 2b; allocate_voices holds the
|
||||
choked voice foreground until the choke); (b) the engine implements note 0x0004
|
||||
(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
|
||||
E2M1 (open hat no longer rings >0.3 s; crashes, not in a class, still ring out).
|
||||
[x] midi2taud: instrument fadeout (release) is significantly longer than Fluidsynth
|
||||
* 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. fadeStep comes out ~4× larger (faster fade). I kept the IT/FT2 engine path untouched
|
||||
— it's shared, byte-faithful tracker behaviour and must stay linear-amplitude; the
|
||||
compensation belongs on the encoder side. 0.25 targets the ~−22 dB "release ended" point.
|
||||
If you find sustained pads now cut a touch short (their long tails are more noticeable),
|
||||
nudging it toward 0.30–0.35 lengthens the tail without returning to the old over-long
|
||||
behaviour.
|
||||
[x] auto-set optimal-ish Tickspeed and RPB using MIDI Time Signature events and note analysis. Break pattern when Time Signature changes.
|
||||
* DONE 2026-06-14. midi2taud.py now parses the time-signature meta event (FF 58) and
|
||||
auto-sets the grid by DEFAULT (--rpb/--speed default to auto; passing one pins that
|
||||
axis and auto-fits the other, pass both to override). auto_timing() picks F = rpb·speed
|
||||
fine-ticks/beat to (a) represent the finest onset subdivision actually used
|
||||
(_detect_subdivision: smallest 1/D-of-quarter grid covering >=95% of note onsets,
|
||||
D in {1,2,3,4,6,8,12,16}), (b) keep every tempo inside the Taud BPM register [25,280],
|
||||
and (c) anchor at the proven 24-fts/beat grid (smallest multiple of D that is >=24), so
|
||||
plain material reproduces the old speed 6 / rpb 4. Lexicographic key: init-tempo-fits >
|
||||
fewest-clamped-tempos > prefer-rpb-4 (rows = beats×rpb, so rpb caps pattern count;
|
||||
speed is "free" sub-row + tempo precision) > closeness-to-F_want > exact-grid > near
|
||||
speed 6. Verified rpb4/speed6 on Onestop / E1M1 / E2M1 / flourish / keep_on_rolling /
|
||||
pokemon-theme. RPB bump (final step, both axes auto only): a bend-heavy (>=24 non-centre
|
||||
bend events AND >=0.25/note) OR many-polyphony (peak simultaneous notes >= 10) song with
|
||||
rpb < 8 gets rpb doubled / speed halved (F=rpb·speed, hence tempo, UNCHANGED) up to rpb 8,
|
||||
so more rows host key-offs / chokes / bend-G / channel-M and fewer are eaten by same-row &
|
||||
per-cell-slot collisions. Guarded by a cue/pattern-budget estimate so it can't flip a long
|
||||
dense song into a hard error (pin --rpb 4 to opt out). Measured on Onestop: key-offs
|
||||
absorbed 1856→1574, bend segments 266→552. All six demo MIDIs are dense → rpb8/speed3.
|
||||
Pattern breaking: plan_cues() breaks a cue at every time-sig change and
|
||||
packs each section into whole-bar cues (largest multiple of the bar length that fits in
|
||||
64 rows) via the LEN cue instruction (constant 4/4 still = 64-row cues; 7/8 flourish =
|
||||
56-row cues; mid-song change starts the new section on a fresh cue). The project-data
|
||||
sMet block gets Primary beat division = rows per NOTATED beat (the time-sig denominator,
|
||||
= round(rpb·4/2^dpow): 4 for x/4, 2 for x/8 — always a divisor of the bar so the highlight
|
||||
stays bar-aligned; rpb=rows-per-quarter would drift on 7/8 since 14%4!=0) and Secondary =
|
||||
rows per bar, so the tracker's bar/beat highlight lines up. sMet notation = 120 (12-TET;
|
||||
MIDI is 12-TET) — REQUIRED now that taut.js honours notation on load (the taud_common
|
||||
default 0 = "Raw format" would show hex note numbers). build_pattern_bin now takes
|
||||
(cue_starts, cue_lens) instead of a flat 64-row chunking.
|
||||
taut.js side: loadTaudSongList now reads beat_pri/beat_sec from sMet (were skipped) into
|
||||
songsMeta.songs[i]; applySongBeatDiv() + applySongPitchPreset() apply per-song beat
|
||||
divisions AND notation/pitch-preset on BOTH initial open and switchSong (initial open
|
||||
previously left both at the 4/16 + 12-TET defaults — the latent pitch-preset gap is now
|
||||
closed too). Only midi2taud emits sMet; the other 2taud converters omit it → 12-TET default.
|
||||
[x] Taut UI commit
|
||||
- Inst > Gen.1 > sample binding: ~~~....[two doubledots] et al. (n extra samples)
|
||||
- Inst > Gen.2 > filter: IT/SF mode toggle (which also need to redefine slider range and their writebacks as IT takes 8-bit and SF takes 16-bit values)
|
||||
- Samples playblobs: only active for actually playing samples
|
||||
- Samples playcursor: true cursors for actually playing samples
|
||||
[x] implement note-fade (0x0003) and wire it to it2taud
|
||||
* DONE 2026-06-14. Engine: note word 0x0003 sets voice.noteFading (IT CHN_NOTEFADE,
|
||||
Schism effects.c:1505-1509) — the instrument's own activeFadeoutStep drives
|
||||
fadeoutVolume to 0 in the line ~3676 fade path while sustain loop + vol envelope
|
||||
keep running; no applyKeyLift (sustain kept), no rate override (unlike fast fade
|
||||
0x0004); a zero fadeout rings on, matching IT. Honours sub-row S$Dx delay (delayed
|
||||
dispatch L3453). it2taud: IT_NOTE_FADE (246) now emits NOTE_NOTEFADE (0x0003)
|
||||
instead of collapsing to key-off; NOTE_NOTEFADE added to taud_common.py.
|
||||
[x] taut.js quit with TypeError: Cannot read property 'terminatorIdx' of undefined:
|
||||
TypeError: Cannot read property 'terminatorIdx' of undefined
|
||||
at drawEnvelopeCursor (<eval>:5312:22)
|
||||
at drawInstrumentsContents (<eval>:4919:41)
|
||||
at Object.onClick (<eval>:4984:17)
|
||||
at dispatchMouseEvent (<eval>:6275:53)
|
||||
at <eval>:6633:13
|
||||
at Object.withEvent (<eval>:1168:27)
|
||||
at _tvdosExec$microtone$tvdos$bin$taut$js (<eval>:6632:11)
|
||||
at execApp (<eval>:1457:16)
|
||||
at Object.execute (<eval>:893:38)
|
||||
at <eval>:867:31
|
||||
[x] Timbres of Heaven's Overdriven Gt decays EXTREMELY SLOWLY even after the fix
|
||||
* DONE 2026-06-15. Root cause (engine). Overdriven Gt becomes a 4-layer Metainstrument.
|
||||
Layer children inherit the parent's key-off only via the per-tick background-voice sync
|
||||
(AudioAdapter.kt:3782), which requires parent.active. With a fast fadeout (fo=1067 = 1-tick
|
||||
cut), the foreground voice deactivates in the same tick KEY_OFF fires — before the sync runs —
|
||||
so the children were detached as orphans that never picked up the release and kept looping at
|
||||
the ~0.70 sustain until the next note. That's the "extremely slow decay"/ringing tail, and it's
|
||||
ToH-specific because ToH leans on multi-layer presets with short releases.
|
||||
Fix (AudioAdapter.kt, inactive-parent branch): inherit parent.keyOff/noteFading before
|
||||
detaching, mirroring the existing active-branch. parent.keyOff survives deactivation and is
|
||||
reset on retrigger, so a true value means this note was released. A parent that ended naturally
|
||||
(no release) still leaves the child to finish on its own.
|
||||
[ ] Some ways to decouple Sample+Inst and patterns into separate files (tsvm-doom needs separate file access; samplepack can be uploaded once on init)
|
||||
|
||||
TODO - list of demo songs that MUST ship with Microtone:
|
||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
||||
|
||||
Reference in New Issue
Block a user