mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-23 12:44:04 +09:00
taut: editable patterns
This commit is contained in:
@@ -68,6 +68,26 @@ class AudioJSR223Delegate(private val vm: VM) {
|
||||
fun stop(playhead: Int) { getPlayhead(playhead)?.isPlaying = false }
|
||||
fun isPlaying(playhead: Int) = getPlayhead(playhead)?.isPlaying
|
||||
|
||||
/**
|
||||
* Audition a single note on [voice] of a tracker-mode [playhead] WITHOUT starting song
|
||||
* playback — the note sounds immediately and its envelope/filter evolve, but rows/cues do
|
||||
* not advance. Intended for note-jamming in an editor (taut). [note] is the 16-bit pattern
|
||||
* note word (0x0020..0xFFFF playable; 0x0001 key-off / 0x0002 cut also work), [inst] the
|
||||
* instrument slot to trigger with. No-op in PCM mode. Stop it with [jamStop].
|
||||
*/
|
||||
fun jamNote(playhead: Int, voice: Int, note: Int, inst: Int) {
|
||||
val ad = getFirstSnd() ?: return
|
||||
val ph = getPlayhead(playhead) ?: return
|
||||
ad.jamNote(ph, voice, note and 0xFFFF, inst and 0xFF)
|
||||
}
|
||||
|
||||
/** Silence any audition started by [jamNote] on this [playhead]. */
|
||||
fun jamStop(playhead: Int) {
|
||||
val ad = getFirstSnd() ?: return
|
||||
val ph = getPlayhead(playhead) ?: return
|
||||
ad.jamStop(ph)
|
||||
}
|
||||
|
||||
/** 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). */
|
||||
|
||||
@@ -71,8 +71,9 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Tracker mode
|
||||
if (playhead.isPlaying) {
|
||||
// Tracker mode — also render while a jam (audition) note is sounding
|
||||
// even though the transport is stopped.
|
||||
if (playhead.isPlaying || playhead.jamActive) {
|
||||
val out = playhead.parent.generateTrackerAudio(playhead)
|
||||
if (out != null) {
|
||||
playhead.audioDevice.writeStereoSamplesUI8(out, 0, AudioAdapter.TRACKER_CHUNK)
|
||||
@@ -4051,7 +4052,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
val out = ByteArray(TRACKER_CHUNK * 2)
|
||||
|
||||
if (ts.firstRow) {
|
||||
// Jam (audition) mode: voices triggered by jamNote are mixed while the song is
|
||||
// stopped, but WITHOUT advancing rows/cues — only the per-tick voice machinery
|
||||
// (applyTrackerTick) runs so the jammed note's envelope / filter / fadeout evolve.
|
||||
// When the song is actually playing, isPlaying takes precedence and the full
|
||||
// tick/row path runs exactly as before (byte-identical to the pre-jam engine).
|
||||
val advancing = playhead.isPlaying
|
||||
|
||||
if (advancing && ts.firstRow) {
|
||||
ts.firstRow = false
|
||||
applyTrackerRow(ts, playhead)
|
||||
}
|
||||
@@ -4059,14 +4067,22 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
for (n in 0 until TRACKER_CHUNK) {
|
||||
// Recompute samples-per-tick every iteration since T/T-slide can mutate BPM mid-row.
|
||||
val spt = SAMPLING_RATE * 2.5 / playhead.bpm
|
||||
ts.samplesIntoTick += 1.0
|
||||
if (ts.samplesIntoTick >= spt) {
|
||||
ts.samplesIntoTick -= spt
|
||||
applyTrackerTick(ts, playhead)
|
||||
ts.tickInRow++
|
||||
if (ts.tickInRow >= playhead.tickRate + ts.finePatternDelayExtra) {
|
||||
ts.tickInRow = 0
|
||||
advanceRow(ts, playhead)
|
||||
if (advancing) {
|
||||
ts.samplesIntoTick += 1.0
|
||||
if (ts.samplesIntoTick >= spt) {
|
||||
ts.samplesIntoTick -= spt
|
||||
applyTrackerTick(ts, playhead)
|
||||
ts.tickInRow++
|
||||
if (ts.tickInRow >= playhead.tickRate + ts.finePatternDelayExtra) {
|
||||
ts.tickInRow = 0
|
||||
advanceRow(ts, playhead)
|
||||
}
|
||||
}
|
||||
} else { // jamActive: evolve envelopes only, never advance the song
|
||||
ts.samplesIntoTick += 1.0
|
||||
if (ts.samplesIntoTick >= spt) {
|
||||
ts.samplesIntoTick -= spt
|
||||
applyTrackerTick(ts, playhead)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4210,9 +4226,38 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
out[n * 2 + 1] = tadDecodedBin[n * 2L + 1]
|
||||
}
|
||||
|
||||
// Stop the jam-render spin once the audition has gone fully silent (no foreground
|
||||
// or background voice left to mix). A real Play press re-arms via isPlaying.
|
||||
if (playhead.jamActive && !playhead.isPlaying &&
|
||||
ts.voices.none { it.active } && ts.backgroundVoices.none { it.active }) {
|
||||
playhead.jamActive = false
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Audition one note immediately on [vi] of [ph]'s tracker state, without starting
|
||||
* song playback (jam mode keeps mixing the voice but never advances rows/cues).
|
||||
* Reuses the normal trigger path so Ixmp patches / Metainstrument layers resolve.
|
||||
*/
|
||||
internal fun jamNote(ph: Playhead, vi: Int, note: Int, inst: Int) {
|
||||
if (ph.isPcmMode) return
|
||||
val ts = ph.trackerState ?: return
|
||||
val v = vi.coerceIn(0, 19)
|
||||
triggerMetaOrNote(ts, ts.voices[v], v, note, inst, -1)
|
||||
ph.jamActive = true
|
||||
}
|
||||
|
||||
/** Silence any running audition and stop the jam-render spin. */
|
||||
internal fun jamStop(ph: Playhead) {
|
||||
ph.trackerState?.let { ts ->
|
||||
ts.voices.forEach { it.active = false }
|
||||
ts.backgroundVoices.forEach { it.active = false }
|
||||
}
|
||||
ph.jamActive = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance to the next row. Resolves pending B/C jumps and pattern-delay repeats.
|
||||
* Called once when [TrackerState.tickInRow] has just wrapped past [Playhead.tickRate].
|
||||
@@ -4837,6 +4882,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
var trackerState: TrackerState? = TrackerState() // default mode is tracker (isPcmMode=false)
|
||||
|
||||
// True while a jamNote() audition is sounding with the transport stopped. The render
|
||||
// thread keeps mixing (envelopes evolve via applyTrackerTick) but rows/cues do NOT
|
||||
// advance; auto-cleared once every voice goes silent. Ignored while isPlaying.
|
||||
var jamActive: Boolean = false
|
||||
|
||||
// Initial global behaviour flags (song-table byte, written via MMIO register 7 in tracker mode).
|
||||
// Applied to TrackerState on every resetParams(); in-pattern effect '1' can override later.
|
||||
var initialGlobalFlags: Int = 0
|
||||
@@ -4859,6 +4909,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
pcmQueue.add(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount))
|
||||
}
|
||||
}
|
||||
// Starting real playback ends any jam audition: drop the leftover jammed voices
|
||||
// so a held audition can't bleed into the first rows of the song.
|
||||
if (!field && value && jamActive) {
|
||||
trackerState?.let { ts ->
|
||||
ts.voices.forEach { it.active = false }
|
||||
ts.backgroundVoices.forEach { it.active = false }
|
||||
}
|
||||
jamActive = false
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
@@ -4917,6 +4976,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
position = 0
|
||||
pcmUploadLength = 0
|
||||
isPlaying = false
|
||||
jamActive = false
|
||||
pcmQueueSizeIndex = 2
|
||||
// Spec §5 defaults — applied on every reset so song-start state is well-defined.
|
||||
bpm = 125
|
||||
|
||||
Reference in New Issue
Block a user