it2taud, instrument format changes

This commit is contained in:
minjaesong
2026-04-29 09:21:28 +09:00
parent e7287fae37
commit 2282e0c10b
3 changed files with 1704 additions and 33 deletions

1580
it2taud.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2001,13 +2001,28 @@ Instrument bin: Registry for 256 instruments, formatted as:
Uint16 Play Start (usually 0 but not always) Uint16 Play Start (usually 0 but not always)
Uint16 Loop Start (can be smaller than Play Start) Uint16 Loop Start (can be smaller than Play Start)
Uint16 Loop End Uint16 Loop End
Bit32 Flags Bit8 Sample Flags
0b hhhh 00pp 0b hhhh 00pp
h: sample pointer high bit h: sample pointer high bit
pp: loop mode. 0-no loop, 1-loop, 2-backandforth, 3-oneshot (ignores note length unless overridden by other notes) pp: loop mode. 0-no loop, 1-loop, 2-backandforth, 3-oneshot (ignores note length unless overridden by other notes)
Bit16x24 Volume envelopes Bit8 Volume envelope sustain loops
Byte 1: Volume 0b u0 eee sss
s: sustain loop start index
e: sustain loop end index
u: set to enable the loop
Bit8 Panning envelope sustain loops
0b u0 eee sss
s: sustain loop start index
e: sustain loop end index
u: set to enable the loop
Bit8 Reserved
Bit16x8 Volume envelopes
Byte 1: Volume (00..3F)
Byte 2: Second offset from the prev point, in 3.5 Unsigned Minifloat Byte 2: Second offset from the prev point, in 3.5 Unsigned Minifloat
Bit16x8 Panning envelopes
Byte 1: Pan (00..FF)
Byte 2: Second offset from the prev point, in 3.5 Unsigned Minifloat
Bit16x8 Reserved
Play Data: play data are series of tracker-like instructions, visualised as: Play Data: play data are series of tracker-like instructions, visualised as:

View File

@@ -1166,24 +1166,68 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0) inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0)
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) { private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
if (voice.envIndex >= 23) { // Volume envelope
voice.envVolume = inst.envelopes[23].volume / 255.0 // sustain byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx
return val vSus = inst.volEnvSustain
} val vSusOn = (vSus and 0x80) != 0 && !voice.keyOff
val offset = inst.envelopes[voice.envIndex].offset.toFloat().toDouble() val vSusStart = vSus and 7
if (offset == 0.0) { val vSusEnd = (vSus ushr 3) and 7
voice.envVolume = inst.envelopes[voice.envIndex].volume / 255.0
return if (voice.envIndex >= 7) {
} voice.envVolume = (inst.volEnvelopes[7].value / 63.0).coerceIn(0.0, 1.0)
voice.envTimeSec += tickSec } else if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
if (voice.envTimeSec >= offset) { // slb == sle: hold at this node until key-off (no cycling)
voice.envTimeSec -= offset voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
voice.envIndex = (voice.envIndex + 1).coerceAtMost(23)
voice.envVolume = inst.envelopes[voice.envIndex].volume / 255.0
} else { } else {
val cur = inst.envelopes[voice.envIndex].volume / 255.0 val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
val nxt = inst.envelopes[(voice.envIndex + 1).coerceAtMost(23)].volume / 255.0 if (vOffset == 0.0) {
voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / offset) voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else {
voice.envTimeSec += tickSec
if (voice.envTimeSec >= vOffset) {
voice.envTimeSec -= vOffset
val nextIdx = if (vSusOn && voice.envIndex == vSusEnd) vSusStart
else (voice.envIndex + 1).coerceAtMost(7)
voice.envIndex = nextIdx
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else {
val cur = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
val nxt = (inst.volEnvelopes[(voice.envIndex + 1).coerceAtMost(7)].value / 63.0).coerceIn(0.0, 1.0)
voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / vOffset)
}
}
}
// Pan envelope (only when active for this instrument)
if (!voice.hasPanEnv) return
val pSus = inst.panEnvSustain
val pSusOn = (pSus and 0x80) != 0 && !voice.keyOff
val pSusStart = pSus and 7
val pSusEnd = (pSus ushr 3) and 7
if (voice.envPanIndex >= 7) {
voice.envPan = inst.panEnvelopes[7].value / 255.0
} else if (pSusOn && voice.envPanIndex == pSusEnd && pSusStart == pSusEnd) {
// slb == sle: hold at this pan node until key-off
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else {
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
if (pOffset == 0.0) {
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else {
voice.envPanTimeSec += tickSec
if (voice.envPanTimeSec >= pOffset) {
voice.envPanTimeSec -= pOffset
val nextIdx = if (pSusOn && voice.envPanIndex == pSusEnd) pSusStart
else (voice.envPanIndex + 1).coerceAtMost(7)
voice.envPanIndex = nextIdx
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else {
val cur = inst.panEnvelopes[voice.envPanIndex].value / 255.0
val nxt = inst.panEnvelopes[(voice.envPanIndex + 1).coerceAtMost(7)].value / 255.0
voice.envPan = cur + (nxt - cur) * (voice.envPanTimeSec / pOffset)
}
}
} }
} }
@@ -1238,9 +1282,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.samplePos = inst.samplePlayStart.toDouble() voice.samplePos = inst.samplePlayStart.toDouble()
voice.forward = true voice.forward = true
voice.active = true voice.active = true
voice.keyOff = false
voice.envIndex = 0 voice.envIndex = 0
voice.envTimeSec = 0.0 voice.envTimeSec = 0.0
voice.envVolume = inst.envelopes[0].volume / 255.0 voice.envVolume = (inst.volEnvelopes[0].value / 63.0).coerceIn(0.0, 1.0)
voice.envPanIndex = 0
voice.envPanTimeSec = 0.0
voice.envPan = inst.panEnvelopes[0].value / 255.0
voice.hasPanEnv = inst.panEnvelopes.any { it.offset.toFloat() > 0.0f }
voice.noteVal = noteVal voice.noteVal = noteVal
voice.basePitch = noteVal voice.basePitch = noteVal
voice.playbackRate = computePlaybackRate(inst, noteVal) voice.playbackRate = computePlaybackRate(inst, noteVal)
@@ -1322,7 +1371,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val toneG = (row.effect == EffectOp.OP_G) val toneG = (row.effect == EffectOp.OP_G)
when (row.note) { when (row.note) {
0xFFFF -> {} // no-op 0xFFFF -> {} // no-op
0x0000 -> voice.active = false // key-off (TODO release envelope) 0x0000 -> { voice.keyOff = true; voice.active = false } // key-off; breaks sustain loop
0xFFFE -> voice.active = false // note cut 0xFFFE -> voice.active = false // note cut
else -> { else -> {
if (toneG && voice.active) { if (toneG && voice.active) {
@@ -1652,8 +1701,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.retrigCounter++ voice.retrigCounter++
if (voice.retrigCounter >= voice.retrigInterval) { if (voice.retrigCounter >= voice.retrigInterval) {
voice.retrigCounter = 0 voice.retrigCounter = 0
voice.samplePos = instruments[voice.instrumentId].samplePlayStart.toDouble() val retrigInst = instruments[voice.instrumentId]
voice.samplePos = retrigInst.samplePlayStart.toDouble()
voice.keyOff = false
voice.envIndex = 0; voice.envTimeSec = 0.0 voice.envIndex = 0; voice.envTimeSec = 0.0
voice.envPanIndex = 0; voice.envPanTimeSec = 0.0
voice.envPan = retrigInst.panEnvelopes[0].value / 255.0
voice.rowVolume = applyRetrigVolMod(voice.rowVolume, voice.retrigVolMod) voice.rowVolume = applyRetrigVolMod(voice.rowVolume, voice.retrigVolMod)
voice.channelVolume = voice.rowVolume voice.channelVolume = voice.rowVolume
} }
@@ -1741,7 +1794,10 @@ 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
val pan = voice.channelPan val pan = if (voice.hasPanEnv) {
val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255)
(voice.channelPan + envPanRaw - 128).coerceIn(0, 255)
} else voice.channelPan
val lGain: Double val lGain: Double
val rGain: Double val rGain: Double
when (ts.panLaw) { when (ts.panLaw) {
@@ -1915,9 +1971,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var channelPan = 0x80 // 8-bit; $80 centre. Cell column packs into 6-bit, S$80xx writes the full 8-bit. var channelPan = 0x80 // 8-bit; $80 centre. Cell column packs into 6-bit, S$80xx writes the full 8-bit.
var rowPan = 32 // 6-bit pan used by mixer, derived from channelPan var rowPan = 32 // 6-bit pan used by mixer, derived from channelPan
var keyOff = false
var envIndex = 0 var envIndex = 0
var envTimeSec = 0.0 var envTimeSec = 0.0
var envVolume = 1.0 var envVolume = 1.0
var envPanIndex = 0
var envPanTimeSec = 0.0
var envPan = 0.5 // 0.0=full-left, 1.0=full-right, 0.5=centre
var hasPanEnv = false
// Pitch state (4096-TET units, signed when slid). // Pitch state (4096-TET units, signed when slid).
var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added) var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added)
@@ -2208,7 +2269,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
} }
data class TaudInstVolEnv(var volume: Int, var offset: ThreeFiveMiniUfloat) data class TaudInstEnvPoint(var value: Int, var offset: ThreeFiveMiniUfloat)
data class TaudInst( data class TaudInst(
var index: Int, var index: Int,
@@ -2220,9 +2281,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var sampleLoopEnd: Int, var sampleLoopEnd: Int,
// flags // flags
var loopMode: Int, var loopMode: Int,
var envelopes: Array<TaudInstVolEnv> // first int: volume (0..255), second int: offsets (minifloat indices) var volEnvSustain: Int, // byte 13: 00 eee sss (0 = no sustain loop)
var panEnvSustain: Int, // byte 14: 00 eee sss (0 = no sustain loop)
var volEnvelopes: Array<TaudInstEnvPoint>, // 8 points, value 0x00-0x3F
var panEnvelopes: Array<TaudInstEnvPoint> // 8 points, value 0x00-0xFF (0x80 = centre)
) { ) {
constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, Array(24) { TaudInstVolEnv(0, ThreeFiveMiniUfloat(0)) }) constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Array(8) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(8) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
// Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region. // Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region.
// Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact. // Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact.
@@ -2260,9 +2326,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
11 -> sampleLoopEnd.ushr(8).toByte() 11 -> sampleLoopEnd.ushr(8).toByte()
12 -> (samplePtr.ushr(16).and(15).shl(4) or loopMode.and(3)).toByte() 12 -> (samplePtr.ushr(16).and(15).shl(4) or loopMode.and(3)).toByte()
13,14,15 -> 0 13 -> volEnvSustain.toByte()
in 16..63 step 2 -> envelopes[(offset - 16) / 2].volume.toByte() 14 -> panEnvSustain.toByte()
in 17..63 step 2 -> envelopes[(offset - 17) / 2].offset.index.toByte() 15 -> 0
in 16..30 step 2 -> volEnvelopes[(offset - 16) / 2].value.toByte()
in 17..31 step 2 -> volEnvelopes[(offset - 17) / 2].offset.index.toByte()
in 32..46 step 2 -> panEnvelopes[(offset - 32) / 2].value.toByte()
in 33..47 step 2 -> panEnvelopes[(offset - 33) / 2].offset.index.toByte()
in 48..63 -> 0
else -> throw InternalError("Bad offset $offset") else -> throw InternalError("Bad offset $offset")
} }
@@ -2290,10 +2361,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
else samplePtr and 0x0ffff else samplePtr and 0x0ffff
loopMode = byte and 3 loopMode = byte and 3
} }
13, 14, 15 -> {} 13 -> { volEnvSustain = byte }
14 -> { panEnvSustain = byte }
15 -> {}
in 16..63 step 2 -> envelopes[(offset - 16) / 2].volume = byte in 16..30 step 2 -> volEnvelopes[(offset - 16) / 2].value = byte
in 17..63 step 2 -> envelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte) in 17..31 step 2 -> volEnvelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)
in 32..46 step 2 -> panEnvelopes[(offset - 32) / 2].value = byte
in 33..47 step 2 -> panEnvelopes[(offset - 33) / 2].offset = ThreeFiveMiniUfloat(byte)
in 48..63 -> {}
else -> throw InternalError("Bad offset $offset") else -> throw InternalError("Bad offset $offset")
} }
} }