mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
tracker impl
This commit is contained in:
@@ -18,7 +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 AUDIO_DEVICE = 0
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -81,9 +81,12 @@ function clearBuffer(buf, offsetSec, lengthSec) {
|
|||||||
// Re-silence a buffer region (fill with 128) for re-use across frames.
|
// Re-silence a buffer region (fill with 128) for re-use across frames.
|
||||||
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
|
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
|
||||||
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
|
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
|
||||||
for (let i = 0; i < total; i++) {
|
if (!buf.native) {
|
||||||
writeU8(buf, 0, start + i, 128)
|
buf[0].fill(128, start, start + total)
|
||||||
writeU8(buf, 1, start + i, 128)
|
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
|
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||||
// length: in seconds
|
// length: in seconds
|
||||||
// offset: 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
|
// 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 —
|
||||||
|
// see makeSquare for details.
|
||||||
if (duty == null) duty = 0.0
|
if (duty == null) duty = 0.0
|
||||||
if (op == null) op = 'add'
|
if (op == null) op = 'add'
|
||||||
if (amp == null) amp = 0.5
|
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
|
// riseFrac: fraction of period spent rising from -1 to +1
|
||||||
// 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw
|
// 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw
|
||||||
const riseFrac = (duty + 1.0) * 0.5
|
const riseFrac = (duty + 1.0) * 0.5
|
||||||
|
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
|
|
||||||
if (riseFrac <= 0) {
|
if (riseFrac <= 0) {
|
||||||
return 1.0 - 2.0 * phase // falling saw
|
return 1.0 - 2.0 * phase // falling saw
|
||||||
} else if (riseFrac >= 1) {
|
} 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
|
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||||
// Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style).
|
// Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style).
|
||||||
// The staircase quantisation introduces harmonics that mimic NES character.
|
// 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
|
// 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 —
|
||||||
|
// see makeSquare for details.
|
||||||
if (duty == null) duty = 0.0
|
if (duty == null) duty = 0.0
|
||||||
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 riseFrac = (duty + 1.0) * 0.5
|
const riseFrac = (duty + 1.0) * 0.5
|
||||||
|
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
|
|
||||||
let v
|
let v
|
||||||
if (riseFrac <= 0) {
|
if (riseFrac <= 0) {
|
||||||
v = 1.0 - 2.0 * phase
|
v = 1.0 - 2.0 * phase
|
||||||
|
|||||||
@@ -9,8 +9,21 @@ import net.torvald.tsvm.peripheral.MP2Env
|
|||||||
* Media decoders (MP2, TAD) are independent to the playheads and there is only one.
|
* Media decoders (MP2, TAD) are independent to the playheads and there is only one.
|
||||||
*
|
*
|
||||||
* NOTES:
|
* NOTES:
|
||||||
* 1. tracker mode is currently unimplemented.
|
* 1. Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks.
|
||||||
* 2. 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
|
* ## 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 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 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) {
|
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
|
||||||
@@ -80,7 +122,7 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
fun getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.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
|
||||||
fun getPcmQueueCapacity(playhead: Int) = getPlayhead(playhead)?.getPcmQueueCapacity()
|
fun getPcmQueueCapacity(playhead: Int) = getPlayhead(playhead)?.getPcmQueueCapacity()
|
||||||
|
|
||||||
fun resetParams(playhead: Int) {
|
fun resetParams(playhead: Int) {
|
||||||
|
|||||||
@@ -34,21 +34,22 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
|||||||
val samples = writeQueue.removeFirst()
|
val samples = writeQueue.removeFirst()
|
||||||
playhead.position = writeQueue.size
|
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)
|
playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size)
|
||||||
|
|
||||||
// printdbg("P${playhead.index+1} go back to spinning")
|
|
||||||
|
|
||||||
Thread.sleep(6)
|
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 ")
|
||||||
|
|
||||||
// TODO: wait for 1-2 seconds then finally stop the device
|
Thread.sleep(6)
|
||||||
// playhead.audioDevice.stop()
|
}
|
||||||
|
} 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)
|
Thread.sleep(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,11 +116,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
companion object {
|
companion object {
|
||||||
internal val DBGPRN = false
|
internal val DBGPRN = false
|
||||||
const val SAMPLING_RATE = 32000
|
const val SAMPLING_RATE = 32000
|
||||||
|
const val TRACKER_CHUNK = 512
|
||||||
|
const val TRACKER_C3 = 0x4000
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val sampleBin = UnsafeHelper.allocate(114687L, this)
|
internal val sampleBin = UnsafeHelper.allocate(114687L, this)
|
||||||
internal val instruments = Array(256) { TaudInst() }
|
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<Playhead>
|
internal val playheads: Array<Playhead>
|
||||||
internal val cueSheet = Array(2048) { PlayCue() }
|
internal val cueSheet = Array(2048) { PlayCue() }
|
||||||
internal val pcmBin = arrayOf(
|
internal val pcmBin = arrayOf(
|
||||||
@@ -327,7 +330,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
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 % 16)
|
||||||
}
|
}
|
||||||
in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536]
|
in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536]
|
||||||
else -> {
|
else -> {
|
||||||
@@ -361,7 +364,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
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 % 16, bi)
|
||||||
} }
|
} }
|
||||||
in 65536..131071 -> { pcmBin[selectedPcmBin][addr - 65536] = byte }
|
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(
|
internal data class PlayCue(
|
||||||
val patterns: IntArray = IntArray(15) { it },
|
val patterns: IntArray = IntArray(15) { it },
|
||||||
var instruction: PlayInstruction = PlayInstNop
|
var instruction: PlayInstruction = PlayInstNop
|
||||||
) {
|
) {
|
||||||
fun write(index: Int, byte: Int) = when (index) {
|
fun write(index: Int, byte: Int) = when (index) {
|
||||||
in 0..14 -> { patterns[index] = byte }
|
in 0..14 -> { patterns[index] = byte }
|
||||||
15 -> { instruction = when (byte) {
|
15 -> { instruction = when {
|
||||||
in 128..255 -> PlayInstGoBack(byte and 127)
|
byte >= 128 -> PlayInstGoBack(byte and 127)
|
||||||
// in 64..127 -> Inst(byte and 63)
|
byte in 16..31 -> PlayInstSkip(byte and 15)
|
||||||
// in 32..63 -> Inst(byte and 31)
|
else -> PlayInstNop
|
||||||
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")
|
|
||||||
} }
|
} }
|
||||||
else -> throw InternalError("Bad offset $index")
|
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 class PlayInstSkip(arg: Int) : PlayInstruction(arg)
|
||||||
internal object PlayInstNop : PlayInstruction(0)
|
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(
|
class Playhead(
|
||||||
private val parent: AudioAdapter,
|
internal val parent: AudioAdapter,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
|
|
||||||
var position: Int = 0,
|
var position: Int = 0,
|
||||||
@@ -1124,20 +1319,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var pcmQueueSizeIndex: Int = 0,
|
var pcmQueueSizeIndex: Int = 0,
|
||||||
val audioDevice: OpenALBufferedAudioDevice,
|
val audioDevice: OpenALBufferedAudioDevice,
|
||||||
) {
|
) {
|
||||||
|
var trackerState: TrackerState? = TrackerState() // default mode is tracker (isPcmMode=false)
|
||||||
|
|
||||||
// flags
|
// flags
|
||||||
var isPcmMode: Boolean = false
|
var isPcmMode: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value != isPcmMode) {
|
if (value != field) {
|
||||||
resetParams()
|
resetParams()
|
||||||
|
trackerState = if (!value) TrackerState() else null
|
||||||
}
|
}
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var isPlaying: Boolean = false
|
var isPlaying: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
// play last bit from the buffer by feeding 0s
|
// play last bit from the buffer by feeding 0s
|
||||||
if (isPlaying && !value) {
|
if (field && !value) {
|
||||||
// println("!! inserting dummy bytes")
|
// println("!! inserting dummy bytes")
|
||||||
pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount))
|
if (isPcmMode) {
|
||||||
|
pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
@@ -1159,10 +1359,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
fun write(index: Int, byte: Int) {
|
fun write(index: Int, byte: Int) {
|
||||||
val byte = byte and 255
|
val byte = byte and 255
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> if (!isPcmMode) { position = position.and(0xff00) or position } else {}
|
0 -> if (!isPcmMode) { position = (position and 0xff00) or byte; trackerState?.cuePos = position } else {}
|
||||||
1 -> if (!isPcmMode) { position = position.and(0x00ff) or position.shl(8) } else {}
|
1 -> if (!isPcmMode) { position = (position and 0x00ff) or (byte shl 8); trackerState?.cuePos = position } else {}
|
||||||
2 -> { pcmUploadLength = pcmUploadLength.and(0xff00) or pcmUploadLength }
|
2 -> { pcmUploadLength = (pcmUploadLength and 0xff00) or byte }
|
||||||
3 -> { pcmUploadLength = pcmUploadLength.and(0x00ff) or pcmUploadLength.shl(8) }
|
3 -> { pcmUploadLength = (pcmUploadLength and 0x00ff) or (byte shl 8) }
|
||||||
4 -> {
|
4 -> {
|
||||||
masterVolume = byte
|
masterVolume = byte
|
||||||
audioDevice.setVolume(masterVolume / 255f)
|
audioDevice.setVolume(masterVolume / 255f)
|
||||||
@@ -1170,10 +1370,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
5 -> { masterPan = byte }
|
5 -> { masterPan = byte }
|
||||||
6 -> { byte.let {
|
6 -> { byte.let {
|
||||||
isPcmMode = (it and 0b10000000) != 0
|
isPcmMode = (it and 0b10000000) != 0
|
||||||
|
if (it and 0b01000000 != 0) resetParams()
|
||||||
isPlaying = (it and 0b00010000) != 0
|
isPlaying = (it and 0b00010000) != 0
|
||||||
pcmQueueSizeIndex = (it and 0b00001111)
|
pcmQueueSizeIndex = (it and 0b00001111)
|
||||||
|
|
||||||
|
|
||||||
if (it and 0b00100000 != 0) purgeQueue()
|
if (it and 0b00100000 != 0) purgeQueue()
|
||||||
} }
|
} }
|
||||||
7 -> if (isPcmMode) { pcmUpload = true } else {}
|
7 -> if (isPcmMode) { pcmUpload = true } else {}
|
||||||
@@ -1195,6 +1394,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
pcmUploadLength = 0
|
pcmUploadLength = 0
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
pcmQueueSizeIndex = 2
|
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() {
|
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()
|
12 -> (samplePtr.ushr(16).and(1).shl(7) or loopMode.and(3)).toByte()
|
||||||
13,14,15 -> -1
|
13,14,15 -> -1
|
||||||
in 16..63 step 2 -> envelopes[offset - 16].volume.toByte()
|
in 16..63 step 2 -> envelopes[(offset - 16) / 2].volume.toByte()
|
||||||
in 17..63 step 2 -> envelopes[offset - 16].offset.index.toByte()
|
in 17..63 step 2 -> envelopes[(offset - 17) / 2].offset.index.toByte()
|
||||||
else -> throw InternalError("Bad offset $offset")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setByte(offset: Int, byte: Int) = when (offset) {
|
fun setByte(offset: Int, byte: Int) = when (offset) {
|
||||||
0 -> { samplePtr = samplePtr.and(0x1ff00) or byte }
|
0 -> { samplePtr = (samplePtr and 0x1ff00) or byte }
|
||||||
1 -> { samplePtr = samplePtr.and(0x000ff) or byte.shl(8) }
|
1 -> { samplePtr = (samplePtr and 0x000ff) or (byte shl 8) }
|
||||||
|
|
||||||
2 -> { sampleLength = sampleLength.and(0x1ff00) or byte }
|
2 -> { sampleLength = (sampleLength and 0xff00) or byte }
|
||||||
3 -> { sampleLength = sampleLength.and(0x000ff) or byte.shl(8) }
|
3 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
4 -> { samplingRate = samplingRate.and(0x1ff00) or byte }
|
4 -> { samplingRate = (samplingRate and 0xff00) or byte }
|
||||||
5 -> { samplingRate = samplingRate.and(0x000ff) or byte.shl(8) }
|
5 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
6 -> { sampleLoopStart = sampleLoopStart.and(0x1ff00) or byte }
|
6 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte }
|
||||||
7 -> { sampleLoopStart = sampleLoopStart.and(0x000ff) or byte.shl(8) }
|
7 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
8 -> { sampleLoopEnd = sampleLoopEnd.and(0x1ff00) or byte }
|
8 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte }
|
||||||
9 -> { sampleLoopEnd = sampleLoopEnd.and(0x000ff) or byte.shl(8) }
|
9 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
10 -> {
|
10 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
|
||||||
if (byte.and(0b1000_0000) != 0)
|
11 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
|
||||||
samplePtr = samplePtr or 0x10000
|
|
||||||
|
|
||||||
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 16..63 step 2 -> envelopes[(offset - 16) / 2].volume = byte
|
||||||
in 17..63 step 2 -> envelopes[offset - 16].offset = ThreeFiveMiniUfloat(byte)
|
in 17..63 step 2 -> envelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)
|
||||||
else -> throw InternalError("Bad offset $offset")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,20 @@ class OpenALBufferedAudioDevice(
|
|||||||
writeSamples(bytes!!, 0, numSamples * 2)
|
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) {
|
override fun writeSamples(samples: ShortArray, offset: Int, numSamples: Int) {
|
||||||
if (bytes == null || bytes!!.size < numSamples * 2) bytes = ByteArray(numSamples * 2)
|
if (bytes == null || bytes!!.size < numSamples * 2) bytes = ByteArray(numSamples * 2)
|
||||||
val end = Math.min(offset + numSamples, samples.size)
|
val end = Math.min(offset + numSamples, samples.size)
|
||||||
|
|||||||
Reference in New Issue
Block a user