taud: panning law toggle

This commit is contained in:
minjaesong
2026-04-26 20:08:02 +09:00
parent 93f7f436a3
commit c5789ec28b
3 changed files with 63 additions and 17 deletions

View File

@@ -749,6 +749,27 @@ 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.

View File

@@ -75,7 +75,7 @@ play:"\u008422u\u008423u",
const fxNames = {
'0':"No effect ",
'1':"UNIMPLEMENTED",
'1':"Mixer config ", // Taud: 1 01xx: set stereo panning law
'2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED",
@@ -94,26 +94,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 +122,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",

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