From 8702104bfed0ed8958f062f7da239da706b1c9fa Mon Sep 17 00:00:00 2001 From: minjaesong Date: Fri, 17 Apr 2026 12:03:43 +0900 Subject: [PATCH] tracker impl --- assets/disk0/tvdos/bin/playtav.js | 2 +- assets/disk0/tvdos/include/psg.mjs | 25 +- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 48 ++- .../torvald/tsvm/peripheral/AudioAdapter.kt | 307 +++++++++++++++--- .../peripheral/OpenALBufferedAudioDevice.kt | 14 + 5 files changed, 333 insertions(+), 63 deletions(-) diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 92342b9..f5cb007 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -18,7 +18,7 @@ const ADDRESSING_INTERNAL = 0x02 const SND_BASE_ADDR = audio.getBaseAddr() const SND_MEM_ADDR = audio.getMemAddr() const pcm = require("pcm") -const AUDIO_DEVICE = 3 +const AUDIO_DEVICE = 0 const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728] const TAV_TEMPORAL_LEVELS = 2 diff --git a/assets/disk0/tvdos/include/psg.mjs b/assets/disk0/tvdos/include/psg.mjs index ec4d80d..5c25c6d 100644 --- a/assets/disk0/tvdos/include/psg.mjs +++ b/assets/disk0/tvdos/include/psg.mjs @@ -81,9 +81,12 @@ function clearBuffer(buf, offsetSec, lengthSec) { // Re-silence a buffer region (fill with 128) for re-use across frames. const start = (offsetSec != null) ? secToSamples(offsetSec) : 0 const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start) - for (let i = 0; i < total; i++) { - writeU8(buf, 0, start + i, 128) - writeU8(buf, 1, start + i, 128) + if (!buf.native) { + buf[0].fill(128, start, start + total) + buf[1].fill(128, start, start + total) + } else { + sys.memset(buf[0] + start, 128, total) + sys.memset(buf[1] + start, 128, total) } } @@ -142,7 +145,7 @@ function makeSquare(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) }) } -function makeTriangle(buf, length, offset, freq, duty, op, amp, pan) { +function makeTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) { // buffer: [Uint8Array, Uint8Array] or native buffer // length: in seconds // offset: in seconds @@ -151,6 +154,8 @@ function makeTriangle(buf, length, offset, freq, duty, op, amp, pan) { // op: add / mul / sub; default: add // amp: 0.0 to 1.0; default: 0.5 // pan: -1.0 to 1.0; default: 0.0 + // phaseOffset: optional absolute-time base (seconds) added to phase calc only — + // see makeSquare for details. if (duty == null) duty = 0.0 if (op == null) op = 'add' if (amp == null) amp = 0.5 @@ -158,9 +163,9 @@ function makeTriangle(buf, length, offset, freq, duty, op, amp, pan) { // riseFrac: fraction of period spent rising from -1 to +1 // 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw const riseFrac = (duty + 1.0) * 0.5 + const tBase = (phaseOffset || 0) + offset mixInto(buf, length, offset, op, amp, pan, function(i) { - const t = offset + i / HW_SAMPLING_RATE - const phase = (t * freq) % 1 + const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1 if (riseFrac <= 0) { return 1.0 - 2.0 * phase // falling saw } else if (riseFrac >= 1) { @@ -173,7 +178,7 @@ function makeTriangle(buf, length, offset, freq, duty, op, amp, pan) { }) } -function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan) { +function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) { // buffer: [Uint8Array, Uint8Array] or native buffer // Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style). // The staircase quantisation introduces harmonics that mimic NES character. @@ -184,14 +189,16 @@ function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan) { // op: add / mul / sub; default: add // amp: 0.0 to 1.0; default: 0.5 // pan: -1.0 to 1.0; default: 0.0 + // phaseOffset: optional absolute-time base (seconds) added to phase calc only — + // see makeSquare for details. if (duty == null) duty = 0.0 if (op == null) op = 'add' if (amp == null) amp = 0.5 if (pan == null) pan = 0.0 const riseFrac = (duty + 1.0) * 0.5 + const tBase = (phaseOffset || 0) + offset mixInto(buf, length, offset, op, amp, pan, function(i) { - const t = offset + i / HW_SAMPLING_RATE - const phase = (t * freq) % 1 + const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1 let v if (riseFrac <= 0) { v = 1.0 - 2.0 * phase diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 023a224..09f318c 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -9,8 +9,21 @@ import net.torvald.tsvm.peripheral.MP2Env * Media decoders (MP2, TAD) are independent to the playheads and there is only one. * * NOTES: - * 1. tracker mode is currently unimplemented. - * 2. Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks. + * 1. Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks. + * + * ## How to use Tracker Mode + * + * 1. Call `setTrackerMode(playhead)` to switch to tracker mode. + * 2. Write sample data into the sample bin via `vm.poke` (peripheral memory space, offset 0+). + * 3. Define instruments via `uploadInstrument(slot, byteArray)` or raw `vm.poke`. + * 4. Define patterns via `uploadPattern(slot, byteArray)` or raw `vm.poke`. + * 5. Define cue entries via `uploadCue(idx, byteArray)` or raw `vm.poke`. + * 6. Set `setBPM(playhead, bpm)` and `setTickRate(playhead, rate)`. + * 7. Set `setMasterVolume(playhead, 255)`. + * 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`. + * + * Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave. + * Empty row: note = 0xFFFF (no trigger). All 256 instrument slots (0-255) are valid. * * ## How to upload PCM audio into a playhead * @@ -68,6 +81,35 @@ class AudioJSR223Delegate(private val vm: VM) { fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 } fun getTickRate(playhead: Int) = getPlayhead(playhead)?.tickRate + fun setCuePosition(playhead: Int, pos: Int) { + getPlayhead(playhead)?.let { ph -> + ph.position = pos and 2047 + ph.trackerState?.cuePos = ph.position + } + } + fun getCuePosition(playhead: Int) = getPlayhead(playhead)?.position + + /** Upload 64 bytes defining instrument `slot` (0-255). */ + fun uploadInstrument(slot: Int, bytes: IntArray) { + getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst -> + for (i in 0 until minOf(64, bytes.size)) inst.setByte(i, bytes[i] and 0xFF) + } + } + + /** Upload 512 bytes (64 rows × 8 bytes) defining pattern `slot` (0-255). */ + fun uploadPattern(slot: Int, bytes: IntArray) { + getFirstSnd()?.playdata?.get(slot and 0xFF)?.let { pat -> + for (i in 0 until minOf(512, bytes.size)) pat[i / 8].setByte(i % 8, bytes[i] and 0xFF) + } + } + + /** Upload 16 bytes defining cue entry `idx` (0-2047): bytes 0-14 = pattern numbers for voices 0-14, byte 15 = instruction. */ + fun uploadCue(idx: Int, bytes: IntArray) { + getFirstSnd()?.cueSheet?.get(idx and 0x7FF)?.let { cue -> + for (i in 0 until minOf(16, bytes.size)) cue.write(i, bytes[i] and 0xFF) + } + } + fun putPcmDataByPtr(playhead: Int, ptr: Int, length: Int, destOffset: Int) { getFirstSnd()?.let { val vkMult = if (ptr >= 0) 1 else -1 @@ -80,7 +122,7 @@ class AudioJSR223Delegate(private val vm: VM) { fun getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.get(index.toLong()) fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index } - fun getPcmQueueCapacityIndex(playhead: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex } + fun getPcmQueueCapacityIndex(playhead: Int) = getPlayhead(playhead)?.pcmQueueSizeIndex fun getPcmQueueCapacity(playhead: Int) = getPlayhead(playhead)?.getPcmQueueCapacity() fun resetParams(playhead: Int) { diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index f97c6c5..66555a7 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -34,21 +34,22 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable { val samples = writeQueue.removeFirst() playhead.position = writeQueue.size -// printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...") -// printdbg(""+(0..42).joinToString { String.format("%.2f", samples[it]) }) - playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size) -// printdbg("P${playhead.index+1} go back to spinning") - Thread.sleep(6) } else if (playhead.isPlaying && writeQueue.isEmpty) { printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") - // TODO: wait for 1-2 seconds then finally stop the device -// playhead.audioDevice.stop() - + Thread.sleep(6) + } + } else { + // Tracker mode + if (playhead.isPlaying) { + val out = playhead.parent.generateTrackerAudio(playhead) + if (out != null) { + playhead.audioDevice.writeStereoSamplesUI8(out, 0, AudioAdapter.TRACKER_CHUNK) + } Thread.sleep(6) } } @@ -115,11 +116,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { companion object { internal val DBGPRN = false const val SAMPLING_RATE = 32000 + const val TRACKER_CHUNK = 512 + const val TRACKER_C3 = 0x4000 } internal val sampleBin = UnsafeHelper.allocate(114687L, this) internal val instruments = Array(256) { TaudInst() } - internal val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } } + internal val playdata = Array(256) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } } internal val playheads: Array internal val cueSheet = Array(2048) { PlayCue() } internal val pcmBin = arrayOf( @@ -327,7 +330,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { in 2368..4095 -> mediaFrameBin[addr - 2368] in 4096..4097 -> 0 in 32768..65535 -> (adi - 32768).let { - cueSheet[it / 16].read(it % 15) + cueSheet[it / 16].read(it % 16) } in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536] else -> { @@ -361,7 +364,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { in 64..2367 -> { mediaDecodedBin[addr - 64] = byte } in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte } in 32768..65535 -> { (adi - 32768).let { - cueSheet[it / 16].write(it % 15, bi) + cueSheet[it / 16].write(it % 16, bi) } } in 65536..131071 -> { pcmBin[selectedPcmBin][addr - 65536] = byte } } @@ -1068,23 +1071,189 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { + //========================================================================= + // Tracker Engine + // + // Effect codes (non-canonical MVP; spec is silent on values): + // 0x00: no effect + // 0x01 arg: pitch slide up, arg = 4096-TET units per tick + // 0x02 arg: pitch slide down, arg = 4096-TET units per tick + // 0x0A arg: volume slide, high nibble = up/tick, low nibble = down/tick + // 0xEC arg: note cut at tick (arg & 0xFF) + //========================================================================= + + private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double = + inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0) + + private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) { + if (voice.envIndex >= 23) { + voice.envVolume = inst.envelopes[23].volume / 255.0 + return + } + val offset = inst.envelopes[voice.envIndex].offset.toFloat().toDouble() + if (offset == 0.0) { + voice.envVolume = inst.envelopes[voice.envIndex].volume / 255.0 + return + } + voice.envTimeSec += tickSec + if (voice.envTimeSec >= offset) { + voice.envTimeSec -= offset + voice.envIndex = (voice.envIndex + 1).coerceAtMost(23) + voice.envVolume = inst.envelopes[voice.envIndex].volume / 255.0 + } else { + val cur = inst.envelopes[voice.envIndex].volume / 255.0 + val nxt = inst.envelopes[(voice.envIndex + 1).coerceAtMost(23)].volume / 255.0 + voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / offset) + } + } + + private fun fetchTrackerSample(voice: Voice, inst: TaudInst): Double { + val basePtr = inst.samplePtr and 0x1FFFF + val sampleLen = inst.sampleLength.coerceAtLeast(1) + val loopStart = inst.sampleLoopStart.toDouble() + val loopEnd = inst.sampleLoopEnd.toDouble().coerceAtLeast(1.0) + val binMax = 114686 // sampleBin is 114687 bytes (0..114686) + + val i0 = voice.samplePos.toInt().coerceIn(0, sampleLen - 1) + val i1 = (i0 + 1).coerceAtMost(sampleLen - 1) + val frac = voice.samplePos - i0.toDouble() + val s0 = (sampleBin[(basePtr + i0).coerceAtMost(binMax).toLong()].toUint() - 128) / 128.0 + val s1 = (sampleBin[(basePtr + i1).coerceAtMost(binMax).toLong()].toUint() - 128) / 128.0 + val sample = s0 + (s1 - s0) * frac + + if (voice.forward) { + voice.samplePos += voice.playbackRate + when (inst.loopMode) { + 0 -> if (voice.samplePos >= sampleLen) voice.active = false + 1 -> if (voice.samplePos >= loopEnd) voice.samplePos -= (loopEnd - loopStart).coerceAtLeast(1.0) + 2 -> if (voice.samplePos >= loopEnd) { voice.samplePos = loopEnd; voice.forward = false } + 3 -> if (voice.samplePos >= sampleLen) { voice.samplePos = sampleLen.toDouble() - 1; voice.active = false } + } + } else { + voice.samplePos -= voice.playbackRate + if (voice.samplePos < loopStart) { voice.samplePos = loopStart; voice.forward = true } + } + return sample + } + + private fun applyTrackerRow(ts: TrackerState, playhead: Playhead) { + val cue = cueSheet[ts.cuePos] + for (vi in 0..14) { + val patIdx = cue.patterns[vi].coerceIn(0, 255) + val row = playdata[patIdx][ts.rowIndex] + val voice = ts.voices[vi] + + voice.rowVolume = row.volume + voice.rowPan = row.pan + voice.cutAtTick = -1 + voice.pitchSlideAmount = 0.0 + voice.volSlidePerTick = 0 + + when (row.effect) { + 0x01 -> voice.pitchSlideAmount = row.effectArg.toDouble() + 0x02 -> voice.pitchSlideAmount = -row.effectArg.toDouble() + 0x0A -> { val a = row.effectArg and 0xFF; voice.volSlidePerTick = ((a ushr 4) and 0xF) - (a and 0xF) } + 0xEC -> voice.cutAtTick = row.effectArg and 0xFF + } + + if (row.note != 0xFFFF) { + val inst = instruments[row.instrment] + voice.instrumentId = row.instrment + voice.samplePos = inst.samplePlayStart.toDouble() + voice.forward = true + voice.active = true + voice.envIndex = 0 + voice.envTimeSec = 0.0 + voice.envVolume = inst.envelopes[0].volume / 255.0 + voice.noteVal = row.note + voice.playbackRate = computePlaybackRate(inst, row.note) + } + } + } + + private fun applyTrackerTick(ts: TrackerState, playhead: Playhead) { + val tickSec = 2.5 / playhead.bpm + for (voice in ts.voices) { + if (!voice.active) continue + val inst = instruments[voice.instrumentId] + if (voice.cutAtTick == ts.tickInRow) { voice.active = false; continue } + if (voice.pitchSlideAmount != 0.0) { + voice.noteVal = (voice.noteVal + voice.pitchSlideAmount).toInt().coerceIn(0, 0xFFFE) + voice.playbackRate = computePlaybackRate(inst, voice.noteVal) + } + if (voice.volSlidePerTick != 0) { + voice.rowVolume = (voice.rowVolume + voice.volSlidePerTick).coerceIn(0, 63) + } + advanceEnvelope(voice, inst, tickSec) + } + } + + private fun advanceTrackerCue(ts: TrackerState, playhead: Playhead) { + val instr = cueSheet[ts.cuePos].instruction + ts.cuePos = when (instr) { + is PlayInstGoBack -> (ts.cuePos - instr.arg).coerceAtLeast(0) + is PlayInstSkip -> (ts.cuePos + instr.arg).coerceAtMost(2047) + else -> (ts.cuePos + 1).coerceAtMost(2047) + } + playhead.position = ts.cuePos + } + + internal fun generateTrackerAudio(playhead: Playhead): ByteArray? { + val ts = playhead.trackerState ?: return null + val bpm = playhead.bpm + val spt = SAMPLING_RATE * 2.5 / bpm + + val out = ByteArray(TRACKER_CHUNK * 2) + + if (ts.firstRow) { + ts.firstRow = false + applyTrackerRow(ts, playhead) + } + + for (n in 0 until TRACKER_CHUNK) { + ts.samplesIntoTick += 1.0 + if (ts.samplesIntoTick >= spt) { + ts.samplesIntoTick -= spt + applyTrackerTick(ts, playhead) + ts.tickInRow++ + if (ts.tickInRow >= playhead.tickRate) { + ts.tickInRow = 0 + ts.rowIndex++ + if (ts.rowIndex >= 64) { + ts.rowIndex = 0 + advanceTrackerCue(ts, playhead) + } + applyTrackerRow(ts, playhead) + } + } + + var mixL = 0.0 + var mixR = 0.0 + for (voice in ts.voices) { + if (!voice.active) continue + val s = fetchTrackerSample(voice, instruments[voice.instrumentId]) + val vol = voice.envVolume * voice.rowVolume / 63.0 * playhead.masterVolume / 255.0 + mixL += s * vol * (63 - voice.rowPan) / 63.0 + mixR += s * vol * voice.rowPan / 63.0 + } + + out[n * 2] = ((mixL.coerceIn(-1.0, 1.0) * 127 + 128).toInt()).toByte() + out[n * 2 + 1] = ((mixR.coerceIn(-1.0, 1.0) * 127 + 128).toInt()).toByte() + } + + return out + } + internal 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") + 15 -> { instruction = when { + byte >= 128 -> PlayInstGoBack(byte and 127) + byte in 16..31 -> PlayInstSkip(byte and 15) + else -> PlayInstNop } } else -> throw InternalError("Bad offset $index") } @@ -1107,8 +1276,34 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { internal class PlayInstSkip(arg: Int) : PlayInstruction(arg) internal object PlayInstNop : PlayInstruction(0) + class Voice { + var active = false + var instrumentId = 0 + var samplePos = 0.0 + var playbackRate = 1.0 + var forward = true + var rowVolume = 63 + var rowPan = 32 + var envIndex = 0 + var envTimeSec = 0.0 + var envVolume = 1.0 + var noteVal = 0xFFFF + var pitchSlideAmount = 0.0 // 4096-TET units per tick; +up, -down + var volSlidePerTick = 0 + var cutAtTick = -1 + } + + class TrackerState { + var cuePos = 0 + var rowIndex = 0 + var tickInRow = 0 + var samplesIntoTick = 0.0 + var firstRow = true + val voices = Array(15) { Voice() } + } + class Playhead( - private val parent: AudioAdapter, + internal val parent: AudioAdapter, val index: Int, var position: Int = 0, @@ -1124,20 +1319,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var pcmQueueSizeIndex: Int = 0, val audioDevice: OpenALBufferedAudioDevice, ) { + var trackerState: TrackerState? = TrackerState() // default mode is tracker (isPcmMode=false) + // flags var isPcmMode: Boolean = false set(value) { - if (value != isPcmMode) { + if (value != field) { resetParams() + trackerState = if (!value) TrackerState() else null } field = value } var isPlaying: Boolean = false set(value) { // play last bit from the buffer by feeding 0s - if (isPlaying && !value) { + if (field && !value) { // println("!! inserting dummy bytes") - pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount)) + if (isPcmMode) { + pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount)) + } } field = value } @@ -1159,10 +1359,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { fun write(index: Int, byte: Int) { val byte = byte and 255 when (index) { - 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) } + 0 -> if (!isPcmMode) { position = (position and 0xff00) or byte; trackerState?.cuePos = position } else {} + 1 -> if (!isPcmMode) { position = (position and 0x00ff) or (byte shl 8); trackerState?.cuePos = position } else {} + 2 -> { pcmUploadLength = (pcmUploadLength and 0xff00) or byte } + 3 -> { pcmUploadLength = (pcmUploadLength and 0x00ff) or (byte shl 8) } 4 -> { masterVolume = byte audioDevice.setVolume(masterVolume / 255f) @@ -1170,10 +1370,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { 5 -> { masterPan = byte } 6 -> { byte.let { isPcmMode = (it and 0b10000000) != 0 + if (it and 0b01000000 != 0) resetParams() isPlaying = (it and 0b00010000) != 0 pcmQueueSizeIndex = (it and 0b00001111) - - if (it and 0b00100000 != 0) purgeQueue() } } 7 -> if (isPcmMode) { pcmUpload = true } else {} @@ -1195,6 +1394,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { pcmUploadLength = 0 isPlaying = false pcmQueueSizeIndex = 2 + trackerState?.let { ts -> + ts.cuePos = 0; ts.rowIndex = 0; ts.tickInRow = 0 + ts.samplesIntoTick = 0.0; ts.firstRow = true + ts.voices.forEach { it.active = false } + } } fun purgeQueue() { @@ -1289,36 +1493,39 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { 12 -> (samplePtr.ushr(16).and(1).shl(7) or loopMode.and(3)).toByte() 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() + in 16..63 step 2 -> envelopes[(offset - 16) / 2].volume.toByte() + in 17..63 step 2 -> envelopes[(offset - 17) / 2].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) } + 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) } + 2 -> { sampleLength = (sampleLength and 0xff00) or byte } + 3 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) } - 4 -> { samplingRate = samplingRate.and(0x1ff00) or byte } - 5 -> { samplingRate = samplingRate.and(0x000ff) or byte.shl(8) } + 4 -> { samplingRate = (samplingRate and 0xff00) or byte } + 5 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) } - 6 -> { sampleLoopStart = sampleLoopStart.and(0x1ff00) or byte } - 7 -> { sampleLoopStart = sampleLoopStart.and(0x000ff) or byte.shl(8) } + 6 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte } + 7 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) } - 8 -> { sampleLoopEnd = sampleLoopEnd.and(0x1ff00) or byte } - 9 -> { sampleLoopEnd = sampleLoopEnd.and(0x000ff) or byte.shl(8) } + 8 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte } + 9 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) } - 10 -> { - if (byte.and(0b1000_0000) != 0) - samplePtr = samplePtr or 0x10000 + 10 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte } + 11 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) } - loopMode = byte.and(3) + 12 -> { + samplePtr = if (byte and 0b1000_0000 != 0) samplePtr or 0x10000 + else samplePtr and 0x0ffff + loopMode = byte and 3 } + 13, 14, 15 -> {} - in 16..63 step 2 -> envelopes[offset - 16].volume = byte - in 17..63 step 2 -> envelopes[offset - 16].offset = ThreeFiveMiniUfloat(byte) + in 16..63 step 2 -> envelopes[(offset - 16) / 2].volume = byte + in 17..63 step 2 -> envelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte) else -> throw InternalError("Bad offset $offset") } } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt index 0c1cc83..245749b 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/OpenALBufferedAudioDevice.kt @@ -81,6 +81,20 @@ class OpenALBufferedAudioDevice( writeSamples(bytes!!, 0, numSamples * 2) } + /** Write interleaved stereo unsigned-8-bit samples [L0, R0, L1, R1, ...] to the device. */ + fun writeStereoSamplesUI8(samples: ByteArray, offset: Int, numPairs: Int) { + if (bytes == null || bytes!!.size < numPairs * 4) bytes = ByteArray(numPairs * 4) + var i = offset + var ii = 0 + repeat(numPairs) { + val l = ui8toI16Hi[samples[i++].toUint()] + val r = ui8toI16Hi[samples[i++].toUint()] + bytes!![ii++] = l; bytes!![ii++] = l // L int16 LE + bytes!![ii++] = r; bytes!![ii++] = r // R int16 LE + } + writeSamples(bytes!!, 0, numPairs * 4) + } + 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)