From 76011d4fa9ba0dd222b6d703fe5f5ea61a8a121b Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 27 Apr 2026 02:25:23 +0900 Subject: [PATCH] taut: better channel sim; s3m converter S8x->PanEff --- TAUD_NOTE_EFFECTS.md | 4 +- assets/disk0/tvdos/bin/taut.js | 114 +++++++++++++++++++++++++++++---- s3m2taud.py | 7 +- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index 8e183eb..f9a542f 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -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. diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 0f712ce..350cf08 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -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 } } diff --git a/s3m2taud.py b/s3m2taud.py index 6f77126..84b34e0 100644 --- a/s3m2taud.py +++ b/s3m2taud.py @@ -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)