sound adapter wip

This commit is contained in:
minjaesong
2022-12-31 01:30:30 +09:00
parent 34def419c1
commit 884a1ebc7e
3 changed files with 412 additions and 0 deletions

View File

@@ -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

View 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()
}

View 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")
}
}
}