> For the complete documentation index, see [llms.txt](https://docs.aic-eec.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aic-eec.com/interfacing-with-infineon-psoc-tm-edge/hmi-development/spectrum-analyzer/ux-ui-design/mic-visualizer.md).

# Mic Visualizer

## Lab 4: Microphone Visualizer

### Part 3 - Oscilloscope & Signal Processing

***

### 1. โครงสร้างภาพรวมของ Lab

#### Why? - ทำไมต้องเรียนรู้เรื่องนี้

* **Audio Level Monitoring**: VU Meter เป็นเครื่องมือพื้นฐานในทุกระบบที่เกี่ยวกับเสียง ตั้งแต่สตูดิโอบันทึกเสียงจนถึงระบบ PA ในอาคาร
* **Peak Detection**: การตรวจจับค่า Peak เป็นเทคนิคสำคัญในระบบ Power Systems (surge detection), Audio (clipping prevention) และ Vibration Monitoring
* **Threshold-based Alarming**: ระบบเตือนภัยในโรงงาน (noise level > 85 dBA), ระบบ Power Quality (voltage sag/swell) ใช้หลักการ threshold เดียวกัน
* **Real-time Data Streaming**: Pattern การรับข้อมูลต่อเนื่องจาก sensor แล้วแสดงผลแบบ real-time ใช้ได้กับทุก sensor ไม่เฉพาะ microphone

#### What? - จะได้เรียนรู้อะไร

1. **Simulated Mic Input**: จำลองสัญญาณ PDM Microphone (เพราะ PC Sim ไม่มี mic จริง)
2. **VU Meter Display**: สร้าง Level Meter ด้วย `lv_bar` พร้อมสี gradient
3. **Peak Detection & Hold**: ตรวจจับค่า Peak แล้วค้างไว้ก่อนลดลง
4. **Color Threshold**: Green (safe) / Yellow (warning) / Red (critical)
5. **Clipping Detection**: ตรวจจับเมื่อสัญญาณเกิน threshold

#### How? - จะทำอย่างไร

1. จำลองข้อมูล Microphone ด้วย Sine + Noise (amplitude แปรผัน)
2. สร้าง Waveform chart แสดง signal
3. สร้าง VU Meter Bar แนวตั้ง พร้อมสี zone
4. คำนวณ Peak-to-Peak, RMS, Level % แบบ real-time
5. เพิ่ม Peak Hold indicator และ Clipping warning

<figure><img src="https://1856353139-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MClo3nC-1US0rbK8Qau%2Fuploads%2FHmhI9ARDyEgoNSS7Qs5f%2Fweek5_ex4_mic_visualizer.gif?alt=media&amp;token=ab564b50-aff9-4cbe-ae64-86736c113526" alt=""><figcaption></figcaption></figure>

***

### 2. หลักการทำงานและ Flowchart

#### 2.1 PDM Microphone

```
┌─────────────────────────────────────────────────────────────┐
│                   PDM MICROPHONE                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   PDM = Pulse Density Modulation                            │
│                                                             │
│   Sound → PDM Mic → 1-bit stream → Decimation → 16-bit PCM  │
│                                                             │
│   ┌─────┐     ┌──────────┐     ┌───────────┐                │
│   │ Mic │────▶│PDM Data  │────▶│ Decimator │────▶ Samples   │
│   └─────┘     │(1-bit)   │     │ Filter    │     (int16_t)  │
│               └──────────┘     └───────────┘                │
│                                                             │
│   Output: 16-bit signed samples at configured sample rate   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

#### 2.2 VU Meter Concept

```
┌─────────────────────────────────────────────────────────────┐
│                     VU METER                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Peak-to-Peak → Level (%)                                  │
│                                                             │
│   Silent:    Vpp ≈ 0        Level ≈ 0%                      │
│   Quiet:     Vpp ≈ 5000     Level ≈ 8%                      │
│   Normal:    Vpp ≈ 20000    Level ≈ 30%                     │
│   Loud:      Vpp ≈ 50000    Level ≈ 75%                     │
│   Clipping:  Vpp = 65535    Level = 100%                    │
│                                                             │
│   ┌───────────────────────────────────────┐                 │
│   │████████████████░░░░░░░░░░░░░░░░░░░░░░ │  Level: 45%     │
│   └───────────────────────────────────────┘                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

***

### 3. ฟังก์ชันสำคัญ

#### 3.1 Simulated Mic API

<table><thead><tr><th width="251.47161865234375">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>sim_mic_generate()</code></td><td>สร้างสัญญาณจำลอง microphone</td></tr><tr><td><code>calc_peak_to_peak()</code></td><td>คำนวณ Peak-to-Peak</td></tr><tr><td><code>calc_rms()</code></td><td>คำนวณ RMS</td></tr></tbody></table>

#### 3.2 VU Meter Functions

<table><thead><tr><th width="250.9176025390625">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>update_vu_meter()</code></td><td>อัพเดทค่าและสี VU bar</td></tr><tr><td><code>update_peak_hold()</code></td><td>อัพเดท Peak Hold indicator</td></tr><tr><td><code>check_clipping()</code></td><td>ตรวจจับ clipping</td></tr></tbody></table>

***

### 4. Code เต็ม

#### 4.1 Constants and Global Variables

```c
#include "lvgl.h"
#include <math.h>
#include <stdlib.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define MIC_BUFFER_SIZE     512
#define DISPLAY_POINTS      200
#define MIC_SAMPLE_RATE     22050
#define PEAK_HOLD_TIME_MS   2000
#define PEAK_DECAY_RATE     1
#define CLIP_THRESHOLD      90   /* % level ที่ถือว่า clipping */
#define MAX_AMPLITUDE       32767

/* Global variables */
static int16_t mic_buffer[MIC_BUFFER_SIZE];
static lv_obj_t *mic_chart = NULL;
static lv_chart_series_t *mic_series = NULL;
static lv_obj_t *vu_bar = NULL;
static lv_obj_t *level_label = NULL;
static lv_obj_t *peak_label = NULL;
static lv_obj_t *rms_label = NULL;
static lv_obj_t *clip_warning = NULL;
static lv_timer_t *mic_timer = NULL;

static uint8_t current_level = 0;
static uint8_t peak_level = 0;
static uint32_t peak_timestamp = 0;
static int16_t sim_volume = 15000;  /* Simulated volume */
static uint32_t sim_phase = 0;      /* Phase counter */
```

#### 4.2 Simulated Microphone

```c
/* จำลองสัญญาณ Microphone
 * ใช้ sine wave หลายความถี่ + noise + amplitude variation
 * เพื่อให้ดูเหมือนเสียงจริง
 */
static void sim_mic_generate(int16_t *buf, uint16_t count, int16_t volume)
{
    for (uint16_t i = 0; i < count; i++) {
        double t = (double)(sim_phase + i) / MIC_SAMPLE_RATE;

        /* สร้างสัญญาณจำลองเสียง (หลายความถี่) */
        double val = 0;
        val += 0.5 * sin(2.0 * M_PI * 330 * t);   /* E4 */
        val += 0.3 * sin(2.0 * M_PI * 440 * t);   /* A4 */
        val += 0.2 * sin(2.0 * M_PI * 660 * t);   /* E5 */

        /* Amplitude variation (เหมือนเสียงพูด - ดังเบาสลับ) */
        double envelope = 0.5 + 0.5 * sin(2.0 * M_PI * 3.0 * t);
        val *= envelope;

        /* เพิ่ม random noise */
        int16_t noise = (rand() % 2001) - 1000;  /* +/- 1000 */
        buf[i] = (int16_t)(val * volume) + (noise * volume / 32000);
    }
    sim_phase += count;
}

/* คำนวณ Peak-to-Peak */
static int16_t calc_peak_to_peak(const int16_t *buf, uint16_t count)
{
    int16_t min_val = buf[0];
    int16_t max_val = buf[0];
    for (uint16_t i = 1; i < count; i++) {
        if (buf[i] < min_val) min_val = buf[i];
        if (buf[i] > max_val) max_val = buf[i];
    }
    return max_val - min_val;
}

/* คำนวณ RMS */
static float calc_rms(const int16_t *buf, uint16_t count)
{
    double sum_sq = 0;
    for (uint16_t i = 0; i < count; i++) {
        sum_sq += (double)buf[i] * buf[i];
    }
    return (float)sqrt(sum_sq / count);
}
```

#### 4.3 VU Meter Update Functions

```c
/* อัพเดทสี VU bar ตามระดับ */
static void update_vu_color(uint8_t level)
{
    lv_color_t color;
    if (level <= 60) {
        color = lv_color_hex(0x00cc44);  /* Green */
    } else if (level <= 80) {
        color = lv_color_hex(0xffcc00);  /* Yellow */
    } else {
        color = lv_color_hex(0xff2222);  /* Red */
    }
    lv_obj_set_style_bg_color(vu_bar, color, LV_PART_INDICATOR);
}

/* อัพเดท Peak Hold */
static void update_peak_hold(uint8_t level)
{
    uint32_t now = lv_tick_get();

    if (level > peak_level) {
        peak_level = level;
        peak_timestamp = now;
    }

    /* Decay หลังจาก hold time */
    if (lv_tick_elaps(peak_timestamp) > PEAK_HOLD_TIME_MS) {
        if (peak_level > level + PEAK_DECAY_RATE) {
            peak_level -= PEAK_DECAY_RATE;
        } else {
            peak_level = level;
        }
    }

    if (peak_label) {
        lv_label_set_text_fmt(peak_label, "Peak: %d%%",
                              (int)peak_level);
    }
}

/* ตรวจจับ Clipping */
static void check_clipping(uint8_t level)
{
    if (!clip_warning) return;

    if (level >= CLIP_THRESHOLD) {
        lv_obj_remove_flag(clip_warning, LV_OBJ_FLAG_HIDDEN);
        /* กระพริบสีแดง */
        static bool blink = false;
        blink = !blink;
        lv_obj_set_style_text_color(clip_warning,
            blink ? lv_color_hex(0xff0000) : lv_color_hex(0xff8888), 0);
    } else {
        lv_obj_add_flag(clip_warning, LV_OBJ_FLAG_HIDDEN);
    }
}
```

#### 4.4 Timer Callback

```c
static void mic_timer_cb(lv_timer_t *timer)
{
    (void)timer;

    /* จำลองข้อมูลจาก Microphone */
    sim_mic_generate(mic_buffer, MIC_BUFFER_SIZE, sim_volume);

    /* อัพเดท Waveform chart */
    if (mic_chart && mic_series) {
        for (int i = 0; i < DISPLAY_POINTS; i++) {
            int idx = i * MIC_BUFFER_SIZE / DISPLAY_POINTS;
            /* Map int16_t (-32768..+32767) to 0..100 */
            int32_t val = ((int32_t)mic_buffer[idx] + MAX_AMPLITUDE)
                          * 100 / (2 * MAX_AMPLITUDE);
            if (val < 0) val = 0;
            if (val > 100) val = 100;
            lv_chart_set_value_by_id(mic_chart, mic_series, i, val);
        }
        lv_chart_refresh(mic_chart);
    }

    /* คำนวณ Level */
    int16_t vpp = calc_peak_to_peak(mic_buffer, MIC_BUFFER_SIZE);
    current_level = (uint8_t)((uint32_t)vpp * 100 / (2 * MAX_AMPLITUDE));
    if (current_level > 100) current_level = 100;

    /* คำนวณ RMS */
    float rms = calc_rms(mic_buffer, MIC_BUFFER_SIZE);

    /* อัพเดท VU Meter */
    lv_bar_set_value(vu_bar, current_level, LV_ANIM_ON);
    update_vu_color(current_level);
    update_peak_hold(current_level);
    check_clipping(current_level);

    /* อัพเดท Labels */
    if (level_label) {
        lv_label_set_text_fmt(level_label, "Level: %d%%",
                              (int)current_level);
    }
    if (rms_label) {
        lv_label_set_text_fmt(rms_label, "RMS: %d",
                              (int)rms);
    }
}
```

#### 4.5 Main Function

```c
void part3_ex4_mic_visualizer(void)
{
    lv_obj_t *scr = lv_screen_active();
    lv_obj_set_style_bg_color(scr, lv_color_hex(0x0a0a1e), 0);

    /* ===== Title ===== */
    lv_obj_t *title = lv_label_create(scr);
    lv_label_set_text(title, "Part 3 Ex4: Mic Visualizer");
    lv_obj_set_style_text_color(title, lv_color_hex(0x00ff88), 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* ===== Waveform Chart ===== */
    mic_chart = lv_chart_create(scr);
    lv_obj_set_size(mic_chart, 340, 140);
    lv_obj_align(mic_chart, LV_ALIGN_TOP_LEFT, 10, 28);
    lv_chart_set_type(mic_chart, LV_CHART_TYPE_LINE);
    lv_chart_set_point_count(mic_chart, DISPLAY_POINTS);
    lv_chart_set_range(mic_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
    lv_chart_set_div_line_count(mic_chart, 4, 5);

    lv_obj_set_style_bg_color(mic_chart, lv_color_hex(0x001a00), 0);
    lv_obj_set_style_line_color(mic_chart, lv_color_hex(0x003300),
                                LV_PART_MAIN);
    lv_obj_set_style_border_color(mic_chart, lv_color_hex(0x006600), 0);
    lv_obj_set_style_border_width(mic_chart, 1, 0);
    lv_obj_set_style_size(mic_chart, 0, 0, LV_PART_INDICATOR);

    mic_series = lv_chart_add_series(mic_chart,
                     lv_color_hex(0x00ff00), LV_CHART_AXIS_PRIMARY_Y);

    /* ===== VU Meter Bar (แนวตั้ง) ===== */
    vu_bar = lv_bar_create(scr);
    lv_obj_set_size(vu_bar, 35, 140);
    lv_bar_set_range(vu_bar, 0, 100);
    lv_bar_set_value(vu_bar, 0, LV_ANIM_OFF);
    lv_obj_align(vu_bar, LV_ALIGN_TOP_RIGHT, -15, 28);

    /* VU Bar styling */
    lv_obj_set_style_bg_color(vu_bar, lv_color_hex(0x1a1a2e), LV_PART_MAIN);
    lv_obj_set_style_bg_color(vu_bar, lv_color_hex(0x00cc44),
                              LV_PART_INDICATOR);
    lv_obj_set_style_border_color(vu_bar, lv_color_hex(0x444466), 0);
    lv_obj_set_style_border_width(vu_bar, 1, 0);
    lv_obj_set_style_radius(vu_bar, 4, 0);

    /* VU zone labels */
    lv_obj_t *vu_red = lv_label_create(scr);
    lv_label_set_text(vu_red, "R");
    lv_obj_set_style_text_color(vu_red, lv_color_hex(0xff4444), 0);
    lv_obj_align_to(vu_red, vu_bar, LV_ALIGN_OUT_RIGHT_TOP, 5, 10);

    lv_obj_t *vu_yel = lv_label_create(scr);
    lv_label_set_text(vu_yel, "Y");
    lv_obj_set_style_text_color(vu_yel, lv_color_hex(0xffcc00), 0);
    lv_obj_align_to(vu_yel, vu_bar, LV_ALIGN_OUT_RIGHT_MID, 5, -15);

    lv_obj_t *vu_grn = lv_label_create(scr);
    lv_label_set_text(vu_grn, "G");
    lv_obj_set_style_text_color(vu_grn, lv_color_hex(0x00cc44), 0);
    lv_obj_align_to(vu_grn, vu_bar, LV_ALIGN_OUT_RIGHT_BOTTOM, 5, -10);

    /* ===== Info Panel ===== */
    level_label = lv_label_create(scr);
    lv_label_set_text(level_label, "Level: 0%");
    lv_obj_set_style_text_color(level_label, lv_color_hex(0x00ff00), 0);
    lv_obj_align(level_label, LV_ALIGN_BOTTOM_LEFT, 10, -45);

    peak_label = lv_label_create(scr);
    lv_label_set_text(peak_label, "Peak: 0%");
    lv_obj_set_style_text_color(peak_label, lv_color_hex(0xffcc00), 0);
    lv_obj_align(peak_label, LV_ALIGN_BOTTOM_MID, 0, -45);

    rms_label = lv_label_create(scr);
    lv_label_set_text(rms_label, "RMS: 0");
    lv_obj_set_style_text_color(rms_label, lv_color_hex(0x888888), 0);
    lv_obj_align(rms_label, LV_ALIGN_BOTTOM_RIGHT, -10, -45);

    /* ===== Volume Control ===== */
    lv_obj_t *vol_lbl = lv_label_create(scr);
    lv_label_set_text(vol_lbl, "Volume:");
    lv_obj_set_style_text_color(vol_lbl, lv_color_hex(0xaaaaaa), 0);
    lv_obj_align(vol_lbl, LV_ALIGN_BOTTOM_LEFT, 10, -15);

    lv_obj_t *vol_slider = lv_slider_create(scr);
    lv_slider_set_range(vol_slider, 1000, 32000);
    lv_slider_set_value(vol_slider, sim_volume, LV_ANIM_OFF);
    lv_obj_set_width(vol_slider, 180);
    lv_obj_align(vol_slider, LV_ALIGN_BOTTOM_LEFT, 80, -15);
    lv_obj_add_event_cb(vol_slider, volume_slider_cb,
                        LV_EVENT_VALUE_CHANGED, NULL);

    /* ===== Clipping Warning ===== */
    clip_warning = lv_label_create(scr);
    lv_label_set_text(clip_warning, "! CLIP !");
    lv_obj_set_style_text_color(clip_warning, lv_color_hex(0xff0000), 0);
    lv_obj_set_style_text_font(clip_warning, &lv_font_montserrat_16, 0);
    lv_obj_align(clip_warning, LV_ALIGN_BOTTOM_RIGHT, -10, -15);
    lv_obj_add_flag(clip_warning, LV_OBJ_FLAG_HIDDEN);

    /* ===== Timer (50ms for smooth display) ===== */
    mic_timer = lv_timer_create(mic_timer_cb, 50, NULL);
}

/* Volume slider callback */
static void volume_slider_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    lv_obj_t *slider = lv_event_get_target(e);
    sim_volume = (int16_t)lv_slider_get_value(slider);
}
```

***

### 5. องค์ความรู้และเทคนิค

#### 5.1 VU Meter vs Peak Meter

```c
/* VU Meter (Volume Unit):
 * - แสดงค่า average level (ballistic response ~300ms)
 * - ตอบสนองช้ากว่า peak
 * - ใช้ในการตั้งระดับเสียง (mixing console)
 *
 * Peak Meter:
 * - แสดงค่า peak สูงสุดทันที
 * - ตอบสนองเร็ว (< 1ms)
 * - ใช้ป้องกัน clipping (digital audio)
 *
 * ในระบบ Power Systems:
 * - เหมือน RMS Voltmeter vs Peak Voltmeter
 * - RMS ใช้คำนวณพลังงาน
 * - Peak ใช้ตรวจจับ transient/surge
 */
```

#### 5.2 Peak Hold Algorithm

```c
/* Peak Hold Pattern:
 *
 * 1. เก็บค่า peak สูงสุดที่เคยเกิด
 * 2. ค้างค่า peak ไว้ HOLD_TIME (เช่น 2 วินาที)
 * 3. หลังจาก hold time ให้ค่า peak ค่อยๆ ลดลง (decay)
 * 4. ถ้ามีค่าใหม่สูงกว่า ให้อัพเดท peak
 *
 * ใช้ได้กับ: audio level, voltage peak, current peak,
 * vibration peak, temperature peak
 */

/* Decay types: */
/* Linear decay: peak -= constant */
peak_level -= 1;

/* Exponential decay: peak *= factor */
peak_level = peak_level * 95 / 100;  /* 5% decay per update */
```

#### 5.3 Color Gradient for Level

```c
/* เทคนิคเปลี่ยนสีตามระดับ:
 *
 * Discrete zones (ง่าย):
 * Green < 60% < Yellow < 80% < Red
 *
 * Continuous gradient (สวยกว่า):
 * Interpolate RGB ตามค่า level
 */
lv_color_t level_to_color(uint8_t level)
{
    if (level < 60) {
        /* Green to Yellow (interpolate) */
        uint8_t r = (level * 255) / 60;
        return lv_color_make(r, 200, 0);
    } else if (level < 80) {
        /* Yellow to Red */
        uint8_t g = 200 - ((level - 60) * 200 / 20);
        return lv_color_make(255, g, 0);
    } else {
        return lv_color_make(255, 0, 0);  /* Red */
    }
}
```

#### 5.4 Timer Rate vs Responsiveness

```c
/* Timer rate กำหนด responsiveness ของ display:
 *
 * 200ms: อัพเดทช้า, CPU load ต่ำ (เพียงพอสำหรับ temperature)
 * 100ms: พอดี (10 Hz) สำหรับ level meter ทั่วไป
 *  50ms: smooth (20 Hz) สำหรับ audio visualization
 *  33ms: 30 fps สำหรับ waveform display
 *  16ms: 60 fps (max framerate ของ LVGL)
 *
 * Trade-off: ยิ่ง timer เร็ว ยิ่ง smooth แต่ CPU load สูง
 */
#define TIMER_PERIOD_MS  50  /* 20 Hz - ดีสำหรับ audio viz */
```

***

### 6. แบบฝึกหัด

#### Exercise 1: Audio Level Meter with Peak Hold Indicator

**โจทย์:** สร้าง VU Meter แบบมืออาชีพ:

* เพิ่ม Peak Hold indicator เป็นเส้นขีดบาง ๆ บน VU bar (ไม่ใช่แค่ text)
* สร้างเป็น object ขนาดเล็ก (เส้นสีขาว) ที่เลื่อนตำแหน่ง Y ตาม peak\_level
* เพิ่ม dB scale ข้าง VU bar (-40dB, -20dB, -10dB, -6dB, -3dB, 0dB)
* แปลง level% เป็น dB: `dB = 20 * log10(level/100)`

**คำใบ้:**

```c
/* Peak indicator object */
static lv_obj_t *peak_indicator = NULL;
peak_indicator = lv_obj_create(scr);
lv_obj_set_size(peak_indicator, 35, 3);  /* เส้นบาง */
lv_obj_set_style_bg_color(peak_indicator, lv_color_hex(0xffffff), 0);

/* อัพเดทตำแหน่ง Y ตาม peak_level */
int16_t y_offset = 140 - (peak_level * 140 / 100);
lv_obj_align_to(peak_indicator, vu_bar, LV_ALIGN_TOP_LEFT, 0, y_offset);
```

#### Exercise 2: Clipping Detector with History

**โจทย์:** สร้างระบบตรวจจับ Clipping พร้อมประวัติ:

* นับจำนวนครั้งที่เกิด clipping (clip count)
* แสดง clip count บนจอ
* เมื่อ clipping เกิด 3 ครั้งใน 5 วินาที ให้แสดง warning ใหญ่กลางจอ
* เพิ่มปุ่ม Reset เพื่อ clear clip count
* เปลี่ยน border สี chart เป็นสีแดงเมื่อ clipping

**คำใบ้:**

```c
static uint16_t clip_count = 0;
static uint32_t clip_times[10] = {0};  /* Circular buffer */
static uint8_t clip_idx = 0;

void record_clip(void) {
    clip_times[clip_idx] = lv_tick_get();
    clip_idx = (clip_idx + 1) % 10;
    clip_count++;
}

/* นับ clips ใน 5 วินาทีล่าสุด */
int count_recent_clips(uint32_t window_ms) {
    uint32_t now = lv_tick_get();
    int count = 0;
    for (int i = 0; i < 10; i++) {
        if (clip_times[i] > 0 && (now - clip_times[i]) < window_ms) {
            count++;
        }
    }
    return count;
}
```

***

### 7. References

* [PDM Microphones](https://www.analog.com/en/technical-articles/pdm-microphones.html)
* [VU Meter Standards](https://en.wikipedia.org/wiki/VU_meter)

***
