需求描述:
kotlin开发安卓app, Jetpack Compose框架,使用2个EvoPlayer播放res/raw中的音乐,实现2个音轨同时播放,一个是人声音轨,有多个人声音频,另一个是背景音乐音轨,有多个背景音乐, 不断循环播放列表中的音乐, 人声轨道为顺序播放,背景音乐轨道为随机播放, MediaPlayer在viewModel中, UI层有2个按钮,分别控制播放和停止,按钮同时控制2个音轨的播放和停止 。 遇到报错,则继续播放。人声轨道和背景音乐轨道,每次播放下一首音频之前,随机设置播放速度在0.9到1.2之间。
dependencies { // Jetpack Compose implementation "androidx.compose.ui:ui:1.1.1" implementation "androidx.compose.material:material:1.1.1" implementation "androidx.compose.ui:ui-tooling-preview:1.1.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" implementation "androidx.activity:activity-compose:1.4.0" implementation "androidx.activity:activity-ktx:1.4.0" // ExoPlayer implementation 'com.google.android.exoplayer:exoplayer:2.14.1' }
接下来,我们创建ViewModel来管理ExoPlayer:
import android.app.Application import android.content.Context import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackParameters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.random.Random class AudioPlayerViewModel(application: Application) : AndroidViewModel(application) { private val context: Context = application.applicationContext private val voicePlayer: ExoPlayer = ExoPlayer.Builder(context).build().apply { addListener(object: Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED) { playNextVoiceTrack() } } override fun onPlayerError(error: PlaybackException) { playNextVoiceTrack() } }) } private val bgmPlayer: ExoPlayer = ExoPlayer.Builder(context).build().apply { addListener(object: Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED) { playNextBgmTrack() } } override fun onPlayerError(error: PlaybackException) { playNextBgmTrack() } }) } private val voiceTracks = listOf(R.raw.voice1, R.raw.voice2) // Add your voice tracks here private val bgmTracks = listOf(R.raw.bgm1, R.raw.bgm2) // Add your BGM tracks here private var currentVoiceTrackIndex = 0 init { playNextVoiceTrack() playNextBgmTrack() } private fun playAudio(player: ExoPlayer, uri: Uri, playbackSpeed: Float) { player.apply { val mediaItem = MediaItem.fromUri(uri) setMediaItem(mediaItem) playbackParameters = PlaybackParameters(playbackSpeed) prepare() playWhenReady = true } } private fun playNextVoiceTrack() { val uri = getUriForRawFile(context, voiceTracks[currentVoiceTrackIndex]) val playbackSpeed = Random.nextFloat() * (1.2f - 0.9f) + 0.9f // Random speed between 0.9 and 1.2 playAudio(voicePlayer, uri, playbackSpeed) currentVoiceTrackIndex = (currentVoiceTrackIndex + 1) % voiceTracks.size } private fun playNextBgmTrack() { val randomIndex = Random.nextInt(bgmTracks.size) val uri = getUriForRawFile(context, bgmTracks[randomIndex]) val playbackSpeed = Random.nextFloat() * (1.2f - 0.9f) + 0.9f // Random speed between 0.9 and 1.2 playAudio(bgmPlayer, uri, playbackSpeed) } private fun getUriForRawFile(context: Context, resId: Int): Uri { return Uri.parse("android.resource://${context.packageName}/$resId") } fun play() { voicePlayer.playWhenReady = true bgmPlayer.playWhenReady = true } fun stop() { voicePlayer.stop() bgmPlayer.stop() } override fun onCleared() { super.onCleared() voicePlayer.release() bgmPlayer.release() } }
接下来,我们在 UI 层创建 Compose 组件,使用按钮来控制两个音轨的播放和停止。
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApp() } } } fun MyApp(audioPlayerViewModel: AudioPlayerViewModel = viewModel()) { Scaffold( topBar = { TopAppBar(title = { Text("Audio Player") }) }, content = { Column( Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally ) { Button( onClick = { audioPlayerViewModel.play() }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) ) { Text("Play Audio") } Button( onClick = { audioPlayerViewModel.stop() }, modifier = Modifier.fillMaxWidth() ) { Text("Stop Audio") } } } ) } showBackground = true) ( fun DefaultPreview() { MyApp() }
解释
- ViewModel:
- AudioPlayerViewModel 中初始化两个 ExoPlayer 实例: voicePlayer 和 bgmPlayer。
- 添加了两个 Player.Listener 来监听播放状态和错误,分别为人声音轨和背景音乐音轨处理播放结束和错误事件。
- 提供 playNextVoiceTrack 和 playNextBgmTrack 方法来实现分别播放人声音轨和背景音乐音轨,并在每次播放时随机设置播放速度。
- playAudio 方法用来播放指定 URI 的音轨,并设置播放速度。
- play 方法和 stop 方法分别控制两个音轨的播放和停止。
- UI层:
- MyApp 组件使用了两个按钮,通过点击按钮来控制 viewModel 中的播放和停止操作。
通过这种方式,应用程序可以同时播放两个音轨,并分别控制播放和停止。人声音轨按照列表顺序播放,背景音乐音轨随机播放。每次播放下一首音轨之前,都会随机设置播放速度。错误处理逻辑保证了在出现错误时可以继续播放下一首音轨。