Android 平台的开机动画实现位于 frameworks/base/cmds/bootanimation
,其动画效果定制自由度较高,整体分为三个部分:enter
(入场)、loop
(循环)、exit
(退场)。
播放逻辑如下:
- 先播放
enter
部分; - 循环播放
loop
部分,直到 Home 界面显示完毕后,WMS 通过SystemProperties.set("service.bootanim.exit", "1")
通知开机动画,此时会播放完当前loop
的剩余部分; - 最后播放
exit
部分。
由于系统开机时间不固定,loop
部分会循环多次以保证三部分动画的完美衔接。但当前开机动画对音频播放的支持存在明显缺陷,其音频播放逻辑位于 frameworks/base/cmds/bootanimation/audioplay.cpp
,核心流程包括:
- init:检测是否存在音频文件,若有则初始化音频播放器;
- play:播放音频;
- shutdown:开机动画退出时,停止音频播放。
现有问题分析
- 音频与动画匹配度低:动画分为三部分,但音频只能关联到第一部分且会一直播放至结束。由于开机时间不确定而音频时长固定,导致两者无法完全同步;
- 音频停止突兀:开机动画结束时,音频会被立即停止,体验生硬。
优化方案:音频渐弱淡出
针对上述问题,优化思路为:使用足够长的音频文件,在开机动画结束后,使音频播放音量逐渐减小至 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,避免了音频的突兀停止,提升了开机体验。