more correct pitch slide conversion rule

This commit is contained in:
minjaesong
2026-04-24 20:17:20 +09:00
parent a1b62f3155
commit d4ea9b2d29
3 changed files with 82 additions and 68 deletions

View File

@@ -159,11 +159,11 @@ Coarse and fine modes are distinguished by the high nibble of the argument:
- `E $F000..$FFFF` — fine slide: on tick 0 only, subtracts `arg & $0FFF` from pitch. - `E $F000..$FFFF` — fine slide: on tick 0 only, subtracts `arg & $0FFF` from pitch.
- `E $0000` — recalls the last E-or-F argument and applies it as a down-slide, preserving the original form (coarse or fine). - `E $0000` — recalls the last E-or-F argument and applies it as a down-slide, preserving the original form (coarse or fine).
**Compatibility.** This is **the single intentionally ST3-incompatible command in Taud**. ST3 pitch slides operate on Amiga periods or linear slide units; Taud operates directly on 4096-TET pitch units. Conversion from ST3 linear-mode slides uses 1 ST3 slide unit ≈ $0005 Taud units (1/64 semitone): **Compatibility.** This is **the single intentionally ST3-incompatible command in Taud**. ST3 pitch slides operate on Amiga periods or linear slide units; Taud operates directly on 4096-TET pitch units. Coarse and fine forms use different unit sizes:
- ST3 `Exx` coarse (where `xx < $E0`) → Taud `E $00xx × $0015` (one ST3 coarse unit = 1/16 semitone ≈ $0015 Taud units). - ST3 `Exx` coarse (where `xx < $E0`) → Taud `E round($00xx × 64/3)` (1 ST3 coarse unit = 1/16 semitone = 64/3 ≈ 21.33 Taud units, rounded).
- ST3 `EFx` fine → Taud `E $F0xx × $0015` with appropriate range packing. - ST3 `EFx` fine → Taud `E $F0 round(x × 16/3)` (1 ST3 fine unit = 1/64 semitone = 16/3 ≈ 5.33 Taud units, applied once per row).
- ST3 `EEx` extra-fine → Taud `E $F0xx × $0005` (one ST3 extra-fine unit = 1/64 semitone ≈ $0005 Taud units). - ST3 `EEx` extra-fine → Taud `E $F0 round(x × 16/3)` (same unit as fine, applied once per row).
ST3 Amiga-mode slides do not have a clean conversion and should be treated as linear-mode equivalents during import. ST3 Amiga-mode slides do not have a clean conversion and should be treated as linear-mode equivalents during import.
@@ -196,7 +196,7 @@ Glissando control (S $1x) snaps the output pitch to the nearest semitone after e
**Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, and memory behaviour are identical in form but inverted in direction. **Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, and memory behaviour are identical in form but inverted in direction.
**Compatibility.** Same as E. ST3 `Fxx` coarse, `FFx` fine, and `FEx` extra-fine convert with the same scaling factors ($0015 and $0005). F and E share one memory slot in Taud. **Compatibility.** Same as E. ST3 `Fxx` coarse converts using `round(x × 64/3)`; `FFx` fine and `FEx` extra-fine convert using `round(x × 16/3)`. F and E share one memory slot in Taud.
**Implementation.** As for E, but add instead of subtract. No upper pitch cap is defined by the effect itself, but the sample-rate conversion at the mixer will saturate well before arithmetic overflow at reasonable playing ranges. **Implementation.** As for E, but add instead of subtract. No upper pitch cap is defined by the effect itself, but the sample-rate conversion at the mixer will saturate well before arithmetic overflow at reasonable playing ranges.
@@ -206,7 +206,7 @@ Glissando control (S $1x) snaps the output pitch to the nearest semitone after e
**Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx Taud units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch. **Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx Taud units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch.
**Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same $0015-per-unit scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F. **Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same `round(× 64/3)` scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F.
**Implementation.** **Implementation.**
@@ -517,7 +517,7 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
--- ---
## X $xx00 — Set Panning ## X $xx00 — Fine Set Panning
**Plain.** **Unimplemented**. On IT, sets the panning position of the current channel, $00 being full-left and $FF being full-right. **Plain.** **Unimplemented**. On IT, sets the panning position of the current channel, $00 being full-left and $FF being full-right.
@@ -756,7 +756,7 @@ This table maps each PT effect to its Taud equivalent. Arguments follow PT's two
| PT effect | Taud effect | Notes | | PT effect | Taud effect | Notes |
|---|---|---| |---|---|---|
| `0 $xy` | `J $xxyy` | Arpeggio; nibble-repeat each byte. See the 12-TET → Taud table above for conversion losses | | `0 $xy` | `J $xxyy` | Arpeggio; nibble-repeat each byte. See the 12-TET → Taud table above for conversion losses |
| `1 $xx` | `F $0xxx × $0015` | Portamento up; ST3 slide unit = 1/16 semitone | | `1 $xx` | `F round($0xxx × 64/3)` | Portamento up; ST3 coarse slide unit = 1/16 semitone |
| `2 $xx` | `E $0xxx × $0015` | Portamento down | | `2 $xx` | `E $0xxx × $0015` | Portamento down |
| `5 $xy` | `L $xy00` | Combined portamento + volume slide | | `5 $xy` | `L $xy00` | Combined portamento + volume slide |
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide | | `6 $xy` | `K $xy00` | Combined vibrato + volume slide |
@@ -803,7 +803,7 @@ These quirks of ST3 are worth preserving or flagging when importing S3M files in
**Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export. **Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export.
**Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly via the $0015-per-unit coarse and $0005-per-unit extra-fine constants; Amiga-mode slides change character slightly because the non-linearity of period math is not replicated. **Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly: coarse forms (Exx/Fxx/Gxx) use `round(× 64/3)` (1/16 semitone per unit), fine/extra-fine forms (EFx/EEx/FFx/FEx) use `round(× 16/3)` (1/64 semitone per unit). Amiga-mode slides change character slightly because the non-linearity of period math is not replicated.
**Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export. **Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export.

View File

@@ -70,52 +70,74 @@ middot:MIDDOT
} }
const fxNames = { const fxNames = {
A:"Tick speed", '0':"No effect ",
'1':"UNIMPLEMENTED",
'2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED",
'5':"UNIMPLEMENTED",
'6':"UNIMPLEMENTED",
'7':"UNIMPLEMENTED",
'8':"UNIMPLEMENTED",
'9':"UNIMPLEMENTED",
A:"Tick speed ",
B:"Jump to order", B:"Jump to order",
C:"Break pattern", C:"Break pattern",
D:"Volume slide", D:"Volume slide ",
E:"Pitch down", E:"Pitch down ",
F:"Pitch up", F:"Pitch up ",
G:"Portamento", G:"Portamento ",
H:"Vibrato", H:"Vibrato ",
U:"Fine vibrato", I:"Tremor ",
I:"Tremor", J:"Arpeggio ",
J:"Arpeggio", K:"UNIMPLEMENTED",
K:"Vibra+v.slide", L:"UNIMPLEMENTED",
L:"Porta+v.slide", M:"UNIMPLEMENTED",
N:"UNIMPLEMENTED",
O:"Sample offset", O:"Sample offset",
Q:"Retrigger", P:"UNIMPLEMENTED",
R:"Tremolo", Q:"Retrigger ",
T:"Tempo", R:"Tremolo ",
V:"Global volume", S:"Special ",
S:"Special", S0:"UNIMPLEMENTED",
S1:"Gliss. ctrl", S1:"Gliss. ctrl ",
S2:"Sample tune", S2:"Sample tune ",
S3:"Vibrato LFO", S3:"Vibrato LFO ",
S4:"Tremolo LFO", S4:"Tremolo LFO ",
S8:"Channel pan", S5:"Panbrello LFO",
SB:"Pattern loop", S6:"UNIMPLEMENTED",
SC:"Note cut", S7:"UNIMPLEMENTED",
SD:"Note delay", S8:"Channel pan ",
S9:"UNIMPLEMENTED",
SA:"UNIMPLEMENTED",
SB:"Pattern loop ",
SC:"Note cut ",
SD:"Note delay ",
SE:"Pattern delay", SE:"Pattern delay",
SF:"Funk it" SF:"Funk it ",
T:"Tempo ",
U:"Fine vibrato ",
V:"Global volume",
W:"UNIMPLEMENTED",
X:"UNIMPLEMENTED",
Y:"Panbrello ",
Z:"UNIMPLEMENTED",
} }
const panFxNames = { const panFxNames = {
0:"Set", 0:"Panning Set ",
1:"Pan slide L", 1:"Pan slide L ",
2:"Pan slide R", 2:"Pan slide R ",
3:"Fine pan slide", 3:"Fpan slide ",
30:"Fine pan slide L", 30:"Fpan slide L ",
31:"Fine pan slide R" 31:"Fpan slide R "
} }
const volFxNames = { const volFxNames = {
0:"Set", 0:"Volume Set ",
1:"Vol slide UP", 1:"Vol slide UP ",
2:"Vol slide DN", 2:"Vol slide DN ",
3:"Fine vol slide", 3:"fVol slide ",
30:"Fine vol slide DN", 30:"fVol slide DN",
31:"Fine vol slide UP" 31:"fVol slide UP"
} }
const pitchTablePresets = { const pitchTablePresets = {
@@ -636,14 +658,9 @@ function drawVoiceDetail() {
`PanEff ${paneffop}.$${paneffarg.hex02()}`) `PanEff ${paneffop}.$${paneffarg.hex02()}`)
con.move(7,1) con.move(7,1)
let fx = effop.toString(36).toUpperCase() let fx = effop.toString(36).toUpperCase()
if (fx == '0') {
print(`\u00F8`+' '.repeat(32))
}
else {
if (fx == 'S') fx += (effarg >>> 12).hex1() if (fx == 'S') fx += (effarg >>> 12).hex1()
let fxName = fxNames[fx] let fxName = fxNames[fx]
print(`\u00F8 ${fxName}\t$${effarg.hex04()} `) print(`\u00F8 ${fxName}\t$${effarg.hex04()} `)
}
} }
function drawAll() { function drawAll() {

View File

@@ -390,18 +390,16 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple:
return (TOP_D, (arg & 0xFF) << 8, None, None) return (TOP_D, (arg & 0xFF) << 8, None, None)
if cmd in (EFF_E, EFF_F): if cmd in (EFF_E, EFF_F):
# ST3 slide unit = 1/16 semitone = $0015 Taud units (per spec PT table). # Coarse: 1/16 semitone = 64/3 Taud units. Fine/extra-fine: 1/64 semitone = 16/3.
op = TOP_E if cmd == EFF_E else TOP_F op = TOP_E if cmd == EFF_E else TOP_F
hi = (arg >> 4) & 0xF hi = (arg >> 4) & 0xF
lo = arg & 0xF lo = arg & 0xF
if hi == 0xF and lo > 0: if hi in (0xE, 0xF) and lo > 0:
return (op, 0xF000 | ((lo * 0x15) & 0xFFF), None, None) return (op, 0xF000 | (round(lo * 16 / 3) & 0xFFF), None, None)
if hi == 0xE and lo > 0: return (op, round(arg * 64 / 3) & 0xFFFF, None, None)
return (op, 0xF000 | ((lo * 0x05) & 0xFFF), None, None)
return (op, (arg * 0x15) & 0xFFFF, None, None)
if cmd == EFF_G: if cmd == EFF_G:
return (TOP_G, (arg * 0x15) & 0xFFFF, None, None) return (TOP_G, round(arg * 64 / 3) & 0xFFFF, None, None)
if cmd in (EFF_H, EFF_I, EFF_R, EFF_U): if cmd in (EFF_H, EFF_I, EFF_R, EFF_U):
op = {EFF_H: TOP_H, EFF_I: TOP_I, EFF_R: TOP_R, EFF_U: TOP_U}[cmd] op = {EFF_H: TOP_H, EFF_I: TOP_I, EFF_R: TOP_R, EFF_U: TOP_U}[cmd]
@@ -901,16 +899,15 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
# Song table row (16 bytes): offset(4)+voices(1)+patsLo(1)+patsHi(1)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+pad(1) # Song table row (16 bytes): offset(4)+voices(1)+patsLo(1)+patsHi(1)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+pad(1)
# Built after dedup so num_taud_pats reflects the unique count. # Built after dedup so num_taud_pats reflects the unique count.
num_taud_pats_lo = num_taud_pats & 0xFF song_table = struct.pack('<IBHBBHf',
num_taud_pats_hi = (num_taud_pats >> 8) & 0xFF
song_table = struct.pack('<IBBBBB',
song_offset, song_offset,
C, C,
num_taud_pats_lo, num_taud_pats,
num_taud_pats_hi,
bpm_stored, bpm_stored,
speed, speed,
) + b'\x00\x90' + b'\x00\xAC\xD02\x46' + b'\x00' 0x9000, # C8
8363.0, # Hz
) + b'\x00'
assert len(song_table) == TAUD_SONG_ENTRY assert len(song_table) == TAUD_SONG_ENTRY
# Cue sheet (using remapped pattern indices) # Cue sheet (using remapped pattern indices)