Android 开机动画音频播放优化方案

Android 平台的开机动画实现位于 frameworks/base/cmds/bootanimation,其动画效果定制自由度较高,整体分为三个部分:enter(入场)、loop(循环)、exit(退场)。

播放逻辑如下:

  1. 先播放 enter 部分;
  2. 循环播放 loop 部分,直到 Home 界面显示完毕后,WMS 通过 SystemProperties.set("service.bootanim.exit", "1") 通知开机动画,此时会播放完当前 loop 的剩余部分;
  3. 最后播放 exit 部分。

由于系统开机时间不固定,loop 部分会循环多次以保证三部分动画的完美衔接。但当前开机动画对音频播放的支持存在明显缺陷,其音频播放逻辑位于 frameworks/base/cmds/bootanimation/audioplay.cpp,核心流程包括:

  • init:检测是否存在音频文件,若有则初始化音频播放器;
  • play:播放音频;
  • shutdown:开机动画退出时,停止音频播放。

现有问题分析

  1. 音频与动画匹配度低:动画分为三部分,但音频只能关联到第一部分且会一直播放至结束。由于开机时间不确定而音频时长固定,导致两者无法完全同步;
  2. 音频停止突兀:开机动画结束时,音频会被立即停止,体验生硬。

优化方案:音频渐弱淡出

针对上述问题,优化思路为:使用足够长的音频文件,在开机动画结束后,使音频播放音量逐渐减小至 0。

实现步骤

1. 计算音频文件播放时长

通过解析音频文件信息,计算其总播放时长,代码实现如下:

rust 复制代码
bool parseClipBuf(const uint8_t* clipBuf, int clipBufSize, const ChunkFormat** oChunkFormat,
                  const uint8_t** oSoundBuf, unsigned* oSoundBufSize) {
    '''''
        ALOGD("num_channels: %d, sample_rate: %d, bits_pre_sample: %d",
         (*oChunkFormat)->num_channels,
         (*oChunkFormat)->sample_rate,
         (*oChunkFormat)->bits_per_sample);
         audioLengthSeconds = (*oSoundBufSize) /
         ((*oChunkFormat)->num_channels * (*oChunkFormat)->sample_rate * (*oChunkFormat)->bits_per_sample / 8);
    ALOGD("audio length seconds %d", audioLengthSeconds);
    return true;
}

2. 实现音量渐弱线程(VolumeFadeOutThread)

新增线程类负责音量的逐步减小,直至音量为 0。该线程包含两个参数:timeout(淡出总时长)和 interval(音量调整间隔),并通过条件变量实现线程同步。播放结束后,通过 audioplay::setPlaying(false) 停止播放。

cpp 复制代码
static std::mutex g_playing_mutex;
static std::condition_variable g_playing_cv;
static bool g_playing = false;

class VolumeFadeOutThread : public Thread {
public:
    VolumeFadeOutThread(int timeout, int interval)
        : Thread(false),
          mTimeout(timeout),
          mInterval(interval) {}
private:
    virtual bool threadLoop() {
        ALOGD("Volume fade out timeout: %d, step %d", mTimeout, mInterval);
        SLresult result;
        int count = mTimeout / mInterval;
        SLmillibel volume = 0; // 0 is the max volume
        SLmillibel volume_step = 3000 / count; // -3000 is the min volume
        while (bqPlayerVolume!= nullptr && count > 0) {
            result = (*bqPlayerVolume)->SetVolumeLevel(bqPlayerVolume, volume);
            if (result != SL_RESULT_SUCCESS) {
                ALOGE("set volume failed with result %d", result);
                return false;
            }
            //ALOGD("set volume %d", volume);
            volume = volume - volume_step;
            count = count - 1;
            usleep(mInterval*1000);
        }
        ALOGD("Volume fade out end.");
        audioplay::setPlaying(false);
        return false;
    }

    int mTimeout;
    int mInterval;
};

3. 修改shutdown逻辑

在开机动画退出逻辑中,根据剩余时长或固定时长启动 VolumeFadeOutThread,并通过 g_playing_cv 等待音频淡出完成。

cpp 复制代码
    void shutdown() override {
        // we've finally played everything we're going to play
        // audioplay::setPlaying(false);
        ALOGD("shutdown");

        std::unique_lock<std::mutex> lock(g_playing_mutex);
        if (g_playing) {
            volumeFadeOutThread = new VolumeFadeOutThread(VOLUME_TIMEOUT, VOLUME_STEP_INTERVAL);
            volumeFadeOutThread->run("BootAnimation::VolumeFadeOutThread", PRIORITY_NORMAL);

            g_playing_cv.wait(lock);
            ALOGD("shutdown waiting");
        }
        ALOGD("shutdown destory");
        audioplay::destroy();
    };

4. 播放状态同步与通知

在播放状态变更时,通过 g_playing_cv.notify_one() 通知阻塞的 shutdown 线程继续执行退出逻辑。

cpp 复制代码
// set the playing state for the buffer queue audio player
void setPlaying(bool isPlaying) {
    if (!hasPlayer()) return;

    SLresult result;

    if (nullptr != bqPlayerPlay) {
        // set the player's state
        result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay,
            isPlaying ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_STOPPED);
        std::unique_lock<std::mutex> lock(g_playing_mutex);
        g_playing = isPlaying;
        g_playing_cv.notify_one();
    }
}

最终效果

开机动画退出、Home 界面显示后,开机音效会继续播放 VOLUME_TIMEOUT 时长,且音量逐渐减小至 0,避免了音频的突兀停止,提升了开机体验。

相关推荐
冰暮流星5 小时前
mysql之新建表及对表的查询
android·数据库·mysql
用户2018792831676 小时前
彻底搞懂LayoutInflater、ViewInflater与textViewStyle优先级:XML属性为何能覆盖全局样式?
android
达达尼昂6 小时前
AI Native 工程实践:如何为 Claude 5 设计更有效的上下文
android·人工智能·后端
Cry丶6 小时前
【已解决】adb devices 能识别手机,但 HBuilderX 搜索不到设备的避坑指南
android·adb·uni-app·hbuilderx·真机调试
墨神谕7 小时前
认识一下Unicode(统一码)
android·java·数据库
Sirens.18 小时前
MySQL表设计进阶-约束范式连接索引与事务
android·数据库·mysql
zh_xuan21 小时前
安卓上 mmap读取并修改文件内容
android·mmap
二流小码农1 天前
鸿蒙开发:实现文本渐变效果
android·ios·harmonyos
光头闪亮亮1 天前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(中)
android·c++·go
光头闪亮亮1 天前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)
android·sqlite·go