📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Bem-vindo à sexta etapa da Fase 7: Arquitetura Avançada & Multimídia! 🎵
Neste projeto prático, você aprenderá como gerenciar recursos de áudio no Android de forma profissional, eficiente e segura contra vazamentos de memória nativa (Native Memory Leaks) utilizando as APIs de mídia do Android (MediaPlayer / ToneGenerator) integradas ao ciclo de vida do Jetpack Compose com DisposableEffect.
Com a temática vibrante da cultura pop oriental e vozes épicas dos animes (Dragon Ball Z, Naruto, JoJo’s Bizarre Adventure, One Piece, Attack on Titan, Demon Slayer), construiremos um soundboard interativo com grade dinâmica, efeitos de iluminação neon e controle inteligente de foco de áudio (Audio Focus)!
| 1. Catálogo Soundboard | 2. Reproduzindo (Dio Brando) | 3. Troca Dinâmica (Goku) |
|---|---|---|
| Grade de 2 colunas com cards de personagens. | Destaque visual neon, áudio ativo e botão de parada. | Liberação imediata do áudio anterior e transição limpa. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
MediaPlayer: Inicializar, tocar e encerrar reproduções sonoras a partir da pasta res/raw/.DisposableEffect: Garantir a chamada de mediaPlayer.release() no gancho onDispose do Compose para desocupar drivers de hardware nativos.ToneGenerator com frequências DTMF quando um arquivo físico .mp3 não for encontrado, impedindo travamentos (Crashes).LazyVerticalGrid: Distribuir cards reativos com bordas dinâmicas de acordo com o estado do som ativo.graph TD
A[Usuário clica no Card de Personagem] --> B[Verifica se existe activePlayer anterior]
B --> C[Executa player.stop e player.release - Limpa driver nativo C++]
C --> D{Arquivo .mp3 existe em res/raw/?}
D -- Sim --> E[MediaPlayer.create -> player.start]
D -- Não --> F[ToneGenerator executa frequência DTMF com release temporizado]
E --> G[Card ativo ganha borda Neon e barra superior exibe TOCANDO AGORA]
G --> H[player.setOnCompletionListener desliga estado ao terminar áudio]
I[Usuário fecha o app ou muda de tela] --> J[DisposableEffect aciona onDispose]
J --> K[activePlayer.release - Garante ZERO Memory Leaks]
[!IMPORTANT] Por que o
MediaPlayercausa Memory Leaks se não for liberado? Ao contrário de objetos comuns da JVM, oMediaPlayere oSoundPoolmantêm ponteiros para drivers de áudio nativos em C++ (OpenSL ES / AAudio) fora do controle do Garbage Collector.Nossas Práticas de Engenharia de Software:
- Garantia de Liberação com
DisposableEffect: Quando o Composable sai da tela, o blocoonDisposeexecutarelease(), devolvendo a memória do codec de áudio ao sistema operacional.- Prevenção de Esgotamento de Canais: Chamamos
release()na instância ativa antes de criar uma nova.- ToneGenerator Resiliente: Usado como fallback sonoro não-bloqueante e devidamente liberado após a execução.
- Higiene de Versionamento: Arquivo
.gitignoreadicionado para proteger artefatos locais.
| Termo / Componente | O que é e para que serve? |
|---|---|
| MediaPlayer | Classe do Android para reprodução e controle de fluxos de áudio e vídeo de média/longa duração. |
| DisposableEffect | Efeito colateral do Jetpack Compose que oferece uma função onDispose para limpar recursos quando o Composable deixa a árvore de nós. |
| mediaPlayer.release() | Método que destrói o decodificador nativo de áudio alocado no driver C++ do sistema operacional. |
| ToneGenerator | Utilitário de baixo nível do Android para emitir frequências de bipe e tons DTMF no canal STREAM_MUSIC. |
| LazyVerticalGrid | Contêiner de alto desempenho do Compose para renderizar itens em múltiplas colunas com reciclagem de memória. |
File > New > New Project).Anime Soundboard (ou android_p17_soundboard_audio)br.com.curso.soundboardKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)build.gradle)Abra app > build.gradle:
dependencies {
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}
AndroidManifest.xml)Abra app > src > main > AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="Anime Soundboard"
android:supportsRtl="true"
android:theme="@style/Theme.CartaoTreinador">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
SoundItem)Abra app > src > main > java > br > com > curso > soundboard > MainActivity.kt:
data class SoundItem(
val name: String,
val quote: String,
val anime: String,
val emoji: String,
val audioResName: String,
val toneFreq: Int
)
val animeSounds = listOf(
SoundItem("Goku", "Kamehameha!", "Dragon Ball Z", "🔥", "goku_sfx", ToneGenerator.TONE_DTMF_1),
SoundItem("Naruto", "Dattebayo!", "Naruto", "🦊", "naruto_sfx", ToneGenerator.TONE_DTMF_2),
SoundItem("Dio Brando", "Za Warudo! Toki yo Tomare!", "JoJo's Adventure", "⏳", "dio_sfx", ToneGenerator.TONE_DTMF_3),
SoundItem("Luffy", "Gomu Gomu No...", "One Piece", "👒", "luffy_sfx", ToneGenerator.TONE_DTMF_4),
SoundItem("Levi", "Kennyyyy!", "Attack on Titan", "⚔️", "levi_sfx", ToneGenerator.TONE_DTMF_5),
SoundItem("Tanjiro", "Hinokami Kagura!", "Demon Slayer", "🌊", "tanjiro_sfx", ToneGenerator.TONE_DTMF_6)
)
@Composable
fun SoundboardScreen() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var somTocando by remember { mutableStateOf<SoundItem?>(null) }
var activePlayer by remember { mutableStateOf<MediaPlayer?>(null) }
// Liberação preventiva de memória nativa ao desmontar o Composable
DisposableEffect(Unit) {
onDispose {
activePlayer?.stop()
activePlayer?.release()
activePlayer = null
}
}
fun playSound(sound: SoundItem) {
// Limpa reprodução anterior
activePlayer?.stop()
activePlayer?.release()
activePlayer = null
somTocando = sound
try {
val resourceId = context.resources.getIdentifier(sound.audioResName, "raw", context.packageName)
if (resourceId != 0) {
val player = MediaPlayer.create(context, resourceId)
activePlayer = player
player.setOnCompletionListener {
it.release()
if (somTocando == sound) somTocando = null
}
player.start()
} else {
// Fallback sonoro nativo caso o aluno ainda não tenha adicionado os arquivos .mp3
val toneGen = ToneGenerator(AudioManager.STREAM_MUSIC, 90)
toneGen.startTone(sound.toneFreq, 300)
coroutineScope.launch {
delay(350)
toneGen.release()
if (somTocando == sound) somTocando = null
}
}
} catch (e: Exception) {
e.printStackTrace()
somTocando = null
}
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(animeSounds, key = { it.name }) { sound ->
val isPlaying = somTocando?.name == sound.name
Card(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.clickable { playSound(sound) },
colors = CardDefaults.cardColors(
containerColor = if (isPlaying) Color(0xFF2E193C) else Color(0xFF191124)
),
shape = RoundedCornerShape(16.dp),
border = if (isPlaying) CardDefaults.outlinedCardBorder().copy(brush = Brush.linearGradient(listOf(Color(0xFFFF4081), Color(0xFF00E5FF)))) else null
) {
Column(
modifier = Modifier.fillMaxSize().padding(12.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(sound.emoji, fontSize = 40.sp)
Text(sound.name, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 14.sp)
Text("\"${sound.quote}\"", color = Color(0xFFFF4081), fontSize = 11.sp, fontWeight = FontWeight.Bold)
}
}
}
}
| Sintoma Observado | Causa Provável | Como Resolver |
|---|---|---|
OutOfMemoryError após clicar em muitos botões seguidos |
O MediaPlayer não está sendo liberado com release(), acumulando dezenas de instâncias no driver de áudio. |
Chame activePlayer?.release() antes de criar um novo MediaPlayer e dentro do onDispose do DisposableEffect. |
| O som não toca no emulador | O volume de mídia do emulador está zerado ou o arquivo .mp3 contém formato incompatível. |
Aumente o volume de multimídia nas configurações do Android e certifique-se de usar .mp3 padrão (44.1kHz / 128kbps). |
| Vários áudios tocam ao mesmo tempo de forma confusa | Falta de uma variável central (activePlayer) para pausar o som anterior ao clicar em um novo card. |
Mantenha um único ponteiro de MediaPlayer ativo e interrompa-o ao disparar um novo item. |
Resources$NotFoundException ao carregar áudio |
O identificador do recurso em res/raw/ foi escrito com letras maiúsculas ou caracteres inválidos. |
Mantenha nomes de arquivos de recursos em minúsculas (ex: goku_sfx.mp3). |
PlaybackParams: Permita ao usuário acelerar o áudio dos animes em 1.5x ou 2.0x usando mediaPlayer.playbackParams.player.isLooping = true)..mp3 do armazenamento interno usando o seletor rememberLauncherForActivityResult(ActivityResultContracts.GetContent()).Disponível em: app/src/main/java/br/com/curso/soundboard/MainActivity.kt.
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd proj_aplicacoes_full_stack/projetos/android_p17_soundboard_audio
./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta
proj_aplicacoes_full_stack/projetos/android_p17_soundboard_audio.