/** * 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 #include #include #include #include #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); }