#ifdef ESP_PLATFORM #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MINIMP3_IMPLEMENTATION #define MINIMP3_NO_SIMD #include namespace tt::mcp { static const auto LOGGER = Logger("McpSystem"); #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*)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(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(sample_rate), .bits_per_sample = AUDIO_BITS_PER_SAMPLE, .channel_left = 0, .channel_right = static_cast(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(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*)malloc(sizeof(mp3dec_t)); mp3d_sample_t* pcm = (mp3d_sample_t*)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*)malloc(MP3_INPUT_BUFFER_SIZE); if (input == NULL) { set_error(error, "Out of memory for MP3 decoding"); goto done; } mp3dec_t* decoder = (mp3dec_t*)malloc(sizeof(mp3dec_t)); mp3d_sample_t* pcm = (mp3d_sample_t*)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