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.
|
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 44
|
versionCode = 45
|
||||||
versionName = "0.18.0"
|
versionName = "0.19.0"
|
||||||
|
|
||||||
// We provide our own instrumentation runner if/when tests are added.
|
// We provide our own instrumentation runner if/when tests are added.
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -506,9 +506,34 @@ class AudioEngine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------- transport
|
// --------------------------------------------------------------- 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()
|
rebuildArrangement()
|
||||||
rebuildFxChains()
|
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
|
pendingTrigger = true
|
||||||
playing = true
|
playing = true
|
||||||
maybeStartRecording()
|
maybeStartRecording()
|
||||||
|
|||||||
@@ -378,13 +378,42 @@ class AppViewModel(
|
|||||||
touched()
|
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
|
// ------------------------------------------------------- transport control
|
||||||
fun playPause() {
|
fun playPause() {
|
||||||
val t = transport.value
|
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. */
|
/** MIDI panic: silence all sounding/stuck notes immediately. */
|
||||||
fun midiPanic() { heldNotes.clear(); engine.panic() }
|
fun midiPanic() { heldNotes.clear(); engine.panic() }
|
||||||
@@ -917,6 +946,7 @@ class AppViewModel(
|
|||||||
fun loadProjectText(text: String) {
|
fun loadProjectText(text: String) {
|
||||||
space.rcmd.android.sizzle.io.ProjectIo.loadInto(project, text)
|
space.rcmd.android.sizzle.io.ProjectIo.loadInto(project, text)
|
||||||
cursorLine = 0; cursorTrack = 0
|
cursorLine = 0; cursorTrack = 0
|
||||||
|
clearArrangementSelection() // the old arrangement's selection is no longer meaningful
|
||||||
touched()
|
touched()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,14 +100,13 @@ fun ArrangementRoll(vm: AppViewModel) {
|
|||||||
val beatWidthPx = with(density) { beatWidth.toPx() }
|
val beatWidthPx = with(density) { beatWidth.toPx() }
|
||||||
val rulerHeightPx = with(density) { rulerHeight.toPx() }
|
val rulerHeightPx = with(density) { rulerHeight.toPx() }
|
||||||
|
|
||||||
// Rectangular cell selection: anchor (…0) + live corner (…1). Beat < 0 = none.
|
// Rectangular cell selection lives on the ViewModel so the transport (Play/Stop)
|
||||||
// Unlike before, selecting does NOT change any cell; the toolbar toggle acts
|
// can read and clear it — Play starts from the selection's first beat, Stop clears
|
||||||
// on the selection. Read in composition (for the toolbar's FILL/CLEAR label)
|
// it. Selecting still does NOT change any cell; the toolbar toggle acts on it.
|
||||||
// and captured by the Canvas draw below.
|
val selLane0 = vm.arrSelLane0
|
||||||
var selLane0 by remember { mutableIntStateOf(-1) }
|
val selBeat0 = vm.arrSelBeat0
|
||||||
var selBeat0 by remember { mutableIntStateOf(-1) }
|
val selLane1 = vm.arrSelLane1
|
||||||
var selLane1 by remember { mutableIntStateOf(-1) }
|
val selBeat1 = vm.arrSelBeat1
|
||||||
var selBeat1 by remember { mutableIntStateOf(-1) }
|
|
||||||
|
|
||||||
val hasSelection = selBeat0 >= 0
|
val hasSelection = selBeat0 >= 0
|
||||||
val laneRange = if (hasSelection) minOf(selLane0, selLane1)..maxOf(selLane0, selLane1) else IntRange.EMPTY
|
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()
|
val lane = ((off.y - rulerHeightPx) / laneH).toInt()
|
||||||
.coerceIn(0, ArrModel.LANE_COUNT - 1)
|
.coerceIn(0, ArrModel.LANE_COUNT - 1)
|
||||||
val beat = (off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
val beat = (off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
||||||
selLane0 = lane; selLane1 = lane
|
vm.setArrangementSelection(lane, beat, lane, beat)
|
||||||
selBeat0 = beat; selBeat1 = beat
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Long-press then drag sweeps out a rectangular selection.
|
// Long-press then drag sweeps out a rectangular selection.
|
||||||
@@ -240,14 +238,16 @@ fun ArrangementRoll(vm: AppViewModel) {
|
|||||||
}
|
}
|
||||||
fun beatAt(off: Offset): Int =
|
fun beatAt(off: Offset): Int =
|
||||||
(off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
(off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
|
||||||
|
var dragLane0 = 0; var dragBeat0 = 0
|
||||||
detectDragGesturesAfterLongPress(
|
detectDragGesturesAfterLongPress(
|
||||||
onDragStart = { off ->
|
onDragStart = { off ->
|
||||||
selLane0 = laneAt(off); selLane1 = selLane0
|
dragLane0 = laneAt(off); dragBeat0 = beatAt(off)
|
||||||
selBeat0 = beatAt(off); selBeat1 = selBeat0
|
vm.setArrangementSelection(dragLane0, dragBeat0, dragLane0, dragBeat0)
|
||||||
},
|
},
|
||||||
onDrag = { change, _ ->
|
onDrag = { change, _ ->
|
||||||
selLane1 = laneAt(change.position)
|
vm.setArrangementSelection(
|
||||||
selBeat1 = beatAt(change.position)
|
dragLane0, dragBeat0, laneAt(change.position), beatAt(change.position),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user