[005] [ESP32开发笔记] ADF基本框架

ESP32
开发笔记
ADF整体框架
音频元素
音频流
音频事件
音频管道
注意事项

1 ADF整体框架

[005] [ESP32开发笔记] ADF基本框架_第1张图片
esp-adf主要是基于pipeline运行,每个pipeline中最基本的运行单元就是element,每个element都由一个ringbuffer连接,每个element之间靠stream传送音频数据(stream∈element)。

例如,将MP3解码器和I2S流两个元素添加进管道,解码器的输入是MP3文件数据流,I2S流将解码后的音频输出到音频解码器芯片(Codec chip):
[005] [ESP32开发笔记] ADF基本框架_第2张图片
pipeline通过链表管理,即每个元素通过链表连接在一起,elementstream基于FreeRTOS的任务实现,即运行pipeline同时会启动了几个任务,基于队列、信号量、互斥体等机制实现数据的传输和消息的传递。

pipeline是adf实现音频处理的基础。可以将pipeline看作是流水线。音频数据从一头进,从另一头出。element流水线上的工人,负责加工音频数据。event监听流水线上所有工人的情况,可用户以通过msg得知。每个element都是一个任务

esp-adf 支持的elementsstream
[005] [ESP32开发笔记] ADF基本框架_第3张图片

  • i2s是通过操作i2s接口控制硬件编解码器的。
  • http是通过http协议将音频数据发送到远程服务器中。
  • fatfs则是实现了fatfs文件系统,一般是用于操作SD卡读写音频文件。
  • raw则是一种数据传输流,本身没有功能,只是负责将音频传送到下一个element。
  • spifss是一种基于flash的文件系统,可以通过它对flash以为文件系统方式操对音频文件进行读写。

源码中还有另外两种数据流algorithm streamtone stream。algorithm stream是回声处理,唤醒词处理加入到里面数据流,tone stream则是另外一种flash操作方法。

2 音频元素

每个解码器decoder、编码器encoder、滤波器filter、输入流input stream以及输出流output stream都属于音频元素。

主要功能为获取输入的数据,对其进行处理,然后输出到下一个,每个元素作为单独的任务运行,为了能够监听控制从输入、处理阶段到输出的数据,有7种可用的回调函数:open、seek、process、close、destroy、read 和 write。例如MP3 解码器正在使用打开、处理、关闭和销毁回调函数。

2.1 元素状态

typedef enum {
    AEL_STATE_NONE          = 0,
    AEL_STATE_INIT          = 1,
    AEL_STATE_INITIALIZING  = 2,
    AEL_STATE_RUNNING       = 3,
    AEL_STATE_PAUSED        = 4,
    AEL_STATE_STOPPED       = 5,
    AEL_STATE_FINISHED      = 6,
    AEL_STATE_ERROR         = 7
} audio_element_state_t;

2.2 元素动作指令

typedef enum {
    AEL_MSG_CMD_NONE                = 0,
    // AEL_MSG_CMD_ERROR               = 1,
    AEL_MSG_CMD_FINISH              = 2,
    AEL_MSG_CMD_STOP                = 3,
    AEL_MSG_CMD_PAUSE               = 4,
    AEL_MSG_CMD_RESUME              = 5,
    AEL_MSG_CMD_DESTROY             = 6,
    // AEL_MSG_CMD_CHANGE_STATE        = 7,
    AEL_MSG_CMD_REPORT_STATUS       = 8,
    AEL_MSG_CMD_REPORT_MUSIC_INFO   = 9,
    AEL_MSG_CMD_REPORT_CODEC_FMT    = 10,
    AEL_MSG_CMD_REPORT_POSITION     = 11,
} audio_element_msg_cmd_t;

2.3 元素执行的当前事件状态

typedef enum {
    AEL_STATUS_NONE                     = 0,
    AEL_STATUS_ERROR_OPEN               = 1,
    AEL_STATUS_ERROR_INPUT              = 2,
    AEL_STATUS_ERROR_PROCESS            = 3,
    AEL_STATUS_ERROR_OUTPUT             = 4,
    AEL_STATUS_ERROR_CLOSE              = 5,
    AEL_STATUS_ERROR_TIMEOUT            = 6,
    AEL_STATUS_ERROR_UNKNOWN            = 7,
    AEL_STATUS_INPUT_DONE               = 8,
    AEL_STATUS_INPUT_BUFFERING          = 9,
    AEL_STATUS_OUTPUT_DONE              = 10,
    AEL_STATUS_OUTPUT_BUFFERING         = 11,
    AEL_STATUS_STATE_RUNNING            = 12,
    AEL_STATUS_STATE_PAUSED             = 13,
    AEL_STATUS_STATE_STOPPED            = 14,
    AEL_STATUS_STATE_FINISHED           = 15,
    AEL_STATUS_MOUNTED                  = 16,
    AEL_STATUS_UNMOUNTED                = 17,
} audio_element_status_t;

2.4 音频信息

typedef struct {
    int sample_rates;                           /*!< 采样率 Hz */
    int channels;                               /*!< 音频通道数,单通道为1,立体声为2 */
    int bits;                                   /*!< 位宽(8, 16, 24, 32 bits) */
    int bps;                                    /*!< 比特率 */
    int64_t byte_pos;                           /*!< 元素当前位置(unit: bytes) */
    int64_t total_bytes;                        /*!< 元素的总字数数 */
    int duration;                               /*!< 元素的持续时间 (可选) */
    char *uri;                                  /*!< URI (可选) */
    esp_codec_type_t codec_fmt;                 /*!< 音乐格式 (可选) */
    audio_element_reserve_data_t reserve_data;  /*!< 该值保留给用户使用 (可选) */
} audio_element_info_t;

默认的音频信息取值:

#define AUDIO_ELEMENT_INFO_DEFAULT()    { \
    .sample_rates = 44100,                \
    .channels = 2,                        \
    .bits = 16,                           \
    .bps = 0,                             \
    .byte_pos = 0,                        \
    .total_bytes = 0,                     \
    .duration = 0,                        \
    .uri = NULL,                          \
    .codec_fmt = ESP_CODEC_TYPE_UNKNOW    \
}

2.5 元素管理句柄

audio_element.c中定义

struct audio_element {
    /* Functions/RingBuffers */
    el_io_func                  open;
    ctrl_func                   seek;
    process_func                process;
    el_io_func                  close;
    el_io_func                  destroy;
    io_type_t                   read_type;
    union {
        ringbuf_handle_t        input_rb;
        io_callback_t           read_cb;
    } in;
    io_type_t                   write_type;
    union {
        ringbuf_handle_t        output_rb;
        io_callback_t           write_cb;
    } out;

    audio_multi_rb_t            multi_in;
    audio_multi_rb_t            multi_out;

    /* Properties */
    volatile bool               is_open;
    audio_element_state_t       state;

    events_type_t               events_type;
    audio_event_iface_handle_t  iface_event;
    audio_callback_t            callback_event;

    int                         buf_size;
    char                        *buf;

    char                        *tag;
    int                         task_stack;
    int                         task_prio;
    int                         task_core;
    xSemaphoreHandle            lock;
    audio_element_info_t        info;
    audio_element_info_t        *report_info;

    bool                        stack_in_ext;
    audio_thread_t              audio_thread;

    /* PrivateData */
    void                        *data;
    EventGroupHandle_t          state_event;
    int                         input_wait_time;
    int                         output_wait_time;
    int                         out_buf_size_expect;
    int                         out_rb_size;
    volatile bool               is_running;
    volatile bool               task_run;
    volatile bool               stopping;
};
typedef struct audio_element *audio_element_handle_t;

element其实是一个freertos的任务,拥有优先级、线程栈等信息,每个任务都会执行callback open -> [loop: read -> process -> write] -> close.读取前一个element的数据,处理,再写入输出buff,传递给下一个element。

前七个元素都是不同阶段的回调函数,tag是每个element的身份证,pipeline凭借tag来识别element,同时还定义了out_buff的长度。

根据audio_element_cfg_t配置的参数来初始化一个element对象,返回element对象句柄:

audio_element_handle_t audio_element_init(audio_element_cfg_t *config);

3 音频流

负责获取音频数据,然后处理后将数据发送出去的Audio Element ,称为Audio Stream。

支持以下流类型:

  • I2S Stream
  • HTTP Stream
  • FatFs Stream
  • Raw Stream
  • Spiffs Stream

示例:audio_element_handle_t http_stream_reader, i2s_stream_writer, aac_decoder;

4 音频事件

音频事件用于管道中的音频元素之间建立通信,事件API基于FreeRTOS 消息队列messagequeue构建的,实现了“监听器”来监听传入的消息,并通过回调函数通知它们。

4.1 事件句柄

struct audio_event_iface {
    QueueHandle_t               internal_queue;
    QueueHandle_t               external_queue;
    QueueSetHandle_t            queue_set;
    int                         internal_queue_size;
    int                         external_queue_size;
    int                         queue_set_size;
    audio_event_iface_list_t    listening_queues;
    void                        *context;
    on_event_iface_func         on_cmd;
    int                         wait_time;
    int                         type;
};
typedef struct audio_event_iface *audio_event_iface_handle_t;

其中audio_event_iface_list_t宏定义如下:

typedef STAILQ_HEAD(audio_event_iface_list, audio_event_iface_item) audio_event_iface_list_t;

STAILQ_HEAD声明 名为audio_event_iface_list,数据类型为audio_event_iface_item的单链表,在queue.h中定义如下:

#define	STAILQ_HEAD(name, type)						\
struct name {								\
	struct type *stqh_first;/* first element */			\
	struct type **stqh_last;/* addr of last next element */		\
}

audio_event_iface_item 数据类型如下:

typedef struct audio_event_iface_item {
    STAILQ_ENTRY(audio_event_iface_item)    next;			// 指向下一个event_item的指针
    QueueHandle_t                           queue;			// 消息队列数据类型
    int                                     queue_size;		// 消息队列大小
    int                                     mark_to_remove;	// 
} audio_event_iface_item_t;

4.2 事件配置结构体

typedef struct {
    int                 internal_queue_size;        /*!< 事件内部队列的大小(可选) */
    int                 external_queue_size;        /*!< 事件外部队列的大小(可选) */
    int                 queue_set_size;             /*!< 设置事件队列的大小(可选) */
    on_event_iface_func on_cmd;                     /*!< 事件到达时,监听器的回调函数 */
    void                *context;                   /*!< 上下文将传递给回调函数 */
    TickType_t          wait_time;                  /*!< 检查事件队列的超时时间 */
    int                 type;                       /*!< 来自audio_event_iface_msg_t source_type */
} audio_event_iface_cfg_t;

默认的事件配置:

#define DEFAULT_AUDIO_EVENT_IFACE_SIZE  (5)

#define AUDIO_EVENT_IFACE_DEFAULT_CFG() {                   \
    .internal_queue_size = DEFAULT_AUDIO_EVENT_IFACE_SIZE,  \
    .external_queue_size = DEFAULT_AUDIO_EVENT_IFACE_SIZE,  \
    .queue_set_size = DEFAULT_AUDIO_EVENT_IFACE_SIZE,       \
    .on_cmd = NULL,                                         \
    .context = NULL,                                        \
    .wait_time = portMAX_DELAY,                             \
    .type = 0,                                              \
}

4.3 监听事件(iface)的初始化

audio_event_iface_handle_t audio_event_iface_init(audio_event_iface_cfg_t *config)
{
    audio_event_iface_handle_t evt = audio_calloc(1, sizeof(struct audio_event_iface));
    evt->queue_set_size   = config->queue_set_size;
    evt->internal_queue_size = config->internal_queue_size;
    evt->external_queue_size = config->external_queue_size;
    evt->context = config->context;
    evt->on_cmd = config->on_cmd;
    evt->type = config->type;
    if (evt->queue_set_size) {
        evt->queue_set = xQueueCreateSet(evt->queue_set_size);
    }
    if (evt->internal_queue_size) {
        evt->internal_queue = xQueueCreate(evt->internal_queue_size, sizeof(audio_event_iface_msg_t));
    }
    if (evt->external_queue_size) {
        evt->external_queue = xQueueCreate(evt->external_queue_size, sizeof(audio_event_iface_msg_t));
    } 
    STAILQ_INIT(&evt->listening_queues);
    return evt;
	[...]
}

创建两个消息队列,其大小通过事件配置结构体audio_event_iface_cfg_t配置,然后初始化audio_event_iface_list_t的单链表。

之后event对象将作为监听器listener,监听pipeline中所有element的通知。

5 音频管道

音频管道组成:输入流->编解码->适配->输出流

功能

  • 用于链接元素
  • 负责将消息从元素任务转发到应用程序
struct audio_pipeline {
    audio_element_list_t        el_list;	// 音频元素单链表
    ringbuf_list_t              rb_list;	// 环形缓冲区单链表
    audio_element_state_t       state;
    xSemaphoreHandle            lock;
    bool                        linked;
    audio_event_iface_handle_t  listener;
};
typedef STAILQ_HEAD(audio_element_list, audio_element_item) audio_element_list_t;
typedef STAILQ_HEAD(ringbuf_list, ringbuf_item) ringbuf_list_t;

STAILQ_HEAD是个宏定义,创建一个名为name,数据类型为type的链表:

#define	STAILQ_HEAD(name, type)						\
struct name {								\
	struct type *stqh_first;/* 第一个元素 */			\
	struct type **stqh_last;/* 下一个元素的地址 */		\
}
  • audio_element_item数据类型(包含元素句柄audio_element_handle_t
typedef struct audio_element_item {
    STAILQ_ENTRY(audio_element_item) next;		// next指针域
    audio_element_handle_t           el;		// 当前的element
    bool                             linked;	// 表示是否连接到pipeline
    bool                             kept_ctx;	// 
    audio_element_status_t           el_state;	// 元素状态
} audio_element_item_t;
  • ringbuf_item数据类型(包含环形缓冲区句柄ringbuf_handle_t和元素句柄audio_element_handle_t
typedef struct ringbuf_item {
    STAILQ_ENTRY(ringbuf_item)  next;		// next指针域
    ringbuf_handle_t            rb;			// 当前的ringbuff
    audio_element_handle_t      host_el;	// 当前ringbuff所属的元素
    bool                        linked;		// 表示是否连接到pipeline
    bool                        kept_ctx;
} ringbuf_item_t;

5.1 环形缓冲区

image-20220409221146569

ringbuffer是一种环形缓冲区,用于:

  • 连接 audio element
  • 作数据缓冲

缓冲区中没有元素时,向ringbuffer请求数据时都会导致ringbuffer任务阻塞,直到ringbufer中的数据可以使用这个任务才可以继续执行。

管道、元素和环形缓冲区大致关系如下:

[005] [ESP32开发笔记] ADF基本框架_第4张图片

5.2 主要函数

5.2.1 audio_pipeline_register

esp_err_t audio_pipeline_register(audio_pipeline_handle_t pipeline, audio_element_handle_t el, const char *name);

根据audio_element_handle_t创建一个element_item,并将element_item插入到element_list末尾,同时设置tag。

5.2.2 audio_pipeline_link

esp_err_t audio_pipeline_link(audio_pipeline_handle_t pipeline, const char *link_tag[], int link_num);

根据已经注册到管道中的元素名称name(audio_pipeline_register),将元素用环形缓冲区ringbuffer连接起来。

audio_pipeline_link函数内部调用:

_pipeline_rb_linked(audio_pipeline_handle_t pipeline, audio_element_handle_t el, bool first, bool last)

该函数创建一个ringbuff_handleringbufff_item,将ringbuff_handle传递给ringbufff_item,并将ringbufff_item添加到ringbuff_list,设置ringbuff_item的host_el(当前ringbuff所属的元素)为el(当前元素),并将ringbuff传递给el。

_pipeline_rb_linked函数内部主要内容:

//判断el的位置并将rb传递给el
audio_element_set_input_ringbuf(el, rb);
audio_element_set_output_ringbuf(el, rb);

//将rb_item插入rb_list队尾
STAILQ_INSERT_TAIL(&pipeline->rb_list, rb_item, next);

5.2.3 audio_pipeline_set_listener

esp_err_t audio_pipeline_set_listener(audio_pipeline_handle_t pipeline, audio_event_iface_handle_t evt);

函数会将pipeline中element_list从表头至表尾取出element_item,并将其指向的element_handle对象中的iface_event事件成员对象的external_queue,插入到listenerevent_list中的event_item中的queue

简单来说,就是将pipeline中所有的element的事件消息队列插入listener中,这样listener就可接收element中的全部消息。

整个pipeline的逻辑大概如下所示:

[005] [ESP32开发笔记] ADF基本框架_第5张图片

简化逻辑图:

[005] [ESP32开发笔记] ADF基本框架_第6张图片

6 注意事项

  • 同一个元素不能同时被链接到不同的管道(因为都挂在了各自的链表上,直接通过地址来修改)
  • 灵活管道操作方法(参考例程):
audio_pipeline_breakup_elements(pipeline, NULL);     // NULL打散所有元素,但它们及其连接的ringbuffer将被保留
// 重新链接AEL_STATE_PAUSED
audio_pipeline_relink(pipeline, (const char *[]) {"fm_http", "aac", "i2s"}, 3);
  • 监听来自管道中所有元素的事件必须是链接(Link)的元素, 若仅register而没link则不会监听

参考:

  • 乐鑫ADF音频应用开发框架
  • ESP32 ADF element,pipeline和event是什么 如何处理音频数据

END

你可能感兴趣的:(ESP32,ESP32,ADF)