From bb7bf2c6f14e9da2f92ff42ededa7ca9aabfac33 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 1 Jan 2023 02:02:34 +0900 Subject: [PATCH] one idea down, one last idea to go --- assets/disk0/home/soundtest.js | 130 ++++++++++ assets/disk0/tvdos/bin/command.js | 4 +- terranmon.txt | 32 ++- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 58 +++++ tsvm_core/src/net/torvald/tsvm/VM.kt | 1 + .../src/net/torvald/tsvm/VMRunnerFactory.kt | 1 + .../{SoundAdapter.kt => AudioAdapter.kt} | 196 +++++++++++---- .../peripheral/OpenALBufferedAudioDevice.kt | 224 ++++++++++++++++++ .../torvald/tsvm/peripheral/TestDiskDrive.kt | 15 ++ 9 files changed, 607 insertions(+), 54 deletions(-) create mode 100644 assets/disk0/home/soundtest.js create mode 100644 tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt rename tsvm_core/src/net/torvald/tsvm/peripheral/{SoundAdapter.kt => AudioAdapter.kt} (56%) create mode 100644 tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt diff --git a/assets/disk0/home/soundtest.js b/assets/disk0/home/soundtest.js new file mode 100644 index 0000000..b7f8e87 --- /dev/null +++ b/assets/disk0/home/soundtest.js @@ -0,0 +1,130 @@ +/*let music = files.open("A:/orstphone-hdma.bytes") +let samples = sys.malloc(65536) +music.pread(samples, 65534) + +audio.setPcmMode(0) +audio.setMasterVolume(0, 255) +audio.putPcmDataByPtr(samples, 65534, 0) +audio.setLoopPoint(0, 65534) +audio.play(0)*/ + +let filename = exec_args[1] +const port = _TVDOS.DRV.FS.SERIAL._toPorts("A")[0] + +let filex = files.open(_G.shell.resolvePathInput(filename).full) +const FILE_SIZE = filex.size + + + +println("Reading...") +serial.println("!!! READING") + +com.sendMessage(port, "DEVRST\x17") +com.sendMessage(port, `OPENR"${filename}",1`) +let statusCode = com.getStatusCode(port) + +if (statusCode != 0) { + printerrln(`No such file (${statusCode})`) + return statusCode +} + +com.sendMessage(port, "READ") +statusCode = com.getStatusCode(port) +if (statusCode != 0) { + printerrln("READ failed with "+statusCode) + return statusCode +} + + + +let readCount = 0 +function readBytes(length) { + let ptr = sys.malloc(length) + let requiredBlocks = Math.floor((readCount + length) / 4096) - Math.floor(readCount / 4096) + + let completedReads = 0 + +// serial.println(`readBytes(${length}); readCount = ${readCount}`) + + for (let bc = 0; bc < requiredBlocks + 1; bc++) { + if (completedReads >= length) break + + if (readCount % 4096 == 0) { +// serial.println("READ from serial") + // pull the actual message + sys.poke(-4093 - port, 6);sys.sleep(0) // spinning is required as Graal run is desynced with the Java side + + let blockTransferStatus = ((sys.peek(-4085 - port*2) & 255) | ((sys.peek(-4086 - port*2) & 255) << 8)) + let thisBlockLen = blockTransferStatus & 4095 + if (thisBlockLen == 0) thisBlockLen = 4096 // [1, 4096] + let hasMore = (blockTransferStatus & 0x8000 != 0) + + +// serial.println(`block: (${thisBlockLen})[${[...Array(thisBlockLen).keys()].map(k => (sys.peek(-4097 - k) & 255).toString(16).padStart(2,'0')).join()}]`) + + let remaining = Math.min(thisBlockLen, length - completedReads) + +// serial.println(`Pulled a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}, remaining = ${remaining}`) + + // copy from read buffer to designated position + sys.memcpy(-4097, ptr + completedReads, remaining) + + // increment readCount properly + readCount += remaining + completedReads += remaining + } + else { + let padding = readCount % 4096 + let remaining = length - completedReads + let thisBlockLen = Math.min(4096 - padding, length - completedReads) + +// serial.println(`padding = ${padding}; remaining = ${remaining}`) +// serial.println(`block: (${thisBlockLen})[${[...Array(thisBlockLen).keys()].map(k => (sys.peek(-4097 - padding - k) & 255).toString(16).padStart(2,'0')).join()}]`) +// serial.println(`Reusing a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}`) + + // copy from read buffer to designated position + sys.memcpy(-4097 - padding, ptr + completedReads, thisBlockLen) + + // increment readCount properly + readCount += thisBlockLen + completedReads += thisBlockLen + } + } + + //serial.println(`END readBytes(${length}); readCount = ${readCount}\n`) + + return ptr +} + +let sampleSize = FILE_SIZE +const FETCH_INTERVAL = 631578947 +let updateAkku = FETCH_INTERVAL +let oldNanoTime = sys.nanoTime() + +const BLOCK_SIZE = 37894 + +audio.setPcmMode(0) +audio.setMasterVolume(0, 255) + +while (sampleSize > 0) { + let newNanoTime = sys.nanoTime() + updateAkku += newNanoTime - oldNanoTime + oldNanoTime = newNanoTime + + if (updateAkku >= FETCH_INTERVAL) { + println((FILE_SIZE - sampleSize) / FILE_SIZE * 100 + "%") + updateAkku -= FETCH_INTERVAL + + let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE + let samples = readBytes(readLength) + + audio.setUploadLength(0, readLength) + audio.putPcmDataByPtr(samples, readLength, 0) + audio.play(0) + + sampleSize -= readLength + sys.free(samples) + } + + sys.spin() +} \ No newline at end of file diff --git a/assets/disk0/tvdos/bin/command.js b/assets/disk0/tvdos/bin/command.js index e293580..e790025 100644 --- a/assets/disk0/tvdos/bin/command.js +++ b/assets/disk0/tvdos/bin/command.js @@ -5,7 +5,7 @@ let shell_pwd = [""] let goInteractive = false let goFancy = false -let DEBUG_PRINT = true +let DEBUG_PRINT = false let errorlevel = 0 @@ -723,7 +723,7 @@ shell.execute = function(line) { if (!gotError && (errorlevel == undefined || (typeof errorlevel.trim == "function" && errorlevel.trim().length == 0) || isNaN(errorlevel))) errorlevel = 0 - serial.printerr(`errorlevel: ${errorlevel}`) + debugprintln(`errorlevel: ${errorlevel}`) _G.shellProgramTitles.pop() sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1]) diff --git a/terranmon.txt b/terranmon.txt index 8b80c73..d08a7e4 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -536,36 +536,46 @@ notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using t 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 +2..3 RW: Play head #1 length param +4 RW: Play head #1 master volume +5 RW: Play head #1 master pan +6..9 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 +10..11 RW:Play head #2 position +12..13 RW:Play head #2 length param +14 RW: Play head #2 master volume +15 RW: Play head #2 master pan +16..19 RW:Play head #2 flags ... auto-fill to Play head #4 32 ??: ??? +Play Head Position + - Cuesheet Counter for Tracker mode + - Numbers of processed internal buffers (0-3) + when the number is 3, get ready to upload more samples + +Length Param + PCM Mode: length of the samples to upload to the speaker + Tracker mode: unused Play Head Flags Byte 1 - - 0b m000 0000 + - 0b m00p 0000 m: mode (0 for Tracker, 1 for PCM) + p: play (0 if not -- mute all output) Byte 2 - PCM Mode: Sampling rate multiplier in 3.5 Unsigned Minifloat (0.03125x to 126x) + Byte 3 (Tracker Mode) - BPM (24 to 280. Play Data will change this register) Byte 4 (Tracker Mode) - Tick Rate (Play Data will change this register) + Byte 3-4 (PCM Mode) - Signed Int16 Sampling rate difference from 30000 Hz -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 diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt new file mode 100644 index 0000000..1bb81b6 --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -0,0 +1,58 @@ +package net.torvald.tsvm + +import net.torvald.tsvm.peripheral.AudioAdapter + +/** + * Created by minjaesong on 2022-12-31. + */ +class AudioJSR223Delegate(private val vm: VM) { + + private fun getFirstSnd(): AudioAdapter? = vm.findPeribyType(VM.PERITYPE_SOUND)?.peripheral as? AudioAdapter + private fun getPlayhead(playhead: Int) = getFirstSnd()?.playheads?.get(playhead)!! + + fun setPcmMode(playhead: Int) { getPlayhead(playhead)?.isPcmMode = true } + fun isPcmMode(playhead: Int) = getPlayhead(playhead)?.isPcmMode == true + + fun setTrackerMode(playhead: Int) { getPlayhead(playhead)?.isPcmMode = false } + fun isTrackerMode(playhead: Int) = getPlayhead(playhead)?.isPcmMode == false + + fun setMasterVolume(playhead: Int, volume: Int) { getPlayhead(playhead)?.masterVolume = volume and 255 } + fun getMasterVolume(playhead: Int) = getPlayhead(playhead)?.masterVolume + + fun setMasterPan(playhead: Int, pan: Int) { getPlayhead(playhead)?.masterPan = pan and 255 } + fun getMasterPan(playhead: Int) = getPlayhead(playhead)?.masterPan + + fun play(playhead: Int) { getPlayhead(playhead)?.isPlaying = true } + fun stop(playhead: Int) { getPlayhead(playhead)?.isPlaying = false } + fun isPlaying(playhead: Int) = getPlayhead(playhead)?.isPlaying + + fun setPosition(playhead: Int, pos: Int) { getPlayhead(playhead)?.position = pos and 65535 } + fun getPosition(playhead: Int) = getPlayhead(playhead)?.position + + fun setUploadLength(playhead: Int, pos: Int) { getPlayhead(playhead)?.pcmUploadLength = pos and 65535 } + fun setUploadLength(playhead: Int) = getPlayhead(playhead)?.pcmUploadLength + + fun setSamplingRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.setSamplingRate(rate) } + fun getSamplingRate(playhead: Int) = getPlayhead(playhead)?.getSamplingRate() + + fun setSamplingRateMult(playhead: Int, mult: Float) { getPlayhead(playhead)?.samplingRateMult = ThreeFiveMiniUfloat(mult) } + fun getSamplingRateMult(playhead: Int) = getPlayhead(playhead)?.samplingRateMult?.toFloat() + + fun setBPM(playhead: Int, bpm: Int) { getPlayhead(playhead)?.bpm = (bpm - 24).and(255) + 24 } + fun getBPM(playhead: Int) = getPlayhead(playhead)?.bpm + + fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 } + fun getTickRate(playhead: Int) = getPlayhead(playhead)?.tickRate + + fun putPcmDataByPtr(ptr: Int, length: Int, destOffset: Int) { + getFirstSnd()?.let { + val vkMult = if (ptr >= 0) 1 else -1 + for (k in 0L until length) { + val vk = k * vkMult + it.pcmBin[k + destOffset] = vm.peek(ptr + vk)!! + } + } + } + fun getPcmData(index: Int) = getFirstSnd()?.pcmBin?.get(index.toLong()) + +} \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/VM.kt b/tsvm_core/src/net/torvald/tsvm/VM.kt index 08fcffd..feb70af 100644 --- a/tsvm_core/src/net/torvald/tsvm/VM.kt +++ b/tsvm_core/src/net/torvald/tsvm/VM.kt @@ -149,6 +149,7 @@ class VM( val USER_SPACE_SIZE = 8192.kB() const val PERITYPE_GPU_AND_TERM = "gpu" + const val PERITYPE_SOUND = "snd" } internal fun translateAddr(addr: Long): Pair { diff --git a/tsvm_core/src/net/torvald/tsvm/VMRunnerFactory.kt b/tsvm_core/src/net/torvald/tsvm/VMRunnerFactory.kt index a6e4cda..bb88bbc 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMRunnerFactory.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMRunnerFactory.kt @@ -75,6 +75,7 @@ object VMRunnerFactory { bind.putMember("base64", Base64Delegate) bind.putMember("com", SerialHelperDelegate(vm)) bind.putMember("dma", DMADelegate(vm)) + bind.putMember("audio", AudioJSR223Delegate(vm)) bind.putMember("parallel", ringOneParallel) val fr = FileReader("$assetsRoot/JS_INIT.js") diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt similarity index 56% rename from tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt rename to tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 9c707de..c07d94c 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/SoundAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -1,23 +1,102 @@ package net.torvald.tsvm.peripheral +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.audio.AudioDevice +import com.badlogic.gdx.backends.lwjgl3.audio.OpenALAudioDevice +import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio import net.torvald.UnsafeHelper import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint import net.torvald.tsvm.ThreeFiveMiniUfloat import net.torvald.tsvm.VM +import org.lwjgl.openal.AL11 +import java.nio.ByteBuffer private fun Boolean.toInt() = if (this) 1 else 0 /** * Created by minjaesong on 2022-12-30. */ -class SoundAdapter(val vm: VM) : PeriBase { +class AudioAdapter(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) + companion object { + const val SAMPLING_RATE = 30000 + } + + internal val sampleBin = UnsafeHelper.allocate(114687L) + internal val instruments = Array(256) { TaudInst() } + internal val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } } + internal val playheads = Array(4) { Playhead() } + internal val cueSheet = Array(2048) { PlayCue() } + internal val pcmBin = UnsafeHelper.allocate(65536L) + + private lateinit var audioDevices: Array + private val renderThreads = Array(4) { Thread(getRenderFun(it)) } + + /*private val alSources = Array(4) { + val audioField = OpenALAudioDevice::class.java.getDeclaredField("audio") + audioField.isAccessible = true + val audio = audioField.get(audioDevices[it]) as OpenALLwjgl3Audio + + val obtainSourceMethod = OpenALLwjgl3Audio::class.java.getDeclaredMethod("obtainSource", java.lang.Boolean.TYPE) + obtainSourceMethod.isAccessible = true + val alSource = obtainSourceMethod.invoke(audio, true) as Int + + alSource + } + + private val alBuffers = Array(4) { + val buffers = IntArray(3) + AL11.alGenBuffers(buffers) + buffers + } + + private fun freeAlSources() { + audioDevices.forEachIndexed { index, adev -> + val audioField = OpenALAudioDevice::class.java.getDeclaredField("audio") + audioField.isAccessible = true + val audio = audioField.get(adev) as OpenALLwjgl3Audio + + val freeSourceMethod = OpenALLwjgl3Audio::class.java.getDeclaredMethod("freeSource", java.lang.Integer.TYPE) + freeSourceMethod.isAccessible = true + freeSourceMethod.invoke(audio, alSources[index]) + } + } + + private fun enqueuePacket(alSource: Int, alBuffer: Int, data: ByteBuffer) { + AL11.alBufferData(alBuffer, AL11.AL_FORMAT_STEREO8, data, SAMPLING_RATE) + AL11.alSourceQueueBuffers(alSource, alBuffer) + + }*/ + + private val pcmCurrentPosInSamples = ShortArray(4) + + private var pcmPlaybackWatchdogs = Array(4) { Thread { + + } } + + private fun getRenderFun(pheadNum: Int): () -> Unit = { while (true) { + render(playheads[pheadNum], pheadNum) + Thread.sleep(1) + } } + + init { + + val deviceBufferSize = Gdx.audio.javaClass.getDeclaredField("deviceBufferSize").let { + it.isAccessible = true + it.get(Gdx.audio) as Int + } + val deviceBufferCount = Gdx.audio.javaClass.getDeclaredField("deviceBufferCount").let { + it.isAccessible = true + it.get(Gdx.audio) as Int + } + + audioDevices = Array(4) { OpenALBufferedAudioDevice(Gdx.audio as OpenALLwjgl3Audio, SAMPLING_RATE, false, deviceBufferSize, deviceBufferCount) } + + +// println("AudioAdapter latency: ${audioDevice.latency}") + renderThreads.forEach { it.start() } + + } override fun peek(addr: Long): Byte { return when (val adi = addr.toInt()) { @@ -41,10 +120,10 @@ class SoundAdapter(val vm: VM) : PeriBase { 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 0..9 -> playheads[0].read(adi) + in 10..19 -> playheads[1].read(adi - 10) + in 20..29 -> playheads[2].read(adi - 20) + in 30..39 -> playheads[3].read(adi - 30) in 32768..65535 -> (adi - 32768).let { cueSheet[it / 16].read(it % 15) } @@ -57,10 +136,10 @@ class SoundAdapter(val vm: VM) : PeriBase { 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 0..9 -> { playheads[0].write(adi, bi) } + in 10..19 -> { playheads[1].write(adi - 10, bi) } + in 20..29 -> { playheads[2].write(adi - 20, bi) } + in 30..39 -> { playheads[3].write(adi - 30, bi) } in 32768..65535 -> { (adi - 32768).let { cueSheet[it / 16].write(it % 15, bi) } } @@ -69,6 +148,9 @@ class SoundAdapter(val vm: VM) : PeriBase { } override fun dispose() { + renderThreads.forEach { it.interrupt() } + audioDevices.forEach { it.dispose() } +// freeAlSources() sampleBin.destroy() pcmBin.destroy() } @@ -80,17 +162,37 @@ class SoundAdapter(val vm: VM) : PeriBase { /** * Put this function into a separate thread and keep track of the delta time by yourself */ - open fun render(delta: Float) { + private fun render(it: Playhead, pheadNum: Int) { + if (it.isPcmMode) { + if (it.isPlaying) { + audioDevices[pheadNum].setVolume(it.masterVolume / 255f) + + if (it.pcmUploadLength > 0) { + val samples = FloatArray(it.pcmUploadLength) { pcmBin[it.toLong()].toUint().div(255f) * 2f - 1f } + + println("[AudioAdapter] P${pheadNum+1} Vol ${it.masterVolume}; LpP ${it.pcmUploadLength}; start playback...") +// println("[AudioAdapter] "+(0..42).joinToString { String.format("%.2f", samples[it]) }) + + if (it.masterVolume == 0) System.err.println("[AudioAdapter] P${pheadNum+1} volume is zero!") + + audioDevices[pheadNum].writeSamples(samples, 0, it.pcmUploadLength) + + println("[AudioAdapter] P${pheadNum+1} go back to spinning") + + } + + it.isPlaying = false + } + } } - override val typestring = "AUDI" + override val typestring = VM.PERITYPE_SOUND - - private data class PlayCue( + internal data class PlayCue( val patterns: IntArray = IntArray(15) { it }, var instruction: PlayInstruction = PlayInstNop ) { @@ -124,18 +226,19 @@ class SoundAdapter(val vm: VM) : PeriBase { } } - 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) + internal open class PlayInstruction(val arg: Int) + internal class PlayInstGoBack(arg: Int) : PlayInstruction(arg) + internal class PlayInstSkip(arg: Int) : PlayInstruction(arg) + internal object PlayInstNop : PlayInstruction(0) - private data class Playhead( + internal data class Playhead( var position: Int = 0, + var pcmUploadLength: Int = 0, var masterVolume: Int = 0, var masterPan: Int = 0, // flags var isPcmMode: Boolean = false, - var loopMode: Int = 0, + var isPlaying: Boolean = false, var samplingRateMult: ThreeFiveMiniUfloat = ThreeFiveMiniUfloat(32), var bpm: Int = 120, // "stored" as 96 var tickRate: Int = 6 @@ -143,32 +246,43 @@ class SoundAdapter(val vm: VM) : PeriBase { 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() + 2 -> pcmUploadLength.toByte() + 3 -> pcmUploadLength.ushr(8).toByte() + 4 -> masterVolume.toByte() + 5 -> masterPan.toByte() + 6 -> (isPcmMode.toInt().shl(7) or isPlaying.toInt().shl(4)).toByte() + 7 -> samplingRateMult.index.toByte() + 8 -> (bpm - 24).toByte() + 9 -> 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 { + 2 -> { pcmUploadLength = pcmUploadLength.and(0xff00) or pcmUploadLength } + 3 -> { pcmUploadLength = pcmUploadLength.and(0x00ff) or pcmUploadLength.shl(8) } + 4 -> { masterVolume = byte } + 5 -> { masterPan = byte } + 6 -> { byte.let { isPcmMode = (it and 0b10000000) != 0 - loopMode = (it and 3) + isPlaying = (it and 0b00010000) != 0 } } - 5 -> { samplingRateMult = ThreeFiveMiniUfloat(byte) } - 6 -> { bpm = byte + 24 } - 7 -> { tickRate = byte } + 7 -> { samplingRateMult = ThreeFiveMiniUfloat(byte) } + 8 -> { bpm = byte + 24 } + 9 -> { tickRate = byte } else -> throw InternalError("Bad offset $index") } + + fun getSamplingRate() = 30000 - ((bpm - 24).and(255) or tickRate.and(255).shl(8)).toShort().toInt() + fun setSamplingRate(rate: Int) { + val rateDiff = (rate.coerceIn(0, 95535) - 30000).toShort().toInt() + bpm = rateDiff.and(255) + 24 + tickRate = rateDiff.ushr(8).and(255) + } } - private data class TaudPlayData( + internal data class TaudPlayData( var note: Int, // 0..65535 var instrment: Int, // 0..255 var volume: Int, // 0..63 @@ -204,8 +318,8 @@ class SoundAdapter(val vm: VM) : PeriBase { } - private data class TaudInstVolEnv(var volume: Int, var offset: ThreeFiveMiniUfloat) - private data class TaudInst( + internal data class TaudInstVolEnv(var volume: Int, var offset: ThreeFiveMiniUfloat) + internal data class TaudInst( var samplePtr: Int, // 17-bit number var sampleLength: Int, var samplingRate: Int, diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt new file mode 100644 index 0000000..6a2f4ac --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt @@ -0,0 +1,224 @@ +package net.torvald.tsvm.peripheral + +import com.badlogic.gdx.audio.AudioDevice +import com.badlogic.gdx.backends.lwjgl3.audio.OpenALAudioDevice +import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio +import com.badlogic.gdx.math.MathUtils +import com.badlogic.gdx.utils.GdxRuntimeException +import org.lwjgl.BufferUtils +import org.lwjgl.openal.AL10 +import org.lwjgl.openal.AL11 +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.IntBuffer + +/** + * Created by minjaesong on 2023-01-01. + */ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** @author Nathan Sweet + */ +class OpenALBufferedAudioDevice( + private val audio: OpenALLwjgl3Audio, + val rate: Int, + isMono: Boolean, + private val bufferSize: Int, + private val bufferCount: Int +) : AudioDevice { + private val channels: Int + private var buffers: IntBuffer? = null + private var sourceID = -1 + private val format: Int + private var isPlaying = false + private var volume = 1f + private var renderedSeconds = 0f + private val secondsPerBuffer: Float + private var bytes: ByteArray? = null + private val tempBuffer: ByteBuffer + + init { + channels = if (isMono) 1 else 2 + format = if (channels > 1) AL10.AL_FORMAT_STEREO16 else AL10.AL_FORMAT_MONO16 + secondsPerBuffer = bufferSize.toFloat() / bytesPerSample / channels / rate + tempBuffer = BufferUtils.createByteBuffer(bufferSize) + } + + override fun writeSamples(samples: ShortArray, offset: Int, numSamples: Int) { + if (bytes == null || bytes!!.size < numSamples * 2) bytes = ByteArray(numSamples * 2) + val end = Math.min(offset + numSamples, samples.size) + var i = offset + var ii = 0 + while (i < end) { + val sample = samples[i] + bytes!![ii++] = (sample.toInt() and 0xFF).toByte() + bytes!![ii++] = (sample.toInt() shr 8 and 0xFF).toByte() + i++ + } + writeSamples(bytes!!, 0, numSamples * 2) + } + + override fun writeSamples(samples: FloatArray, offset: Int, numSamples: Int) { + if (bytes == null || bytes!!.size < numSamples * 2) bytes = ByteArray(numSamples * 2) + val end = Math.min(offset + numSamples, samples.size) + var i = offset + var ii = 0 + while (i < end) { + var floatSample = samples[i] + floatSample = MathUtils.clamp(floatSample, -1f, 1f) + val intSample = (floatSample * 32767).toInt() + bytes!![ii++] = (intSample and 0xFF).toByte() + bytes!![ii++] = (intSample shr 8 and 0xFF).toByte() + i++ + } + writeSamples(bytes!!, 0, numSamples * 2) + } + + private fun audioObtainSource(isMusic: Boolean): Int { + val obtainSourceMethod = OpenALLwjgl3Audio::class.java.getDeclaredMethod("obtainSource", java.lang.Boolean.TYPE) + obtainSourceMethod.isAccessible = true + return obtainSourceMethod.invoke(audio, isMusic) as Int + } + private fun audioFreeSource(sourceID: Int) { + val freeSourceMethod = OpenALLwjgl3Audio::class.java.getDeclaredMethod("freeSource", java.lang.Integer.TYPE) + freeSourceMethod.isAccessible = true + freeSourceMethod.invoke(audio, sourceID) + } + + fun writeSamples(data: ByteArray, offset: Int, length: Int) { + var offset = offset + var length = length + require(length >= 0) { "length cannot be < 0." } + if (sourceID == -1) { + sourceID = audioObtainSource(true) + if (sourceID == -1) return + if (buffers == null) { + buffers = BufferUtils.createIntBuffer(bufferCount) + AL10.alGenBuffers(buffers) + if (AL10.alGetError() != AL10.AL_NO_ERROR) throw GdxRuntimeException("Unabe to allocate audio buffers.") + } + AL10.alSourcei(sourceID, AL10.AL_LOOPING, AL10.AL_FALSE) + AL10.alSourcef(sourceID, AL10.AL_GAIN, volume) + // Fill initial buffers. + var queuedBuffers = 0 + for (i in 0 until bufferCount) { + val bufferID = buffers!![i] + val written = Math.min(bufferSize, length) + (tempBuffer as Buffer).clear() + (tempBuffer.put(data, offset, written) as Buffer).flip() + AL10.alBufferData(bufferID, format, tempBuffer, rate) + AL10.alSourceQueueBuffers(sourceID, bufferID) + length -= written + offset += written + queuedBuffers++ + } + // Queue rest of buffers, empty. + (tempBuffer as Buffer).clear().flip() + for (i in queuedBuffers until bufferCount) { + val bufferID = buffers!![i] + AL10.alBufferData(bufferID, format, tempBuffer, rate) + AL10.alSourceQueueBuffers(sourceID, bufferID) + } + AL10.alSourcePlay(sourceID) + isPlaying = true + } + while (length > 0) { + val written = fillBuffer(data, offset, length) + length -= written + offset += written + } + } + + /** Blocks until some of the data could be buffered. */ + private fun fillBuffer(data: ByteArray, offset: Int, length: Int): Int { + val written = Math.min(bufferSize, length) + outer@ while (true) { + var buffers = AL10.alGetSourcei(sourceID, AL10.AL_BUFFERS_PROCESSED) + while (buffers-- > 0) { + val bufferID = AL10.alSourceUnqueueBuffers(sourceID) + if (bufferID == AL10.AL_INVALID_VALUE) break + renderedSeconds += secondsPerBuffer + (tempBuffer as Buffer).clear() + (tempBuffer.put(data, offset, written) as Buffer).flip() + AL10.alBufferData(bufferID, format, tempBuffer, rate) + AL10.alSourceQueueBuffers(sourceID, bufferID) + break@outer + } + // Wait for buffer to be free. + try { + Thread.sleep((1000 * secondsPerBuffer).toLong()) + } + catch (ignored: InterruptedException) { + } + } + + // A buffer underflow will cause the source to stop. + if (!isPlaying || AL10.alGetSourcei(sourceID, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) { + AL10.alSourcePlay(sourceID) + isPlaying = true + } + return written + } + + fun stop() { + if (sourceID == -1) return + audioFreeSource(sourceID) + sourceID = -1 + renderedSeconds = 0f + isPlaying = false + } + + fun isPlaying(): Boolean { + return if (sourceID == -1) false else isPlaying + } + + override fun setVolume(volume: Float) { + this.volume = volume + if (sourceID != -1) AL10.alSourcef(sourceID, AL10.AL_GAIN, volume) + } + + var position: Float + get() = if (sourceID == -1) 0f else renderedSeconds + AL10.alGetSourcef(sourceID, AL11.AL_SEC_OFFSET) + set(position) { + renderedSeconds = position + } + + fun getChannels(): Int { + return if (format == AL10.AL_FORMAT_STEREO16) 2 else 1 + } + + override fun dispose() { + if (buffers == null) return + if (sourceID != -1) { + audioFreeSource(sourceID) + sourceID = -1 + } + AL10.alDeleteBuffers(buffers) + buffers = null + } + + override fun isMono(): Boolean { + return channels == 1 + } + + override fun getLatency(): Int { + return (secondsPerBuffer * bufferCount * 1000).toInt() + } + + companion object { + private const val bytesPerSample = 2 + } +} \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt index 7852d8c..cfd61dc 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt @@ -76,6 +76,17 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath: private val messageComposeBuffer = ByteArrayOutputStream(BLOCK_SIZE) // always use this and don't alter blockSendBuffer please private var blockSendBuffer = ByteArray(1) private var blockSendCount = 0 + /*set(value) { + println("[TestDiskDrive] blockSendCount $field -> $value") + val indentation = " ".repeat(this.javaClass.simpleName.length + 4) + Thread.currentThread().stackTrace.forEachIndexed { index, it -> + if (index == 1) + println("[${this.javaClass.simpleName}]> $it") + else if (index in 1..8) + println("$indentation$it") + } + field = value + }*/ init { @@ -112,6 +123,8 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath: blockSendBuffer.size % BLOCK_SIZE else BLOCK_SIZE +// println("blockSendCount = ${blockSendCount}; sendSize = $sendSize; blockSendBuffer.size = ${blockSendBuffer.size}") + recipient.writeout(ByteArray(sendSize) { blockSendBuffer[blockSendCount * BLOCK_SIZE + it] }) @@ -163,6 +176,8 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath: else { val inputString = inputData.trimNull().toString(VM.CHARSET) +// println("[TestDiskDrive] $inputString") + if (inputString.startsWith("DEVRST\u0017")) { printdbg("Device Reset") //readModeLength = -1