【Android】安卓原生应用播放背景音乐与音效(笔记)


本文提供完整的音频管理器代码,涵盖了背景音乐(BGM)和短音效的播放控制。无论是游戏中的音效,还是应用中的背景音乐,通过 AudioManager,你可以方便地管理和控制音频资源。


前言

在 Android 开发中,音频处理是提升用户体验的重要部分,尤其在游戏、音效反馈以及媒体应用中,背景音乐(BGM)和短音效(如点击音效、通知音效)是常见的需求。通过有效的音频管理,能确保音效播放的流畅性和低延迟,以及背景音乐的播放控制。

本篇文章将展示如何通过封装一个 AudioManager 类,来统一管理背景音乐和短音效的播放。我们将实现以下功能:

  • 短音效:如点击、提示音等,通过 SoundPool 实现快速播放。
  • 背景音乐(BGM):适用于长时间播放的音频,通过 MediaPlayer 实现稳定播放。
  • 音频管理器:封装所有音频控制操作,方便全局调用,支持音效加载、播放、暂停、停止等功能。

音频播放机制

在 Android 中,音频播放通常分为两类:背景音乐和短音效。

MediaPlayer

使用MediaPlayer播放背景音乐,MediaPlayer 适合播放较长的音频文件,它支持流式播放,可以处理长时间的音频内容。

SoundPool

SoundPool 是专为短小音效设计的音频播放机制,适用于播放简单的音效(如短的 MP3、WAV 文件)。其特点是延迟低、资源占用少、支持同时播放多个音效,非常适合游戏或交互性较强的应用。

在 Android 中播放短音效(如短的 mp3、wav 文件),推荐用 SoundPool,专门为短小音效优化,延迟低、效率高!

如果用 MediaPlayer 播放短音效,性能没那么好,启动也慢一点。

初始化SoundPool

java 复制代码
import android.media.AudioAttributes;
import android.media.SoundPool;

private SoundPool soundPool;
private int soundId;

创建SoundPool实例

(适配 Android 5.0+ 和更低版本)

java 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    AudioAttributes audioAttributes = new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .build();

    soundPool = new SoundPool.Builder()
            .setMaxStreams(5)  // 同时播放几个音效
            .setAudioAttributes(audioAttributes)
            .build();
} else {
    // 老版本
    soundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
}

加载音效文件

java 复制代码
// 例如文件名是 sound_effect.mp3
soundId = soundPool.load(context, R.raw.sound_effect, 1);

播放音效

java 复制代码
soundPool.play(soundId, 1f, 1f, 1, 0, 1f);
// 参数解释:
// soundId      -> 加载时返回的 id
// leftVolume   -> 左声道音量 (0-1)
// rightVolume  -> 右声道音量 (0-1)
// priority     -> 优先级(通常为 1)
// loop         -> 循环次数,0 表示不循环,-1 表示无限循环
// rate         -> 播放速率 (0.5-2.0)

释放资源

在 Activity 或 Fragment 销毁时调用

java 复制代码
@Override
protected void onDestroy() {
    super.onDestroy();
    if (soundPool != null) {
        soundPool.release();
        soundPool = null;
    }
}

AudioManager

需求概述

我们的 AudioManager 类将负责以下操作:

  • 背景音乐(BGM)控制:播放、暂停、停止和循环。
  • 短音效控制:加载、播放、释放资源。
  • 支持 绝对路径 音频文件(如 /sdcard/...)。
  • 实现 单例模式,便于全局调用。

完整代码

java 复制代码
import android.content.Context;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.os.Build;
import android.util.Log;

import java.io.IOException;
import java.util.HashMap;

/**
 * AudioManager - 统一管理 BGM 和 音效
 */
public class AudioManager {

    private static final String TAG = "AudioManager";

    private static AudioManager instance;

    // 音效
    private SoundPool soundPool;
    private HashMap<Integer, Integer> soundMap;
    private boolean soundPoolLoaded = false;

    // 背景音乐
    private MediaPlayer bgmPlayer;
    private boolean isBgmPrepared = false;

    private AudioManager() {
        soundMap = new HashMap<>();
    }

    public static AudioManager getInstance() {
        if (instance == null) {
            synchronized (AudioManager.class) {
                if (instance == null) {
                    instance = new AudioManager();
                }
            }
        }
        return instance;
    }

    // -------------------- SoundPool - 音效部分 --------------------

    /**
     * 初始化 SoundPool
     */
    public void initSoundPool(Context context) {
        if (soundPool != null) return;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build();

            soundPool = new SoundPool.Builder()
                    .setMaxStreams(10)
                    .setAudioAttributes(audioAttributes)
                    .build();
        } else {
            soundPool = new SoundPool(10, android.media.AudioManager.STREAM_MUSIC, 0);
        }

        soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
            soundPoolLoaded = true;
            Log.d(TAG, "SoundPool loaded: " + sampleId);
        });
    }

    /**
     * 加载音效(绝对路径)
     */
    public void loadSound(String absolutePath, int soundKey) {
        if (soundPool == null) {
            throw new IllegalStateException("SoundPool not initialized! Call initSoundPool(context) first.");
        }

        int soundId = soundPool.load(absolutePath, 1);
        soundMap.put(soundKey, soundId);
    }

    /**
     * 播放音效
     */
    public void playSound(int soundKey) {
        if (!soundPoolLoaded) {
            Log.w(TAG, "SoundPool not loaded yet");
            return;
        }

        Integer soundId = soundMap.get(soundKey);
        if (soundId != null) {
            soundPool.play(soundId, 1f, 1f, 1, 0, 1f);
        } else {
            Log.w(TAG, "Sound key not found: " + soundKey);
        }
    }

    /**
     * 释放音效资源
     */
    public void releaseSoundPool() {
        if (soundPool != null) {
            soundPool.release();
            soundPool = null;
            soundMap.clear();
            soundPoolLoaded = false;
        }
    }

    // -------------------- MediaPlayer - 背景音乐部分 --------------------

    /**
     * 播放背景音乐
     * @param absolutePath 文件绝对路径
     * @param looping 是否循环
     */
    public void playBgm(String absolutePath, boolean looping) {
        stopBgm(); // 先停止再播放

        bgmPlayer = new MediaPlayer();
        try {
            bgmPlayer.setDataSource(absolutePath);
            bgmPlayer.setLooping(looping);
            bgmPlayer.setOnPreparedListener(mp -> {
                isBgmPrepared = true;
                mp.start();
                Log.d(TAG, "BGM started");
            });
            bgmPlayer.setOnCompletionListener(mp -> Log.d(TAG, "BGM completed"));
            bgmPlayer.prepareAsync();
        } catch (IOException e) {
            Log.e(TAG, "Error playing BGM: " + e.getMessage());
        }
    }

    /**
     * 暂停 BGM
     */
    public void pauseBgm() {
        if (bgmPlayer != null && bgmPlayer.isPlaying()) {
            bgmPlayer.pause();
            Log.d(TAG, "BGM paused");
        }
    }

    /**
     * 继续播放 BGM
     */
    public void resumeBgm() {
        if (bgmPlayer != null && !bgmPlayer.isPlaying() && isBgmPrepared) {
            bgmPlayer.start();
            Log.d(TAG, "BGM resumed");
        }
    }

    /**
     * 停止 BGM
     */
    public void stopBgm() {
        if (bgmPlayer != null) {
            if (bgmPlayer.isPlaying()) {
                bgmPlayer.stop();
            }
            bgmPlayer.release();
            bgmPlayer = null;
            isBgmPrepared = false;
            Log.d(TAG, "BGM stopped and released");
        }
    }

    // -------------------- 全局释放 --------------------

    /**
     * 释放所有资源(退出时调用)
     */
    public void releaseAll() {
        releaseSoundPool();
        stopBgm();
    }
}

播放音效

初始化音效池

java 复制代码
AudioManager.getInstance().initSoundPool(context);

加载音效

这里采用绝对路径

java 复制代码
AudioManager.getInstance().loadSound("/sdcard/sound/click.wav", 1);
AudioManager.getInstance().loadSound("/sdcard/sound/explosion.wav", 2);

播放音效

java 复制代码
AudioManager.getInstance().playSound(1);

播放BGM

播放BGM

java 复制代码
AudioManager.getInstance().playBgm("/sdcard/music/background.mp3", true);

暂停 / 继续播放

java 复制代码
AudioManager.getInstance().pauseBgm();
AudioManager.getInstance().resumeBgm();

停止BGM

java 复制代码
AudioManager.getInstance().stopBgm();

资源释放

java 复制代码
AudioManager.getInstance().releaseAll();

注意事项

项目 要点
BGM格式 mp3、aac、wav,推荐 mp3
音效格式 推荐 wav(低延迟),支持 mp3、ogg
路径 绝对路径(如 /sdcard/yourdir/xxx.mp3)
权限 动态申请 READ_EXTERNAL_STORAGE

动态权限申请:

java 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1001);
    }
}
相关推荐
雨白37 分钟前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹2 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空4 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭4 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日5 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安5 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑5 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟10 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡11 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0011 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体