mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
2 Commits
2ac084acd7
...
7d899936e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d899936e2 | ||
|
|
6aa2542bb8 |
@@ -4,7 +4,7 @@ music.pread(samples, 65534)
|
||||
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.putPcmDataByPtr(samples, 65534, 0)
|
||||
audio.putPcmDataByPtr(0, samples, 65534, 0)
|
||||
audio.setLoopPoint(0, 65534)
|
||||
audio.play(0)*/
|
||||
|
||||
@@ -127,7 +127,7 @@ while (sampleSize > 0) {
|
||||
let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE
|
||||
readBytes(readLength, decodePtr)
|
||||
|
||||
audio.putPcmDataByPtr(decodePtr, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, decodePtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
5
assets/disk0/tvdos/bin/hopper.js
Normal file
5
assets/disk0/tvdos/bin/hopper.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Hopper is a package manager for TSVM
|
||||
* Created by CuriousTorvald on 2026-04-16
|
||||
*/
|
||||
|
||||
@@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
|
||||
// RAW PCM packets (decode on the fly)
|
||||
else if (packetType == 0x1000 || packetType == 0x1001) {
|
||||
let frame = seqread.readBytes(readLength)
|
||||
audio.putPcmDataByPtr(frame, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, frame, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
sys.free(frame)
|
||||
|
||||
@@ -162,7 +162,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
||||
|
||||
seqread.readBytes(readLength, readPtr)
|
||||
|
||||
audio.putPcmDataByPtr(readPtr, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, readPtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
@@ -18,6 +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 MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
|
||||
const TAV_TEMPORAL_LEVELS = 2
|
||||
|
||||
@@ -152,10 +153,10 @@ graphics.clearPixels4(0)
|
||||
const gpuGraphicsMode = graphics.getGraphicsMode()
|
||||
|
||||
// Initialize audio
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.resetParams(AUDIO_DEVICE)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
audio.setPcmMode(AUDIO_DEVICE)
|
||||
audio.setMasterVolume(AUDIO_DEVICE, 255)
|
||||
|
||||
// set colour zero as half-opaque black
|
||||
graphics.setPalette(0, 0, 0, 0, 7)
|
||||
@@ -1152,10 +1153,10 @@ try {
|
||||
else if (keyCode == 62) { // SPACE - pause/resume
|
||||
paused = !paused
|
||||
if (paused) {
|
||||
audio.stop(0)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
serial.println(`Paused at frame ${frameCount}`)
|
||||
} else {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
serial.println(`Resumed`)
|
||||
}
|
||||
}
|
||||
@@ -1176,10 +1177,10 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1201,10 +1202,10 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1232,10 +1233,10 @@ try {
|
||||
break
|
||||
}
|
||||
}
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1271,10 +1272,10 @@ try {
|
||||
break
|
||||
}
|
||||
}
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
} else if (!seekTarget) {
|
||||
@@ -1313,7 +1314,7 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
currentFileIndex++
|
||||
if (skipped) {
|
||||
skipped = false
|
||||
@@ -1737,7 +1738,7 @@ try {
|
||||
|
||||
seqread.readBytes(audioLen, SND_BASE_ADDR - 2368)
|
||||
audio.mp2Decode()
|
||||
audio.mp2UploadDecoded(0)
|
||||
audio.mp2UploadDecoded(AUDIO_DEVICE)
|
||||
|
||||
}
|
||||
else if (packetType === TAV_PACKET_AUDIO_TAD) {
|
||||
@@ -1750,7 +1751,7 @@ try {
|
||||
|
||||
seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144)
|
||||
audio.tadDecode()
|
||||
audio.tadUploadDecoded(0, sampleLen)
|
||||
audio.tadUploadDecoded(AUDIO_DEVICE, sampleLen)
|
||||
}
|
||||
else if (packetType === TAV_PACKET_AUDIO_NATIVE) {
|
||||
// PCM length must not exceed 65536 bytes!
|
||||
@@ -1762,10 +1763,10 @@ try {
|
||||
let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr) // <- segfaults!
|
||||
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.startSampleUpload(0)
|
||||
audio.setSampleUploadLength(AUDIO_DEVICE, pcmLen)
|
||||
audio.startSampleUpload(AUDIO_DEVICE)
|
||||
sys.free(zstdPtr)
|
||||
|
||||
sys.free(pcmPtr)
|
||||
@@ -2049,7 +2050,7 @@ try {
|
||||
|
||||
// Fire audio on first frame
|
||||
if (!audioFired) {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audioFired = true
|
||||
}
|
||||
|
||||
@@ -2137,7 +2138,7 @@ try {
|
||||
|
||||
// Fire audio on first frame
|
||||
if (!audioFired) {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audioFired = true
|
||||
}
|
||||
|
||||
@@ -2173,8 +2174,8 @@ try {
|
||||
sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize)
|
||||
|
||||
// Set upload parameters and trigger upload to queue
|
||||
audio.setSampleUploadLength(0, uploadSize)
|
||||
audio.startSampleUpload(0)
|
||||
audio.setSampleUploadLength(AUDIO_DEVICE, uploadSize)
|
||||
audio.startSampleUpload(AUDIO_DEVICE)
|
||||
|
||||
predecodedPcmOffset += uploadSize
|
||||
}
|
||||
@@ -2458,8 +2459,8 @@ finally {
|
||||
sys.poke(-1299460, 20)
|
||||
sys.poke(-1299460, 21)
|
||||
|
||||
audio.stop(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
}
|
||||
|
||||
graphics.setPalette(0, 0, 0, 0, 0)
|
||||
|
||||
@@ -289,7 +289,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) {
|
||||
let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength)
|
||||
printdbg(` decodedSampleLength: ${decodedSampleLength}`)
|
||||
|
||||
audio.putPcmDataByPtr(decodePtr, decodedSampleLength, 0)
|
||||
audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0)
|
||||
audio.setSampleUploadLength(0, decodedSampleLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
|
||||
|
||||
// ── 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
|
||||
// length: 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
|
||||
// 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,
|
||||
// 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 (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 t = offset + i / HW_SAMPLING_RATE
|
||||
const phase = (t * freq) % 1
|
||||
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||
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_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
|
||||
// length: 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
|
||||
// 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/LFSR calc only —
|
||||
// see makeSquare for details.
|
||||
//
|
||||
// LFSR types (1 and 2) are deterministic given (offset, freq): calling with
|
||||
// monotonically advancing offset values produces a seamless noise stream
|
||||
// LFSR types (1 and 2) are deterministic given (phaseOffset+offset, freq): calling
|
||||
// with monotonically advancing phaseOffset+offset produces a seamless noise stream
|
||||
// across frames. White noise types (-1, 0) are random per call.
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
|
||||
if (type === -1) {
|
||||
// 8-bit white: new random float in [-1, 1] each clock period
|
||||
let prevClock = -1
|
||||
let noiseVal = 0.0
|
||||
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) {
|
||||
prevClock = currentClock
|
||||
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 noiseVal = 1.0
|
||||
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) {
|
||||
prevClock = currentClock
|
||||
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)
|
||||
const mode = (type === 2) ? 1 : 0
|
||||
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
|
||||
// Advance to deterministic position for this offset so consecutive frame
|
||||
// calls with advancing offsets produce a seamless noise stream.
|
||||
const startClock = Math.floor(offset * freq) | 0
|
||||
// Advance to deterministic position for this tBase so consecutive frame
|
||||
// calls with monotonically advancing phaseOffset produce a seamless noise stream.
|
||||
const startClock = Math.floor(tBase * freq) | 0
|
||||
let lfsr = lfsrAdvance(1, startClock % period, mode)
|
||||
let prevClock = startClock
|
||||
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
|
||||
if (delta > 0) {
|
||||
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 ──────────────────────────────────────────────────
|
||||
|
||||
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))
|
||||
}
|
||||
// Wait for room in the playback queue (mirrors playwav.js idiom)
|
||||
while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
||||
audio.putPcmDataByPtr(stagingPtr, take * 2, 0)
|
||||
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
||||
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.startSampleUpload(playhead)
|
||||
remaining -= take
|
||||
@@ -336,6 +402,6 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
|
||||
exports = {
|
||||
HW_SAMPLING_RATE,
|
||||
makeBuffer, makeBufferNative, freeBufferNative, clearBuffer,
|
||||
makeSquare, makeTriangle, makeAliasedTriangle, makeNoise,
|
||||
sendBuffer
|
||||
makeSquare, makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES, makeNoise,
|
||||
sendBuffer, sendBufferFast
|
||||
}
|
||||
|
||||
@@ -1978,6 +1978,11 @@ Sound Adapter
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
0..1 RW: Play head #1 position
|
||||
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
|
||||
0..1 RW: Play head #0 position (how many samples has been queued)
|
||||
2..3 RW: Play head #0 length param
|
||||
4 RW: Play head #0 master volume
|
||||
5 RW: Play head #0 master pan
|
||||
6..9 RW: Play head #0 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
|
||||
10..11 RW:Play head #1 position (how many samples has been queued)
|
||||
12..13 RW:Play head #1 length param
|
||||
14 RW: Play head #1 master volume
|
||||
15 RW: Play head #1 master pan
|
||||
16..19 RW:Play head #1 flags
|
||||
|
||||
... auto-fill to Play head #4
|
||||
|
||||
@@ -2034,13 +2039,16 @@ Sound Adapter MMIO
|
||||
|
||||
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
|
||||
|
||||
42 WO: TAD Decoder Control
|
||||
Write 1 to decode TAD data
|
||||
43 RW: TAD Quality
|
||||
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)
|
||||
2368..4095 RW: MP2 Frame to be decoded
|
||||
|
||||
@@ -5,6 +5,23 @@ import net.torvald.tsvm.peripheral.AudioAdapter
|
||||
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.
|
||||
*/
|
||||
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 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 {
|
||||
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)!!
|
||||
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 getPcmQueueCapacityIndex(playhead: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex }
|
||||
|
||||
@@ -763,7 +763,7 @@ class VM(
|
||||
else if (dev is AudioAdapter) {
|
||||
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, 65536, 131072)) dev.pcmBin.ptr + fromRel - 65536
|
||||
else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin[dev.selectedPcmBin].ptr + fromRel - 65536
|
||||
else null
|
||||
}
|
||||
else if (dev is GraphicsAdapter) {
|
||||
|
||||
@@ -41,7 +41,7 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
||||
|
||||
// printdbg("P${playhead.index+1} go back to spinning")
|
||||
|
||||
Thread.sleep(12)
|
||||
Thread.sleep(6)
|
||||
}
|
||||
else if (playhead.isPlaying && writeQueue.isEmpty) {
|
||||
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
|
||||
// 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 playheads: Array<Playhead>
|
||||
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 mediaDecodedBin = UnsafeHelper.allocate(2304, this)
|
||||
|
||||
@Volatile private var mp2Busy = false
|
||||
|
||||
@Volatile var selectedPcmBin = 0
|
||||
|
||||
// TAD (Terrarum Advanced Audio) decoder buffers
|
||||
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)
|
||||
@@ -241,7 +248,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
renderRunnables = Array(4) { RenderRunnable(playheads[it]) }
|
||||
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") }
|
||||
|
||||
// printdbg("AudioAdapter latency: ${audioDevice.latency}")
|
||||
@@ -315,13 +322,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
42 -> -1 // TAD control (write-only)
|
||||
43 -> tadQuality.toByte()
|
||||
44 -> tadBusy.toInt().toByte()
|
||||
45 -> selectedPcmBin.toByte()
|
||||
in 64..2367 -> mediaDecodedBin[addr - 64]
|
||||
in 2368..4095 -> mediaFrameBin[addr - 2368]
|
||||
in 4096..4097 -> 0
|
||||
in 32768..65535 -> (adi - 32768).let {
|
||||
cueSheet[it / 16].read(it % 15)
|
||||
}
|
||||
in 65536..131071 -> pcmBin[addr - 65536]
|
||||
in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536]
|
||||
else -> {
|
||||
println("[AudioAdapter] Bus mirroring on mmio_reading while trying to read address $addr")
|
||||
mmio_read(addr % 131072)
|
||||
@@ -349,12 +357,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// TAD quality (0-5)
|
||||
tadQuality = bi.coerceIn(0, 5)
|
||||
}
|
||||
45 -> selectedPcmBin = bi % 4
|
||||
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)
|
||||
} }
|
||||
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()
|
||||
playheads.forEach { it.dispose() }
|
||||
sampleBin.destroy()
|
||||
pcmBin.destroy()
|
||||
pcmBin.forEach { it.destroy() }
|
||||
mediaFrameBin.destroy()
|
||||
mediaDecodedBin.destroy()
|
||||
tadInputBin.destroy()
|
||||
|
||||
Reference in New Issue
Block a user