mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
more correct pitch slide conversion rule
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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') {
|
if (fx == 'S') fx += (effarg >>> 12).hex1()
|
||||||
print(`\u00F8`+' '.repeat(32))
|
let fxName = fxNames[fx]
|
||||||
}
|
print(`\u00F8 ${fxName}\t$${effarg.hex04()} `)
|
||||||
else {
|
|
||||||
if (fx == 'S') fx += (effarg >>> 12).hex1()
|
|
||||||
let fxName = fxNames[fx]
|
|
||||||
print(`\u00F8 ${fxName}\t$${effarg.hex04()} `)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAll() {
|
function drawAll() {
|
||||||
|
|||||||
23
s3m2taud.py
23
s3m2taud.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user