taut: better channel sim; s3m converter S8x->PanEff

This commit is contained in:
minjaesong
2026-04-27 02:25:23 +09:00
parent b44d9c6b68
commit 76011d4fa9
3 changed files with 110 additions and 15 deletions

View File

@@ -618,7 +618,9 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
**Plain.** Sets the channel pan to `$xx`, with $00 being full left and $FF being full right. $80 is centre.
**Compatibility.** ST3 `S8x` uses a 4-bit value; convert by nibble-repeat: ST3 `S83` → Taud `S $8033`. Panning column command `0.$xx` has the same semantics and is the preferred form when a pan column is available in the pattern. ProTracker `8xx` (fine pan) and `E8x` (coarse pan) both map into Taud's 8-bit pan — the ProTracker 8-bit form maps directly; the 4-bit form nibble-repeats.
**Compatibility.** ST3 `S8x` uses a 4-bit value.
1. convert by nibble-repeat: ST3 `S83` → Taud `S $8033`. Panning column command `0.$xx` has the same semantics and is the preferred form when a pan column is available in the pattern. ProTracker `8xx` (fine pan) and `E8x` (coarse pan) both map into Taud's 8-bit pan — the ProTracker 8-bit form maps directly; the 4-bit form nibble-repeats.
2. convert to PanEff: ST3 `S8x` → PanEff `0.yy`, where `yy = round(4.2 * x)`
**Implementation.** Write `channel_pan = arg & $FF`. The pan value is applied at the mixer: `left_gain = (($FF pan) × $100) >> 8`, `right_gain = (pan × $100) >> 8`, with both applied before the global volume stage.

View File

@@ -296,7 +296,7 @@ function buildRowCell(ptnDat, row) {
let sVolEff = volEffSym[voleff >>> 6]
let sVolArg = voleffarg.hexD2()
if (voleff === 0) {
if (voleff === 0xC0) {
sVolEff = ''
sVolArg = sym.middot.repeat(2)
}
@@ -320,7 +320,7 @@ function buildRowCell(ptnDat, row) {
let sPanEff = panEffSym[paneff >>> 6]
let sPanArg = paneffarg.hexD2()
if (paneff === 0) {
if (paneff === 0xC0) {
sPanEff = ''
sPanArg = sym.middot.repeat(2)
}
@@ -953,6 +953,12 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
lines.push({ label: 'L.Inst', value: cumState.lastInst === 0 ? '--' : cumState.lastInst.hex02(), fg: colInst })
lines.push({ label: 'Vol ', value: `$${cumState.volAbs.hex02()}`, fg: colVol })
lines.push({ label: 'Pan ', value: `$${cumState.panAbs.hex02()}`, fg: colPan })
const _apo = Math.abs(cumState.pitchOff)
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : '='
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0)
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' '
: ''
lines.push({ label: 'Pitch ', value: `${_absN}(${_psgn}$${_apo.hex04()})`, fg: colNote })
lines.push({ label: `E${MIDDOT}F `, value: `$${cumState.memEF.hex04()}`, fg: colEffArg })
lines.push({ label: 'G ', value: `$${cumState.memG.hex04()}`, fg: colEffArg })
lines.push({ label: `H${MIDDOT}U `, value: `$${cumState.memHU.speed.hex02()}/$${cumState.memHU.depth.hex02()}`, fg: colEffArg })
@@ -1424,18 +1430,23 @@ function getActiveRowForDetail() {
// Walk pattern rows 0..uptoRow and accumulate effect-memory cohort state
function simulateRowState(ptnDat, uptoRow) {
const OP_A = 10
const OP_D = 13, OP_E = 14, OP_F = 15, OP_G = 16
const OP_H = 17, OP_I = 18, OP_J = 19, OP_O = 24
const OP_Q = 26, OP_R = 27, OP_T = 29, OP_U = 30, OP_Y = 34
let lastNote = 0xFFFF, lastInst = 0
let volAbs = 0x3F, panAbs = 0x20
let pitchOff = 0, portaTarget = -1
let speed = 6
let memEF = 0, memG = 0
let memHU = { speed: 0, depth: 0 }
let memR = { speed: 0, depth: 0 }
let memY = { speed: 0, depth: 0 }
let memD = 0, memI = 0, memJ = 0, memO = 0, memQ = 0, memTSlide = 0
const clampV = v => Math.max(0, Math.min(0x3F, v | 0))
const limit = Math.min(uptoRow, ROWS_PER_PAT - 1)
for (let row = 0; row <= limit; row++) {
const off = 8 * row
@@ -1446,17 +1457,99 @@ function simulateRowState(ptnDat, uptoRow) {
const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8)
if (note !== 0xFFFF && note !== 0xFFFE) lastNote = note
// Notes on a portamento row (G) become the slide target; they don't retrigger
const isGRow = (effop === OP_G)
if (note !== 0xFFFF && note !== 0xFFFE) {
if (!isGRow) {
lastNote = note
pitchOff = 0
portaTarget = -1
} else {
portaTarget = note
}
}
if (inst !== 0) lastInst = inst
const volop = (voleff >>> 6) & 3
if (voleff !== 0 && volop === 0) volAbs = voleff & 63
const panop = (paneff >>> 6) & 3
if (paneff !== 0 && panop === 0) panAbs = paneff & 63
// Volume column: set OR slide
const volop = (voleff >>> 6) & 3
const volefarg = voleff & 63
if (voleff !== 0) {
if (volop === 0) {
volAbs = volefarg
} else if (volop === 1) {
volAbs = clampV(volAbs + (volefarg & 15) * (speed - 1))
} else if (volop === 2) {
volAbs = clampV(volAbs - (volefarg & 15) * (speed - 1))
} else if (volop === 3 && volefarg !== 0) {
if (volefarg >= 32) volAbs = clampV(volAbs + (volefarg & 15)) // fine slide up
else volAbs = clampV(volAbs - (volefarg & 15)) // fine slide down
}
}
// Pan column: set OR slide
const panop = (paneff >>> 6) & 3
const panefarg = paneff & 63
if (paneff !== 0) {
if (panop === 0) {
panAbs = panefarg
} else if (panop === 1) {
panAbs = clampV(panAbs + (panefarg & 15) * (speed - 1))
} else if (panop === 2) {
panAbs = clampV(panAbs - (panefarg & 15) * (speed - 1))
} else if (panop === 3 && panefarg !== 0) {
if (panefarg >= 32) panAbs = clampV(panAbs + (panefarg & 15))
else panAbs = clampV(panAbs - (panefarg & 15))
}
}
if (effop !== 0 || effarg !== 0) {
if (effop === OP_E || effop === OP_F) { if (effarg !== 0) memEF = effarg }
else if (effop === OP_G) { if (effarg !== 0) memG = effarg }
if (effop === OP_A) {
if ((effarg >>> 8) !== 0) speed = (effarg >>> 8)
}
else if (effop === OP_D) {
const raw = (effarg !== 0) ? (memD = effarg) : memD
if (raw !== 0) {
const hb = (raw >>> 8) & 0xFF
const hiNib = (hb >>> 4) & 0xF
const loNib = hb & 0xF
if (hiNib === 0xF) {
// $Fy00 fine slide down, but $F000/$FF00 → fine slide up by $F
if (hb === 0xFF || loNib === 0) volAbs = clampV(volAbs + 0xF)
else volAbs = clampV(volAbs - loNib)
} else if (loNib === 0xF) {
volAbs = clampV(volAbs + hiNib) // $xF00 fine slide up
} else if (hiNib === 0 && loNib !== 0) {
volAbs = clampV(volAbs - loNib * (speed - 1)) // $0y00 coarse down
} else if (hiNib !== 0 && loNib === 0) {
volAbs = clampV(volAbs + hiNib * (speed - 1)) // $x000 coarse up
}
}
}
else if (effop === OP_E || effop === OP_F) {
const raw = (effarg !== 0) ? (memEF = effarg) : memEF
if (raw !== 0) {
const fine = (raw & 0xF000) === 0xF000
const amt = fine ? (raw & 0x0FFF) : raw * (speed - 1)
if (effop === OP_E) pitchOff -= amt
else pitchOff += amt
}
}
else if (effop === OP_G) {
if (effarg !== 0) memG = effarg
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) {
const curPitch = lastNote + pitchOff
const diff = portaTarget - curPitch
if (diff !== 0) {
const absDiff = Math.abs(diff)
const maxStep = memG * (speed - 1)
pitchOff += Math.sign(diff) * Math.min(absDiff, maxStep)
if (absDiff <= maxStep) {
pitchOff = portaTarget - lastNote
portaTarget = -1
}
}
}
}
else if (effop === OP_H || effop === OP_U) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memHU.speed = spd; if (dep !== 0) memHU.depth = dep
@@ -1469,7 +1562,6 @@ function simulateRowState(ptnDat, uptoRow) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memY.speed = spd; if (dep !== 0) memY.depth = dep
}
else if (effop === OP_D) { if (effarg !== 0) memD = effarg }
else if (effop === OP_I) { if (effarg !== 0) memI = effarg }
else if (effop === OP_J) { if (effarg !== 0) memJ = effarg }
else if (effop === OP_O) { if (effarg !== 0) memO = effarg }
@@ -1478,7 +1570,7 @@ function simulateRowState(ptnDat, uptoRow) {
}
}
return { lastNote, lastInst, volAbs, panAbs,
return { lastNote, lastInst, volAbs, panAbs, pitchOff,
memEF, memG, memHU, memR, memY,
memD, memI, memJ, memO, memQ, memTSlide }
}

View File

@@ -19,7 +19,8 @@ Effect support:
Cxx is BCD-decoded. K/L are split into H $0000 / G $0000 + volume-column
slide. M/N/X/P fold into volume / pan columns. W (global vol slide) is
dropped with a -v warning. X converts to pan column. Y (panbrello) converts
to Taud Y. S5 selects the panbrello LFO waveform.
to Taud Y. S5 selects the panbrello LFO waveform. S8x converts to a pan
column SET of round(x * 4.2), mapping nibble 0-15 directly to pan 0-63.
"""
import argparse
@@ -446,8 +447,8 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple:
# Panbrello LFO waveform — maps directly to Taud S$5x00.
return (TOP_S, 0x5000 | (val << 8), None, None)
if sub == 0x8:
# Coarse pan: nibble-repeat into Taud's S $80xx full-8-bit pan.
return (TOP_S, 0x8000 | (val * 0x11), None, None)
# S8x → PanEff 0.yy where yy = round(x * 4.2), mapping nibble 0-15 to pan 0-63.
return (TOP_NONE, 0, None, (SEL_SET, round(val * 4.2)))
# S0/S6/S7/S9/SA: filter, NNA, sound-control, stereo — drop silently.
return (TOP_NONE, 0, None, None)