Funpack2-3 基于firebeetle ESP32-E 实现的音乐频谱时钟
本项目使用Dfrobot firebeetle -E搭配了2.8寸的ili9391触控屏和一个I2S麦克风实现了声音彩色频谱显示、苹果风时钟及动态壁纸功能。
标签
Funpack2-3
ESP32-E
Fifi_cen
更新2023-01-04
1060

     本项目使用Dfrobot firebeetle -E搭配了2.8寸的ili9391触控屏和一个I2S麦克风实现了声音彩色频谱显示、苹果风时钟及动态壁纸功能。

FuoSbwumZiX6hq7t3VVc5T5QSUAC

作业流程图

一、时钟功能

     本项目原本只有声音频谱的显示,觉得略为单调,所以个加上了苹果风时钟。逻辑也比较简单好理解。我们将屏幕上下一分为二,上面半区用于显示时间,下半区用于显示音乐频谱。

时钟的显示用了也是lvgl的基本功能,分别建立小时、分钟的矩形控件,控件上建立时间文本,小时和分钟时间放置闪烁频率为2的冒号”:”进行分隔,这闪烁是这时钟技术关键点。时间区域横置一条黑线,正式这条普通黑线,使得整个时钟给人留下了深刻的印象。

时间区域的内容就不一一分解了,可以查看对应的代码。这里我们用lvgl提供的定时器来实现这个炫酷的分隔标志的闪烁功能。

 lv_timer_t *segment_timer = lv_timer_create(timer_task/*定时执行的函数*/, 500/*运行间隔时间*/, seg_text/*被传递的控件*/); // 创建定时器,

定时执行的函数的功能为500毫秒进行显示和隐藏的操作,原理不难,但是使用lvgl处理以来非常的优雅。

static void timer_task(lv_timer_t *t)

{ // show and hide every half second .

    lv_obj_t *seg = (lv_obj_t *)t->user_data;

    static bool j;

    if (j)                                        // true

        lv_obj_add_flag(seg, LV_OBJ_FLAG_HIDDEN); // use the add flag or clear_flag function.

    else                                          // it tells us that we can hide obj as we want and show it again if needed.

        lv_obj_clear_flag(seg, LV_OBJ_FLAG_HIDDEN);

    j = !j; // reverse it .

}

二、动态壁纸功能

FhvYj3-7RFEEUuaEl4cyTYWsoGDd

    FvyH61ojIIbntS9xRukP3gJBIpRvFjRViwqBG27ak-jWonyGJGcOF4lK   

为了让整个玩具具有更佳的视觉效果,加入了动态背景功能,在整个屏幕底部显示不用的图片。要实现这个功能我们需要把所有的控件都建立在墙纸上,壁纸上的控件的主体设置为透明或者高透明度,(lv_obj_set_style_bg_opa(obj, LV_OPA_0, LV_PART_MAIN);  )

这样就不会遮挡壁纸。然后切换背景可以通过firebeetleE上的按钮来触发。原来还有使用lvgl定时进行定时切换壁纸的功能,效果挺好的,就是对帧率影响比较严重,频谱的显示效果不太理想,所以删减掉了,目前通过按钮切换壁纸,实现了两者的平衡,帧率(fps)在25左右。

//swithing the background paper.

void btn_switch(void)

{

    static uint8_t index_;

    index_++;

    lv_img_set_src(spectrum_bg, bg_pic[index_ % 8]);

}

三、声音频谱分析功能

(1)FFT理论:

    声音频谱分析是本项目的硬核内容,说到频谱分析大家肯定想到是使用快速傅里叶FFT(fast Fourier Transform),简单的理解是它的可以把时域上的模拟信号量转化成频域上的信号量。时域分析是以时间轴为坐标表示动态信号的关系;频域分析是把信号变为以频率轴为坐标表示出来。一般来说,时域的表示较为形象与直观,频域分析则更为简练,剖析问题更为深刻和方便。FFT具有计算量小的显著的优点,结合高速硬件就能实现对信号的实时处理,使得FFT在信号处理技术领域获得了广泛应用。但这里肯定没有数学公式(cost too much time,and painful),为了保住头上那几根倔强的毛发,我们把FFT当成黑盒子来使用即可(下面借用一张神图来增强大家抵抗力)。

FgL7NhxPsmoigwQjDUHIdvrUqmdO

镇宅神图

 

(2)音频数据获取

     经过前面“愉快”的基础理论学习后,我们进入实操环节。因为前期在I2S麦克风信号采集学习过几个程序,也算简单了解了。我们手机上用的i2s麦克风,这种麦克风与驻极体麦克风相比体积小、效果好,价格便宜。最主要在Arduino编程环境下已有高速处理数据的案例,我们将精力放在I2S麦克驱动的参数调节上,以达到更好数据呈现效果。(使用直接读取模拟量的麦克风的方案也测试过,程序是没问题的,但是频谱略显滞后,应该是数据采集的速度上没有上手段,这个问题往后等待时机在解开)

   下面我们看看基于esp32的I2S麦克风的驱动方案。

     i2s_config 中我们主要看sample_rate,dma_buf_cout,dma_buf_len这3个参数,因为其他的参数我按照字面理解就好也不能做什么调节,硬件实际情况就那样。采样率、dma数量、dma_buf长度 参数的不同会对数据最终呈现产生明显区别。采样率越快就更能捕获声音的细节信息,但也不是可以无限的放大,硬件能力是有限的,我这里设了44100,把硬件的能力压榨干净,往更高数值设置,可能会引发引发一些莫名其妙的错误。dma这个东西实现数据采集的是时候不需要cpu干预,这个孩子会自己独立学习生活,太棒了,然后等它完成作业后,就会向老爸报告并把作业交给老爸审查,然后在老爸看着数学作业的时候,这个好孩子又自觉得跑去做语文作业了,一刻也没停过就是把老爸给忙坏了。这样理解的话dma_buf_count 参数至少是2,(就是给孩子布置两份作业一个数学,一个语文,轮流干,不然做完一门作业他就问你要手机去玩游戏了,也可是3,多布置一份英语作业)。dma_buf_len这里呢,就更有意思了,因为好孩子一把作业做完就会立即送去给老爸看,如果这孩子做一个题目就去给老爸看,就不断的去打扰老爸的工作啦,老爸也很烦呀,老爸就跟孩子说你把这一整页都做完了再来找我好吗,所以为了减少对cpu的占用次数和时间,也把这参数舍得尽量大,但也有个范围8-1024,这样孩子跟老爸默契的配合下,老爸工作安心工作,孩子认真学习,其乐融融,大家的工作都完成得很漂亮。最后i2s_pins 是i2s引脚的配置,大家根据自己的实际情况配置即可,把这两个东西准好后,等下我们再投喂给数据采集工程函数,就没有我们什么事了。

      (后附一张调参侠提供的指引,祝大家食用愉快)

 

// i2s config for reading mic

i2s_config_t i2s_config = {

    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),

    .sample_rate = 44100,   // the faster the better, but ...

    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,

    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,

    .communication_format = I2S_COMM_FORMAT_STAND_I2S,

    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,

    .dma_buf_count = 3, //should be over 2,

    .dma_buf_len = 512, //the longer data collection will be better,show more details of the sound.

                        // trade of cpu load and the memory cost, decrease will increase the cpu load.

                        // and lost some details. the biggist number should be 1024.

};




// i2s pins

i2s_pin_config_t i2s_pins = {.bck_io_num = I2S_PIN_NO_CHANGE,

                             .ws_io_num = I2S_WS_PIN,

                             .data_out_num = I2S_PIN_NO_CHANGE,

                             .data_in_num = I2S_DATA_PIN};

FjbeNqJLLGlK1I30FEpTtmY4srsY

FFT调参技巧

 

(3)FFT数据处理

    在台面上我们只看到两个入口函数,第一步用FreeRTOS 在esp32的第二个核心上创建一个音频数据处理任务,这个任务的句柄是processing_task_handle,第二个是 i2s_sampler.start 函数,我们把刚才配置好的i2s设置传递给它,它就会在后台默默的干活并通过上述句柄将dma采集到的数据释放出来。在I2SSampler.cpp里面FreeRTOS的经典招式都用上了,包括消息队列,消息直接通知,多任务创建,看似慌乱,实际是在密锣紧鼓的忙乎这给老板们做饭。另外一个功臣是Processor.cpp,它就是FFT的化身,它接收了I2SSampler传来的消息,然后嚼烂消化成漂亮干净的数据。我就是喜欢这样的好同事,一直默默无闻地工作,做好事不留名,实际上这个项目可以充分展示了音乐的张力就是靠这些在后台默默无闻埋头苦干的小伙伴们来完成的。这里就不过多的对代码解读了,其实这个项目最精妙操作之一,如果识宝的话赶紧收藏起来吧。

 

xTaskCreatePinnedToCore(audio_processing_task, "audio processing task",

                            4096, NULL, 2, &processing_task_handle, 1); // the sampler is running on Pin0

    i2s_sampler.start(I2S_NUM_0, i2s_pins, i2s_config, WINDOW_SIZE,

                      processing_task_handle); // use the p

 

(4)绘制频谱图

    数据拿到了原本也就一些冰冷的数据,就像互不相识的你我,都是陌生人。那怎么把气氛搞起来呢,我们来一起跟着音乐蹦迪吧。

    我将获取到的数据分成4个一组取均值然后作一定的映射限制,将数据投影到我们的显示范围内,这里我们使用柱形图来显示频谱,如果只有这柱子直上直下肯定没什么好看的,这里我们看到类似于海浪拍打岸边抛洒出一下缓缓降落的水珠的效果,为这些频谱增添了不少活力。这里它们的运行机理是柱子的数值组成是该位点旧数据占比为70%,后面新加入的数据占30%,而顶部水珠的数组组成是旧数据占90%,而新加入数据占10%,这样就可以制造出水珠缓慢下降的效果。

 

 

static void bar_value_update2(float *mag)

{

    for (int i = 2, k = 0; k < SAMPLE_SIZE; i += 4, k++)

    {

        float aver = 0;

        for (int j = 0; j < 4; j++)

        {

            aver += mag[i + j];

        }

        aver /= 32.0; // for average  and down scale.

        int bar_value = std::min(float(WINDOW_HEIGHT*0.9),aver);

        bar_chart[k] = (bar_value > bar_chart[k]) // some break throught of the old data

                           ? bar_value

                           : 0.7 * bar_chart[k] + 0.3 * bar_value; // the new income data occupied 30PC ,it will change faster.

        bar_chart_peaks[k] = (bar_value > bar_chart_peaks[k])

                                 ? bar_value

                                 : 0.9 * bar_chart_peaks[k] + 0.1 * bar_value; // the old data occupy 90PC

                                                                               // means that is change not that fast.

    }

}

 

       而使用lvgl来绘制频谱是本项目的难点也是两点,因为lvgl提供丰富的控件,但是这些控件不一定可以适配我们所有的应用场合。那么这时候需要我们多底层绘图逻辑有一定的了解才能够做出一些个性化的东西。下列绘图函数中,我们在绘图事件后期(code == LV_EVENT_DRAW_POST_BEGIN),启动绘图工作,采用了绘制矩形和线段的方法对整幅画面进行绘制,绘制过程有如坐过山车,酸爽刺激,其中细节请慢慢享用。

 

void spectrum_draw_event_cb2(lv_event_t *e)

{

    lv_event_code_t code = lv_event_get_code(e);




    if (code == LV_EVENT_DRAW_POST_BEGIN)

    { // draw the ampitute with bars and the value peaks with the horizontal line above it .

        lv_obj_t *obj = lv_event_get_target(e);

        lv_obj_set_style_bg_opa(obj, LV_OPA_0, LV_PART_MAIN);     //No bg IMPROVE THE FPS VERY MUCH.

        lv_obj_set_style_border_opa(obj, LV_OPA_0, LV_PART_MAIN); // No border.




        lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e);       // the whole place we draw the map.




        lv_draw_rect_dsc_t draw_rect_dsc;

        lv_draw_rect_dsc_init(&draw_rect_dsc);

        draw_rect_dsc.bg_opa = LV_OPA_COVER;




        lv_draw_line_dsc_t draw_line_dsc;

        lv_draw_line_dsc_init(&draw_line_dsc);

        draw_line_dsc.width = 4;




        for (int i = 0; i < SAMPLE_SIZE; i++)

        {

            draw_line_dsc.color = draw_rect_dsc.bg_color = lv_color_hex(color_map[i]); // set the color of the bars.

           

            /* 5 is the bar width,  bar_value is bar height */

            lv_area_t above_rect;

            above_rect.x1 = 25 + i * x_step;               // x_step is the width of the bar and the gap between those bars.

            above_rect.x2 = above_rect.x1 + 5;             // THE WIDTH OF THE BAR IS 5 PIXEL

            above_rect.y1 = CENTER_Y - int(bar_chart[i] / 2); // THE HEIGTH OF THE BAR ,UP SIDE OF THE CENTER.

            above_rect.y2 = CENTER_Y + int(bar_chart[i] / 2);

            lv_draw_rect(draw_ctx, &draw_rect_dsc, &above_rect);




            lv_point_t above_line[2]; // a line need two points at least.

            /* upside line always 2 px above the bar */

            above_line[0].x = 25 + i * x_step;                         // a horizontal line, only change x value.

            above_line[0].y = CENTER_Y - int(bar_chart_peaks[i] / 2) - 2; // the peak value and the offset.

            above_line[1].x = 25 + i * x_step + 5;

            above_line[1].y = CENTER_Y - int(bar_chart_peaks[i] / 2) - 2; // devided by 2 ,it may be too big of the data.

            lv_draw_line(draw_ctx, &draw_line_dsc, &above_line[0],

                         &above_line[1]);




            lv_point_t blow_line[2];

            /* under line always 2 px below the bar */

            blow_line[0].x = 25 + i * x_step;

            blow_line[0].y = CENTER_Y + int(bar_chart_peaks[i] / 2) + 2;

            ;

            blow_line[1].x = 25 + i * x_step + 5;

            blow_line[1].y = CENTER_Y + int(bar_chart_peaks[i] / 2) + 2;

            lv_draw_line(draw_ctx, &draw_line_dsc, &blow_line[0],

                         &blow_line[1]); // where to draw, how to draw, the pose of the line.

        }

    }

}

将声音映射到颜色

其中最为亮丽的一笔是为每个频谱附上相应的彩色,这32个颜色数值组合是设计出来的呢,下面有一段故事。

 

联觉:联觉是一种迷人的状态,对许多人来说,它融合了颜色和声音。数百年来,研究人员一直在寻找声音和颜色之间的联系。事实上,艾萨克牛顿著名的七色光谱 (ROY G. BIV) 现在已被广泛接受是不正确的,并且出现了将靛蓝包括在内只是因为牛顿强烈认为色谱中的颜色数量必须与西方音乐中的音符数量是正确的。据推测,牛顿患有一种被称为联觉的独特疾病,在这种情况下,两种或多种感官之间的关系是结合在一起的:一种颜色可能有一种特殊的气味,音乐间隔可能各有一种特定的味道,或者字母或星期几可能有一个对应的颜色。据美国心理学会称,每两千人中就有一人受到联觉的影响。虽然联觉可以有多种形式,但在数以千计的情况下,个人会“听到”颜色,也就是说,他们可能会将一段特定的音乐或和弦或音阶视为蓝色或绿色。有趣的是,对于那些具有将声音等同于颜色的联觉的人来说,颜色不一定是统一的:一个人可能将 D 大调视为蓝色,另一个人可能将其视为绿色、红色或黄色。 实际上,无论您是在混音、设计还是录音,除了创建的波形之外,我们总能以视觉方式呈现我们所听到的内容。频谱分析对于音频恢复和清理特别有用,因为您可以看到可能出现问题的区域,但令人着迷的是通过波形的频谱图像传达了多少音频信息。毫不奇怪,如果我们有多种感官,我们应该协同使用它们来帮助更好地交流信息。虽然联觉是一种独特的情况,但通过频谱分析对声音进行着色等方面就可以做到这一点;以视觉方式传达我们所听到的内容,并使我们能够确定有关声音的更多信息,而不是我们单独听到的信息。

https://designingsound.org/2017/12/20/mapping-sound-to-color

FiI3kpUPOpacvnO9XxC75ZjYtYMs

这一串就是音乐的颜色:

uint32_t color_map[32] = {

0x24c4e6, 0x05a6fb, 0x0571fb, 0x053ffb, 0x0509fb, 0x3305fb, 0x6905fb, 0x9705fb,0xcd05fb,    0xfb05f7, 0xfb05c1, 0xfb058f, 0xfb055a, 0xfb0528, 0xfb1505, 0xfb4a05, 0xfb7c05, 0xfbb205, 0xfbe405, 0xe0fb05, 0xaefb05, 0x78fb05, 0x46fb05,0x11fb05, 0x05fb2c,0x05fb5d,0x05fb93, 0x05fbc5,0x05fbfb,0x05c9fb, 0x0593fb, 0x0584fb,

};

 

我常常痴痴的看着这跟随这音乐节奏跳动的彩色频谱,惊叹着它对音乐细节的捕捉,生动的诠释歌词深意,不单止用耳朵听到了音乐还可看到了音乐的色彩,于是我陶醉其中。。。。

 

 

总结:

偶以为在Arduino 嵌入式编程中 FreeRTOS 和LVGL是两大武林绝学,人人都想据为所有,为己所用,FreeRTOS负责复杂任务调度,LVGL则负责“美丽”用户界面,我决定与她们厮守终生。这段日子她们虐我千百遍,我待她们依然如初恋,希望“疫情”过后的春天,我们能够过上幸福美满的生活。就这样一次奇妙的旅程就这么结束了,愿我们春天来临的时候再次相遇。

 

代码链接:

链接:https://pan.baidu.com/s/1nqpLkr5bOcSLDzwuiaJr3Q?pwd=kyuz 
提取码:kyuz

团队介绍
沸羊羊这个喜欢单挑
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号