【立创·实战派ESP32-S3】文档教程
第 14 章 MP3 音乐播放器
本例程在液晶屏上显示一个简单的播放器界面,能控制音乐播放暂停,能控制上一首下一首,可以调节音量。是一个非常简单的画面,有利于大家学习其中的原理。
14.1 使用例程
把开发板提供的【11-mp3_player】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
开发板开始运行程序后,液晶屏显示播放器画面。
点击列表,显示当前文件系统所有的音乐名称,点击列表中的名称选择你要播放的音乐。
可以点击播放、暂停、上一首、下一首按钮,最下面还有声音调节滑动条。
mp3 文件存储在 Flash 中,在 Flash 中使用分区表划分了一块区域,初始化成了 spiffs 文件系统,从这个文件系统中,读取文件并播放。
如果想更换成你自己的 mp3 音乐,把你的 mp3 音乐放到工程文件夹里面的 spiffs 文件夹里面就可以,然后重新编译下载。需要注意两点:第一个是文件大小。我们目前在分区表中给 spiffs 的大小是 3M,这个你看分区表文件就可以看到。如果你的文件超过了 3M,编译的时候,会有提示,可以改大,但要注意,flash 的总大小是 16M,这其中还包含其它分区。第二个是 spiffs 不支持中文名称,把你要添加的歌曲名称改成字母就可以放进去了,非要让他显示中文,可以使用 SD 卡实现。
如果你想播放 SD 卡中的音乐,可以自己添加代码实现。主函数中的初始化 spiffs 部分,换成初始化 sd 的代码,在第 7 章已经写好了这个初始化 SD 卡的代码。然后在 mp3_player_init 函数中,file_iterator_new 函数的参数,换成 SD 卡的路径就可以。
对于获取到的文件,例程中并没有区分是不是 mp3 文件,所以扫描到的文件都会显示到列表中。你可以只给 spiffs 中添加 mp3 音乐,或者在使用 SD 卡时,建立一个 music 文件夹,只添加 mp3 音乐。如果非要和其它文件一起放在一个文件夹,你可以添加识别是否是 mp3 音乐的代码,只给音乐列表中添加 mp3 文件。
本例程使用的 mp3 解码文件,是 esp-libheiix-mp3 组件,因为 mp3 文件也并非统一标准,而且此解码文件的解码容错能力有限,也没有解决完所有的 BUG,所以你的 mp3 文件,不一定都能正确解码播放。如果你对解码 mp3 文件感兴趣,可以试着完善此组件,或者给 esp-libheiix-mp3 组件的作者提交 BUG。
14.2 例程讲解
实现 mp3 音乐播放,有两种方法,一种是使用乐鑫 ADF 音频框架,另外一种是使用 esp-audio-player 组件。本例程使用的是 esp-audio-player 组件实现,此组件会自动加载 esp-libheiix-mp3 组件解码 mp3。
打开 app_main 函数,代码如下:
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lvgl_start(); // 初始化液晶屏lvgl接口
bsp_spiffs_mount(); // SPIFFS文件系统初始化
bsp_codec_init(); // 音频初始化
mp3_player_init(); // MP3播放器初始化
}
2
3
4
5
6
7
8
9
10
前 3 行语句,初始化完成液晶屏显示。
第 4 行语句,初始化了 SPIFFS 文件系统,这是一个在 SPI FLASH 里面划分出一片区域做的文件系统。mp3 音乐文件,就放在这个文件系统里面。
第 5 行语句,初始化音频的硬件。
第 6 行语句,初始化 mp3 播放器界面以及需要的组件。
bsp_spiffs_mount()初始化文件系统
esp_err_t bsp_spiffs_mount(void)
{
esp_vfs_spiffs_conf_t conf = {
.base_path = SPIFFS_BASE,
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = false,
};
esp_err_t ret_val = esp_vfs_spiffs_register(&conf);
ESP_ERROR_CHECK(ret_val);
size_t total = 0, used = 0;
ret_val = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret_val != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret_val));
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
return ret_val;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SPIFFS_BASE,是文件系统的路径,在.h 文件中定义成了 "/spiffs"
。
.partition_label
赋值为 "storage"
,和分区表中 spiffs 分区名称一致。
bsp_codec_init()音频硬件初始化
esp_err_t bsp_codec_init(void)
{
play_dev_handle = bsp_audio_codec_speaker_init();
assert((play_dev_handle) && "play_dev_handle not initialized");
// record_dev_handle = bsp_audio_codec_microphone_init();
// assert((record_dev_handle) && "record_dev_handle not initialized");
bsp_codec_set_fs(CODEC_DEFAULT_SAMPLE_RATE, CODEC_DEFAULT_BIT_WIDTH, CODEC_DEFAULT_CHANNEL);
return ESP_OK;
}
2
3
4
5
6
7
8
9
10
11
12
bsp_audio_codec_speaker_init()
函数初始化 es8311 芯片。
bsp_audio_codec_microphone_init()
函数初始化 es7210 芯片。本例程并没有用到麦克风,所以把这两条语句注释掉。
bsp_codec_set_fs()
函数设置采样率、位宽、通道数。
具体实现的代码,和前面音频相关章节讲的差不多,这里就不多说了。
主要看一下 mp3_player_init()
的实现。
mp3_player_init() MP3 播放器初始化
// mp3播放器初始化
void mp3_player_init(void)
{
// 获取文件信息
file_iterator = file_iterator_new(SPIFFS_BASE);
assert(file_iterator != NULL);
// 初始化音频播放
player_config.mute_fn = _audio_player_mute_fn;
player_config.write_fn = _audio_player_write_fn;
player_config.clk_set_fn = _audio_player_std_clock;
player_config.priority = 1;
ESP_ERROR_CHECK(audio_player_new(player_config));
ESP_ERROR_CHECK(audio_player_callback_register(_audio_player_callback, NULL));
// 显示界面
music_ui();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
第 5、6 行,获取 spiffs 文件系统中所有文件名称,存储到了 file_iterator 中的 list 数组中。
第 9~15 行,初始化 audio_player 组件,mute_fn、 wirite_fn、 clk_set_fn 以及 _audio_player_callback 是事件函数和回调函数,在运行的时候,会调用。
_audio_player_mute_fn()
函数,用于设置音量,每次刚开始播放一首歌曲的时候,都会进入。
// 设置声音处理函数
static esp_err_t _audio_player_mute_fn(AUDIO_PLAYER_MUTE_SETTING setting)
{
esp_err_t ret = ESP_OK;
// 获取当前音量
uint8_t volume = get_sys_volume();
// 判断是否需要静音
bsp_codec_mute_set(setting == AUDIO_PLAYER_MUTE ? true : false);
// 如果不是静音 设置音量
if (setting == AUDIO_PLAYER_UNMUTE) {
bsp_codec_volume_set(volume, NULL);
}
ret = ESP_OK;
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_audio_player_write_fn()
函数,用于播放音乐,播放一帧,进入一次,所以在播放的时候,会不断进入。
// 播放音乐函数 播放音乐的时候 会不断进入
static esp_err_t _audio_player_write_fn(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = bsp_i2s_write(audio_buffer, len, bytes_written, timeout_ms);
return ret;
}
2
3
4
5
6
7
8
9
_audio_player_std_clock()函数,根据 mp3 文件信息,设置采样率,位宽,通道数。每换一首歌曲,都会调用一次。
// 设置采样率 播放的时候进入一次
static esp_err_t _audio_player_std_clock(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
esp_err_t ret = ESP_OK;
ret = bsp_codec_set_fs(rate, bits_cfg, ch);
return ret;
}
2
3
4
5
6
7
8
9
_audio_player_callback()
函数,每次播放器状态转换,都会自动进入,然后执行相应动作。
// 回调函数 播放器每次动作都会进入
static void _audio_player_callback(audio_player_cb_ctx_t *ctx)
{
ESP_LOGI(TAG, "ctx->audio_event = %d", ctx->audio_event);
switch (ctx->audio_event) {
case AUDIO_PLAYER_CALLBACK_EVENT_IDLE: { // 播放完一首歌 进入这个case
ESP_LOGI(TAG, "AUDIO_PLAYER_REQUEST_IDLE");
// 指向下一首歌
file_iterator_next(file_iterator);
int index = file_iterator_get_index(file_iterator);
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
// 修改当前播放的音乐名称
lvgl_port_lock(0);
lv_dropdown_set_selected(music_list, index);
lv_obj_t *label_title = (lv_obj_t *) music_list->user_data;
lv_label_set_text_static(label_title, file_iterator_get_name_from_index(file_iterator, index));
lvgl_port_unlock();
break;
}
case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING: // 播放音乐
ESP_LOGI(TAG, "AUDIO_PLAYER_REQUEST_PLAY");
pa_en(1); // 打开音频功放
break;
case AUDIO_PLAYER_CALLBACK_EVENT_PAUSE: // 暂停音乐
ESP_LOGI(TAG, "AUDIO_PLAYER_REQUEST_PAUSE");
pa_en(0); // 关闭音频功放
break;
default:
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
AUDIO_PLAYER_CALLBACK_EVENT_IDLE:播放完一首歌曲后,会自动进入这个状态,在这里,执行自动播放下一首歌曲,界面也显示歌曲名称。如果播放完一首歌曲后,你并不需要自动播放下一首歌曲,这里就可以删除。
AUDIO_PLAYER_CALLBACK_EVENT_PLAYING:进入播放歌曲状态,会进入这里一次,在这里执行 pa_en(1)打开音频功放。
AUDIO_PLAYER_CALLBACK_EVENT_PAUSE:进入暂停歌曲状态,会进入这里一次,在这里执行 pa_en(0)关闭音频功放。
最后一行,music_ui(),显示播放器界面。
// 播放器界面初始化
void music_ui(void)
{
lvgl_port_lock(0);
ui_button_style_init();// 初始化按键风格
/* 创建播放暂停控制按键 */
lv_obj_t *btn_play_pause = lv_btn_create(lv_scr_act());
lv_obj_align(btn_play_pause, LV_ALIGN_CENTER, 0, 40);
lv_obj_set_size(btn_play_pause, 50, 50);
lv_obj_set_style_radius(btn_play_pause, 25, LV_STATE_DEFAULT);
lv_obj_add_flag(btn_play_pause, LV_OBJ_FLAG_CHECKABLE);
lv_obj_add_style(btn_play_pause, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_pause, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_t *label_play_pause = lv_label_create(btn_play_pause);
lv_label_set_text_static(label_play_pause, LV_SYMBOL_PLAY);
lv_obj_center(label_play_pause);
lv_obj_set_user_data(btn_play_pause, (void *) label_play_pause);
lv_obj_add_event_cb(btn_play_pause, btn_play_pause_cb, LV_EVENT_VALUE_CHANGED, NULL);
/* 创建上一首控制按键 */
lv_obj_t *btn_play_prev = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_play_prev, 50, 50);
lv_obj_set_style_radius(btn_play_prev, 25, LV_STATE_DEFAULT);
lv_obj_clear_flag(btn_play_prev, LV_OBJ_FLAG_CHECKABLE);
lv_obj_align_to(btn_play_prev, btn_play_pause, LV_ALIGN_OUT_LEFT_MID, -40, 0);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_DEFAULT);
lv_obj_t *label_prev = lv_label_create(btn_play_prev);
lv_label_set_text_static(label_prev, LV_SYMBOL_PREV);
lv_obj_set_style_text_font(label_prev, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label_prev, lv_color_make(0, 0, 0), LV_STATE_DEFAULT);
lv_obj_center(label_prev);
lv_obj_set_user_data(btn_play_prev, (void *) label_prev);
lv_obj_add_event_cb(btn_play_prev, btn_prev_next_cb, LV_EVENT_CLICKED, (void *) false);
/* 创建下一首控制按键 */
lv_obj_t *btn_play_next = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_play_next, 50, 50);
lv_obj_set_style_radius(btn_play_next, 25, LV_STATE_DEFAULT);
lv_obj_clear_flag(btn_play_next, LV_OBJ_FLAG_CHECKABLE);
lv_obj_align_to(btn_play_next, btn_play_pause, LV_ALIGN_OUT_RIGHT_MID, 40, 0);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_DEFAULT);
lv_obj_t *label_next = lv_label_create(btn_play_next);
lv_label_set_text_static(label_next, LV_SYMBOL_NEXT);
lv_obj_set_style_text_font(label_next, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label_next, lv_color_make(0, 0, 0), LV_STATE_DEFAULT);
lv_obj_center(label_next);
lv_obj_set_user_data(btn_play_next, (void *) label_next);
lv_obj_add_event_cb(btn_play_next, btn_prev_next_cb, LV_EVENT_CLICKED, (void *) true);
/* 创建声音调节滑动条 */
lv_obj_t *volume_slider = lv_slider_create(lv_scr_act());
lv_obj_set_size(volume_slider, 200, 10);
lv_obj_set_ext_click_area(volume_slider, 15);
lv_obj_align(volume_slider, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_slider_set_range(volume_slider, 0, 100);
lv_slider_set_value(volume_slider, g_sys_volume, LV_ANIM_ON);
lv_obj_add_event_cb(volume_slider, volume_slider_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t *lab_vol_min = lv_label_create(lv_scr_act());
lv_label_set_text_static(lab_vol_min, LV_SYMBOL_VOLUME_MID);
lv_obj_set_style_text_font(lab_vol_min, &lv_font_montserrat_20, LV_STATE_DEFAULT);
lv_obj_align_to(lab_vol_min, volume_slider, LV_ALIGN_OUT_LEFT_MID, -10, 0);
lv_obj_t *lab_vol_max = lv_label_create(lv_scr_act());
lv_label_set_text_static(lab_vol_max, LV_SYMBOL_VOLUME_MAX);
lv_obj_set_style_text_font(lab_vol_max, &lv_font_montserrat_20, LV_STATE_DEFAULT);
lv_obj_align_to(lab_vol_max, volume_slider, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
/* 创建音乐标题 */
lv_obj_t *lab_title = lv_label_create(lv_scr_act());
lv_obj_set_user_data(lab_title, (void *) btn_play_pause);
lv_label_set_text_static(lab_title, "Scanning Files...");
lv_obj_set_style_text_font(lab_title, &lv_font_montserrat_32, LV_STATE_DEFAULT);
lv_obj_align(lab_title, LV_ALIGN_TOP_MID, 0, 20);
/* 创建音乐列表 */
music_list = lv_dropdown_create(lv_scr_act());
lv_dropdown_clear_options(music_list);
lv_dropdown_set_options_static(music_list, "Scanning...");
lv_obj_set_style_text_font(music_list, &lv_font_montserrat_20, LV_STATE_ANY);
lv_obj_set_width(music_list, 200);
lv_obj_align_to(music_list, lab_title, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);
lv_obj_set_user_data(music_list, (void *) lab_title);
lv_obj_add_event_cb(music_list, music_list_cb, LV_EVENT_VALUE_CHANGED, NULL);
build_file_list(music_list);
lvgl_port_unlock();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
界面显示,基本上看函数名称就知道作用了,这里我们主要看一下它们的事件处理函数。
btn_play_pause_cb() 播放暂停按钮 事件处理函数
// 播放暂停按钮 事件处理函数
static void btn_play_pause_cb(lv_event_t *event)
{
lv_obj_t *btn = lv_event_get_target(event);
lv_obj_t *lab = (lv_obj_t *) btn->user_data;
audio_player_state_t state = audio_player_get_state();
printf("state=%d\n", state);
if(state == AUDIO_PLAYER_STATE_IDLE){
lvgl_port_lock(0);
lv_label_set_text_static(lab, LV_SYMBOL_PAUSE);
lvgl_port_unlock();
int index = file_iterator_get_index(file_iterator);
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
}else if (state == AUDIO_PLAYER_STATE_PAUSE) {
lvgl_port_lock(0);
lv_label_set_text_static(lab, LV_SYMBOL_PAUSE);
lvgl_port_unlock();
audio_player_resume();
} else if (state == AUDIO_PLAYER_STATE_PLAYING) {
lvgl_port_lock(0);
lv_label_set_text_static(lab, LV_SYMBOL_PLAY);
lvgl_port_unlock();
audio_player_pause();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
点击播放按钮后进入这个函数。在没有播放之前,也不是暂停状态时,会进入 AUDIO_PLAYER_STATE_IDLE。进入这个 if 后,把按钮显示为暂停图标,并开始播放当前的音乐。这时候,再点击暂停按钮,会进入 AUDIO_PLAYER_STATE_PLAYING,把按钮换成播放图标,并使用 audio_player_pause()函数暂停音乐。这时候,再点击播放按钮,会进入 AUDIO_PLAYER_STATE_PAUSE,切换成暂停图标,使用 audio_player_resume()继续播放音乐。之后点击播放暂停,会不断进入 AUDIO_PLAYER_STATE_PAUSE 和 AUDIO_PLAYER_STATE_PLAYING。
btn_prev_next_cb() 上一首 下一首 事件处理函数
// 上一首 下一首 按键事件处理函数
static void btn_prev_next_cb(lv_event_t *event)
{
bool is_next = (bool) event->user_data;
if (is_next) {
ESP_LOGI(TAG, "btn next");
file_iterator_next(file_iterator);
} else {
ESP_LOGI(TAG, "btn prev");
file_iterator_prev(file_iterator);
}
// 修改当前的音乐名称
int index = file_iterator_get_index(file_iterator);
lvgl_port_lock(0);
lv_dropdown_set_selected(music_list, index);
lv_obj_t *label_title = (lv_obj_t *) music_list->user_data;
lv_label_set_text_static(label_title, file_iterator_get_name_from_index(file_iterator, index));
lvgl_port_unlock();
// 执行音乐事件
audio_player_state_t state = audio_player_get_state();
printf("prev_next_state=%d\n", state);
if (state == AUDIO_PLAYER_STATE_IDLE) {
// Nothing to do
}else if (state == AUDIO_PLAYER_STATE_PAUSE){ // 如果当前正在暂停歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
audio_player_pause();
} else if (state == AUDIO_PLAYER_STATE_PLAYING) { // 如果当前正在播放歌曲
// 播放歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上一首和下一首是两个按钮,共用一个事件处理函数。在函数中,通过获取 user_data,来确定按的是上一首还是下一首按钮。
file_iterator_next()
指向下一首歌曲,file_iterator_prev()
指向上一首歌曲。
file_iterator_get_index()
获取当前指向歌曲的序号。
file_iterator_get_name_from_index()
获取当前序号的文件名称。
再往下,判断当前状态。
如果是 AUDIO_PLAYER_STATE_PAUSE,表示按下按钮的时候,当前正在暂停歌曲,play_index()
播放选中的歌曲后,通过 audio_player_pause()
暂停。
如果是 AUDIO_PLAYER_STATE_PLAYING,表示按下按钮的时候,当前正在播放歌曲,play_index()
播放选中的歌曲。
volume_slider_cb() 音量滑动条
// 音量调节滑动条 事件处理函数
static void volume_slider_cb(lv_event_t *event)
{
lv_obj_t *slider = lv_event_get_target(event);
int volume = lv_slider_get_value(slider); // 获取slider的值
bsp_codec_volume_set(volume, NULL); // 设置声音大小
g_sys_volume = volume; // 把声音赋值给g_sys_volume保存
ESP_LOGI(TAG, "volume '%d'", volume);
}
2
3
4
5
6
7
8
9
在创建 slider 的时候,已经把值设置为了 0~100,所以这里获取的 slider 值,正好对应 0~100 的声音。
music_list_cb() 音乐事件处理函数
// 音乐列表 点击事件处理函数
static void music_list_cb(lv_event_t *event)
{
lv_obj_t *label_title = (lv_obj_t *) music_list->user_data;
uint16_t index = lv_dropdown_get_selected(music_list);
ESP_LOGI(TAG, "switching index to '%d'", index);
file_iterator_set_index(file_iterator, index);
lvgl_port_lock(0);
lv_label_set_text_static(label_title, file_iterator_get_name_from_index(file_iterator, index));
lvgl_port_unlock();
audio_player_state_t state = audio_player_get_state();
if (state == AUDIO_PLAYER_STATE_PAUSE){ // 如果当前正在暂停歌曲
play_index(index);
audio_player_pause();
} else if (state == AUDIO_PLAYER_STATE_PLAYING) { // 如果当前正在播放歌曲
play_index(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
选择 list 中的音乐名称后,会进入这个函数。
lv_dropdown_get_selected()
获取选择好的 list 序号。
file_iterator_set_index()
指向当前的序号的音乐。
lv_label_set_text_static()
把 list 中选择的音乐名称显示到屏幕上。
再往后,还是两种状态,如果正在播放,就继续播放 list 中选择的音乐,如果正在暂停,就先选中当前的音乐,然后使用 audio_player_pause()
暂停音乐。
14.3 例程制作
本例程是在【08-lcd_lvgl】例程上修改,参考乐鑫官方 esp-box 中的例程 mp3_demo,以及乐鑫提供的 esp-bsp 库文件。
esp-box 源码下载:https://github.com/espressif/esp-box 参考例程路径:esp-box\examples\mp3_demo
esp-bsp 代码开源地址:https://github.com/espressif/esp-bsp
复制【08-lcd_lvgl】例程修改为【11-mp3_player】。
在一级 CMakeLists.txt 文件中,把工程名称修改为 mp3_player。
把 yinwu.h 和 logo_en_240x240_lcd.h 文件删除,同时把 main.c 文件中包含这两个头文件的语句删除,因为本例程用不着这两个图片了。
把 app_main 函数中,把 lvgl 的 demo 函数都删除,同时把 lv_demos.h 头文件也删除,因为本例程也不用运行 lvgl 的 demo 了。
修改后的 main 函数如下:
#include <stdio.h>
#include "esp32_s3_szp.h"
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lvgl_start(); // 初始化液晶屏lvgl接口
}
2
3
4
5
6
7
8
9
10
因为不跑 lvgl 的 demo 了,在 sdkconfig.default 文件中,把下面 5 条使能语句也删除。
CONFIG_LV_USE_DEMO_WIDGETS=y
CONFIG_LV_USE_DEMO_KEYPAD_AND_ENCODER=y
CONFIG_LV_USE_DEMO_BENCHMARK=y
CONFIG_LV_USE_DEMO_STRESS=y
CONFIG_LV_USE_DEMO_MUSIC=y
2
3
4
5
其中的 12 和 16 字体,是 lvgl 的 music demo 需要的。本例程需要使用 20、24、32 字体。
CONFIG_LV_FONT_MONTSERRAT_20=y
CONFIG_LV_FONT_MONTSERRAT_24=y
CONFIG_LV_FONT_MONTSERRAT_32=y
2
3
接下来,就可以开始增加本例程的应用程序了,主要增加两部分,一个是音频驱动,一个是界面显示。
音频驱动
播放音乐,需要写好音频芯片 es8311 的驱动程序。但是我们为了方便以后使用麦克风,在移植的时候,就顺便把 es7210 的驱动程序也准备好了。
乐鑫官方的许多开发板音频部分都是使用同一个 I2S 接口驱动 es8311 和 es7210,比如 esp-box、ESP32-S3-Korvo、ESP32-S3-LCD_EV_Board 等。
在 esp-box 中,我们可以找到 bsp_codec_init()函数,路径是 esp-box\components\bsp\src\boards。如下图所示:
bsp_codec_init()
函数用于初始化 es8311 和 es7210。
static esp_err_t bsp_codec_init()
{
play_dev_handle = bsp_audio_codec_speaker_init();
assert((play_dev_handle) && "play_dev_handle not initialized");
record_dev_handle = bsp_audio_codec_microphone_init();
assert((record_dev_handle) && "record_dev_handle not initialized");
bsp_codec_set_fs(CODEC_DEFAULT_SAMPLE_RATE, CODEC_DEFAULT_BIT_WIDTH, CODEC_DEFAULT_CHANNEL);
return ESP_OK;
}
2
3
4
5
6
7
8
9
10
11
把这个函数复制到我们自己工程中,放到 esp32_s3_szp.c 文件中。
bsp_audio_codec_speaker_init()
函数用于初始化 es8311 芯片。
bsp_audio_codec_microphone_init()
函数用于初始化 es7210 芯片。
bsp_codec_set_fs()
用于设置 I2S 外设的采样率、位宽、通道数。
这 3 个函数定义,在 esp-bsp 库中可以找到,我们看 esp-box-3 中的函数就可以,路径是 esp-bsp\bsp\esp-box-3。如下图所示:
bsp_audio_codec_speaker_init()
函数代码如下:
esp_codec_dev_handle_t bsp_audio_codec_speaker_init(void)
{
const audio_codec_data_if_t *i2s_data_if = bsp_audio_get_codec_itf();
if (i2s_data_if == NULL) {
/* Initilize I2C */
BSP_ERROR_CHECK_RETURN_ERR(bsp_i2c_init());
/* Configure I2S peripheral and Power Amplifier */
BSP_ERROR_CHECK_RETURN_ERR(bsp_audio_init(NULL));
i2s_data_if = bsp_audio_get_codec_itf();
}
assert(i2s_data_if);
const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio();
audio_codec_i2c_cfg_t i2c_cfg = {
.port = BSP_I2C_NUM,
.addr = ES8311_CODEC_DEFAULT_ADDR,
};
const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
BSP_NULL_CHECK(i2c_ctrl_if, NULL);
esp_codec_dev_hw_gain_t gain = {
.pa_voltage = 5.0,
.codec_dac_voltage = 3.3,
};
es8311_codec_cfg_t es8311_cfg = {
.ctrl_if = i2c_ctrl_if,
.gpio_if = gpio_if,
.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC,
.pa_pin = BSP_POWER_AMP_IO,
.pa_reverted = false,
.master_mode = false,
.use_mclk = true,
.digital_mic = false,
.invert_mclk = false,
.invert_sclk = false,
.hw_gain = gain,
};
const audio_codec_if_t *es8311_dev = es8311_codec_new(&es8311_cfg);
BSP_NULL_CHECK(es8311_dev, NULL);
esp_codec_dev_cfg_t codec_dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = es8311_dev,
.data_if = i2s_data_if,
};
return esp_codec_dev_new(&codec_dev_cfg);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
bsp_audio_codec_microphone_init()
函数代码如下:
esp_codec_dev_handle_t bsp_audio_codec_microphone_init(void)
{
const audio_codec_data_if_t *i2s_data_if = bsp_audio_get_codec_itf();
if (i2s_data_if == NULL) {
/* Initilize I2C */
BSP_ERROR_CHECK_RETURN_ERR(bsp_i2c_init());
/* Configure I2S peripheral and Power Amplifier */
BSP_ERROR_CHECK_RETURN_ERR(bsp_audio_init(NULL));
i2s_data_if = bsp_audio_get_codec_itf();
}
assert(i2s_data_if);
audio_codec_i2c_cfg_t i2c_cfg = {
.port = BSP_I2C_NUM,
.addr = ES7210_CODEC_DEFAULT_ADDR,
};
const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
BSP_NULL_CHECK(i2c_ctrl_if, NULL);
es7210_codec_cfg_t es7210_cfg = {
.ctrl_if = i2c_ctrl_if,
};
const audio_codec_if_t *es7210_dev = es7210_codec_new(&es7210_cfg);
BSP_NULL_CHECK(es7210_dev, NULL);
esp_codec_dev_cfg_t codec_es7210_dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = es7210_dev,
.data_if = i2s_data_if,
};
return esp_codec_dev_new(&codec_es7210_dev_cfg);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
这两个函数的第 3~11 行一模一样,用于初始化 I2C 和 I2S 接口。
因为 I2C 接口,我们已经在主函数的最开始处初始化完成了,所以这里的 I2C 初始化函数,就可以删掉了。
再看 I2S 接口,使用 bsp_audio_init()
函数初始化。bsp_audio_init()
函数位于 esp-box-3_idf5.c 中,和上面的 speaker 和 microphone 初始化函数不在一个文件中。bsp_audio_init()
函数中,会初始化一个名称为 i2s_data_if 的数据接口结构体指针,这个变量是个全局变量,定义在了 esp-box-3_idf5.c 文件中,上面两个函数中,第 3~11 行出现的 i2s_data_if,是通过第 3 行的语句,传输到了 esp-box-3.c 文件中。因为在我们例程中,会把 bsp_audio_init()
函数以及 i2s_data_if 的定义,都放到 esp32_s3_szp.c 文件中,所以在 speaker 和 microphone 的初始化中,就不需要传递 i2s_data_if 了,直接使用即可。那第 3~ 11 行,就修改为下面这个样子了:
if (i2s_data_if == NULL) {
/* Configure I2S peripheral and Power Amplifier */
ESP_ERROR_CHECK(bsp_audio_init());
}
assert(i2s_data_if);
2
3
4
5
microphone 初始化函数中,把 ES7210_CODEC_DEFAULT_ADDR 修改为 0x82,因为默认是 0x80,0x82 是根据我们的原理图接线修改的地址。注意,原理图中的地址 0x41 是 7 位地址,这里的 0x82 是 8 位地址,两者表示同一个数据。
另外,把两个函数中的 BSP_NULL_CHECK 修改为 assert,去掉第 2 个参数,保留第 1 个参数。
把 i2s_data_if 定义成全局。
static const audio_codec_data_if_t *i2s_data_if = NULL; /* Codec data interface */
至此,这两个初始化函数就修改好了。
下面是 bsp_audio_init()函数,把它复制到 speaker 和 microphone 初始化函数的前面。
esp_err_t bsp_audio_init(const i2s_std_config_t *i2s_config)
{
esp_err_t ret = ESP_FAIL;
if (i2s_tx_chan && i2s_rx_chan) {
/* Audio was initialized before */
return ESP_OK;
}
/* Setup I2S peripheral */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(CONFIG_BSP_I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
BSP_ERROR_CHECK_RETURN_ERR(i2s_new_channel(&chan_cfg, &i2s_tx_chan, &i2s_rx_chan));
/* Setup I2S channels */
const i2s_std_config_t std_cfg_default = BSP_I2S_DUPLEX_MONO_CFG(22050);
const i2s_std_config_t *p_i2s_cfg = &std_cfg_default;
if (i2s_config != NULL) {
p_i2s_cfg = i2s_config;
}
if (i2s_tx_chan != NULL) {
ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_tx_chan, p_i2s_cfg), err, TAG, "I2S channel initialization failed");
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_tx_chan), err, TAG, "I2S enabling failed");
}
if (i2s_rx_chan != NULL) {
ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_rx_chan, p_i2s_cfg), err, TAG, "I2S channel initialization failed");
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_rx_chan), err, TAG, "I2S enabling failed");
}
audio_codec_i2s_cfg_t i2s_cfg = {
.port = CONFIG_BSP_I2S_NUM,
.rx_handle = i2s_rx_chan,
.tx_handle = i2s_tx_chan,
};
i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg);
BSP_NULL_CHECK_GOTO(i2s_data_if, err);
return ESP_OK;
err:
if (i2s_tx_chan) {
i2s_del_channel(i2s_tx_chan);
}
if (i2s_rx_chan) {
i2s_del_channel(i2s_rx_chan);
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
把其中的两个 CONFIG_BSP_I2S_NUM,修改为 BSP_I2S_NUM,然后在 esp32_s3_szp.h 文件中,把 BSP_I2S_NUM 定义为 I2S_NUM_1。
#define BSP_I2S_NUM I2S_NUM_1
把 BSP_ERROR_CHECK_RETURN_ERR,修改为 ESP-ERROR_CHECK。
把 std_cfg_default 修改一下,把 p_i2s_cfg 的相关内容删除,最后修改后的代码如下:
/* Setup I2S channels */
const i2s_std_config_t std_cfg_default = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000), // 采样率16000
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(32, I2S_SLOT_MODE_STEREO), // 32位 2通道
.gpio_cfg = {
.mclk = GPIO_I2S_MCLK,
.bclk = GPIO_I2S_SCLK,
.ws = GPIO_I2S_LRCK,
.dout = GPIO_I2S_DOUT,
.din = GPIO_I2S_SDIN,
},
};
2
3
4
5
6
7
8
9
10
11
12
把 gpio_cfg 中的这些引脚,根据原理图,在 esp32_s3_szp.h 文件中定义。
#define GPIO_I2S_LRCK (GPIO_NUM_13)
#define GPIO_I2S_MCLK (GPIO_NUM_38)
#define GPIO_I2S_SCLK (GPIO_NUM_14)
#define GPIO_I2S_SDIN (GPIO_NUM_12)
#define GPIO_I2S_DOUT (GPIO_NUM_45)
#define GPIO_PWR_CTRL (GPIO_NUM_NC)
2
3
4
5
6
再往下,有个 BSP_NULL_CHECK_GOTO,它的意思是如果第 1 个参数是 NULL,就执行参数 2 的位置。我们把这个宏定义直接修改成如下代码所示:
if (i2s_data_if == NULL) {
goto err;
}
2
3
i2s_tx_chan 和 i2s_rx_chan,需要在前面定义成全局。
static i2s_chan_handle_t i2s_tx_chan = NULL; // 发送通道
static i2s_chan_handle_t i2s_rx_chan = NULL; // 接收通道
2
修改好的 bsp_audio_init 函数如下所示:
// I2S总线初始化
esp_err_t bsp_audio_init(void)
{
esp_err_t ret = ESP_FAIL;
if (i2s_tx_chan && i2s_rx_chan) {
/* Audio was initialized before */
return ESP_OK;
}
/* Setup I2S peripheral */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(BSP_I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &i2s_tx_chan, &i2s_rx_chan));
/* Setup I2S channels */
const i2s_std_config_t std_cfg_default = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000), // 采样率16000
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(32, I2S_SLOT_MODE_STEREO), // 32位 2通道
.gpio_cfg = {
.mclk = GPIO_I2S_MCLK,
.bclk = GPIO_I2S_SCLK,
.ws = GPIO_I2S_LRCK,
.dout = GPIO_I2S_DOUT,
.din = GPIO_I2S_SDIN,
},
};
if (i2s_tx_chan != NULL) {
ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_tx_chan, &std_cfg_default), err, TAG, "I2S channel initialization failed");
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_tx_chan), err, TAG, "I2S enabling failed");
}
if (i2s_rx_chan != NULL) {
ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_rx_chan, &std_cfg_default), err, TAG, "I2S channel initialization failed");
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_rx_chan), err, TAG, "I2S enabling failed");
}
audio_codec_i2s_cfg_t i2s_cfg = {
.port = BSP_I2S_NUM,
.rx_handle = i2s_rx_chan,
.tx_handle = i2s_tx_chan,
};
i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg);
if (i2s_data_if == NULL) {
goto err;
}
return ESP_OK;
err:
if (i2s_tx_chan) {
i2s_del_channel(i2s_tx_chan);
}
if (i2s_rx_chan) {
i2s_del_channel(i2s_rx_chan);
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
在 bsp_codec_init()
函数中,我们还使用了 bsp_codec_set_fs()
函数。
bsp_codec_set_fs()
函数就在 bsp_codec_init()
函数的前面,如下所示,把它复制到 esp32_s3_szp.c 文件中,放到 bsp_codec_init()
函数前面就可以。
esp_err_t bsp_codec_set_fs(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
esp_err_t ret = ESP_OK;
esp_codec_dev_sample_info_t fs = {
.sample_rate = rate,
.channel = ch,
.bits_per_sample = bits_cfg,
};
if (play_dev_handle) {
ret = esp_codec_dev_close(play_dev_handle);
}
if (record_dev_handle) {
ret |= esp_codec_dev_close(record_dev_handle);
ret |= esp_codec_dev_set_in_gain(record_dev_handle, CODEC_DEFAULT_ADC_VOLUME);
}
if (play_dev_handle) {
ret |= esp_codec_dev_open(play_dev_handle, &fs);
}
if (record_dev_handle) {
ret |= esp_codec_dev_open(record_dev_handle, &fs);
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这个函数不需要修改。
play_dev_handle 和 record_dev_handle 需要在前面定义成全局。
static esp_codec_dev_handle_t play_dev_handle; // speaker句柄
static esp_codec_dev_handle_t record_dev_handle; // microphone句柄
2
至此,音频硬件初始化函数就写好了。
接下来,我们开始写 mp3_player_init()
函数。
打开 esp-box 中的 mp3_demo 例程,在 mp3_demo.c 文件中找到 app_main 函数,复制它的第 361、362、367~375 行这几条语句,放到 mp3_player_init()函数中。
代码如下所示:
// mp3播放器初始化
void mp3_player_init(void)
{
// 获取文件信息
file_iterator = file_iterator_new(SPIFFS_BASE);
assert(file_iterator != NULL);
// 初始化音频播放
player_config.mute_fn = _audio_player_mute_fn;
player_config.write_fn = _audio_player_write_fn;
player_config.clk_set_fn = _audio_player_std_clock;
player_config.priority = 1;
ESP_ERROR_CHECK(audio_player_new(player_config));
ESP_ERROR_CHECK(audio_player_callback_register(_audio_player_callback, NULL));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
该函数使用了两个组件,一个是 esp-audio-player 组件,另外一个是 esp-file-iterator 组件。esp-file-iterator 组件用于从文件系统中获取文件名称。esp-audio-player 组件用于播放 mp3。
esp-audio-player 组件初始化,需要设置 4 个事件处理回调函数,这 4 个函数,都在 mp3_demo.c 文件中,把它们复制到 esp32_s3_szp.c 文件中后,再修改。在 mp3_demo 文件中,这几个函数是既支持 I2S 播放,又支持 USB 播放,我们把这几个函数中,USB 播放的部分删除,保留 I2S 播放的部分。第上一个小节,已经把这几个函数源码列出来并做出了解释。
接着,写一个 music_ui()函数,使用 lvgl 显示音乐播放界面。这个界面,我们也直接使用 mp3_demo 例程中的代码就可以。
在 mp3_demo.c 文件中 app_main 函数的最后,调用了一个 ui_audio_start 函数。这个函数就是用来显示界面的。这个函数位于 ui_audio.c 文件中。我们把它稍微修改一下,修改后的代码如下:
// 播放器界面初始化
void music_ui(void)
{
lvgl_port_lock(0);
ui_button_style_init();// 初始化按键风格
/* 创建播放暂停控制按键 */
lv_obj_t *btn_play_pause = lv_btn_create(lv_scr_act());
lv_obj_align(btn_play_pause, LV_ALIGN_CENTER, 0, 40);
lv_obj_set_size(btn_play_pause, 50, 50);
lv_obj_set_style_radius(btn_play_pause, 25, LV_STATE_DEFAULT);
lv_obj_add_flag(btn_play_pause, LV_OBJ_FLAG_CHECKABLE);
lv_obj_add_style(btn_play_pause, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_pause, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_t *label_play_pause = lv_label_create(btn_play_pause);
lv_label_set_text_static(label_play_pause, LV_SYMBOL_PLAY);
lv_obj_center(label_play_pause);
lv_obj_set_user_data(btn_play_pause, (void *) label_play_pause);
lv_obj_add_event_cb(btn_play_pause, btn_play_pause_cb, LV_EVENT_VALUE_CHANGED, NULL);
/* 创建上一首控制按键 */
lv_obj_t *btn_play_prev = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_play_prev, 50, 50);
lv_obj_set_style_radius(btn_play_prev, 25, LV_STATE_DEFAULT);
lv_obj_clear_flag(btn_play_prev, LV_OBJ_FLAG_CHECKABLE);
lv_obj_align_to(btn_play_prev, btn_play_pause, LV_ALIGN_OUT_LEFT_MID, -40, 0);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_prev, &ui_button_styles()->style_bg, LV_STATE_DEFAULT);
lv_obj_t *label_prev = lv_label_create(btn_play_prev);
lv_label_set_text_static(label_prev, LV_SYMBOL_PREV);
lv_obj_set_style_text_font(label_prev, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label_prev, lv_color_make(0, 0, 0), LV_STATE_DEFAULT);
lv_obj_center(label_prev);
lv_obj_set_user_data(btn_play_prev, (void *) label_prev);
lv_obj_add_event_cb(btn_play_prev, btn_prev_next_cb, LV_EVENT_CLICKED, (void *) false);
/* 创建下一首控制按键 */
lv_obj_t *btn_play_next = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_play_next, 50, 50);
lv_obj_set_style_radius(btn_play_next, 25, LV_STATE_DEFAULT);
lv_obj_clear_flag(btn_play_next, LV_OBJ_FLAG_CHECKABLE);
lv_obj_align_to(btn_play_next, btn_play_pause, LV_ALIGN_OUT_RIGHT_MID, 40, 0);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_focus_no_outline, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_FOCUS_KEY);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_FOCUSED);
lv_obj_add_style(btn_play_next, &ui_button_styles()->style_bg, LV_STATE_DEFAULT);
lv_obj_t *label_next = lv_label_create(btn_play_next);
lv_label_set_text_static(label_next, LV_SYMBOL_NEXT);
lv_obj_set_style_text_font(label_next, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label_next, lv_color_make(0, 0, 0), LV_STATE_DEFAULT);
lv_obj_center(label_next);
lv_obj_set_user_data(btn_play_next, (void *) label_next);
lv_obj_add_event_cb(btn_play_next, btn_prev_next_cb, LV_EVENT_CLICKED, (void *) true);
/* 创建声音调节滑动条 */
lv_obj_t *volume_slider = lv_slider_create(lv_scr_act());
lv_obj_set_size(volume_slider, 200, 10);
lv_obj_set_ext_click_area(volume_slider, 15);
lv_obj_align(volume_slider, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_slider_set_range(volume_slider, 0, 100);
lv_slider_set_value(volume_slider, g_sys_volume, LV_ANIM_ON);
lv_obj_add_event_cb(volume_slider, volume_slider_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t *lab_vol_min = lv_label_create(lv_scr_act());
lv_label_set_text_static(lab_vol_min, LV_SYMBOL_VOLUME_MID);
lv_obj_set_style_text_font(lab_vol_min, &lv_font_montserrat_20, LV_STATE_DEFAULT);
lv_obj_align_to(lab_vol_min, volume_slider, LV_ALIGN_OUT_LEFT_MID, -10, 0);
lv_obj_t *lab_vol_max = lv_label_create(lv_scr_act());
lv_label_set_text_static(lab_vol_max, LV_SYMBOL_VOLUME_MAX);
lv_obj_set_style_text_font(lab_vol_max, &lv_font_montserrat_20, LV_STATE_DEFAULT);
lv_obj_align_to(lab_vol_max, volume_slider, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
/* 创建音乐标题 */
lv_obj_t *lab_title = lv_label_create(lv_scr_act());
lv_obj_set_user_data(lab_title, (void *) btn_play_pause);
lv_label_set_text_static(lab_title, "Scanning Files...");
lv_obj_set_style_text_font(lab_title, &lv_font_montserrat_32, LV_STATE_DEFAULT);
lv_obj_align(lab_title, LV_ALIGN_TOP_MID, 0, 20);
/* 创建音乐列表 */
music_list = lv_dropdown_create(lv_scr_act());
lv_dropdown_clear_options(music_list);
lv_dropdown_set_options_static(music_list, "Scanning...");
lv_obj_set_style_text_font(music_list, &lv_font_montserrat_20, LV_STATE_ANY);
lv_obj_set_width(music_list, 200);
lv_obj_align_to(music_list, lab_title, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);
lv_obj_set_user_data(music_list, (void *) lab_title);
lv_obj_add_event_cb(music_list, music_list_cb, LV_EVENT_VALUE_CHANGED, NULL);
build_file_list(music_list);
lvgl_port_unlock();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
代码只做了一点改变,大家可以对比一下。
接下来,就是修改它们的事件处理函数。它们的事件处理函数,也位于 ui_audio.c 文件中。修改后的事件处理函数代码,在上一小节中,已经给出源码,并作出了解释,大家可以对比一下修改前后的区别。
在 mp3_player_init()函数最后,调用 music_ui()函数。
例程中,play_index()函数中,定义的 filename 是 128 个字节,所以我们需要在 sdkconfig.default 文件中,把 CONFIG_SPIFFS_OBJ_NAME_LEN 定义为 128。
CONFIG_SPIFFS_OBJ_NAME_LEN=128
其它细枝末节的修改,请参考例程源代码,修改的代码,都在 esp32_s3_szp.c 文件和 esp32_s3_szp.h 文件中。