🦊 P17: Anime Soundboard (Mídia Avançada com MediaPlayer)
Bem-vindo à Fase 9: Mídia & Gráficos Customizados! 🦊
Neste projeto focado na manipulação de recursos de áudio do smartphone, você aprenderá a carregar e reproduzir arquivos sonoros e gerenciar a concorrência de múltiplos players para evitar poluição auditiva, utilizando a ferramenta nativa MediaPlayer (ou Media3 ExoPlayer do Google). O tema do app será um painel de mesa de som oriental (Anime Soundboard), disparando vozes marcantes dos animes (Goku, Naruto, Luffy) em cards vibrantes dispostos em grade na interface do Compose.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P16: Alerta Gotham
🎯 Objetivo do Projeto
Estruturar o controle e a reprodução de mídia de forma limpa contendo:
- Controle de Players: Gerenciar o ciclo de vida de alocação de tocadores em segundo plano (
release). - Identificação de Recursos Dinâmicos: Buscar referências de IDs da pasta
/res/rawde forma dinâmica usando nomes em Strings. - Concorrência de Canais: Evitar a sobreposição indesejada de sons, interrompendo sons anteriores ao tocar um novo card.
- Grade de Mídias: Organizar e estilizar células de grade reativas com
LazyVerticalGrid.
graph TD
A[Usuário toca no card de áudio Goku] --> B[Dispara função playSound goku_sfx]
B --> C[Executa activePlayer.release para encerrar qualquer som anterior]
C --> D[Busca dinamicamente o ID do arquivo goku_sfx na pasta res/raw]
D --> E{Arquivo res/raw existe?}
E -- Sim --> F[MediaPlayer.create carrega o áudio em memória]
E -- Não --> G[Dispara oscilador ToneGenerator como Fallback sonoro de segurança]
F --> H[activePlayer.start executa o áudio]
📖 Dicionário do Projeto
- MediaPlayer: A classe nativa do ecossistema Android usada para controlar a reprodução de mídias de áudio e vídeo de fontes locais (arquivos do app) ou remotas (URLs web).
- activePlayer.release(): Comando essencial que libera a memória RAM e os codecs físicos do processador de áudio do smartphone quando o som acaba ou é interrompido.
- Folder /res/raw: A pasta especial dentro dos recursos do Android Studio reservada para armazenar mídias cruas não modificadas (como arquivos MP3, WAV e OGG).
- ToneGenerator: Utilitário nativo de hardware capaz de oscilar bipes e bips sonoros em frequências específicas diretamente no alto-falante, muito útil para fallbacks de som em testes.
🛠️ Passo 1: Criando a Pasta de Áudios Recortados (/res/raw)
Para que seu app possa tocar os arquivos reais, o Android exige uma pasta física específica de mídias.
- No painel de arquivos esquerdo do Android Studio, clique com o botão direito na pasta
app > src > main > res. - Selecione
New > Android Resource Directory. - Em Resource type, escolha
rawe clique emOK. - Copie seus arquivos de som curtos preferidos no formato
.mp3para dentro desta pasta (use apenas letras minúsculas e sem caracteres especiais, ex:goku_sfx.mp3).
🛠️ Passo 2: Codificando a Grade e Lógica de Concorrência
Em seu arquivo MainActivity.kt, declare os itens de som em uma lista estática e gerencie a liberação de canais para evitar vazamentos de memória:
var activePlayer: MediaPlayer? = null
fun playSound(audioName: String) {
// 1. Liberação Crítica de Canais Anteriores
activePlayer?.release()
// 2. Busca do Arquivo de Áudio por String Dinâmica
val resourceId = context.resources.getIdentifier(audioName, "raw", context.packageName)
if (resourceId != 0) {
// 3. Inicialização e Disparo
activePlayer = MediaPlayer.create(context, resourceId)
activePlayer?.start()
}
}
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Controle de Volume: adicione um
Sliderpara ajustar o volume doMediaPlayerem tempo real. - Novo Personagem: adicione um novo
SoundItemà listaanimeSoundscom seu próprio som e cor.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/soundboard/MainActivity.kt
package br.com.curso.soundboard
import android.media.MediaPlayer
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SoundboardTheme {
SoundboardScreen()
}
}
}
}
@Composable
fun SoundboardTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFFF4081), // Rosa Choque Anime
background = Color(0xFF0F0B13),
surface = Color(0xFF1B1424)
),
content = content
)
}
// 1. DATA MODEL
data class SoundItem(val name: String, val quote: String, val emoji: String, val audioResName: String)
val animeSounds = listOf(
SoundItem("Goku", "Kamehameha!", "🔥", "goku_sfx"),
SoundItem("Naruto", "Dattebayo!", "🦊", "naruto_sfx"),
SoundItem("Dio Brando", "Za Warudo!", "⏳", "dio_sfx"),
SoundItem("Luffy", "Gomu Gomu No...", "👒", "luffy_sfx")
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SoundboardScreen() {
val context = LocalContext.current
var activePlayer: MediaPlayer? = null
fun playSound(audioName: String) {
// Libera qualquer tocador em andamento para evitar sobreposição de canais (Audio Focus local)
activePlayer?.release()
// Em um app de verdade, o áudio seria carregado da pasta res/raw/audioName.mp3
// Para fins didáticos de conferência, encapsulamos o construtor padrão
try {
val resourceId = context.resources.getIdentifier(audioName, "raw", context.packageName)
if (resourceId != 0) {
activePlayer = MediaPlayer.create(context, resourceId)
activePlayer?.start()
} else {
// Caso o arquivo físico de exemplo raw/ ainda não exista no build do aluno,
// simulamos o disparo auditivo via bip do sistema para não crashar
val toneGen = android.media.ToneGenerator(android.media.AudioManager.STREAM_MUSIC, 100)
toneGen.startTone(android.media.ToneGenerator.TONE_PROP_BEEP2, 200)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("ANIME SOUNDBOARD v1.0", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color(0xFF1B1424),
titleContentColor = Color(0xFFFF4081)
)
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFF0F0B13))
.padding(16.dp)
) {
Text(
text = "Tire o celular do silencioso e toque nos cards abaixo para disparar as vozes mais marcantes da cultura pop oriental!",
color = Color.Gray,
fontSize = 14.sp,
modifier = Modifier.padding(bottom = 16.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(animeSounds) { sound ->
Card(
modifier = Modifier
.fillMaxWidth()
.height(160.dp)
.clickable { playSound(sound.audioResName) },
colors = CardDefaults.cardColors(containerColor = Color(0xFF1B1424)),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(sound.emoji, fontSize = 48.sp)
Spacer(modifier = Modifier.height(8.dp))
Text(sound.name, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 16.sp)
Text("\"${sound.quote}\"", color = Color(0xFFFF4081), fontSize = 12.sp)
}
}
}
}
}
}
}