mirror of
https://github.com/m5stack/StackChan.git
synced 2026-06-14 18:20:27 +00:00
update firmware v1.4.1
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "1.4.0")
|
||||
set(PROJECT_VER "1.4.1")
|
||||
add_definitions(-DFIRMWARE_VERSION=\"${PROJECT_VER}\")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
|
||||
@@ -92,6 +92,11 @@ void AppSetup::onOpen()
|
||||
_destroy_menu = true;
|
||||
_worker = std::make_unique<ZeroCalibrationWorker>();
|
||||
}},
|
||||
{"Microphone",
|
||||
[&]() {
|
||||
_destroy_menu = true;
|
||||
_worker = std::make_unique<MicTestWorker>();
|
||||
}},
|
||||
{"RGB Strip",
|
||||
[&]() {
|
||||
_destroy_menu = true;
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "workers.h"
|
||||
#include <hal/hal.h>
|
||||
#include <mooncake_log.h>
|
||||
|
||||
using namespace smooth_ui_toolkit::lvgl_cpp;
|
||||
using namespace setup_workers;
|
||||
|
||||
static std::string _tag = "Setup-Audio";
|
||||
|
||||
static constexpr uint32_t _btn_test_color_default_bg = 0xFFDF9A;
|
||||
static constexpr uint32_t _btn_test_color_default_txt = 0x47330A;
|
||||
static constexpr uint32_t _btn_test_color_done_bg = 0xCBEFC9;
|
||||
static constexpr uint32_t _btn_test_color_done_txt = 0x184A20;
|
||||
static constexpr uint32_t _btn_test_color_failed_bg = 0xF4B7B2;
|
||||
static constexpr uint32_t _btn_test_color_failed_txt = 0x6A1D18;
|
||||
static constexpr uint32_t _waveform_point_count = 128;
|
||||
static constexpr uint32_t _waveform_update_interval_ms = 1000 / 24;
|
||||
|
||||
MicTestWorker::MicTestWorker()
|
||||
{
|
||||
_original_volume = GetHAL().getSpeakerVolume();
|
||||
GetHAL().setSpeakerVolume(100, false);
|
||||
_waveform_frame.resize(_waveform_point_count, 0);
|
||||
|
||||
_panel = std::make_unique<Container>(lv_screen_active());
|
||||
_panel->setBgColor(lv_color_hex(0xEDF4FF));
|
||||
_panel->align(LV_ALIGN_CENTER, 0, 0);
|
||||
_panel->setBorderWidth(0);
|
||||
_panel->setSize(320, 240);
|
||||
_panel->setRadius(0);
|
||||
_panel->removeFlag(LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
_chart_waveform = std::make_unique<Chart>(_panel->get());
|
||||
_chart_waveform->align(LV_ALIGN_CENTER, 0, -68);
|
||||
_chart_waveform->setSize(260, 76);
|
||||
_chart_waveform->setPointCount(_waveform_point_count);
|
||||
_chart_waveform->setStyleSize(0, 0, LV_PART_INDICATOR);
|
||||
_chart_waveform->setRange(LV_CHART_AXIS_PRIMARY_Y, INT16_MIN, INT16_MAX);
|
||||
_chart_waveform->setDivLineCount(2, 6);
|
||||
_chart_waveform->setBorderWidth(0);
|
||||
_chart_waveform->setRadius(12);
|
||||
_chart_waveform->setBgColor(lv_color_hex(0xDDE8FF));
|
||||
// _chart_waveform->setBgOpa(LV_OPA_100);
|
||||
_waveform_series = _chart_waveform->addSeries(lv_color_hex(0x4A79E8), LV_CHART_AXIS_PRIMARY_Y);
|
||||
update_waveform();
|
||||
|
||||
_btn_test = std::make_unique<Button>(_panel->get());
|
||||
apply_button_common_style(*_btn_test);
|
||||
_btn_test->align(LV_ALIGN_CENTER, 0, 13);
|
||||
_btn_test->setSize(260, 60);
|
||||
_btn_test->onClick().connect([this]() { _test_flag = true; });
|
||||
|
||||
_btn_back = std::make_unique<Button>(_panel->get());
|
||||
apply_button_common_style(*_btn_back);
|
||||
_btn_back->align(LV_ALIGN_CENTER, 0, 81);
|
||||
_btn_back->setSize(180, 50);
|
||||
_btn_back->label().setText("Back");
|
||||
_btn_back->onClick().connect([this]() { _back_flag = true; });
|
||||
|
||||
update_button_text();
|
||||
update_button_state();
|
||||
update_button_color();
|
||||
}
|
||||
|
||||
MicTestWorker::~MicTestWorker()
|
||||
{
|
||||
GetHAL().clearupMicTest();
|
||||
GetHAL().setSpeakerVolume(_original_volume, false);
|
||||
}
|
||||
|
||||
void MicTestWorker::update()
|
||||
{
|
||||
update_waveform();
|
||||
|
||||
if (_back_flag) {
|
||||
_back_flag = false;
|
||||
|
||||
if (_is_testing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_is_done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_test_flag) {
|
||||
return;
|
||||
}
|
||||
|
||||
_test_flag = false;
|
||||
|
||||
if (_is_testing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_is_testing = true;
|
||||
_status = MicTestStatus::Starting;
|
||||
_error_message.clear();
|
||||
mclog::tagInfo(_tag, "start mic test");
|
||||
update_button_text();
|
||||
update_button_state();
|
||||
update_button_color();
|
||||
|
||||
GetHAL().lvglUnlock();
|
||||
auto error_message = GetHAL().startMicTest([this](MicTestStatus status) {
|
||||
LvglLockGuard lock;
|
||||
_status = status;
|
||||
update_button_text();
|
||||
update_button_state();
|
||||
update_button_color();
|
||||
});
|
||||
GetHAL().lvglLock();
|
||||
|
||||
_is_testing = false;
|
||||
|
||||
if (!error_message.empty()) {
|
||||
_status = MicTestStatus::Failed;
|
||||
_error_message = std::move(error_message);
|
||||
mclog::tagError(_tag, "mic test failed: {}", _error_message);
|
||||
}
|
||||
|
||||
if (_status == MicTestStatus::Failed && _error_message.empty()) {
|
||||
_error_message = "Mic test failed";
|
||||
}
|
||||
|
||||
update_button_text();
|
||||
update_button_state();
|
||||
update_button_color();
|
||||
}
|
||||
|
||||
void MicTestWorker::update_button_text()
|
||||
{
|
||||
switch (_status) {
|
||||
case MicTestStatus::Starting:
|
||||
_btn_test->label().setText("Starting...");
|
||||
break;
|
||||
case MicTestStatus::Recording:
|
||||
_btn_test->label().setText("Recording...");
|
||||
break;
|
||||
case MicTestStatus::Playing:
|
||||
_btn_test->label().setText("Playing back...");
|
||||
break;
|
||||
case MicTestStatus::Failed:
|
||||
_btn_test->label().setText(_error_message.empty() ? "Mic test failed" : _error_message);
|
||||
break;
|
||||
case MicTestStatus::Done:
|
||||
_btn_test->label().setText("Record Test");
|
||||
break;
|
||||
default:
|
||||
_btn_test->label().setText("Record Test");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MicTestWorker::update_button_state()
|
||||
{
|
||||
if (_is_testing) {
|
||||
_btn_test->addState(LV_STATE_DISABLED);
|
||||
_btn_back->addState(LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
|
||||
_btn_test->removeState(LV_STATE_DISABLED);
|
||||
_btn_back->removeState(LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
void MicTestWorker::update_button_color()
|
||||
{
|
||||
uint32_t bg_color = _btn_test_color_default_bg;
|
||||
uint32_t txt_color = _btn_test_color_default_txt;
|
||||
|
||||
if (_status == MicTestStatus::Done) {
|
||||
bg_color = _btn_test_color_done_bg;
|
||||
txt_color = _btn_test_color_done_txt;
|
||||
} else if (_status == MicTestStatus::Failed) {
|
||||
bg_color = _btn_test_color_failed_bg;
|
||||
txt_color = _btn_test_color_failed_txt;
|
||||
}
|
||||
|
||||
_btn_test->setBgColor(lv_color_hex(bg_color));
|
||||
_btn_test->label().setTextColor(lv_color_hex(txt_color));
|
||||
}
|
||||
|
||||
void MicTestWorker::update_waveform()
|
||||
{
|
||||
if (!_chart_waveform || _waveform_series < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t tick = lv_tick_get();
|
||||
if (tick - _last_waveform_tick < _waveform_update_interval_ms) {
|
||||
return;
|
||||
}
|
||||
_last_waveform_tick = tick;
|
||||
|
||||
GetHAL().getMicWaveformFrame(_waveform_frame);
|
||||
|
||||
auto* series = _chart_waveform->getSeries(_waveform_series);
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < _waveform_frame.size(); i++) {
|
||||
lv_chart_set_series_value_by_id(_chart_waveform->get(), series, i, _waveform_frame[i]);
|
||||
}
|
||||
lv_chart_refresh(_chart_waveform->get());
|
||||
}
|
||||
@@ -161,6 +161,38 @@ private:
|
||||
std::vector<std::unique_ptr<uitk::lvgl_cpp::Button>> _buttons;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
*/
|
||||
class MicTestWorker : public WorkerBase {
|
||||
public:
|
||||
MicTestWorker();
|
||||
~MicTestWorker();
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
void update_button_text();
|
||||
void update_button_state();
|
||||
void update_button_color();
|
||||
void update_waveform();
|
||||
|
||||
std::unique_ptr<uitk::lvgl_cpp::Container> _panel;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Chart> _chart_waveform;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Button> _btn_test;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Button> _btn_back;
|
||||
|
||||
MicTestStatus _status = MicTestStatus::Done;
|
||||
bool _test_flag = false;
|
||||
bool _back_flag = false;
|
||||
bool _is_testing = false;
|
||||
int _waveform_series = -1;
|
||||
uint8_t _original_volume = 80;
|
||||
uint32_t _last_waveform_tick = 0;
|
||||
std::string _error_message;
|
||||
std::vector<int16_t> _waveform_frame;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "hal.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <mooncake_log.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <board.h>
|
||||
#include <audio/audio_codec.h>
|
||||
#include <hal/board/config.h>
|
||||
|
||||
static const std::string_view _tag = "HAL-Audio";
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t _mic_test_duration_seconds = 3;
|
||||
constexpr size_t _mic_test_playback_chunk_frames = 512;
|
||||
constexpr size_t _mic_waveform_point_count = 128;
|
||||
constexpr size_t _mic_waveform_samples_per_point = 6;
|
||||
constexpr size_t _mic_waveform_capture_frames = _mic_waveform_point_count * _mic_waveform_samples_per_point;
|
||||
|
||||
struct MicTestFrame {
|
||||
int16_t mic;
|
||||
int16_t reference;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string Hal::startMicTest(std::function<void(MicTestStatus)> onStatusUpdate)
|
||||
{
|
||||
mclog::tagInfo(_tag, "start mic test");
|
||||
onStatusUpdate(MicTestStatus::Starting);
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (!audio_codec) {
|
||||
mclog::tagError(_tag, "audio codec unavailable");
|
||||
clearupMicTest();
|
||||
onStatusUpdate(MicTestStatus::Failed);
|
||||
return "audio codec unavailable";
|
||||
}
|
||||
|
||||
const size_t total_frames = AUDIO_INPUT_SAMPLE_RATE * _mic_test_duration_seconds;
|
||||
const size_t input_channels = std::max(audio_codec->input_channels(), 1);
|
||||
auto* recorded_frames = static_cast<MicTestFrame*>(
|
||||
heap_caps_malloc(total_frames * sizeof(MicTestFrame), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (!recorded_frames) {
|
||||
mclog::tagError(_tag, "failed to allocate %u bytes for mic test buffer",
|
||||
static_cast<unsigned>(total_frames * sizeof(MicTestFrame)));
|
||||
clearupMicTest();
|
||||
onStatusUpdate(MicTestStatus::Failed);
|
||||
return "failed to allocate mic test buffer";
|
||||
}
|
||||
|
||||
audio_codec->EnableInput(true);
|
||||
onStatusUpdate(MicTestStatus::Recording);
|
||||
|
||||
size_t recorded_frame_count = 0;
|
||||
std::vector<int16_t> input_chunk;
|
||||
|
||||
while (recorded_frame_count < total_frames) {
|
||||
const size_t frames_to_read = std::min(total_frames - recorded_frame_count, _mic_test_playback_chunk_frames);
|
||||
input_chunk.resize(frames_to_read * input_channels);
|
||||
if (!audio_codec->InputData(input_chunk)) {
|
||||
mclog::tagError(_tag, "mic read failed after %u frames", static_cast<unsigned>(recorded_frame_count));
|
||||
break;
|
||||
}
|
||||
|
||||
for (size_t frame_index = 0; frame_index < frames_to_read; ++frame_index) {
|
||||
const size_t sample_index = frame_index * input_channels;
|
||||
recorded_frames[recorded_frame_count + frame_index].mic = input_chunk[sample_index];
|
||||
recorded_frames[recorded_frame_count + frame_index].reference =
|
||||
input_channels > 1 ? input_chunk[sample_index + 1] : input_chunk[sample_index];
|
||||
}
|
||||
recorded_frame_count += frames_to_read;
|
||||
}
|
||||
|
||||
audio_codec->EnableInput(false);
|
||||
|
||||
if (recorded_frame_count == 0) {
|
||||
mclog::tagError(_tag, "mic test captured no audio");
|
||||
heap_caps_free(recorded_frames);
|
||||
clearupMicTest();
|
||||
onStatusUpdate(MicTestStatus::Failed);
|
||||
return "mic test captured no audio";
|
||||
}
|
||||
|
||||
audio_codec->EnableOutput(true);
|
||||
onStatusUpdate(MicTestStatus::Playing);
|
||||
|
||||
std::array<int16_t, _mic_test_playback_chunk_frames> playback_chunk{};
|
||||
std::vector<int16_t> output_chunk;
|
||||
output_chunk.reserve(_mic_test_playback_chunk_frames);
|
||||
size_t played_frames = 0;
|
||||
while (played_frames < recorded_frame_count) {
|
||||
const size_t frames_to_write = std::min(recorded_frame_count - played_frames, playback_chunk.size());
|
||||
for (size_t i = 0; i < frames_to_write; ++i) {
|
||||
playback_chunk[i] = recorded_frames[played_frames + i].mic;
|
||||
}
|
||||
|
||||
output_chunk.assign(playback_chunk.begin(), playback_chunk.begin() + frames_to_write);
|
||||
audio_codec->OutputData(output_chunk);
|
||||
played_frames += frames_to_write;
|
||||
}
|
||||
|
||||
heap_caps_free(recorded_frames);
|
||||
clearupMicTest();
|
||||
onStatusUpdate(MicTestStatus::Done);
|
||||
return {};
|
||||
}
|
||||
|
||||
void Hal::getMicWaveformFrame(std::vector<int16_t>& data)
|
||||
{
|
||||
data.assign(_mic_waveform_point_count, 0);
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (!audio_codec) {
|
||||
mclog::tagError(_tag, "audio codec unavailable for waveform capture");
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t input_channels = std::max(audio_codec->input_channels(), 1);
|
||||
std::vector<int16_t> input_chunk(_mic_waveform_capture_frames * input_channels);
|
||||
if (!audio_codec->input_enabled()) {
|
||||
audio_codec->EnableInput(true);
|
||||
}
|
||||
|
||||
const bool read_ok = audio_codec->InputData(input_chunk);
|
||||
|
||||
if (!read_ok) {
|
||||
mclog::tagError(_tag, "mic waveform capture failed");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t point_index = 0; point_index < _mic_waveform_point_count; ++point_index) {
|
||||
int16_t selected_sample = 0;
|
||||
int32_t peak_magnitude = -1;
|
||||
|
||||
for (size_t sample_offset = 0; sample_offset < _mic_waveform_samples_per_point; ++sample_offset) {
|
||||
const size_t frame_index = point_index * _mic_waveform_samples_per_point + sample_offset;
|
||||
const int16_t sample = input_chunk[frame_index * input_channels];
|
||||
const int32_t magnitude = std::abs(static_cast<int32_t>(sample));
|
||||
if (magnitude > peak_magnitude) {
|
||||
peak_magnitude = magnitude;
|
||||
selected_sample = sample;
|
||||
}
|
||||
}
|
||||
|
||||
data[point_index] = selected_sample;
|
||||
}
|
||||
}
|
||||
|
||||
void Hal::clearupMicTest()
|
||||
{
|
||||
auto& board = Board::GetInstance();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (!audio_codec) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio_codec->output_enabled()) {
|
||||
audio_codec->EnableOutput(false);
|
||||
}
|
||||
|
||||
if (audio_codec->input_enabled()) {
|
||||
audio_codec->EnableInput(false);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ CoreS3AudioCodec::CoreS3AudioCodec(void* i2c_master_handle, int input_sample_rat
|
||||
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
input_gain_ = 30;
|
||||
input_gain_ = 60;
|
||||
|
||||
CreateDuplexChannels(mclk, bclk, ws, dout, din);
|
||||
|
||||
|
||||
@@ -628,8 +628,13 @@ void hal_bridge::board_set_speaker_volume(uint8_t volume, bool permanent)
|
||||
auto& board = Board::GetInstance();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (audio_codec) {
|
||||
if (permanent) {
|
||||
audio_codec->SetOutputVolume(volume);
|
||||
Settings settings("audio", false);
|
||||
const int persisted_volume = settings.GetInt("output_volume", audio_codec->output_volume());
|
||||
audio_codec->SetOutputVolume(volume);
|
||||
if (!permanent) {
|
||||
Settings writable_settings("audio", true);
|
||||
writable_settings.SetInt("output_volume", persisted_volume);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,18 @@ struct XiaozhiConfig_t {
|
||||
uint8_t idleRandomMovementLevel = 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
*/
|
||||
enum class MicTestStatus {
|
||||
Starting = 0,
|
||||
Recording,
|
||||
Playing,
|
||||
Done,
|
||||
Failed,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
@@ -284,6 +296,9 @@ public:
|
||||
/* ---------------------------------- Audio --------------------------------- */
|
||||
void setSpeakerVolume(uint8_t volume, bool permanent = false);
|
||||
uint8_t getSpeakerVolume();
|
||||
std::string startMicTest(std::function<void(MicTestStatus)> onStatusUpdate);
|
||||
void getMicWaveformFrame(std::vector<int16_t>& data);
|
||||
void clearupMicTest();
|
||||
|
||||
private:
|
||||
bool _xiaozhi_start_requested = false;
|
||||
|
||||
Reference in New Issue
Block a user