mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
sound adapter wip
This commit is contained in:
@@ -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
|
||||
55
tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt
Normal file
55
tsvm_core/src/net/torvald/tsvm/ThreeFiveMinifloat.kt
Normal file
@@ -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<Int, Int> {
|
||||
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()
|
||||
|
||||
|
||||
}
|
||||
274
tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt
Normal file
274
tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt
Normal file
@@ -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<TaudInstVolEnv> // 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user