From 884a1ebc7eb83321c8f099bfc26cfcf0a2bd7054 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 31 Dec 2022 01:30:30 +0900 Subject: [PATCH] sound adapter wip --- terranmon.txt | 83 ++++++ .../net/torvald/tsvm/ThreeFiveMinifloat.kt | 55 ++++ .../torvald/tsvm/peripheral/SoundAdapter.kt | 274 ++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt create mode 100644 tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt diff --git a/terranmon.txt b/terranmon.txt index dbf15ba..d8a2379 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -496,3 +496,86 @@ Image is divided into 4x4 blocks and each block is serialised, then the entire i 11111010 -> 0xFA which packs into: [ 30 | 30 | FA | FA ] (because little endian) + +-------------------------------------------------------------------------------- + +Sound Adapter + +Endianness: little + +0..114687 RW: Sample bin +114688..131071 RW: Instrument bin (256 instruments, 64 bytes each) +131072..196607 RW: Play data 1 +196608..262143 RW: Play data 2 + +Sample bin: just raw sample data thrown in there. You need to keep track of starting point for each sample + +Instrument bin: Registry for 256 instruments, formatted as: + Uint16 Sample Pointer + Uint16 Sample length + Uint16 Sampling rate at C3 + Uint16 Loop start + Uint16 Loop end + Bit16 Flags + 0b h000 00pp + 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) + Bit32 Unused + Bit16x24 Volume envelopes + Byte 1: Volume + Byte 2: Second offset from the prev point, in 3.5 Unsigned Minifloat + +Play Data: play data are series of tracker-like instructions, visualised as: + +rr||NOTE|Ins|E.Vol|E.Pan|EE.ff| +63||FFFF|255|3+ 64|3+ 64|16 FF| (8 bytes per line, 512 bytes per pattern, 256 patterns on 128 kB block) + +notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value. + + +Sound Adapter MMIO + +0..1 RW: Play head #1 position +2 RW: Play head #1 master volume +3 RW: Play head #1 master pan +4..7 RW: Play head #1 flags + +8..9 RW: Play head #2 position +10 RW: Play head #2 master volume +11 RW: Play head #2 master pan +12..15 RW:Play head #2 flags + +... auto-fill to Play head #4 + +32 ??: ??? + + +Play Head Flags + Byte 1 + - 0b m000 0000 + m: mode (0 for Tracker, 1 for PCM) + Byte 2 + - PCM Mode: Sampling rate multiplier in 3.5 Unsigned Minifloat (0.03125x to 126x) + Byte 3 + - BPM (24 to 280. Play Data will change this register; unused in PCM Mode) + Byte 4 + - Tick Rate (Play Data will change this register; unused in PCM Mode) + +Play Head Position interpretion + - Cuesheet Counter for Tracker mode + - Sample Counter for PCM mode + +32768..65535 RW: Cue Sheet (2048 cues) + Byte 1..15: pattern number for voice 1..15 + Byte 16: instruction + 1 xxxxxxx - Go back (128, 1-127) patterns to form a loop + 01 xxxxxx - + 001 xxxxx - + 0001 xxxx - Skip (16, 1-15) patterns + 00001 xxx - + 000001 xx - + 0000001 x - + 0000000 1 - + 0000000 0 - No operation + +65536..131071 RW: PCM Sample buffer \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt b/tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt new file mode 100644 index 0000000..584f795 --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt @@ -0,0 +1,55 @@ +package net.torvald.tsvm + +/** + * Created by minjaesong on 2022-12-30. + */ +inline class ThreeFiveMiniUfloat(val index: Int = 0) { + + init { + if (index and 0xffffff00.toInt() != 0) throw IllegalArgumentException("Index not in 0..255 ($index)") + } + + companion object { + val LUT = floatArrayOf(0f,0.03125f,0.0625f,0.09375f,0.125f,0.15625f,0.1875f,0.21875f,0.25f,0.28125f,0.3125f,0.34375f,0.375f,0.40625f,0.4375f,0.46875f,0.5f,0.53125f,0.5625f,0.59375f,0.625f,0.65625f,0.6875f,0.71875f,0.75f,0.78125f,0.8125f,0.84375f,0.875f,0.90625f,0.9375f,0.96875f,1f,1.03125f,1.0625f,1.09375f,1.125f,1.15625f,1.1875f,1.21875f,1.25f,1.28125f,1.3125f,1.34375f,1.375f,1.40625f,1.4375f,1.46875f,1.5f,1.53125f,1.5625f,1.59375f,1.625f,1.65625f,1.6875f,1.71875f,1.75f,1.78125f,1.8125f,1.84375f,1.875f,1.90625f,1.9375f,1.96875f,2f,2.0625f,2.125f,2.1875f,2.25f,2.3125f,2.375f,2.4375f,2.5f,2.5625f,2.625f,2.6875f,2.75f,2.8125f,2.875f,2.9375f,3f,3.0625f,3.125f,3.1875f,3.25f,3.3125f,3.375f,3.4375f,3.5f,3.5625f,3.625f,3.6875f,3.75f,3.8125f,3.875f,3.9375f,4f,4.125f,4.25f,4.375f,4.5f,4.625f,4.75f,4.875f,5f,5.125f,5.25f,5.375f,5.5f,5.625f,5.75f,5.875f,6f,6.125f,6.25f,6.375f,6.5f,6.625f,6.75f,6.875f,7f,7.125f,7.25f,7.375f,7.5f,7.625f,7.75f,7.875f,8f,8.25f,8.5f,8.75f,9f,9.25f,9.5f,9.75f,10f,10.25f,10.5f,10.75f,11f,11.25f,11.5f,11.75f,12f,12.25f,12.5f,12.75f,13f,13.25f,13.5f,13.75f,14f,14.25f,14.5f,14.75f,15f,15.25f,15.5f,15.75f,16f,16.5f,17f,17.5f,18f,18.5f,19f,19.5f,20f,20.5f,21f,21.5f,22f,22.5f,23f,23.5f,24f,24.5f,25f,25.5f,26f,26.5f,27f,27.5f,28f,28.5f,29f,29.5f,30f,30.5f,31f,31.5f,32f,33f,34f,35f,36f,37f,38f,39f,40f,41f,42f,43f,44f,45f,46f,47f,48f,49f,50f,51f,52f,53f,54f,55f,56f,57f,58f,59f,60f,61f,62f,63f,64f,66f,68f,70f,72f,74f,76f,78f,80f,82f,84f,86f,88f,90f,92f,94f,96f,98f,100f,102f,104f,106f,108f,110f,112f,114f,116f,118f,120f,122f,124f,126f) + + private fun fromFloatToIndex(fval: Float): Int { + val (llim, hlim) = binarySearchInterval(fval, LUT) + return if (llim % 2 == 0) llim else hlim // round to nearest even + } + + /** + * e.g. + * + * 0 2 4 5 7 , find 3 + * + * will return (1, 2), which corresponds value (2, 4) of which input value 3 is in between. + */ + private fun binarySearchInterval(value: Float, array: FloatArray): Pair { + var low: Int = 0 + var high: Int = array.size - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) + val midVal = array[mid] + + if (value < midVal) + high = mid - 1 + else if (value > midVal) + low = mid + 1 + else + return Pair(mid, mid) + } + + val first = Math.max(high, 0) + val second = Math.min(low, array.size - 1) + return Pair(first, second) + } + } + + constructor(fval: Float) : this(fromFloatToIndex(fval)) + + fun toFloat() = LUT[index] + fun toDouble() = LUT[index].toDouble() + + +} \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt new file mode 100644 index 0000000..9c707de --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt @@ -0,0 +1,274 @@ +package net.torvald.tsvm.peripheral + +import net.torvald.UnsafeHelper +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint +import net.torvald.tsvm.ThreeFiveMiniUfloat +import net.torvald.tsvm.VM + +private fun Boolean.toInt() = if (this) 1 else 0 + +/** + * Created by minjaesong on 2022-12-30. + */ +class SoundAdapter(val vm: VM) : PeriBase { + + private val sampleBin = UnsafeHelper.allocate(114687L) + private val instruments = Array(256) { TaudInst() } + private val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } } + private val playheads = Array(4) { Playhead() } + private val cueSheet = Array(2048) { PlayCue() } + private val pcmBin = UnsafeHelper.allocate(65536L) + + override fun peek(addr: Long): Byte { + return when (val adi = addr.toInt()) { + in 0..114687 -> sampleBin[addr] + in 114688..131071 -> (adi - 114688).let { instruments[it / 64].getByte(it % 64) } + in 131072..262143 -> (adi - 131072).let { playdata[it / (8*64)][(it / 8) % 64].getByte(it % 8) } + else -> peek(addr % 262144) + } + } + + override fun poke(addr: Long, byte: Byte) { + val adi = addr.toInt() + val bi = byte.toUint() + when (adi) { + in 0..114687 -> { sampleBin[addr] = byte } + in 114688..131071 -> (adi - 114688).let { instruments[it / 64].setByte(it % 64, bi) } + in 131072..262143 -> (adi - 131072).let { playdata[it / (8*64)][(it / 8) % 64].setByte(it % 8, bi) } + } + } + + override fun mmio_read(addr: Long): Byte { + val adi = addr.toInt() + return when (adi) { + in 0..15 -> playheads[0].read(adi) + in 16..31 -> playheads[1].read(adi - 16) + in 32..47 -> playheads[2].read(adi - 32) + in 48..63 -> playheads[3].read(adi - 48) + in 32768..65535 -> (adi - 32768).let { + cueSheet[it / 16].read(it % 15) + } + in 65536..131071 -> pcmBin[addr - 65536] + else -> mmio_read(addr % 131072) + } + } + + override fun mmio_write(addr: Long, byte: Byte) { + val adi = addr.toInt() + val bi = byte.toUint() + when (adi) { + in 0..15 -> { playheads[0].write(adi, bi) } + in 16..31 -> { playheads[1].write(adi - 16, bi) } + in 32..47 -> { playheads[2].write(adi - 32, bi) } + in 48..63 -> { playheads[3].write(adi - 48, bi) } + in 32768..65535 -> { (adi - 32768).let { + cueSheet[it / 16].write(it % 15, bi) + } } + in 65536..131071 -> { pcmBin[addr - 65536] = byte } + } + } + + override fun dispose() { + sampleBin.destroy() + pcmBin.destroy() + } + + override fun getVM(): VM { + return vm + } + + /** + * Put this function into a separate thread and keep track of the delta time by yourself + */ + open fun render(delta: Float) { + + } + + override val typestring = "AUDI" + + + + + + private data class PlayCue( + val patterns: IntArray = IntArray(15) { it }, + var instruction: PlayInstruction = PlayInstNop + ) { + fun write(index: Int, byte: Int) = when (index) { + in 0..14 -> { patterns[index] = byte } + 15 -> { instruction = when (byte) { + in 128..255 -> PlayInstGoBack(byte and 127) +// in 64..127 -> Inst(byte and 63) +// in 32..63 -> Inst(byte and 31) + in 16..31 -> PlayInstSkip(byte and 15) +// in 8..15 -> Inst(byte and 7) +// in 4..7 -> Inst(byte and 3) +// in 2..3 -> Inst(byte and 1) +// 1 -> Inst() + 0 -> PlayInstNop + else -> throw InternalError("Bad offset $index") + } } + else -> throw InternalError("Bad offset $index") + } + fun read(index: Int): Byte = when(index) { + in 0..14 -> patterns[index].toByte() + 15 -> { + when (instruction) { + is PlayInstGoBack -> (0b10000000 or instruction.arg).toByte() + is PlayInstSkip -> (0b00010000 or instruction.arg).toByte() + is PlayInstNop -> 0 + else -> throw InternalError("Bad instruction ${instruction.javaClass.simpleName}") + } + } + else -> throw InternalError("Bad offset $index") + } + } + + private open class PlayInstruction(val arg: Int) + private class PlayInstGoBack(arg: Int) : PlayInstruction(arg) + private class PlayInstSkip(arg: Int) : PlayInstruction(arg) + private object PlayInstNop : PlayInstruction(0) + + private data class Playhead( + var position: Int = 0, + var masterVolume: Int = 0, + var masterPan: Int = 0, + // flags + var isPcmMode: Boolean = false, + var loopMode: Int = 0, + var samplingRateMult: ThreeFiveMiniUfloat = ThreeFiveMiniUfloat(32), + var bpm: Int = 120, // "stored" as 96 + var tickRate: Int = 6 + ) { + fun read(index: Int): Byte = when (index) { + 0 -> position.toByte() + 1 -> position.ushr(8).toByte() + 2 -> masterVolume.toByte() + 3 -> masterPan.toByte() + 4 -> (isPcmMode.toInt().shl(7) or loopMode.and(3)).toByte() + 5 -> samplingRateMult.index.toByte() + 6 -> (bpm - 24).toByte() + 7 -> tickRate.toByte() + else -> throw InternalError("Bad offset $index") + } + + fun write(index: Int, byte: Int) = when (index) { + 0 -> { position = position.and(0xff00) or position } + 1 -> { position = position.and(0x00ff) or position.shl(8) } + 2 -> { masterVolume = byte } + 3 -> { masterPan = byte } + 4 -> { byte.let { + isPcmMode = (it and 0b10000000) != 0 + loopMode = (it and 3) + } } + 5 -> { samplingRateMult = ThreeFiveMiniUfloat(byte) } + 6 -> { bpm = byte + 24 } + 7 -> { tickRate = byte } + else -> throw InternalError("Bad offset $index") + } + } + + private data class TaudPlayData( + var note: Int, // 0..65535 + var instrment: Int, // 0..255 + var volume: Int, // 0..63 + var volumeEff: Int, // 0..3 + var pan: Int, // 0..63 + var panEff: Int, // 0..3 + var effect: Int, // 0..255 + var effectArg: Int // 0..65535 + ) { + fun getByte(offset: Int): Byte = when (offset) { + 0 -> note.toByte() + 1 -> note.ushr(8).toByte() + 2 -> instrment.toByte() + 3 -> (volume or volumeEff.shl(6)).toByte() + 4 -> (pan or panEff.shl(6)).toByte() + 5 -> effect.toByte() + 6 -> effectArg.toByte() + 7 -> effectArg.ushr(8).toByte() + else -> throw InternalError("Bad offset $offset") + } + + fun setByte(offset: Int, byte: Int) = when (offset) { + 0 -> { note = note.and(0xff00) or byte } + 1 -> { note = note.and(0x00ff) or byte.shl(8) } + 2 -> { instrment = byte } + 3 -> { volume = byte.and(63); volumeEff = byte.ushr(6).and(3) } + 4 -> { pan = byte.and(63); panEff = byte.ushr(6).and(3) } + 5 -> { effect = byte } + 6 -> { effectArg = effectArg.and(0xff00) or byte } + 7 -> { effectArg = effectArg.and(0x00ff) or byte.shl(8) } + else -> throw InternalError("Bad offset $offset") + } + + } + + private data class TaudInstVolEnv(var volume: Int, var offset: ThreeFiveMiniUfloat) + private data class TaudInst( + var samplePtr: Int, // 17-bit number + var sampleLength: Int, + var samplingRate: Int, + var sampleLoopStart: Int, + var sampleLoopEnd: Int, + // flags + var loopMode: Int, + var envelopes: Array // first int: volume (0..255), second int: offsets (minifloat indices) + ) { + constructor() : this(0, 0, 0, 0, 0, 0, Array(24) { TaudInstVolEnv(0, ThreeFiveMiniUfloat(0)) }) + + fun getByte(offset: Int): Byte = when (offset) { + 0 -> samplePtr.toByte() + 1 -> samplePtr.ushr(8).toByte() + + 2 -> sampleLength.toByte() + 3 -> sampleLength.ushr(8).toByte() + + 4 -> samplingRate.toByte() + 5 -> samplingRate.ushr(8).toByte() + + 6 -> sampleLoopStart.toByte() + 7 -> sampleLoopStart.ushr(8).toByte() + + 8 -> sampleLoopEnd.toByte() + 9 -> sampleLoopEnd.ushr(8).toByte() + + 10 -> (samplePtr.ushr(16).and(1).shl(7) or loopMode.and(3)).toByte() + 11,12,13,14,15 -> -1 + in 16..63 step 2 -> envelopes[offset - 16].volume.toByte() + in 17..63 step 2 -> envelopes[offset - 16].offset.index.toByte() + else -> throw InternalError("Bad offset $offset") + } + + fun setByte(offset: Int, byte: Int) = when (offset) { + 0 -> { samplePtr = samplePtr.and(0x1ff00) or byte } + 1 -> { samplePtr = samplePtr.and(0x000ff) or byte.shl(8) } + + 2 -> { sampleLength = sampleLength.and(0x1ff00) or byte } + 3 -> { sampleLength = sampleLength.and(0x000ff) or byte.shl(8) } + + 4 -> { samplingRate = samplingRate.and(0x1ff00) or byte } + 5 -> { samplingRate = samplingRate.and(0x000ff) or byte.shl(8) } + + 6 -> { sampleLoopStart = sampleLoopStart.and(0x1ff00) or byte } + 7 -> { sampleLoopStart = sampleLoopStart.and(0x000ff) or byte.shl(8) } + + 8 -> { sampleLoopEnd = sampleLoopEnd.and(0x1ff00) or byte } + 9 -> { sampleLoopEnd = sampleLoopEnd.and(0x000ff) or byte.shl(8) } + + 10 -> { + if (byte.and(0b1000_0000) != 0) + samplePtr = samplePtr or 0x10000 + + loopMode = byte.and(3) + } + + in 16..63 step 2 -> envelopes[offset - 16].volume = byte + in 17..63 step 2 -> envelopes[offset - 16].offset = ThreeFiveMiniUfloat(byte) + else -> throw InternalError("Bad offset $offset") + } + } + + + +} \ No newline at end of file