diff --git a/Apps/AudioTest/CMakeLists.txt b/Apps/AudioTest/CMakeLists.txt new file mode 100644 index 0000000..551e900 --- /dev/null +++ b/Apps/AudioTest/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(AudioTest) +tactility_project(AudioTest) diff --git a/Apps/AudioTest/main/CMakeLists.txt b/Apps/AudioTest/main/CMakeLists.txt new file mode 100644 index 0000000..db2068e --- /dev/null +++ b/Apps/AudioTest/main/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) + +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilitySDK +) diff --git a/Apps/AudioTest/main/Source/main.c b/Apps/AudioTest/main/Source/main.c new file mode 100644 index 0000000..45c860a --- /dev/null +++ b/Apps/AudioTest/main/Source/main.c @@ -0,0 +1,337 @@ +/** + * 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; +} diff --git a/Apps/AudioTest/manifest.properties b/Apps/AudioTest/manifest.properties new file mode 100644 index 0000000..b16f61b --- /dev/null +++ b/Apps/AudioTest/manifest.properties @@ -0,0 +1,10 @@ +[manifest] +version=0.1 +[target] +sdk=0.7.0-dev +platforms=esp32s3 +[app] +id=one.tactility.audiotest +versionName=0.1.0 +versionCode=1 +name=Audio Test