taud: 12 envelope nodes; taut proj tab

This commit is contained in:
minjaesong
2026-05-01 01:35:52 +09:00
parent 515e0268e6
commit 80c26c6b35
7 changed files with 158 additions and 42 deletions

View File

@@ -196,7 +196,7 @@ sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10122:{index:10122,name:"Pythagorean Augmented Fourth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10123:{index:10123,name:"Shi'er lu", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],
10123:{index:10123,name:"\u00FC\u00FD\u00FE", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
@@ -218,6 +218,7 @@ const colBackPtn = 255
let PITCH_PRESET_IDX = 240 // TODO read from the Project Data section of the .taud
let beatDivPrimary = 4 // TODO read from the Project Data section of the .taud
let beatDivSecondary = 16
let hasUnsavedChanges = false
// pitchSymLut[pitchInOct] = [symString, octaveOffset]
// octaveOffset is 1 when pitchInOct is closer to the next octave's root (wraps up) than to any table entry.
@@ -850,18 +851,22 @@ function drawControlHint() {
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
['WER','ViewMode'],
['WER','View'],
['sep'],
['Sp','Edit'],
['sep'],
['m','Mute'],
['s','Solo'],
['sep'],
['Tab','Panel'],
['Tab','Panel']
// ['sep'],
// ['q','Quit'],
]
let hintElemOrders = [
[`\u008428u\u008429u`,'Nav'],
[`Ent`,'Go to cue'],
['sep'],
['Sp','Edit'],
['sep'],
['Tab','Panel'],
// ['sep'],
@@ -871,14 +876,84 @@ function drawControlHint() {
let hintElemPatterns = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Ptn'],
['sep'],
['Sp','Edit'],
['sep'],
['Tab','Panel'],
// ['sep'],
// ['q','Quit'],
]
let hintElemEditNoteValue = [ // only enabled in viewmode 'E' or in pattern editor
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
[`A${sym.doubledot}G`,'Note'],
[`0${sym.doubledot}9`,'Oct'],
['[]',`Tone\u008418u`],
['sep'],
['#',sym.sharp],
['@','Acc'],
['sep'],
['=','KOff'],
['^','KCut'],
// ['sep'],
// ['Sp','ExitEdit'],
]
let hintElemEditInstValue = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Instrument'],
['sep'],
['Sp','ExitEdit'],
]
let hintElemEditVolEff = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
['h','Set'],
['j','SlideDn'],
['k','SlideUp'],
['u','FineDn'],
['i','FineUp'],
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Val'],
// ['sep'],
// ['Sp','ExitEdit'],
]
let hintElemEditPanEff = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
['h','Set'],
['j','SlideL'],
['k','SlideR'],
['u','FineL'],
['i','FineR'],
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Val'],
// ['sep'],
// ['Sp','ExitEdit'],
]
let hintElemEditFxSym = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
[`0${sym.doubledot}9 A${sym.doubledot}F`,`FxSym`],
['sep'],
['Sp','ExitEdit'],
]
let hintElemEditFxVal = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
[`0${sym.doubledot}9 A${sym.doubledot}F`,`FxVal`],
['sep'],
['Sp','ExitEdit'],
]
const hintElemExternal = [['Tab','Panel']]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemExternal, hintElemExternal]
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
// erase current line
con.move(SCRH, 1)
@@ -1837,9 +1912,36 @@ function makeExternalPanelDraw(progName) {
function drawProjectContents(wo) {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
con.move(PTNVIEW_OFFSET_Y + 2, 3)
con.color_pair(colStatus, 255)
print('[Project settings — not yet implemented]')
let mixerflag = initialTrackerMixerflags
let flagstrbuf = ''
let flagstr = [
['Linear pan','Equal-energy pan'],
['Linear tone','Amiga tone'],
]
for (let i = 0; i < flagstr.length; i++) {
let s = flagstr[i][(mixerflag >>> i) & 1 != 0]
if (i > 0) flagstrbuf += ', ';
flagstrbuf += s
}
let projMeta = {
Filename: fullPathObj.string.split('\\').last(),
Patterns: `${song.numPats}/4095 ($${song.numPats.hex03()})`,
Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`,
Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
Flags: `${flagstrbuf} ($${mixerflag.hex02()})`,
}
Object.entries(projMeta).forEach(([key, value], index) => {
con.move(PTNVIEW_OFFSET_Y + index, 2)
con.color_pair(colStatus, 255); print(key)
con.move(PTNVIEW_OFFSET_Y + index, 12)
con.color_pair(colVoiceHdr, colBLACK); print(value)
})
con.color_pair(colStatus, 255) // reset colour
}
function externalPanelInput(wo, event) {}
@@ -2148,7 +2250,7 @@ function drawGotoPopup(popup, buf) {
const promptStr = prompts[currentPanel] || 'Number:'
con.move(popup.y + 2, popup.x + 2)
con.color_pair(colStatus, colPopupBack)
con.color_pair(colWHITE, colPopupBack)
print(promptStr + ' ')
con.color_pair(230, 240)
print('[' + buf.padEnd(3, '_') + ']')
@@ -2171,8 +2273,8 @@ function applyGoto(num) {
}
function openConfirmQuit() {
const pw = 25
const ph = 5
const pw = 25 + hasUnsavedChanges * 4
const ph = 5 + hasUnsavedChanges
const px = ((SCRW - pw) / 2 | 0) + 1
const py = ((SCRH - ph) / 2 | 0)
@@ -2184,11 +2286,17 @@ function openConfirmQuit() {
popup.drawFrame()
con.move(py + 2, px + 2)
con.color_pair(colStatus, colPopupBack)
con.color_pair(colWHITE, colPopupBack)
print('Exit Microtone? ')
con.color_pair(230, 240)
print('[Y/N]')
if (hasUnsavedChanges) {
con.move(py + 3, px + 2)
con.color_pair(colWHITE, colPopupBack)
print('You have unsaved changes.')
}
con.color_pair(colStatus, 255) // reset colour
let result = false
@@ -2261,6 +2369,7 @@ resetAudioDevice()
taud.uploadTaudFile(fullPathObj.full, 0, PLAYHEAD)
audio.setMasterVolume(PLAYHEAD, 255)
audio.setMasterPan(PLAYHEAD, 128)
const initialTrackerMixerflags = audio.getTrackerMixerFlags(PLAYHEAD)
function isExternalPanel(p) {
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE

Binary file not shown.

View File

@@ -43,9 +43,9 @@ function _pokeU32LE(ptr, off, v) {
*
* @param inFile Full path with drive letter, e.g. "A:/music/song.taud"
* @param songIndex 0-based index of the song in the SONG TABLE
* @param targetPlaydataSlot Playhead number (0-3) to configure
* @param playhead Playhead number (0-3) to configure
*/
function uploadTaudFile(inFile, songIndex, targetPlaydataSlot) {
function uploadTaudFile(inFile, songIndex, playhead) {
const drive = inFile[0].toUpperCase()
const diskPath = inFile.substring(2)
@@ -107,6 +107,7 @@ function uploadTaudFile(inFile, songIndex, targetPlaydataSlot) {
let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF
let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF
let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF
let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF
let bpm = bpmStored + 24
let patsToLoad = numPatsLo | (numPatsHi << 8)
@@ -130,9 +131,10 @@ function uploadTaudFile(inFile, songIndex, targetPlaydataSlot) {
}
// -- 8. Configure playhead ------------------------------------------------
audio.setTrackerMode(targetPlaydataSlot)
audio.setBPM(targetPlaydataSlot, bpm)
audio.setTickRate(targetPlaydataSlot, tickRate > 0 ? tickRate : 6)
audio.setTrackerMode(playhead)
audio.setBPM(playhead, bpm)
audio.setTickRate(playhead, tickRate > 0 ? tickRate : 6)
audio.setTrackerMixerFlags(playhead, mixerflags)
fileHandle.close()

View File

@@ -2022,13 +2022,12 @@ Instrument bin: Registry for 256 instruments, formatted as:
Uint8 Instrument Global Volume (0..255)
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
* FastTracker2 has range of 0..64; multiply by (255/64) then round to int
Bit16x8 Volume envelopes
Bit16x12 Volume envelopes
Byte 1: Volume (00..3F)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
Bit16x8 Panning envelopes
Bit16x12 Panning envelopes
Byte 1: Pan (00..FF)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
Bit16x8 Reserved
Play Data: play data are series of tracker-like instructions, visualised as:

View File

@@ -131,6 +131,14 @@ class AudioJSR223Delegate(private val vm: VM) {
}
}
fun setTrackerMixerFlags(playhead: Int, flags: Int) {
getFirstSnd()?.playheads?.get(playhead)?.initialGlobalFlags = flags
}
fun getTrackerMixerFlags(playhead: Int): Int? {
return getFirstSnd()?.playheads?.get(playhead)?.initialGlobalFlags
}
fun putPcmDataByPtr(playhead: Int, ptr: Int, length: Int, destOffset: Int) {
getFirstSnd()?.let {
val vkMult = if (ptr >= 0) 1 else -1

View File

@@ -1202,8 +1202,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.envTimeSec = 0.0
voice.envIndex = vSusStart
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else if (voice.envIndex >= 7) {
voice.envVolume = (inst.volEnvelopes[7].value / 63.0).coerceIn(0.0, 1.0)
} else if (voice.envIndex >= 11) {
voice.envVolume = (inst.volEnvelopes[11].value / 63.0).coerceIn(0.0, 1.0)
} else {
val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
if (vOffset == 0.0) {
@@ -1213,12 +1213,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.envTimeSec >= vOffset) {
voice.envTimeSec -= vOffset
val nextIdx = if (vSusOn && voice.envIndex == vSusEnd) vSusStart
else (voice.envIndex + 1).coerceAtMost(7)
else (voice.envIndex + 1).coerceAtMost(11)
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)
val nxt = (inst.volEnvelopes[(voice.envIndex + 1).coerceAtMost(11)].value / 63.0).coerceIn(0.0, 1.0)
voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / vOffset)
}
}
@@ -1242,8 +1242,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.envPanTimeSec = 0.0
voice.envPanIndex = pSusStart
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else if (voice.envPanIndex >= 7) {
voice.envPan = inst.panEnvelopes[7].value / 255.0
} else if (voice.envPanIndex >= 11) {
voice.envPan = inst.panEnvelopes[11].value / 255.0
} else {
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
if (pOffset == 0.0) {
@@ -1253,12 +1253,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.envPanTimeSec >= pOffset) {
voice.envPanTimeSec -= pOffset
val nextIdx = if (pSusOn && voice.envPanIndex == pSusEnd) pSusStart
else (voice.envPanIndex + 1).coerceAtMost(7)
else (voice.envPanIndex + 1).coerceAtMost(11)
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
val nxt = inst.panEnvelopes[(voice.envPanIndex + 1).coerceAtMost(11)].value / 255.0
voice.envPan = cur + (nxt - cur) * (voice.envPanTimeSec / pOffset)
}
}
@@ -2336,12 +2336,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var volEnvSustain: Int, // byte 13: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var panEnvSustain: Int, // byte 14: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var instGlobalVolume: Int, // byte 15: instrument global volume (0..255, 255 = unity)
var volEnvelopes: Array<TaudInstEnvPoint>, // 8 points, value 0x00-0x3F
var panEnvelopes: Array<TaudInstEnvPoint> // 8 points, value 0x00-0xFF (0x80 = centre)
var volEnvelopes: Array<TaudInstEnvPoint>, // 12 points, value 0x00-0x3F
var panEnvelopes: Array<TaudInstEnvPoint> // 12 points, value 0x00-0xFF (0x80 = centre)
) {
constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
Array(8) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(8) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
Array(12) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(12) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
// 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.
@@ -2382,11 +2382,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
13 -> volEnvSustain.toByte()
14 -> panEnvSustain.toByte()
15 -> instGlobalVolume.toByte()
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
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value.toByte()
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset.index.toByte()
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value.toByte()
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset.index.toByte()
else -> throw InternalError("Bad offset $offset")
}
@@ -2418,11 +2417,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
14 -> { panEnvSustain = byte }
15 -> { instGlobalVolume = byte and 0xFF }
in 16..30 step 2 -> volEnvelopes[(offset - 16) / 2].value = 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 -> {}
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value = byte
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value = byte
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset = ThreeFiveMiniUfloat(byte)
else -> throw InternalError("Bad offset $offset")
}
}