HiFi player project, first commit.
This commit is contained in:
commit
4126ea13a1
49 files changed
+6333
No files matched your search
@@ -0,0 +1,770 @@
|
||||
/**
|
||||
****************************************************************************************************
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user