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. **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. **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 # 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. 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 | | `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 | | `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 | | `2 $xx` | `E round($0xxx × 64/3)` | Portamento down |
| `5 $xy` | `L $xy00` | Combined portamento + volume slide (see compatibility note) | | `3 $xx` | `G round($0xxx × 64/3)` | Portamento to note |
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide (see compatibility note) | | `4 $xy` | `H $xxyy` | Vibrato; nibble-repeat each byte. |
| `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat | | `5 $xy` | `L $xy00` | Combined portamento + volume slide (see compatibility note) |
| `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan | | `6 $xy` | `K $xy00` | Combined vibrato + volume slide (see compatibility note) |
| `9 $xx` | `O $xx00` | Sample offset | | `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat |
| `A $xy` | Volume column `1.$xy` | Volume slide | | `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan |
| `B $xx` | `B $00xx` | Position jump | | `9 $xx` | `O $xx00` | Sample offset |
| `C $xx` | Volume column `0.$xx` | Set volume | | `A $xy` | Volume column `1.$xy` | Volume slide |
| `D $xx` | `C $00xx` (after BCD decode) | Pattern break | | `B $xx` | `B $00xx` | Position jump |
| `E $3x` | `S $1x00` | Glissando control | | `C $xx` | Volume column `0.$xx` | Set volume |
| `E $4x` | `S $3x00` | Vibrato waveform | | `D $xx` | `C $00xx` (after BCD decode) | Pattern break |
| `E $5x` | `S $2x00` | Set fine-tune | | `E $0x` | `S $000x` | (UNIMPLEMENTED) Set filter |
| `E $6x` | `S $Bx00` | Pattern loop | | `E $1x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide up |
| `E $7x` | `S $4x00` | Tremolo waveform | | `E $2x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide down |
| `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) | | `E $3x` | `S $1x00` | Glissando control |
| `E $9x` | `Q $0x00` | Retrigger | | `E $4x` | `S $3x00` | Vibrato waveform |
| `E $Cx` | `S $Cx00` | Note cut | | `E $5x` | `S $2x00` | Set fine-tune |
| `E $Dx` | `S $Dx00` | Note delay | | `E $6x` | `S $Bx00` | Pattern loop |
| `E $Ex` | `S $Ex00` | Pattern delay | | `E $7x` | `S $4x00` | Tremolo waveform |
| `E $Fx` | `S $Fx00` | Funk repeat | | `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) |
| `F $xx` (xx < $20) | `A $xx00` | Set speed | | `E $9x` | `Q $0x00` | Retrigger |
| `F $xx` (xx ≥ $20) | `T $(xx$18)00` | Set tempo | | `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", panfinele:"\u008427u",
panfineri:"\u008426u", panfineri:"\u008426u",
/* Fx/Vx/Px */
fx:'\u00F8',
px:'\u00AC',
vx:'\u00AD',
/* transport control */
playall:'\u00A8',
playcue:'\u00A9',
playrow:'\u00AA',
stop:'\u00AB',
/* miscellaneous */ /* miscellaneous */
unticked:"\u009E", unticked:"\u00AE",
ticked:"\u009F", ticked:"\u00AF",
middot:MIDDOT, middot:MIDDOT,
doubledot:"\u008419u", doubledot:"\u008419u",
stop:"\u008420u\u008421u", statusstop:"\u008420u\u008421u",
play:"\u008422u\u008423u", statusplay:"\u008422u\u008423u",
playhead:"\u00A7",
} }
const fxNames = { const fxNames = {
'0':"No effect ", '0':"-- ",
'1':"UNIMPLEMENTED", '1':"Mixer config ", // Taud: 1 01xx: set stereo panning law
'2':"UNIMPLEMENTED", '2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED", '3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED", '4':"UNIMPLEMENTED",
@@ -94,26 +106,26 @@ G:"Portamento ",
H:"Vibrato ", H:"Vibrato ",
I:"Tremor ", I:"Tremor ",
J:"Arpeggio ", J:"Arpeggio ",
K:"UNIMPLEMENTED", K:"UNIMPLEMENTED", // Volume slide+Vibrato. Use H0000 and VolEff instead
L:"UNIMPLEMENTED", L:"UNIMPLEMENTED", // Volume slide+Portamento. Use G0000 and VolEff instead
M:"UNIMPLEMENTED", M:"UNIMPLEMENTED", // IT: Set channel volume. Use VolEff instead
N:"UNIMPLEMENTED", N:"UNIMPLEMENTED", // IT: Channel volume slide. Use VolEff instead
O:"Sample offset", O:"Sample offset",
P:"UNIMPLEMENTED", P:"UNIMPLEMENTED", // IT: panning slide. Use PanEff instead
Q:"Retrigger ", Q:"Retrigger ",
R:"Tremolo ", R:"Tremolo ",
S:"Special ", S:"Special ",
S0:"UNIMPLEMENTED", S0:"UNIMPLEMENTED", // PT: Set audio filter.
S1:"Gliss. ctrl ", S1:"Gliss. ctrl ",
S2:"Sample tune ", S2:"Sample tune ",
S3:"Vibrato LFO ", S3:"Vibrato LFO ",
S4:"Tremolo LFO ", S4:"Tremolo LFO ",
S5:"Panbrello LFO", S5:"Panbrello LFO",
S6:"UNIMPLEMENTED", S6:"UNIMPLEMENTED", // IT: Fine pattern delay.
S7:"UNIMPLEMENTED", S7:"UNIMPLEMENTED", // IT: misc. functions
S8:"Channel pan ", S8:"Channel pan ", // Taud: 8-bit channel panning.
S9:"UNIMPLEMENTED", S9:"UNIMPLEMENTED", // IT: Sound control.
SA:"UNIMPLEMENTED", SA:"UNIMPLEMENTED", // SC3: Stereo control. IT: Sample offset high twobyte.
SB:"Pattern loop ", SB:"Pattern loop ",
SC:"Note cut ", SC:"Note cut ",
SD:"Note delay ", SD:"Note delay ",
@@ -122,10 +134,10 @@ SF:"Funk it ",
T:"Tempo ", T:"Tempo ",
U:"Fine vibrato ", U:"Fine vibrato ",
V:"Global volume", V:"Global volume",
W:"UNIMPLEMENTED", W:"UNIMPLEMENTED", // IT: Global volume slide.
X:"UNIMPLEMENTED", X:"UNIMPLEMENTED", // IT: 8-bit channel panning. Use PanEff or S80xx instead
Y:"Panbrello ", Y:"Panbrello ",
Z:"UNIMPLEMENTED", Z:"UNIMPLEMENTED", // IT: MIDI macro.
} }
const panFxNames = { const panFxNames = {
0:"Set to", 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 volEffSym = [sym.volset, sym.volup, sym.voldn, sym.volfineup, sym.volfinedn]
const panEffSym = [sym.panset, sym.panle, sym.panri, sym.panfinele, sym.panfineri] const panEffSym = [sym.panset, sym.panle, sym.panri, sym.panfinele, sym.panfineri]
const colNote = 254 const colNote = 239
const colInst = 114 const colInst = 114
const colVol = 155 const colVol = 155
const colPan = 219 const colPan = 219
@@ -202,7 +214,33 @@ const colEffOp = 220
const colEffArg = 231 const colEffArg = 231
const colBackPtn = 255 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() { Number.prototype.hex02 = function() {
return this.toString(16).toUpperCase().padStart(2,'0') 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 === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff if (note === 0x0000) return sym.keyoff
const table = pitchTablePresets[PITCH_PRESET_IDX].table if (pitchTablePresets[PITCH_PRESET_IDX].table.length === 0) return note.hex04()
const syms = pitchTablePresets[PITCH_PRESET_IDX].sym const [s, o] = pitchSymLut[note & 0xFFF]
if (table.length === 0) return note.hex04() return s + ((note >> 12) - 1 + o)
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
} }
/** /**
@@ -267,7 +296,7 @@ function buildRowCell(ptnDat, row) {
let sVolEff = volEffSym[voleff >>> 6] let sVolEff = volEffSym[voleff >>> 6]
let sVolArg = voleffarg.hexD2() let sVolArg = voleffarg.hexD2()
if (voleff === 0) { if (voleff === 0xC0) {
sVolEff = '' sVolEff = ''
sVolArg = sym.middot.repeat(2) sVolArg = sym.middot.repeat(2)
} }
@@ -291,7 +320,7 @@ function buildRowCell(ptnDat, row) {
let sPanEff = panEffSym[paneff >>> 6] let sPanEff = panEffSym[paneff >>> 6]
let sPanArg = paneffarg.hexD2() let sPanArg = paneffarg.hexD2()
if (paneff === 0) { if (paneff === 0xC0) {
sPanEff = '' sPanEff = ''
sPanArg = sym.middot.repeat(2) sPanArg = sym.middot.repeat(2)
} }
@@ -508,7 +537,7 @@ function loadTaud(filePath, songIndex) {
const [SCRH, SCRW] = con.getmaxyx() const [SCRH, SCRW] = con.getmaxyx()
const PTNVIEW_OFFSET_X = 3 const PTNVIEW_OFFSET_X = 3
const PTNVIEW_OFFSET_Y = 9 const PTNVIEW_OFFSET_Y = 5
const PTNVIEW_HEIGHT = SCRH - PTNVIEW_OFFSET_Y const PTNVIEW_HEIGHT = SCRH - PTNVIEW_OFFSET_Y
const TIMELINE_COLSIZES = [15, 7, 5] const TIMELINE_COLSIZES = [15, 7, 5]
@@ -533,6 +562,15 @@ const colRowNumEmph1 = 180
const colStatus = 253 const colStatus = 253
const colVoiceHdr = 230 const colVoiceHdr = 230
const colSep = 252 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 let separatorStyle = 0
@@ -543,6 +581,11 @@ const PATEDITOR_CELL_X = 10
const PATEDITOR_SEP2_X = 30 const PATEDITOR_SEP2_X = 30
const PATEDITOR_DETAIL_X = 32 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) { function fillLine(y, c, back) {
con.color_pair(c, back) con.color_pair(c, back)
for (let x = 1; x <= SCRW; x++) { for (let x = 1; x <= SCRW; x++) {
@@ -550,37 +593,108 @@ function fillLine(y, c, back) {
} }
} }
const TAB_GAP = 2 const TAB_GAP = 3
const PANEL_NAMES = ['Timeline', 'Orders', 'Patterns', 'Samples', 'Instruments', 'Project', 'File'] const PANEL_NAMES = ['Timeline', 'Cues', 'Patterns', 'Samples', 'Instrmnt', 'Project', 'File']
function drawAlwaysOnElems() { function drawAlwaysOnElems() {
drawStatusBar() 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() { function drawStatusBar() {
fillLine(1, colStatus, 255) fillLine(1, colStatus, 255)
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue fillLine(2, colStatus, 255)
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)} ` const sCueIdx = cueIdx.hex03()
con.move(1, 1) 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) 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() { function drawTabBar() {
const XOFF = 2 con.color_pair(colTabBarOrn, colTabBarBack)
const YOFF = PTNVIEW_OFFSET_Y - 4 con.move(3,1)
print(`\u00FB`.repeat(SCRW))
// TODO make it fancier const XOFF = 2
const YOFF = 3
con.move(YOFF, XOFF) con.move(YOFF, XOFF)
for (let i = 0; i < PANEL_NAMES.length; i++) { for (let i = 0; i < PANEL_NAMES.length; i++) {
if (i > 0) con.curs_right(TAB_GAP); if (i > 0) con.curs_right(TAB_GAP);
let panStr = PANEL_NAMES[i] let tabName = PANEL_NAMES[i]
print((currentPanel === i) ? `[${panStr}]` : ` ${panStr} `)
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'], [`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'], [`Pg\u008418u`,'Cue'],
['sep'], ['sep'],
['Y','Song'], ['WER','ViewMode'],
['U','Cue'],
['I','Row'],
['O/Sp','Stop'],
['sep'], ['sep'],
['m','Mute'], ['m','Mute'],
['s','Solo'], ['s','Solo'],
['sep'], ['sep'],
['Tab','Panel'], ['Tab','Panel'],
//['q','Quit'], // ['sep'],
// ['q','Quit'],
] ]
let hintElemOrders = [ let hintElemOrders = [
[`\u008428u\u008429u`,'Nav'], [`\u008428u\u008429u`,'Nav'],
[`Ent`,'Go to cue'], [`Ent`,'Go to cue'],
['sep'],
['U','Cue'],
['O/Sp','Stop'],
['sep'], ['sep'],
['Tab','Panel'], ['Tab','Panel'],
['sep'], // ['sep'],
['q','Quit'], // ['q','Quit'],
] ]
let hintElemPatterns = [ let hintElemPatterns = [
[`\u008428u\u008429u`,'Nav'], [`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Ptn'], [`Pg\u008418u`,'Ptn'],
['sep'],
['U','Ptn'],
['I','Row'],
['O/Sp','Stop'],
['sep'], ['sep'],
['Tab','Panel'], ['Tab','Panel'],
['sep'], // ['sep'],
['q','Quit'], // ['q','Quit'],
] ]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns] let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns]
@@ -818,11 +923,10 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
const fxName = fxNames[fx] || '? ' const fxName = fxNames[fx] || '? '
if (!isVerticalLayout) { if (!isVerticalLayout) {
con.move(6, 1) return
print(`Pitch $${note.hex04()}\tInst $${inst.hex02()}\tVolEff ${voleffop}.$${voleffarg.hex02()}\t` + con.move(PTNVIEW_OFFSET_Y-2, 1)
`PanEff ${paneffop}.$${paneffarg.hex02()}`) print(`Pitch $${note.hex04()} Inst $${inst.hex02()} ${sym.vx} ${voleffop}.$${voleffarg.hex02()} ` +
con.move(7, 1) `${sym.px} ${paneffop}.$${paneffarg.hex02()} ${sym.fx} ${fxName} $${effarg.hex04()}`)
print(`\u0084248u ${fxName}\t$${effarg.hex04()} `)
} else { } else {
const dx = PATEDITOR_DETAIL_X const dx = PATEDITOR_DETAIL_X
const detailW = SCRW - dx + 1 const detailW = SCRW - dx + 1
@@ -835,13 +939,13 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
if (paneff == 0xC0) { paneffop1 = 999; paneffarg1 = '' } if (paneff == 0xC0) { paneffop1 = 999; paneffarg1 = '' }
const lines = [] const lines = []
lines.push({ label: 'Note ', value: `${noteToStr(note)} ($${note.hex04()})`, fg: colNote }) 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: 'Inst ', value: inst === 0 ? '--' : inst.hex02(), fg: colInst })
lines.push({ label: 'VolEff', value: `${volFxNames[voleffop1]} ${voleffarg1}`, fg: colVol }) lines.push({ label: 'Vx ', value: `${volFxNames[voleffop1]} ${voleffarg1}`, fg: colVol })
lines.push({ label: 'PanEff', value: `${panFxNames[paneffop1]} ${paneffarg1}`, fg: colPan }) lines.push({ label: 'Px ', 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: 'Fx ', value: fxName.trimEnd(), fg: colEffOp }) 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) { if (cumState !== null) {
lines.push({ label: '------', value: '', fg: colSep }) 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: 'L.Inst', value: cumState.lastInst === 0 ? '--' : cumState.lastInst.hex02(), fg: colInst })
lines.push({ label: 'Vol ', value: `$${cumState.volAbs.hex02()}`, fg: colVol }) lines.push({ label: 'Vol ', value: `$${cumState.volAbs.hex02()}`, fg: colVol })
lines.push({ label: 'Pan ', value: `$${cumState.panAbs.hex02()}`, fg: colPan }) 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: '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: '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: 'Y ', value: `$${cumState.memY.speed.hex02()}/$${cumState.memY.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'D ', value: `$${cumState.memD.hex04()}`, 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 y = PTNVIEW_OFFSET_Y + i
const line = lines[i] const line = lines[i]
con.move(y, dx) con.move(y, dx)
con.color_pair(colNote, 255) con.color_pair(colStatus, 255)
print((line.label + ' ').substring(0, 6) + ' ') print((line.label + ' ').substring(0, 6) + ' ')
con.color_pair(line.fg, 255) con.color_pair(line.fg, 255)
print((line.value + ' '.repeat(detailW)).substring(0, detailW - 8)) print((line.value + ' '.repeat(detailW)).substring(0, detailW - 8))
@@ -1098,7 +1208,7 @@ function drawOrdersContents(wo) {
print(' ') print(' ')
// CMD column — crosshair highlight at (ordersCursor, col 0) // CMD column — crosshair highlight at (ordersCursor, col 0)
const cmdBack = (isSel && ordersColCursor === 0) ? colPlayback : back 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() : '--') print(cue.instr ? cue.instr.hex02() : '--')
con.color_pair(colBackPtn, back) con.color_pair(colBackPtn, back)
print(' ') print(' ')
@@ -1107,7 +1217,7 @@ function drawOrdersContents(wo) {
const v = ordersVoiceOff + c const v = ordersVoiceOff + c
const ptn = v < song.numVoices ? cue.ptns[v] : CUE_EMPTY const ptn = v < song.numVoices ? cue.ptns[v] : CUE_EMPTY
const vBack = (isSel && ordersColCursor === v + 1) ? colPlayback : back 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()) print(ptn === CUE_EMPTY ? '---' : ptn.hex03())
con.color_pair(colBackPtn, back) con.color_pair(colBackPtn, back)
print(' ') print(' ')
@@ -1129,7 +1239,7 @@ function timelineInput(wo, event) {
if (keyJustHit && shiftDown && event.includes(keys.R)) { setTimelineRowStyle(2); return } if (keyJustHit && shiftDown && event.includes(keys.R)) { setTimelineRowStyle(2); return }
if (playbackMode !== PLAYMODE_NONE) { 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>") { else if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const dir = (keysym === "<LEFT>") ? -1 : 1 const dir = (keysym === "<LEFT>") ? -1 : 1
const oldVoiceOff = voiceOff 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.Y)) { startPlaySong(); redrawPanel(); return }
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayCue(); redrawPanel(); return } if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayCue(); redrawPanel(); return }
if ( shiftDown && event.includes(keys.I)) { startPlayRow(); drawPatternRowAt(cursorRow - scrollRow); 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 oldCursor = cursorRow
const oldScroll = scrollRow const oldScroll = scrollRow
@@ -1320,18 +1430,23 @@ function getActiveRowForDetail() {
// Walk pattern rows 0..uptoRow and accumulate effect-memory cohort state // Walk pattern rows 0..uptoRow and accumulate effect-memory cohort state
function simulateRowState(ptnDat, uptoRow) { function simulateRowState(ptnDat, uptoRow) {
const OP_A = 10
const OP_D = 13, OP_E = 14, OP_F = 15, OP_G = 16 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_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 const OP_Q = 26, OP_R = 27, OP_T = 29, OP_U = 30, OP_Y = 34
let lastNote = 0xFFFF, lastInst = 0 let lastNote = 0xFFFF, lastInst = 0
let volAbs = 0x3F, panAbs = 0x20 let volAbs = 0x3F, panAbs = 0x20
let pitchOff = 0, portaTarget = -1
let speed = 6
let memEF = 0, memG = 0 let memEF = 0, memG = 0
let memHU = { speed: 0, depth: 0 } let memHU = { speed: 0, depth: 0 }
let memR = { speed: 0, depth: 0 } let memR = { speed: 0, depth: 0 }
let memY = { speed: 0, depth: 0 } let memY = { speed: 0, depth: 0 }
let memD = 0, memI = 0, memJ = 0, memO = 0, memQ = 0, memTSlide = 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) const limit = Math.min(uptoRow, ROWS_PER_PAT - 1)
for (let row = 0; row <= limit; row++) { for (let row = 0; row <= limit; row++) {
const off = 8 * row const off = 8 * row
@@ -1342,17 +1457,99 @@ function simulateRowState(ptnDat, uptoRow) {
const effop = ptnDat[off+5] const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8) 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 if (inst !== 0) lastInst = inst
const volop = (voleff >>> 6) & 3 // Volume column: set OR slide
if (voleff !== 0 && volop === 0) volAbs = voleff & 63 const volop = (voleff >>> 6) & 3
const panop = (paneff >>> 6) & 3 const volefarg = voleff & 63
if (paneff !== 0 && panop === 0) panAbs = paneff & 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 !== 0 || effarg !== 0) {
if (effop === OP_E || effop === OP_F) { if (effarg !== 0) memEF = effarg } if (effop === OP_A) {
else if (effop === OP_G) { if (effarg !== 0) memG = effarg } 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) { else if (effop === OP_H || effop === OP_U) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memHU.speed = spd; if (dep !== 0) memHU.depth = dep 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 const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memY.speed = spd; if (dep !== 0) memY.depth = dep 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_I) { if (effarg !== 0) memI = effarg }
else if (effop === OP_J) { if (effarg !== 0) memJ = effarg } else if (effop === OP_J) { if (effarg !== 0) memJ = effarg }
else if (effop === OP_O) { if (effarg !== 0) memO = 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, memEF, memG, memHU, memR, memY,
memD, memI, memJ, memO, memQ, memTSlide } memD, memI, memJ, memO, memQ, memTSlide }
} }
@@ -1389,7 +1585,7 @@ function drawPatternListColumn() {
con.color_pair(255, colBackPtn) con.color_pair(255, colBackPtn)
print(' ') print(' ')
} else { } else {
con.color_pair(isCur ? colNote : colRowNum, isCur ? colHighlight : 255) con.color_pair(isCur ? colStatus : colRowNum, isCur ? colHighlight : 255)
print(pi.hex03()) print(pi.hex03())
con.color_pair(colSep, 255) con.color_pair(colSep, 255)
print(' ') print(' ')
@@ -1498,14 +1694,14 @@ function patternsInput(wo, event) {
if (playbackMode !== PLAYMODE_NONE) { if (playbackMode !== PLAYMODE_NONE) {
if ((keyJustHit && shiftDown && event.includes(keys.Y)) || keysym === " ") { if ((keyJustHit && shiftDown && event.includes(keys.Y)) || keysym === " ") {
stopPlayback(); simStateKey = ''; drawPatternsContents(wo) stopPlayback(); simStateKey = ''; drawPatternsContents(wo); drawAlwaysOnElems()
} }
return return
} }
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayPattern(); drawPatternsContents(wo); return } if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayPattern(); drawPatternsContents(wo); return }
if ( shiftDown && event.includes(keys.I)) { startPlayPatternRow(); drawPatternGrid(); 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 if (song.numPats === 0) return
@@ -1560,11 +1756,6 @@ const panels = [panelTimeline, panelOrders, panelPatterns]
const PLAYHEAD = 0 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 // Scratch cue slot used for pattern-only preview; beyond any real cue the song uses
const PREVIEW_CUE_IDX = NUM_CUES - 1 const PREVIEW_CUE_IDX = NUM_CUES - 1
@@ -1707,6 +1898,7 @@ function updatePlayback() {
stopPlayback() stopPlayback()
if (currentPanel === VIEW_TIMELINE) redrawPanel() if (currentPanel === VIEW_TIMELINE) redrawPanel()
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() } else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
drawAlwaysOnElems()
return return
} }
if (playbackMode === PLAYMODE_ROW && (nowRow !== playStartRow || nowCue !== playStartCue)) { if (playbackMode === PLAYMODE_ROW && (nowRow !== playStartRow || nowCue !== playStartCue)) {
@@ -1848,6 +2040,8 @@ while (!exitFlag) {
audio.stop(PLAYHEAD) audio.stop(PLAYHEAD)
resetAudioDevice() resetAudioDevice()
sys.free(SCRATCH_PTR) sys.free(SCRATCH_PTR)
font.resetLowRom()
font.resetHighRom()
con.clear() con.clear()
con.move(1, 1) con.move(1, 1)
con.curs_set(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 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 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 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 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. # Panbrello LFO waveform — maps directly to Taud S$5x00.
return (TOP_S, 0x5000 | (val << 8), None, None) return (TOP_S, 0x5000 | (val << 8), None, None)
if sub == 0x8: if sub == 0x8:
# Coarse pan: nibble-repeat into Taud's S $80xx full-8-bit pan. # S8x → PanEff 0.yy where yy = round(x * 4.2), mapping nibble 0-15 to pan 0-63.
return (TOP_S, 0x8000 | (val * 0x11), None, None) return (TOP_NONE, 0, None, (SEL_SET, round(val * 4.2)))
# S0/S6/S7/S9/SA: filter, NNA, sound-control, stereo — drop silently. # S0/S6/S7/S9/SA: filter, NNA, sound-control, stereo — drop silently.
return (TOP_NONE, 0, None, None) 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.VM
import net.torvald.tsvm.toInt import net.torvald.tsvm.toInt
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.PI
private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable { private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
private fun printdbg(msg: Any) { 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). // Letters A..Z map to 0x0A..0x23 (digit value 10..35).
private object EffectOp { private object EffectOp {
const val OP_NONE = 0x00 const val OP_NONE = 0x00
const val OP_1 = 0x01
const val OP_A = 0x0A const val OP_A = 0x0A
const val OP_B = 0x0B const val OP_B = 0x0B
const val OP_C = 0x0C 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) { private fun applyEffectRow(ts: TrackerState, playhead: Playhead, voice: Voice, vi: Int, op: Int, rawArg: Int) {
when (op) { when (op) {
EffectOp.OP_NONE -> {} 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 -> { EffectOp.OP_A -> {
val tr = (rawArg ushr 8) and 0xFF val tr = (rawArg ushr 8) and 0xFF
if (tr != 0) playhead.tickRate = tr 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 if (!voice.active || voice.muted) continue
val s = fetchTrackerSample(voice, instruments[voice.instrumentId]) val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0 val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
mixL += s * vol * (63 - voice.rowPan) / 63.0 val pan = voice.channelPan
mixR += s * vol * voice.rowPan / 63.0 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) 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 var firstRow = true
val voices = Array(20) { Voice() } 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). // 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 pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
var pendingRowJump = -1 // -1 = none; otherwise the row index for the next pattern 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.pendingOrderJump = -1; ts.pendingRowJump = -1
ts.patternDelayRemaining = 0; ts.patternDelayActive = false ts.patternDelayRemaining = 0; ts.patternDelayActive = false
ts.sexWinningChannel = -1 ts.sexWinningChannel = -1
ts.panLaw = 0
ts.voices.forEach { ts.voices.forEach {
it.active = false it.active = false
it.channelVolume = 0x3F it.channelVolume = 0x3F