/** * Audio Test - Record & Playback * * Records from the ES8311 microphone via I2S and plays it back through * the FM8002E speaker amplifier. Uses the Tactility kernel I2S controller API. * * UI: Two buttons - Record and Play * Record: holds recording for up to ~3 seconds (16kHz 16-bit mono ≈ 96KB) * Play: plays back the last recorded buffer */ #include #include #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #define TAG "AudioTest" /* ─── Audio params ─── */ #define SAMPLE_RATE 16000 #define BITS_PER_SAMPLE 16 #define RECORD_SECONDS 3 /* 16-bit mono → 2 bytes per sample */ #define AUDIO_BUF_SIZE (SAMPLE_RATE * (BITS_PER_SAMPLE / 8) * RECORD_SECONDS) /* Chunk size for read/write calls (512 samples = 1024 bytes at 16-bit) */ #define CHUNK_SAMPLES 512 #define CHUNK_BYTES (CHUNK_SAMPLES * (BITS_PER_SAMPLE / 8)) /* ─── App state ─── */ typedef enum { STATE_IDLE, STATE_RECORDING, STATE_PLAYING } AudioState; typedef struct { struct Device* i2s_dev; uint8_t* audio_buf; size_t recorded_bytes; AudioState state; lv_obj_t* btn_record; lv_obj_t* btn_play; lv_obj_t* lbl_status; lv_obj_t* bar_progress; TaskHandle_t task_handle; } AppCtx; /* ─── Forward decls ─── */ static void record_task(void* arg); static void play_task(void* arg); static void update_ui(AppCtx* ctx); /* ─── Callbacks ─── */ static void on_record_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); if (ctx->state != STATE_IDLE) return; if (ctx->i2s_dev == NULL) return; ctx->state = STATE_RECORDING; ctx->recorded_bytes = 0; update_ui(ctx); xTaskCreate(record_task, "audio_rec", 4096, ctx, 5, &ctx->task_handle); } static void on_play_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); if (ctx->state != STATE_IDLE) return; if (ctx->recorded_bytes == 0) return; if (ctx->i2s_dev == NULL) return; ctx->state = STATE_PLAYING; update_ui(ctx); xTaskCreate(play_task, "audio_play", 4096, ctx, 5, &ctx->task_handle); } /* ─── Update UI labels/buttons from any context ─── */ static void update_ui(AppCtx* ctx) { if (ctx->lbl_status == NULL) return; switch (ctx->state) { case STATE_RECORDING: lv_label_set_text(ctx->lbl_status, "🔴 Recording..."); lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED); lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED); break; case STATE_PLAYING: lv_label_set_text(ctx->lbl_status, "🔊 Playing..."); lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED); lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED); break; case STATE_IDLE: default: if (ctx->recorded_bytes > 0) { char buf[64]; snprintf(buf, sizeof(buf), "Ready (%u bytes recorded)", (unsigned)ctx->recorded_bytes); lv_label_set_text(ctx->lbl_status, buf); } else { lv_label_set_text(ctx->lbl_status, "Press Record to capture audio"); } lv_obj_clear_state(ctx->btn_record, LV_STATE_DISABLED); if (ctx->recorded_bytes > 0) { lv_obj_clear_state(ctx->btn_play, LV_STATE_DISABLED); } else { lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED); } break; } } /* ─── Record task ─── */ static void record_task(void* arg) { AppCtx* ctx = (AppCtx*)arg; size_t total = 0; size_t bytes_read = 0; /* Configure I2S for recording */ struct I2sConfig cfg = { .communication_format = I2S_FORMAT_STAND_I2S, .sample_rate = SAMPLE_RATE, .bits_per_sample = BITS_PER_SAMPLE, .channel_left = 0, .channel_right = I2S_CHANNEL_NONE }; device_lock(ctx->i2s_dev); error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg); device_unlock(ctx->i2s_dev); if (err != ERROR_NONE) { ESP_LOGE(TAG, "Failed to set I2S config for recording: %d", err); tt_lvgl_lock(portMAX_DELAY); ctx->state = STATE_IDLE; update_ui(ctx); tt_lvgl_unlock(); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "Recording started (max %d bytes)", AUDIO_BUF_SIZE); while (total < AUDIO_BUF_SIZE && ctx->state == STATE_RECORDING) { size_t remaining = AUDIO_BUF_SIZE - total; size_t to_read = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES; err = i2s_controller_read(ctx->i2s_dev, ctx->audio_buf + total, to_read, &bytes_read, pdMS_TO_TICKS(1000)); if (err != ERROR_NONE) { ESP_LOGE(TAG, "I2S read error: %d", err); break; } total += bytes_read; /* Update progress bar */ int pct = (int)((total * 100) / AUDIO_BUF_SIZE); tt_lvgl_lock(portMAX_DELAY); lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF); tt_lvgl_unlock(); } ctx->recorded_bytes = total; ctx->state = STATE_IDLE; ctx->task_handle = NULL; ESP_LOGI(TAG, "Recording complete: %u bytes", (unsigned)total); tt_lvgl_lock(portMAX_DELAY); lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF); update_ui(ctx); tt_lvgl_unlock(); vTaskDelete(NULL); } /* ─── Play task ─── */ static void play_task(void* arg) { AppCtx* ctx = (AppCtx*)arg; size_t total = 0; size_t bytes_written = 0; /* Configure I2S for playback */ struct I2sConfig cfg = { .communication_format = I2S_FORMAT_STAND_I2S, .sample_rate = SAMPLE_RATE, .bits_per_sample = BITS_PER_SAMPLE, .channel_left = 0, .channel_right = I2S_CHANNEL_NONE }; device_lock(ctx->i2s_dev); error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg); device_unlock(ctx->i2s_dev); if (err != ERROR_NONE) { ESP_LOGE(TAG, "Failed to set I2S config for playback: %d", err); tt_lvgl_lock(portMAX_DELAY); ctx->state = STATE_IDLE; update_ui(ctx); tt_lvgl_unlock(); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "Playback started (%u bytes)", (unsigned)ctx->recorded_bytes); while (total < ctx->recorded_bytes && ctx->state == STATE_PLAYING) { size_t remaining = ctx->recorded_bytes - total; size_t to_write = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES; err = i2s_controller_write(ctx->i2s_dev, ctx->audio_buf + total, to_write, &bytes_written, pdMS_TO_TICKS(1000)); if (err != ERROR_NONE) { ESP_LOGE(TAG, "I2S write error: %d", err); break; } total += bytes_written; /* Update progress bar */ int pct = (int)((total * 100) / ctx->recorded_bytes); tt_lvgl_lock(portMAX_DELAY); lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF); tt_lvgl_unlock(); } ctx->state = STATE_IDLE; ctx->task_handle = NULL; ESP_LOGI(TAG, "Playback complete"); tt_lvgl_lock(portMAX_DELAY); lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF); update_ui(ctx); tt_lvgl_unlock(); vTaskDelete(NULL); } /* ─── App lifecycle ─── */ static AppCtx g_ctx; static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { memset(&g_ctx, 0, sizeof(g_ctx)); /* Allocate audio buffer */ g_ctx.audio_buf = (uint8_t*)malloc(AUDIO_BUF_SIZE); if (g_ctx.audio_buf == NULL) { ESP_LOGE(TAG, "Failed to allocate audio buffer (%d bytes)", AUDIO_BUF_SIZE); } /* Find I2S device */ g_ctx.i2s_dev = device_find_by_name("i2s0"); if (g_ctx.i2s_dev == NULL) { ESP_LOGE(TAG, "I2S device 'i2s0' not found!"); } else { ESP_LOGI(TAG, "Found I2S device: %s", g_ctx.i2s_dev->name); } /* ─── UI ─── */ lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); /* Status label */ g_ctx.lbl_status = lv_label_create(parent); lv_label_set_text(g_ctx.lbl_status, "Initializing..."); lv_obj_set_width(g_ctx.lbl_status, lv_pct(90)); lv_label_set_long_mode(g_ctx.lbl_status, LV_LABEL_LONG_WRAP); lv_obj_set_style_text_align(g_ctx.lbl_status, LV_TEXT_ALIGN_CENTER, 0); lv_obj_align(g_ctx.lbl_status, LV_ALIGN_TOP_MID, 0, 50); /* Progress bar */ g_ctx.bar_progress = lv_bar_create(parent); lv_obj_set_size(g_ctx.bar_progress, lv_pct(80), 10); lv_bar_set_range(g_ctx.bar_progress, 0, 100); lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF); lv_obj_align(g_ctx.bar_progress, LV_ALIGN_CENTER, 0, -10); /* Button container */ lv_obj_t* btn_container = lv_obj_create(parent); lv_obj_remove_style_all(btn_container); lv_obj_set_size(btn_container, lv_pct(90), 50); lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_align(btn_container, LV_ALIGN_CENTER, 0, 30); /* Record button */ g_ctx.btn_record = lv_btn_create(btn_container); lv_obj_set_size(g_ctx.btn_record, 100, 40); lv_obj_t* lbl_rec = lv_label_create(g_ctx.btn_record); lv_label_set_text(lbl_rec, LV_SYMBOL_AUDIO " Record"); lv_obj_center(lbl_rec); lv_obj_add_event_cb(g_ctx.btn_record, on_record_click, LV_EVENT_CLICKED, &g_ctx); /* Play button */ g_ctx.btn_play = lv_btn_create(btn_container); lv_obj_set_size(g_ctx.btn_play, 100, 40); lv_obj_t* lbl_play = lv_label_create(g_ctx.btn_play); lv_label_set_text(lbl_play, LV_SYMBOL_PLAY " Play"); lv_obj_center(lbl_play); lv_obj_add_event_cb(g_ctx.btn_play, on_play_click, LV_EVENT_CLICKED, &g_ctx); /* Check initialization status */ if (g_ctx.i2s_dev == NULL) { lv_label_set_text(g_ctx.lbl_status, "ERROR: I2S device not found"); lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED); lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED); } else if (g_ctx.audio_buf == NULL) { lv_label_set_text(g_ctx.lbl_status, "ERROR: Out of memory"); lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED); lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED); } else { g_ctx.state = STATE_IDLE; update_ui(&g_ctx); } } int main(int argc, char* argv[]) { tt_app_register((AppRegistration) { .onShow = onShowApp }); return 0; }