Skip to content

Commit

Permalink
✨ feat(NowPlaying): Add playback speed & pitch control (fixed #733)
Browse files Browse the repository at this point in the history
This commit introduces playback speed and pitch controls to the Now Playing screen.  Users can now adjust these settings via a new modal bottom sheet.  The changes also include updated UI elements and data storage for persistent settings.
  • Loading branch information
maxrave-dev committed Jan 30, 2025
1 parent eda2dcc commit 5e2da6c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
Expand Down Expand Up @@ -726,6 +727,32 @@ class DataStoreManager(
}
}

val playbackSpeed =
settingsDataStore.data.map { preferences ->
preferences[PLAYBACK_SPEED] ?: 1.0f
}

fun setPlaybackSpeed(speed: Float) {
runBlocking {
settingsDataStore.edit { settings ->
settings[PLAYBACK_SPEED] = speed
}
}
}

val pitch =
settingsDataStore.data.map { preferences ->
preferences[PITCH] ?: 0
}

fun setPitch(pitch: Int) {
runBlocking {
settingsDataStore.edit { settings ->
settings[PITCH] = pitch
}
}
}

companion object Settings {
val COOKIE = stringPreferencesKey("cookie")
val LOGGED_IN = stringPreferencesKey("logged_in")
Expand Down Expand Up @@ -771,6 +798,8 @@ class DataStoreManager(
val SHOULD_SHOW_LOG_IN_REQUIRED_ALERT = stringPreferencesKey("should_show_log_in_required_alert")
val AUTO_CHECK_FOR_UPDATES = stringPreferencesKey("auto_check_for_updates")
val BLUR_FULLSCREEN_LYRICS = stringPreferencesKey("blur_fullscreen_lyrics")
val PLAYBACK_SPEED = floatPreferencesKey("playback_speed")
val PITCH = intPreferencesKey("pitch")
const val REPEAT_MODE_OFF = "REPEAT_MODE_OFF"
const val REPEAT_ONE = "REPEAT_ONE"
const val REPEAT_ALL = "REPEAT_ALL"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
Expand Down Expand Up @@ -67,6 +68,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
Expand All @@ -80,6 +82,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.LocalDateTime
import kotlin.math.pow

@UnstableApi
class SimpleMediaServiceHandler(
Expand Down Expand Up @@ -291,8 +294,23 @@ class SimpleMediaServiceHandler(
}
}
}
val playbackSpeedPitchJob =
launch {
combine(dataStoreManager.playbackSpeed, dataStoreManager.pitch) { speed, pitch ->
Pair(speed, pitch)
}.collectLatest { pair ->
Log.w(TAG, "Playback speed: ${pair.first}, Pitch: ${pair.second}")
player.playbackParameters =
PlaybackParameters(
pair.first,
2f.pow(pair.second.toFloat() / 12),
)
Log.w(TAG, "Playback current speed: ${player.playbackParameters.speed}, Pitch: ${player.playbackParameters.pitch}")
}
}
skipSegmentsJob.join()
playbackJob.join()
playbackSpeedPitchJob.join()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand Down Expand Up @@ -612,6 +613,7 @@ fun NowPlayingBottomSheet(
changeMainLyricsProviderEnable: Boolean = false,
// Delete is specific to playlist
onDelete: (() -> Unit)? = null,
dataStoreManager: DataStoreManager = koinInject(),
) {
val uiState by viewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()
Expand Down Expand Up @@ -639,6 +641,7 @@ fun NowPlayingBottomSheet(
mutableStateOf(false)
}
var isBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
var changePlaybackSpeedPitch by remember { mutableStateOf(false) }

LaunchedEffect(uiState) {
if (uiState.songUIState.videoId.isNotEmpty() && !isBottomSheetVisible) {
Expand All @@ -650,6 +653,23 @@ fun NowPlayingBottomSheet(
viewModel.setSongEntity(song)
}

if (changePlaybackSpeedPitch) {
val playbackSpeed by dataStoreManager.playbackSpeed.collectAsState(1f)
val pitch by dataStoreManager.pitch.collectAsState(0)
PlaybackSpeedPitchBottomSheet(
onDismiss = { changePlaybackSpeedPitch = false },
playbackSpeed = playbackSpeed,
pitch = pitch,
) { speed, p ->
viewModel.onUIEvent(
NowPlayingBottomSheetUIEvent.ChangePlaybackSpeedPitch(
speed = speed,
pitch = p,
),
)
}
}

if (addToAPlaylist) {
AddToPlaylistModalBottomSheet(
isBottomSheetVisible = true,
Expand Down Expand Up @@ -1066,6 +1086,17 @@ fun NowPlayingBottomSheet(
}
}
}
Crossfade(targetState = setSleepTimerEnable) {
if (it) {
// Sleep timer is enabled, so this screen is player screen
ActionButton(
icon = painterResource(R.drawable.round_speed_24),
text = R.string.playback_speed_pitch,
) {
changePlaybackSpeedPitch = true
}
}
}
ActionButton(
icon = painterResource(id = R.drawable.baseline_share_24),
text = R.string.share,
Expand Down Expand Up @@ -1216,6 +1247,91 @@ fun HeartCheckBox(
}
}

@ExperimentalMaterial3Api
@Composable
fun PlaybackSpeedPitchBottomSheet(
onDismiss: () -> Unit,
playbackSpeed: Float,
pitch: Int,
onSet: (playbackSpeed: Float, pitch: Int) -> Unit,
) {
val modelBottomSheetState =
rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = modelBottomSheetState,
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
scrimColor = Color.Black.copy(alpha = .5f),
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
) {
Card(
modifier =
Modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 10.dp),
) {
Spacer(modifier = Modifier.height(5.dp))
Card(
modifier =
Modifier
.width(60.dp)
.height(4.dp),
colors =
CardDefaults.cardColors().copy(
containerColor = Color(0xFF474545),
),
shape = RoundedCornerShape(50),
) {}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = stringResource(R.string.playback_speed) + " ${playbackSpeed}x",
style = typo.labelSmall,
)
Spacer(modifier = Modifier.height(5.dp))
Slider(
value = playbackSpeed,
onValueChange = {
onSet(it, pitch)
},
modifier = Modifier,
enabled = true,
valueRange = 0.25f..2f,
steps = 6,
onValueChangeFinished = {},
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.pitch) + " $pitch",
style = typo.labelSmall,
)
Spacer(modifier = Modifier.height(5.dp))
Slider(
value = pitch.toFloat(),
onValueChange = {
onSet(playbackSpeed, it.toInt())
},
modifier = Modifier,
enabled = true,
valueRange = -12f..12f,
steps = 23,
onValueChangeFinished = {},
)
EndOfModalBottomSheet()
}
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddToPlaylistModalBottomSheet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ class NowPlayingBottomSheetViewModel(
simpleMediaServiceHandler.sleepStart(ev.minutes)
}
}
is NowPlayingBottomSheetUIEvent.ChangePlaybackSpeedPitch -> {
dataStoreManager.setPlaybackSpeed(ev.speed)
dataStoreManager.setPitch(ev.pitch)
}
is NowPlayingBottomSheetUIEvent.Share -> {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
Expand Down Expand Up @@ -292,5 +296,10 @@ sealed class NowPlayingBottomSheetUIEvent {
val minutes: Int = 0,
) : NowPlayingBottomSheetUIEvent()

data class ChangePlaybackSpeedPitch(
val speed: Float,
val pitch: Int,
) : NowPlayingBottomSheetUIEvent()

data object Share : NowPlayingBottomSheetUIEvent()
}
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/round_speed_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M19.46,10a1,1 0,0 0,-0.07 1,7.55 7.55,0 0,1 0.52,1.81 8,8 0,0 1,-0.69 4.73,1 1,0 0,1 -0.89,0.53H5.68a1,1 0,0 1,-0.89 -0.54A8,8 0,0 1,13 6.06a7.69,7.69 0,0 1,2.11 0.56,1 1,0 0,0 1,-0.07 1,1 0,0 0,-0.17 -1.76A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 0.55,-8.89 1,1 0,0 0,-1.75 -0.11z"/>

<path android:fillColor="@android:color/white" android:pathData="M10.59,12.59a2,2 0,0 0,2.83 2.83l5.66,-8.49z"/>

</vector>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,7 @@
<string name="auto_check_for_update_description">Checking for update when you open app</string>
<string name="blur_fullscreen_lyrics">Blur fullscreen lyrics effect</string>
<string name="blur_fullscreen_lyrics_description">Blurring the background of the Fullscreen lyrics screen effect</string>
<string name="playback_speed_pitch"><![CDATA[Playback speed & pitch]]></string>
<string name="playback_speed">Playback speed</string>
<string name="pitch">Pitch</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ class YouTube {
fun removeProxy() {
ytMusic.proxy = null
newPipeDownloader.updateProxy(null)
NewPipe.init(newPipeDownloader)
}

/**
Expand All @@ -192,6 +193,7 @@ class YouTube {
}.onSuccess {
ytMusic.proxy = it
newPipeDownloader.updateProxy(it)
NewPipe.init(newPipeDownloader)
}.onFailure {
it.printStackTrace()
}
Expand Down

0 comments on commit 5e2da6c

Please sign in to comment.