Record 16 kHz mono PCM to microSD /rec, with level meter and DAC2 playback. Co-authored-by: Cursor <cursoragent@cursor.com>
452 lines
14 KiB
C++
452 lines
14 KiB
C++
/**
|
|
* CYD LVGL INMP441 WAV recorder
|
|
* 2.8 (2432S028R): WS=22 SCK=27 SD=35, TFT_BL=21
|
|
* 3.5C (3248S035C): WS=22 SCK=21 SD=35, TFT_BL=27 — see ../CYD_PROJECT_GUIDE.md
|
|
*
|
|
* Record mono 16 kHz PCM WAV to microSD /rec/Rxxxx.WAV
|
|
* Playback via DAC2 GPIO26 only (never GPIO25).
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <lvgl.h>
|
|
#include <TFT_eSPI.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include "pins.h"
|
|
#include "touch.h"
|
|
#include "sdcard.h"
|
|
#include "recorder.h"
|
|
#include "player.h"
|
|
|
|
#if defined(CYD_BOARD_3248S035)
|
|
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 20 * (LV_COLOR_DEPTH / 8))
|
|
#else
|
|
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
|
|
#endif
|
|
|
|
#if defined(CYD_BOARD_3248S035)
|
|
#define UI_M 12
|
|
#define UI_G 10
|
|
#define UI_BTN_H 52
|
|
#define UI_BAR_H 18
|
|
#else
|
|
#define UI_M 8
|
|
#define UI_G 6
|
|
#define UI_BTN_H 40
|
|
#define UI_BAR_H 14
|
|
#endif
|
|
|
|
static const lv_color_t COL_BG = lv_color_hex(0x0A1018);
|
|
static const lv_color_t COL_PANEL = lv_color_hex(0x121C2A);
|
|
static const lv_color_t COL_EDGE = lv_color_hex(0x1E2C40);
|
|
static const lv_color_t COL_TEXT = lv_color_hex(0xF2F5FA);
|
|
static const lv_color_t COL_MUTED = lv_color_hex(0x7E90A8);
|
|
static const lv_color_t COL_ACCENT = lv_color_hex(0x3DBF8C);
|
|
static const lv_color_t COL_OK = lv_color_hex(0x4CAF7A);
|
|
static const lv_color_t COL_WARN = lv_color_hex(0xE6B84D);
|
|
static const lv_color_t COL_BAD = lv_color_hex(0xE05A5A);
|
|
static const lv_color_t COL_REC = lv_color_hex(0xE04545);
|
|
static const lv_color_t COL_ARC_BG = lv_color_hex(0x243247);
|
|
|
|
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
|
|
|
|
static lv_obj_t *lbl_status;
|
|
static lv_obj_t *lbl_uptime;
|
|
static lv_obj_t *lbl_hint;
|
|
static lv_obj_t *lbl_time;
|
|
static lv_obj_t *lbl_file;
|
|
static lv_obj_t *lbl_level;
|
|
static lv_obj_t *lbl_sd;
|
|
static lv_obj_t *led_status;
|
|
static lv_obj_t *bar_level;
|
|
static lv_obj_t *btn_rec;
|
|
static lv_obj_t *lbl_rec;
|
|
static lv_obj_t *btn_play;
|
|
static lv_obj_t *lbl_play;
|
|
|
|
static uint8_t bright_level = 2;
|
|
static const uint8_t BRIGHT_PWM[4] = {40, 90, 160, 255};
|
|
static uint32_t hint_until_ms = 0;
|
|
static uint32_t last_ui_ms = 0;
|
|
static bool mic_ok = false;
|
|
static bool card_ok = false;
|
|
|
|
static void set_backlight(uint8_t level) {
|
|
bright_level = level & 0x03;
|
|
ledcWrite(0, BRIGHT_PWM[bright_level]);
|
|
}
|
|
|
|
static void show_hint(const char *text, uint32_t ms) {
|
|
lv_label_set_text(lbl_hint, text);
|
|
lv_obj_remove_flag(lbl_hint, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(lbl_hint);
|
|
hint_until_ms = millis() + ms;
|
|
}
|
|
|
|
static const char *basename_of(const char *path) {
|
|
if (!path || !path[0]) return "-";
|
|
const char *p = strrchr(path, '/');
|
|
return p ? p + 1 : path;
|
|
}
|
|
|
|
static void touchscreen_read(lv_indev_t *indev, lv_indev_data_t *data) {
|
|
static bool was_pressed = false;
|
|
static uint32_t press_ms = 0;
|
|
static bool consumed = false;
|
|
|
|
touch_read_cb(indev, data);
|
|
|
|
if (data->state == LV_INDEV_STATE_PRESSED) {
|
|
if (!was_pressed) {
|
|
was_pressed = true;
|
|
press_ms = millis();
|
|
consumed = false;
|
|
}
|
|
} else if (was_pressed) {
|
|
was_pressed = false;
|
|
uint32_t held = millis() - press_ms;
|
|
if (!consumed && held < 350) {
|
|
/* Brightness only if press was not on a button — LVGL already handled
|
|
* buttons; use short idle-area taps via header region. */
|
|
if (data->point.y < 28) {
|
|
set_backlight((bright_level + 1) & 0x03);
|
|
char buf[24];
|
|
lv_snprintf(buf, sizeof(buf), "brightness %u/3", (unsigned)bright_level);
|
|
show_hint(buf, 900);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static lv_obj_t *make_panel(lv_obj_t *parent, int x, int y, int w, int h) {
|
|
lv_obj_t *p = lv_obj_create(parent);
|
|
lv_obj_set_pos(p, x, y);
|
|
lv_obj_set_size(p, w, h);
|
|
lv_obj_set_style_bg_color(p, COL_PANEL, 0);
|
|
lv_obj_set_style_bg_opa(p, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(p, COL_EDGE, 0);
|
|
lv_obj_set_style_border_width(p, 1, 0);
|
|
lv_obj_set_style_radius(p, 12, 0);
|
|
lv_obj_set_style_pad_all(p, 8, 0);
|
|
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
|
return p;
|
|
}
|
|
|
|
static void style_btn(lv_obj_t *btn, lv_color_t bg) {
|
|
lv_obj_set_style_bg_color(btn, bg, 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_radius(btn, 12, 0);
|
|
lv_obj_set_style_border_width(btn, 0, 0);
|
|
lv_obj_set_style_shadow_width(btn, 0, 0);
|
|
lv_obj_set_ext_click_area(btn, 8);
|
|
}
|
|
|
|
static void on_rec_clicked(lv_event_t *e) {
|
|
(void)e;
|
|
if (player_is_playing()) {
|
|
player_stop();
|
|
show_hint("stopped play", 800);
|
|
}
|
|
|
|
if (recorder_is_active()) {
|
|
recorder_stop();
|
|
show_hint("saved", 1200);
|
|
} else {
|
|
if (!card_ok && !sd_init()) {
|
|
show_hint("insert microSD", 2000);
|
|
return;
|
|
}
|
|
card_ok = sd_ready();
|
|
if (!recorder_start()) {
|
|
show_hint(recorder_status(), 2000);
|
|
return;
|
|
}
|
|
show_hint("recording…", 1000);
|
|
}
|
|
}
|
|
|
|
static void on_play_clicked(lv_event_t *e) {
|
|
(void)e;
|
|
if (recorder_is_active()) {
|
|
show_hint("stop REC first", 1200);
|
|
return;
|
|
}
|
|
if (player_is_playing()) {
|
|
player_stop();
|
|
show_hint("stopped", 800);
|
|
return;
|
|
}
|
|
const char *path = recorder_last_path();
|
|
if (!path || !path[0]) {
|
|
show_hint("no recording", 1200);
|
|
return;
|
|
}
|
|
if (!player_play(path)) {
|
|
show_hint(player_status(), 1500);
|
|
return;
|
|
}
|
|
show_hint("playing", 800);
|
|
}
|
|
|
|
static void create_ui(void) {
|
|
lv_obj_t *scr = lv_screen_active();
|
|
lv_obj_set_style_bg_color(scr, COL_BG, 0);
|
|
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
|
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t *title = lv_label_create(scr);
|
|
lv_label_set_text(title, "RECORD");
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(title, COL_TEXT, 0);
|
|
lv_obj_set_pos(title, UI_M, 4);
|
|
|
|
lbl_uptime = lv_label_create(scr);
|
|
lv_label_set_text(lbl_uptime, "up 00:00");
|
|
lv_obj_set_style_text_font(lbl_uptime, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(lbl_uptime, COL_MUTED, 0);
|
|
lv_obj_align_to(lbl_uptime, title, LV_ALIGN_OUT_RIGHT_MID, 8, 1);
|
|
|
|
led_status = lv_led_create(scr);
|
|
lv_obj_set_size(led_status, 9, 9);
|
|
lv_obj_align(led_status, LV_ALIGN_TOP_RIGHT, -UI_M, 6);
|
|
lv_led_set_color(led_status, COL_WARN);
|
|
lv_led_on(led_status);
|
|
|
|
lbl_status = lv_label_create(scr);
|
|
lv_label_set_text(lbl_status, "INIT");
|
|
lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(lbl_status, COL_MUTED, 0);
|
|
lv_obj_align(lbl_status, LV_ALIGN_TOP_RIGHT, -(UI_M + 16), 4);
|
|
|
|
lbl_hint = lv_label_create(scr);
|
|
lv_label_set_text(lbl_hint, "");
|
|
lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(lbl_hint, COL_WARN, 0);
|
|
lv_obj_set_style_bg_color(lbl_hint, COL_BG, 0);
|
|
lv_obj_set_style_bg_opa(lbl_hint, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_pad_hor(lbl_hint, 6, 0);
|
|
lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, 0, 4);
|
|
lv_obj_add_flag(lbl_hint, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
const int content_w = SCREEN_WIDTH - 2 * UI_M;
|
|
const int y0 = 28;
|
|
|
|
lv_obj_t *p_main = make_panel(scr, UI_M, y0, content_w,
|
|
SCREEN_HEIGHT - y0 - UI_M);
|
|
|
|
lbl_time = lv_label_create(p_main);
|
|
lv_label_set_text(lbl_time, "00:00");
|
|
lv_obj_set_style_text_font(lbl_time, &lv_font_montserrat_28, 0);
|
|
lv_obj_set_style_text_color(lbl_time, COL_TEXT, 0);
|
|
lv_obj_align(lbl_time, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
|
|
lbl_level = lv_label_create(p_main);
|
|
lv_label_set_text(lbl_level, "--.- dB");
|
|
lv_obj_set_style_text_font(lbl_level, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(lbl_level, COL_MUTED, 0);
|
|
lv_obj_align(lbl_level, LV_ALIGN_TOP_RIGHT, 0, 8);
|
|
|
|
bar_level = lv_bar_create(p_main);
|
|
lv_obj_set_size(bar_level, content_w - 16, UI_BAR_H);
|
|
lv_obj_align(bar_level, LV_ALIGN_TOP_MID, 0, 40);
|
|
lv_bar_set_range(bar_level, 30, 110);
|
|
lv_bar_set_value(bar_level, 30, LV_ANIM_OFF);
|
|
lv_obj_set_style_bg_color(bar_level, COL_ARC_BG, LV_PART_MAIN);
|
|
lv_obj_set_style_bg_opa(bar_level, LV_OPA_COVER, LV_PART_MAIN);
|
|
lv_obj_set_style_radius(bar_level, 6, LV_PART_MAIN);
|
|
lv_obj_set_style_bg_color(bar_level, COL_ACCENT, LV_PART_INDICATOR);
|
|
lv_obj_set_style_radius(bar_level, 6, LV_PART_INDICATOR);
|
|
|
|
lbl_file = lv_label_create(p_main);
|
|
lv_label_set_text(lbl_file, "file: -");
|
|
lv_obj_set_style_text_font(lbl_file, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(lbl_file, COL_MUTED, 0);
|
|
lv_obj_align(lbl_file, LV_ALIGN_TOP_LEFT, 0, 40 + UI_BAR_H + 10);
|
|
|
|
lbl_sd = lv_label_create(p_main);
|
|
lv_label_set_text(lbl_sd, "SD: -");
|
|
lv_obj_set_style_text_font(lbl_sd, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(lbl_sd, COL_MUTED, 0);
|
|
lv_obj_align(lbl_sd, LV_ALIGN_TOP_LEFT, 0, 40 + UI_BAR_H + 28);
|
|
|
|
const int btn_y = 40 + UI_BAR_H + 52;
|
|
const int btn_w = (content_w - 16 - UI_G) / 2;
|
|
|
|
btn_rec = lv_button_create(p_main);
|
|
lv_obj_set_size(btn_rec, btn_w, UI_BTN_H);
|
|
lv_obj_align(btn_rec, LV_ALIGN_TOP_LEFT, 0, btn_y);
|
|
style_btn(btn_rec, COL_REC);
|
|
lv_obj_add_event_cb(btn_rec, on_rec_clicked, LV_EVENT_CLICKED, nullptr);
|
|
lbl_rec = lv_label_create(btn_rec);
|
|
lv_label_set_text(lbl_rec, "REC");
|
|
lv_obj_set_style_text_font(lbl_rec, &lv_font_montserrat_16, 0);
|
|
lv_obj_set_style_text_color(lbl_rec, COL_TEXT, 0);
|
|
lv_obj_center(lbl_rec);
|
|
|
|
btn_play = lv_button_create(p_main);
|
|
lv_obj_set_size(btn_play, btn_w, UI_BTN_H);
|
|
lv_obj_align(btn_play, LV_ALIGN_TOP_RIGHT, 0, btn_y);
|
|
style_btn(btn_play, COL_ACCENT);
|
|
lv_obj_add_event_cb(btn_play, on_play_clicked, LV_EVENT_CLICKED, nullptr);
|
|
lbl_play = lv_label_create(btn_play);
|
|
lv_label_set_text(lbl_play, "PLAY");
|
|
lv_obj_set_style_text_font(lbl_play, &lv_font_montserrat_16, 0);
|
|
lv_obj_set_style_text_color(lbl_play, COL_TEXT, 0);
|
|
lv_obj_center(lbl_play);
|
|
|
|
lv_obj_t *hint2 = lv_label_create(p_main);
|
|
lv_label_set_text(hint2, "tap header: brightness | max 120s");
|
|
lv_obj_set_style_text_font(hint2, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(hint2, COL_MUTED, 0);
|
|
lv_obj_align(hint2, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
}
|
|
|
|
static void refresh_ui(void) {
|
|
char buf[48];
|
|
uint32_t up = millis() / 1000;
|
|
lv_snprintf(buf, sizeof(buf), "up %02lu:%02lu bl%u",
|
|
(unsigned long)(up / 60), (unsigned long)(up % 60),
|
|
(unsigned)bright_level);
|
|
lv_label_set_text(lbl_uptime, buf);
|
|
|
|
if (hint_until_ms && (int32_t)(millis() - hint_until_ms) >= 0) {
|
|
hint_until_ms = 0;
|
|
lv_obj_add_flag(lbl_hint, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
const bool rec = recorder_is_active();
|
|
const bool play = player_is_playing();
|
|
|
|
if (rec) {
|
|
uint32_t ms = recorder_elapsed_ms();
|
|
lv_snprintf(buf, sizeof(buf), "%02lu:%02lu",
|
|
(unsigned long)(ms / 60000),
|
|
(unsigned long)((ms / 1000) % 60));
|
|
lv_label_set_text(lbl_time, buf);
|
|
lv_obj_set_style_text_color(lbl_time, COL_REC, 0);
|
|
lv_label_set_text(lbl_rec, "STOP");
|
|
lv_obj_set_style_bg_color(btn_rec, COL_WARN, 0);
|
|
lv_label_set_text(lbl_status, "REC");
|
|
lv_led_set_color(led_status, COL_REC);
|
|
} else if (play) {
|
|
lv_label_set_text(lbl_time, "PLAY");
|
|
lv_obj_set_style_text_color(lbl_time, COL_ACCENT, 0);
|
|
lv_label_set_text(lbl_rec, "REC");
|
|
lv_obj_set_style_bg_color(btn_rec, COL_REC, 0);
|
|
lv_label_set_text(lbl_play, "STOP");
|
|
lv_obj_set_style_bg_color(btn_play, COL_WARN, 0);
|
|
lv_label_set_text(lbl_status, "PLAY");
|
|
lv_led_set_color(led_status, COL_ACCENT);
|
|
} else {
|
|
lv_label_set_text(lbl_time, "00:00");
|
|
lv_obj_set_style_text_color(lbl_time, COL_TEXT, 0);
|
|
lv_label_set_text(lbl_rec, "REC");
|
|
lv_obj_set_style_bg_color(btn_rec, COL_REC, 0);
|
|
lv_label_set_text(lbl_play, "PLAY");
|
|
lv_obj_set_style_bg_color(btn_play, COL_ACCENT, 0);
|
|
if (!mic_ok) {
|
|
lv_label_set_text(lbl_status, "NO MIC");
|
|
lv_led_set_color(led_status, COL_BAD);
|
|
} else if (!card_ok) {
|
|
lv_label_set_text(lbl_status, "NO SD");
|
|
lv_led_set_color(led_status, COL_WARN);
|
|
} else {
|
|
lv_label_set_text(lbl_status, "READY");
|
|
lv_led_set_color(led_status, COL_OK);
|
|
}
|
|
}
|
|
|
|
float db = recorder_level_db();
|
|
if (!isnan(db)) {
|
|
lv_snprintf(buf, sizeof(buf), "%.0f dB", db);
|
|
lv_label_set_text(lbl_level, buf);
|
|
int v = (int)lroundf(constrain(db, 30.0f, 110.0f));
|
|
lv_bar_set_value(bar_level, v, LV_ANIM_OFF);
|
|
lv_color_t c = (db < 70) ? COL_ACCENT : ((db < 90) ? COL_WARN : COL_BAD);
|
|
lv_obj_set_style_bg_color(bar_level, c, LV_PART_INDICATOR);
|
|
lv_obj_set_style_text_color(lbl_level, c, 0);
|
|
} else {
|
|
lv_label_set_text(lbl_level, "-- dB");
|
|
}
|
|
|
|
lv_snprintf(buf, sizeof(buf), "file: %s", basename_of(recorder_last_path()));
|
|
lv_label_set_text(lbl_file, buf);
|
|
|
|
if (card_ok) {
|
|
uint64_t free_mb = sd_free_bytes() / (1024ULL * 1024ULL);
|
|
uint64_t tot_mb = sd_total_bytes() / (1024ULL * 1024ULL);
|
|
lv_snprintf(buf, sizeof(buf), "SD: %llu/%llu MB free 16kHz WAV",
|
|
(unsigned long long)free_mb, (unsigned long long)tot_mb);
|
|
lv_label_set_text(lbl_sd, buf);
|
|
lv_obj_set_style_text_color(lbl_sd, COL_MUTED, 0);
|
|
} else {
|
|
lv_label_set_text(lbl_sd, "SD: not mounted — insert card");
|
|
lv_obj_set_style_text_color(lbl_sd, COL_BAD, 0);
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(200);
|
|
|
|
ledcSetup(0, 5000, 8);
|
|
ledcAttachPin(TFT_BL_PIN, 0);
|
|
set_backlight(2);
|
|
|
|
Serial.println("CYD LVGL INMP441 WAV Recorder");
|
|
Serial.printf("LVGL %d.%d.%d WS=%d SCK=%d SD=%d BL=%d spk=%d\n",
|
|
lv_version_major(), lv_version_minor(), lv_version_patch(),
|
|
I2S_WS_PIN, I2S_SCK_PIN, I2S_SD_PIN, TFT_BL_PIN, SPEAKER_PIN);
|
|
|
|
/* 3.5: GT911 may pulse INT(=21) during addr select; release before I2S SCK. */
|
|
touch_init();
|
|
|
|
card_ok = sd_init();
|
|
mic_ok = recorder_mic_init();
|
|
player_init();
|
|
|
|
Serial.println(mic_ok ? "I2S mic OK" : "I2S mic FAIL — check wiring");
|
|
Serial.println(card_ok ? "SD OK" : "SD FAIL — insert FAT32 microSD");
|
|
#if defined(CYD_BOARD_3248S035)
|
|
Serial.println("3.5C: SCK=GPIO21 (shared GT911 INT; touch is poll-only)");
|
|
#endif
|
|
|
|
lv_init();
|
|
|
|
#if defined(CYD_BOARD_3248S035)
|
|
lv_display_t *disp = lv_tft_espi_create(320, 480, draw_buf, sizeof(draw_buf));
|
|
#else
|
|
lv_display_t *disp = lv_tft_espi_create(240, 320, draw_buf, sizeof(draw_buf));
|
|
#endif
|
|
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
|
|
|
|
lv_indev_t *indev = lv_indev_create();
|
|
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
|
lv_indev_set_read_cb(indev, touchscreen_read);
|
|
lv_indev_set_display(indev, disp);
|
|
|
|
create_ui();
|
|
if (!mic_ok) show_hint("mic init fail", 3000);
|
|
else if (!card_ok) show_hint("insert microSD", 3000);
|
|
|
|
refresh_ui();
|
|
}
|
|
|
|
void loop() {
|
|
/* Drain mic (and write WAV while recording) often enough for DMA. */
|
|
recorder_tick();
|
|
recorder_tick();
|
|
player_tick();
|
|
|
|
uint32_t now = millis();
|
|
if (now - last_ui_ms >= 100) {
|
|
last_ui_ms = now;
|
|
refresh_ui();
|
|
}
|
|
|
|
lv_timer_handler();
|
|
lv_tick_inc(5);
|
|
delay(5);
|
|
}
|