Skip to content

Commit afffcce

Browse files
authored
PowerPlay: Add playback speed and pitch control (#2372)
* PowerPlay: Add playback speed and pitch control * PowerPlay: Refine playback parameter support and add reset button * PowerPlay: Allow speed control for 44100 Hz when using MMAP. * refactor: remove explicit buffer capacity configuration in PowerPlayMultiPlayer audio stream builder * refactor: update playback parameters API to return success status and handle state synchronization in UI
1 parent e5c061b commit afffcce

6 files changed

Lines changed: 162 additions & 3 deletions

File tree

samples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
/captures
99
.externalNativeBuild
1010
test/build
11+
*.salive

samples/powerplay/src/main/cpp/PowerPlayJNI.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,18 @@ Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_removeSampleS
420420
return player.removeSampleSource(index);
421421
}
422422

423+
/**
424+
* Native (JNI) implementation of PowerPlayAudioPlayer.setPlaybackParametersNative()
425+
*/
426+
JNIEXPORT jboolean JNICALL
427+
Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_setPlaybackParametersNative(
428+
JNIEnv *env,
429+
jobject,
430+
jfloat speed,
431+
jfloat pitch) {
432+
return player.setPlaybackParameters(speed, pitch);
433+
}
434+
423435
#ifdef __cplusplus
424436
}
425437
#endif

samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.cpp

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ bool PowerPlayMultiPlayer::openStream(oboe::PerformanceMode performanceMode) {
7171

7272
if (mAudioStream->getPerformanceMode() != oboe::PerformanceMode::PowerSavingOffloaded ||
7373
!OboeExtensions::isMMapUsed(mAudioStream.get())) {
74-
constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer)
75-
result = mAudioStream->setBufferSizeInFrames(
76-
mAudioStream->getFramesPerBurst() * kBufferSizeInBursts);
74+
result = mAudioStream->setBufferSizeInFrames(mAudioStream->getBufferCapacityInFrames());
7775
if (result != Result::OK) {
7876
__android_log_print(
7977
ANDROID_LOG_WARN,
@@ -84,6 +82,10 @@ bool PowerPlayMultiPlayer::openStream(oboe::PerformanceMode performanceMode) {
8482
}
8583

8684
mSampleRate = mAudioStream->getSampleRate();
85+
86+
// Apply stored playback parameters
87+
setPlaybackParameters(mPlaybackSpeed, mPlaybackPitch);
88+
8789
return true;
8890
}
8991

@@ -350,3 +352,23 @@ bool PowerPlayMultiPlayer::removeSampleSource(int32_t index) {
350352
index, mNumSampleBuffers);
351353
return true;
352354
}
355+
356+
bool PowerPlayMultiPlayer::setPlaybackParameters(float speed, float pitch) {
357+
if (mAudioStream) {
358+
oboe::PlaybackParameters params = {
359+
oboe::FallbackMode::Default,
360+
oboe::StretchMode::Default,
361+
pitch,
362+
speed
363+
};
364+
auto result = mAudioStream->setPlaybackParameters(params);
365+
if (result != oboe::Result::OK) {
366+
__android_log_print(ANDROID_LOG_ERROR, "PowerPlayMultiPlayer",
367+
"setPlaybackParameters failed: %s", oboe::convertToText(result));
368+
return false;
369+
}
370+
}
371+
mPlaybackSpeed = speed;
372+
mPlaybackPitch = pitch;
373+
return true;
374+
}

samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ class PowerPlayMultiPlayer : public iolib::SimpleMultiPlayer {
6060

6161
bool removeSampleSource(int32_t index);
6262

63+
bool setPlaybackParameters(float speed, float pitch);
64+
6365
private:
6466
class MyPresentationCallback : public oboe::AudioStreamPresentationCallback {
6567
public:
@@ -79,6 +81,8 @@ class PowerPlayMultiPlayer : public iolib::SimpleMultiPlayer {
7981
oboe::PerformanceMode mLastPerformanceMode;
8082

8183
bool mLastMMapEnabled;
84+
float mPlaybackSpeed = 1.0f;
85+
float mPlaybackPitch = 1.0f;
8286
};
8387

8488
#endif //SAMPLES_POWERPLAYMULTIPLAYER_H

samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/MainActivity.kt

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ class MainActivity : ComponentActivity() {
477477
val playerStateWrapper = player.getPlayerStateLive().observeAsState(PlayerState.NoResultYet)
478478
val isPlaying = playerStateWrapper.value == PlayerState.Playing
479479
var sliderPosition by remember { mutableFloatStateOf(0f) }
480+
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
481+
var playbackPitch by remember { mutableFloatStateOf(1.0f) }
480482

481483
var showBottomSheet by remember { mutableStateOf(false) }
482484
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -731,12 +733,30 @@ class MainActivity : ComponentActivity() {
731733
containerColor = Color.White,
732734
shape = androidx.compose.foundation.shape.RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
733735
) {
736+
val currentTrack = playList.getOrNull(playingSongIndex.intValue)
734737
PerformanceBottomSheetContent(
735738
offload = offload,
736739
isMMapEnabled = isMMapEnabled,
737740
isPlaying = isPlaying,
738741
sliderPosition = sliderPosition,
739742
onSliderPositionChange = { sliderPosition = it },
743+
playbackSpeed = playbackSpeed,
744+
playbackPitch = playbackPitch,
745+
onSpeedChange = {
746+
val success = player.setPlaybackParameters(it, playbackPitch)
747+
if (success) {
748+
playbackSpeed = it
749+
}
750+
success
751+
},
752+
onPitchChange = {
753+
val success = player.setPlaybackParameters(playbackSpeed, it)
754+
if (success) {
755+
playbackPitch = it
756+
}
757+
success
758+
},
759+
fileSampleRate = currentTrack?.wavInfo?.sampleRate ?: 48000,
740760
onDismiss = { showBottomSheet = false }
741761
)
742762
}
@@ -843,11 +863,23 @@ class MainActivity : ComponentActivity() {
843863
isPlaying: Boolean,
844864
sliderPosition: Float,
845865
onSliderPositionChange: (Float) -> Unit,
866+
playbackSpeed: Float,
867+
playbackPitch: Float,
868+
onSpeedChange: (Float) -> Boolean,
869+
onPitchChange: (Float) -> Boolean,
870+
fileSampleRate: Int,
846871
onDismiss: () -> Unit
847872
) {
848873
var localSliderPosition by remember { mutableFloatStateOf(sliderPosition) }
849874
val requestedFrames = remember { mutableIntStateOf(0) }
850875
val actualFrames = remember { mutableIntStateOf(0) }
876+
var isModified by remember { mutableStateOf(playbackSpeed != 1.0f || playbackPitch != 1.0f) }
877+
878+
var localSpeed by remember { mutableFloatStateOf(playbackSpeed) }
879+
var localPitch by remember { mutableFloatStateOf(playbackPitch) }
880+
881+
LaunchedEffect(playbackSpeed) { localSpeed = playbackSpeed }
882+
LaunchedEffect(playbackPitch) { localPitch = playbackPitch }
851883

852884
Column(
853885
modifier = Modifier
@@ -955,6 +987,92 @@ class MainActivity : ComponentActivity() {
955987
)
956988
}
957989

990+
val isPlaybackParamsSupported = android.os.Build.VERSION.SDK_INT >= 37
991+
val isOffload = offload.intValue == 3
992+
val isMMap = isMMapEnabled.value
993+
994+
var canUseSpeed = isPlaybackParamsSupported
995+
var canUsePitch = isPlaybackParamsSupported
996+
997+
// For testing: allow everything except API 37 gate
998+
// The previous offload restrictions have been removed to test allowing everything.
999+
1000+
Spacer(modifier = Modifier.height(16.dp))
1001+
val speedSupportText = if (!isPlaybackParamsSupported) " (Requires API 37)" else ""
1002+
Text(
1003+
text = "Playback Speed: ${"%.2f".format(playbackSpeed)}x$speedSupportText",
1004+
style = MaterialTheme.typography.bodyMedium,
1005+
color = if (canUseSpeed) Color.Unspecified else Color.Gray
1006+
)
1007+
Slider(
1008+
value = localSpeed,
1009+
onValueChange = {
1010+
localSpeed = it
1011+
onSpeedChange(it)
1012+
},
1013+
onValueChangeFinished = {
1014+
if (localSpeed != playbackSpeed) {
1015+
localSpeed = playbackSpeed
1016+
}
1017+
isModified = (playbackSpeed != 1.0f || playbackPitch != 1.0f)
1018+
},
1019+
valueRange = 0.5f..2.0f,
1020+
enabled = canUseSpeed,
1021+
colors = SliderDefaults.colors(
1022+
thumbColor = MaterialTheme.colorScheme.primary,
1023+
activeTrackColor = MaterialTheme.colorScheme.primary
1024+
),
1025+
modifier = Modifier.fillMaxWidth()
1026+
)
1027+
1028+
Spacer(modifier = Modifier.height(8.dp))
1029+
val pitchSupportText = if (!isPlaybackParamsSupported) " (Requires API 37)" else ""
1030+
Text(
1031+
text = "Playback Pitch: ${"%.2f".format(playbackPitch)}x$pitchSupportText",
1032+
style = MaterialTheme.typography.bodyMedium,
1033+
color = if (canUsePitch) Color.Unspecified else Color.Gray
1034+
)
1035+
Slider(
1036+
value = localPitch,
1037+
onValueChange = {
1038+
localPitch = it
1039+
onPitchChange(it)
1040+
},
1041+
onValueChangeFinished = {
1042+
if (localPitch != playbackPitch) {
1043+
localPitch = playbackPitch
1044+
}
1045+
isModified = (playbackSpeed != 1.0f || playbackPitch != 1.0f)
1046+
},
1047+
valueRange = 0.5f..2.0f,
1048+
enabled = canUsePitch,
1049+
colors = SliderDefaults.colors(
1050+
thumbColor = MaterialTheme.colorScheme.primary,
1051+
activeTrackColor = MaterialTheme.colorScheme.primary
1052+
),
1053+
modifier = Modifier.fillMaxWidth()
1054+
)
1055+
1056+
AnimatedVisibility(
1057+
visible = isModified,
1058+
enter = androidx.compose.animation.fadeIn(animationSpec = androidx.compose.animation.core.tween(durationMillis = 500)) + androidx.compose.animation.expandVertically(),
1059+
exit = androidx.compose.animation.fadeOut(animationSpec = androidx.compose.animation.core.tween(durationMillis = 500)) + androidx.compose.animation.shrinkVertically()
1060+
) {
1061+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
1062+
Spacer(modifier = Modifier.height(16.dp))
1063+
TextButton(
1064+
onClick = {
1065+
onSpeedChange(1.0f)
1066+
onPitchChange(1.0f)
1067+
isModified = false
1068+
},
1069+
enabled = canUseSpeed || canUsePitch
1070+
) {
1071+
Text("Reset to Defaults")
1072+
}
1073+
}
1074+
}
1075+
9581076
AnimatedVisibility(
9591077
visible = offload.intValue == 3,
9601078
enter = androidx.compose.animation.expandVertically() + fadeIn(),

samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/engine/PowerPlayAudioPlayer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class PowerPlayAudioPlayer() : DefaultLifecycleObserver {
6666
fun setLooping(index: Int, looping: Boolean) = setLoopingNative(index, looping)
6767
fun teardownAudioStream() = teardownAudioStreamNative()
6868
fun unloadAssets() = unloadAssetsNative()
69+
fun setPlaybackParameters(speed: Float, pitch: Float): Boolean = setPlaybackParametersNative(speed, pitch)
6970

7071
/**
7172
* Loads a file from assets into memory and returns its WAV properties.
@@ -258,6 +259,7 @@ class PowerPlayAudioPlayer() : DefaultLifecycleObserver {
258259
private external fun getDurationMillisNative(index: Int): Long
259260
private external fun getWavFileInfoNative(wavBytes: ByteArray): IntArray
260261
private external fun removeSampleSourceNative(index: Int): Boolean
262+
private external fun setPlaybackParametersNative(speed: Float, pitch: Float): Boolean
261263

262264
/**
263265
* Companion

0 commit comments

Comments
 (0)