1 ADF整体框架
esp-adf主要是基于pipeline
运行,每个pipeline
中最基本的运行单元就是element
,每个element
都由一个ringbuffer
连接,每个element
之间靠stream
传送音频数据(stream∈element
)。
例如,将MP3解码器和I2S流两个元素添加进管道,解码器的输入是MP3文件数据流,I2S流将解码后的音频输出到音频解码器芯片(Codec chip):
pipeline
通过链表管理,即每个元素通过链表连接在一起,element
和stream
基于FreeRTOS的任务实现,即运行pipeline
同时会启动了几个任务,基于队列、信号量、互斥体等机制实现数据的传输和消息的传递。
pipeline是adf实现音频处理的基础。可以将
pipeline
看作是流水线。音频数据从一头进,从另一头出。element
是流水线上的工人,负责加工音频数据。event
是监听流水线上所有工人的情况,可用户以通过msg
得知。每个element都是一个任务。
esp-adf 支持的elements
和stream
:
- i2s是通过操作i2s接口控制硬件编解码器的。
- http是通过http协议将音频数据发送到远程服务器中。
- fatfs则是实现了fatfs文件系统,一般是用于操作SD卡读写音频文件。
- raw则是一种数据传输流,本身没有功能,只是负责将音频传送到下一个element。
- spifss是一种基于flash的文件系统,可以通过它对flash以为文件系统方式操对音频文件进行读写。
源码中还有另外两种数据流algorithm stream
和tone 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。
支持以下流类型:
示例: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 环形缓冲区
ringbuffer是一种环形缓冲区,用于:
- 连接 audio element
- 作数据缓冲
缓冲区中没有元素时,向ringbuffer请求数据时都会导致ringbuffer任务阻塞,直到ringbufer中的数据可以使用这个任务才可以继续执行。
管道、元素和环形缓冲区大致关系如下:
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_handle
和ringbufff_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
,插入到listener
的event_list
中的event_item
中的queue
。
简单来说,就是将pipeline中所有的element
的事件消息队列插入listener中,这样listener就可接收element中的全部消息。
整个pipeline的逻辑大概如下所示:
简化逻辑图:
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则不会监听
参考:
END