tracker impl, s3m converter, larger tracker sample bin

This commit is contained in:
minjaesong
2026-04-19 02:52:12 +09:00
parent f02ad1de79
commit bef85f6e2f
12 changed files with 1656 additions and 221 deletions

View File

@@ -85,6 +85,107 @@ object TinyAlphNum : BitmapFont() {
private fun isColourCodeHigh(c: Char) = c.toInt() in 0b110110_1111000000..0b110110_1111111111
private fun isColourCodeLow(c: Char) = c.toInt() in 0b110111_0000000000..0b110111_1111111111
private fun getColour(charHigh: Char, charLow: Char): Color { // input: 0x10ARGB, out: RGBA8888
val codePoint = Character.toCodePoint(charHigh, charLow)
if (colourBuffer.containsKey(codePoint))
return colourBuffer[codePoint]!!
val a = codePoint.and(0xF000).ushr(12)
val r = codePoint.and(0x0F00).ushr(8)
val g = codePoint.and(0x00F0).ushr(4)
val b = codePoint.and(0x000F)
val col = Color(r.shl(28) or r.shl(24) or g.shl(20) or g.shl(16) or b.shl(12) or b.shl(8) or a.shl(4) or a)
colourBuffer[codePoint] = col
return col
}
private val colourBuffer = HashMap<Int, Color>()
}
/**
* Created by minjaesong on 2026-04-19.
*/
object PatternView : BitmapFont() {
internal val W = 5
internal val H = 6
internal val fontSheet = TextureRegionPack(Gdx.files.internal("net/torvald/terrarum/imagefont/tiny.tga"), W, H)
init {
setOwnsTexture(true)
setUseIntegerPositions(true)
}
fun getWidth(str: String): Int {
var l = 0
for (char in str) {
if (!isColourCodeHigh(char) && !isColourCodeLow(char)) {
l += 1
}
}
return W * l
}
lateinit var colMain: Color
lateinit var colShadow: Color
override fun draw(batch: Batch, text: CharSequence, x: Float, y: Float): GlyphLayout? {
val originalColour = batch.color.cpy()
colMain = batch.color.cpy()
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
val x = x.roundToInt().toFloat()
val y = y.roundToInt().toFloat()
var charsPrinted = 0
text.forEachIndexed { index, c ->
if (isColourCodeHigh(c)) {
val cchigh = c
val cclow = text[index + 1]
val colour = getColour(cchigh, cclow)
colMain = colour
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
}
else if (c in 0.toChar()..255.toChar()) {
batch.color = colShadow
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y)
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y + 1)
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y + 1)
batch.color = colMain
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y)
charsPrinted += 1
}
}
batch.color = originalColour
return null
}
fun drawRalign(batch: Batch, text: CharSequence, x: Float, y: Float): GlyphLayout? {
return draw(batch, text, x - W*text.length, y)
}
override fun getLineHeight() = H.toFloat()
override fun getCapHeight() = getLineHeight()
override fun getXHeight() = getLineHeight()
private fun isColourCodeHigh(c: Char) = c.toInt() in 0b110110_1111000000..0b110110_1111111111
private fun isColourCodeLow(c: Char) = c.toInt() in 0b110111_0000000000..0b110111_1111111111

Binary file not shown.

View File

@@ -1,6 +1,8 @@
package net.torvald.tsvm
import com.badlogic.gdx.Audio
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input.Buttons
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.reflection.extortField
@@ -9,8 +11,8 @@ import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_ACTIVE3
import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_HIGHLIGHT2
import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_WELL
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.FONT
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.TINY
import net.torvald.tsvm.peripheral.AudioAdapter
import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
@@ -21,19 +23,70 @@ import kotlin.math.roundToInt
*/
class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMenu(parent, x, y, w, h) {
// Per-playhead view mode: 0=detailed pattern, 1=abridged pattern (stub), 2=super-abridged (stub), 3=cuesheet detail
private val scopeMode = IntArray(4)
override fun show() {
}
override fun hide() {
}
private var guiClickLatched = arrayOf(false, false, false, false, false, false, false, false)
override fun update() {
if (Gdx.input.isButtonPressed(Buttons.LEFT)) {
if (!guiClickLatched[Buttons.LEFT]) {
val mx = Gdx.input.x - x
val my = Gdx.input.y - y
if (mx in 117..629) {
for (i in 0..3) {
val syTop = h - 7 - 115 * i - 8 * FONT.H
val syBot = h - 3 - 115 * i
if (my in syTop..syBot) {
scopeMode[3 - i] = (scopeMode[3 - i] + 1) % 4
break
}
}
}
guiClickLatched[Buttons.LEFT] = true
}
}
else {
guiClickLatched[Buttons.LEFT] = false
}
}
private val COL_SOUNDSCOPE_BACK = Color(0x081c08ff.toInt())
private val COL_SOUNDSCOPE_FORE = Color(0x80f782ff.toInt())
private val COL_TRACKER_ROW = Color(0x103010ff.toInt())
private val STR_PLAY = "\u00D2\u00D3"
// Pattern field colours (loosely following MilkyTracker scheme)
private val COL_NOTE = Color(1f, 1f, 1f, 1f) // white
private val COL_INST = Color(0x6BB5FFff.toInt()) // sky blue
private val COL_VOL = Color(0x80FF50ff.toInt()) // lime
private val COL_PAN = Color(0xFFC040ff.toInt()) // amber
private val COL_EFF = Color(0xFF50FFff.toInt()) // magenta
private val COL_EFFARG = Color(0xFFAF7Fff.toInt()) // apricot
// Voice colours for cue-sheet view — 10-colour palette cycling across 20 voices
private val COL_VOICE_PALETTE = arrayOf(
Color(0xC0C0C0ff.toInt()), // 0: silver
Color(0xFF8080ff.toInt()), // 1: salmon
Color(0xFFBF60ff.toInt()), // 2: tangerine
Color(0xFFFF70ff.toInt()), // 3: yellow
Color(0x80FF80ff.toInt()), // 4: lime
Color(0x60EEEEff.toInt()), // 5: aqua
Color(0x80A0FFff.toInt()), // 6: periwinkle
Color(0xD080FFff.toInt()), // 7: orchid
Color(0xFF80C0ff.toInt()), // 8: pink
Color(0xA0D0A0ff.toInt()), // 9: sage
)
override fun render(batch: SpriteBatch) {
@@ -111,7 +164,7 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
FONT.draw(batch, "Tickrate", x, y + 6*FONT.H)
batch.color = COL_ACTIVE3
FONT.drawRalign(batch, "${ahead.position}", x + 84, y + 2*FONT.H)
FONT.drawRalign(batch, "${ahead.trackerState?.cuePos}:${ahead.trackerState?.rowIndex?.toString(16)?.uppercase()?.padStart(2,'0')}", x + 84, y + 2*FONT.H)
FONT.drawRalign(batch, "${ahead.masterVolume}", x + 84, y + 3*FONT.H)
FONT.drawRalign(batch, "${ahead.masterPan}", x + 84, y + 4*FONT.H)
FONT.drawRalign(batch, "${ahead.bpm}", x + 84, y + 5*FONT.H)
@@ -122,9 +175,20 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
fun Int.u16Tos16() = if (this > 32767) this - 65536 else this
private fun readCuePat12(audio: AudioAdapter, ci: Int, vi: Int): Int {
val byteGroup = vi / 2
val shift = if (vi % 2 == 0) 4 else 0
val lo = (audio.mmio_read(32768L + ci * 32 + byteGroup ).toUint() ushr shift) and 0xF
val mid = (audio.mmio_read(32768L + ci * 32 + 10 + byteGroup).toUint() ushr shift) and 0xF
val hi = (audio.mmio_read(32768L + ci * 32 + 20 + byteGroup).toUint() ushr shift) and 0xF
return (hi shl 8) or (mid shl 4) or lo
}
private fun bipolarCeil(d: Double) = (if (d >= 0.0) ceil(d) else floor(d)).toInt()
private fun bipolarFloor(d: Double) = (if (d >= 0.0) floor(d) else ceil(d)).toInt()
private val VOX_PER_VIEW = arrayOf(5,8,16)
private fun drawSoundscope(audio: AudioAdapter, ahead: AudioAdapter.Playhead, batch: SpriteBatch, index: Int, x: Float, y: Float) {
val gdxadev = ahead.audioDevice
val bytes = gdxadev.extortField<ByteArray>("bytes")
@@ -175,7 +239,191 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
}
else {
// Tracker pattern visualiser.
// Modes: 0=detailed pattern, 1=abridged (stub), 2=super-abridged (stub), 3=cuesheet detail
val ts = ahead.trackerState
if (ts == null) {
batch.color = COL_SOUNDSCOPE_FORE
FONT.draw(batch, "No tracker state", x, y + 4)
} else {
val cuePos = ts.cuePos
val rowIdx = ts.rowIndex
val ROWS = 17
val PTN_MAX_ROWS = 63
when (scopeMode[index]) {
// ── Mode 3: Cue-sheet detail ─────────────────────────────────────
3 -> {
// Layout per row: >NNN|p00p01…p19|INS
// Voice pattern numbers are colour-coded; no spaces (colour provides separation).
val cueFirst = (cuePos - ROWS / 2).coerceAtLeast(0).coerceAtMost(1023 - ROWS + 1)
for (r in 0 until ROWS) {
val ci = cueFirst + r
if (ci > 1023) break
val here = ci == cuePos
val ry = y + 4 + r * TINY.H
if (here) {
batch.color = COL_TRACKER_ROW
batch.fillRect(x, ry, 512, TINY.H)
}
var cx = x
// cursor + cue number
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
TINY.draw(batch, "${if (here) ">" else " "}${ci.toString(16).padStart(3, '0').uppercase()}|", cx, ry)
cx += 5 * TINY.W
// voice pattern numbers
for (vi in 0 until 20) {
if (vi > 0) { cx += TINY.W }
val pat = readCuePat12(audio, ci, vi)
val patStr = if (pat == 0xFFF) "---"
else pat.toString(16).padStart(3, '0').uppercase()
batch.color = if (here) Color.WHITE else COL_VOICE_PALETTE[vi % COL_VOICE_PALETTE.size]
TINY.draw(batch, patStr, cx, ry)
cx += 3 * TINY.W
}
// instruction
val instrByte = audio.mmio_read(32768L + ci * 32 + 30).toUint()
val instrStr3 = when {
instrByte == 0x00 -> " " // no-op
instrByte == 0x01 -> "HALT"
instrByte and 0x80 != 0 -> "BACK ${(instrByte and 0x7F).toString(16).padStart(2, '0').uppercase()}"
instrByte and 0xF0 == 0x10 -> "FWRD ${(instrByte and 0x0F).toString(16).uppercase()}"
else -> "?${instrByte.toString(16).padStart(2, '0').uppercase()}"
}
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
TINY.draw(batch, "|$instrStr3", cx, ry)
}
}
// ── Mode 0: Detailed pattern with colour-coded fields ────────────
// ── Mode 1: Abridged pattern with colour-coded fields ────────────
// ── Mode 2: Super-abridged pattern with colour-coded fields ────────────
0, 1, 2 -> {
val cueW = 4 * TINY.W
val sepW = TINY.W
// val patX = x + cueW + sepW
val patX = x
val VOICES = VOX_PER_VIEW[scopeMode[index]]
// Abridged cue sheet (left column, 8 entries centred on current cue)
/*val cueFirst = (cuePos - ROWS / 2).coerceAtLeast(0).coerceAtMost(1023 - ROWS + 1)
for (r in 0 until ROWS) {
val ci = cueFirst + r
if (ci > 1023) break
val here = ci == cuePos
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
TINY.draw(batch,
"${if (here) ">" else " "}${ci.toString(16).padStart(3, '0').uppercase()}",
x, y + 4 + r * TINY.H)
}
// Vertical separator
batch.color = COL_SOUNDSCOPE_FORE
for (r in 0 until ROWS) TINY.draw(batch, "|", x + cueW, y + 4 + r * TINY.H)
*/
// Pattern index for each voice in current cue
val cuePats = IntArray(VOICES) { vi -> readCuePat12(audio, cuePos, vi) }
// Pattern rows (right area, 8 rows centred on current row)
// Layout: > rr NOTE in E.Vo E.Pn Eff ffff [voice1 …]
// 1 2 4 2 4 4 2 4
val rowFirst = (rowIdx - ROWS / 2).coerceAtLeast(0).coerceAtMost(PTN_MAX_ROWS - ROWS + 1)
for (r in 0 until ROWS) {
val ri = rowFirst + r
if (ri > PTN_MAX_ROWS) break
val here = ri == rowIdx
val ry = y + 4 + r * TINY.H
if (here) {
batch.color = COL_TRACKER_ROW
batch.fillRect(patX, ry, 512 - cueW - sepW, TINY.H)
}
var cx = patX
// cursor + row number (drawn once per row)
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
TINY.draw(batch, if (here) ">" else " ", cx, ry)
cx += TINY.W
TINY.draw(batch, ri.toString().padStart(2, '0').uppercase(), cx, ry)
cx += 2 * TINY.W
for (vi in 0 until VOICES) {
val pat12 = cuePats[vi]
if (pat12 == 0xFFF) {
// disabled voice — dimmed placeholder, same width as a live voice
batch.color = COL_SOUNDSCOPE_FORE
TINY.draw(batch, "(NO PATTERN DATA OR REACHED THE END OF THE SONG) ", cx, ry)
} else {
val localPat = pat12 and 0xFF
val base = if (localPat < 128) 786432L + localPat * 512 + ri * 8
else 851968L + (localPat - 128) * 512 + ri * 8
val noteLo = audio.peek(base + 0).toUint()
val noteHi = audio.peek(base + 1).toUint()
val noteVal = noteLo or (noteHi shl 8)
val instr = audio.peek(base + 2).toUint()
val volByte = audio.peek(base + 3).toUint()
val panByte = audio.peek(base + 4).toUint()
val eff = audio.peek(base + 5).toUint()
val eaLo = audio.peek(base + 6).toUint()
val eaHi = audio.peek(base + 7).toUint()
val vol = volByte and 63
val volEff = (volByte ushr 6) and 3
val pan = panByte and 63
val panEff = (panByte ushr 6) and 3
val effArg = eaLo or (eaHi shl 8)
val noteStr = when (noteVal) {
0xFFFF -> "@@@@"
0x0000 -> "===="
0xFFFE -> "^^^^"
else -> noteVal.toString(16).uppercase().padStart(4, '0')
}
// note
batch.color = if (here) Color.WHITE else COL_NOTE
TINY.draw(batch, noteStr, cx, ry)
cx += 4 * TINY.W
// instrument
batch.color = if (here) Color.WHITE else COL_INST
TINY.draw(batch, instr.toString(16).padStart(2, '0').uppercase(), cx, ry)
cx += 2 * TINY.W
if (scopeMode[index] == 0) {
// volume
batch.color = if (here) Color.WHITE else COL_VOL
TINY.draw(batch, "$volEff.${vol.toString().padStart(2, '0')}", cx, ry)
cx += 4 * TINY.W
}
// pan
if (scopeMode[index] == 0) {
batch.color = if (here) Color.WHITE else COL_PAN
TINY.draw(batch, "$panEff.${pan.toString().padStart(2, '0')}", cx, ry)
cx += 4 * TINY.W
}
if (scopeMode[index] < 2) {
// effect opcode
batch.color = if (here) Color.WHITE else COL_EFF
TINY.draw(batch, eff.toString(16).padStart(2, '0').uppercase(), cx, ry)
cx += 2 * TINY.W
// effect argument
batch.color = if (here) Color.WHITE else COL_EFFARG
TINY.draw(batch, effArg.toString(16).padStart(4, '0').uppercase(), cx, ry)
cx += 4 * TINY.W
}
}
}
}
}
}
}
}
}
}
@@ -184,4 +432,4 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
}
}
}

View File

@@ -10,6 +10,7 @@ import com.badlogic.gdx.utils.JsonValue
import com.badlogic.gdx.utils.JsonWriter
import net.torvald.terrarum.DefaultGL32Shaders
import net.torvald.terrarum.FlippingSpriteBatch
import net.torvald.terrarum.imagefont.PatternView
import net.torvald.terrarum.imagefont.TinyAlphNum
import net.torvald.terrarum.utils.JsonFetcher
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.FONT
@@ -25,10 +26,12 @@ class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var pa
companion object {
lateinit var SQTEX: Texture; private set
lateinit var FONT: TinyAlphNum; private set
lateinit var TINY: PatternView; private set
}
override fun create() {
FONT = TinyAlphNum
TINY = PatternView
SQTEX = Texture(Gdx.files.internal("net/torvald/tsvm/sq.tga"))
executable = VMEmuExecutable(windowWidth, windowHeight, panelsX, panelsY, diskPathRoot)
executable.create()
@@ -54,6 +57,8 @@ class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var pa
// println("App Dispose")
executable.dispose()
SQTEX.dispose()
FONT.dispose()
TINY.dispose()
exitProcess(0)
}
}