ba5e47bc77
1. 应用界面增加了返回主页的按钮 2. 修复了gif渲染内存泄漏的严重bug 3. 将PetDao当中的cJSON API替换为cpp_json,完美通过测试 4. 整合已经实现的各种上层建筑,实现了一个宠物对话基本业务应用,用于样品测试展示用 5. 重构了音频播放类,使其更modern,更加便于移植和拓展
365 lines
10 KiB
C++
365 lines
10 KiB
C++
//
|
|
// Created by misaki on 2025/9/9.
|
|
//
|
|
#include "AudioOutput.h"
|
|
#include "esp_log.h"
|
|
#include <memory>
|
|
#include "PCM5101.h"
|
|
|
|
|
|
// 初始化静态成员
|
|
AudioOutput* AudioOutput::instance = nullptr;
|
|
std::mutex AudioOutput::instanceMutex;
|
|
|
|
AudioOutput* AudioOutput::getInstance() {
|
|
std::lock_guard<std::mutex> lock(instanceMutex);
|
|
if (instance == nullptr) {
|
|
instance = new AudioOutput();
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
AudioOutput::AudioOutput()
|
|
: currentState(AudioState::IDLE),
|
|
currentVolume(Volume_MAX - 2),
|
|
hardwareInitialized(false) {
|
|
// 初始化硬件
|
|
init();
|
|
}
|
|
|
|
AudioOutput::~AudioOutput() {
|
|
// 停止播放并清理资源
|
|
stop();
|
|
}
|
|
|
|
bool AudioOutput::init() {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (hardwareInitialized) {
|
|
ESP_LOGI("AudioOutput", "Audio hardware already initialized");
|
|
return true;
|
|
}
|
|
|
|
// 初始化SD卡
|
|
SDFileManager::getInstance()->tryInitSDCard();
|
|
|
|
// 初始化音频硬件
|
|
Audio_Init();
|
|
hardwareInitialized = true;
|
|
ESP_LOGI("AudioOutput", "Audio hardware initialized successfully");
|
|
|
|
return hardwareInitialized;
|
|
}
|
|
|
|
void AudioOutput::tryInitAudioOutput() {
|
|
ESP_LOGI("AudioOutput", "Trying to initialize audio output......");
|
|
}
|
|
|
|
|
|
bool AudioOutput::playSync(const char* directory, const char* fileName) {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (!hardwareInitialized) {
|
|
ESP_LOGE("AudioOutput", "Audio hardware not initialized");
|
|
return false;
|
|
}
|
|
|
|
// 停止当前播放 TODO:有bug,需要fix
|
|
// stop();
|
|
|
|
// 播放新文件
|
|
Play_Music(directory, fileName);
|
|
|
|
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
|
currentState = AudioState::PLAYING;
|
|
currentFilePath = std::string(directory) + "/" + fileName;
|
|
return true;
|
|
}
|
|
|
|
currentState = AudioState::ERROR;
|
|
return false;
|
|
}
|
|
|
|
void AudioOutput::playAsync(const char* directory, const char* fileName, const AudioCallback& callback) {
|
|
const ThreadConfig config = getThreadConfig("play_async");
|
|
|
|
ThreadManager::createThread(config, [this, directory = std::string(directory),
|
|
fileName = std::string(fileName), callback]() {
|
|
this->playInternal(directory.c_str(), fileName.c_str(), callback);
|
|
}).detach();
|
|
}
|
|
|
|
void AudioOutput::playInternal(const char* directory, const char* fileName, const AudioCallback& callback) {
|
|
bool success = false;
|
|
auto finalState = AudioState::ERROR;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (!hardwareInitialized) {
|
|
ESP_LOGE("AudioOutput", "Audio hardware not initialized");
|
|
finalState = AudioState::ERROR;
|
|
} else {
|
|
// 停止当前播放
|
|
// stop();
|
|
|
|
// 播放新文件
|
|
Play_Music(directory, fileName);
|
|
|
|
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
|
currentState = AudioState::PLAYING;
|
|
currentFilePath = std::string(directory) + "/" + fileName;
|
|
success = true;
|
|
finalState = AudioState::PLAYING;
|
|
} else {
|
|
currentState = AudioState::ERROR;
|
|
finalState = AudioState::ERROR;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (callback) {
|
|
callback(finalState, success ? currentFilePath.c_str() : nullptr);
|
|
}
|
|
}
|
|
|
|
bool AudioOutput::pause() {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (currentState != AudioState::PLAYING) {
|
|
return false;
|
|
}
|
|
|
|
Music_pause();
|
|
|
|
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PAUSE) {
|
|
currentState = AudioState::PAUSED;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AudioOutput::resume() {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (currentState != AudioState::PAUSED) {
|
|
return false;
|
|
}
|
|
|
|
Music_resume();
|
|
|
|
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
|
currentState = AudioState::PLAYING;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AudioOutput::stop() {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (currentState == AudioState::IDLE || currentState == AudioState::STOPPED) { // 如果当前状态为IDLE或STOPPED,则直接返回成功
|
|
return true;
|
|
}
|
|
|
|
// 暂停播放
|
|
Music_pause(); // 内部已经完成关闭文件的操作了
|
|
|
|
currentState = AudioState::STOPPED;
|
|
currentFilePath.clear();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AudioOutput::setVolume(uint8_t volume) {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
|
|
if (volume > Volume_MAX) {
|
|
ESP_LOGE("AudioOutput", "Volume value %d is out of range (0-%d)", volume, Volume_MAX);
|
|
return false;
|
|
}
|
|
|
|
Volume_adjustment(volume);
|
|
currentVolume = volume;
|
|
return true;
|
|
}
|
|
|
|
uint8_t AudioOutput::getVolume() const {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
return currentVolume;
|
|
}
|
|
|
|
uint8_t AudioOutput::getMaxVolume() const {
|
|
return Volume_MAX;
|
|
}
|
|
|
|
uint32_t AudioOutput::getDuration() const {
|
|
return Music_Duration();
|
|
}
|
|
|
|
uint32_t AudioOutput::getElapsed() const {
|
|
return Music_Elapsed();
|
|
}
|
|
|
|
uint16_t AudioOutput::getEnergy() const {
|
|
return Music_Energy();
|
|
}
|
|
|
|
AudioState AudioOutput::getState() const {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
return currentState;
|
|
}
|
|
|
|
bool AudioOutput::isPlaying() const {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
return currentState == AudioState::PLAYING;
|
|
}
|
|
|
|
bool AudioOutput::isPaused() const {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
return currentState == AudioState::PAUSED;
|
|
}
|
|
|
|
bool AudioOutput::isStopped() const {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
return currentState == AudioState::STOPPED || currentState == AudioState::IDLE;
|
|
}
|
|
|
|
bool AudioOutput::isFinished() const {
|
|
return Music_Next_Flag;
|
|
}
|
|
|
|
ThreadConfig AudioOutput::getThreadConfig(const char* operation) {
|
|
ThreadConfig config;
|
|
config.name = "audio_" + std::string(operation);
|
|
config.core_id = -1; // 不绑定核心
|
|
config.stack_size = 4096;
|
|
config.priority = 5;
|
|
config.inherit_cfg = false;
|
|
return config;
|
|
}
|
|
|
|
void AudioOutput::setState(AudioState newState) {
|
|
std::lock_guard<std::mutex> lock(stateMutex);
|
|
currentState = newState;
|
|
}
|
|
|
|
bool AudioOutput::playPcmFile(const char* filePath,
|
|
uint32_t sampleRate,
|
|
i2s_data_bit_width_t bits,
|
|
i2s_slot_mode_t ch)
|
|
{
|
|
// 简单文件尺寸获取
|
|
FILE* f = fopen(filePath, "rb");
|
|
if (!f) return false;
|
|
fseek(f, 0, SEEK_END);
|
|
const size_t bytes = ftell(f);
|
|
fclose(f);
|
|
|
|
playPcmCommon(filePath, bytes, true, sampleRate, bits, ch, nullptr);
|
|
return getState() == AudioState::PLAYING;
|
|
}
|
|
|
|
bool AudioOutput::playPcmStream(const uint8_t* pcmData,
|
|
size_t dataBytes,
|
|
uint32_t sampleRate,
|
|
i2s_data_bit_width_t bits,
|
|
i2s_slot_mode_t ch)
|
|
{
|
|
playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, nullptr);
|
|
return getState() == AudioState::PLAYING;
|
|
}
|
|
|
|
void AudioOutput::playPcmFileAsync(const char* filePath,
|
|
uint32_t sampleRate,
|
|
i2s_data_bit_width_t bits,
|
|
i2s_slot_mode_t ch,
|
|
const AudioCallback& cb)
|
|
{
|
|
ThreadConfig cfg = getThreadConfig("pcm_file");
|
|
std::thread([=](){
|
|
playPcmCommon(filePath, 0, true, sampleRate, bits, ch, cb);
|
|
}).detach();
|
|
}
|
|
|
|
void AudioOutput::playPcmStreamAsync(const uint8_t* pcmData,
|
|
size_t dataBytes,
|
|
uint32_t sampleRate,
|
|
i2s_data_bit_width_t bits,
|
|
i2s_slot_mode_t ch,
|
|
const AudioCallback& cb)
|
|
{
|
|
ThreadConfig cfg = getThreadConfig("pcm_stream");
|
|
std::thread([=](){
|
|
playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, cb);
|
|
}).detach();
|
|
}
|
|
|
|
void AudioOutput::playPcmCommon(const void* source,
|
|
size_t bytes,
|
|
bool isFile,
|
|
uint32_t sampleRate,
|
|
i2s_data_bit_width_t bits,
|
|
i2s_slot_mode_t ch,
|
|
const AudioCallback& cb)
|
|
{
|
|
// 停止旧播放
|
|
stop();
|
|
// 重新配置 I2S 时钟/位宽/声道
|
|
bsp_i2s_reconfig_clk(sampleRate, bits, ch);
|
|
// 打开“文件”或“内存”数据源
|
|
FILE* f = nullptr;
|
|
const uint8_t* mem = nullptr;
|
|
size_t memLeft = 0;
|
|
if (isFile) {
|
|
f = fopen(static_cast<const char *>(source), "rb");
|
|
if (!f) {
|
|
if (cb) cb(AudioState::ERROR, static_cast<const char *>(source));
|
|
return;
|
|
}
|
|
} else {
|
|
mem = static_cast<const uint8_t *>(source);
|
|
memLeft = bytes;
|
|
}
|
|
// 状态置为 PLAYING
|
|
{
|
|
std::lock_guard<std::mutex> lk(stateMutex);
|
|
currentState = AudioState::PLAYING;
|
|
currentFilePath = isFile ? static_cast<const char *>(source) : "<stream>";
|
|
}
|
|
if (cb) cb(AudioState::PLAYING, currentFilePath.c_str());
|
|
|
|
// 循环送 PCM 数据到 I2S
|
|
constexpr size_t CHUNK = 512; // 任意 2 的幂
|
|
int16_t buf[CHUNK];
|
|
size_t bw;
|
|
while (true) {
|
|
size_t rd = 0;
|
|
if (isFile) {
|
|
rd = fread(buf, 1, sizeof(buf), f);
|
|
} else {
|
|
rd = memLeft > sizeof(buf) ? sizeof(buf) : memLeft;
|
|
memcpy(buf, mem, rd);
|
|
mem += rd;
|
|
memLeft -= rd;
|
|
}
|
|
if (rd == 0) break;
|
|
|
|
// 音量实时缩放(复用已有逻辑)
|
|
const float vf = currentVolume / 100.0f;
|
|
for (size_t i = 0; i < rd / 2; ++i) buf[i] = static_cast<int16_t>(buf[i] * vf);
|
|
|
|
// 写 I2S
|
|
i2s_channel_write(i2s_tx_chan, buf, rd, &bw, portMAX_DELAY);
|
|
}
|
|
|
|
// 播放结束
|
|
if (f) fclose(f);
|
|
{
|
|
std::lock_guard<std::mutex> lk(stateMutex);
|
|
currentState = AudioState::STOPPED;
|
|
}
|
|
if (cb) cb(AudioState::STOPPED, currentFilePath.c_str());
|
|
} |