📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

🎵 P17: Anime Soundboard (MediaPlayer, Áudio & Ciclo de Vida)

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)!

Fluxo Completo de Telas do Anime Soundboard


📱 Galeria de Telas da Aplicação

1. Catálogo Soundboard 2. Reproduzindo (Dio Brando) 3. Troca Dinâmica (Goku)
Tela 1 - Soundboard Grid Tela 2 - Dio Brando Tela 3 - 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.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Manipular Áudios com MediaPlayer: Inicializar, tocar e encerrar reproduções sonoras a partir da pasta res/raw/.
  2. Prevenir Vazamentos de Memória com DisposableEffect: Garantir a chamada de mediaPlayer.release() no gancho onDispose do Compose para desocupar drivers de hardware nativos.
  3. Controlar Foco e Sobreposição de Áudio: Liberar instâncias anteriores de mídia antes de iniciar um novo som, evitando cacofonia e sobrecarga de canais de áudio.
  4. Implementar Fallbacks de Áudio Seguros: Usar ToneGenerator com frequências DTMF quando um arquivo físico .mp3 não for encontrado, impedindo travamentos (Crashes).
  5. Construir Layouts em Grade com LazyVerticalGrid: Distribuir cards reativos com bordas dinâmicas de acordo com o estado do som ativo.

🏗️ Ciclo de Vida do Áudio no Compose

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]

🛡️ Boas Práticas de Gerenciamento de Memória e Áudio

[!IMPORTANT] Por que o MediaPlayer causa Memory Leaks se não for liberado? Ao contrário de objetos comuns da JVM, o MediaPlayer e o SoundPool mantê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:

  1. Garantia de Liberação com DisposableEffect: Quando o Composable sai da tela, o bloco onDispose executa release(), devolvendo a memória do codec de áudio ao sistema operacional.
  2. Prevenção de Esgotamento de Canais: Chamamos release() na instância ativa antes de criar uma nova.
  3. ToneGenerator Resiliente: Usado como fallback sonoro não-bloqueante e devidamente liberado após a execução.
  4. Higiene de Versionamento: Arquivo .gitignore adicionado para proteger artefatos locais.

📖 Dicionário Técnico do Projeto

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.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: Anime Soundboard (ou android_p17_soundboard_audio)
    • Package name: br.com.curso.soundboard
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde o Gradle sincronizar.

📦 Passo 2: Configurando Dependências no Gradle (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'
}

📄 Passo 3: Manifesto do Aplicativo (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>

🎴 Passo 4: Modelagem de Dados dos Personagens (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)
)

🔊 Passo 5: Gerenciamento de Ciclo de Vida e Tocador com Fallback

@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
        }
    }
}

🍱 Passo 6: Grade de Cards do Soundboard com Compose

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

🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

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).

🏆 Desafios e Upgrades (Mão na Massa!)

  1. 🎚️ Controle de Pitch e Velocidade com PlaybackParams: Permita ao usuário acelerar o áudio dos animes em 1.5x ou 2.0x usando mediaPlayer.playbackParams.
  2. 🔄 Modo Loop: Adicione um botão para manter a frase do personagem repetindo em loop contínuo (player.isLooping = true).
  3. 📁 Carregamento de Áudios Externos: Permita ao usuário importar novos arquivos .mp3 do armazenamento interno usando o seletor rememberLauncherForActivityResult(ActivityResultContracts.GetContent()).

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/soundboard/MainActivity.kt.


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

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

2. Execute a aplicação e os testes

./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.