Play starts from the arrangement selection; Stop clears it (v0.19.0)
Highlight a cell on the arrangement roll and Play now begins at that selection's first beat instead of always from the top. Stop clears the highlight, so the next Play resumes from the beginning. The rectangular selection is hoisted from ArrangementRoll to the ViewModel (four ints) so the transport can read/clear it; engine.play() gains a startBeat that seeks playlistIndex in the built playlist (A/B expansion respected; falls back to the top if the beat isn't in it) or currentLine in pattern-loop mode. Loading/importing a project also clears the selection so a stale one can't apply to a new arrangement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,8 @@ android {
|
||||
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 44
|
||||
versionName = "0.18.0"
|
||||
versionCode = 45
|
||||
versionName = "0.19.0"
|
||||
|
||||
// We provide our own instrumentation runner if/when tests are added.
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -506,9 +506,34 @@ class AudioEngine(
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- transport
|
||||
fun play() {
|
||||
/**
|
||||
* Start playback. When [startBeat] is >= 0, seek to that beat in the arrangement
|
||||
* (or that line in pattern-loop mode) so Play resumes from a marker the user
|
||||
* placed on the arrangement, instead of always from the top. Ignored if it doesn't
|
||||
* land inside the current playlist / pattern.
|
||||
*/
|
||||
fun play(startBeat: Int = -1) {
|
||||
rebuildArrangement()
|
||||
rebuildFxChains()
|
||||
if (startBeat >= 0) {
|
||||
if (arrangementMode) {
|
||||
val list = playlist
|
||||
// Find the first occurrence of that beat in the (possibly A/B-expanded)
|
||||
// playlist; if it isn't in there just start from the top.
|
||||
var seek = -1
|
||||
for (i in list.indices) if (list[i] == startBeat) { seek = i; break }
|
||||
if (seek >= 0) {
|
||||
playlistIndex = seek; lineInBeat = 0; arrPlayLine = 0; samplesIntoLine = 0.0
|
||||
}
|
||||
} else {
|
||||
val proj = project
|
||||
if (proj != null) {
|
||||
val pat = proj.activePattern()
|
||||
val line = (startBeat * proj.timeSignature.linesPerBeat).coerceIn(0, pat.length - 1)
|
||||
currentLine = line; samplesIntoLine = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingTrigger = true
|
||||
playing = true
|
||||
maybeStartRecording()
|
||||
|
||||
@@ -378,13 +378,42 @@ class AppViewModel(
|
||||
touched()
|
||||
}
|
||||
|
||||
// ---- Arrangement selection (hoisted from ArrangementRoll so Play/Stop can read it).
|
||||
// Rectangular selection anchor (…0) and live corner (…1); beat < 0 = no selection.
|
||||
// Play starts from [arrSelBeat0]'s beat if set; Stop clears the selection so the
|
||||
// highlight disappears and the next Play resumes from the top.
|
||||
var arrSelLane0 by mutableIntStateOf(-1); private set
|
||||
var arrSelBeat0 by mutableIntStateOf(-1); private set
|
||||
var arrSelLane1 by mutableIntStateOf(-1); private set
|
||||
var arrSelBeat1 by mutableIntStateOf(-1); private set
|
||||
|
||||
/** Set/update the arrangement selection rectangle. Pass all corners at once so the
|
||||
* ArrangementRoll's tap / drag gestures update it atomically. */
|
||||
fun setArrangementSelection(lane0: Int, beat0: Int, lane1: Int, beat1: Int) {
|
||||
arrSelLane0 = lane0; arrSelBeat0 = beat0; arrSelLane1 = lane1; arrSelBeat1 = beat1
|
||||
}
|
||||
|
||||
fun clearArrangementSelection() {
|
||||
arrSelLane0 = -1; arrSelBeat0 = -1; arrSelLane1 = -1; arrSelBeat1 = -1
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- transport control
|
||||
fun playPause() {
|
||||
val t = transport.value
|
||||
if (t.isPlaying) engine.pause() else engine.play()
|
||||
if (t.isPlaying) {
|
||||
engine.pause()
|
||||
} else {
|
||||
// Start from the earliest beat of the arrangement selection if there is
|
||||
// one; otherwise from wherever the engine last was (top after a Stop).
|
||||
val start = if (arrSelBeat0 >= 0) minOf(arrSelBeat0, arrSelBeat1) else -1
|
||||
engine.play(start)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() = engine.stop()
|
||||
fun stop() {
|
||||
clearArrangementSelection() // resets the highlight so the next Play starts from the top
|
||||
engine.stop()
|
||||
}
|
||||
|
||||
/** MIDI panic: silence all sounding/stuck notes immediately. */
|
||||
fun midiPanic() { heldNotes.clear(); engine.panic() }
|
||||
@@ -917,6 +946,7 @@ class AppViewModel(
|
||||
fun loadProjectText(text: String) {
|
||||
space.rcmd.android.sizzle.io.ProjectIo.loadInto(project, text)
|
||||
cursorLine = 0; cursorTrack = 0
|
||||
clearArrangementSelection() // the old arrangement's selection is no longer meaningful
|
||||
touched()
|
||||
}
|
||||
|
||||
|
||||
@@ -100,14 +100,13 @@ fun ArrangementRoll(vm: AppViewModel) {
|
||||
val beatWidthPx = with(density) { beatWidth.toPx() }
|
||||
val rulerHeightPx = with(density) { rulerHeight.toPx() }
|
||||
|
||||
// Rectangular cell selection: anchor (…0) + live corner (…1). Beat < 0 = none.
|
||||
// Unlike before, selecting does NOT change any cell; the toolbar toggle acts
|
||||
// on the selection. Read in composition (for the toolbar's FILL/CLEAR label)
|
||||
// and captured by the Canvas draw below.
|
||||
var selLane0 by remember { mutableIntStateOf(-1) }
|
||||
var selBeat0 by remember { mutableIntStateOf(-1) }
|
||||
var selLane1 by remember { mutableIntStateOf(-1) }
|
||||
var selBeat1 by remember { mutableIntStateOf(-1) }
|
||||
// Rectangular cell selection lives on the ViewModel so the transport (Play/Stop)
|
||||
// can read and clear it — Play starts from the selection's first beat, Stop clears
|
||||
// it. Selecting still does NOT change any cell; the toolbar toggle acts on it.
|
||||
val selLane0 = vm.arrSelLane0
|
||||
val selBeat0 = vm.arrSelBeat0
|
||||
val selLane1 = vm.arrSelLane1
|
||||
val selBeat1 = vm.arrSelBeat1
|
||||
|
||||
val hasSelection = selBeat0 >= 0
|
||||
val laneRange = if (hasSelection) minOf(selLane0, selLane1)..maxOf(selLane0, selLane1) else IntRange.EMPTY
|
||||
@@ -227,8 +226,7 @@ fun ArrangementRoll(vm: AppViewModel) {
|
||||
val lane = ((off.y - rulerHeightPx) / laneH).toInt()
|
||||
.coerceIn(0, ArrModel.LANE_COUNT - 1)
|
||||
val beat = (off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
||||
selLane0 = lane; selLane1 = lane
|
||||
selBeat0 = beat; selBeat1 = beat
|
||||
vm.setArrangementSelection(lane, beat, lane, beat)
|
||||
}
|
||||
}
|
||||
// Long-press then drag sweeps out a rectangular selection.
|
||||
@@ -240,14 +238,16 @@ fun ArrangementRoll(vm: AppViewModel) {
|
||||
}
|
||||
fun beatAt(off: Offset): Int =
|
||||
(off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
||||
var dragLane0 = 0; var dragBeat0 = 0
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { off ->
|
||||
selLane0 = laneAt(off); selLane1 = selLane0
|
||||
selBeat0 = beatAt(off); selBeat1 = selBeat0
|
||||
dragLane0 = laneAt(off); dragBeat0 = beatAt(off)
|
||||
vm.setArrangementSelection(dragLane0, dragBeat0, dragLane0, dragBeat0)
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
selLane1 = laneAt(change.position)
|
||||
selBeat1 = beatAt(change.position)
|
||||
vm.setArrangementSelection(
|
||||
dragLane0, dragBeat0, laneAt(change.position), beatAt(change.position),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user