ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32-S3 Cardputer ADV 声音生态缸:用声音与孢子打造桌面生命体

ESP32-S3 Cardputer ADV 声音生态缸:用声音与孢子打造桌面生命体 1. 项目缘起与整体设计思路1.1 为什么要在 Cardputer ADV 上做一个声音生态缸第一次拿到 M5Stack Cardputer ADV 的时候我脑子里冒出来的第一个念头其实很朴素这玩意儿屏幕不大、喇叭不小、还带全键盘能不能让它变成一个活的小东西不是那种跑个菜单、点个按钮的常规小工具而是那种你把它放在桌角它会自己呼吸、自己发声、自己变化的小生态。SOUND TERRARIUM这个名字就是这么来的。Terrarium 是生态缸、玻璃花园的意思通常指一个封闭的、能自我维持的小型生态系统。我把它搬到 Cardputer ADV 上核心想法是用声音作为这个微型世界的生命体征让屏幕上的视觉元素和喇叭里的声音形成一种共生关系——声音不是背景音乐而是这个世界的呼吸、心跳和生长信号。这个项目解决的核心问题其实是一个很个人化的需求我想在桌面上有一个不需要我持续操作、但又能让我感知到有东西在活着的设备。它不推送通知、不弹窗、不打扰我只是安静地发出一些细微的、有规律的声音同时屏幕上有一个极简的视觉呈现。你忙的时候可以完全忽略它闲下来瞥一眼、听一耳朵就能感受到一种微妙的陪伴感。适合谁来参考这篇内容我觉得有三类人。第一类是手里已经有 Cardputer ADV 或者类似 M5Stack 设备、想找点有意思的玩法的人第二类是对 Arduino IDE 和嵌入式音频感兴趣、想找一个完整小项目练手的人第三类是做互动装置、桌面玩具、声音艺术相关方向想看看在资源受限的硬件上怎么做出有生命感的效果的人。不需要你有多深的编程功底但至少得能看懂基本的 C 语法知道怎么在 Arduino IDE 里选板子、烧程序。1.2 硬件选型背后的取舍逻辑Cardputer ADV 这个设备本身挺有意思。它用的是 ESP32-S3 主控自带 1.14 寸的 IPS 屏幕240x135 分辨率、一个物理键盘、一个扬声器还有麦克风、IMU、红外收发这些传感器。我选它做这个项目主要看中三点。第一是自带扬声器和麦克风。很多 ESP32 开发板要外接喇叭才能出声Cardputer ADV 直接集成了省掉了一堆接线和功放电路的麻烦。麦克风的存在还给了我一个额外的可能性让这个生态缸能听到环境声音并做出反应虽然这个功能我在第一版里只做了很轻量的处理。第二是屏幕尺寸和分辨率刚好。240x135 这个分辨率听起来很寒酸但对于一个生态缸的视觉呈现来说反而是一种约束带来的美感。你不能画复杂的场景只能做极简的、符号化的表达——几个光点、几条曲线、一片渐变的色块。这种限制逼着你去思考什么才是这个世界的核心视觉语言。第三是键盘。键盘在这个项目里不是用来输入的而是用来干预这个生态的。你可以按键去投喂它、去扰动它、去切换它的状态。这种物理交互比触摸屏更有质感按下去有反馈像真的在跟一个活物互动。至于开发环境我选的是 Arduino IDE 而不是 ESP-IDF 或者 PlatformIO。原因很简单Arduino IDE 对 M5Stack 的库支持最成熟M5Cardputer 这个库把屏幕、键盘、扬声器、麦克风都封装好了你不需要去啃底层寄存器。对于这种快速做出一个能跑的东西的项目来说开发效率比性能优化重要得多。当然如果你后面要做更复杂的音频合成可能会考虑迁移到 ESP-IDF但第一版用 Arduino IDE 完全够用。1.3 声音作为生命信号的设计哲学这个项目最核心的设计决策是声音不是装饰而是状态的外化。我见过很多嵌入式项目里的声音处理方式基本就是事件触发音效——按个键响一声报个错响一声。这种方式的问题是声音和系统状态之间是割裂的声音只是一个通知不承载信息。在 SOUND TERRARIUM 里我让声音直接映射系统的内部状态。具体来说这个生态缸有一个能量值energy范围是 0 到 100。能量值会随着时间自然衰减也会因为你的交互而增加。能量值的高低直接决定三件事屏幕上光点的亮度和运动速度、声音的音高、声音的节奏密度。能量高的时候声音是明亮的、快的、密集的像一个小动物在活跃地叫能量低的时候声音是低沉的、慢的、稀疏的像它在打瞌睡。你不需要看屏幕光听声音就知道它现在是什么状态。这就是我说的声音作为生命信号——声音本身就是信息而不是信息的附属品。这个设计还有一个好处它让这个设备有了时间感。它不是一成不变的你离开一段时间回来它的状态已经变了。这种变化感是活着的核心体验。2. 核心细节解析与实操要点2.1 音频生成方案从方波到有机感在 ESP32 上用 Arduino 做音频最直接的方式是使用tone()函数或者 M5Cardputer 库自带的Speaker.tone()方法。这些方法本质上都是输出方波音色很电子像老式游戏机。对于很多项目来说这够了但 SOUND TERRARIUM 要的是有机感方波太硬了。我的解决方案是在方波的基础上做两层处理。第一层是频率微调每次发声的时候频率不是固定的而是在一个基准值上下随机浮动几个赫兹。这个浮动很小人耳不一定能明确分辨但会让声音听起来不那么死板。第二层是包络控制每个音符不是突然开始、突然结束而是有一个快速的淡入和稍慢的淡出。M5Cardputer 的 Speaker 库支持设置音量我通过在发声前后快速调整音量来模拟包络。具体实现上我没有用tone()而是用了 M5Cardputer 的Speaker.tone(freq, duration)配合手动音量控制。下面是一个简化的发声函数void playOrganicTone(float baseFreq, int durationMs, int volume) { // 频率微调在基准频率上下浮动 2% float detune random(-20, 21) / 1000.0; // -0.02 到 0.02 float freq baseFreq * (1.0 detune); // 淡入快速提升音量 for (int v 0; v volume; v volume / 4) { M5Cardputer.Speaker.setVolume(v); delay(2); } // 持续发声 M5Cardputer.Speaker.tone(freq, durationMs); delay(durationMs); // 淡出缓慢降低音量 for (int v volume; v 0; v - volume / 8) { M5Cardputer.Speaker.setVolume(v); delay(3); } M5Cardputer.Speaker.stop(); }这段代码里有个细节需要注意M5Cardputer.Speaker.tone()是阻塞式的它会占用 CPU 直到发声结束。这意味着你不能在发声的同时做其他事情。对于这个项目来说问题不大因为声音本身就是主要输出但在更复杂的场景下你可能需要用非阻塞的方式比如用 LEDC 硬件定时器自己生成波形。提示M5Cardputer 的 Speaker 库在不同版本的 M5Cardputer 库中 API 可能略有差异。如果你用的是较新的版本Speaker.tone()可能已经改成了Speaker.tone(freq, duration, channel)的形式。建议先跑一个最简单的发声示例确认 API 可用。2.2 视觉呈现240x135 上的极简生态屏幕只有 240x135这个尺寸下你画不了任何写实的东西。我的设计思路是用最少的光点表达最多的状态。生态缸的视觉核心是一组孢子spore。每个孢子是一个小圆点有位置、速度、亮度和大小四个属性。孢子的数量不是固定的而是跟能量值挂钩能量高的时候孢子多能量低的时候孢子少。孢子的运动也不是随机的而是遵循一个简单的向心规则——它们会被屏幕中心吸引但同时又有一个排斥力让它们不会挤在一起。这个规则用代码实现起来很简单struct Spore { float x, y; float vx, vy; float brightness; float size; }; void updateSpore(Spore s, float energy) { // 向心吸引力 float dx 120 - s.x; // 屏幕中心 x float dy 67 - s.y; // 屏幕中心 y float dist sqrt(dx * dx dy * dy); // 吸引力与距离成正比与能量成反比 float attract 0.02 * (1.0 - energy / 100.0); s.vx dx / dist * attract; s.vy dy / dist * attract; // 阻尼 s.vx * 0.98; s.vy * 0.98; // 更新位置 s.x s.vx; s.y s.vy; // 边界反弹 if (s.x 0 || s.x 240) s.vx * -1; if (s.y 0 || s.y 135) s.vy * -1; // 亮度与能量挂钩 s.brightness 50 energy * 2; }这段代码里attract这个参数是关键。能量高的时候吸引力弱孢子散得开看起来活跃能量低的时候吸引力强孢子聚在中心看起来蜷缩。这种视觉上的聚散直接对应了声音上的快慢形成视听一致性。绘制的时候我用的是 M5Cardputer 的M5Cardputer.Display对象它封装了 TFT 的基本绘图功能。画圆点用fillCircle()但为了性能我实际上用的是drawPixel()画一个 2x2 的小方块因为fillCircle()在 240x135 上画几十个点会明显掉帧。注意ESP32-S3 的 SPI 屏幕刷新是有带宽限制的。如果你每帧都全屏重绘帧率会很低。我的做法是只重绘变化的部分——用fillRect()擦除上一帧孢子的位置再在新位置画点。这样帧率能稳定在 30fps 左右。2.3 能量系统让生态活起来的核心机制能量系统是这个项目的灵魂。没有它孢子和声音就只是随机运动没有生命感。能量的变化规则我设计了三条第一条是自然衰减。每过一段时间我设的是 100 毫秒能量减少 0.1。这个衰减速度很慢满能量到零大概需要 100 秒。这个时间尺度是经过调试的太快了你会觉得它死得快太慢了你会觉得它没变化。第二条是交互增益。每次你按键盘上的任意键能量增加 5。这个增益不能太大否则你狂按键盘就能让它一直满能量失去了照顾的感觉。5 这个值意味着你需要每隔几秒按一下才能维持高能量状态。第三条是声音反馈。当能量低于 20 的时候声音会变得非常稀疏大概每 3 秒才响一声而且音高很低。当能量高于 80 的时候声音会变得密集每 0.3 秒就响一声音高也高。这个反馈是即时的你按键之后马上就能听到声音变密。能量的计算和声音触发逻辑大概是这样float energy 100.0; unsigned long lastDecay 0; unsigned long lastSound 0; void loop() { M5Cardputer.update(); // 自然衰减 if (millis() - lastDecay 100) { energy - 0.1; if (energy 0) energy 0; lastDecay millis(); } // 键盘交互 if (M5Cardputer.Keyboard.isChange() M5Cardputer.Keyboard.isPressed()) { energy 5; if (energy 100) energy 100; } // 声音触发 int soundInterval map(energy, 0, 100, 3000, 300); if (millis() - lastSound soundInterval) { float baseFreq map(energy, 0, 100, 200, 800); playOrganicTone(baseFreq, 50, 128); lastSound millis(); } // 更新和绘制孢子 updateAndDrawSpores(energy); }这里map()函数是 Arduino 的核心工具它把一个范围的值线性映射到另一个范围。map(energy, 0, 100, 3000, 300)的意思是能量 0 对应 3000 毫秒间隔能量 100 对应 300 毫秒间隔。这个映射是线性的但你可以改成指数映射让变化更敏感——比如低能量区间变化慢高能量区间变化快。实操心得map()函数在处理浮点数时会有精度损失因为它内部用的是长整型运算。如果你需要更精确的映射建议自己写一个浮点版本的 map 函数。我在这个项目里因为对精度要求不高就直接用了自带的。3. 完整实操流程与核心环节实现3.1 开发环境搭建从零到第一个声音先把环境搭起来。你需要的东西不多一台电脑、一根 USB-C 数据线、一块 Cardputer ADV、Arduino IDE。第一步是装 Arduino IDE。去官网下载最新版安装过程没什么好说的。装完之后打开进入文件-首选项在附加开发板管理器网址里加上 M5Stack 的板子支持地址。这个地址在 M5Stack 的官方文档里有格式是https://static-cdn.m5stack.com/resource/arduino/package_m5stack_index.json。加上之后进入工具-开发板-开发板管理器搜索 M5Stack安装 M5Stack by M5Stack 这个包。第二步是装库。进入工具-管理库搜索 M5Cardputer安装它。这个库会自动把依赖的 M5Unified、M5GFX 这些库一起装上。装完之后你可以在文件-示例里找到 M5Cardputer 的示例代码先跑一个最简单的键盘测试确认环境没问题。第三步是选板子和端口。把 Cardputer ADV 用 USB-C 线连上电脑在工具-开发板里选 M5Cardputer在工具-端口里选对应的串口。如果你用的是 macOS端口名可能是/dev/cu.usbmodem开头Windows 上是COM开头。第四步是烧一个测试程序。我建议先用这个最简单的代码确认声音和屏幕都能工作#include M5Cardputer.h void setup() { auto cfg M5.config(); M5Cardputer.begin(cfg); M5Cardputer.Display.setRotation(1); M5Cardputer.Display.fillScreen(BLACK); M5Cardputer.Display.setTextColor(WHITE); M5Cardputer.Display.setTextSize(2); M5Cardputer.Display.drawString(Hello, 20, 50); M5Cardputer.Speaker.setVolume(128); M5Cardputer.Speaker.tone(440, 500); } void loop() { M5Cardputer.update(); }烧进去之后你应该能看到屏幕上显示 Hello同时听到一声 440Hz 的短音。如果屏幕亮了但没声音检查一下M5Cardputer.Speaker是否需要先调用begin()。有些版本的库需要在M5Cardputer.begin()之后额外调用M5Cardputer.Speaker.begin()。注意Cardputer ADV 的扬声器音量不要设太大128 已经挺响了。在安静的办公室里64 左右就够了。长时间大音量播放可能会损伤扬声器而且方波信号本身对扬声器不太友好。3.2 孢子系统的完整实现孢子系统的代码我拆成了三个部分初始化、更新、绘制。初始化部分在setup()里跑一次创建初始的孢子数组。我设的最大孢子数量是 30但实际活跃数量跟能量挂钩#define MAX_SPORES 30 Spore spores[MAX_SPORES]; int activeSpores 0; void initSpores() { for (int i 0; i MAX_SPORES; i) { spores[i].x random(0, 240); spores[i].y random(0, 135); spores[i].vx random(-10, 11) / 10.0; spores[i].vy random(-10, 11) / 10.0; spores[i].brightness 128; spores[i].size 2; } activeSpores 10; }更新部分在loop()里每帧调用根据能量值调整活跃孢子数量然后更新每个孢子的位置void updateAndDrawSpores(float energy) { // 根据能量调整活跃孢子数 int targetSpores map(energy, 0, 100, 3, MAX_SPORES); // 平滑过渡避免孢子突然出现或消失 if (activeSpores targetSpores) activeSpores; if (activeSpores targetSpores) activeSpores--; // 擦除上一帧 M5Cardputer.Display.fillScreen(BLACK); // 更新并绘制 for (int i 0; i activeSpores; i) { updateSpore(spores[i], energy); // 根据亮度选择颜色 uint16_t color M5Cardputer.Display.color565( spores[i].brightness, spores[i].brightness / 2, spores[i].brightness / 4 ); M5Cardputer.Display.fillRect( spores[i].x - 1, spores[i].y - 1, 3, 3, color ); } }这里有个性能上的取舍我用fillScreen(BLACK)全屏擦除而不是局部擦除。原因是局部擦除需要记录上一帧每个孢子的位置代码复杂度会上升而且 240x135 的全屏填充在 SPI 屏幕上其实很快实测下来帧率影响不大。如果你要跑更复杂的视觉效果再考虑局部擦除。颜色方面我用的是暖色调——红色分量最高绿色次之蓝色最低。这样孢子看起来像微弱的火光或生物荧光比纯白色更有生命感。color565()是 M5GFX 库的颜色转换函数把 RGB888 转成 RGB565。3.3 声音与视觉的同步机制声音和视觉的同步是这个项目里最微妙的部分。如果声音和视觉各跑各的你会觉得它们是两个独立的东西如果同步得好你会觉得它们是同一个生命的不同表现。我的同步策略是声音触发时孢子产生一次脉冲。具体来说每次playOrganicTone()被调用的时候我会给所有活跃孢子一个向外的速度增量让它们炸开一下然后慢慢被吸引力拉回来。这个脉冲的强度跟音量挂钩。实现上我在playOrganicTone()里加了一个回调或者更简单的方式是让playOrganicTone()返回一个布尔值表示刚刚发声了然后在主循环里检查这个值bool soundJustPlayed false; void playOrganicTone(float baseFreq, int durationMs, int volume) { // ... 发声代码 ... soundJustPlayed true; } void loop() { // ... 其他逻辑 ... if (soundJustPlayed) { // 给所有孢子一个向外的脉冲 for (int i 0; i activeSpores; i) { float dx spores[i].x - 120; float dy spores[i].y - 67; float dist sqrt(dx * dx dy * dy); if (dist 0) { spores[i].vx dx / dist * 2.0; spores[i].vy dy / dist * 2.0; } } soundJustPlayed false; } // ... 更新和绘制 ... }这个脉冲效果很微妙但它是让视听同步的关键。你听到声音的同时看到孢子轻轻一震大脑就会把这两个事件关联起来产生它在发声的错觉。实操心得脉冲强度不要太大2.0 这个值是我调试了很多次才确定的。太大了孢子会飞出屏幕太小了看不出效果。你可以根据自己屏幕的刷新率和孢子的阻尼系数微调这个值。3.4 键盘交互的细节处理Cardputer ADV 的键盘是通过 I2C 或 GPIO 矩阵扫描的M5Cardputer 库把它封装成了M5Cardputer.Keyboard对象。基本的用法是if (M5Cardputer.Keyboard.isChange()) { if (M5Cardputer.Keyboard.isPressed()) { // 有键被按下 auto status M5Cardputer.Keyboard.keysState(); // status.word 包含当前按下的所有键 } }isChange()检测键盘状态是否变化isPressed()检测是否有键按下。这两个配合使用可以避免重复触发。我在这个项目里不关心具体按的是哪个键只关心有没有按键。所以逻辑很简单只要有键按下能量就加 5。但这里有个坑如果你按住不放isChange()只会在按下的瞬间返回 true之后一直按住不会重复触发。这其实是好事防止你按住一个键就把能量刷满。但如果你想要按住持续加能量的效果就需要用isPressed()而不是isChange()然后自己加一个时间间隔控制unsigned long lastKeyBoost 0; if (M5Cardputer.Keyboard.isPressed()) { if (millis() - lastKeyBoost 200) { // 每 200ms 加一次 energy 2; lastKeyBoost millis(); } }我最终选的是按一下加 5的方案因为这样更有投喂的仪式感。你按一下它亮一下、响一下像在跟它互动。4. 常见问题与排查技巧实录4.1 声音相关的问题排查问题一完全没有声音。这是最常见的问题。排查顺序是这样的先确认M5Cardputer.Speaker.setVolume()被调用了而且值不是 0。然后确认M5Cardputer.Speaker.tone()的参数是合理的——频率在 20 到 20000 之间持续时间大于 0。如果都没问题检查一下是不是M5Cardputer.begin()之后没有调用M5Cardputer.Speaker.begin()。有些版本的库需要显式初始化扬声器。还有一个容易被忽略的点Cardputer ADV 的扬声器是通过 I2S 驱动的如果你在代码里同时用了 I2S 做其他事情比如麦克风录音可能会冲突。我的建议是不要在发声的同时做录音分时复用。问题二声音断断续续、有杂音。这通常是电源问题。Cardputer ADV 用电池供电的时候如果电池电量低扬声器大音量播放会导致电压跌落声音就会断。插上 USB 供电试试如果问题消失那就是电源问题。另外tone()是阻塞式的如果你在发声的delay()期间做了其他耗时操作也会导致声音不连续。问题三音高不对。tone()的频率参数是浮点数但底层实现可能会取整。如果你设了 440.5Hz实际出来的可能是 440Hz 或 441Hz。对于这个项目来说无所谓但如果你要做音乐相关的项目需要用更精确的音频生成方式比如用 LEDC 硬件定时器。4.2 屏幕显示的问题排查问题一屏幕花屏或闪烁。先检查M5Cardputer.Display.setRotation(1)是否在begin()之后调用。旋转设置不对会导致显示区域错位。如果花屏可能是 SPI 速率太高可以在M5.config()里降低 SPI 频率试试。问题二帧率太低。240x135 的屏幕全屏刷新在 SPI 上大概需要 10-20 毫秒这意味着理论最大帧率是 50-100fps。但如果你每帧都做大量计算帧率会掉。我的优化建议是减少fillScreen()的调用频率用局部擦除代替减少浮点运算能用整数就用整数孢子的sqrt()调用可以用平方距离代替避免开方。问题三颜色不对。M5GFX 的颜色格式是 RGB565不是 RGB888。如果你直接用0xFF0000这种 RGB888 的值颜色会错乱。用color565(r, g, b)转换或者用库预定义的颜色常量如RED、GREEN、BLUE。4.3 能量系统的调试技巧能量系统的参数衰减速度、交互增益、声音间隔映射都需要根据你的实际体验来调。我建议在调试的时候把能量值显示在屏幕上这样你能直观地看到它的变化M5Cardputer.Display.setCursor(0, 0); M5Cardputer.Display.printf(E: %.1f, energy);调好之后再把这行注释掉。我自己的经验是衰减速度 0.1/100ms 适合放在桌上偶尔看一眼的场景如果你希望它更活跃可以调到 0.2/100ms如果你希望它更持久调到 0.05/100ms。声音间隔的映射范围3000ms 到 300ms也是可以调的。如果你觉得低能量时太安静了把 3000 改成 2000如果你觉得高能量时太吵了把 300 改成 500。4.4 常见问题速查表问题现象可能原因排查方法解决方案完全没声音扬声器未初始化检查是否调用 Speaker.begin()在 M5Cardputer.begin() 后加 Speaker.begin()声音断续电源供电不足插 USB 供电测试换用 USB 供电或充满电池屏幕花屏SPI 速率过高降低 SPI 频率在 M5.config() 里设置较低频率帧率低全屏刷新太频繁注释掉 fillScreen() 测试改用局部擦除颜色错乱用了 RGB888 值检查颜色定义用 color565() 转换按键无反应键盘未更新检查是否调用 M5Cardputer.update()在 loop() 开头加 update()孢子飞出屏幕脉冲强度太大减小脉冲系数把 2.0 改成 1.0 或 0.5能量不衰减时间判断有误检查 millis() 逻辑确认 lastDecay 正确更新提示这张表里的问题都是我实际踩过的坑。特别是按键无反应这一条我一开始忘了在 loop() 里调用M5Cardputer.update()结果键盘和 IMU 都不工作排查了半天才发现是这个问题。M5Cardputer 库的 update() 负责轮询所有外设状态不调用它等于什么都没接上。4.5 进阶扩展方向这个项目的第一版只是一个起点。如果你想继续深挖有几个方向可以走。第一个方向是加入麦克风输入。Cardputer ADV 有麦克风你可以让它听到环境声音然后根据环境噪音水平调整生态缸的状态。安静的时候它安静吵闹的时候它活跃。这个功能需要用 I2S 读取麦克风数据然后做简单的能量检测。注意麦克风和扬声器不能同时用 I2S需要分时切换。第二个方向是加入 IMU 交互。Cardputer ADV 有加速度计和陀螺仪你可以摇晃设备来扰动生态缸让孢子乱飞、声音变调。这个实现起来很简单读 IMU 数据把加速度映射到孢子的速度上就行。第三个方向是多生态缸共存。如果你有多台 Cardputer ADV可以通过 ESP-NOW 让它们互相通信一个生态缸的能量变化会影响另一个。这个就有点复杂了涉及到无线通信和状态同步但做出来会很有意思。第四个方向是持久化状态。把能量值存到 Flash 里断电重启后恢复。这样你的生态缸就有了记忆你离开几天回来它还记得你上次照顾它到什么程度。ESP32 的 Preferences 库可以很方便地做这件事。5. 个人实操体会与几个小技巧这个项目我从起意到第一版能跑大概花了两个周末。中间踩的坑不少但整体来说 Cardputer ADV 这个平台对快速原型开发非常友好。M5Cardputer 库把大部分底层细节都封装好了你只需要关注我想让它做什么而不是怎么驱动这个外设。有几个小技巧我觉得值得分享。第一个是先用串口打印调试再上屏幕。屏幕刷新会占用大量 CPU 时间如果你在调试逻辑的时候开着屏幕刷新串口输出会变得很慢影响判断。我一般是先把逻辑跑通用串口确认数据正确再打开屏幕显示。第二个是参数不要硬编码。能量衰减速度、声音间隔、孢子数量这些参数我一开始都是直接写在代码里的后来发现每次调整都要重新编译烧录很麻烦。后来我把它们改成了#define宏放在文件开头改起来方便多了。如果你愿意还可以用键盘在运行时调整这些参数但那需要额外的 UI第一版没必要。第三个是声音的留白很重要。我一开始把声音间隔设得很短结果它一直在响很烦。后来我把低能量时的间隔拉长到 3 秒高能量时也只有 0.3 秒整体听起来就舒服多了。声音不是越多越好适当的沉默反而让每次发声更有存在感。第四个是孢子的运动不要用随机数。我试过用random()给孢子加速度结果它们看起来像在抽搐完全没有生命感。后来改成向心吸引力加阻尼运动就自然多了。这个经验在很多粒子系统里都适用有规则的运动比随机运动更像活的。最后说一个我个人的感受。这个项目最让我满意的不是技术实现而是它带来的那种桌面上有个小东西在呼吸的感觉。我写代码的时候它在旁边偶尔响一声我不用看它就知道它还在。这种低干扰的陪伴感是很多智能设备给不了的。如果你也想做一个类似的东西我的建议是不要追求功能多追求状态真。一个能真实反映内部状态的声音比十个花哨的音效更有价值。
返回列表