pcm streaming does work but...

This commit is contained in:
minjaesong
2023-01-01 04:38:16 +09:00
parent bb7bf2c6f1
commit 7d55827551
5 changed files with 170 additions and 46 deletions

View File

@@ -96,7 +96,7 @@ function readBytes(length) {
return ptr
}
let sampleSize = FILE_SIZE
/*let sampleSize = FILE_SIZE
const FETCH_INTERVAL = 631578947
let updateAkku = FETCH_INTERVAL
let oldNanoTime = sys.nanoTime()
@@ -127,4 +127,49 @@ while (sampleSize > 0) {
}
sys.spin()
}
}*/
let sampleSize = FILE_SIZE
const BLOCK_SIZE = 4096
const QUEUEING_SIZE = 4
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setMasterVolume(0, 255)
// FIXME: when a playback was interrupted using SHIFT-CTRL-T-R, then re-tried, the ghost from the previous run
// briefly manifests, even if you're queueing only once
while (sampleSize > 0) {
let queueSize = audio.getPosition(0)
serial.println(`[js] Trying to upload samples, queueSize = ${queueSize}`)
print(".")
if (queueSize == 0) {
println()
println((FILE_SIZE - sampleSize) / FILE_SIZE * 100 + " %")
// upload four samples for lag-safely
for (let repeat = QUEUEING_SIZE; repeat > 0; repeat--) {
let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE
let samples = readBytes(readLength)
audio.putPcmDataByPtr(samples, readLength, 0)
audio.uploadSamples(0, readLength)
sampleSize -= readLength
sys.free(samples)
if (repeat > 1) sys.sleep(10)
}
audio.play(0)
}
sys.sleep(10)
}
audio.stop(0) // this shouldn't be necessary, it should stop automatically

View File

@@ -552,9 +552,8 @@ Sound Adapter MMIO
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
- Tracker mode: Cuesheet Counter
- PCM mode: Number of buffers uploaded and received by the adapter
Length Param
PCM Mode: length of the samples to upload to the speaker
@@ -562,9 +561,17 @@ Length Param
Play Head Flags
Byte 1
- 0b m00p 0000
- 0b mrqp 0000
m: mode (0 for Tracker, 1 for PCM)
r: reset parameters; always 0 when read
resetting will:
set position to 0,
set length param to 0,
unset play bit
q: purge queues (likely do nothing if not PCM); always 0 when read
p: play (0 if not -- mute all output)
NOTE: changing from PCM mode to Tracker mode or vice versa will also reset the parameters as described above
Byte 2
- PCM Mode: Sampling rate multiplier in 3.5 Unsigned Minifloat (0.03125x to 126x)

View File

@@ -26,11 +26,10 @@ class AudioJSR223Delegate(private val vm: VM) {
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 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 uploadSamples(playhead: Int, length: Int) { getPlayhead(playhead)?.pcmUploadLength = length and 65535 }
fun setSamplingRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.setSamplingRate(rate) }
fun getSamplingRate(playhead: Int) = getPlayhead(playhead)?.getSamplingRate()
@@ -55,4 +54,11 @@ class AudioJSR223Delegate(private val vm: VM) {
}
fun getPcmData(index: Int) = getFirstSnd()?.pcmBin?.get(index.toLong())
fun resetParams(playhead: Int) {
getPlayhead(playhead)?.resetParams()
}
fun purgeQueue(playhead: Int) {
getPlayhead(playhead)?.purgeQueue()
}
}

View File

@@ -2,14 +2,12 @@ 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 com.badlogic.gdx.utils.Queue
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
@@ -18,6 +16,11 @@ private fun Boolean.toInt() = if (this) 1 else 0
*/
class AudioAdapter(val vm: VM) : PeriBase {
private val DBGPRN = true
private fun printdbg(msg: Any) {
if (DBGPRN) println("[AudioAdapter] $msg")
}
companion object {
const val SAMPLING_RATE = 30000
}
@@ -31,6 +34,8 @@ class AudioAdapter(val vm: VM) : PeriBase {
private lateinit var audioDevices: Array<AudioDevice>
private val renderThreads = Array(4) { Thread(getRenderFun(it)) }
private val writeQueueingThreads = Array(4) { Thread(getQueueingFun(it)) }
// private val writeQueues = Array(4) { Queue<FloatArray>() }
/*private val alSources = Array(4) {
val audioField = OpenALAudioDevice::class.java.getDeclaredField("audio")
@@ -79,6 +84,24 @@ class AudioAdapter(val vm: VM) : PeriBase {
Thread.sleep(1)
} }
private fun getQueueingFun(pheadNum: Int): () -> Unit = { while (true) {
playheads[pheadNum].let {
if (it.pcmUploadLength > 0) {
printdbg("Downloading samples ${it.pcmUploadLength}")
val samples = FloatArray(it.pcmUploadLength) { pcmBin[it.toLong()].toUint().div(255f) * 2f - 1f }
it.pcmQueue.addLast(samples)
it.pcmUploadLength = 0
it.position += 1
}
}
Thread.sleep(4)
} }
init {
val deviceBufferSize = Gdx.audio.javaClass.getDeclaredField("deviceBufferSize").let {
@@ -90,14 +113,56 @@ class AudioAdapter(val vm: VM) : PeriBase {
it.get(Gdx.audio) as Int
}
audioDevices = Array(4) { OpenALBufferedAudioDevice(Gdx.audio as OpenALLwjgl3Audio, SAMPLING_RATE, false, deviceBufferSize, deviceBufferCount) }
printdbg("buffer size: $deviceBufferSize x $deviceBufferCount")
audioDevices = Array(4) { OpenALBufferedAudioDevice(
Gdx.audio as OpenALLwjgl3Audio,
SAMPLING_RATE,
false,
deviceBufferSize,
deviceBufferCount) {
} }
// println("AudioAdapter latency: ${audioDevice.latency}")
// printdbg("AudioAdapter latency: ${audioDevice.latency}")
renderThreads.forEach { it.start() }
writeQueueingThreads.forEach { it.start() }
}
/**
* Put this function into a separate thread and keep track of the delta time by yourself
*/
private fun render(playhead: Playhead, pheadNum: Int) {
if (playhead.isPcmMode) {
val writeQueue = playhead.pcmQueue
if (playhead.isPlaying && writeQueue.notEmpty()) {
printdbg("Taking samples from queue (queue size: ${writeQueue.size})")
val samples = writeQueue.removeFirst()
playhead.position = writeQueue.size
printdbg("P${pheadNum+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...")
// printdbg(""+(0..42).joinToString { String.format("%.2f", samples[it]) })
if (playhead.masterVolume == 0) printdbg("P${pheadNum+1} volume is zero!")
audioDevices[pheadNum].setVolume(playhead.masterVolume / 255f)
audioDevices[pheadNum].writeSamples(samples, 0, samples.size)
printdbg("P${pheadNum+1} go back to spinning")
}
else if (playhead.isPlaying) {
// printdbg("Queue exhausted, stopping...")
// it.isPlaying = false
}
}
}
override fun peek(addr: Long): Byte {
return when (val adi = addr.toInt()) {
in 0..114687 -> sampleBin[addr]
@@ -149,6 +214,7 @@ class AudioAdapter(val vm: VM) : PeriBase {
override fun dispose() {
renderThreads.forEach { it.interrupt() }
writeQueueingThreads.forEach { it.interrupt() }
audioDevices.forEach { it.dispose() }
// freeAlSources()
sampleBin.destroy()
@@ -159,34 +225,6 @@ class AudioAdapter(val vm: VM) : PeriBase {
return vm
}
/**
* Put this function into a separate thread and keep track of the delta time by yourself
*/
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 = VM.PERITYPE_SOUND
@@ -241,7 +279,9 @@ class AudioAdapter(val vm: VM) : PeriBase {
var isPlaying: Boolean = false,
var samplingRateMult: ThreeFiveMiniUfloat = ThreeFiveMiniUfloat(32),
var bpm: Int = 120, // "stored" as 96
var tickRate: Int = 6
var tickRate: Int = 6,
var pcmQueue: Queue<FloatArray> = Queue<FloatArray>()
) {
fun read(index: Int): Byte = when (index) {
0 -> position.toByte()
@@ -258,15 +298,19 @@ class AudioAdapter(val vm: VM) : PeriBase {
}
fun write(index: Int, byte: Int) = when (index) {
0 -> { position = position.and(0xff00) or position }
1 -> { position = position.and(0x00ff) or position.shl(8) }
0 -> if (!isPcmMode) { position = position.and(0xff00) or position } else {}
1 -> if (!isPcmMode) { position = position.and(0x00ff) or position.shl(8) } else {}
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 {
val oldPcmMode = isPcmMode
isPcmMode = (it and 0b10000000) != 0
isPlaying = (it and 0b00010000) != 0
if (it and 0b01000000 != 0 || oldPcmMode != isPcmMode) resetParams()
if (it and 0b00100000 != 0) purgeQueue()
} }
7 -> { samplingRateMult = ThreeFiveMiniUfloat(byte) }
8 -> { bpm = byte + 24 }
@@ -280,6 +324,20 @@ class AudioAdapter(val vm: VM) : PeriBase {
bpm = rateDiff.and(255) + 24
tickRate = rateDiff.ushr(8).and(255)
}
fun resetParams() {
position = 0
pcmUploadLength = 0
isPlaying = false
}
fun purgeQueue() {
pcmQueue.clear()
if (isPcmMode) {
position = 0
pcmUploadLength = 0
}
}
}
internal data class TaudPlayData(

View File

@@ -37,7 +37,8 @@ class OpenALBufferedAudioDevice(
val rate: Int,
isMono: Boolean,
private val bufferSize: Int,
private val bufferCount: Int
private val bufferCount: Int,
private val fillBufferCallback: () -> Unit
) : AudioDevice {
private val channels: Int
private var buffers: IntBuffer? = null
@@ -50,6 +51,12 @@ class OpenALBufferedAudioDevice(
private var bytes: ByteArray? = null
private val tempBuffer: ByteBuffer
/**
* Invoked whenever a buffer is emptied after writing samples
*
* Preferably you write 2-3 buffers worth of samples at the beginning of the playback
*/
init {
channels = if (isMono) 1 else 2
format = if (channels > 1) AL10.AL_FORMAT_STEREO16 else AL10.AL_FORMAT_MONO16
@@ -160,6 +167,7 @@ class OpenALBufferedAudioDevice(
// Wait for buffer to be free.
try {
Thread.sleep((1000 * secondsPerBuffer).toLong())
fillBufferCallback()
}
catch (ignored: InterruptedException) {
}