🦊 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:


🎯 Objetivo do Projeto

Estruturar o controle e a reprodução de mídia de forma limpa contendo:

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


🛠️ 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.

  1. No painel de arquivos esquerdo do Android Studio, clique com o botão direito na pasta app > src > main > res.
  2. Selecione New > Android Resource Directory.
  3. Em Resource type, escolha raw e clique em OK.
  4. Copie seus arquivos de som curtos preferidos no formato .mp3 para 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:


📖 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)
                        }
                    }
                }
            }
        }
    }
}