海思mpp音频编码

音频编码

音频编码

海思MPP AUDIO模块包括音频输入、音频输出、音频编码、音频解码四个子模块。音频输入(AI)和输出模块(AO)通过对芯片音频接口的控制实现音频输入输出功能。音频编码和解码模块提供对G711、G726、ADPCM等格式的音频编解码功能,并支持录制和播放LPCM格式的原始音频文件。

详细音频模块介绍详见《海思MPP 媒体处理软件 V6.0 开发参考》,这里仅列举简单常用点

源码链接:https://gitee.com/kidwjb/codec_video_audio/tree/master/%E6%B5%B7%E6%80%9D/src

音频接口和AI、AO设备

音频输入输出接口简称为AIO(Audio Input/Output)接口,用于和Audio Codec对接,完成声音的录制和播放。

软件中负责抽象音频接口输入功能的单元,称之为AI设备;负责抽象音频接口输出功能的单元,称之为AO设备。

对每个输入输出接口,软件根据该接口支持的功能,分别与AI设备和AO设备建立映射 关系。例如:AIP0只支持音频输入,则AIP0映射为AiDev0;AOP0只支持音频输出, 则AOP0映射为AoDev0

音频接口时序

海思MPP音频接口支持标准的I2S接口时序模式和PCM接口时序模式,并提供灵活的配置以支持与多种Audio Codec对接。

  • AI设备支持扩展的多路接收的I2S及PCM接口时序。对接时,Codec的时序模式选择、同步时钟、采样位宽等配置必须与AI设备的配置保持一致,否则可能采集不到正确的数据。
  • AI/AO设备支持主模式和从模式,主模式即AI/AO设备提供时钟,从模式即Audio Codec提供时钟(注意这里的Audio Codec就是指的外部的音频编码芯片)。主模式时,如果AI设备与AO设备同时对接同一个Codec,则时钟供输入和输出共同使用,其他情况没有此限制。而从模式时的输入输出时钟可以分别由外围Audio Codec提供。
  • 由于时序的问题,在AI/AO设备从模式下,建议用户先配置好对接的Codec,再配 置AI或AO设备;而在AI/AO设备主模式下,建议用户先配置好AI或AO设备,再配 置对接的Codec。笔者这里选择的是内部编码,所以使用第二条方案

音频输入输出支持重采样,声音质量增强(VQE),不过这里都未用到,在代码中需要设置去初始化

音频接口与设备的对应关系

芯片集成的AIO内部分两类:只支持音频输入的AIP和只支持音频输出的AOP,相应的AIO规格详见对应芯片的用户指南。

AIO接口中只支持音频输出的AOP0可以配置是否共用帧同步时钟和位流时钟给 AIP0使用。共用时,AI设备0和AO设备0的帧同步时钟与位流时钟必须相同,即工 作模式必须一致,采样频率也必须一致,并且位流时钟按二者中的较高频率进行 配置。

音频输入输出设备号:

屏幕截图 20260312 135640png

音频的输入输出设备的通道数范围:

屏幕截图 20260312 135652png

内置 Audio Codec

Hi3519DV500/Hi3516CV610提供一个内置的Audio Codec(其中,Hi3519DV500/ Hi3516CV610采用V751),用于对接AIP0/AOP0实现声音的播放和录制。AIP0/AOP0 接口可以选择对接内置的Audio Codec或外接的Audio Codec,进行声音的播放及录 制。因为内置Audio Codec不能发送同步时钟,所以AIP0/AOP0接口对接内置Codec时 只能配置为I2S时序的主模式,用户仍需要正确配置AIP0/AOP0和内置Audio Codec对 接的时序才可接收或发送音频数据。

ioctl函数

内置Audio Codec的用户态接口以ioctl形式体现,其形式如下:

int ioctl (int fd,
    unsigned long cmd,
……
);

但在Audio Codec中,实际只需要3个参数。因此,其语法形式等同于。但在Audio Codec中,实际只需要3个参数。因此,其语法形式等同于

int ioctl (int fd,
    unsigned long cmd,
    CMD_DATA_TYPE *cmddata);

其中,CMD_DATA_TYPE随参数cmd的变化而变化

具体cmd类型详见手册,这里只列举几个常用的:

屏幕截图 20260312 140807png

代码编写

音频设备初始化

首先进行音频设备初始化,调用的海思封装了一层的接口,底层调用的hi_mpi_audio_init(前缀hi是进行了又一次封装,手册里是ss,有些地方是ot)

  • 应用程序启动Audio业务前,必须完成Audio模块初始化工作。同理,应用程序退出Audio业务后,也要完成Audio模块去初始化工作,释放资源。
  • 如果多次初始化,仍会返回成功,但实际上系统不会对Audio的运行状态有任何影 响。
  • 由于音频模块依赖用户态属性,故音频不支持多进程操作。对于Hi3519DV500/ Hi3516CV610,用户需要保证音频的相关操作和ss_mpi_audio_init在同一个进程 中。
/* audio system init */
hi_s32 sample_comm_audio_init(hi_void)
{
    hi_s32 ret;

    hi_mpi_audio_exit();

    ret = hi_mpi_audio_init();
    if (ret != HI_SUCCESS) {
        sample_print("hi_mpi_audio_init failed!\n");
        return HI_FAILURE;
    }

    return HI_SUCCESS;
}

初始化aac函数

笔者最终会把音频流编码为aac文件,根据查看例程发现会调用这个函数,但是手册没有说,也看不到这个函数定义,但是保险起见还是加上

td_s32 hi_mpi_aenc_aac_init(td_void);

初始化aio_attr相关配置

AIO设备依赖于hi_aio_attr设置参数,所以需要对其进行初始化赋值,这里我借鉴海思例程函数,修改海思的初始化函数内部参数即可

注意:后两个参数是ai,ao设备号

static hi_void sample_audio_ai_aenc_init_param(hi_aio_attr *aio_attr, hi_audio_dev *ai_dev, hi_audio_dev *ao_dev)
{
    aio_attr->sample_rate  = HI_AUDIO_SAMPLE_RATE_48000;
    aio_attr->bit_width    = HI_AUDIO_BIT_WIDTH_16;
    aio_attr->work_mode    = HI_AIO_MODE_I2S_MASTER;
    aio_attr->snd_mode     = HI_AUDIO_SOUND_MODE_STEREO;
    aio_attr->expand_flag  = 0;
    aio_attr->frame_num    = SAMPLE_AUDIO_AI_USER_FRAME_DEPTH;
    aio_attr->point_num_per_frame = OT_AACLC_SAMPLES_PER_FRAME;
    aio_attr->chn_cnt      = 2; /* 2:chn num */
#ifdef OT_ACODEC_TYPE_INNER
    *ai_dev = SAMPLE_AUDIO_INNER_AI_DEV;
    *ao_dev = SAMPLE_AUDIO_INNER_AO_DEV;
    aio_attr->clk_share  = 1;
    aio_attr->i2s_type   = HI_AIO_I2STYPE_INNERCODEC;
#else
    *ai_dev = SAMPLE_AUDIO_EXTERN_AI_DEV;
    *ao_dev = SAMPLE_AUDIO_EXTERN_AO_DEV;
    aio_attr->clk_share  = 1;
    aio_attr->i2s_type   = HI_AIO_I2STYPE_EXTERN;
#endif

}

可以看到这里我的配置是48k采样率,16bit位宽,是使用I2S主模式,双声道,aio_attr中的frame_num项用于配置AI中用于接收音频数据的缓存的音频帧帧数,建议配置为5以上,否则可能出现采集丢帧等异常。每帧的采样点数为OT_AACLC_SAMPLES_PER_FRAME定义为1024

在这里需要注意一点:笔者使用的是内部音频编解码,所以要定义OT_ACODEC_TYPE_INNER宏,否则很多关于内部配置代码会被注释

初始化AI

在配置完属性变量之后就可以开始初始化AI,调用下面海思封装的函数,这里ai_chn_cn要等于上面aio_attr->chn_cnt,并且如果不需要vqe,需要设置vqe参数无效:

//不使用vqe
    ai_vqe_param.out_sample_rate = HI_AUDIO_SAMPLE_RATE_BUTT;
    ai_vqe_param.resample_en = HI_FALSE;
    ai_vqe_param.ai_vqe_attr = HI_NULL;
    ai_vqe_param.ai_vqe_type = SAMPLE_AUDIO_VQE_TYPE_NONE;
/* start ai */
hi_s32 sample_comm_audio_start_ai(hi_audio_dev ai_dev_id, hi_u32 ai_chn_cnt, hi_aio_attr *aio_attr,
    const sample_comm_ai_vqe_param *ai_vqe_param, hi_audio_dev ao_dev_id)
{
    hi_s32 i;
    hi_s32 ret;
    hi_u32 chn_cnt;

    ret = hi_mpi_ai_set_pub_attr(ai_dev_id, aio_attr);
    if (ret) {
        printf("%s: hi_mpi_ai_set_pub_attr(%d) failed with %#x\n", __FUNCTION__, ai_dev_id, ret);
        return ret;
    }

    ret = hi_mpi_ai_enable(ai_dev_id);
    if (ret) {
        printf("%s: hi_mpi_ai_enable(%d) failed with %#x\n", __FUNCTION__, ai_dev_id, ret);
        return ret;
    }

    chn_cnt = ai_chn_cnt >> ((hi_u32)aio_attr->snd_mode);
    for (i = 0; i < (hi_s32)chn_cnt; i++) {
        ret = hi_mpi_ai_enable_chn(ai_dev_id, i);
        if (ret) {
            printf("%s: hi_mpi_ai_enable_chn(%d,%d) failed with %#x\n", __FUNCTION__, ai_dev_id, i, ret);
            return ret;
        }

        if (ai_vqe_param->resample_en == HI_TRUE) {
            ret = hi_mpi_ai_enable_resample(ai_dev_id, i, ai_vqe_param->out_sample_rate);
            if (ret) {
                printf("%s: hi_mpi_ai_enable_re_smp(%d,%d) failed with %#x\n", __FUNCTION__, ai_dev_id, i, ret);
                return ret;
            }
        }

        ret = sample_comm_audio_start_ai_vqe(ai_dev_id, i, ai_vqe_param, ao_dev_id);
        if (ret != HI_SUCCESS) {
            return ret;
        }
    }

    return HI_SUCCESS;
}

配置编码参数

完成AI初始化之后就可以开始进行AENC的初始化,首先需要配置编码参数

/* config codec */
hi_s32 sample_comm_audio_cfg_acodec(const hi_aio_attr *aio_attr)
{
    hi_bool codec_cfg = HI_FALSE;

#ifdef OT_ACODEC_TYPE_ES8388
    hi_s32 ret = sample_es8388_cfg_audio(aio_attr->work_mode, aio_attr->sample_rate, aio_attr->bit_width);
    if (ret != HI_SUCCESS) {
        printf("%s: sample_es8388_cfg_audio failed\n", __FUNCTION__);
        return ret;
    }

    codec_cfg = HI_TRUE;
#endif

#ifdef OT_ACODEC_TYPE_INNER
    /* INNER AUDIO CODEC */
    hi_s32 ret = sample_inner_codec_cfg_audio(aio_attr->sample_rate);
    if (ret != HI_SUCCESS) {
        printf("%s:sample_inner_codec_cfg_audio failed\n", __FUNCTION__);
        return ret;
    }
    codec_cfg = HI_TRUE;
#endif

    if (codec_cfg == HI_FALSE) {
        printf("can not find the right codec.\n");
        return HI_FAILURE;
    }

    return HI_SUCCESS;
}

使能AENC

在使能AENC之前,需要先设置AENC通道计数

aenc_chn_cnt = aio_attr.chn_cnt >> ((hi_u32)aio_attr.snd_mode);

这是海思的设计:STEREO 模式下,左右声道合并为一个 aenc 通道处理,chn_cnt >> snd_mode 这个右移运算正是为此设计的(MONO=0不移位,STEREO=1右移1位÷2)。

然后调用AENC初始化函数

/* start aenc */
hi_s32 sample_comm_audio_start_aenc(hi_u32 aenc_chn_cnt, const hi_aio_attr *aio_attr, hi_payload_type type)
{
    hi_aenc_chn ae_chn;
    hi_s32 ret, i, j;
    hi_aenc_chn_attr aenc_attr;
    hi_aenc_attr_adpcm adpcm_aenc;
    hi_aenc_attr_g711 aenc_g711;
    hi_aenc_attr_g726 aenc_g726;
    hi_aenc_attr_lpcm aenc_lpcm;
    hi_aenc_attr_aac aenc_aac;
    hi_aenc_attr_mp3 aenc_mp3;
    hi_aenc_attr_opus aenc_opus;

    /* set AENC chn attr */
    aenc_attr.type = type;
    aenc_attr.buf_size = 30; /* 30:size */
    aenc_attr.point_num_per_frame = aio_attr->point_num_per_frame;

    if (aenc_attr.type == HI_PT_ADPCMA) {
        aenc_attr.value = &adpcm_aenc;
        adpcm_aenc.adpcm_type = AUDIO_ADPCM_TYPE;
    } else if ((aenc_attr.type == HI_PT_G711A) || (aenc_attr.type == HI_PT_G711U)) {
        aenc_attr.value = &aenc_g711;
    } else if (aenc_attr.type == HI_PT_G726) {
        aenc_attr.value = &aenc_g726;
        aenc_g726.g726bps = G726_BPS;
    } else if (aenc_attr.type == HI_PT_LPCM) {
        aenc_attr.value = &aenc_lpcm;
    } else if (aenc_attr.type == HI_PT_AAC) {
        aenc_attr.value = &aenc_aac;
        aenc_attr_aac_get_param(aio_attr, &aenc_aac);
    } else if (aenc_attr.type == HI_PT_MP3) {
        aenc_attr.value = &aenc_mp3;
        aenc_attr_mp3_get_param(aio_attr, &aenc_mp3);
    } else if (aenc_attr.type == HI_PT_OPUS) {
        aenc_attr.value = &aenc_opus;
        aenc_attr_opus_get_param(aio_attr, &aenc_opus);
    } else {
        printf("%s: invalid aenc payload type:%d\n", __FUNCTION__, aenc_attr.type);
        return HI_FAILURE;
    }

    for (i = 0; i < (hi_s32)aenc_chn_cnt; i++) {
        ae_chn = i;
        /* create aenc chn */
        ret = hi_mpi_aenc_create_chn(ae_chn, &aenc_attr);
        if (ret != HI_SUCCESS) {
            printf("%s: hi_mpi_aenc_create_chn(%d) failed with %#x!\n", __FUNCTION__, ae_chn, ret);
            for (j = 0; j < (hi_s32)ae_chn; j++) {
                (hi_void)hi_mpi_aenc_destroy_chn((hi_aenc_chn)j);
            }
            return ret;
        }
    }

    return HI_SUCCESS;
}

绑定AI和AENC

这里可以选择绑定AI和AENC通道,这样AI数据就会硬件自动处理到达AENC,当然也可以选择手动获取AI数据然后发送给AENC,这里我选择第一种

static hi_s32 sample_audio_aenc_bind_ai(hi_audio_dev ai_dev, hi_u32 aenc_chn_cnt)
{
    hi_s32 ret;
    hi_u32 i, j;
    hi_ai_chn ai_chn;
    hi_aenc_chn ae_chn;

    for (i = 0; i < aenc_chn_cnt; i++) {
        ae_chn = i;
        ai_chn = i;
        ret = sample_comm_audio_aenc_bind_ai(ai_dev, ai_chn, ae_chn);

        if (ret != HI_SUCCESS) {
            sample_print("sample_comm_audio_aenc_bind_ai failed,ret:%d\r\n",ret);
            goto aenc_bind_err;
        }
        printf("ai(%d,%d) bind to aenc_chn:%d ok!\n", ai_dev, ai_chn, ae_chn);
    }
    return HI_SUCCESS;

aenc_bind_err:
    for (j = 0; j < i; j++) {
        ae_chn = j;
        ai_chn = j;

        sample_comm_audio_aenc_unbind_ai(ai_dev, ai_chn, ae_chn);
    }
    return HI_FAILURE;
}

综上音频流的初始化就完成了

接下来就是获取数据

查看当前状态

为了代码健壮性可以查看当前状态,使用ioctl接口,参数是HI_ACODEC_QUERY_STATUS

fd_acodec = open(ACODEC_FILE, O_RDWR);
    if (fd_acodec < 0) {
        printf("%s: can't open audio codec,%s\n", __FUNCTION__, ACODEC_FILE);
        goto EXIT_AI_AENC;
    }

    //检查状态,不要也可以,但是为了健壮性还是加上,也懒得删了
    td_u32 status;
    ret = ioctl(fd_acodec, HI_ACODEC_QUERY_STATUS, &status);
    if(ret != HI_SUCCESS)
    {
        sample_print("HI_ACODEC_QUERY_STATUS failed\r\n");
        goto EXIT_AI_AENC;
    }
    printf("acodec status is %d\r\n",status);//为1则表示acodec初始化成功
  • 加载acodec驱动后,需要一段时间进行初始化,Normal模式下为100ms,Fast模式下为10ms,Nice模式下为300ms。
  • 该接口的作用是查询ACODEC的当前状态。
  • 在未完成初始化时,其获取到的状态是0,即未使能;在已完成初始化时,其获取 到的状态是1,即已使能。

设置输入音量

在实际使用时发现编码后的音频音量较小,于是笔者在获取音频流前先去调节音量,可以调节输入和输出音量,这里我只选择调节输入

调用:HI_ACODEC_SET_INPUT_VOLUME

输入总音量控制。此接口依据优先调整模拟增益的原则,把用户期望设置的总增益分解成模拟增益和数字增益,分别配置到模拟增益控制寄存器和数字增益控制寄存器。

  • Hi3519DV500/Hi3516CV610输入音量范围为[-78, 80],包括模拟增益和数字增益,赋值越大,音量越大。赋值为80时,音量最大,为80dB,赋值为-78时,音量最小,为静音。调节时左右声道一起生效。建议调节范围限制为[0, 50]中的偶数值,此范围只调节模拟增益,这样引入的噪声最小,能够更好的保证声音质量
  • 当使用了RecordVQE的HDR功能,且HDR功能配置为自动模式时,HDR算法会根 据输入音量的大小动态修改acodec的模拟增益。
//设置输入音量,默认音量只有6,声音不大
    ret = ioctl(fd_acodec, HI_ACODEC_SET_INPUT_VOLUME, &set_volume);
    if(ret != HI_SUCCESS)
    {
        sample_print("HI_ACODEC_SET_INPUT_VOLUME failed\r\n");
        goto EXIT_AI_AENC;
    }

    //查看设置输入音量成功没有
    hi_s32 get_volume;
    ret = ioctl(fd_acodec, HI_ACODEC_GET_INPUT_VOLUME, &get_volume);
    if(ret != HI_SUCCESS)
    {
        sample_print("HI_ACODEC_GET_INPUT_VOLUME failed\r\n");
        goto EXIT_AI_AENC;
    }
    printf("volume is %d\r\n",get_volume);

    if(status != 1 || get_volume != set_volume)
    {
        goto EXIT_AI_AENC;
    }

启动音频流获取线程

在做完上述配置后就可以获取音频流了,这里我单独创建一个线程去获取

static hi_s32 sample_aenc_normal_start_encode(hi_void)
{
    hi_s32 ret = 0;
    ret = pthread_create(&tid_aenc, NULL, get_audio, NULL);
    if (ret < 0) {
        sample_print("create get_audio thread failed !!! ret %d\n", ret);
        return HI_FAILURE;
    }

    // ===== 启动 Qt 事件循环 =====
    printf("Starting Qt event loop in main thread...\n");
    qt_ui_run();  // 阻塞在这里,直到收到 CTRL+C
    printf("Qt event loop exited\n");

    pthread_join(tid_aenc, NULL);

    return HI_SUCCESS;
}

通过select机制去等待获取通道的音频数据,这里只设置一个fd,因为上述说的左右通道合并在一块了

音频流的获取是使用接口:hi_mpi_aenc_get_stream

hi_s32 hi_mpi_aenc_get_stream(hi_aenc_chn aenc_chn, hi_audio_stream *stream, hi_s32 milli_sec);
  • aenc_chn:通道号。取值范围:[0, OT_AENC_MAX_CHN_NUM)
  • stream:获取的音频码流
  • milli_sec:获取数据的超时时间
    • -1表示阻塞模式,无数据时一直等待;
    • 0表示非阻塞模式,无数据时则报错返回;
    • 大于0表示阻塞milli_sec毫秒,超时则报错返回。
  • 必须创建通道后才可能获取码流,否则直接返回失败,如果在获取码流过程中销毁通道则会立刻返回失败。
  • milli_sec的值必须大于等于-1,等于-1时采用阻塞模式获取数据,等于0时采用非阻塞模式获取数据,大于0时,阻塞milli_sec毫秒后,没有数据则返回超时并报错。
  • 直接获取AI原始音频数据的方法: 创建一路AENC通道,编码协议类型设置为OT_PT_LPCM,绑定AI通道后,从此 AENC通道获取的音频数据即AI原始数据。

获取码流后就可以进行自定义操作,这里我把他保存为文件

/* save audio stream to file */
    (hi_void)fwrite(stream.stream, 1, stream.len, output_audio_fd);
    (hi_void)fflush(output_audio_fd);

在使用完后要释放流

hi_s32 hi_mpi_aenc_release_stream(hi_aenc_chn aenc_chn, const hi_audio_stream *stream);
上一篇
下一篇