diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index cc34fa1..8e183eb 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -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. diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index c4ade89..dfc8477 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -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", diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index c5a96be..0abc894 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -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