/** **************************************************************************************************** * @file player.c * @author HiFi Player Project * @version V1.4 * @date 2026-01-01 * @brief HiFi 播放器核心实现(WAV / FLAC 播放 + I2S 输出) * * 与其他模块的关系: * - 按键由 ui.c 统一消费,再通过 player_request() 投递动作请求到本模块(位标记,无阻塞)。 * - 播放状态写入 s_state(互斥锁保护),播放器界面只读取快照。 * - 只有处于 APP_MODE_PLAYER 时才输出音频;离开该模式自动关闭 I2S 并保留播放位置。 * * 音频格式支持由 audio_source.c 统一处理: * WAV —— 线性 PCM 16/24/32bit(无需解码库,直接取高位) * FLAC —— 原生 FLAC / Ogg FLAC(esphome/micro-flac 解码) * 两者对外都输出 16bit 立体声交错 PCM,因此本文件的播放循环与格式无关。 **************************************************************************************************** */ #include "player.h" #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_heap_caps.h" #include "esp_log.h" #include "nvs.h" #include "ff.h" #include "spi_sd.h" #include "audio_source.h" #include "es8388.h" #include "myi2s.h" #include "xl9555.h" #include "led.h" #define TAG "player" /* ---------------- 播放器参数 ---------------- */ #define MUSIC_DIR "0:/MUSIC" /* SD 卡音乐目录 */ #define MAX_TRACKS 200 /* 最多扫描的曲目数 */ #define AUDIO_CHUNK_FRAMES 1024 /* 每次送给 I2S 的帧数(约 23ms @44.1kHz) */ #define AUDIO_BUF_SIZE (AUDIO_CHUNK_FRAMES * 8) /* 32bit 立体声 = 8 字节/帧 */ #define POLL_MS 20 /* 挂起/暂停时的轮询周期 */ #define TASK_STACK_SIZE (6 * 1024) #define TASK_PRIORITY 6 /* 高于 LVGL 任务,保证音频不断流 */ /* ---------------- 内部类型 ---------------- */ typedef struct { char path[300]; /* 完整路径:0:/MUSIC/xxx.wav|flac */ char name[64]; /* 显示名(去扩展名、仅 ASCII) */ } track_info_t; typedef enum { TRACK_RESULT_END = 0, /* 正常播完 */ TRACK_RESULT_NEXT, /* 用户按了下一曲 / 文件不支持 */ TRACK_RESULT_ERROR, /* 文件 / SD 卡错误 */ } track_result_t; /* ---------------- 全局状态 ---------------- */ static player_state_t s_state; static SemaphoreHandle_t s_lock = NULL; static track_info_t *s_tracks = NULL; static uint8_t *s_audio_buf = NULL; static bool s_audio_ready = false; /* 动作请求:UI 任务写入,播放任务读取(位标记累积) */ static portMUX_TYPE s_req_lock = portMUX_INITIALIZER_UNLOCKED; static volatile uint32_t s_requests = 0; static const uint8_t s_vol_steps[] = {20, 40, 60, 80, 100}; #define VOL_STEP_NUM (sizeof(s_vol_steps) / sizeof(s_vol_steps[0])) static uint8_t s_vol_percent = 60; /* ---------------- 状态访问(互斥锁) ---------------- */ static void state_lock(void) { if (s_lock != NULL) { xSemaphoreTake(s_lock, portMAX_DELAY); } } static void state_unlock(void) { if (s_lock != NULL) { xSemaphoreGive(s_lock); } } static void state_set_message(player_status_t status, const char *message) { state_lock(); s_state.status = status; if (message != NULL) { strncpy(s_state.message, message, sizeof(s_state.message) - 1); s_state.message[sizeof(s_state.message) - 1] = '\0'; } state_unlock(); } static void state_clear_track(void) { state_lock(); s_state.track_name[0] = '\0'; s_state.codec[0] = '\0'; s_state.track_index = 0; s_state.track_total = 0; s_state.cur_sec = 0; s_state.tot_sec = 0; s_state.samplerate = 0; s_state.bits = 0; s_state.channels = 0; s_state.bitrate = 0; state_unlock(); } /* ---------------- 动作请求 ---------------- */ static uint32_t take_requests(void) { uint32_t reqs; taskENTER_CRITICAL(&s_req_lock); reqs = s_requests; s_requests = 0; taskEXIT_CRITICAL(&s_req_lock); return reqs; } void player_request(player_request_t req) { if ((unsigned)req >= PLAYER_REQ_NUM) { return; } taskENTER_CRITICAL(&s_req_lock); s_requests |= (1u << (uint32_t)req); taskEXIT_CRITICAL(&s_req_lock); } /* ---------------- 音量 ---------------- */ static void volume_apply(void) { uint32_t reg = (uint32_t)s_vol_percent * 33 / 100; if (reg > 33) { reg = 33; } if (s_audio_ready) { es8388_hpvol_set((uint8_t)reg); /* 耳机音量 */ es8388_spkvol_set((uint8_t)reg); /* 喇叭音量 */ } state_lock(); s_state.volume = s_vol_percent; state_unlock(); } static void volume_load(void) { nvs_handle_t handle; uint8_t value = 0; if (nvs_open("hifi", NVS_READONLY, &handle) == ESP_OK) { if ((nvs_get_u8(handle, "vol", &value) == ESP_OK) && (value <= 100)) { s_vol_percent = value; } nvs_close(handle); } } static void volume_save(void) { nvs_handle_t handle; if (nvs_open("hifi", NVS_READWRITE, &handle) == ESP_OK) { nvs_set_u8(handle, "vol", s_vol_percent); nvs_commit(handle); nvs_close(handle); } } static void volume_next(void) { uint32_t index = 0; for (uint32_t i = 0; i < VOL_STEP_NUM; i++) { if (s_vol_percent == s_vol_steps[i]) { index = i; break; } } index = (index + 1) % VOL_STEP_NUM; s_vol_percent = s_vol_steps[index]; volume_apply(); volume_save(); ESP_LOGI(TAG, "volume -> %u%%", s_vol_percent); } /* ---------------- 音频硬件 ---------------- */ static bool audio_init(void) { if (es8388_init() != 0) { ESP_LOGE(TAG, "ES8388 init failed"); return false; } s_audio_ready = true; es8388_adda_cfg(1, 0); /* 打开 DAC,关闭 ADC */ es8388_input_cfg(0); /* 关闭录音输入 */ es8388_output_cfg(1, 1); /* 耳机 + 喇叭通道都打开 */ es8388_3d_set(0); /* 关闭 3D 环绕 */ es8388_i2s_cfg(0, 3); /* Philips I2S,16bit */ volume_apply(); if (myi2s_init() != ESP_OK) { return false; } i2s_trx_stop(); /* 空闲时先关闭通道 */ xl9555_pin_write(SPK_EN_IO, 0); /* 打开板载喇叭功放(低电平有效) */ vTaskDelay(pdMS_TO_TICKS(50)); return true; } /** * @brief 按源文件的采样率与位深配置 I2S / ES8388 * @param sample_rate 采样率(Hz) * @param bits I2S 输出位宽:16 / 24 / 32 * @note 位宽不同会重新配置 I2S 的 data_bit_width / ws_width 与 ES8388 的 DAC 字长, * 因此 16bit、24bit、32bit 文件之间切换都是“真正按格式输出”,不做降位。 */ static void audio_set_format(uint32_t sample_rate, uint16_t bits) { uint8_t es_len; /* ES8388 R23 的数据字长字段 */ switch (bits) { case 24: es_len = 0; /* 24bit */ break; case 32: es_len = 4; /* 32bit */ break; case 16: default: es_len = 3; /* 16bit */ bits = 16; break; } es8388_i2s_cfg(0, es_len); /* Philips I2S + 对应字长 */ i2s_set_samplerate_bits_sample((int)sample_rate, (int)bits); /* 通道保持关闭,由播放循环在真正要出声时再 i2s_trx_start() */ ESP_LOGI(TAG, "audio: %u Hz / %u bit", (unsigned)sample_rate, bits); } /* ---------------- 曲目扫描 ---------------- */ /** * @brief 判断是否为可播放的音频文件(.wav / .flac) * @note 两种扩展名都要检查,不能因为不是 .wav 就提前 return false, * 否则 .flac 永远被过滤掉。 */ static bool name_is_audio(const char *name) { size_t len = strlen(name); if (len < 5) { return false; } const char *ext = name + len - 4; /* 指向最后 4 个字符 */ /* .wav —— 最后 4 个字符为 ".wav" */ if ((ext[0] == '.') && ((ext[1] == 'w') || (ext[1] == 'W')) && ((ext[2] == 'a') || (ext[2] == 'A')) && ((ext[3] == 'v') || (ext[3] == 'V'))) { return true; } /* .flac —— 最后 5 个字符为 ".flac" */ const char *ext5 = name + len - 5; if ((ext5[0] == '.') && ((ext5[1] == 'f') || (ext5[1] == 'F')) && ((ext5[2] == 'l') || (ext5[2] == 'L')) && ((ext5[3] == 'a') || (ext5[3] == 'A')) && ((ext5[4] == 'c') || (ext5[4] == 'C'))) { return true; } return false; } /** * @brief 生成用于 LVGL 显示的曲目名(去掉扩展名,非 ASCII 字符用 '?' 代替) */ static void make_display_name(const char *fname, char *out, size_t out_size) { size_t len = strlen(fname); size_t end = len; size_t j = 0; bool last_unknown = false; for (size_t i = len; i > 0; i--) { if (fname[i - 1] == '.') { end = i - 1; break; } } for (size_t i = 0; (i < end) && (j + 1 < out_size); i++) { unsigned char c = (unsigned char)fname[i]; if ((c >= 0x20) && (c < 0x7F)) { out[j++] = (char)c; last_unknown = (c == '?'); } else if (!last_unknown) { /* 中文文件名是 GBK 编码,LVGL 内置字体无法显示,统一用 '?' 占位 */ out[j++] = '?'; last_unknown = true; } } out[j] = '\0'; } static int playlist_scan(void) { FF_DIR dir; FILINFO *info = NULL; FRESULT res; int count = 0; if (s_tracks == NULL) { return 0; } info = (FILINFO *)malloc(sizeof(FILINFO)); if (info == NULL) { ESP_LOGE(TAG, "no memory for FILINFO"); return 0; } res = f_opendir(&dir, MUSIC_DIR); if (res == FR_OK) { while (count < MAX_TRACKS) { res = f_readdir(&dir, info); if ((res != FR_OK) || (info->fname[0] == '\0')) { break; } if (info->fattrib & AM_DIR) { continue; } if (!name_is_audio(info->fname)) { continue; } snprintf(s_tracks[count].path, sizeof(s_tracks[count].path), "%s/%s", MUSIC_DIR, info->fname); make_display_name(info->fname, s_tracks[count].name, sizeof(s_tracks[count].name)); count++; } f_closedir(&dir); } else { ESP_LOGW(TAG, "open %s failed (%d)", MUSIC_DIR, res); } free(info); state_lock(); s_state.track_total = (int16_t)count; state_unlock(); ESP_LOGI(TAG, "found %d audio file(s) in %s", count, MUSIC_DIR); return count; } /* ---------------- 单曲播放 ---------------- */ /** * @brief 播放一首曲子(WAV / FLAC 通用) * @retval TRACK_RESULT_END 正常播完 * TRACK_RESULT_NEXT 需要跳到下一首(用户操作或不支持的文件) * TRACK_RESULT_ERROR 文件系统 / SD 卡错误 */ static track_result_t play_track(int index) { audio_source_t *src = NULL; audio_info_t info; track_result_t result = TRACK_RESULT_END; bool paused = false; bool audio_running = false; bool active = (app_get_mode() == APP_MODE_PLAYER); /* 是否停留在播放器界面 */ uint32_t last_sec = 0xFFFFFFFF; if ((s_audio_buf == NULL) || (s_tracks == NULL) || (index < 0) || (index >= MAX_TRACKS) || (s_tracks[index].path[0] == '\0')) { return TRACK_RESULT_ERROR; } memset(&info, 0, sizeof(info)); src = audio_source_open(s_tracks[index].path, &info); if (src == NULL) { /* 不支持的格式或文件损坏:跳过,继续下一首 */ ESP_LOGW(TAG, "cannot open %s", s_tracks[index].path); state_set_message(PLAYER_STATUS_ERROR, "UNSUPPORTED FILE"); return TRACK_RESULT_NEXT; } /* ---------------- 更新界面状态 ---------------- */ state_lock(); s_state.track_index = (int16_t)index; strncpy(s_state.track_name, s_tracks[index].name, sizeof(s_state.track_name) - 1); s_state.track_name[sizeof(s_state.track_name) - 1] = '\0'; snprintf(s_state.codec, sizeof(s_state.codec), "%s", (info.codec == AUDIO_CODEC_FLAC) ? "FLAC" : "WAV"); s_state.samplerate = (uint16_t)info.sample_rate; s_state.bits = (uint8_t)info.bits; s_state.i2s_bits = (uint8_t)info.out_bits; s_state.channels = (uint8_t)info.channels; s_state.bitrate = (uint32_t)info.sample_rate * info.channels * info.out_bits; s_state.cur_sec = 0; s_state.tot_sec = info.total_sec; s_state.status = active ? PLAYER_STATUS_PLAYING : PLAYER_STATUS_PAUSED; strncpy(s_state.message, active ? "PLAYING" : "READY", sizeof(s_state.message) - 1); s_state.message[sizeof(s_state.message) - 1] = '\0'; state_unlock(); ESP_LOGI(TAG, "load [%d] %s (%s %u Hz, %u bit -> I2S %u bit, %u ch, %u s)", index + 1, s_tracks[index].name, (info.codec == AUDIO_CODEC_FLAC) ? "FLAC" : "WAV", (unsigned)info.sample_rate, info.bits, info.out_bits, info.channels, (unsigned)info.total_sec); /* ---------------- 配置音频(按源文件采样率 + 原生位深) ---------------- */ audio_set_format(info.sample_rate, info.out_bits); while (1) { /* ---- 1. 只有停留在播放器界面才输出音频 ---- */ if (app_get_mode() != APP_MODE_PLAYER) { if (audio_running) { i2s_trx_stop(); audio_running = false; } if (active) /* 刚从播放器返回主界面 */ { active = false; LED0(0); state_set_message(PLAYER_STATUS_PAUSED, "PAUSED"); } vTaskDelay(pdMS_TO_TICKS(POLL_MS)); /* 后台挂起,不占用 CPU */ continue; } if (!active) /* 从主界面重新进入播放器 */ { active = true; paused = false; take_requests(); /* 丢弃进入播放器前残留的请求 */ state_set_message(PLAYER_STATUS_PLAYING, "PLAYING"); } /* ---- 2. 处理 UI 投递的请求 ---- */ uint32_t reqs = take_requests(); bool quit_track = false; if (reqs & (1u << PLAYER_REQ_NEXT)) { result = TRACK_RESULT_NEXT; quit_track = true; } if (!quit_track && (reqs & (1u << PLAYER_REQ_PLAY_PAUSE))) { paused = !paused; state_set_message(paused ? PLAYER_STATUS_PAUSED : PLAYER_STATUS_PLAYING, paused ? "PAUSED" : "PLAYING"); } if (!quit_track && (reqs & (1u << PLAYER_REQ_VOLUME))) { volume_next(); } if (quit_track) { break; } if (app_get_mode() != APP_MODE_PLAYER) { continue; /* 已切回主界面 */ } /* ---- 3. 暂停时不输出音频,但保持解码位置 ---- */ bool should_run = !paused; if (should_run && !audio_running) { i2s_trx_start(); audio_running = true; LED0(1); } else if (!should_run && audio_running) { i2s_trx_stop(); audio_running = false; LED0(0); } if (!should_run) { vTaskDelay(pdMS_TO_TICKS(POLL_MS)); continue; } /* ---- 4. 解一块 PCM(WAV 直读 / FLAC 解码,立体声 + 源文件原生位深) ---- */ int frames = audio_source_read(src, (void *)s_audio_buf, AUDIO_CHUNK_FRAMES); if (frames < 0) { ESP_LOGE(TAG, "decode/read error"); result = TRACK_RESULT_ERROR; break; } if (frames == 0) { result = TRACK_RESULT_END; break; } /* 每帧 = 左右声道 × out_bytes 字节(16bit→4,24bit→6,32bit→8) */ i2s_tx_write(s_audio_buf, (uint32_t)frames * 2u * info.out_bytes); /* ---- 5. 更新播放进度 ---- */ uint32_t sec = audio_source_elapsed_sec(src); if (sec != last_sec) { last_sec = sec; state_lock(); s_state.cur_sec = sec; state_unlock(); } } /* 关闭 I2S,避免切歌/退出时产生杂音 */ if (audio_running) { i2s_trx_stop(); } LED0(0); audio_source_close(src); return result; } /* ---------------- 播放器主任务 ---------------- */ static void player_task(void *arg) { bool sd_ready = false; bool audio_ok = false; int index = 0; (void)arg; audio_ok = audio_init(); if (!audio_ok) { state_set_message(PLAYER_STATUS_ERROR, "AUDIO INIT FAILED"); } s_audio_buf = (uint8_t *)heap_caps_malloc(AUDIO_BUF_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); s_tracks = (track_info_t *)calloc(MAX_TRACKS, sizeof(track_info_t)); if ((s_audio_buf == NULL) || (s_tracks == NULL)) { ESP_LOGE(TAG, "no memory for player buffers"); state_set_message(PLAYER_STATUS_ERROR, "OUT OF MEMORY"); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "player task started"); while (1) { /* ---------------- 0. 空闲时也响应音量(不依赖 SD 卡) ---------------- */ uint32_t idle_reqs = take_requests(); if (idle_reqs & (1u << PLAYER_REQ_VOLUME)) { volume_next(); } /* ---------------- 1. 音频硬件异常 ---------------- */ if (!audio_ok) { state_set_message(PLAYER_STATUS_ERROR, "AUDIO INIT FAILED"); vTaskDelay(pdMS_TO_TICKS(2000)); continue; } /* ---------------- 2. 挂载 SD 卡 ---------------- */ if (!sd_ready) { state_set_message(PLAYER_STATUS_BOOT, "MOUNTING SD CARD..."); if (sd_spi_init() == ESP_OK) { sd_ready = true; ESP_LOGI(TAG, "sd card mounted"); } else { state_clear_track(); state_set_message(PLAYER_STATUS_NO_SD, "NO SD CARD"); vTaskDelay(pdMS_TO_TICKS(1000)); continue; } } /* ---------------- 3. 扫描曲目 ---------------- */ int total = playlist_scan(); if (total == 0) { state_clear_track(); state_set_message(PLAYER_STATUS_NO_MUSIC, "PUT WAV/FLAC IN /MUSIC"); vTaskDelay(pdMS_TO_TICKS(1500)); continue; } if (index >= total) { index = 0; } /* ---------------- 4. 播放当前曲目 ---------------- */ track_result_t result = play_track(index); if ((result == TRACK_RESULT_NEXT) || (result == TRACK_RESULT_END)) { index = (index + 1) % total; /* 循环播放列表(没有上一首) */ } else { /* 文件系统 / SD 卡异常:重新挂载 */ sd_ready = false; state_clear_track(); state_set_message(PLAYER_STATUS_NO_SD, "SD CARD ERROR"); vTaskDelay(pdMS_TO_TICKS(500)); } } } /* ---------------- 对外接口 ---------------- */ void player_start(void) { if (s_lock == NULL) { s_lock = xSemaphoreCreateMutex(); if (s_lock == NULL) { ESP_LOGE(TAG, "create mutex failed"); } } volume_load(); memset(&s_state, 0, sizeof(s_state)); s_state.status = PLAYER_STATUS_BOOT; s_state.volume = s_vol_percent; snprintf(s_state.message, sizeof(s_state.message), "STARTING..."); if (xTaskCreate(player_task, "player", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL) != pdPASS) { ESP_LOGE(TAG, "create player task failed"); } } void player_get_state(player_state_t *out) { if (out == NULL) { return; } if ((s_lock != NULL) && (xSemaphoreTake(s_lock, pdMS_TO_TICKS(50)) == pdTRUE)) { *out = s_state; xSemaphoreGive(s_lock); } else { *out = s_state; } }