feat: feed accel to native player
This commit is contained in:
@@ -81,6 +81,13 @@ class LockstepViewModel(
|
||||
false,
|
||||
)
|
||||
|
||||
val feedAccelToPasada: StateFlow<Boolean> =
|
||||
prefs.feedAccelToPasada.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
true,
|
||||
)
|
||||
|
||||
fun setAnnotationMode(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
prefs.setAnnotationMode(enabled)
|
||||
@@ -93,6 +100,12 @@ class LockstepViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setFeedAccelToPasada(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
prefs.setFeedAccelToPasada(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private val context get() = getApplication<Application>()
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,6 +39,12 @@ class UserPreferencesRepository(
|
||||
prefs[KEY_COLLECT_RUN_DATA] == true
|
||||
}
|
||||
|
||||
/** When true, accelerometer samples are forwarded to libpasada during playback (not while annotating). */
|
||||
val feedAccelToPasada: Flow<Boolean> =
|
||||
dataStore.data.map { prefs ->
|
||||
prefs[KEY_FEED_ACCEL_TO_PASADA] != false
|
||||
}
|
||||
|
||||
suspend fun setOnboardingComplete(done: Boolean) {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[KEY_ONBOARDING_COMPLETE] = done
|
||||
@@ -67,10 +73,17 @@ class UserPreferencesRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setFeedAccelToPasada(enabled: Boolean) {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[KEY_FEED_ACCEL_TO_PASADA] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete")
|
||||
private val KEY_SPOTIFY_ACCESS_TOKEN = stringPreferencesKey("spotify_access_token")
|
||||
private val KEY_ANNOTATION_MODE = booleanPreferencesKey("annotation_mode")
|
||||
private val KEY_COLLECT_RUN_DATA = booleanPreferencesKey("collect_run_data")
|
||||
private val KEY_FEED_ACCEL_TO_PASADA = booleanPreferencesKey("feed_accel_to_pasada")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,18 @@ class PlaybackService : Service() {
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private var engine: MusicPlayerEngine? = null
|
||||
private var accelFeeder: PasadaAccelFeeder? = null
|
||||
private var positionPollJob: Job? = null
|
||||
private var positionCachePollJob: Job? = null
|
||||
|
||||
/** User preference: forward accelerometer samples to libpasada. */
|
||||
@Volatile
|
||||
private var feedAccelEnabled = true
|
||||
|
||||
/** True while the beat annotation screen is visible. */
|
||||
@Volatile
|
||||
private var isAnnotating = false
|
||||
|
||||
private val _uiState = MutableStateFlow(PlaybackUiState.initial())
|
||||
val uiState: StateFlow<PlaybackUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -213,10 +222,44 @@ class PlaybackService : Service() {
|
||||
it.setListener(engineListener)
|
||||
it.initSession()
|
||||
engine = it
|
||||
refreshAccelFeeding()
|
||||
startPositionPolling()
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the user toggles "Adapt playback to movement" in Settings. */
|
||||
fun setFeedAccelEnabled(enabled: Boolean) {
|
||||
feedAccelEnabled = enabled
|
||||
refreshAccelFeeding()
|
||||
}
|
||||
|
||||
/** Called when entering or leaving the beat annotation screen. */
|
||||
fun setAnnotating(annotating: Boolean) {
|
||||
isAnnotating = annotating
|
||||
refreshAccelFeeding()
|
||||
}
|
||||
|
||||
private fun refreshAccelFeeding() {
|
||||
if (engine == null) {
|
||||
return
|
||||
}
|
||||
if (feedAccelEnabled && !isAnnotating) {
|
||||
startAccelFeeding()
|
||||
} else {
|
||||
stopAccelFeeding()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAccelFeeding() {
|
||||
val feeder =
|
||||
accelFeeder ?: PasadaAccelFeeder(this).also { accelFeeder = it }
|
||||
feeder.start()
|
||||
}
|
||||
|
||||
private fun stopAccelFeeding() {
|
||||
accelFeeder?.stop()
|
||||
}
|
||||
|
||||
private fun startPositionPolling() {
|
||||
positionPollJob?.cancel()
|
||||
positionPollJob =
|
||||
@@ -243,6 +286,7 @@ class PlaybackService : Service() {
|
||||
positionPollJob = null
|
||||
positionCachePollJob?.cancel()
|
||||
positionCachePollJob = null
|
||||
stopAccelFeeding()
|
||||
engine?.setListener(null)
|
||||
engine?.releaseSession()
|
||||
engine = null
|
||||
@@ -569,6 +613,8 @@ class PlaybackService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
stopPlaybackAndTeardown()
|
||||
accelFeeder?.release()
|
||||
accelFeeder = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
@@ -85,6 +86,13 @@ fun AnnotationRoute(
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(playback) {
|
||||
playback?.setAnnotating(true)
|
||||
onDispose {
|
||||
playback?.setAnnotating(false)
|
||||
}
|
||||
}
|
||||
|
||||
val beatTimesMs = remember { mutableStateListOf<Long>() }
|
||||
val annotationSessionFolder = remember { RunDataStorage.newRunSessionFolderName() }
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.IBinder
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -34,8 +35,13 @@ fun LockstepAppNavHost(
|
||||
viewModel: LockstepViewModel,
|
||||
) {
|
||||
val annotationMode by viewModel.annotationMode.collectAsStateWithLifecycle()
|
||||
val feedAccelToPasada by viewModel.feedAccelToPasada.collectAsStateWithLifecycle()
|
||||
var playback by remember { mutableStateOf<PlaybackService?>(null) }
|
||||
|
||||
LaunchedEffect(feedAccelToPasada, playback) {
|
||||
playback?.setFeedAccelEnabled(feedAccelToPasada)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val connection =
|
||||
object : ServiceConnection {
|
||||
|
||||
@@ -234,6 +234,11 @@ fun NowPlayingRoute(
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(playback) {
|
||||
playback?.setAnnotating(false)
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
LaunchedEffect(collectRunData, playback) {
|
||||
if (!collectRunData) {
|
||||
collector.setCollectingEnabled(false)
|
||||
|
||||
@@ -38,6 +38,7 @@ fun SettingsScreen(
|
||||
val context = LocalContext.current
|
||||
val annotationMode by viewModel.annotationMode.collectAsStateWithLifecycle()
|
||||
val collectRunData by viewModel.collectRunData.collectAsStateWithLifecycle()
|
||||
val feedAccelToPasada by viewModel.feedAccelToPasada.collectAsStateWithLifecycle()
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
@@ -83,6 +84,30 @@ fun SettingsScreen(
|
||||
onCheckedChange = { viewModel.setAnnotationMode(it) },
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 16.dp)) {
|
||||
Text(
|
||||
text = context.getString(R.string.settings_feed_accel_to_pasada),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = context.getString(R.string.settings_feed_accel_to_pasada_help),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = feedAccelToPasada,
|
||||
onCheckedChange = { viewModel.setFeedAccelToPasada(it) },
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<string name="settings_annotation_mode_help">When enabled, choosing a playlist opens beat annotation (tap in time) instead of Now playing.</string>
|
||||
<string name="settings_collect_run_data">Collect run data</string>
|
||||
<string name="settings_collect_run_data_help">When enabled, Now playing records accelerometer, gyroscope, and GPS (1 Hz) per song into Documents/Pasada/ under a timestamped run folder.</string>
|
||||
<string name="settings_feed_accel_to_pasada">Adapt playback to movement</string>
|
||||
<string name="settings_feed_accel_to_pasada_help">When enabled, accelerometer data is sent to libpasada during Now playing so tempo can follow your pace. Disabled automatically on the beat annotation screen.</string>
|
||||
<string name="annotation_title">Beat annotation</string>
|
||||
<string name="annotation_subtitle">Tap on each beat; times use the same clock as playback (ExoPlayer position).</string>
|
||||
<string name="annotation_tap_area_label">Tap here on the beat</string>
|
||||
|
||||
Reference in New Issue
Block a user