Compare commits

...

2 Commits

Author SHA1 Message Date
minjaesong
7d899936e2 audio changes 2026-04-16 21:58:06 +09:00
minjaesong
6aa2542bb8 audio device changes 2026-04-16 15:04:44 +09:00
11 changed files with 179 additions and 73 deletions

View File

@@ -4,7 +4,7 @@ music.pread(samples, 65534)
audio.setPcmMode(0) audio.setPcmMode(0)
audio.setMasterVolume(0, 255) audio.setMasterVolume(0, 255)
audio.putPcmDataByPtr(samples, 65534, 0) audio.putPcmDataByPtr(0, samples, 65534, 0)
audio.setLoopPoint(0, 65534) audio.setLoopPoint(0, 65534)
audio.play(0)*/ audio.play(0)*/
@@ -127,7 +127,7 @@ while (sampleSize > 0) {
let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE
readBytes(readLength, decodePtr) readBytes(readLength, decodePtr)
audio.putPcmDataByPtr(decodePtr, readLength, 0) audio.putPcmDataByPtr(0, decodePtr, readLength, 0)
audio.setSampleUploadLength(0, readLength) audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0) audio.startSampleUpload(0)

View File

@@ -0,0 +1,5 @@
/**
* Hopper is a package manager for TSVM
* Created by CuriousTorvald on 2026-04-16
*/

View File

@@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
// RAW PCM packets (decode on the fly) // RAW PCM packets (decode on the fly)
else if (packetType == 0x1000 || packetType == 0x1001) { else if (packetType == 0x1000 || packetType == 0x1001) {
let frame = seqread.readBytes(readLength) let frame = seqread.readBytes(readLength)
audio.putPcmDataByPtr(frame, readLength, 0) audio.putPcmDataByPtr(0, frame, readLength, 0)
audio.setSampleUploadLength(0, readLength) audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0) audio.startSampleUpload(0)
sys.free(frame) sys.free(frame)

View File

@@ -162,7 +162,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
seqread.readBytes(readLength, readPtr) seqread.readBytes(readLength, readPtr)
audio.putPcmDataByPtr(readPtr, readLength, 0) audio.putPcmDataByPtr(0, readPtr, readLength, 0)
audio.setSampleUploadLength(0, readLength) audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0) audio.startSampleUpload(0)

View File

@@ -18,6 +18,7 @@ const ADDRESSING_INTERNAL = 0x02
const SND_BASE_ADDR = audio.getBaseAddr() const SND_BASE_ADDR = audio.getBaseAddr()
const SND_MEM_ADDR = audio.getMemAddr() const SND_MEM_ADDR = audio.getMemAddr()
const pcm = require("pcm") const pcm = require("pcm")
const AUDIO_DEVICE = 3
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728] const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
const TAV_TEMPORAL_LEVELS = 2 const TAV_TEMPORAL_LEVELS = 2
@@ -152,10 +153,10 @@ graphics.clearPixels4(0)
const gpuGraphicsMode = graphics.getGraphicsMode() const gpuGraphicsMode = graphics.getGraphicsMode()
// Initialize audio // Initialize audio
audio.resetParams(0) audio.resetParams(AUDIO_DEVICE)
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
audio.setPcmMode(0) audio.setPcmMode(AUDIO_DEVICE)
audio.setMasterVolume(0, 255) audio.setMasterVolume(AUDIO_DEVICE, 255)
// set colour zero as half-opaque black // set colour zero as half-opaque black
graphics.setPalette(0, 0, 0, 0, 7) graphics.setPalette(0, 0, 0, 0, 7)
@@ -1152,10 +1153,10 @@ try {
else if (keyCode == 62) { // SPACE - pause/resume else if (keyCode == 62) { // SPACE - pause/resume
paused = !paused paused = !paused
if (paused) { if (paused) {
audio.stop(0) audio.stop(AUDIO_DEVICE)
serial.println(`Paused at frame ${frameCount}`) serial.println(`Paused at frame ${frameCount}`)
} else { } else {
audio.play(0) audio.play(AUDIO_DEVICE)
serial.println(`Resumed`) serial.println(`Resumed`)
} }
} }
@@ -1176,10 +1177,10 @@ try {
baseTimecodeFrameCount = 0 baseTimecodeFrameCount = 0
currentTimecodeNs = 0 currentTimecodeNs = 0
nextSubtitleEventIndex = 0 // Reset subtitle event processing nextSubtitleEventIndex = 0 // Reset subtitle event processing
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
if (paused) { if (paused) {
audio.play(0) audio.play(AUDIO_DEVICE)
audio.stop(0) audio.stop(AUDIO_DEVICE)
} }
skipped = true skipped = true
} }
@@ -1201,10 +1202,10 @@ try {
baseTimecodeFrameCount = 0 baseTimecodeFrameCount = 0
currentTimecodeNs = 0 currentTimecodeNs = 0
nextSubtitleEventIndex = 0 // Reset subtitle event processing nextSubtitleEventIndex = 0 // Reset subtitle event processing
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
if (paused) { if (paused) {
audio.play(0) audio.play(AUDIO_DEVICE)
audio.stop(0) audio.stop(AUDIO_DEVICE)
} }
skipped = true skipped = true
} }
@@ -1232,10 +1233,10 @@ try {
break break
} }
} }
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
if (paused) { if (paused) {
audio.play(0) audio.play(AUDIO_DEVICE)
audio.stop(0) audio.stop(AUDIO_DEVICE)
} }
skipped = true skipped = true
} }
@@ -1271,10 +1272,10 @@ try {
break break
} }
} }
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
if (paused) { if (paused) {
audio.play(0) audio.play(AUDIO_DEVICE)
audio.stop(0) audio.stop(AUDIO_DEVICE)
} }
skipped = true skipped = true
} else if (!seekTarget) { } else if (!seekTarget) {
@@ -1313,7 +1314,7 @@ try {
baseTimecodeFrameCount = 0 baseTimecodeFrameCount = 0
currentTimecodeNs = 0 currentTimecodeNs = 0
nextSubtitleEventIndex = 0 // Reset subtitle event processing nextSubtitleEventIndex = 0 // Reset subtitle event processing
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
currentFileIndex++ currentFileIndex++
if (skipped) { if (skipped) {
skipped = false skipped = false
@@ -1737,7 +1738,7 @@ try {
seqread.readBytes(audioLen, SND_BASE_ADDR - 2368) seqread.readBytes(audioLen, SND_BASE_ADDR - 2368)
audio.mp2Decode() audio.mp2Decode()
audio.mp2UploadDecoded(0) audio.mp2UploadDecoded(AUDIO_DEVICE)
} }
else if (packetType === TAV_PACKET_AUDIO_TAD) { else if (packetType === TAV_PACKET_AUDIO_TAD) {
@@ -1750,7 +1751,7 @@ try {
seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144) seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144)
audio.tadDecode() audio.tadDecode()
audio.tadUploadDecoded(0, sampleLen) audio.tadUploadDecoded(AUDIO_DEVICE, sampleLen)
} }
else if (packetType === TAV_PACKET_AUDIO_NATIVE) { else if (packetType === TAV_PACKET_AUDIO_NATIVE) {
// PCM length must not exceed 65536 bytes! // PCM length must not exceed 65536 bytes!
@@ -1762,10 +1763,10 @@ try {
let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr) // <- segfaults! let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr) // <- segfaults!
if (pcmLen > 65536) throw Error(`PCM data too long -- got ${pcmLen} bytes`) if (pcmLen > 65536) throw Error(`PCM data too long -- got ${pcmLen} bytes`)
audio.putPcmDataByPtr(pcmPtr, pcmLen, 0) audio.putPcmDataByPtr(AUDIO_DEVICE, pcmPtr, pcmLen, 0)
audio.setSampleUploadLength(0, pcmLen) audio.setSampleUploadLength(AUDIO_DEVICE, pcmLen)
audio.startSampleUpload(0) audio.startSampleUpload(AUDIO_DEVICE)
sys.free(zstdPtr) sys.free(zstdPtr)
sys.free(pcmPtr) sys.free(pcmPtr)
@@ -2049,7 +2050,7 @@ try {
// Fire audio on first frame // Fire audio on first frame
if (!audioFired) { if (!audioFired) {
audio.play(0) audio.play(AUDIO_DEVICE)
audioFired = true audioFired = true
} }
@@ -2137,7 +2138,7 @@ try {
// Fire audio on first frame // Fire audio on first frame
if (!audioFired) { if (!audioFired) {
audio.play(0) audio.play(AUDIO_DEVICE)
audioFired = true audioFired = true
} }
@@ -2173,8 +2174,8 @@ try {
sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize) sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize)
// Set upload parameters and trigger upload to queue // Set upload parameters and trigger upload to queue
audio.setSampleUploadLength(0, uploadSize) audio.setSampleUploadLength(AUDIO_DEVICE, uploadSize)
audio.startSampleUpload(0) audio.startSampleUpload(AUDIO_DEVICE)
predecodedPcmOffset += uploadSize predecodedPcmOffset += uploadSize
} }
@@ -2458,8 +2459,8 @@ finally {
sys.poke(-1299460, 20) sys.poke(-1299460, 20)
sys.poke(-1299460, 21) sys.poke(-1299460, 21)
audio.stop(0) audio.stop(AUDIO_DEVICE)
audio.purgeQueue(0) audio.purgeQueue(AUDIO_DEVICE)
} }
graphics.setPalette(0, 0, 0, 0, 0) graphics.setPalette(0, 0, 0, 0, 0)

View File

@@ -289,7 +289,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) {
let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength) let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength)
printdbg(` decodedSampleLength: ${decodedSampleLength}`) printdbg(` decodedSampleLength: ${decodedSampleLength}`)
audio.putPcmDataByPtr(decodePtr, decodedSampleLength, 0) audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0)
audio.setSampleUploadLength(0, decodedSampleLength) audio.setSampleUploadLength(0, decodedSampleLength)
audio.startSampleUpload(0) audio.startSampleUpload(0)

View File

@@ -119,7 +119,7 @@ function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
// ── Waveform generators ───────────────────────────────────────────────────── // ── Waveform generators ─────────────────────────────────────────────────────
function makeSquare(buf, length, offset, freq, duty, op, amp, pan) { function makeSquare(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer // buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds // length: in seconds
// offset: in seconds // offset: in seconds
@@ -128,13 +128,16 @@ function makeSquare(buf, length, offset, freq, duty, op, amp, pan) {
// op: add / mul / sub; default: add // op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5 // amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0 // pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase calc only,
// not to the buffer write position — use to ensure phase continuity
// across successive calls (e.g. frame boundaries).
if (duty == null) duty = 0.5 if (duty == null) duty = 0.5
if (op == null) op = 'add' if (op == null) op = 'add'
if (amp == null) amp = 0.5 if (amp == null) amp = 0.5
if (pan == null) pan = 0.0 if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) { mixInto(buf, length, offset, op, amp, pan, function(i) {
const t = offset + i / HW_SAMPLING_RATE const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
const phase = (t * freq) % 1
return (phase < duty) ? 1.0 : -1.0 return (phase < duty) ? 1.0 : -1.0
}) })
} }
@@ -225,7 +228,7 @@ function lfsrAdvance(state, steps, mode) {
const LFSR_PERIOD_LONG = 32767 // mode 0 const LFSR_PERIOD_LONG = 32767 // mode 0
const LFSR_PERIOD_SHORT = 93 // mode 1 const LFSR_PERIOD_SHORT = 93 // mode 1
function makeNoise(buf, length, offset, freq, type, op, amp, pan) { function makeNoise(buf, length, offset, freq, type, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer // buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds // length: in seconds
// offset: in seconds // offset: in seconds
@@ -238,20 +241,23 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
// op: add / mul / sub; default: add // op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5 // amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0 // pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase/LFSR calc only —
// see makeSquare for details.
// //
// LFSR types (1 and 2) are deterministic given (offset, freq): calling with // LFSR types (1 and 2) are deterministic given (phaseOffset+offset, freq): calling
// monotonically advancing offset values produces a seamless noise stream // with monotonically advancing phaseOffset+offset produces a seamless noise stream
// across frames. White noise types (-1, 0) are random per call. // across frames. White noise types (-1, 0) are random per call.
if (op == null) op = 'add' if (op == null) op = 'add'
if (amp == null) amp = 0.5 if (amp == null) amp = 0.5
if (pan == null) pan = 0.0 if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
if (type === -1) { if (type === -1) {
// 8-bit white: new random float in [-1, 1] each clock period // 8-bit white: new random float in [-1, 1] each clock period
let prevClock = -1 let prevClock = -1
let noiseVal = 0.0 let noiseVal = 0.0
mixInto(buf, length, offset, op, amp, pan, function(i) { mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0 const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) { if (currentClock !== prevClock) {
prevClock = currentClock prevClock = currentClock
noiseVal = Math.random() * 2.0 - 1.0 noiseVal = Math.random() * 2.0 - 1.0
@@ -263,7 +269,7 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
let prevClock = -1 let prevClock = -1
let noiseVal = 1.0 let noiseVal = 1.0
mixInto(buf, length, offset, op, amp, pan, function(i) { mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0 const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) { if (currentClock !== prevClock) {
prevClock = currentClock prevClock = currentClock
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0 noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
@@ -274,13 +280,13 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
// LFSR-based noise (types 1 and 2) // LFSR-based noise (types 1 and 2)
const mode = (type === 2) ? 1 : 0 const mode = (type === 2) ? 1 : 0
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
// Advance to deterministic position for this offset so consecutive frame // Advance to deterministic position for this tBase so consecutive frame
// calls with advancing offsets produce a seamless noise stream. // calls with monotonically advancing phaseOffset produce a seamless noise stream.
const startClock = Math.floor(offset * freq) | 0 const startClock = Math.floor(tBase * freq) | 0
let lfsr = lfsrAdvance(1, startClock % period, mode) let lfsr = lfsrAdvance(1, startClock % period, mode)
let prevClock = startClock let prevClock = startClock
mixInto(buf, length, offset, op, amp, pan, function(i) { mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0 const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
const delta = currentClock - prevClock const delta = currentClock - prevClock
if (delta > 0) { if (delta > 0) {
const steps = delta % period const steps = delta % period
@@ -292,6 +298,25 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
} }
} }
function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase.
// The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending).
// This mirrors the real NES triangle DAC which has 32 equal-height steps per period.
// duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric).
// phaseOffset: optional absolute-time base (seconds) — see makeSquare for details.
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) {
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
const step32 = Math.floor(phase * 32) | 0 // 0..31
// step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15
const level = (step32 < 16) ? (15 - step32) : (step32 - 16)
return level / 7.5 - 1.0 // map 0..15 → -1..+1
})
}
// ── Send to audio hardware ────────────────────────────────────────────────── // ── Send to audio hardware ──────────────────────────────────────────────────
function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) { function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
@@ -322,8 +347,49 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i)) sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i))
} }
// Wait for room in the playback queue (mirrors playwav.js idiom) // Wait for room in the playback queue (mirrors playwav.js idiom)
while (audio.getPosition(playhead) > 2) sys.sleep(2) // while (audio.getPosition(playhead) > 2) sys.sleep(2)
audio.putPcmDataByPtr(stagingPtr, take * 2, 0) audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
audio.setSampleUploadLength(playhead, take * 2)
audio.startSampleUpload(playhead)
remaining -= take
cursor += take
}
if (ownsStaging) sys.free(stagingPtr)
}
// Lazily-allocated JS-side interleave scratch; shared across sendBufferFast calls.
let _sendFastScratch = null
function sendBufferFast(buf, playhead, offsetSec, lengthSec, stagingPtr) {
// Like sendBuffer but interleaves L/R via a JS Uint8Array + one sys.pokeBytes per chunk,
// instead of ~2n sys.poke calls. Requires a non-native (JS-backed) buffer.
// Falls back to sendBuffer for native buffers.
if (isNative(buf)) { sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr); return }
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
const MAX_CHUNK = 32768
const ownsStaging = (stagingPtr == null)
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
const scratchNeeded = Math.min(total, MAX_CHUNK) * 2
if (_sendFastScratch == null || _sendFastScratch.length < scratchNeeded) {
_sendFastScratch = new Uint8Array(scratchNeeded)
}
let remaining = total
let cursor = start
while (remaining > 0) {
const take = Math.min(remaining, MAX_CHUNK)
const L = buf[0], R = buf[1], sc = _sendFastScratch
for (let i = 0; i < take; i++) {
sc[2 * i] = L[cursor + i]
sc[2 * i + 1] = R[cursor + i]
}
sys.pokeBytes(stagingPtr, sc.subarray(0, take * 2), take * 2)
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
audio.setSampleUploadLength(playhead, take * 2) audio.setSampleUploadLength(playhead, take * 2)
audio.startSampleUpload(playhead) audio.startSampleUpload(playhead)
remaining -= take remaining -= take
@@ -336,6 +402,6 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
exports = { exports = {
HW_SAMPLING_RATE, HW_SAMPLING_RATE,
makeBuffer, makeBufferNative, freeBufferNative, clearBuffer, makeBuffer, makeBufferNative, freeBufferNative, clearBuffer,
makeSquare, makeTriangle, makeAliasedTriangle, makeNoise, makeSquare, makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES, makeNoise,
sendBuffer sendBuffer, sendBufferFast
} }

View File

@@ -1978,6 +1978,11 @@ Sound Adapter
Endianness: little Endianness: little
TSVM Sound Adapter is consisted of 4 playheads, each playhead is capable of playing one PCM or Tracker track.
Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks.
Memory Space Memory Space
0..114687 RW: Sample bin 0..114687 RW: Sample bin
@@ -2014,17 +2019,17 @@ notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using t
Sound Adapter MMIO Sound Adapter MMIO
0..1 RW: Play head #1 position 0..1 RW: Play head #0 position (how many samples has been queued)
2..3 RW: Play head #1 length param 2..3 RW: Play head #0 length param
4 RW: Play head #1 master volume 4 RW: Play head #0 master volume
5 RW: Play head #1 master pan 5 RW: Play head #0 master pan
6..9 RW: Play head #1 flags 6..9 RW: Play head #0 flags
10..11 RW:Play head #2 position 10..11 RW:Play head #1 position (how many samples has been queued)
12..13 RW:Play head #2 length param 12..13 RW:Play head #1 length param
14 RW: Play head #2 master volume 14 RW: Play head #1 master volume
15 RW: Play head #2 master pan 15 RW: Play head #1 master pan
16..19 RW:Play head #2 flags 16..19 RW:Play head #1 flags
... auto-fill to Play head #4 ... auto-fill to Play head #4
@@ -2034,13 +2039,16 @@ Sound Adapter MMIO
When called with byte 17, initialisation will precede before the decoding When called with byte 17, initialisation will precede before the decoding
41 RO: Media Decoder Status 41 RO: MP2 Decoder Status
Non-zero value indicates the decoder is busy Non-zero value indicates the decoder is busy
42 WO: TAD Decoder Control 42 WO: TAD Decoder Control
Write 1 to decode TAD data Write 1 to decode TAD data
43 RW: TAD Quality 43 RW: TAD Quality
Must be set to appropriate value before decoding Must be set to appropriate value before decoding
44 RW: TAD Decoder Status
45 RW: Select PCM Bin for playhead (writing causes side effects)
64..2367 RW: MP2 Decoded Samples (unsigned 8-bit stereo) 64..2367 RW: MP2 Decoded Samples (unsigned 8-bit stereo)
2368..4095 RW: MP2 Frame to be decoded 2368..4095 RW: MP2 Frame to be decoded

View File

@@ -5,6 +5,23 @@ import net.torvald.tsvm.peripheral.AudioAdapter
import net.torvald.tsvm.peripheral.MP2Env import net.torvald.tsvm.peripheral.MP2Env
/** /**
* Each playhead is separate OpenAL device with its own PCM sample buffers.
* 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.
*
* ## How to upload PCM audio into a playhead
*
* 1. prepare PCM data
* 2. queue up PCM data by `audio.putPcmDataByPtr(pcmDataPtr, pcmDataLength, playhead)`
* 3. specify PCM upload length by `audio.setSampleUploadLength(playhead, pcmDataLength)`
* 4. start uploading `audio.startSampleUpload(playhead)`
* 5. sample will be ready after a few microseconds.
*
* Uploaded samples will be queued by the playhead for gapless playback
*
* Created by minjaesong on 2022-12-31. * Created by minjaesong on 2022-12-31.
*/ */
class AudioJSR223Delegate(private val vm: VM) { class AudioJSR223Delegate(private val vm: VM) {
@@ -51,16 +68,16 @@ class AudioJSR223Delegate(private val vm: VM) {
fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 } fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 }
fun getTickRate(playhead: Int) = getPlayhead(playhead)?.tickRate fun getTickRate(playhead: Int) = getPlayhead(playhead)?.tickRate
fun putPcmDataByPtr(ptr: Int, length: Int, destOffset: Int) { fun putPcmDataByPtr(playhead: Int, ptr: Int, length: Int, destOffset: Int) {
getFirstSnd()?.let { getFirstSnd()?.let {
val vkMult = if (ptr >= 0) 1 else -1 val vkMult = if (ptr >= 0) 1 else -1
for (k in 0L until length) { for (k in 0L until length) {
val vk = k * vkMult val vk = k * vkMult
it.pcmBin[k + destOffset] = vm.peek(ptr + vk)!! it.pcmBin[playhead][k + destOffset] = vm.peek(ptr + vk)!!
} }
} }
} }
fun getPcmData(index: Int) = getFirstSnd()?.pcmBin?.get(index.toLong()) fun getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.get(index.toLong())
fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index } fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index }
fun getPcmQueueCapacityIndex(playhead: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex } fun getPcmQueueCapacityIndex(playhead: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex }

View File

@@ -763,7 +763,7 @@ class VM(
else if (dev is AudioAdapter) { else if (dev is AudioAdapter) {
if (relPtrInDev(fromRel, len, 64, 2367)) dev.mediaDecodedBin.ptr + fromRel - 64 if (relPtrInDev(fromRel, len, 64, 2367)) dev.mediaDecodedBin.ptr + fromRel - 64
else if (relPtrInDev(fromRel, len, 2368, 4096)) dev.mediaFrameBin.ptr + fromRel - 2368 else if (relPtrInDev(fromRel, len, 2368, 4096)) dev.mediaFrameBin.ptr + fromRel - 2368
else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin.ptr + fromRel - 65536 else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin[dev.selectedPcmBin].ptr + fromRel - 65536
else null else null
} }
else if (dev is GraphicsAdapter) { else if (dev is GraphicsAdapter) {

View File

@@ -41,7 +41,7 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
// printdbg("P${playhead.index+1} go back to spinning") // printdbg("P${playhead.index+1} go back to spinning")
Thread.sleep(12) Thread.sleep(6)
} }
else if (playhead.isPlaying && writeQueue.isEmpty) { else if (playhead.isPlaying && writeQueue.isEmpty) {
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
@@ -49,7 +49,7 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
// TODO: wait for 1-2 seconds then finally stop the device // TODO: wait for 1-2 seconds then finally stop the device
// playhead.audioDevice.stop() // playhead.audioDevice.stop()
Thread.sleep(12) Thread.sleep(6)
} }
} }
@@ -122,13 +122,20 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } } internal val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } }
internal val playheads: Array<Playhead> internal val playheads: Array<Playhead>
internal val cueSheet = Array(2048) { PlayCue() } internal val cueSheet = Array(2048) { PlayCue() }
internal val pcmBin = UnsafeHelper.allocate(65536L, this) internal val pcmBin = arrayOf(
UnsafeHelper.allocate(65536L, this),
UnsafeHelper.allocate(65536L, this),
UnsafeHelper.allocate(65536L, this),
UnsafeHelper.allocate(65536L, this),
)
internal val mediaFrameBin = UnsafeHelper.allocate(1728, this) internal val mediaFrameBin = UnsafeHelper.allocate(1728, this)
internal val mediaDecodedBin = UnsafeHelper.allocate(2304, this) internal val mediaDecodedBin = UnsafeHelper.allocate(2304, this)
@Volatile private var mp2Busy = false @Volatile private var mp2Busy = false
@Volatile var selectedPcmBin = 0
// TAD (Terrarum Advanced Audio) decoder buffers // TAD (Terrarum Advanced Audio) decoder buffers
internal val tadInputBin = UnsafeHelper.allocate(65536L, this) // Input: compressed TAD chunk (max 64KB) internal val tadInputBin = UnsafeHelper.allocate(65536L, this) // Input: compressed TAD chunk (max 64KB)
internal val tadDecodedBin = UnsafeHelper.allocate(65536L, this) // Output: PCMu8 stereo (32768 samples * 2 channels) internal val tadDecodedBin = UnsafeHelper.allocate(65536L, this) // Output: PCMu8 stereo (32768 samples * 2 channels)
@@ -241,7 +248,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
renderRunnables = Array(4) { RenderRunnable(playheads[it]) } renderRunnables = Array(4) { RenderRunnable(playheads[it]) }
renderThreads = Array(4) { Thread(renderThreadGroup, renderRunnables[it], "AudioRenderHead${it+1}!$hash") } renderThreads = Array(4) { Thread(renderThreadGroup, renderRunnables[it], "AudioRenderHead${it+1}!$hash") }
writeQueueingRunnables = Array(4) { WriteQueueingRunnable(playheads[it], pcmBin) } writeQueueingRunnables = Array(4) { WriteQueueingRunnable(playheads[it], pcmBin[it]) }
writeQueueingThreads = Array(4) { Thread(writeQueueingGroup, writeQueueingRunnables[it], "AudioQueueingHead${it+1}!$hash") } writeQueueingThreads = Array(4) { Thread(writeQueueingGroup, writeQueueingRunnables[it], "AudioQueueingHead${it+1}!$hash") }
// printdbg("AudioAdapter latency: ${audioDevice.latency}") // printdbg("AudioAdapter latency: ${audioDevice.latency}")
@@ -315,13 +322,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
42 -> -1 // TAD control (write-only) 42 -> -1 // TAD control (write-only)
43 -> tadQuality.toByte() 43 -> tadQuality.toByte()
44 -> tadBusy.toInt().toByte() 44 -> tadBusy.toInt().toByte()
45 -> selectedPcmBin.toByte()
in 64..2367 -> mediaDecodedBin[addr - 64] in 64..2367 -> mediaDecodedBin[addr - 64]
in 2368..4095 -> mediaFrameBin[addr - 2368] in 2368..4095 -> mediaFrameBin[addr - 2368]
in 4096..4097 -> 0 in 4096..4097 -> 0
in 32768..65535 -> (adi - 32768).let { in 32768..65535 -> (adi - 32768).let {
cueSheet[it / 16].read(it % 15) cueSheet[it / 16].read(it % 15)
} }
in 65536..131071 -> pcmBin[addr - 65536] in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536]
else -> { else -> {
println("[AudioAdapter] Bus mirroring on mmio_reading while trying to read address $addr") println("[AudioAdapter] Bus mirroring on mmio_reading while trying to read address $addr")
mmio_read(addr % 131072) mmio_read(addr % 131072)
@@ -349,12 +357,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// TAD quality (0-5) // TAD quality (0-5)
tadQuality = bi.coerceIn(0, 5) tadQuality = bi.coerceIn(0, 5)
} }
45 -> selectedPcmBin = bi % 4
in 64..2367 -> { mediaDecodedBin[addr - 64] = byte } in 64..2367 -> { mediaDecodedBin[addr - 64] = byte }
in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte } in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte }
in 32768..65535 -> { (adi - 32768).let { in 32768..65535 -> { (adi - 32768).let {
cueSheet[it / 16].write(it % 15, bi) cueSheet[it / 16].write(it % 15, bi)
} } } }
in 65536..131071 -> { pcmBin[addr - 65536] = byte } in 65536..131071 -> { pcmBin[selectedPcmBin][addr - 65536] = byte }
} }
} }
@@ -368,7 +377,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
writeQueueingGroup.interrupt() writeQueueingGroup.interrupt()
playheads.forEach { it.dispose() } playheads.forEach { it.dispose() }
sampleBin.destroy() sampleBin.destroy()
pcmBin.destroy() pcmBin.forEach { it.destroy() }
mediaFrameBin.destroy() mediaFrameBin.destroy()
mediaDecodedBin.destroy() mediaDecodedBin.destroy()
tadInputBin.destroy() tadInputBin.destroy()