HiFi player project, first commit.

This commit is contained in:
francis.wang committed 2026-10-26 20:07:12 +08:00
commit 4126ea13a1
49 files changed
+6333

No files matched your search

+450
View File
@@ -0,0 +1,450 @@
/**
****************************************************************************************************
* @file audio_source.c
* @author HiFi Player Project
* @version V1.5
* @date 2026-01-01
* @brief 统一音源实现(WAV 解析 + FLAC 解码转发)
*
* 两种格式都输出「立体声 + 源文件原生位深」的 PCM,不做降位:
*
* WAV :f_read 原始 PCM ──▶ 单声道则复制 ──▶ 输出
* FLAC :flac_dec_read() ──▶ 已在内部做声道规整 + 原生位深输出
*
* 格式识别:读取文件头 4 字节
* "RIFF" → WAV
* "fLaC" → 原生 FLAC
* "OggS" → Ogg FLAC
****************************************************************************************************
*/
#include "audio_source.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "ff.h"
#include "flac_dec.h"
#define TAG "audio_src"
#define WAV_MAX_BLOCKALIGN 8 /* 支持的 WAV 帧对齐上限(16/24/32bit × 最多 2 声道) */
#define WAV_MAX_FRAMES 1024 /* WAV 原始 PCM 中转缓冲容纳的最大帧数 */
/* ---------------- 内部结构 ---------------- */
struct audio_source {
audio_codec_t codec;
audio_info_t info;
uint64_t frames_read; /* 已输出帧数(用于计算进度) */
/* ---- WAV ---- */
FIL wav_fp;
bool wav_open;
uint16_t block_align; /* 源每帧字节数 = channels * out_bytes */
uint32_t byte_rate;
uint32_t data_offset;
uint32_t data_size;
uint32_t data_read; /* 已读字节数 */
uint8_t *scratch; /* 原始 PCM 中转缓冲 */
int scratch_frames; /* 缓冲能容纳的帧数 */
/* ---- FLAC ---- */
flac_dec_t *flac;
};
/* ---------------- 位宽映射 ---------------- */
/**
* @brief 源位深 → I2S 输出位宽
*/
static inline uint16_t pick_out_bits(uint16_t bits)
{
if (bits <= 16)
{
return 16;
}
if (bits <= 24)
{
return 24;
}
return 32;
}
/* ---------------- WAV 解析 ---------------- */
static inline uint16_t rd_u16(const uint8_t *p)
{
return (uint16_t)(p[0] | (p[1] << 8));
}
static inline uint32_t rd_u32(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
/**
* @brief 解析 WAV 头部(支持 fmt/data 之间夹着 LIST、fact 等块)
* @note 调用前文件指针需在文件开头;结束后文件指针位于 data 起始处
*/
static bool wav_parse(FIL *fp, audio_info_t *info, uint32_t *data_offset,
uint32_t *data_size, uint16_t *block_align, uint32_t *byte_rate,
uint16_t *bits, uint16_t *channels)
{
uint8_t header[12];
uint8_t chunk[8];
uint8_t fmt[40];
UINT br = 0;
bool have_fmt = false;
bool have_data = false;
uint16_t audio_format = 0;
if ((f_read(fp, header, sizeof(header), &br) != FR_OK) || (br != sizeof(header)))
{
return false;
}
if ((memcmp(header, "RIFF", 4) != 0) || (memcmp(header + 8, "WAVE", 4) != 0))
{
return false;
}
while (!have_data)
{
if ((f_read(fp, chunk, sizeof(chunk), &br) != FR_OK) || (br != sizeof(chunk)))
{
break;
}
uint32_t size = rd_u32(chunk + 4);
if (memcmp(chunk, "fmt ", 4) == 0)
{
if ((size < 16) || (size > sizeof(fmt)))
{
break;
}
if ((f_read(fp, fmt, size, &br) != FR_OK) || (br != size))
{
break;
}
audio_format = rd_u16(fmt);
*channels = rd_u16(fmt + 2);
info->sample_rate = rd_u32(fmt + 4);
*byte_rate = rd_u32(fmt + 8);
*block_align = rd_u16(fmt + 12);
*bits = rd_u16(fmt + 14);
if ((audio_format == 0xFFFE) && (size >= 26))
{
/* WAVE_FORMAT_EXTENSIBLE:真实格式在 SubFormat GUID 的前两个字节 */
audio_format = rd_u16(fmt + 24);
}
have_fmt = true;
}
else if (memcmp(chunk, "data", 4) == 0)
{
FSIZE_t offset = f_tell(fp);
FSIZE_t remain = f_size(fp) - offset;
*data_offset = (uint32_t)offset;
/* 有些录音文件 data 长度写 0 或 0xFFFFFFFF,此时用文件剩余长度 */
*data_size = ((size == 0) || (size > (uint32_t)remain)) ? (uint32_t)remain : size;
have_data = true;
break;
}
else
{
/* 跳过不关心的块(块长度 2 字节对齐) */
if (f_lseek(fp, f_tell(fp) + size + (size & 0x01)) != FR_OK)
{
break;
}
}
}
if (!have_fmt || !have_data)
{
return false;
}
if (*block_align == 0)
{
*block_align = (uint16_t)(((*bits) / 8) * (*channels));
}
if (*byte_rate == 0)
{
*byte_rate = info->sample_rate * (*block_align);
}
/* 只接受线性 PCM,位深需为 16/24/32(非整字节位深无法从 WAV 头可靠判断) */
return (audio_format == 1) &&
(*channels >= 1) && (*channels <= 2) &&
(*block_align > 0) && (*block_align <= WAV_MAX_BLOCKALIGN) &&
((*bits == 16) || (*bits == 24) || (*bits == 32)) &&
(info->sample_rate >= 8000) && (*byte_rate > 0);
}
/**
* @brief WAV:把原始 PCM 规整成立体声(保留原生位深)
*/
static int wav_read(audio_source_t *src, void *out, int max_frames)
{
if ((src->data_read >= src->data_size) || (max_frames <= 0))
{
return 0;
}
uint32_t remain_frames = (src->data_size - src->data_read) / src->block_align;
uint32_t frames = (remain_frames < (uint32_t)max_frames) ? remain_frames : (uint32_t)max_frames;
if ((int)frames > src->scratch_frames)
{
frames = (uint32_t)src->scratch_frames;
}
if (frames == 0)
{
return 0;
}
UINT br = 0;
size_t want = (size_t)frames * src->block_align;
if (f_read(&src->wav_fp, src->scratch, (UINT)want, &br) != FR_OK)
{
return -1;
}
if (br < src->block_align)
{
return 0; /* 不足一帧,视作结束 */
}
frames = br / src->block_align;
src->data_read += br;
/* ---- 声道规整(位深保持不变) ---- */
const uint8_t *s = src->scratch;
uint8_t *dst = (uint8_t *)out;
const size_t ob = src->info.out_bytes;
if (src->info.channels == 2)
{
/* 最常见路径:立体声直接拷贝(block_align == 2 * out_bytes) */
memcpy(dst, s, (size_t)frames * 2 * ob);
}
else
{
/* 单声道:左右都写同一样本 */
for (uint32_t i = 0; i < frames; i++)
{
uint8_t *o = dst + i * 2 * ob;
memcpy(o, s + i * ob, ob);
memcpy(o + ob, s + i * ob, ob);
}
}
src->frames_read += frames;
return (int)frames;
}
/* ---------------- 对外接口 ---------------- */
audio_source_t *audio_source_open(const char *path, audio_info_t *info)
{
if (path == NULL)
{
return NULL;
}
audio_source_t *src = (audio_source_t *)calloc(1, sizeof(audio_source_t));
if (src == NULL)
{
return NULL;
}
/* ---------------- 1. 打开文件并嗅探文件头 ---------------- */
if (f_open(&src->wav_fp, path, FA_READ) != FR_OK)
{
ESP_LOGE(TAG, "open %s failed", path);
audio_source_close(src);
return NULL;
}
src->wav_open = true;
uint8_t magic[4] = {0};
UINT br = 0;
bool is_flac = false;
if ((f_read(&src->wav_fp, magic, sizeof(magic), &br) == FR_OK) && (br == sizeof(magic)))
{
is_flac = (memcmp(magic, "fLaC", 4) == 0) || (memcmp(magic, "OggS", 4) == 0);
}
/* ---------------- 2A. FLAC ---------------- */
if (is_flac)
{
/* 交给专门的解码器重新打开(它需要从文件开头开始解析) */
f_close(&src->wav_fp);
src->wav_open = false;
flac_info_t fi;
src->flac = flac_dec_open(path);
if ((src->flac == NULL) || (flac_dec_get_info(src->flac, &fi) != 0))
{
ESP_LOGE(TAG, "flac open failed: %s", path);
audio_source_close(src);
return NULL;
}
src->codec = AUDIO_CODEC_FLAC;
src->info.codec = AUDIO_CODEC_FLAC;
src->info.sample_rate = fi.sample_rate;
src->info.bits = fi.bits;
src->info.channels = fi.channels;
src->info.out_bytes = fi.out_bytes;
src->info.out_bits = (uint16_t)fi.out_bytes * 8;
src->info.total_sec = (fi.total_samples > 0) ?
(uint32_t)(fi.total_samples / fi.sample_rate) : 0;
if (info != NULL)
{
*info = src->info;
}
ESP_LOGI(TAG, "%s: FLAC %u Hz, %u bit -> I2S %u bit, %u ch, %u s",
path, (unsigned)src->info.sample_rate, src->info.bits,
src->info.out_bits, src->info.channels,
(unsigned)src->info.total_sec);
return src;
}
/* ---------------- 2B. WAV ---------------- */
src->codec = AUDIO_CODEC_WAV;
src->info.codec = AUDIO_CODEC_WAV;
f_lseek(&src->wav_fp, 0); /* 回到文件开头重新解析 */
uint16_t bits = 0;
uint16_t channels = 0;
if (!wav_parse(&src->wav_fp, &src->info, &src->data_offset, &src->data_size,
&src->block_align, &src->byte_rate, &bits, &channels))
{
ESP_LOGW(TAG, "unsupported wav: %s", path);
audio_source_close(src);
return NULL;
}
src->info.bits = bits;
src->info.channels = channels;
src->info.out_bits = pick_out_bits(bits);
src->info.out_bytes = (uint8_t)(bits / 8); /* WAV 位深必为 16/24/32 */
src->info.total_sec = src->data_size / src->byte_rate;
/* 原始 PCM 中转缓冲(16/24/32bit × 最多 2 声道 → 每帧最多 8 字节) */
src->scratch = (uint8_t *)malloc((size_t)WAV_MAX_FRAMES * src->block_align);
if (src->scratch == NULL)
{
ESP_LOGE(TAG, "no memory for wav scratch");
audio_source_close(src);
return NULL;
}
src->scratch_frames = WAV_MAX_FRAMES;
f_lseek(&src->wav_fp, src->data_offset);
if (info != NULL)
{
*info = src->info;
}
ESP_LOGI(TAG, "%s: WAV %u Hz, %u bit -> I2S %u bit, %u ch, %u s",
path, (unsigned)src->info.sample_rate, src->info.bits,
src->info.out_bits, src->info.channels,
(unsigned)src->info.total_sec);
return src;
}
int audio_source_read(audio_source_t *src, void *out, int max_frames)
{
if ((src == NULL) || (out == NULL) || (max_frames <= 0))
{
return -1;
}
if (src->codec == AUDIO_CODEC_FLAC)
{
int n = flac_dec_read(src->flac, out, max_frames);
if (n > 0)
{
src->frames_read += (uint64_t)n;
}
return n;
}
return wav_read(src, out, max_frames);
}
uint32_t audio_source_elapsed_sec(const audio_source_t *src)
{
if ((src == NULL) || (src->info.sample_rate == 0))
{
return 0;
}
uint32_t sec = (uint32_t)(src->frames_read / src->info.sample_rate);
if ((src->info.total_sec > 0) && (sec > src->info.total_sec))
{
sec = src->info.total_sec;
}
return sec;
}
void audio_source_close(audio_source_t *src)
{
if (src == NULL)
{
return;
}
if (src->flac != NULL)
{
flac_dec_close(src->flac);
src->flac = NULL;
}
if (src->wav_open)
{
f_close(&src->wav_fp);
src->wav_open = false;
}
free(src->scratch);
src->scratch = NULL;
free(src);
}
+86
View File
@@ -0,0 +1,86 @@
/**
****************************************************************************************************
* @file audio_source.h
* @author HiFi Player Project
* @version V1.5
* @date 2026-01-01
* @brief 统一音源接口(WAV / FLAC)
*
* 上层播放器只关心“给我一块立体声 PCM 和它对应的位宽”,不必区分容器格式:
*
* audio_source_open() ── 按文件头自动识别 RIFF(WAV) / fLaC、OggS(FLAC)
* │ 同时给出该曲目应使用的 I2S 位宽 out_bits(16/24/32)
* audio_source_read() ── 输出 **源文件原生位深** 的立体声交错 PCM
* │ 单声道自动复制;>2 声道只取前两声道;不做降位
* audio_source_close()
*
* 时间进度统一用“已播放帧数 / 采样率”计算,对两种格式都准确。
*
* 位宽映射(源位深 → I2S 位宽):
* ≤16bit → 16bit(每采样 2 字节)
* 17~24bit → 24bit(每采样 3 字节)
* 25~32bit → 32bit(每采样 4 字节)
* 非整字节位深(如 20bit)按左对齐放入更宽的槽位,音量不受影响。
****************************************************************************************************
*/
#ifndef __AUDIO_SOURCE_H
#define __AUDIO_SOURCE_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** 支持的音频容器 */
typedef enum {
AUDIO_CODEC_WAV = 0, /* RIFF/WAVE,PCM 16/24/32bit */
AUDIO_CODEC_FLAC, /* 原生 FLAC 或 Ogg FLAC */
} audio_codec_t;
/** 音源信息 */
typedef struct {
audio_codec_t codec; /* 容器类型 */
uint32_t sample_rate; /* 采样率 → 用于配置 I2S 频率 */
uint16_t bits; /* 源位深(界面显示用) */
uint16_t channels; /* 源声道数 */
uint16_t out_bits; /* I2S 输出位宽:16 / 24 / 32 */
uint8_t out_bytes; /* 输出每采样字节数:2 / 3 / 4 */
uint32_t total_sec; /* 总时长(秒),0 表示未知 */
} audio_info_t;
typedef struct audio_source audio_source_t;
/**
* @brief 打开音频文件并解析头部
* @param path 文件路径
* @param info 输出音源信息(可为 NULL)
* @retval 句柄;失败返回 NULL
*/
audio_source_t *audio_source_open(const char *path, audio_info_t *info);
/**
* @brief 读取一块 PCM
* @param src 音源句柄
* @param out 输出缓冲区(立体声交错,位宽 = info.out_bits)
* @param max_frames 最多读取多少帧(1 帧 = 左右各 1 个采样)
* @retval >0 实际帧数
* 0 播放结束
* <0 解码/读取错误
* @note 每帧占用 2 * out_bytes 字节,调用者需保证缓冲区足够
*/
int audio_source_read(audio_source_t *src, void *out, int max_frames);
/** @brief 已播放的秒数(按解码帧数计算) */
uint32_t audio_source_elapsed_sec(const audio_source_t *src);
/** @brief 关闭音源并释放资源(可传 NULL) */
void audio_source_close(audio_source_t *src);
#ifdef __cplusplus
}
#endif
#endif /* __AUDIO_SOURCE_H */
+578
View File
@@ -0,0 +1,578 @@
/**
****************************************************************************************************
* @file flac_dec.cpp
* @author HiFi Player Project
* @version V1.5
* @date 2026-01-01
* @brief FLAC 解码器的 C++ 实现(对外暴露纯 C 接口,见 flac_dec.h)
*
* 数据流:
* FATFS 文件 ──f_read──▶ 8KB 码流缓冲 ──decode()──▶ 原生位深交错 PCM
* │
* 单声道复制 / 多声道取前两声道
* ▼
* 立体声 + 原生位深(左对齐)→ I2S
*
* 位的处理:
* 库的 uint8_t 输出重载是“原生字节打包 + 左对齐”:
* 16bit → 2 字节、24bit → 3 字节、32bit → 4 字节,
* 20bit 这类非整字节位深则占 3 字节并填满其高 20 位。
* 正好等价于 I2S 的 16/24/32bit 数据格式,所以这里不做任何降位,
* 上层按 out_bytes 配置 I2S 位宽即可获得无损通路。
*
* 码流缓冲策略:未消费数据前移 + 追加读取,保证 NEED_MORE_DATA 时不丢数据,
* 并带“无进展”保护,坏文件不会死循环。
****************************************************************************************************
*/
#include "flac_dec.h"
#include <cstdlib>
#include <cstring>
#include <new>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "micro_flac/flac_decoder.h"
#include "ff.h"
#define TAG "flac_dec"
#define IN_BUF_SIZE (8 * 1024) /* 码流缓冲(压缩数据) */
#define MAX_STALL 8 /* 连续无进展的容忍次数 */
struct flac_dec {
FIL fp; /* FATFS 文件对象 */
bool fp_open;
micro_flac::FLACDecoder *decoder;
uint8_t *in_buf; /* 压缩码流缓冲 */
size_t in_len; /* 缓冲内有效字节数 */
size_t in_pos; /* 已消费位置 */
bool in_eof; /* 文件已读完 */
uint8_t *pcm; /* 解码输出(源声道数 × 原生位深,交错) */
size_t pcm_capacity; /* 缓冲字节数 */
size_t pcm_bytes; /* 本帧有效字节数 */
size_t pcm_pos; /* 已消费字节位置 */
uint8_t bps; /* 源每采样字节数 */
uint8_t out_bytes; /* 输出每采样字节数(2/3/4) */
flac_info_t info;
bool header_ready;
bool stream_end;
bool failed;
};
/* ================================================================================================
* 内部工具
* ============================================================================================== */
/**
* @brief 按 STREAMINFO 的块大小分配解码输出缓冲(字节数)
* @retval true 成功
*/
static bool ensure_pcm_buffer(flac_dec_t *d, size_t bytes)
{
if (bytes <= d->pcm_capacity)
{
return true;
}
/* 优先放 PSRAM,失败再退回内部 RAM */
void *p = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM);
if (p == NULL)
{
ESP_LOGW(TAG, "psram alloc failed (%u bytes), try internal", (unsigned)bytes);
p = malloc(bytes);
}
if (p == NULL)
{
ESP_LOGE(TAG, "no memory for pcm buffer (%u bytes)", (unsigned)bytes);
return false;
}
free(d->pcm); /* heap_caps_malloc 的指针可以用 free 释放 */
d->pcm = (uint8_t *)p;
d->pcm_capacity = bytes;
return true;
}
/**
* @brief 把未消费的码流前移,并从文件追加新数据
* @retval true 操作成功(不代表一定有新数据;看 in_len 是否增长)
* false 读文件失败
*/
static bool feed_more(flac_dec_t *d)
{
size_t remain = d->in_len - d->in_pos;
if ((d->in_pos > 0) && (remain > 0))
{
memmove(d->in_buf, d->in_buf + d->in_pos, remain);
}
d->in_len = remain;
d->in_pos = 0;
if (d->in_eof || (d->in_len >= IN_BUF_SIZE))
{
return true; /* 文件读完或缓冲已满,只能靠现有数据 */
}
UINT br = 0;
FRESULT fr = f_read(&d->fp, d->in_buf + d->in_len, (UINT)(IN_BUF_SIZE - d->in_len), &br);
if (fr != FR_OK)
{
ESP_LOGE(TAG, "f_read failed (%d)", (int)fr);
return false;
}
d->in_len += br;
if (br == 0)
{
d->in_eof = true;
}
return true;
}
/** @brief 是否还有可交给解码器的数据 */
static inline bool has_input(const flac_dec_t *d)
{
return d->in_pos < d->in_len;
}
/**
* @brief 按源位深推导输出每采样字节数
* @note 1 字节(8bit 及以下)统一扩展成 2 字节(左对齐),
* 其余直接沿用源字节数(2/3/4),这样 I2S 用 16/24/32bit 都能直接接收
*/
static inline uint8_t pick_out_bytes(uint8_t bps)
{
if (bps < 2)
{
return 2;
}
if (bps > 4)
{
return 4;
}
return bps;
}
/**
* @brief 写一个输出采样(把源采样按需要补齐/复制)
* @param dst 输出位置(out_bytes 字节,小端,左对齐)
* @param src 源采样起点(bps 字节,小端,左对齐)
*/
static inline void write_sample(uint8_t *dst, const uint8_t *src, uint8_t bps, uint8_t out_bytes)
{
if (bps == out_bytes)
{
memcpy(dst, src, out_bytes);
return;
}
if (bps == 1)
{
/* 8bit 及以下:左对齐扩成 16bit(低字节补 0,高字节就是原样本) */
dst[0] = 0;
dst[1] = src[0];
for (uint8_t i = 2; i < out_bytes; i++)
{
dst[i] = (src[0] & 0x80) ? 0xFF : 0x00; /* 符号扩展到更高的字节 */
}
return;
}
/* 理论上不会走到这里;保守处理为左对齐拷贝 */
uint8_t n = (bps < out_bytes) ? bps : out_bytes;
memset(dst, 0, out_bytes);
memcpy(dst + (out_bytes - n), src, n);
}
/**
* @brief 把解码结果(源声道数)规整成 2 声道,写入调用者缓冲
* @param frames 本次要输出的帧数
* @retval 无
*/
static void expand_frames(flac_dec_t *d, const uint8_t *src, uint8_t *dst, size_t frames)
{
const size_t ch = d->info.channels;
const size_t bps = d->bps;
const size_t ob = d->out_bytes;
if ((ch == 2) && (bps == ob))
{
memcpy(dst, src, frames * 2 * ob); /* 最常见路径:立体声原生位深 */
return;
}
for (size_t i = 0; i < frames; i++)
{
const uint8_t *s = src + i * ch * bps;
uint8_t *o = dst + i * 2 * ob;
write_sample(o, s, (uint8_t)bps, (uint8_t)ob); /* 左声道 = 声道 0 */
if (ch >= 2)
{
write_sample(o + ob, s + bps, (uint8_t)bps, (uint8_t)ob); /* 右声道 = 声道 1 */
}
else
{
memcpy(o + ob, o, ob); /* 单声道复制 */
}
}
}
/* ================================================================================================
* 对外接口
* ============================================================================================== */
flac_dec_t *flac_dec_open(const char *path)
{
if (path == NULL)
{
return NULL;
}
flac_dec_t *d = new (std::nothrow) flac_dec();
if (d == NULL)
{
ESP_LOGE(TAG, "no memory for decoder");
return NULL;
}
memset(d, 0, sizeof(*d));
if (f_open(&d->fp, path, FA_READ) != FR_OK)
{
ESP_LOGE(TAG, "open %s failed", path);
flac_dec_close(d);
return NULL;
}
d->fp_open = true;
d->decoder = new (std::nothrow) micro_flac::FLACDecoder();
if (d->decoder == NULL)
{
ESP_LOGE(TAG, "no memory for FLACDecoder");
flac_dec_close(d);
return NULL;
}
d->in_buf = (uint8_t *)malloc(IN_BUF_SIZE);
if (d->in_buf == NULL)
{
ESP_LOGE(TAG, "no memory for input buffer");
flac_dec_close(d);
return NULL;
}
/* ---------------- 解析头部(STREAMINFO) ---------------- */
int stall = 0;
while (!d->header_ready)
{
if (!has_input(d) && !feed_more(d))
{
flac_dec_close(d);
return NULL;
}
if (!has_input(d))
{
ESP_LOGE(TAG, "empty or truncated stream");
flac_dec_close(d);
return NULL;
}
size_t consumed = 0;
size_t samples = 0;
/* 头部解析阶段不需要输出缓冲 */
micro_flac::FLACDecoderResult res =
d->decoder->decode(d->in_buf + d->in_pos, d->in_len - d->in_pos,
(uint8_t *)NULL, 0, consumed, samples);
d->in_pos += consumed;
if (res == micro_flac::FLAC_DECODER_HEADER_READY)
{
const micro_flac::FLACStreamInfo &si = d->decoder->get_stream_info();
if (!si.is_valid() || (si.num_channels() < 1) || (si.num_channels() > 8) ||
(si.max_block_size() == 0) || (si.bytes_per_sample() == 0) ||
(si.bytes_per_sample() > 4))
{
ESP_LOGE(TAG, "invalid stream info");
flac_dec_close(d);
return NULL;
}
d->info.sample_rate = si.sample_rate();
d->info.channels = (uint8_t)si.num_channels();
d->info.bits = (uint8_t)si.bits_per_sample();
d->info.max_block_size = si.max_block_size();
d->info.total_samples = si.total_samples_per_channel();
d->bps = (uint8_t)si.bytes_per_sample();
d->out_bytes = pick_out_bytes(d->bps);
d->info.out_bytes = d->out_bytes;
/* 解码输出缓冲按「最大块 × 声道数 × 源字节数」分配 */
if (!ensure_pcm_buffer(d, (size_t)d->info.max_block_size *
d->info.channels * d->bps))
{
flac_dec_close(d);
return NULL;
}
d->header_ready = true;
stall = 0;
}
else if (res == micro_flac::FLAC_DECODER_NEED_MORE_DATA)
{
size_t before = d->in_len - d->in_pos; /* feed 之前的待解析字节数 */
if (!feed_more(d))
{
flac_dec_close(d);
return NULL;
}
/* 没有新数据可加(文件读完 / 缓冲已满)→ 头部不完整 */
if ((d->in_len <= before) && d->in_eof)
{
ESP_LOGE(TAG, "truncated header");
flac_dec_close(d);
return NULL;
}
if (consumed == 0)
{
if (++stall > MAX_STALL)
{
ESP_LOGE(TAG, "header parse stalled");
flac_dec_close(d);
return NULL;
}
}
else
{
stall = 0;
}
}
else if (res == micro_flac::FLAC_DECODER_END_OF_STREAM)
{
ESP_LOGE(TAG, "no audio in stream");
flac_dec_close(d);
return NULL;
}
else
{
ESP_LOGE(TAG, "header parse failed (%d)", (int)res);
flac_dec_close(d);
return NULL;
}
}
ESP_LOGI(TAG, "%s: %u Hz, %u bit (out %u byte, %u bit slot), %u ch, block=%u",
path, (unsigned)d->info.sample_rate, d->info.bits,
d->out_bytes, (unsigned)d->out_bytes * 8,
d->info.channels, (unsigned)d->info.max_block_size);
return d;
}
int flac_dec_get_info(flac_dec_t *dec, flac_info_t *info)
{
if ((dec == NULL) || (info == NULL) || !dec->header_ready)
{
return -1;
}
*info = dec->info;
return 0;
}
int flac_dec_read(flac_dec_t *d, void *out, int max_frames)
{
if ((d == NULL) || (out == NULL) || (max_frames <= 0))
{
return -1;
}
if (d->failed || !d->header_ready)
{
return -1;
}
const size_t ch = d->info.channels;
const size_t bps = d->bps;
const size_t ob = d->out_bytes;
const size_t src_frame = ch * bps; /* 源每帧字节数 */
uint8_t *dst_base = (uint8_t *)out;
int written = 0;
int stall = 0;
while (written < max_frames)
{
/* ---- 1. 先把已解码但没送出的帧搬走 ---- */
if (d->pcm_pos < d->pcm_bytes)
{
size_t avail = (d->pcm_bytes - d->pcm_pos) / src_frame; /* 剩余帧数 */
size_t need = (size_t)(max_frames - written);
size_t n = (avail < need) ? avail : need;
if (n == 0)
{
d->pcm_pos = d->pcm_bytes; /* 丢弃不足一帧的残料 */
continue;
}
expand_frames(d, d->pcm + d->pcm_pos, dst_base + (size_t)written * 2 * ob, n);
d->pcm_pos += n * src_frame;
written += (int)n;
continue;
}
if (d->stream_end)
{
break;
}
/* ---- 2. 码流用尽则补充 ---- */
if (!has_input(d) && !feed_more(d))
{
d->failed = true;
return (written > 0) ? written : -1;
}
if (!has_input(d))
{
d->stream_end = true; /* 文件读完且没有待解码数据 */
break;
}
/* ---- 3. 解一帧 ---- */
size_t consumed = 0;
size_t samples = 0;
micro_flac::FLACDecoderResult res =
d->decoder->decode(d->in_buf + d->in_pos, d->in_len - d->in_pos,
d->pcm, d->pcm_capacity, consumed, samples);
d->in_pos += consumed;
if (res == micro_flac::FLAC_DECODER_SUCCESS)
{
size_t bytes = samples * bps;
if (bytes > d->pcm_capacity)
{
ESP_LOGE(TAG, "decoder overflow (%u > %u)",
(unsigned)bytes, (unsigned)d->pcm_capacity);
d->failed = true;
return (written > 0) ? written : -1;
}
d->pcm_bytes = bytes;
d->pcm_pos = 0;
/* 无进展保护:空帧且未消费数据 */
if ((bytes == 0) && (consumed == 0))
{
if (++stall > MAX_STALL)
{
ESP_LOGW(TAG, "decode stalled, stop stream");
d->stream_end = true;
break;
}
}
else
{
stall = 0;
}
}
else if (res == micro_flac::FLAC_DECODER_HEADER_READY)
{
/* 正常不会二次出现;出现时忽略即可 */
continue;
}
else if (res == micro_flac::FLAC_DECODER_NEED_MORE_DATA)
{
size_t before = d->in_len - d->in_pos; /* feed 之前的待解码字节数 */
if (!feed_more(d))
{
d->failed = true;
return (written > 0) ? written : -1;
}
/* 文件结束且没有新数据可加 → 剩余数据不足以解出完整帧,正常收尾 */
if ((d->in_len <= before) && d->in_eof)
{
d->stream_end = true;
break;
}
}
else if (res == micro_flac::FLAC_DECODER_END_OF_STREAM)
{
d->stream_end = true;
break;
}
else
{
ESP_LOGE(TAG, "decode error (%d)", (int)res);
d->failed = true;
return (written > 0) ? written : -1;
}
}
return written;
}
void flac_dec_close(flac_dec_t *d)
{
if (d == NULL)
{
return;
}
if (d->fp_open)
{
f_close(&d->fp);
d->fp_open = false;
}
delete d->decoder;
d->decoder = NULL;
free(d->in_buf);
free(d->pcm);
d->in_buf = NULL;
d->pcm = NULL;
delete d;
}
+77
View File
@@ -0,0 +1,77 @@
/**
****************************************************************************************************
* @file flac_dec.h
* @author HiFi Player Project
* @version V1.5
* @date 2026-01-01
* @brief FLAC 解码器 C 接口封装
*
* 底层使用 esphome/micro-flac(ESP32-S3 带 Xtensa 汇编优化的 FLAC 解码器),
* 这里用一层很薄的 C++ 包装,把接口暴露成纯 C,方便 player.c 直接调用。
*
* 输出策略(V1.5 起):
* **保留源文件的位深**,不再统一压到 16bit。
* 库输出的是“原生字节打包 + 左对齐”(16bit→2 字节,24bit→3 字节,32bit→4 字节,
* 20bit 这类非整字节位深则填满其 3 字节的高 20 位),正好符合 I2S 的要求,
* 因此上层可以按 out_bytes 配置 I2S/ES8388 的位宽,实现真正的无损通路。
*
* 声道处理:单声道复制成左右;多于 2 声道时只取前两声道。
****************************************************************************************************
*/
#ifndef __FLAC_DEC_H
#define __FLAC_DEC_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct flac_dec flac_dec_t;
/** FLAC 流信息 */
typedef struct {
uint32_t sample_rate; /* 采样率 */
uint8_t channels; /* 源声道数(1~8) */
uint8_t bits; /* 源位深(4~32) */
uint8_t out_bytes; /* 输出每采样字节数:2 / 3 / 4(对应 I2S 16/24/32bit) */
uint32_t max_block_size; /* 最大块大小(每声道采样数) */
uint64_t total_samples; /* 每声道总采样数,0 表示文件未记录 */
} flac_info_t;
/**
* @brief 打开 FLAC 文件并解析 STREAMINFO
* @param path 文件路径(如 "0:/MUSIC/a.flac")
* @retval 句柄;失败返回 NULL
* @note 返回非 NULL 时 info 已经可用(见 flac_dec_get_info)
*/
flac_dec_t *flac_dec_open(const char *path);
/**
* @brief 获取流信息(仅在 flac_dec_open 成功后有效)
* @retval 0 成功
*/
int flac_dec_get_info(flac_dec_t *dec, flac_info_t *info);
/**
* @brief 读取一块 PCM 数据
* @param dec 解码器句柄
* @param out 输出缓冲区,写入 **立体声交错** 的原生位深 PCM(左对齐)
* @param max_frames 最多输出多少帧(1 帧 = 左右各 1 个采样)
* @retval >0 实际写入的帧数
* 0 文件结束
* <0 解码错误
* @note 每帧占用 2 * out_bytes 字节,调用者需保证缓冲区足够
*/
int flac_dec_read(flac_dec_t *dec, void *out, int max_frames);
/** @brief 关闭并释放所有资源(可传 NULL) */
void flac_dec_close(flac_dec_t *dec);
#ifdef __cplusplus
}
#endif
#endif /* __FLAC_DEC_H */
+770
View File
@@ -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;
}
}
+87
View File
@@ -0,0 +1,87 @@
/**
****************************************************************************************************
* @file player.h
* @author HiFi Player Project
* @version V1.2
* @date 2026-01-01
* @brief HiFi 播放器核心:SD 卡曲目扫描、WAV 解码、I2S 输出
* @note 按键由 ui.c 统一消费后通过 player_request() 转发进来;本模块不直接读按键。
* 播放器只在 APP_MODE_PLAYER 模式下输出音频;离开该模式会自动暂停并保留进度。
****************************************************************************************************
*/
#ifndef __PLAYER_H
#define __PLAYER_H
#include <stdbool.h>
#include <stdint.h>
#include "app.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---------------- 播放器状态 ---------------- */
typedef enum {
PLAYER_STATUS_BOOT = 0, /* 正在初始化 / 挂载 SD 卡 */
PLAYER_STATUS_NO_SD, /* 没有检测到 SD 卡 */
PLAYER_STATUS_NO_MUSIC, /* SD 卡中没有可播放的 WAV 文件 */
PLAYER_STATUS_PLAYING, /* 正在播放 */
PLAYER_STATUS_PAUSED, /* 已暂停 */
PLAYER_STATUS_ERROR, /* 音频硬件 / 文件系统错误 */
} player_status_t;
/* ---------------- 供 UI 读取的状态快照 ---------------- */
typedef struct {
player_status_t status; /* 当前状态 */
uint8_t volume; /* 音量百分比 0~100 */
int16_t track_index; /* 当前曲目序号(0 基) */
int16_t track_total; /* 曲目总数 */
uint32_t cur_sec; /* 当前播放秒数 */
uint32_t tot_sec; /* 曲目总秒数 */
uint16_t samplerate; /* 采样率 */
uint8_t bits; /* 源位深(16/24/32,界面显示用) */
uint8_t i2s_bits; /* 实际 I2S 输出位宽(16/24/32) */
uint8_t channels; /* 声道数 */
uint32_t bitrate; /* 比特率,单位 bps */
char track_name[64]; /* 曲目显示名(已去掉扩展名,仅保留 ASCII) */
char codec[8]; /* 容器格式:"WAV" / "FLAC" */
char message[64]; /* 状态提示信息 */
} player_state_t;
/* ---------------- 播放器可接收的请求 ---------------- */
typedef enum {
PLAYER_REQ_PLAY_PAUSE = 0, /* 播放 / 暂停 */
PLAYER_REQ_NEXT, /* 下一曲 */
PLAYER_REQ_VOLUME, /* 音量切档 */
PLAYER_REQ_NUM,
} player_request_t;
/**
* @brief 启动播放器(创建播放任务)
* @note 调用前必须完成 NVS / SPI / IIC / XL9555 / LCD 初始化
*/
void player_start(void);
/**
* @brief 获取播放器状态快照(线程安全,供 LVGL 任务调用)
* @param out 输出缓冲区
*/
void player_get_state(player_state_t *out);
/**
* @brief 向播放任务投递一个动作请求(可在任意任务中调用,不阻塞)
* @param req 请求类型
* @note 请求是位标记累积的,播放任务下次循环时统一处理
*/
void player_request(player_request_t req);
#ifdef __cplusplus
}
#endif
#endif /* __PLAYER_H */
+323
View File
@@ -0,0 +1,323 @@
/**
****************************************************************************************************
* @file ui_player.c
* @author HiFi Player Project
* @version V1.2
* @date 2026-01-01
* @brief 播放器界面
*
* ┌──────────────────────────┐
* │ ♪ HiFi PLAYER 01/12 │
* ├──────────────────────────┤
* │ ┌────┐ 歌名 │
* │ │ ♪ │ WAV 44100Hz 16bit │
* │ └────┘ ( PLAYING ) │
* │ 00:42 ────●────── 03:35 │
* │ 🔊 ──────●───── 60% │
* ├──────────────────────────┤
* │ [←K0] [⏯K1] [⏭K2] [🔊K3] │
* │ BACK PLAY NEXT VOL │
* └──────────────────────────┘
****************************************************************************************************
*/
#include "ui_player.h"
#include <stdio.h>
#include <string.h>
#include "ui_common.h"
#include "app.h"
#include "player.h"
/* ---------------- 控件 ---------------- */
static lv_obj_t *s_scr = NULL;
static lv_obj_t *s_lbl_index = NULL;
static lv_obj_t *s_lbl_name = NULL;
static lv_obj_t *s_lbl_format = NULL;
static lv_obj_t *s_lbl_chip = NULL;
static lv_obj_t *s_bar_prog = NULL;
static lv_obj_t *s_lbl_cur = NULL;
static lv_obj_t *s_lbl_tot = NULL;
static lv_obj_t *s_bar_vol = NULL;
static lv_obj_t *s_lbl_vol = NULL;
static ui_key_bar_t s_bar;
static ui_flash_t s_flash;
static uint32_t s_chip_color = 0xFFFFFFFF;
/* ---------------- 按键提示内容 ---------------- */
static const char *const s_symbols[APP_KEY_NUM] = {
LV_SYMBOL_LEFT, /* K0:返回主界面 */
LV_SYMBOL_PLAY, /* K1:播放 / 暂停(图标随状态切换) */
LV_SYMBOL_NEXT, /* K2:下一曲 */
LV_SYMBOL_VOLUME_MAX, /* K3:音量 */
};
static const char *const s_captions[APP_KEY_NUM] = {
"K0 BACK",
"K1 PLAY",
"K2 NEXT",
"K3 VOL",
};
static const bool s_enabled[APP_KEY_NUM] = { true, true, true, true };
/* ---------------- 状态显示 ---------------- */
static const char *status_text(player_status_t status, const char *message)
{
switch (status)
{
case PLAYER_STATUS_PLAYING:
return "PLAYING";
case PLAYER_STATUS_PAUSED:
return (message && message[0]) ? message : "PAUSED";
case PLAYER_STATUS_NO_SD:
return (message && message[0]) ? message : "NO SD CARD";
case PLAYER_STATUS_NO_MUSIC:
return (message && message[0]) ? message : "NO MUSIC";
case PLAYER_STATUS_ERROR:
return (message && message[0]) ? message : "ERROR";
case PLAYER_STATUS_BOOT:
default:
return (message && message[0]) ? message : "STARTING...";
}
}
static uint32_t status_color(player_status_t status)
{
switch (status)
{
case PLAYER_STATUS_PLAYING:
return UI_COL_OK;
case PLAYER_STATUS_PAUSED:
return UI_COL_WARN;
case PLAYER_STATUS_NO_SD:
case PLAYER_STATUS_ERROR:
return UI_COL_ERR;
case PLAYER_STATUS_NO_MUSIC:
return UI_COL_WARN;
case PLAYER_STATUS_BOOT:
default:
return UI_COL_SUB;
}
}
/* ---------------- 创建界面 ---------------- */
void ui_player_create(void)
{
s_scr = lv_obj_create(NULL);
ui_style_screen(s_scr);
/* ---------------- 标题栏 ---------------- */
lv_obj_t *header = ui_card(s_scr, 0, 0, UI_HOR_RES, 30, UI_COL_PANEL, 0);
lv_obj_t *title = ui_label(header, LV_SYMBOL_AUDIO " HiFi PLAYER",
&lv_font_montserrat_16, UI_COL_ACCENT);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 10, 0);
s_lbl_index = ui_label(header, "--/--", &lv_font_montserrat_14, UI_COL_SUB);
lv_obj_align(s_lbl_index, LV_ALIGN_RIGHT_MID, -10, 0);
/* ---------------- 封面卡片 ---------------- */
lv_obj_t *cover = ui_card(s_scr, 10, 36, 92, 92, 0x283A6B, 12);
lv_obj_set_style_bg_grad_color(cover, lv_color_hex(UI_COL_ACCENT2), LV_PART_MAIN);
lv_obj_set_style_bg_grad_dir(cover, LV_GRAD_DIR_VER, LV_PART_MAIN);
lv_obj_set_style_shadow_width(cover, 18, LV_PART_MAIN);
lv_obj_set_style_shadow_color(cover, lv_color_hex(UI_COL_ACCENT2), LV_PART_MAIN);
lv_obj_set_style_shadow_opa(cover, LV_OPA_30, LV_PART_MAIN);
lv_obj_t *note = ui_label(cover, LV_SYMBOL_AUDIO, &lv_font_montserrat_40, 0xFFFFFF);
lv_obj_center(note);
/* ---------------- 曲目信息 ---------------- */
s_lbl_name = ui_label(s_scr, "No media", &lv_font_montserrat_16, UI_COL_TEXT);
lv_obj_set_pos(s_lbl_name, 112, 36);
lv_obj_set_size(s_lbl_name, 198, 20);
lv_label_set_long_mode(s_lbl_name, LV_LABEL_LONG_SCROLL_CIRCULAR);
s_lbl_format = ui_label(s_scr, "WAV / PCM", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_set_pos(s_lbl_format, 112, 60);
lv_obj_t *chip = ui_card(s_scr, 112, 82, LV_SIZE_CONTENT, 22, UI_COL_CARD, 11);
lv_obj_set_style_pad_hor(chip, 8, LV_PART_MAIN);
lv_obj_set_style_border_width(chip, 1, LV_PART_MAIN);
lv_obj_set_style_border_color(chip, lv_color_hex(UI_COL_TRACK), LV_PART_MAIN);
s_lbl_chip = ui_label(chip, "STARTING...", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_center(s_lbl_chip);
/* ---------------- 播放进度 ---------------- */
s_bar_prog = lv_bar_create(s_scr);
lv_obj_set_pos(s_bar_prog, 10, 130);
lv_obj_set_size(s_bar_prog, 300, 8);
lv_bar_set_range(s_bar_prog, 0, 100);
lv_bar_set_value(s_bar_prog, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(s_bar_prog, lv_color_hex(UI_COL_TRACK), LV_PART_MAIN);
lv_obj_set_style_bg_opa(s_bar_prog, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(s_bar_prog, 4, LV_PART_MAIN);
lv_obj_set_style_bg_color(s_bar_prog, lv_color_hex(UI_COL_ACCENT), LV_PART_INDICATOR);
lv_obj_set_style_radius(s_bar_prog, 4, LV_PART_INDICATOR);
s_lbl_cur = ui_label(s_scr, "00:00", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_set_pos(s_lbl_cur, 10, 142);
s_lbl_tot = ui_label(s_scr, "--:--", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_align(s_lbl_tot, LV_ALIGN_TOP_RIGHT, -10, 142);
/* ---------------- 音量 ---------------- */
lv_obj_t *vol_icon = ui_label(s_scr, LV_SYMBOL_VOLUME_MAX, &lv_font_montserrat_14,
UI_COL_ACCENT);
lv_obj_set_pos(vol_icon, 10, 160);
s_bar_vol = lv_bar_create(s_scr);
lv_obj_set_pos(s_bar_vol, 32, 165);
lv_obj_set_size(s_bar_vol, 228, 6);
lv_bar_set_range(s_bar_vol, 0, 100);
lv_bar_set_value(s_bar_vol, 60, LV_ANIM_OFF);
lv_obj_set_style_bg_color(s_bar_vol, lv_color_hex(UI_COL_TRACK), LV_PART_MAIN);
lv_obj_set_style_bg_opa(s_bar_vol, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(s_bar_vol, 3, LV_PART_MAIN);
lv_obj_set_style_bg_color(s_bar_vol, lv_color_hex(UI_COL_ACCENT2), LV_PART_INDICATOR);
lv_obj_set_style_radius(s_bar_vol, 3, LV_PART_INDICATOR);
s_lbl_vol = ui_label(s_scr, "60%", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_align(s_lbl_vol, LV_ALIGN_TOP_RIGHT, -10, 158);
/* ---------------- 底部按键提示 ---------------- */
ui_key_bar_create(s_scr, &s_bar, s_symbols, s_captions, s_enabled);
}
/* ---------------- 显示 / 刷新 ---------------- */
void ui_player_show(void)
{
ui_key_bar_set_highlight(&s_bar, -1);
ui_player_tick();
lv_scr_load(s_scr);
}
void ui_player_tick(void)
{
player_state_t state;
char buffer[96];
player_get_state(&state);
/* ---- 标题栏:曲目序号 ---- */
if (state.track_total > 0)
{
snprintf(buffer, sizeof(buffer), "%02d/%02d",
(int)state.track_index + 1, (int)state.track_total);
}
else
{
snprintf(buffer, sizeof(buffer), "--/--");
}
ui_label_set_if_changed(s_lbl_index, buffer);
/* ---- 曲名 / 格式 / 状态 ---- */
ui_label_set_if_changed(s_lbl_name, state.track_name[0] ? state.track_name : "No media");
if (state.tot_sec > 0)
{
/* 位数相同只显示一个;源位深被提升时显示 "24->32bit" */
char bits_text[16];
uint8_t i2s_bits = state.i2s_bits ? state.i2s_bits : state.bits;
if (i2s_bits == state.bits)
{
snprintf(bits_text, sizeof(bits_text), "%ubit", (unsigned)state.bits);
}
else
{
snprintf(bits_text, sizeof(bits_text), "%u->%ubit",
(unsigned)state.bits, (unsigned)i2s_bits);
}
snprintf(buffer, sizeof(buffer), "%s %uHz %s %s",
state.codec[0] ? state.codec : "AUDIO",
(unsigned)state.samplerate, bits_text,
(state.channels == 1) ? "MONO" : "STEREO");
}
else
{
snprintf(buffer, sizeof(buffer), "%s / PCM",
state.codec[0] ? state.codec : "AUDIO");
}
ui_label_set_if_changed(s_lbl_format, buffer);
ui_label_set_if_changed(s_lbl_chip, status_text(state.status, state.message));
uint32_t color = status_color(state.status);
if (color != s_chip_color)
{
s_chip_color = color;
lv_obj_set_style_text_color(s_lbl_chip, lv_color_hex(color), LV_PART_MAIN);
}
/* ---- 播放进度 ---- */
uint32_t total = (state.tot_sec > 0) ? state.tot_sec : 1;
uint32_t current = (state.cur_sec > state.tot_sec) ? state.tot_sec : state.cur_sec;
lv_bar_set_value(s_bar_prog, (int32_t)((uint64_t)current * 100 / total), LV_ANIM_OFF);
ui_format_time(current, buffer, sizeof(buffer));
ui_label_set_if_changed(s_lbl_cur, buffer);
if (state.tot_sec > 0)
{
ui_format_time(state.tot_sec, buffer, sizeof(buffer));
}
else
{
snprintf(buffer, sizeof(buffer), "--:--");
}
ui_label_set_if_changed(s_lbl_tot, buffer);
/* ---- 音量 ---- */
lv_bar_set_value(s_bar_vol, (int32_t)state.volume, LV_ANIM_OFF);
snprintf(buffer, sizeof(buffer), "%u%%", (unsigned)state.volume);
ui_label_set_if_changed(s_lbl_vol, buffer);
/* ---- 播放键图标提示“按下后会发生什么” ---- */
ui_key_bar_set_icon(&s_bar, APP_KEY_1,
(state.status == PLAYER_STATUS_PLAYING) ? LV_SYMBOL_PAUSE
: LV_SYMBOL_PLAY);
/* ---- 按键高亮 ---- */
ui_key_bar_set_highlight(&s_bar, ui_flash_active(&s_flash));
}
/* ---------------- 按键 ---------------- */
void ui_player_flash(int key)
{
ui_flash_trigger(&s_flash, key);
}
void ui_player_handle_key(uint8_t key)
{
ui_flash_trigger(&s_flash, key);
switch (key)
{
case APP_KEY_1:
player_request(PLAYER_REQ_PLAY_PAUSE);
break;
case APP_KEY_2:
player_request(PLAYER_REQ_NEXT);
break;
case APP_KEY_3:
player_request(PLAYER_REQ_VOLUME);
break;
default:
break;
}
}
+43
View File
@@ -0,0 +1,43 @@
/**
****************************************************************************************************
* @file ui_player.h
* @author HiFi Player Project
* @version V1.2
* @date 2026-01-01
* @brief 播放器界面接口
*
* 按键:K0 返回主界面 / K1 播放暂停 / K2 下一曲 / K3 音量
****************************************************************************************************
*/
#ifndef __UI_PLAYER_H
#define __UI_PLAYER_H
#include <stdint.h>
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
/** 创建播放器界面(只创建,不显示) */
void ui_player_create(void);
/** 显示播放器界面并立即刷新一次 */
void ui_player_show(void);
/** 周期刷新界面数据(进度、音量、状态等) */
void ui_player_tick(void);
/** 处理本界面的功能键(K1/K2/K3);K0 由 ui.c 统一处理 */
void ui_player_handle_key(uint8_t key);
/** 仅触发按键高亮(不做动作),供 K0 使用 */
void ui_player_flash(int key);
#ifdef __cplusplus
}
#endif
#endif /* __UI_PLAYER_H */