Add variable playback speed feature for voice messages

Add playback speed control for voice messages with support for 0.5×, 1×, 1.5×, and 2× playback speeds. The speed button is displayed above the timestamp and cycles through the available speeds when tapped.
This commit is contained in:
Florian 2025-10-09 21:43:47 +02:00 committed by GitHub
parent 1c26c03cde
commit 5e07691fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 203 additions and 15 deletions

View file

@ -79,6 +79,13 @@ interface VoiceMessagePlayer {
*/
fun seekTo(positionMs: Long)
/**
* Set the playback speed.
*
* @param speed The playback speed (e.g., 0.5f for half speed, 1.0f for normal, 2.0f for double speed)
*/
fun setPlaybackSpeed(speed: Float)
data class State(
/**
* Whether the player is ready to play.
@ -218,6 +225,10 @@ class Factory(
}
}
override fun setPlaybackSpeed(speed: Float) {
mediaPlayer.setPlaybackSpeed(speed)
}
private val MediaPlayer.State.isMyTrack: Boolean
get() = if (eventId == null) false else this.mediaId == eventId.value

View file

@ -37,6 +37,9 @@ class VoiceMessagePresenter(
private val duration: Duration,
) : Presenter<VoiceMessageState> {
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
private val playbackSpeed = mutableStateOf(1.0f)
private val availablePlaybackSpeeds = listOf(0.5f, 1.0f, 1.5f, 2.0f)
@Composable
override fun present(): VoiceMessageState {
@ -111,6 +114,13 @@ class VoiceMessagePresenter(
is VoiceMessageEvents.Seek -> {
player.seekTo((event.percentage * duration).toLong())
}
is VoiceMessageEvents.ChangePlaybackSpeed -> {
val currentIndex = availablePlaybackSpeeds.indexOf(playbackSpeed.value)
val nextIndex = (currentIndex + 1) % availablePlaybackSpeeds.size
val newSpeed = availablePlaybackSpeeds[nextIndex]
playbackSpeed.value = newSpeed
player.setPlaybackSpeed(newSpeed)
}
}
}
@ -119,6 +129,7 @@ class VoiceMessagePresenter(
progress = progress,
time = time,
showCursor = showCursor,
playbackSpeed = playbackSpeed.value,
eventSink = { eventSink(it) },
)
}

View file

@ -222,6 +222,40 @@ class VoiceMessagePresenterTest {
}
}
}
@Test
fun `changing playback speed cycles through available speeds`() = runTest {
val presenter = createVoiceMessagePresenter(
duration = 10_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem().also {
assertThat(it.playbackSpeed).isEqualTo(1.0f)
}
initialState.eventSink(VoiceMessageEvents.ChangePlaybackSpeed)
awaitItem().also {
assertThat(it.playbackSpeed).isEqualTo(1.5f)
}
initialState.eventSink(VoiceMessageEvents.ChangePlaybackSpeed)
awaitItem().also {
assertThat(it.playbackSpeed).isEqualTo(2.0f)
}
initialState.eventSink(VoiceMessageEvents.ChangePlaybackSpeed)
awaitItem().also {
assertThat(it.playbackSpeed).isEqualTo(0.5f)
}
initialState.eventSink(VoiceMessageEvents.ChangePlaybackSpeed)
awaitItem().also {
assertThat(it.playbackSpeed).isEqualTo(1.0f)
}
}
}
}
fun TestScope.createVoiceMessagePresenter(