CPS251 Android Development by Scott Shaper

Audio Handling

Imagine you're building a music app like Spotify or a podcast app. You need to play audio files, let users control playback, and show information about what's playing. Just like a music player that lets you play, pause, skip tracks, and adjust volume, audio handling in Android apps requires special components that can play audio files, manage playback state, and provide controls. Audio playback is similar to video but simpler because you only need to handle sound, not visuals.

In this lesson, we'll learn how to play audio in your Android app using ExoPlayer, which can handle both audio and video. We'll create a simple music player with play, pause, and seek controls, and learn how to manage audio playback state.

When to Use Audio Handling

  • When creating music or podcast applications
  • When playing sound effects or notifications
  • When implementing audio books or educational content
  • When building voice recording and playback features
  • When creating audio streaming applications
  • When implementing background audio playback
  • When building radio or live audio streaming apps
  • When adding sound effects to games or apps

Key Audio Handling Concepts

Concept What It Does When to Use It
ExoPlayer Media player library that can play audio files When you need to play audio from URLs or files
MediaItem Represents an audio source (URL or file) When you need to specify what audio to play
Player The main interface for controlling audio playback When you need to control play, pause, seek, etc.
prepare() Prepares the player to play an audio item When you want to load audio before playing
play() / pause() Controls audio playback When you need to start or stop playback
currentPosition Gets the current playback position When you need to show progress or seek
duration Gets the total duration of the audio When you need to show total time or progress

Project Setup: Music Player App

Let's start by setting up our MusicPlayer project. This will be a simple app that demonstrates how to play audio files using ExoPlayer.

Step 1: Adding Dependencies

We'll use the same ExoPlayer dependency we used for video. Update the gradle/libs.versions.toml file:

[versions]
...
exoplayer = "2.19.1"  # Add this line

[libraries]
...
exoplayer-core = { group = "com.google.android.exoplayer", name = "exoplayer-core", version.ref = "exoplayer" }  # Add this line

Then update the app/build.gradle.kts file:

dependencies {
    ...
    implementation(libs.exoplayer.core)
    
}

Step 2: Adding Internet Permission

Since our app will play audio from the internet, we need to add the internet permission to AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permission for internet access to stream audio -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        <!--manifest code here-->
    </application>
</manifest>

Step 3: Understanding the App Structure

Our MusicPlayer app will have a simple structure with:

  • Song List - A list of available songs to play
  • Now Playing Screen - Shows the currently playing song with controls
  • Playback Controls - Play, pause, previous, next buttons
  • Progress Indicator - Shows current position and duration

Step 4: Creating the Data Model

First, let's create our data model. Create a new file called Song.kt:

package com.example.musicplayer

// Data class representing a song in our player
data class Song(
    val id: String,
    val title: String,
    val artist: String,
    val audioUrl: String,
    val coverImageUrl: String
)

What this does:

  • id - Unique identifier for each song
  • title - The title of the song
  • artist - The artist name
  • audioUrl - URL for the audio file
  • coverImageUrl - URL for the album cover image

Step 5: Creating Sample Data

Now let's create sample data for our app. Create SampleSongData.kt:

package com.example.musicplayer

// Sample song data using public test audio files
object SampleSongData {
    
    fun getSampleSongs(): List {
        return listOf(
            Song(
                id = "1",
                title = "Sample Song 1",
                artist = "Test Artist",
                audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
                coverImageUrl = "https://picsum.photos/400/400?random=1"
            ),
            Song(
                id = "2",
                title = "Sample Song 2",
                artist = "Test Artist",
                audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
                coverImageUrl = "https://picsum.photos/400/400?random=2"
            ),
            Song(
                id = "3",
                title = "Sample Song 3",
                artist = "Test Artist",
                audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3",
                coverImageUrl = "https://picsum.photos/400/400?random=3"
            )
        )
    }
}

Step 6: Creating an Audio Player Manager

Let's create a class to manage audio playback. Create AudioPlayerManager.kt:

package com.example.musicplayer

import android.content.Context
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem

class AudioPlayerManager(private val context: Context) {
    private var exoPlayer: ExoPlayer? = null
    
    fun initializePlayer(): ExoPlayer {
        if (exoPlayer == null) {
            exoPlayer = ExoPlayer.Builder(context).build()
        }
        return exoPlayer!!
    }
    
    fun playSong(audioUrl: String) {
        val player = initializePlayer()
        val mediaItem = MediaItem.fromUri(audioUrl)
        player.setMediaItem(mediaItem)
        player.prepare()
        player.play()
    }
    
    fun pause() {
        exoPlayer?.pause()
    }
    
    fun resume() {
        exoPlayer?.play()
    }
    
    fun stop() {
        exoPlayer?.stop()
    }
    
    fun release() {
        exoPlayer?.release()
        exoPlayer = null
    }
    
    fun getPlayer(): ExoPlayer? = exoPlayer
}

Step 7: Creating the Now Playing Screen

Now let's create a screen that shows the currently playing song with controls. Create NowPlayingScreen.kt:

package com.example.musicplayer

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.google.android.exoplayer2.Player

@Composable
fun NowPlayingScreen(
    song: Song,
    player: Player?,
    onPlayPauseClick: () -> Unit,
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    val isPlaying = player?.isPlaying ?: false
    val currentPosition = remember { mutableLongStateOf(0L) }
    val duration = player?.duration ?: 0L
    
    // Update current position periodically
    LaunchedEffect(player) {
        while (player != null) {
            currentPosition.longValue = player.currentPosition
            kotlinx.coroutines.delay(100) // Update every 100ms
        }
    }
    
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Now Playing") },
                navigationIcon = {
                    IconButton(onClick = onBackClick) {
                        Icon(
                            imageVector = Icons.Default.ArrowBack,
                            contentDescription = "Back"
                        )
                    }
                }
            )
        }
    ) { paddingValues ->
        Column(
            modifier = modifier
                .fillMaxSize()
                .padding(paddingValues)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            // Album cover
            AsyncImage(
                model = song.coverImageUrl,
                contentDescription = "${song.title} by ${song.artist}",
                modifier = Modifier
                    .size(300.dp)
                    .clip(CircleShape),
                contentScale = ContentScale.Crop
            )
            
            Spacer(modifier = Modifier.height(32.dp))
            
            // Song info
            Text(
                text = song.title,
                style = MaterialTheme.typography.headlineMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = song.artist,
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
            )
            
            Spacer(modifier = Modifier.height(32.dp))
            
            // Progress bar
            if (duration > 0) {
                LinearProgressIndicator(
                    progress = { (currentPosition.longValue.toFloat() / duration.toFloat()).coerceIn(0f, 1f) },
                    modifier = Modifier.fillMaxWidth()
                )
                Spacer(modifier = Modifier.height(8.dp))
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    Text(
                        text = formatTime(currentPosition.longValue),
                        style = MaterialTheme.typography.bodySmall
                    )
                    Text(
                        text = formatTime(duration),
                        style = MaterialTheme.typography.bodySmall
                    )
                }
            }
            
            Spacer(modifier = Modifier.height(32.dp))
            
            // Playback controls
            Row(
                horizontalArrangement = Arrangement.spacedBy(24.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                IconButton(onClick = { /* Previous song */ }) {
                    Icon(
                        imageVector = Icons.Default.SkipPrevious,
                        contentDescription = "Previous",
                        modifier = Modifier.size(32.dp)
                    )
                }
                
                FloatingActionButton(
                    onClick = onPlayPauseClick,
                    modifier = Modifier.size(64.dp)
                ) {
                    Icon(
                        imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
                        contentDescription = if (isPlaying) "Pause" else "Play",
                        modifier = Modifier.size(32.dp)
                    )
                }
                
                IconButton(onClick = { /* Next song */ }) {
                    Icon(
                        imageVector = Icons.Default.SkipNext,
                        contentDescription = "Next",
                        modifier = Modifier.size(32.dp)
                    )
                }
            }
        }
    }
}

fun formatTime(milliseconds: Long): String {
    val seconds = (milliseconds / 1000) % 60
    val minutes = (milliseconds / (1000 * 60)) % 60
    return String.format("%02d:%02d", minutes, seconds)
}

Step 8: Creating the Song List Screen

Let's create a screen that shows a list of available songs. Create SongListScreen.kt:

package com.example.musicplayer

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun SongListScreen(
    songs: List,
    onSongClick: (Song) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .padding(top = 50.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
    ) {
        items(songs) { song ->
            SongListItem(
                song = song,
                onClick = { onSongClick(song) }
            )
        }
    }
}

@Composable
fun SongListItem(
    song: Song,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // Album cover thumbnail
            AsyncImage(
                model = song.coverImageUrl,
                contentDescription = "${song.title} by ${song.artist}",
                modifier = Modifier
                    .size(64.dp)
                    .clip(CircleShape),
                contentScale = ContentScale.Crop
            )
            
            Spacer(modifier = Modifier.width(16.dp))
            
            // Song info
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = song.title,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = song.artist,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
                )
            }
        }
    }
}

Step 9: Updating MainActivity

Finally, let's update MainActivity.kt to use our music player:

package com.example.musicplayer

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.example.musicplayer.ui.theme.MusicPlayerTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MusicPlayerTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    MusicPlayerApp(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun MusicPlayerApp(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val audioPlayerManager = remember { AudioPlayerManager(context) }
    var selectedSong by remember { mutableStateOf(null) }
    val songs = remember { SampleSongData.getSampleSongs() }
    
    // Initialize player
    val player = remember { audioPlayerManager.initializePlayer() }
    
    // Clean up when done
    DisposableEffect(Unit) {
        onDispose {
            audioPlayerManager.release()
        }
    }
    
    if (selectedSong == null) {
        SongListScreen(
            songs = songs,
            onSongClick = { song ->
                selectedSong = song
                audioPlayerManager.playSong(song.audioUrl)
            }
        )
    } else {
        NowPlayingScreen(
            song = selectedSong!!,
            player = player,
            onPlayPauseClick = {
                if (player.isPlaying) {
                    audioPlayerManager.pause()
                } else {
                    audioPlayerManager.resume()
                }
            },
            onBackClick = {
                audioPlayerManager.stop()
                selectedSong = null
            }
        )
    }
}

Understanding Each Part of the Implementation

Audio Player Setup:

  • AudioPlayerManager - Manages the ExoPlayer instance
  • initializePlayer() - Creates or returns the player
  • playSong() - Loads and plays an audio file
  • pause() / resume() - Controls playback

Playback Controls:

  • isPlaying - Checks if audio is currently playing
  • currentPosition - Gets the current playback position
  • duration - Gets the total duration of the audio
  • LaunchedEffect - Updates position periodically

UI Components:

  • LinearProgressIndicator - Shows playback progress
  • formatTime() - Formats milliseconds to MM:SS
  • Play/Pause button - Toggles playback state

What This App Demonstrates

This MusicPlayer app demonstrates several important concepts:

Basic Audio Playback
  • Loading Audio - How to load audio from URLs
  • Playback Control - Play, pause, and stop functionality
  • State Management - Managing playback state
Progress Tracking
  • Current Position - Tracking where you are in the song
  • Duration - Getting the total length
  • Progress Display - Showing progress visually

Running the App

To run the MusicPlayer app:

  1. Open the project in Android Studio
  2. Build and run the app on a device or emulator
  3. Test the following features:
    • Browse the list of songs
    • Click on a song to play it
    • Use the play/pause button
    • Observe the progress bar
    • Go back to the song list

Why This Implementation is Important

This example demonstrates how to:

  • Play audio files from the internet or local storage
  • Control playback with play, pause, and stop
  • Track progress and show it to users
  • Manage player lifecycle to avoid memory leaks
  • Create a music player UI with controls and information

By using ExoPlayer for audio playback, you create apps that can reliably play audio with proper controls and state management. This is essential for any app that needs to play audio, whether it's a music app, podcast app, or any app with sound effects.

Tips for Success

  • Use ExoPlayer for audio playback - it works great for both audio and video
  • Always release the player when done to avoid memory leaks
  • Use DisposableEffect to clean up resources properly
  • Update progress indicators periodically using LaunchedEffect
  • Format time display in a user-friendly way (MM:SS)
  • Handle network errors gracefully when streaming audio
  • Show loading states while audio prepares
  • Add internet permission when streaming audio from URLs
  • Test with different audio formats and qualities
  • Consider background playback for music apps

Common Mistakes to Avoid

  • Forgetting to release the ExoPlayer instance, causing memory leaks
  • Not adding internet permission for streaming audio
  • Forgetting to add ExoPlayer dependencies
  • Not handling errors when audio fails to load
  • Not updating progress indicators, leaving them static
  • Playing multiple audio files at once without proper management
  • Ignoring player lifecycle and resource cleanup
  • Not providing proper error messages when playback fails
  • Forgetting to format time display properly
  • Not testing on slower devices or network connections

Best Practices

  • Always release ExoPlayer instances when done to prevent memory leaks
  • Use DisposableEffect for proper resource cleanup
  • Update progress indicators regularly for good user experience
  • Format time display in a readable format (MM:SS or HH:MM:SS)
  • Handle network errors and playback failures gracefully
  • Show loading states while audio prepares
  • Test with various audio formats and network conditions
  • Consider implementing background playback for music apps
  • Provide clear error messages when audio fails to load
  • Follow Android's media playback guidelines