Files
esp32s3_player/main/APP/player/player.c
T

771 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
****************************************************************************************************
* @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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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)
*/
static bool name_is_audio(const char *name)
{
size_t len = strlen(name);
if (len < 5)
{
return false;
}
const char *ext = name + len - 4; /* 指向 ".xxx" */
if (ext[0] != '.')
{
return false;
}
/* .wav */
if (((ext[1] == 'w') || (ext[1] == 'W')) &&
((ext[2] == 'a') || (ext[2] == 'A')) &&
((ext[3] == 'v') || (ext[3] == 'V')))
{
return true;
}
/* .flac(长度至少 5:"a.flac") */
if (len >= 5)
{
const char *f = name + len - 5;
if ((f[0] == '.') &&
((f[1] == 'f') || (f[1] == 'F')) &&
((f[2] == 'l') || (f[2] == 'L')) &&
((f[3] == 'a') || (f[3] == 'A')) &&
((f[4] == 'c') || (f[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;
}
}