diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b36481a..9b0187a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt b/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt index 1e0fdb2..00bd45d 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt @@ -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() diff --git a/app/src/main/java/space/rcmd/android/sizzle/ui/AppViewModel.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/AppViewModel.kt index a78caa2..2ff9895 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/AppViewModel.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/AppViewModel.kt @@ -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() } diff --git a/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/ArrangementRoll.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/ArrangementRoll.kt index c8d0910..c944bcc 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/ArrangementRoll.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/ArrangementRoll.kt @@ -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), + ) }, ) },