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 # 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.

View File

@@ -75,7 +75,7 @@ play:"\u008422u\u008423u",
const fxNames = { const fxNames = {
'0':"No effect ", '0':"No effect ",
'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 +94,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 +122,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",

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