mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-15 17:04: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,
|
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 '
|
||||||
|
|||||||
211
terranmon.txt
211
terranmon.txt
@@ -2775,112 +2775,111 @@ TODO:
|
|||||||
[x] Ixmp version 2, supporting per-patch ADSR
|
[x] Ixmp version 2, supporting per-patch ADSR
|
||||||
For UI concerns, taut_instredit.js will take care of it (aka problem for later)
|
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] .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] Midi2Taud using .mid and .sf2 as input, trim unused samples and Ixmp patches
|
||||||
[x] .sf2 specific resample handling
|
[x] .sf2 specific resample handling
|
||||||
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] 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] 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-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-15. Added `--force-synth-loop`
|
||||||
[x] SF2 filter still sounds way too muffled?
|
[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] Drum notes get eaten (E2M1.mid)
|
* 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. Root cause: midi2taud ignored SF2 exclusiveClass (gen 57). SGM's
|
[x] SF2 filter still sounds way too muffled?
|
||||||
STANDARD 1 puts the closed/pedal/open hi-hats (keys 42/44/46) in class 1; without the
|
[x] Drum notes get eaten (E2M1.mid)
|
||||||
choke the open hi-hat's multi-second tail (≈4.9 s) washes over the whole beat and
|
* DONE 2026-06-14. Root cause: midi2taud ignored SF2 exclusiveClass (gen 57). SGM's
|
||||||
buries the other hits. FluidSynth kills same-class voices on note-on
|
STANDARD 1 puts the closed/pedal/open hi-hats (keys 42/44/46) in class 1; without the
|
||||||
(fluid_synth_kill_by_exclusive_class → fluid_voice_kill_excl, a fast −2000-timecent
|
choke the open hi-hat's multi-second tail (≈4.9 s) washes over the whole beat and
|
||||||
release ≈ 0.3 s). Fix in two parts: (a) midi2taud.py parses gen 57, and per drum
|
buries the other hits. FluidSynth kills same-class voices on note-on
|
||||||
channel chokes each note at the next same-class onset — emitting the new fast-fade
|
(fluid_synth_kill_by_exclusive_class → fluid_voice_kill_excl, a fast −2000-timecent
|
||||||
note 0x0004 (apply_exclusive_class + emit_cells pass 2b; allocate_voices holds the
|
release ≈ 0.3 s). Fix in two parts: (a) midi2taud.py parses gen 57, and per drum
|
||||||
choked voice foreground until the choke); (b) the engine implements note 0x0004
|
channel chokes each note at the next same-class onset — emitting the new fast-fade
|
||||||
(startFastFade): a ~0.3 s note-fade while the sample keeps playing, distinct from
|
note 0x0004 (apply_exclusive_class + emit_cells pass 2b; allocate_voices holds the
|
||||||
^^CUT's hard stop. Headless coverage: devtests/ixmp/FastFadeTest. Verified on
|
choked voice foreground until the choke); (b) the engine implements note 0x0004
|
||||||
E2M1 (open hat no longer rings >0.3 s; crashes, not in a class, still ring out).
|
(startFastFade): a ~0.3 s note-fade while the sample keeps playing, distinct from
|
||||||
[ ] midi2taud: toggleable option for disabling filter for percussions [default: on]
|
^^CUT's hard stop. Headless coverage: devtests/ixmp/FastFadeTest. Verified on
|
||||||
- Anything on bank 127 and 128 (usually asso siated with ch 10)
|
E2M1 (open hat no longer rings >0.3 s; crashes, not in a class, still ring out).
|
||||||
- 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
|
release. fadeStep comes out ~4× larger (faster fade). I kept the IT/FT2 engine path untouched
|
||||||
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
|
||||||
— 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.
|
||||||
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),
|
||||||
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
|
||||||
nudging it toward 0.30–0.35 lengthens the tail without returning to the old over-long
|
behaviour.
|
||||||
behaviour.
|
[x] auto-set optimal-ish Tickspeed and RPB using MIDI Time Signature events and note analysis. Break pattern when Time Signature changes.
|
||||||
[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
|
||||||
* 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
|
||||||
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
|
||||||
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
|
||||||
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,
|
||||||
(_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],
|
||||||
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
|
||||||
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 >
|
||||||
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;
|
||||||
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 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 /
|
||||||
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
|
||||||
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
|
||||||
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,
|
||||||
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 &
|
||||||
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
|
||||||
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
|
||||||
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.
|
||||||
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
|
||||||
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
|
||||||
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 =
|
||||||
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
|
||||||
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,
|
||||||
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
|
||||||
= 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 =
|
||||||
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;
|
||||||
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
|
||||||
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
|
||||||
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.
|
||||||
(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
|
||||||
taut.js side: loadTaudSongList now reads beat_pri/beat_sec from sMet (were skipped) into
|
songsMeta.songs[i]; applySongBeatDiv() + applySongPitchPreset() apply per-song beat
|
||||||
songsMeta.songs[i]; applySongBeatDiv() + applySongPitchPreset() apply per-song beat
|
divisions AND notation/pitch-preset on BOTH initial open and switchSong (initial open
|
||||||
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
|
||||||
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.
|
||||||
closed too). Only midi2taud emits sMet; the other 2taud converters omit it → 12-TET default.
|
[x] Taut UI commit
|
||||||
[x] Taut UI commit
|
- Inst > Gen.1 > sample binding: ~~~....[two doubledots] et al. (n extra samples)
|
||||||
- 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)
|
||||||
- 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 playblobs: only active for actually playing samples
|
- Samples playcursor: true cursors for actually playing samples
|
||||||
- Samples playcursor: true cursors for actually playing samples
|
[x] implement note-fade (0x0003) and wire it to it2taud
|
||||||
[x] implement note-fade (0x0003) and wire it to it2taud
|
* DONE 2026-06-14. Engine: note word 0x0003 sets voice.noteFading (IT CHN_NOTEFADE,
|
||||||
* 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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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)
|
||||||
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.
|
||||||
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:
|
||||||
[x] taut.js quit with TypeError: Cannot read property 'terminatorIdx' of undefined:
|
TypeError: Cannot read property 'terminatorIdx' of undefined
|
||||||
TypeError: Cannot read property 'terminatorIdx' of undefined
|
at drawEnvelopeCursor (<eval>:5312:22)
|
||||||
at drawEnvelopeCursor (<eval>:5312:22)
|
at drawInstrumentsContents (<eval>:4919:41)
|
||||||
at drawInstrumentsContents (<eval>:4919:41)
|
at Object.onClick (<eval>:4984:17)
|
||||||
at Object.onClick (<eval>:4984:17)
|
at dispatchMouseEvent (<eval>:6275:53)
|
||||||
at dispatchMouseEvent (<eval>:6275:53)
|
at <eval>:6633:13
|
||||||
at <eval>:6633:13
|
at Object.withEvent (<eval>:1168:27)
|
||||||
at Object.withEvent (<eval>:1168:27)
|
at _tvdosExec$microtone$tvdos$bin$taut$js (<eval>:6632:11)
|
||||||
at _tvdosExec$microtone$tvdos$bin$taut$js (<eval>:6632:11)
|
at execApp (<eval>:1457:16)
|
||||||
at execApp (<eval>:1457:16)
|
at Object.execute (<eval>:893:38)
|
||||||
at Object.execute (<eval>:893:38)
|
at <eval>:867:31
|
||||||
at <eval>:867:31
|
[x] Timbres of Heaven's Overdriven Gt decays EXTREMELY SLOWLY even after the fix
|
||||||
[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.
|
||||||
* 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
|
||||||
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
|
||||||
(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 —
|
||||||
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
|
||||||
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
|
||||||
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.
|
||||||
ToH-specific because ToH leans on multi-layer presets with short releases.
|
Fix (AudioAdapter.kt, inactive-parent branch): inherit parent.keyOff/noteFading before
|
||||||
Fix (AudioAdapter.kt, inactive-parent branch): inherit parent.keyOff/noteFading before
|
detaching, mirroring the existing active-branch. parent.keyOff survives deactivation and is
|
||||||
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
|
||||||
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.
|
||||||
(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)
|
||||||
[ ] 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:
|
TODO - list of demo songs that MUST ship with Microtone:
|
||||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
||||||
|
|||||||
Reference in New Issue
Block a user