Compare commits

...

5 Commits

Author SHA1 Message Date
minjaesong
76011d4fa9 taut: better channel sim; s3m converter S8x->PanEff 2026-04-27 02:25:23 +09:00
minjaesong
b44d9c6b68 taut: fancier UI 2026-04-26 23:36:29 +09:00
minjaesong
e47e9e1259 taut: transport control 2026-04-26 22:36:39 +09:00
minjaesong
c5789ec28b taud: panning law toggle 2026-04-26 20:08:02 +09:00
minjaesong
93f7f436a3 taud note eff more PT conv table 2026-04-26 19:24:01 +09:00
7 changed files with 386 additions and 136 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.
@@ -749,37 +751,65 @@ NOTE: **`3.00` — is No-op**
---
# Effects That Modifies Global Behaviour
Effects in this section modifies the behaviour of the mixer. Primary intention of the commands is to provide switches for legacy tracker and modern DAW behaviours.
## 1 $01xx — Set stereo panning law
**Plain.** Sets how the mixer should treat the panning. Available modes are:
- 0: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
- 1: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
**Implementation.**
- Mode 0:
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
- R_gain = if (pan < 0x80) pan / 128.0 else 1.0
- Mode 1:
- L_gain = cos(pi*x / 512.0)
- R_gain = sin(pi*x / 512.0)
---
# ProTracker to Taud conversion table
This table maps each PT effect to its Taud equivalent. Arguments follow PT's two-nibble form and expand to Taud's 16-bit form as shown.
| 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 |
| `1 $xx` | `F round($0xxx × 64/3)` | Portamento up; ST3 coarse slide unit = 1/16 semitone |
| `2 $xx` | `E round($0xxx × 64/3)` | Portamento down |
| `5 $xy` | `L $xy00` | Combined portamento + volume slide (see compatibility note) |
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide (see compatibility note) |
| `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat |
| `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan |
| `9 $xx` | `O $xx00` | Sample offset |
| `A $xy` | Volume column `1.$xy` | Volume slide |
| `B $xx` | `B $00xx` | Position jump |
| `C $xx` | Volume column `0.$xx` | Set volume |
| `D $xx` | `C $00xx` (after BCD decode) | Pattern break |
| `E $3x` | `S $1x00` | Glissando control |
| `E $4x` | `S $3x00` | Vibrato waveform |
| `E $5x` | `S $2x00` | Set fine-tune |
| `E $6x` | `S $Bx00` | Pattern loop |
| `E $7x` | `S $4x00` | Tremolo waveform |
| `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) |
| `E $9x` | `Q $0x00` | Retrigger |
| `E $Cx` | `S $Cx00` | Note cut |
| `E $Dx` | `S $Dx00` | Note delay |
| `E $Ex` | `S $Ex00` | Pattern delay |
| `E $Fx` | `S $Fx00` | Funk repeat |
| `F $xx` (xx < $20) | `A $xx00` | Set speed |
| `F $xx` (xx ≥ $20) | `T $(xx$18)00` | Set tempo |
| `1 $xx` | `F round($0xxx × 64/3)` | Portamento up; ST3 coarse slide unit = 1/16 semitone |
| `2 $xx` | `E round($0xxx × 64/3)` | Portamento down |
| `3 $xx` | `G round($0xxx × 64/3)` | Portamento to note |
| `4 $xy` | `H $xxyy` | Vibrato; nibble-repeat each byte. |
| `5 $xy` | `L $xy00` | Combined portamento + volume slide (see compatibility note) |
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide (see compatibility note) |
| `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat |
| `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan |
| `9 $xx` | `O $xx00` | Sample offset |
| `A $xy` | Volume column `1.$xy` | Volume slide |
| `B $xx` | `B $00xx` | Position jump |
| `C $xx` | Volume column `0.$xx` | Set volume |
| `D $xx` | `C $00xx` (after BCD decode) | Pattern break |
| `E $0x` | `S $000x` | (UNIMPLEMENTED) Set filter |
| `E $1x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide up |
| `E $2x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide down |
| `E $3x` | `S $1x00` | Glissando control |
| `E $4x` | `S $3x00` | Vibrato waveform |
| `E $5x` | `S $2x00` | Set fine-tune |
| `E $6x` | `S $Bx00` | Pattern loop |
| `E $7x` | `S $4x00` | Tremolo waveform |
| `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) |
| `E $9x` | `Q $0x00` | Retrigger |
| `E $Ax` | Volume column `3.$1x` | Fine volume slide up |
| `E $Bx` | Volume column `3.$0x` | Fine volume slide down |
| `E $Cx` | `S $Cx00` | Note cut |
| `E $Dx` | `S $Dx00` | Note delay |
| `E $Ex` | `S $Ex00` | Pattern delay |
| `E $Fx` | `S $Fx00` | Funk repeat |
| `F $xx` (xx < $20) | `A $xx00` | Set speed |
| `F $xx` (xx ≥ $20) | `T $(xx$18)00` | Set tempo |
---

View File

@@ -64,18 +64,30 @@ panri:"\u008416u",
panfinele:"\u008427u",
panfineri:"\u008426u",
/* Fx/Vx/Px */
fx:'\u00F8',
px:'\u00AC',
vx:'\u00AD',
/* transport control */
playall:'\u00A8',
playcue:'\u00A9',
playrow:'\u00AA',
stop:'\u00AB',
/* miscellaneous */
unticked:"\u009E",
ticked:"\u009F",
unticked:"\u00AE",
ticked:"\u00AF",
middot:MIDDOT,
doubledot:"\u008419u",
stop:"\u008420u\u008421u",
play:"\u008422u\u008423u",
statusstop:"\u008420u\u008421u",
statusplay:"\u008422u\u008423u",
playhead:"\u00A7",
}
const fxNames = {
'0':"No effect ",
'1':"UNIMPLEMENTED",
'0':"-- ",
'1':"Mixer config ", // Taud: 1 01xx: set stereo panning law
'2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED",
@@ -94,26 +106,26 @@ G:"Portamento ",
H:"Vibrato ",
I:"Tremor ",
J:"Arpeggio ",
K:"UNIMPLEMENTED",
L:"UNIMPLEMENTED",
M:"UNIMPLEMENTED",
N:"UNIMPLEMENTED",
K:"UNIMPLEMENTED", // Volume slide+Vibrato. Use H0000 and VolEff instead
L:"UNIMPLEMENTED", // Volume slide+Portamento. Use G0000 and VolEff instead
M:"UNIMPLEMENTED", // IT: Set channel volume. Use VolEff instead
N:"UNIMPLEMENTED", // IT: Channel volume slide. Use VolEff instead
O:"Sample offset",
P:"UNIMPLEMENTED",
P:"UNIMPLEMENTED", // IT: panning slide. Use PanEff instead
Q:"Retrigger ",
R:"Tremolo ",
S:"Special ",
S0:"UNIMPLEMENTED",
S0:"UNIMPLEMENTED", // PT: Set audio filter.
S1:"Gliss. ctrl ",
S2:"Sample tune ",
S3:"Vibrato LFO ",
S4:"Tremolo LFO ",
S5:"Panbrello LFO",
S6:"UNIMPLEMENTED",
S7:"UNIMPLEMENTED",
S8:"Channel pan ",
S9:"UNIMPLEMENTED",
SA:"UNIMPLEMENTED",
S6:"UNIMPLEMENTED", // IT: Fine pattern delay.
S7:"UNIMPLEMENTED", // IT: misc. functions
S8:"Channel pan ", // Taud: 8-bit channel panning.
S9:"UNIMPLEMENTED", // IT: Sound control.
SA:"UNIMPLEMENTED", // SC3: Stereo control. IT: Sample offset high twobyte.
SB:"Pattern loop ",
SC:"Note cut ",
SD:"Note delay ",
@@ -122,10 +134,10 @@ SF:"Funk it ",
T:"Tempo ",
U:"Fine vibrato ",
V:"Global volume",
W:"UNIMPLEMENTED",
X:"UNIMPLEMENTED",
W:"UNIMPLEMENTED", // IT: Global volume slide.
X:"UNIMPLEMENTED", // IT: 8-bit channel panning. Use PanEff or S80xx instead
Y:"Panbrello ",
Z:"UNIMPLEMENTED",
Z:"UNIMPLEMENTED", // IT: MIDI macro.
}
const panFxNames = {
0:"Set to",
@@ -194,7 +206,7 @@ sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u0
const volEffSym = [sym.volset, sym.volup, sym.voldn, sym.volfineup, sym.volfinedn]
const panEffSym = [sym.panset, sym.panle, sym.panri, sym.panfinele, sym.panfineri]
const colNote = 254
const colNote = 239
const colInst = 114
const colVol = 155
const colPan = 219
@@ -202,7 +214,33 @@ const colEffOp = 220
const colEffArg = 231
const colBackPtn = 255
const PITCH_PRESET_IDX = 240 // TODO read from the Project Data section of the .taud
let PITCH_PRESET_IDX = 240 // TODO read from the Project Data section of the .taud
// pitchSymLut[pitchInOct] = [symString, octaveOffset]
// octaveOffset is 1 when pitchInOct is closer to the next octave's root (wraps up) than to any table entry.
// Call rebuildPitchLut() whenever PITCH_PRESET_IDX changes.
const pitchSymLut = new Array(0x1000)
function rebuildPitchLut() {
const preset = pitchTablePresets[PITCH_PRESET_IDX]
if (!preset || preset.table.length === 0) return
const table = preset.table
const syms = preset.sym
for (let p = 0; p < 0x1000; p++) {
let best = 0, bestDist = 0x1000
for (let i = 0; i < table.length; i++) {
const d = Math.abs(p - table[i])
if (d < bestDist) { bestDist = d; best = i }
}
// Distance to the next octave's root (0x1000) vs nearest table entry.
if ((0x1000 - p) < bestDist) {
pitchSymLut[p] = [syms[0], 1]
} else {
pitchSymLut[p] = [syms[best], 0]
}
}
}
rebuildPitchLut()
Number.prototype.hex02 = function() {
return this.toString(16).toUpperCase().padStart(2,'0')
@@ -231,18 +269,9 @@ function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff
const table = pitchTablePresets[PITCH_PRESET_IDX].table
const syms = pitchTablePresets[PITCH_PRESET_IDX].sym
if (table.length === 0) return note.hex04()
const pitchInOct = note & 0xFFF
const octave = (note >> 12) - 1
let best = 0, bestDist = 0x1000
for (let i = 0; i < table.length; i++) {
const d = Math.abs(pitchInOct - table[i])
if (d < bestDist) { bestDist = d; best = i }
}
if ((0x1000 - pitchInOct) < bestDist) return syms[0] + (octave + 1)
return syms[best] + octave
if (pitchTablePresets[PITCH_PRESET_IDX].table.length === 0) return note.hex04()
const [s, o] = pitchSymLut[note & 0xFFF]
return s + ((note >> 12) - 1 + o)
}
/**
@@ -267,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)
}
@@ -291,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)
}
@@ -508,7 +537,7 @@ function loadTaud(filePath, songIndex) {
const [SCRH, SCRW] = con.getmaxyx()
const PTNVIEW_OFFSET_X = 3
const PTNVIEW_OFFSET_Y = 9
const PTNVIEW_OFFSET_Y = 5
const PTNVIEW_HEIGHT = SCRH - PTNVIEW_OFFSET_Y
const TIMELINE_COLSIZES = [15, 7, 5]
@@ -533,6 +562,15 @@ const colRowNumEmph1 = 180
const colStatus = 253
const colVoiceHdr = 230
const colSep = 252
const colPushBtnBack = 143
const colTabBarBack = 187
const colTabBarOrn = 135
const colBrand = 211
// protip: avoid using colour zero
const colWHITE = 239
const colBLACK = 240
let separatorStyle = 0
@@ -543,6 +581,11 @@ const PATEDITOR_CELL_X = 10
const PATEDITOR_SEP2_X = 30
const PATEDITOR_DETAIL_X = 32
const PLAYMODE_NONE = 0
const PLAYMODE_SONG = 1
const PLAYMODE_CUE = 2
const PLAYMODE_ROW = 3
function fillLine(y, c, back) {
con.color_pair(c, back)
for (let x = 1; x <= SCRW; x++) {
@@ -550,37 +593,108 @@ function fillLine(y, c, back) {
}
}
const TAB_GAP = 2
const PANEL_NAMES = ['Timeline', 'Orders', 'Patterns', 'Samples', 'Instruments', 'Project', 'File']
const TAB_GAP = 3
const PANEL_NAMES = ['Timeline', 'Cues', 'Patterns', 'Samples', 'Instrmnt', 'Project', 'File']
function drawAlwaysOnElems() {
drawStatusBar()
drawTabIndicator()
drawTabBar()
}
const transportControlReverse = [PLAYMODE_NONE, PLAYMODE_ROW, PLAYMODE_CUE, PLAYMODE_SONG]
const transportControlSymbol = [sym.stop, sym.playrow, sym.playcue, sym.playall]
const transportControlColour = [160,20,20,20]
const transportControlHint = ["O","I","U","Y"]
function drawStatusBar() {
fillLine(1, colStatus, 255)
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
const vHi = Math.min(voiceOff + VOCSIZE_TIMELINE_FULL, song.numVoices)
const txt = `${song.filePath} Cue ${cueIdx.hex03()}/${maxCue.hex03()} Row ${cursorRow.dec02()} V${(voiceOff+1).dec02()}-${vHi.dec02()}/${song.numVoices.dec02()} BPM ${audio.getBPM(PLAYHEAD)} Spd ${audio.getTickRate(PLAYHEAD)} `
con.move(1, 1)
fillLine(2, colStatus, 255)
const sCueIdx = cueIdx.hex03()
const sCueMax = (song.lastActiveCue < 0 ? 0 : song.lastActiveCue).hex03()
const vMax = song.numVoices.dec02()
const vHi = Math.min(voiceOff + VOCSIZE_TIMELINE_FULL, song.numVoices).dec02()
const vLow = (voiceOff+1).dec02()
const songPath = song.filePath
const sRow = cursorRow.dec02()
const sBPM = ''+audio.getBPM(PLAYHEAD)
const sSpd = ''+audio.getTickRate(PLAYHEAD)
// transport control and its control hints
transportControlReverse.forEach((thisMode, j) => {
let active = (playbackMode == thisMode)
if (active)
con.color_pair(transportControlColour[j], colPushBtnBack)
else
con.color_pair(colStatus, 255)
con.move(1, SCRW - 5*(j+1) + 1)
print(` ${transportControlSymbol[j]} `)
if (active)
con.color_pair(transportControlColour[j], colPushBtnBack)
else
con.color_pair(colVoiceHdr, 255)
con.move(2, SCRW - 5*(j+1) + 1)
print(` ${transportControlHint[j]} `)
})
// current audio device status
// play/stop sym
con.color_pair(colStatus, 255)
print(txt)
con.move(1,1)
print(`${sym.playhead}${PLAYHEAD}`)
con.move(2,1)
print((playbackMode == PLAYMODE_NONE) ? sym.statusstop : sym.statusplay)
// cue row
con.move(1,4)
con.color_pair(colStatus, 255); print(`Cue `)
con.color_pair(colVol, 255); print(`${sCueIdx}`)
con.color_pair(colStatus, 255); print(`/`)
con.color_pair(colVol, 255); print(`${sCueMax}`)
con.color_pair(colStatus, 255); print(` Row `)
con.color_pair(colVoiceHdr, 255); print(`${sRow}`)
// bpm spd
con.move(2,4)
con.color_pair(colStatus, 255); print(`BPM `)
con.color_pair(colPan, 255); print(`${sBPM}`)
con.color_pair(colStatus, 255); print(` Tickspeed `)
con.color_pair(colEffOp, 255); print(`${sSpd}`)
// app title
let s1 = "Microtone"
let s2 = "tracker for tsvm"
con.move(1, (SCRW - (s1.length & 254)) >>> 1)
con.color_pair(colBrand, 255); print('Micro')
con.color_pair(colStatus, 255); print('tone')
con.move(2, (SCRW - (s2.length & 254)) >>> 1)
con.color_pair(colSep, 255); print('tracker for ')
con.color_pair(74, 255); print('tsvm')
}
function drawTabIndicator() {
const XOFF = 2
const YOFF = PTNVIEW_OFFSET_Y - 4
function drawTabBar() {
con.color_pair(colTabBarOrn, colTabBarBack)
con.move(3,1)
print(`\u00FB`.repeat(SCRW))
// TODO make it fancier
const XOFF = 2
const YOFF = 3
con.move(YOFF, XOFF)
for (let i = 0; i < PANEL_NAMES.length; i++) {
if (i > 0) con.curs_right(TAB_GAP);
let panStr = PANEL_NAMES[i]
print((currentPanel === i) ? `[${panStr}]` : ` ${panStr} `)
let tabName = PANEL_NAMES[i]
let col = (currentPanel === i) ? 161 : 240
con.color_pair(col, colTabBarBack); print(` ${tabName} `)
}
con.color_pair(colStatus, 255)
}
/**
@@ -699,40 +813,31 @@ function drawControlHint() {
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
['Y','Song'],
['U','Cue'],
['I','Row'],
['O/Sp','Stop'],
['WER','ViewMode'],
['sep'],
['m','Mute'],
['s','Solo'],
['sep'],
['Tab','Panel'],
//['q','Quit'],
// ['sep'],
// ['q','Quit'],
]
let hintElemOrders = [
[`\u008428u\u008429u`,'Nav'],
[`Ent`,'Go to cue'],
['sep'],
['U','Cue'],
['O/Sp','Stop'],
['sep'],
['Tab','Panel'],
['sep'],
['q','Quit'],
// ['sep'],
// ['q','Quit'],
]
let hintElemPatterns = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Ptn'],
['sep'],
['U','Ptn'],
['I','Row'],
['O/Sp','Stop'],
['sep'],
['Tab','Panel'],
['sep'],
['q','Quit'],
// ['sep'],
// ['q','Quit'],
]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns]
@@ -818,11 +923,10 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
const fxName = fxNames[fx] || '? '
if (!isVerticalLayout) {
con.move(6, 1)
print(`Pitch $${note.hex04()}\tInst $${inst.hex02()}\tVolEff ${voleffop}.$${voleffarg.hex02()}\t` +
`PanEff ${paneffop}.$${paneffarg.hex02()}`)
con.move(7, 1)
print(`\u0084248u ${fxName}\t$${effarg.hex04()} `)
return
con.move(PTNVIEW_OFFSET_Y-2, 1)
print(`Pitch $${note.hex04()} Inst $${inst.hex02()} ${sym.vx} ${voleffop}.$${voleffarg.hex02()} ` +
`${sym.px} ${paneffop}.$${paneffarg.hex02()} ${sym.fx} ${fxName} $${effarg.hex04()}`)
} else {
const dx = PATEDITOR_DETAIL_X
const detailW = SCRW - dx + 1
@@ -835,13 +939,13 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
if (paneff == 0xC0) { paneffop1 = 999; paneffarg1 = '' }
const lines = []
lines.push({ label: 'Note ', value: `${noteToStr(note)} ($${note.hex04()})`, fg: colNote })
lines.push({ label: 'Inst ', value: inst === 0 ? '--' : inst.hex02(), fg: colInst })
lines.push({ label: 'VolEff', value: `${volFxNames[voleffop1]} ${voleffarg1}`, fg: colVol })
lines.push({ label: 'PanEff', value: `${panFxNames[paneffop1]} ${paneffarg1}`, fg: colPan })
lines.push({ label: 'FxOp ', value: fx, fg: colEffOp })
lines.push({ label: 'FxArg ', value: `$${effarg.hex04()}`, fg: colEffArg })
lines.push({ label: 'Note ', value: `${noteToStr(note)} ($${note.hex04()})`, fg: colNote })
lines.push({ label: 'Inst ', value: inst === 0 ? '--' : inst.hex02(), fg: colInst })
lines.push({ label: 'Vx ', value: `${volFxNames[voleffop1]} ${voleffarg1}`, fg: colVol })
lines.push({ label: 'Px ', value: `${panFxNames[paneffop1]} ${paneffarg1}`, fg: colPan })
lines.push({ label: 'Fx ', value: fxName.trimEnd(), fg: colEffOp })
lines.push({ label: 'FxOp ', value: fx, fg: colEffOp })
lines.push({ label: 'FxArg', value: `$${effarg.hex04()}`, fg: colEffArg })
if (cumState !== null) {
lines.push({ label: '------', value: '', fg: colSep })
@@ -849,9 +953,15 @@ 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 })
lines.push({ label: 'EF ', value: `$${cumState.memEF.hex04()}`, fg: colEffArg })
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: 'HU ', value: `$${cumState.memHU.speed.hex02()}/$${cumState.memHU.depth.hex02()}`, fg: colEffArg })
lines.push({ label: `H${MIDDOT}U `, value: `$${cumState.memHU.speed.hex02()}/$${cumState.memHU.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'R ', value: `$${cumState.memR.speed.hex02()}/$${cumState.memR.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'Y ', value: `$${cumState.memY.speed.hex02()}/$${cumState.memY.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'D ', value: `$${cumState.memD.hex04()}`, fg: colEffArg })
@@ -867,7 +977,7 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
const y = PTNVIEW_OFFSET_Y + i
const line = lines[i]
con.move(y, dx)
con.color_pair(colNote, 255)
con.color_pair(colStatus, 255)
print((line.label + ' ').substring(0, 6) + ' ')
con.color_pair(line.fg, 255)
print((line.value + ' '.repeat(detailW)).substring(0, detailW - 8))
@@ -1098,7 +1208,7 @@ function drawOrdersContents(wo) {
print(' ')
// CMD column — crosshair highlight at (ordersCursor, col 0)
const cmdBack = (isSel && ordersColCursor === 0) ? colPlayback : back
con.color_pair(cue.instr ? colNote : colSep, cmdBack)
con.color_pair(cue.instr ? colStatus : colSep, cmdBack)
print(cue.instr ? cue.instr.hex02() : '--')
con.color_pair(colBackPtn, back)
print(' ')
@@ -1107,7 +1217,7 @@ function drawOrdersContents(wo) {
const v = ordersVoiceOff + c
const ptn = v < song.numVoices ? cue.ptns[v] : CUE_EMPTY
const vBack = (isSel && ordersColCursor === v + 1) ? colPlayback : back
con.color_pair(ptn === CUE_EMPTY ? colSep : colNote, vBack)
con.color_pair(ptn === CUE_EMPTY ? colSep : colStatus, vBack)
print(ptn === CUE_EMPTY ? '---' : ptn.hex03())
con.color_pair(colBackPtn, back)
print(' ')
@@ -1129,7 +1239,7 @@ function timelineInput(wo, event) {
if (keyJustHit && shiftDown && event.includes(keys.R)) { setTimelineRowStyle(2); return }
if (playbackMode !== PLAYMODE_NONE) {
if (keyJustHit && shiftDown && event.includes(keys.Y) || keysym === " ") { stopPlayback(); redrawPanel() }
if (keyJustHit && shiftDown && event.includes(keys.Y) || keysym === " ") { stopPlayback(); redrawPanel(); drawAlwaysOnElems() }
else if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const dir = (keysym === "<LEFT>") ? -1 : 1
const oldVoiceOff = voiceOff
@@ -1148,7 +1258,7 @@ function timelineInput(wo, event) {
if (keyJustHit && shiftDown && event.includes(keys.Y)) { startPlaySong(); redrawPanel(); return }
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayCue(); redrawPanel(); return }
if ( shiftDown && event.includes(keys.I)) { startPlayRow(); drawPatternRowAt(cursorRow - scrollRow); return }
if (keyJustHit && shiftDown && event.includes(keys.O) || keysym === " ") { stopPlayback(); return }
if (keyJustHit && shiftDown && event.includes(keys.O) || keysym === " ") { stopPlayback(); drawAlwaysOnElems(); return }
const oldCursor = cursorRow
const oldScroll = scrollRow
@@ -1320,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
@@ -1342,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
@@ -1365,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 }
@@ -1374,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 }
}
@@ -1389,7 +1585,7 @@ function drawPatternListColumn() {
con.color_pair(255, colBackPtn)
print(' ')
} else {
con.color_pair(isCur ? colNote : colRowNum, isCur ? colHighlight : 255)
con.color_pair(isCur ? colStatus : colRowNum, isCur ? colHighlight : 255)
print(pi.hex03())
con.color_pair(colSep, 255)
print(' ')
@@ -1498,14 +1694,14 @@ function patternsInput(wo, event) {
if (playbackMode !== PLAYMODE_NONE) {
if ((keyJustHit && shiftDown && event.includes(keys.Y)) || keysym === " ") {
stopPlayback(); simStateKey = ''; drawPatternsContents(wo)
stopPlayback(); simStateKey = ''; drawPatternsContents(wo); drawAlwaysOnElems()
}
return
}
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayPattern(); drawPatternsContents(wo); return }
if ( shiftDown && event.includes(keys.I)) { startPlayPatternRow(); drawPatternGrid(); return }
if ((keyJustHit && shiftDown && event.includes(keys.O)) || keysym === " ") { stopPlayback(); return }
if ((keyJustHit && shiftDown && event.includes(keys.O)) || keysym === " ") { stopPlayback(); drawAlwaysOnElems(); return }
if (song.numPats === 0) return
@@ -1560,11 +1756,6 @@ const panels = [panelTimeline, panelOrders, panelPatterns]
const PLAYHEAD = 0
const PLAYMODE_NONE = 0
const PLAYMODE_SONG = 1
const PLAYMODE_CUE = 2
const PLAYMODE_ROW = 3
// Scratch cue slot used for pattern-only preview; beyond any real cue the song uses
const PREVIEW_CUE_IDX = NUM_CUES - 1
@@ -1707,6 +1898,7 @@ function updatePlayback() {
stopPlayback()
if (currentPanel === VIEW_TIMELINE) redrawPanel()
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
drawAlwaysOnElems()
return
}
if (playbackMode === PLAYMODE_ROW && (nowRow !== playStartRow || nowCue !== playStartCue)) {
@@ -1848,6 +2040,8 @@ while (!exitFlag) {
audio.stop(PLAYHEAD)
resetAudioDevice()
sys.free(SCRATCH_PTR)
font.resetLowRom()
font.resetHighRom()
con.clear()
con.move(1, 1)
con.curs_set(1)

Binary file not shown.

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)

View File

@@ -12,8 +12,11 @@ import net.torvald.tsvm.ThreeFiveMiniUfloat
import net.torvald.tsvm.VM
import net.torvald.tsvm.toInt
import java.io.ByteArrayInputStream
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.PI
private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
private fun printdbg(msg: Any) {
@@ -1133,6 +1136,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Letters A..Z map to 0x0A..0x23 (digit value 10..35).
private object EffectOp {
const val OP_NONE = 0x00
const val OP_1 = 0x01
const val OP_A = 0x0A
const val OP_B = 0x0B
const val OP_C = 0x0C
@@ -1352,6 +1356,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private fun applyEffectRow(ts: TrackerState, playhead: Playhead, voice: Voice, vi: Int, op: Int, rawArg: Int) {
when (op) {
EffectOp.OP_NONE -> {}
EffectOp.OP_1 -> {
// 1 $01xx — Set stereo panning law. High byte selects subcommand; only $01 is defined.
if ((rawArg ushr 8) == 0x01) ts.panLaw = rawArg and 0xFF
}
EffectOp.OP_A -> {
val tr = (rawArg ushr 8) and 0xFF
if (tr != 0) playhead.tickRate = tr
@@ -1733,8 +1741,21 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (!voice.active || voice.muted) continue
val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
mixL += s * vol * (63 - voice.rowPan) / 63.0
mixR += s * vol * voice.rowPan / 63.0
val pan = voice.channelPan
val lGain: Double
val rGain: Double
when (ts.panLaw) {
1 -> { // equal-power: constant loudness at centre (0.707 each)
lGain = cos(PI * pan / 512.0)
rGain = sin(PI * pan / 512.0)
}
else -> { // linear balance (tracker default): centre gives 0 dB on both channels
lGain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
rGain = if (pan < 0x80) pan / 128.0 else 1.0
}
}
mixL += s * vol * lGain
mixR += s * vol * rGain
}
ts.mixLeft[n] = mixL.toFloat().coerceIn(-1.0f, 1.0f)
@@ -1988,6 +2009,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var firstRow = true
val voices = Array(20) { Voice() }
// Global mixer config (effect 1).
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
// Pending row-end events (set during a row by B/C; consumed at row end).
var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
var pendingRowJump = -1 // -1 = none; otherwise the row index for the next pattern
@@ -2109,6 +2133,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
ts.pendingOrderJump = -1; ts.pendingRowJump = -1
ts.patternDelayRemaining = 0; ts.patternDelayActive = false
ts.sexWinningChannel = -1
ts.panLaw = 0
ts.voices.forEach {
it.active = false
it.channelVolume = 0x3F