【Audio开发四】音频audio中underrun和overrun原因详解和解决方案

一,underrun & overrun定义

我们知道,在Audio模块中数据采用的是生产者-消费者模式,生产者负责生产数据,消费者用于消费数据,针对AudioTrack和AudioRecord,其对应的角色不同;

AudioTrack
生产者:AudioTrack,向sharedBuffer中添加数据;
消费者:PlaybackThread(AudioFlinger),从sharedBuffer中读取数据;

AudioRecord
生产者:AudioFlinger(输入设备),向指定sharedBuffer中添加数据;
消费者:AudioRecord,从sharedBuffer中读取数据;

但是ALSA数据传输是不可控的,有可能生产者生产数据量不够,或者是消费者消费速率不快,这种情况下,就会出现音频数据不正确的情况;

欠载 ( UnderRun ) : 播放音频流时 , 如果当前现有数据已经播放完毕 , 新数据还没有来得及写入 , 此时会发生欠载情况,现象为断断续续,一卡一卡

在Playback中出现EPIPE是因为ALSA驱动buffer没有数据可以丢给codec所致;

超限 ( OverRun ) : 录制音频流时 , 如果没有及时读取音频流数据 , 并且这些数据没有妥善保存 , 发生溢出 , 导致数据丢失 , 这种情况叫做超限,现象为“爆破杂音

在Capture中出现EPIPE是因为ALSA驱动中有一块专门存储录音数据的buffer已满,没有及时的读取其中的数据将其写入文件中导致的overrun;

二,音频Xrun(underrun或overrun)问题分析

2.1 通常来说,出现 Xrun 问题时原因可能是以下几个之一:

(1) Linux CFS 调度器导致。因为 CFS 调度器的“公平调度”是较长一段时间的平均表现,在很短的一个窗口时间段内,CFS 也可能会将 CPU 时间片完全分配给一个 nice 值更高的线程而不顾及另一个 nice 值更低的线程。如果这个低 nice 值的线程恰好是音频相关的,就会导致 Xrun 问题。

(2) 更高优先级的 SCHED_FIFO 线程调度。除了音频线程以外,其它线程也可以使用 SCHED_FIFO 标记,如果这个其它线程的优先级高于音频线程,那么它会被优先调度。这样也会导致 Xrun 问题。

(3) 优先级反转。所谓的优先级反转是指一个更高优先级的线程需要使用另一个更低优先级线程所持有的资源,而不得不等待低优先级线程在使用完毕后将资源释放掉。如果音频线程恰好是这个高优先级线程,此时也将导致 Xrun 问题。

(4) 过长的调度延时。

(5) 顶半部中断处理程序执行时间过长。

(6) 禁用中断时间过长。

(7) 电源管理。内核会对芯片的工作电源进行适时地控制以免芯片温度过高而烧毁,这些管理策略可能会暂时挂起芯片中正在进行的工作。如果音频相关的任务因此被挂起,那么就会出现 Xrun 问题。

(8) 内核安全策略原因。

2.2 进一步分析:

alsa driver使用了环形缓冲区对dma buffer进行管理,如下图。
在这里插入图片描述

播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform的dma操作则不停地从该buffer中取出数据,经dai送往codec中,当写入的数据慢,播放的数据快时声音会出现断断续续,一卡一卡的现象。

录音时,codec源源不断地把A/D转换好的音频数据经过dai送入dma buffer中,而应用程序则不断地从该buffer中读走音频数据,当写入的数据快,播放的数据慢时,当数据量较多buffer有可能被冲掉,声音会出现类似“爆破”(Pop-Click)杂音的现象。

2.3 排查问题

看到这个问题的现象后,第一猜测就是设备出现了 UnderRun。

大胆假设后还需小心求证。于是我在代码中开启 Verbose Log打印并重新编译系统镜像烧写到设备上。然后复现问题,并查看出现问题的时间点附近的 Log 消息。日志文件中出现了大量如下记录:

03-07 18:44:04.290  2828  3521 V AudioFlinger: track(0xf5ea3a80) underrun,  framesReady(407) < framesDesired(516)
03-07 18:44:04.470  2828  3521 V AudioFlinger: track(0xf5ea3a80) underrun,  framesReady(31) < framesDesired(516)
03-07 18:44:04.570  2828  3521 V AudioFlinger: track(0xf5ea3a80) underrun,  framesReady(47) < framesDesired(516)
03-07 18:44:04.730  2828  3521 V AudioFlinger: track(0xf5ea3a80) underrun,  framesReady(79) < framesDesired(516)

果然这是个 UnderRun 问题,Log 中的信息证实了猜想:音频播放需要 1026 帧数据,但 APP 只准备好了 1024 帧。

2.4 代码流程上分析
我们知道,在AudioFlinger初始化的时候,就创建了PlaybackThread线程,但是thread创建成功之后,不是立马进行处理数据,这样对功耗和CPU是一种浪费。当应用上层创建AudioTrack并调用start之后,PlaybackThread会将对应的AudioTrack对应的Track实例填充到mActiveTracks集合中变更为active状态,然后发送广播,threadLoop就开始工作处理数据;

status_t AudioFlinger::PlaybackThread::Track::start(AudioSystem::sync_event_t event __unused,
                                                    audio_session_t triggerSession __unused)
{
    ……………………

    sp<ThreadBase> thread = mThread.promote();
    if (thread != 0) {
        ……………………

        ……………………
        PlaybackThread *playbackThread = (PlaybackThread *)thread.get();
        ……………………
        status = playbackThread->addTrack_l(this);
        ……………………
    return status;
}

在调用了Track实例的start函数之后,获取之前初始化中创建的PlaybackThread,其本质就是MixerThread,然后调用MixerThread的addTrack_l函数,将当前Track实例添加到Thread中进行数据处理流程;

// addTrack_l() must be called with ThreadBase::mLock held
status_t AudioFlinger::PlaybackThread::addTrack_l(const sp<Track>& track)
{
    status_t status = ALREADY_EXISTS;

    // 判断当前mActiveTracks集合中是否存在当前Track实例,< 0代表不存在
    if (mActiveTracks.indexOf(track) < 0) {
        ………………

        // set retry count for buffer fill
        if (track->isOffloaded()) {
            ………………
        } else {
            track->mRetryCount = kMaxTrackStartupRetries;
            track->mFillingUpStatus =
                    track->sharedBuffer() != 0 ? Track::FS_FILLED : Track::FS_FILLING;
        }

        track->mResetDone = false;
        track->mPresentationCompleteFrames = 0;
        mActiveTracks.add(track);
        ………………
    }

    ……………………
}

在addTrack_l函数中,首先先将track->mRetryCount值赋值为kMaxTrackStartupRetries,但是这个通过分析代码可知,没有真正使用到,因为在调用AudioTrack的start函数中之后MixerThread开始进行工作,而在MixerThread的prepareTracks_l函数中,又对track->mRetryCount进行了重新赋值;

// prepareTracks_l() must be called with ThreadBase::mLock held
AudioFlinger::PlaybackThread::mixer_state AudioFlinger::MixerThread::prepareTracks_l(
        Vector< sp<Track> > *tracksToRemove)
{
    ……………………

    mMixerBufferValid = false;  // mMixerBuffer has no valid data until appropriate tracks found.
    mEffectBufferValid = false; // mEffectBuffer has no valid data until tracks found.

    for (size_t i=0 ; i<count ; i++) {
        const sp<Track> t = mActiveTracks[i];

        // this const just means the local variable doesn't change
        Track* const track = t.get();

        // process fast tracks
        ……………………
        // 判断sharedBuffer可用frame帧数已经大于等于最小规定帧数,且track已准备就绪、track状态不是暂定或者是中断状态,说明当前共享内存中的数据可以进行准备前期的准备工作,创建对应的AudioMixer为后续混音做准备
        if ((framesReady >= minFrames) && track->isReady() &&
                !track->isPaused() && !track->isTerminated())
        {
            ……………………
            mAudioMixer->setParameter(
                name,
                AudioMixer::TRACK,
                AudioMixer::AUX_BUFFER, (void *)track->auxBuffer());

            // reset retry count
            // AudioMixer创建成功且配置完成之后,将track的重计数值重置为kMaxTrackRetries = 50,开始underrun计数统计
            track->mRetryCount = kMaxTrackRetries;

            // If one track is ready, set the mixer ready if:
            //  - the mixer was not ready during previous round OR
            //  - no other track is not ready
            if (mMixerStatusIgnoringFastTracks != MIXER_TRACKS_READY ||
                    mixerStatus != MIXER_TRACKS_ENABLED) {
                // 将MixerStatus设置为MIXER_TRACKS_READY状态,后续就可以执行threadLoop_mix函数进行混音操作
                mixerStatus = MIXER_TRACKS_READY;
            }
        } else {
            // 能够进入这个分支,说明当前的数据或者是Track状态不满足可以混音的条件
            ……………………

            ALOGVV("track %d s=%08x [NOT READY] on thread %p", name, cblk->mServer, this);
            if ((track->sharedBuffer() != 0) || track->isTerminated() ||
                    track->isStopped() || track->isPaused()) {
                // 这一块的逻辑,首先先分析track->sharedBuffer() != 0,这个代表的就是mode = MODE_STATIC,一次性数据传递
                ……………………
            } else {
                // 这个分支中,首先先可以保证,track->sharedBuffer() == 0,代表mode = MODE_STREAM,使用的是AudioFlinger创建的共享内存
                // No buffers for this track. Give it a few chances to
                // fill a buffer, then remove it from active list.
                // 针对track的mRetryCount进行计算,每空执行(代表没有进行数据混音操作)一次,就需要对mRetryCount进行 -- 操作,当 = 0时,则代表可用次数完结,代表空轮询了50次,超时
                if (--(track->mRetryCount) <= 0) {
                    ALOGI("BUFFER TIMEOUT: remove(%d) from active list on thread %p", name, this);
                    // 将当前对应的track添加到tracksToRemove集合中,用于后续从mActiveTracks集合中移除失效的track实例
                    tracksToRemove->add(track);
                    // indicate to client process that the track was disabled because of underrun;
                    // it will then automatically call start() when data is available
                    track->disable();
                // If one track is not ready, mark the mixer also not ready if:
                //  - the mixer was ready during previous round OR
                //  - no other track is ready
                } else if (mMixerStatusIgnoringFastTracks == MIXER_TRACKS_READY ||
                                mixerStatus != MIXER_TRACKS_READY) {
                    mixerStatus = MIXER_TRACKS_ENABLED;
                }
            }
            mAudioMixer->disable(name);
        }

        }   // local variable scope to avoid goto warning

    }

    ……………………
}

我们不考虑track状态的情况,目前只分析frame帧数影响的逻辑:

当framesReady >= minFrames时,代表当前的audio数据量满足混音时要求的最小数据量,则进行混音前准备,同时将mRetryCount重置为kMaxTrackRetries,每次进入该分支,都需要重置;

当不满足framesReady >= minFrames时,mRetryCount就需要递减,直到满足framesReady >= minFrames,mRetryCount才会重置,否则直到递减为0时,提示track超时,track失效;

framesReady:代表共享Buffer中的帧数;
minFrames:代表了AudioMixer支持的最少的混音帧数;
desiredFrames:代表了当前track配置下,HAL层正常播放支持的帧数,属于用户层操作;
mNormalFrameCount:当前系统HAL层正常播放支持的帧数,属于原生系统层配置;
srcSampleRate:采样器的采样率
dstSampleRate:播放时将音频数据单位时间内处理的数量也是有一个转换率,也可以理解为采样率,应该叫转换率
dstFramesRequired:播放时转换率能处理完的数据;

如果需要的采样率与硬件支持的采样率相同(源采样率 == 目标采样率),直接就返回硬件的dstFramesRequired,当源采样率 != 目标采样率时,通过size_t((uint64_t)dstFramesRequired * srcSampleRate / dstSampleRate + 1 + 1)这个公式来计算多少的数据才能满足目的帧数;

speed:倍速,在最终计算需要的帧数的时候,需要乘以speed,再 + 2 两个帧作为缓冲;

三,解决方案

方案一,从framework角度分析:
应该根据差值大小来调节延时时间来解决问题。只要我们检测到 framesReady 小于 framesDesired,我们就进行 1 毫秒延时,然后再获取延时后的 framesReady 值与 framesDesired 值进行比较,如果 framesReady 仍然小于 framesDesired,那么则继续延时 1 毫秒。如此循环,直到 framesReady 值大于等于 framesDesired 或者超时退出。

或者延长应用层下发的数据写入时长。

然后系统会调用 usleep() 函数对当前 PlaybackThread 进行短时间阻塞,这样上层 APP 就能为 PlaybackThread 准备好更多音频数据。这个 usleep() 的时长是根据相邻 2 次写入音频数据的时间间隔实时计算出的。

相应的代码可以在 frameworks/av/sevices/audioflinger/Threads.cpp 中的 AudioFlinger::PlaybackThread::threadLoop() 函数中找到:

bool AudioFlinger::PlaybackThread::threadLoop()
{
    ......
    if (mType == MIXER && !mStandby) {
    // write blocked detection
    nsecs_t now = systemTime();
    nsecs_t delta = now - mLastWriteTime;    // 相邻 2 次写入音频数据操作的时间间隔
    if (delta > maxPeriod) {
        mNumDelayedWrites++;
        if ((now - lastWarning) > kWarningThrottleNs) {    // 如果本次写入数据时间与上次警告出现时间间隔大于kWarningThrottleNs(5秒)则判断出现underrun
            ATRACE_NAME("underrun");
            ALOGW("write blocked for %llu msecs, %d delayed writes, thread %p",
                   ns2ms(delta), mNumDelayedWrites, this);
            lastWarning = now;
        }
    }
    
    if (mThreadThrottle
           && mMixerStatus == MIXER_TRACKS_READY // we are mixing (active tracks)
           && ret > 0) {                         // we wrote something
 
       // The throttle smooths out sudden large data drains from the device,
       // e.g. when it comes out of standby, which often causes problems with
       // (1) mixer threads without a fast mixer (which has its own warm-up)
       // (2) minimum buffer sized tracks (even if the track is full,
       //     the app won't fill fast enough to handle the sudden draw).
 
       const int32_t deltaMs = delta / 1000000;
       const int32_t throttleMs = mHalfBufferMs - deltaMs;
       if ((signed)mHalfBufferMs >= throttleMs && throttleMs > 0) {
           //usleep(throttleMs * 1000);    // 通过usleep()短时间阻塞当前PlaybackThread,让app可以准备更多的数据
           usleep((throttleMs + 3) * 1000);     /* 增加 3ms 的延时时间
                                                 * 修复腾讯视频APP播放视频有噪声的问题   20161216 
                                                 */
           // notify of throttle start on verbose log
           ALOGV_IF(mThreadThrottleEndMs == mThreadThrottleTimeMs,
                  "mixer(%p) throttle begin:"
                  " ret(%zd) deltaMs(%d) requires sleep %d ms",
                  this, ret, deltaMs, throttleMs);
           mThreadThrottleTimeMs += throttleMs;
       } else {
           uint32_t diff = mThreadThrottleTimeMs - mThreadThrottleEndMs;
           if (diff > 0) {
               // notify of throttle end on debug log
               ALOGD("mixer(%p) throttle end: throttle time(%u)", this, diff);
               mThreadThrottleEndMs = mThreadThrottleTimeMs;
           }
       }
    }
 }
 ......
}

方案二,从驱动层角度分析:
1、播放的数据快,写入的数据慢时,触发DMA中断,将音频数据写入dma buffer中,出现一卡一卡的现象!检查codec和machine的采样频率,设置ASoc框架的Codec驱动和Machine驱动的输出freq = 12288000。

//设置codec dai的主时钟,采样率

static int xvf3500_set_dai_sysclk(struct snd_soc_dai *codec_dai,int clk_id, unsigned int freq, int dir)

{

...

freq = 12288000;

switch (freq) {

    ...
    case 12288000:
    case 16934400:
    case 24576000:
    case 33868800:
    xvf3500->sysclk_constraints = &constraints_12288;
    xvf3500->sysclk = freq;
    return 0;
    ...
    }
    return -EINVAL;
    } 

//在machine设置codec dai采样率,保持codec驱动采样率一致

static int rk29_xvf3500_init(struct snd_soc_pcm_runtime *rtd)

{

...

 

ret = snd_soc_dai_set_sysclk(codec_dai, 0,12288000, SND_SOC_CLOCK_IN);

if (ret < 0) {

printk(KERN_ERR "Failed to set xvf3500 SYSCLK: %d\n", ret);

return ret;

}

...

return 0;

}

2、当写入的数据快,播放的数据慢时,当数据量较多buffer有可能被冲掉,声音会出现类似“爆破”或“呲呲”杂音的现象。通过调整硬件抽象层的period_size和period_count,来改变dma的传输数据量。

    struct pcm_config {
	    unsigned int channels;
	    unsigned int rate;
	    unsigned int period_size;
	    unsigned int period_count;
	    enum pcm_format format;
	    unsigned int start_threshold;
	    unsigned int stop_threshold;
	    unsigned int silence_threshold;
	    int avail_min;
    };

合理的pcm_config可以做到更好的低时延和功耗。解释一下结构中的各个参数,每个参数的单位都是frame(1帧 = 通道*采样位深):

period_size. 每次传输的数据长度。值越小,时延越小,cpu占用就越高。

period_count. 缓冲区period的个数。缓冲区越大,发生XRUN的机会就越少。

format. 定义数据格式,如采样位深,大小端。

start_threshold. 缓冲区的数据超过该值时,硬件开始启动数据传输。如果太大, 从开始播放到声音出来时延太长,甚至可导致太短促的声音根本播不出来;如果太小, 又可能容易导致XRUN.

stop_threshold. 缓冲区空闲区大于该值时,硬件停止传输。默认情况下,这个数 为整个缓冲区的大小,即整个缓冲区空了,就停止传输。但偶尔的原因导致缓冲区空, 如CPU忙,增大该值,继续播放缓冲区的历史数据,而不关闭再启动硬件传输(一般此 时有明显的声音卡顿),可以达到更好的体验。

silence_threshold. 这个值本来是配合stop_threshold使用,往缓冲区填充静音 数据,这样就不会重播历史数据了。但如果没有设定silence_size,

avail_min. 缓冲区空闲区大于该值时,pcm_mmap_write()才往缓冲写数据。这个 值越大,往缓冲区写入数据的次数就越少,面临XRUN的机会就越大。Android samsung tuna 设备在screen_off时增大该值以减小功耗,在screen_on时减小该 值以减小XRUN的机会。

在不同的场景下,合理的参数就是在性能、时延、功耗等之间达到较好的平衡。

//修改HAL层的period_size 和 period_count路径…hardware/rockchip/audio/tinyalsa_hal/audio_hw.h

//播放
struct pcm_config pcm_config = {

.channels = 2,

.rate = 48000,

.period_size = 2048,

.period_count = 2,

.format = PCM_FORMAT_S16_LE,

};

//录音

struct pcm_config pcm_config_in = {

.channels = 2,

.rate = 48000,

.period_size = 512,

.period_count = 2,

.format = PCM_FORMAT_S16_LE,

};

总结:

这种方法通过增加或减少音频数据的 period_count或period_size来进行补偿。但这样也会使音频播放/录音的数据准备时间变长,增加音频操作的延迟。

pcm播放的时候,接口snd_pcm_writei 返回 -EPIPE,为underrun

录制音频的时候, 接口snd_pcm_readi 返回 -EPIPE, 为overrun

使用ALSA架构的驱动程序,在实际开发使用过程中,比较常见的错误有-EPIPE,也就是-32?为什么会出现呢?肯定是系统内部不和谐了!EPIPE的错误在播放时出现就是因为驱动buffer没有数据可以丢给codec所致,通俗一点就是上层给下面喂数据的速度慢了,下面饿晕了,所以抱怨你上层慢啊,给你一个-EPIPE错误出来,自己去找原因。

在录音的时候,出现EPIPE也是有原因的,ALSA的驱动也有一块专门用来存储录音数据的buffer,上层从该buffer搬运数据再存储起来就能得到我们需要的录音文件。一旦驱动的buffer满了,就会出现EPIPE的错误,因为你上层读录音buffer数据的速度慢了,这就不能抱怨下层不给面子了。

通过分析出现原因后,我们得找找对策,说起来容易,做的可能因为系统的原因并不是想的那么容易。

在播放的时候,如果会出现这种-EPIPE的错误,请调整下发数据的数据,加快一点点!

录音的时候出现这种错误的时候,请读得更快一点!可以提高任务的优先级来处理,也可以把驱动buffer扩大一下,给系统更多一点的缓冲时间!

<think>嗯,用户现在问的是关于Android音频开发的内容。首先,我需要回想一下之前的对话历史,看看有没有相关的信息或者用户之前的需求。之前用户询问的是智能驾驶数据上传,现在转向Android音频开发,可能是同一个项目中的不同部分,或者用户有更广泛的技术兴趣。 接下来,我需要分析用户的需求。用户可能是一位开发者,正在学习Android开发,特别是音频相关的部分。也有可能是在开发一个需要音频功能的应用程序,比如音乐播放器、语音识别、实时通信应用等。因此,回答需要覆盖基础知识、核心模块、常见场景以及开发中的注意事项。 考虑到用户之前的问题是关于数据上传,可能需要考虑音频数据的处理与传输是否有关联,但当前问题更专注于Android音频开发本身。因此,回答应重点放在Android平台的音频框架、API使用、常见问题及解决方案上。 用户可能希望了解的内容包括:Android音频系统的架构、常用的API(如AudioTrack、MediaPlayer等)、音频流处理、延迟问题、兼容性处理、以及高级功能如回声消除或低延迟音频。此外,可能还需要提供实际开发中的技巧最佳实践。 需要确保回答结构清晰,分步骤解析,帮助用户逐步理解。同时,按照之前的系统指令,需要使用中文,数学公式用$$...$$格式,行内公式用$...$,并且符合LaTeX语法。 在整理信息时,要确保技术细节准确,例如AudioManager的不同音频流类型,AudioTrackMediaPlayer的区别,以及ExoPlayer的优势。同时,注意提到Android不同版本的变化,比如Android 10引入的动态延迟调整,Android 12的低延迟模式,这些对开发者来说都是重要的知识点。 另外,用户可能需要了解如何处理常见的兼容性问题,比如不同设备的音频参数不一致,如何选择合适的采样率缓冲区大小。还有性能优化方面,比如避免主线程阻塞,使用JNI进行本地音频处理等。 安全与权限也是不可忽视的部分,需要提到Android的权限机制,如RECORD_AUDIO权限的必要性,以及用户隐私保护的措施。 最后,案例部分可以举一些实际应用场景,如直播应用的音频处理,或者语音助手的关键技术点,帮助用户更好地将理论应用到实践中。 总结来说,回答需要覆盖Android音频开发的基础知识、核心模块、常见场景、开发技巧、安全权限以及未来趋势,结构清晰,内容详实,满足不同层次开发者的需求。</think>### Android音频开发分步解析 --- #### 一、Android音频系统架构 1. **层次结构** Android音频系统基于**分层设计**,从应用层到硬件层分为: - **应用层**:通过Java/Kotlin API调用(如`MediaPlayer`、`AudioTrack`)。 - **框架层**:`AudioManager`、`AudioService`管理音频策略路由。 - **本地层(Native)**:`AAudio`(低延迟)、`OpenSL ES`(高性能)。 - **硬件抽象层(HAL)**:`audio.primary`驱动适配不同硬件。 2. **关键组件** - **AudioFlinger**:负责混音音频流分发(如混合多个应用的音频流)。 - **AudioPolicyManager**:决定音频输出路径(如耳机切换、蓝牙优先级)。 --- #### 二、核心开发模块 1. **音频播放** - **MediaPlayer**:封装高层API,支持本地/网络音频(MP3、AAC等): ```java MediaPlayer player = MediaPlayer.create(context, R.raw.sample); player.start(); // 播放 ``` - **AudioTrack**:低层级控制,适合实时音频流(PCM数据): ```java AudioTrack track = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM ); track.write(pcmData, 0, pcmData.length); // 写入PCM数据 ``` 2. **音频录制** - **MediaRecorder**:快速录制压缩格式(AMR、AAC): ```java MediaRecorder recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setOutputFile("record.mp4"); recorder.prepare(); recorder.start(); ``` - **AudioRecord**:获取原始PCM数据(用于语音识别或自定义处理): ```java AudioRecord record = new AudioRecord( MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize ); byte[] buffer = new byte[bufferSize]; record.read(buffer, 0, bufferSize); // 读取原始音频数据 ``` 3. **音频特效** - **音效API**:`Equalizer`、`BassBoost`(需`AudioSessionId`绑定): ```java Equalizer eq = new Equalizer(0, audioSessionId); eq.setEnabled(true); eq.setBandLevel((short)0, (short)1000); // 调节频段 ``` --- #### 三、关键开发场景与解决方案 1. **低延迟音频(游戏/实时通信)** - **使用AAudio**(Android 8.0+): - 延迟可降至$<10\text{ms}$(传统AudioTrack约为$50\text{ms}$)。 - 通过`AAudioStreamBuilder`设置性能模式:`AAUDIO_PERFORMANCE_MODE_LOW_LATENCY`。 - **缓冲区优化**:根据设备能力动态调整(公式): $$ \text{缓冲区大小 } B = \frac{\text{采样率 } f_s \times \text{延迟要求 } L}{1000} $$ 例如:$f_s=48\text{kHz}, L=5\text{ms} \Rightarrow B=240$帧。 2. **多音频流混合** - **焦点管理**:通过`AudioManager.requestAudioFocus()`处理通话与音乐冲突。 - **音量独立控制**:对不同音频流(如`STREAM_MUSIC`、`STREAM_ALARM`)单独调节。 3. **音频处理链** - **自定义效果**:通过`AudioEffect`或NDK实现(如回声消除、降噪): ```cpp // NDK中使用OpenSL ES SLresult result = (*player)->Enqueue(player, buffer, bufferSize); ``` - **可视化分析**:对PCM数据做FFT(快速傅里叶变换)生成频谱: $$ X(k) = \sum_{n=0}^{N-1} x(n)e^{-j2\pi kn/N} \quad (0 \leq k \leq N-1) $$ --- #### 、常见问题与调试技巧 1. **延迟问题** - **测试工具**:使用`adb shell dumpsys audio`查看音频流状态。 - **设备兼容性**:不同厂商HAL实现差异(如某些设备强制重采样到48kHz)。 2. **杂音/破音** - **缓冲区下溢(Underrun)**:增大缓冲区或优化数据写入线程优先级。 - **采样率不匹配**:确保输入输出采样率一致(如`AudioTrack`与音频文件参数对齐)。 3. **权限与隐私** - **动态权限申请**:Android 6.0+需运行时请求`RECORD_AUDIO`权限。 - **隐私合规**:录音前需明确提示用户(如弹出对话框说明用途)。 --- #### 五、高级特性与未来趋势 1. **Android 12+新特性** - **低延迟模式**:支持`AudioTrack.setLowLatency(true)`。 - **空间音频**:基于陀螺仪数据的3D音效(`Spatializer`类)。 2. **机器学习集成** - **TensorFlow Lite音频分类**:实时识别环境声音(如婴儿哭声、玻璃破碎)。 - **端侧语音增强**:使用TFLite模型降噪(输入PCM,输出滤波后数据)。 3. **无线音频协议** - **蓝牙LE Audio**:支持多设备同步(如多个耳机共享音频流)。 - **Android Auto**:车载场景下的音频路由与降噪优化。 --- #### 六、典型应用案例 1. **直播应用音频处理** - **回声消除(AEC)**:通过`AcousticEchoCanceler`抑制扬声器回传。 - **音频编码**:使用`MediaCodec`将PCM实时压缩为AAC(码率控制公式): $$ \text{码率 } R = \text{采样率 } \times \text{位深 } \times \text{通道数 } \times \text{压缩比} $$ 2. **语音助手实现** - **关键词唤醒**:通过`VoiceInteractionService`监听特定语音指令。 - **音频焦点抢占**:在用户说话时暂停其他音频播放。 --- ### 总结 Android音频开发需平衡**性能**、**延迟**与**兼容性**,核心在于: 1. 根据场景选择API(高层`MediaPlayer` vs 底层`AAudio`)。 2. 优化音频线程优先级与缓冲区策略。 3. 遵循隐私与权限规范。 未来随着AI与无线技术的演进,音频处理将更智能化(如环境感知降噪)与无缝化(多设备协同)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孤舟簔笠翁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值