diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b54104b..708ec71 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 = 38 - versionName = "0.14.0" + versionCode = 39 + versionName = "0.15.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/ui/tracker/TrackerScreen.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/TrackerScreen.kt index 9fbf013..f56523d 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/TrackerScreen.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/tracker/TrackerScreen.kt @@ -4,6 +4,7 @@ package space.rcmd.android.sizzle.ui.tracker import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,8 +20,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -147,14 +150,31 @@ private fun TransportButtons(vm: AppViewModel) { // Fixed width so the button doesn't resize as the label toggles. RetroButton(if (isPlaying) "❚❚ PAUSE" else "▶ PLAY", active = isPlaying, width = 112.dp, onClick = vm::playPause) - // Tempo nudger: tap -/+ around a fixed-width readout. + // Tempo nudger: tap -/+ around a fixed-width readout. Tapping the readout itself + // is TAP TEMPO — two or more taps in time set the tempo from the average interval. + val tapTimes = remember { ArrayList() } Row(verticalAlignment = Alignment.CenterVertically) { RepeatButton("-", onStep = { vm.setTempo(project.tempoBpm - 1) }) Text( "${project.tempoBpm.toInt()} BPM", color = c.text, fontFamily = FontFamily.Monospace, fontSize = 12.sp, textAlign = TextAlign.Center, maxLines = 1, - modifier = Modifier.width(72.dp).padding(vertical = 6.dp), + modifier = Modifier + .width(72.dp) + .pointerInput(Unit) { + detectTapGestures { + val now = System.currentTimeMillis() + // A long pause starts a fresh count rather than averaging across it. + if (tapTimes.isNotEmpty() && now - tapTimes.last() > TAP_RESET_MS) tapTimes.clear() + tapTimes.add(now) + while (tapTimes.size > TAP_MAX) tapTimes.removeAt(0) + if (tapTimes.size >= 2) { + val span = tapTimes.last() - tapTimes.first() + if (span > 0) vm.setTempo(60_000f * (tapTimes.size - 1) / span) + } + } + } + .padding(vertical = 6.dp), ) RepeatButton("+", onStep = { vm.setTempo(project.tempoBpm + 1) }) } @@ -224,3 +244,8 @@ private fun EditButtons(vm: AppViewModel) { // Swap the bottom half between the arranger and the punch-in keyboard. RetroButton("⌨ KBD", active = vm.punchInMode, onClick = vm::togglePunchIn) } + +// Tap-tempo tuning: a gap longer than this starts a fresh count; keep at most this +// many recent taps in the averaging window. +private const val TAP_RESET_MS = 2000L +private const val TAP_MAX = 8