diff --git a/assets/disk0/tvdos/bin/playmp2.js b/assets/disk0/tvdos/bin/playmp2.js index 36a4893..b2868f9 100644 --- a/assets/disk0/tvdos/bin/playmp2.js +++ b/assets/disk0/tvdos/bin/playmp2.js @@ -57,13 +57,17 @@ let decodedLength = 0 const bufRealTimeLen = 36 // one MP2 frame at 32 kHz ≈ 36 ms -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setPcmQueueCapacityIndex(0, 2) -const QUEUE_MAX = audio.getPcmQueueCapacity(0) -audio.setMasterVolume(0, 255) -audio.play(0) +// Occupy the first idle playhead rather than always grabbing #0, so playback +// doesn't cut off audio already running on another playhead. Falls back to #0 +// when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +audio.setPcmMode(PLAYHEAD) +audio.setPcmQueueCapacityIndex(PLAYHEAD, 2) +const QUEUE_MAX = audio.getPcmQueueCapacity(PLAYHEAD) +audio.setMasterVolume(PLAYHEAD, 255) +audio.play(PLAYHEAD) audio.mp2Init() function bytesToSec(i) { return i / (FRAME_SIZE * 1000 / bufRealTimeLen) } @@ -91,8 +95,8 @@ try { gui.audioFeedPcm(mp2VisScratch, MP2_VIS_SAMPLE_COUNT) } - if (audio.getPosition(0) >= QUEUE_MAX) { - while (audio.getPosition(0) >= (QUEUE_MAX >>> 1)) { + if (audio.getPosition(PLAYHEAD) >= QUEUE_MAX) { + while (audio.getPosition(PLAYHEAD) >= (QUEUE_MAX >>> 1)) { if (interactive) gui.audioRender() sys.sleep(bufRealTimeLen) } diff --git a/assets/disk0/tvdos/bin/playmv1.js b/assets/disk0/tvdos/bin/playmv1.js index 955f76d..5b4b96e 100644 --- a/assets/disk0/tvdos/bin/playmv1.js +++ b/assets/disk0/tvdos/bin/playmv1.js @@ -97,10 +97,14 @@ let startTime = sys.nanoTime() let framesRead = 0 let audioFired = false -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setMasterVolume(0, 255) +// Occupy the first idle playhead rather than always grabbing #0, so playback +// doesn't cut off audio already running on another playhead. Falls back to #0 +// when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +audio.setPcmMode(PLAYHEAD) +audio.setMasterVolume(PLAYHEAD, 255) function s16StTou8St(inPtrL, inPtrR, outPtr, length) { for (let k = 0; k < length; k+=2) { @@ -204,7 +208,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) { // defer audio playback until a first frame is sent if (!audioFired) { - audio.play(0) + audio.play(PLAYHEAD) audioFired = true } @@ -263,7 +267,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) { // defer audio playback until a first frame is sent if (!audioFired) { - audio.play(0) + audio.play(PLAYHEAD) audioFired = true } @@ -326,9 +330,9 @@ 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(0, frame, readLength, 0) - audio.setSampleUploadLength(0, readLength) - audio.startSampleUpload(0) + audio.putPcmDataByPtr(PLAYHEAD, frame, readLength, 0) + audio.setSampleUploadLength(PLAYHEAD, readLength) + audio.startSampleUpload(PLAYHEAD) sys.free(frame) } else { @@ -382,14 +386,14 @@ finally { if (AUDIO_QUEUE_BYTES > 0 && AUDIO_QUEUE_LENGTH > 1) { } - //audio.stop(0) + //audio.stop(PLAYHEAD) let timeTook = (endTime - startTime) / 1000000000.0 //println(`Actual FPS: ${framesRendered / timeTook}`) - audio.stop(0) - audio.purgeQueue(0) + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) if (interactive) { con.clear() diff --git a/assets/disk0/tvdos/bin/playpcm.js b/assets/disk0/tvdos/bin/playpcm.js index e83a183..23782ac 100644 --- a/assets/disk0/tvdos/bin/playpcm.js +++ b/assets/disk0/tvdos/bin/playpcm.js @@ -23,10 +23,14 @@ function bytesToSec(i) { return i / byterate } seqread.prepare(filePath) const readPtr = sys.malloc(BLOCK_SIZE) -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setMasterVolume(0, 255) +// Occupy the first idle playhead rather than always grabbing #0, so playback +// doesn't cut off audio already running on another playhead. Falls back to #0 +// when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +audio.setPcmMode(PLAYHEAD) +audio.setMasterVolume(PLAYHEAD, 255) if (interactive) { gui.audioInit({ @@ -42,7 +46,7 @@ try { while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) { if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } - const queueSize = audio.getPosition(0) + const queueSize = audio.getPosition(PLAYHEAD) if (queueSize <= 1) { for (let repeat = QUEUE_MAX - queueSize; repeat > 0; repeat--) { const remainingBytes = FILE_SIZE - seqread.getReadCount() @@ -54,13 +58,13 @@ try { // Raw PCMu8 stereo — sampleCount = bytes / 2. if (interactive) gui.audioFeedPcm(readPtr, readLength >> 1) - audio.putPcmDataByPtr(0, readPtr, readLength, 0) - audio.setSampleUploadLength(0, readLength) - audio.startSampleUpload(0) + audio.putPcmDataByPtr(PLAYHEAD, readPtr, readLength, 0) + audio.setSampleUploadLength(PLAYHEAD, readLength) + audio.startSampleUpload(PLAYHEAD) if (repeat > 1) sys.sleep(10) } - audio.play(0) + audio.play(PLAYHEAD) } if (interactive) { diff --git a/assets/disk0/tvdos/bin/playtad.js b/assets/disk0/tvdos/bin/playtad.js index 8c64d4d..47d02f9 100644 --- a/assets/disk0/tvdos/bin/playtad.js +++ b/assets/disk0/tvdos/bin/playtad.js @@ -109,13 +109,17 @@ function bytesToSec(i) { return Math.round((i / FILE_SIZE) * (FILE_SIZE / AVG_CHUNK_SIZE) * (bufRealTimeLen / 1000)) } -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setPcmQueueCapacityIndex(0, 2) -const QUEUE_MAX = audio.getPcmQueueCapacity(0) -audio.setMasterVolume(0, 255) -audio.play(0) +// Occupy the first idle playhead rather than always grabbing #0, so playback +// doesn't cut off audio already running on another playhead. Falls back to #0 +// when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +audio.setPcmMode(PLAYHEAD) +audio.setPcmQueueCapacityIndex(PLAYHEAD, 2) +const QUEUE_MAX = audio.getPcmQueueCapacity(PLAYHEAD) +audio.setMasterVolume(PLAYHEAD, 255) +audio.play(PLAYHEAD) if (interactive) { gui.audioInit({ @@ -163,7 +167,7 @@ try { filebuf.readBytes(7 + payloadSize, TAD_INPUT_ADDR) audio.tadDecode() - audio.tadUploadDecoded(0, sampleCount) + audio.tadUploadDecoded(PLAYHEAD, sampleCount) // After upload tadDecodedBin still holds the chunk until the next // tadDecode call, so it's safe to keep slicing samples out of it // during the playback wait below. diff --git a/assets/disk0/tvdos/bin/playtaud.js b/assets/disk0/tvdos/bin/playtaud.js index 6860af3..0b5e9a0 100644 --- a/assets/disk0/tvdos/bin/playtaud.js +++ b/assets/disk0/tvdos/bin/playtaud.js @@ -304,9 +304,13 @@ function parseTaud(path, songIndex) { const song = parseTaud(filePath, songArg) // ── Hand the file to the audio adapter ───────────────────────────────────── -audio.resetParams(0) -audio.purgeQueue(0) -taud.uploadTaudFile(filePath, songArg, 0) +// Occupy the first idle playhead rather than always grabbing #0, so launching +// playtaud doesn't cut off music already playing on another playhead. Falls +// back to #0 when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +taud.uploadTaudFile(filePath, songArg, PLAYHEAD) // ── Instrument archetype classification ───────────────────────────────────── // @@ -565,8 +569,8 @@ function pad(n, w) { let lastStatus = '' function drawStatus(curCue) { - const bpm = audio.getBPM(0) || song.bpm - const tick = audio.getTickRate(0) || song.tickRate + const bpm = audio.getBPM(PLAYHEAD) || song.bpm + const tick = audio.getTickRate(PLAYHEAD) || song.tickRate const cueStr = pad(curCue, 3) + '/' + pad(song.lastCue, 3) const s = 'BPM ' + pad(bpm,3) + ' Tick ' + pad(tick,2) + ' Voices ' + pad(song.numVoices,2) + ' Cue ' + cueStr @@ -714,7 +718,7 @@ function spawnEventsForRow(cueIdx, rowIdx) { const arch = archByInst[effInst] let pan = 128 if (panSel === 0) pan = (panVal / 63 * 255) | 0 - const livePan = audio.getVoiceEffectivePan(0, v) + const livePan = audio.getVoiceEffectivePan(PLAYHEAD, v) if (typeof livePan === 'number' && livePan !== 128) pan = livePan // Replace whatever was in voice v's slot. peakVol seeds at 0 and is // tracked per-frame so the colour ramp normalises by attack peak, @@ -1058,11 +1062,11 @@ function renderEvents() { // The engine's `active` flag is the source of truth — set by note-on, // cleared by note-cut, sample-end, envelope-end-of-decay, or NNA cut. // Once it drops, the voice is genuinely silent so the visual goes too. - if (!audio.getVoiceActive(0, v)) { events[v] = null; continue } + if (!audio.getVoiceActive(PLAYHEAD, v)) { events[v] = null; continue } - const liveVol = audio.getVoiceEffectiveVolume(0, v) || 0 - const livePan = audio.getVoiceEffectivePan(0, v) - const liveNote = audio.getVoiceNote(0, v) + const liveVol = audio.getVoiceEffectiveVolume(PLAYHEAD, v) || 0 + const livePan = audio.getVoiceEffectivePan(PLAYHEAD, v) + const liveNote = audio.getVoiceNote(PLAYHEAD, v) if (liveVol > ev.peakVol) ev.peakVol = liveVol ev.ageFrames++ @@ -1094,10 +1098,10 @@ function drawStereo() { const W = LANE_W const bins = new Float32Array(W) for (let v = 0; v < song.numVoices; v++) { - if (!audio.getVoiceActive(0, v)) continue - const vol = Math.pow(audio.getVoiceEffectiveVolume(0, v) || 0, 0.125) + if (!audio.getVoiceActive(PLAYHEAD, v)) continue + const vol = Math.pow(audio.getVoiceEffectiveVolume(PLAYHEAD, v) || 0, 0.125) if (vol <= 0) continue - const pan = audio.getVoiceEffectivePan(0, v) + const pan = audio.getVoiceEffectivePan(PLAYHEAD, v) let col = Math.round((pan / 255) * (W - 1)) if (col < 0) col = 0 if (col >= W) col = W - 1 @@ -1143,7 +1147,7 @@ function drawTickLights(tickInRow, tickRate) { // Voice activity counter on the right. let nActive = 0 for (let v = 0; v < song.numVoices; v++) { - if (audio.getVoiceActive(0, v)) nActive++ + if (audio.getVoiceActive(PLAYHEAD, v)) nActive++ } colour(COL_DIM, COL_BG) const s = 'ACTIVE ' + pad(nActive, 2) + '/' + pad(song.numVoices, 2) @@ -1157,10 +1161,10 @@ drawStatus(0) drawOrderStrip(0) // ── Playback ──────────────────────────────────────────────────────────────── -audio.setCuePosition(0, 0) -audio.setTrackerRow(0, 0) -audio.setMasterVolume(0, 255) -audio.play(0) +audio.setCuePosition(PLAYHEAD, 0) +audio.setTrackerRow(PLAYHEAD, 0) +audio.setMasterVolume(PLAYHEAD, 255) +audio.play(PLAYHEAD) let stopReq = false let errorlevel = 0 @@ -1174,13 +1178,13 @@ let errorlevel = 0 let ticksPerRow = Math.max(1, song.tickRate) let synthTick = 0 // tick within current row, 0..ticksPerRow-1 try { - while (audio.isPlaying(0) && !stopReq) { + while (audio.isPlaying(PLAYHEAD) && !stopReq) { // Backspace polling (mirrors playtad). sys.poke(-40, 1) if (sys.peek(-41) === 67) stopReq = true - const curCue = audio.getCuePosition(0) - const curRow = audio.getTrackerRow(0) + const curCue = audio.getCuePosition(PLAYHEAD) + const curRow = audio.getTrackerRow(PLAYHEAD) if (curCue !== lastSeenCue || curRow !== lastSeenRow) { // Row boundary — spawn new events, advance the matrix background // (scrolls within a cue, wraps to the top on a cue change), reset @@ -1192,7 +1196,7 @@ try { synthTick = 0 // Pull a fresh tickRate read here in case a T effect changed it // mid-song. - ticksPerRow = Math.max(1, audio.getTickRate(0) || song.tickRate) + ticksPerRow = Math.max(1, audio.getTickRate(PLAYHEAD) || song.tickRate) } else { // Same row — advance the synthetic tick counter against wall time. // Tick period (ms) = (60000 / BPM) / 24 ... but the spec is @@ -1212,7 +1216,7 @@ try { drawStereo() drawTickLights(synthTick, ticksPerRow) - sys.sleep((2500 / audio.getBPM(0))|0) // one visual frame = one tick + sys.sleep((2500 / audio.getBPM(PLAYHEAD))|0) // one visual frame = one tick } } catch (e) { @@ -1220,7 +1224,7 @@ catch (e) { errorlevel = 1 } finally { - audio.stop(0) + audio.stop(PLAYHEAD) con.move(ROW_BOT_BORDER + 1, 1) con.curs_set(1) } diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 390d672..6d46c11 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 = 0 +const AUDIO_DEVICE = audio.getFreePlayhead(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/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index f9d871d..cd94e5d 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -100,10 +100,14 @@ graphics.clearPixels(0) graphics.clearPixels2(0) // Initialize audio -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setMasterVolume(0, 255) +// Occupy the first idle playhead rather than always grabbing #0, so playback +// doesn't cut off audio already running on another playhead. Falls back to #0 +// when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) +audio.resetParams(PLAYHEAD) +audio.purgeQueue(PLAYHEAD) +audio.setPcmMode(PLAYHEAD) +audio.setMasterVolume(PLAYHEAD, 255) // set colour zero as half-opaque black graphics.setPalette(0, 0, 0, 0, 9) @@ -791,14 +795,14 @@ try { if (isInterlaced) { // fire audio after frame 1 if (!audioFired && frameCount > 0) { - audio.play(0) + audio.play(PLAYHEAD) audioFired = true } } else { // fire audio after frame 0 if (!audioFired) { - audio.play(0) + audio.play(PLAYHEAD) audioFired = true } } @@ -900,8 +904,8 @@ finally { if (PREV_FIELD_BUFFER > 0) sys.free(PREV_FIELD_BUFFER) if (NEXT_FIELD_BUFFER > 0) sys.free(NEXT_FIELD_BUFFER) - audio.stop(0) - audio.purgeQueue(0) + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) if (interactive) { //con.clear() diff --git a/assets/disk0/tvdos/bin/playwav.js b/assets/disk0/tvdos/bin/playwav.js index 0358857..85710b0 100644 --- a/assets/disk0/tvdos/bin/playwav.js +++ b/assets/disk0/tvdos/bin/playwav.js @@ -131,16 +131,20 @@ try { readPtr = sys.malloc(pcmType === 2 ? BLOCK_SIZE : BLOCK_SIZE * bitsPerSample / 8) decodePtr = sys.malloc(BLOCK_SIZE * pcm.HW_SAMPLING_RATE / samplingRate) - audio.resetParams(0) - audio.purgeQueue(0) - audio.setPcmMode(0) - audio.setMasterVolume(0, 255) + // Occupy the first idle playhead rather than always grabbing #0, so + // playback doesn't cut off audio already running on another playhead. + // Falls back to #0 when all four are busy. + const PLAYHEAD = audio.getFreePlayhead(0) + audio.resetParams(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) + audio.setPcmMode(PLAYHEAD) + audio.setMasterVolume(PLAYHEAD, 255) let readLength = 1 while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) { if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } - if (audio.getPosition(0) <= 1) { + if (audio.getPosition(PLAYHEAD) <= 1) { for (let repeat = 0; repeat < QUEUE_MAX; repeat++) { const remainingBytes = FILE_SIZE - 8 - seqread.getReadCount() readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE @@ -153,13 +157,13 @@ try { // before queueing — the buffer is reused next iteration. if (interactive) gui.audioFeedPcm(decodePtr, decodedSampleLength >> 1) - audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0) - audio.setSampleUploadLength(0, decodedSampleLength) - audio.startSampleUpload(0) + audio.putPcmDataByPtr(PLAYHEAD, decodePtr, decodedSampleLength, 0) + audio.setSampleUploadLength(PLAYHEAD, decodedSampleLength) + audio.startSampleUpload(PLAYHEAD) sys.spin() } - audio.play(0) + audio.play(PLAYHEAD) } if (interactive) { diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 4179574..7fd461c 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -5241,7 +5241,10 @@ const panels = [panelTimeline, panelOrders, panelPatterns, panelSamples, panelIn // PLAYBACK STATE ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -const PLAYHEAD = 0 +// Occupy the first idle playhead rather than always grabbing #0, so launching +// taut doesn't cut off music already playing on another playhead. Falls back to +// #0 when all four are busy. +const PLAYHEAD = audio.getFreePlayhead(0) // Scratch cue slot used for pattern-only preview; beyond any real cue the song uses const PREVIEW_CUE_IDX = NUM_CUES - 1 diff --git a/doc/implementation.tex b/doc/implementation.tex index 85dae36..90cbbd7 100644 --- a/doc/implementation.tex +++ b/doc/implementation.tex @@ -1162,6 +1162,7 @@ This guide deliberately stops at that overview. The Taud file format, the instru \1\formalsynopsis{play}{playhead: Int}{Starts the playhead.} \1\formalsynopsis{stop}{playhead: Int}{Stops the playhead.} \1\formalsynopsis{isPlaying}{playhead: Int}[Boolean]{Whether the playhead is currently playing.} +\1\formalsynopsis{getFreePlayhead}{fallback: Int}[Int]{Returns the lowest-numbered playhead that is not currently playing.} \1\formalsynopsis{getPosition}{playhead: Int}[Int]{Current playback position of the playhead.} \1\formalsynopsis{setMasterVolume}{playhead: Int, volume: Int}{Sets the playhead's output volume.} \1\formalsynopsis{setMasterPan}{playhead: Int, pan: Int}{Sets the playhead's stereo pan.} diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 94244d9..e3190e3 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -67,6 +67,17 @@ class AudioJSR223Delegate(private val vm: VM) { fun stop(playhead: Int) { getPlayhead(playhead)?.isPlaying = false } fun isPlaying(playhead: Int) = getPlayhead(playhead)?.isPlaying + /** Lowest-numbered playhead that is not currently playing, so a player app can + * "occupy" an idle playhead instead of always clobbering playhead 0. Returns + * [fallback] when every playhead is busy (or no audio device is present). */ + fun getFreePlayhead(fallback: Int): Int { + val playheads = getFirstSnd()?.playheads ?: return fallback + for (i in playheads.indices) { + if (!playheads[i].isPlaying) return i + } + return fallback + } + // fun setPosition(playhead: Int, pos: Int) { getPlayhead(playhead)?.position = pos and 65535 } fun getPosition(playhead: Int) = getPlayhead(playhead)?.position