audio: getFreePlayhead()

This commit is contained in:
minjaesong
2026-06-07 02:21:21 +09:00
parent 3444bdf63b
commit aa45c2194f
11 changed files with 124 additions and 81 deletions

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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

View File

@@ -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.}

View File

@@ -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