Files
tactility/Tactility/Source/mcp/McpSystem.cpp
T

1107 lines
33 KiB
C++

#ifdef ESP_PLATFORM
#include <Tactility/mcp/McpSystem.h>
#include <Tactility/settings/McpSettings.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/file/File.h>
#include <Tactility/app/App.h>
#include <Tactility/Logger.h>
#include <Tactility/service/displayidle/DisplayIdleService.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <tactility/drivers/i2s_controller.h>
#include <mbedtls/base64.h>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#define MINIMP3_IMPLEMENTATION
#define MINIMP3_NO_SIMD
#include <Tactility/mcp/minimp3.h>
namespace tt::mcp {
static const auto LOGGER = Logger("McpSystem");
static void* psram_malloc(size_t size) {
void* ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr != nullptr) {
return ptr;
}
return malloc(size);
}
#define AUDIO_SAMPLE_RATE 16000
#define AUDIO_BITS_PER_SAMPLE 16
#define AUDIO_CHUNK_SAMPLES 512
#define AUDIO_CHUNK_BYTES (AUDIO_CHUNK_SAMPLES * 2)
#define MP3_INPUT_BUFFER_SIZE 16384
struct WavInfo {
uint16_t channels;
uint32_t sample_rate;
uint16_t bits_per_sample;
size_t data_offset;
size_t data_size;
};
// Global singleton state
McpSystemState& getState() {
static McpSystemState state;
return state;
}
static bool ascii_space(uint8_t value) {
return value == ' ' || value == '\t' || value == '\r' ||
value == '\n' || value == '\f' || value == '\v';
}
static bool ascii_digit(uint8_t value) {
return value >= '0' && value <= '9';
}
static uint16_t read_le16(const uint8_t* value) {
return (uint16_t)value[0] | ((uint16_t)value[1] << 8);
}
static uint32_t read_le32(const uint8_t* value) {
return (uint32_t)value[0] |
((uint32_t)value[1] << 8) |
((uint32_t)value[2] << 16) |
((uint32_t)value[3] << 24);
}
static bool display_ready(McpSystemState& state) {
return state.drawArea != nullptr &&
state.framebuffer != nullptr &&
state.drawWidth > 0 &&
state.drawHeight > 0;
}
static bool ensureOverrideScreen() {
auto& state = getState();
if (state.drawArea == nullptr) {
// Activate the MCP screensaver via DisplayIdle (force McpScreensaver type)
auto idleService = service::displayidle::findService();
if (idleService) {
LOGGER.info("MCP draw triggered: activating MCP screensaver");
idleService->startMcpScreensaver();
// Wait up to 500ms for the canvas to be registered by McpScreensaver::start()
for (int i = 0; i < 10; ++i) {
vTaskDelay(pdMS_TO_TICKS(50));
if (state.drawArea != nullptr) {
break;
}
}
} else {
LOGGER.warn("DisplayIdle service not found; cannot activate MCP screensaver");
}
}
return state.drawArea != nullptr;
}
static uint16_t rgb888_to_rgb565(uint8_t red, uint8_t green, uint8_t blue) {
return (uint16_t)(((red & 0xF8) << 8) |
((green & 0xFC) << 3) |
(blue >> 3));
}
static void put_pixel(McpSystemState& state, int x, int y, uint16_t color) {
if (x >= 0 && y >= 0 && x < state.drawWidth && y < state.drawHeight) {
state.framebuffer[(size_t)y * state.drawWidth + x] = color;
}
}
bool clearScreen(int color) {
if (!ensureOverrideScreen()) return false;
auto& state = getState();
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
if (display_ready(state)) {
lv_obj_clean(state.drawArea);
uint16_t fill = color == 1 ? 0x0000 : 0xFFFF;
size_t pixel_count = (size_t)state.drawWidth * state.drawHeight;
for (size_t i = 0; i < pixel_count; ++i) {
state.framebuffer[i] = fill;
}
lv_obj_invalidate(state.drawArea);
state.drawColor = color;
state.overrideActive = true;
success = true;
}
lvgl::unlock();
}
return success;
}
bool drawText(const std::string& text, int x, int y, int size) {
if (!ensureOverrideScreen()) return false;
auto& state = getState();
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
if (display_ready(state)) {
int max_x = state.drawWidth > 0 ? state.drawWidth - 1 : 0;
int max_y = state.drawHeight > 0 ? state.drawHeight - 1 : 0;
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x > max_x) x = max_x;
if (y > max_y) y = max_y;
lv_obj_t* label = lv_label_create(state.drawArea);
lv_label_set_text(label, text.c_str());
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_width(label, LV_MAX(1, state.drawWidth - x));
lv_obj_set_pos(label, x, y);
lv_obj_set_style_text_color(
label,
state.drawColor == 0 ? lv_color_black() : lv_color_white(),
LV_PART_MAIN
);
#if LV_FONT_MONTSERRAT_24
if (size >= 2) {
lv_obj_set_style_text_font(label, &lv_font_montserrat_24, LV_PART_MAIN);
}
#endif
lv_obj_invalidate(state.drawArea); // Trigger repaint
state.overrideActive = true;
success = true;
}
lvgl::unlock();
}
return success;
}
bool drawRgb565(const uint8_t* data, size_t size, int x, int y, int w, int h) {
if (!ensureOverrideScreen()) return false;
auto& state = getState();
if (data == NULL || w <= 0 || h <= 0 || size < (size_t)w * h * 2) {
return false;
}
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(2000))) {
if (display_ready(state)) {
for (int source_y = 0; source_y < h; ++source_y) {
int destination_y = y + source_y;
if (destination_y < 0 || destination_y >= state.drawHeight) {
continue;
}
for (int source_x = 0; source_x < w; ++source_x) {
int destination_x = x + source_x;
if (destination_x < 0 || destination_x >= state.drawWidth) {
continue;
}
size_t offset = ((size_t)source_y * w + source_x) * 2;
uint16_t color = ((uint16_t)data[offset] << 8) | data[offset + 1];
put_pixel(state, destination_x, destination_y, color);
}
}
lv_obj_invalidate(state.drawArea);
state.overrideActive = true;
success = true;
}
lvgl::unlock();
}
return success;
}
bool drawBmp(const uint8_t* data, size_t size, int x, int y) {
if (!ensureOverrideScreen()) return false;
auto& state = getState();
if (data == NULL || size < 54 || data[0] != 'B' || data[1] != 'M') {
return false;
}
uint32_t pixel_offset = read_le32(data + 10);
uint32_t dib_size = read_le32(data + 14);
int32_t w = (int32_t)read_le32(data + 18);
int32_t signed_height = (int32_t)read_le32(data + 22);
uint16_t planes = read_le16(data + 26);
uint16_t bits_per_pixel = read_le16(data + 28);
uint32_t compression = read_le32(data + 30);
if (dib_size < 40 || w <= 0 || signed_height == 0 || planes != 1 ||
(bits_per_pixel != 24 && bits_per_pixel != 32) || compression != 0) {
return false;
}
int h = signed_height < 0 ? -signed_height : signed_height;
bool top_down = signed_height < 0;
size_t row_stride = (((size_t)w * bits_per_pixel + 31) / 32) * 4;
if (pixel_offset > size ||
row_stride > size ||
(size_t)h > (size - pixel_offset) / row_stride) {
return false;
}
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(2500))) {
if (display_ready(state)) {
size_t bytes_per_pixel = bits_per_pixel / 8;
for (int source_y = 0; source_y < h; ++source_y) {
int file_y = top_down ? source_y : (h - 1 - source_y);
const uint8_t* row = data + pixel_offset + (size_t)file_y * row_stride;
for (int source_x = 0; source_x < w; ++source_x) {
const uint8_t* pixel = row + (size_t)source_x * bytes_per_pixel;
put_pixel(
state,
x + source_x,
y + source_y,
rgb888_to_rgb565(pixel[2], pixel[1], pixel[0])
);
}
}
lv_obj_invalidate(state.drawArea);
state.overrideActive = true;
success = true;
}
lvgl::unlock();
}
return success;
}
static bool pbm_next_number(const uint8_t* data, size_t data_size, size_t* offset, int* result) {
while (*offset < data_size) {
if (data[*offset] == '#') {
while (*offset < data_size && data[*offset] != '\n') {
(*offset)++;
}
} else if (ascii_space(data[*offset])) {
(*offset)++;
} else {
break;
}
}
if (*offset >= data_size || !ascii_digit(data[*offset])) {
return false;
}
int value = 0;
while (*offset < data_size && ascii_digit(data[*offset])) {
value = value * 10 + (data[*offset] - '0');
(*offset)++;
}
*result = value;
return true;
}
bool drawPbm(const uint8_t* data, size_t size, int x, int y) {
if (!ensureOverrideScreen()) return false;
auto& state = getState();
if (data == NULL || size < 8 || data[0] != 'P' || data[1] != '4') {
return false;
}
size_t offset = 2;
int w = 0;
int h = 0;
if (!pbm_next_number(data, size, &offset, &w) ||
!pbm_next_number(data, size, &offset, &h) ||
w <= 0 || h <= 0) {
return false;
}
if (offset >= size || !ascii_space(data[offset])) {
return false;
}
if (data[offset] == '\r' && offset + 1 < size && data[offset + 1] == '\n') {
offset += 2;
} else {
offset++;
}
size_t row_bytes = ((size_t)w + 7) / 8;
if (offset > size || (size_t)h > (size - offset) / row_bytes) {
return false;
}
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(2000))) {
if (display_ready(state)) {
for (int source_y = 0; source_y < h; ++source_y) {
const uint8_t* row = data + offset + (size_t)source_y * row_bytes;
for (int source_x = 0; source_x < w; ++source_x) {
bool black = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0;
put_pixel(state, x + source_x, y + source_y, black ? 0x0000 : 0xFFFF);
}
}
lv_obj_invalidate(state.drawArea);
state.overrideActive = true;
success = true;
}
lvgl::unlock();
}
return success;
}
std::string getScreenshotPbmBase64() {
auto& state = getState();
if (!display_ready(state)) {
return "";
}
size_t row_bytes = ((size_t)state.drawWidth + 7) / 8;
size_t header_size = 32;
size_t payload_size = row_bytes * state.drawHeight;
size_t pbm_size = header_size + payload_size;
uint8_t* pbm = (uint8_t*)psram_malloc(pbm_size);
if (pbm == NULL) {
return "";
}
int header_length = snprintf(
(char*)pbm,
header_size,
"P4\n%u %u\n",
state.drawWidth,
state.drawHeight
);
if (header_length <= 0 || (size_t)header_length >= header_size) {
free(pbm);
return "";
}
uint8_t* payload = pbm + header_length;
memset(payload, 0, payload_size);
bool success = false;
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
if (display_ready(state)) {
for (int y = 0; y < state.drawHeight; ++y) {
uint8_t* row = payload + (size_t)y * row_bytes;
for (int x = 0; x < state.drawWidth; ++x) {
uint16_t color = state.framebuffer[(size_t)y * state.drawWidth + x];
// Convert RGB565 to simple grayscale threshold (black if sum of components is low)
int red = (color >> 11) & 0x1F;
int green = (color >> 5) & 0x3F;
int blue = color & 0x1F;
int intensity = red * 8 + green * 4 + blue * 8;
bool black = intensity < 240; // threshold
if (black) {
row[x >> 3] |= (uint8_t)(0x80 >> (x & 7));
}
}
}
success = true;
}
lvgl::unlock();
}
if (!success) {
free(pbm);
return "";
}
size_t total_size = (size_t)header_length + payload_size;
size_t base64_len = 0;
mbedtls_base64_encode(nullptr, 0, &base64_len, pbm, total_size);
std::string encoded(base64_len + 1, '\0');
size_t actual_len = 0;
mbedtls_base64_encode(reinterpret_cast<unsigned char*>(encoded.data()),
encoded.length(), &actual_len, pbm, total_size);
encoded.resize(actual_len);
free(pbm);
return encoded;
}
// Audio Porting Helper
static void set_error(std::string& error, const std::string& message) {
error = message;
}
static bool get_audio_device(McpSystemState& state, std::string& error) {
if (state.i2sDevice == nullptr) {
state.i2sDevice = device_find_by_name("i2s0");
}
if (state.i2sDevice == nullptr) {
set_error(error, "I2S device 'i2s0' was not found");
return false;
}
return true;
}
static bool begin_audio(McpSystemState& state, std::string& error) {
if (!get_audio_device(state, error)) {
return false;
}
if (state.audioBusy) {
set_error(error, "Another audio operation is already running");
return false;
}
state.audioBusy = true;
state.audioRunning = true;
return true;
}
static void end_audio(McpSystemState& state) {
state.audioRunning = false;
state.audioBusy = false;
if (state.i2sDevice != nullptr) {
device_lock(state.i2sDevice);
i2s_controller_reset(state.i2sDevice);
device_unlock(state.i2sDevice);
}
}
static bool configure_i2s(McpSystemState& state, int sample_rate, int channels, std::string& error) {
I2sConfig config = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = static_cast<uint32_t>(sample_rate),
.bits_per_sample = AUDIO_BITS_PER_SAMPLE,
.channel_left = 0,
.channel_right = static_cast<int8_t>(channels == 2 ? 1 : I2S_CHANNEL_NONE)
};
device_lock(state.i2sDevice);
error_t result = i2s_controller_set_config(state.i2sDevice, &config);
device_unlock(state.i2sDevice);
if (result != ERROR_NONE) {
LOGGER.error("I2S configuration failed: {}", result);
set_error(error, "Failed to configure I2S");
return false;
}
return true;
}
static void apply_volume(uint8_t* data, size_t data_size, int volume) {
int16_t* samples = (int16_t*)data;
size_t sample_count = data_size / sizeof(int16_t);
for (size_t index = 0; index < sample_count; ++index) {
int32_t scaled = (int32_t)samples[index] * volume / 100;
samples[index] = (int16_t)scaled;
}
}
static bool write_audio(McpSystemState& state, uint8_t* data, size_t data_size, int volume, std::string& error) {
apply_volume(data, data_size, volume);
size_t offset = 0;
while (offset < data_size && state.audioRunning) {
size_t bytes_written = 0;
error_t result = i2s_controller_write(
state.i2sDevice,
data + offset,
data_size - offset,
&bytes_written,
pdMS_TO_TICKS(250)
);
if (result != ERROR_NONE || bytes_written == 0) {
LOGGER.error("I2S write failed: result={} written={}", result, (unsigned)bytes_written);
set_error(error, "I2S playback failed");
return false;
}
offset += bytes_written;
}
if (!state.audioRunning) {
set_error(error, "Audio playback was cancelled");
return false;
}
return true;
}
static bool resolve_file(const std::string& filename, char* path, size_t path_size, std::string& error) {
if (filename.empty() || filename.length() > 96 ||
filename.find("..") != std::string::npos ||
filename.find("/") != std::string::npos ||
filename.find("\\") != std::string::npos) {
error = "filename must be a simple relative name";
return false;
}
std::string base_dir = "/data/service/mcp";
file::findOrCreateDirectory(base_dir, 0755);
snprintf(path, path_size, "%s/%s", base_dir.c_str(), filename.c_str());
return true;
}
static bool parse_wav(const uint8_t* data, size_t size, WavInfo* info) {
if (data == NULL || info == NULL || size < 12 ||
memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WAVE", 4) != 0) {
return false;
}
bool have_format = false;
bool have_data = false;
uint16_t audio_format = 0;
size_t offset = 12;
memset(info, 0, sizeof(*info));
while (offset + 8 <= size) {
const uint8_t* chunk = data + offset;
uint32_t chunk_size = read_le32(chunk + 4);
offset += 8;
if (chunk_size > size - offset) {
return false;
}
if (memcmp(chunk, "fmt ", 4) == 0) {
if (chunk_size < 16) {
return false;
}
audio_format = read_le16(data + offset);
info->channels = read_le16(data + offset + 2);
info->sample_rate = read_le32(data + offset + 4);
info->bits_per_sample = read_le16(data + offset + 14);
have_format = true;
} else if (memcmp(chunk, "data", 4) == 0) {
info->data_offset = offset;
info->data_size = chunk_size;
have_data = true;
break;
}
offset += chunk_size + (chunk_size & 1U);
}
return have_format && have_data &&
audio_format == 1 &&
(info->channels == 1 || info->channels == 2) &&
info->sample_rate == AUDIO_SAMPLE_RATE &&
info->bits_per_sample == AUDIO_BITS_PER_SAMPLE;
}
static bool parse_wav_file(FILE* file, size_t file_size, WavInfo* info) {
uint8_t riff[12];
if (file == NULL || info == NULL || file_size < sizeof(riff) ||
fseek(file, 0, SEEK_SET) != 0 ||
fread(riff, 1, sizeof(riff), file) != sizeof(riff) ||
memcmp(riff, "RIFF", 4) != 0 ||
memcmp(riff + 8, "WAVE", 4) != 0) {
return false;
}
bool have_format = false;
bool have_data = false;
uint16_t audio_format = 0;
size_t offset = sizeof(riff);
memset(info, 0, sizeof(*info));
while (offset + 8 <= file_size) {
uint8_t chunk[8];
if (fseek(file, (long)offset, SEEK_SET) != 0 ||
fread(chunk, 1, sizeof(chunk), file) != sizeof(chunk)) {
return false;
}
uint32_t chunk_size = read_le32(chunk + 4);
offset += sizeof(chunk);
if (chunk_size > file_size - offset) {
return false;
}
if (memcmp(chunk, "fmt ", 4) == 0) {
uint8_t format[16];
if (chunk_size < sizeof(format) ||
fread(format, 1, sizeof(format), file) != sizeof(format)) {
return false;
}
audio_format = read_le16(format);
info->channels = read_le16(format + 2);
info->sample_rate = read_le32(format + 4);
info->bits_per_sample = read_le16(format + 14);
have_format = true;
} else if (memcmp(chunk, "data", 4) == 0) {
info->data_offset = offset;
info->data_size = chunk_size;
have_data = true;
break;
}
offset += chunk_size + (chunk_size & 1U);
}
return have_format && have_data &&
audio_format == 1 &&
(info->channels == 1 || info->channels == 2) &&
info->sample_rate == AUDIO_SAMPLE_RATE &&
info->bits_per_sample == AUDIO_BITS_PER_SAMPLE;
}
bool playTone(int frequency, int durationMs, int volume, std::string& error) {
auto& state = getState();
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
if (!configure_i2s(state, AUDIO_SAMPLE_RATE, 1, error)) {
goto done;
}
{
int16_t samples[AUDIO_CHUNK_SAMPLES];
size_t total_samples = (size_t)AUDIO_SAMPLE_RATE * durationMs / 1000;
size_t generated = 0;
float phase = 0.0f;
float phase_step = 2.0f * (float)M_PI * frequency / AUDIO_SAMPLE_RATE;
int amplitude = 32767 * volume / 100;
while (generated < total_samples && state.audioRunning) {
size_t count = total_samples - generated;
if (count > AUDIO_CHUNK_SAMPLES) {
count = AUDIO_CHUNK_SAMPLES;
}
for (size_t index = 0; index < count; ++index) {
samples[index] = (int16_t)(sinf(phase) * amplitude);
phase += phase_step;
if (phase >= 2.0f * (float)M_PI) {
phase -= 2.0f * (float)M_PI;
}
}
size_t bytes_written = 0;
error_t result = i2s_controller_write(
state.i2sDevice,
samples,
count * sizeof(int16_t),
&bytes_written,
pdMS_TO_TICKS(250)
);
if (result != ERROR_NONE || bytes_written != count * sizeof(int16_t)) {
LOGGER.error("Tone write failed: result={} written={}", result, (unsigned)bytes_written);
set_error(error, "I2S tone playback failed");
goto done;
}
generated += count;
}
}
success = state.audioRunning;
if (!success) {
set_error(error, "Tone playback was cancelled");
}
done:
end_audio(state);
return success;
}
bool recordVoice(int durationSec, const std::string& filename, size_t& recordedBytes, std::string& error) {
auto& state = getState();
recordedBytes = 0;
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
FILE* file = NULL;
char path[256];
if (!resolve_file(filename, path, sizeof(path), error) ||
!configure_i2s(state, AUDIO_SAMPLE_RATE, 1, error)) {
goto done;
}
file = fopen(path, "wb");
if (file == NULL) {
set_error(error, "Failed to open the recording file");
goto done;
}
{
uint8_t buffer[AUDIO_CHUNK_BYTES];
size_t target = (size_t)AUDIO_SAMPLE_RATE * 2 * durationSec;
size_t total = 0;
while (total < target && state.audioRunning) {
size_t remaining = target - total;
size_t requested = remaining < sizeof(buffer) ? remaining : sizeof(buffer);
size_t bytes_read = 0;
error_t result = i2s_controller_read(
state.i2sDevice,
buffer,
requested,
&bytes_read,
pdMS_TO_TICKS(250)
);
if (result != ERROR_NONE || bytes_read == 0) {
LOGGER.error("I2S read failed: result={} read={}", result, (unsigned)bytes_read);
set_error(error, "I2S recording failed");
goto done;
}
if (fwrite(buffer, 1, bytes_read, file) != bytes_read) {
set_error(error, "Failed to write the recording file");
goto done;
}
total += bytes_read;
}
if (!state.audioRunning) {
set_error(error, "Recording was cancelled");
goto done;
}
recordedBytes = total;
success = true;
}
done:
if (file != NULL) {
fclose(file);
}
end_audio(state);
return success;
}
bool playWavMemory(const uint8_t* data, size_t size, int volume, std::string& error) {
auto& state = getState();
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
WavInfo info;
if (!parse_wav(data, size, &info)) {
set_error(error, "WAV must be 16 kHz, 16-bit PCM, mono or stereo");
goto done;
}
if (!configure_i2s(state, info.sample_rate, info.channels, error)) {
goto done;
}
{
size_t offset = 0;
while (offset < info.data_size) {
size_t chunk_size = info.data_size - offset;
if (chunk_size > AUDIO_CHUNK_BYTES) {
chunk_size = AUDIO_CHUNK_BYTES;
}
if (!write_audio(
state,
const_cast<uint8_t*>(data) + info.data_offset + offset,
chunk_size,
volume,
error)) {
goto done;
}
offset += chunk_size;
}
success = true;
}
done:
end_audio(state);
return success;
}
bool playAudioFile(const std::string& filename, int volume, std::string& error) {
auto& state = getState();
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
FILE* file = NULL;
char path[256];
long file_size = 0;
if (!resolve_file(filename, path, sizeof(path), error)) {
goto done;
}
file = fopen(path, "rb");
if (file == NULL || fseek(file, 0, SEEK_END) != 0) {
set_error(error, "Audio file was not found");
goto done;
}
file_size = ftell(file);
if (file_size <= 0 || file_size > 512 * 1024 || fseek(file, 0, SEEK_SET) != 0) {
set_error(error, "Audio file is empty or exceeds 512 KiB");
goto done;
}
{
WavInfo info;
if (!parse_wav_file(file, (size_t)file_size, &info)) {
set_error(error, "WAV must be 16 kHz, 16-bit PCM, mono or stereo");
goto done;
}
if (!configure_i2s(state, info.sample_rate, info.channels, error)) {
goto done;
}
if (fseek(file, (long)info.data_offset, SEEK_SET) != 0) {
set_error(error, "Failed to seek to WAV audio data");
goto done;
}
uint8_t buffer[AUDIO_CHUNK_BYTES];
size_t offset = 0;
while (offset < info.data_size) {
size_t chunk_size = info.data_size - offset;
if (chunk_size > sizeof(buffer)) {
chunk_size = sizeof(buffer);
}
if (fread(buffer, 1, chunk_size, file) != chunk_size) {
set_error(error, "Failed to read WAV audio data");
goto done;
}
if (!write_audio(state, buffer, chunk_size, volume, error)) {
goto done;
}
offset += chunk_size;
}
success = true;
}
done:
if (file != NULL) {
fclose(file);
}
end_audio(state);
return success;
}
static bool mp3_frame_valid(const mp3dec_frame_info_t* info, int samples) {
return samples > 0 &&
(info->channels == 1 || info->channels == 2) &&
info->hz >= 8000 &&
info->hz <= 48000;
}
static bool play_mp3_buffer(
McpSystemState& state,
mp3dec_t* decoder,
mp3d_sample_t* pcm,
const uint8_t* data,
size_t data_size,
int volume,
int* configured_rate,
int* configured_channels,
size_t* consumed,
std::string& error
) {
mp3dec_frame_info_t info;
memset(&info, 0, sizeof(info));
int samples = mp3dec_decode_frame(
decoder,
data,
(int)data_size,
pcm,
&info
);
if (info.frame_bytes <= 0) {
*consumed = 0;
return true;
}
*consumed = (size_t)info.frame_bytes;
if (samples == 0) {
return true;
}
if (!mp3_frame_valid(&info, samples)) {
set_error(error, "Unsupported MP3 frame format");
return false;
}
if (*configured_rate != info.hz || *configured_channels != info.channels) {
if (!configure_i2s(
state,
info.hz,
info.channels,
error)) {
return false;
}
*configured_rate = info.hz;
*configured_channels = info.channels;
}
return write_audio(
state,
(uint8_t*)pcm,
(size_t)samples * info.channels * sizeof(mp3d_sample_t),
volume,
error
);
}
bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& error) {
auto& state = getState();
if (data == NULL || size == 0) {
set_error(error, "MP3 data is empty");
return false;
}
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
bool decoded_audio = false;
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t));
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc(
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
);
int configured_rate = 0;
int configured_channels = 0;
size_t offset = 0;
if (decoder == NULL || pcm == NULL) {
free(decoder);
free(pcm);
set_error(error, "Out of memory for MP3 decoding");
goto done;
}
mp3dec_init(decoder);
while (offset < size && state.audioRunning) {
size_t consumed = 0;
if (!play_mp3_buffer(
state,
decoder,
pcm,
data + offset,
size - offset,
volume,
&configured_rate,
&configured_channels,
&consumed,
error)) {
free(decoder);
free(pcm);
goto done;
}
if (consumed == 0) {
break;
}
decoded_audio = decoded_audio || configured_rate != 0;
offset += consumed;
taskYIELD();
}
free(decoder);
free(pcm);
if (!state.audioRunning) {
set_error(error, "MP3 playback was cancelled");
goto done;
}
if (!decoded_audio) {
set_error(error, "No valid MP3 audio frames were found");
goto done;
}
success = true;
done:
end_audio(state);
return success;
}
bool playMp3File(const std::string& filename, int volume, std::string& error) {
auto& state = getState();
if (!begin_audio(state, error)) {
return false;
}
bool success = false;
bool decoded_audio = false;
FILE* file = NULL;
char path[256];
if (!resolve_file(filename, path, sizeof(path), error)) {
goto done;
}
file = fopen(path, "rb");
if (file == NULL) {
set_error(error, "MP3 file was not found");
goto done;
}
{
uint8_t* input = (uint8_t*)psram_malloc(MP3_INPUT_BUFFER_SIZE);
if (input == NULL) {
set_error(error, "Out of memory for MP3 decoding");
goto done;
}
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t));
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc(
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
);
if (decoder == NULL || pcm == NULL) {
free(decoder);
free(pcm);
free(input);
set_error(error, "Out of memory for MP3 decoding");
goto done;
}
mp3dec_init(decoder);
int configured_rate = 0;
int configured_channels = 0;
size_t buffered = 0;
bool end_of_file = false;
while (state.audioRunning) {
if (!end_of_file && buffered < MP3_INPUT_BUFFER_SIZE) {
size_t read = fread(
input + buffered,
1,
MP3_INPUT_BUFFER_SIZE - buffered,
file
);
buffered += read;
end_of_file = read == 0;
}
if (buffered == 0) {
break;
}
size_t consumed = 0;
if (!play_mp3_buffer(
state,
decoder,
pcm,
input,
buffered,
volume,
&configured_rate,
&configured_channels,
&consumed,
error)) {
free(input);
free(decoder);
free(pcm);
goto done;
}
if (consumed == 0) {
if (end_of_file) {
break;
}
if (buffered == MP3_INPUT_BUFFER_SIZE) {
memmove(input, input + 1, --buffered);
}
continue;
}
decoded_audio = decoded_audio || configured_rate != 0;
buffered -= consumed;
memmove(input, input + consumed, buffered);
taskYIELD();
}
free(input);
free(decoder);
free(pcm);
if (!state.audioRunning) {
set_error(error, "MP3 playback was cancelled");
goto done;
}
if (!decoded_audio) {
set_error(error, "No valid MP3 audio frames were found");
goto done;
}
success = true;
}
done:
if (file != NULL) {
fclose(file);
}
end_audio(state);
return success;
}
} // namespace
#endif