feat(audiotest): add AudioTest application for testing recording and playback
This commit is contained in:
@@ -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)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -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 <tt_app.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user