该程序摘取自GitHub,英国威廉希尔SparkFun,
原网址https://github.com/bartlettmic/SparkFun-RGB-LED-Music-Sound-Visualizer-Arduino-Code,笔者在此基础上添加了个人对这个可视化音乐项目的理解和领悟,愿与诸位共同进步!
代码之骨骼与灵魂:
一.局部的大声与整体的大声(音量反映到视觉效果上) (How loud is loud?)
1)这个项目就是要用声音传感器感知周围的声音环境再做到一个视觉效果上的匹配,因为这个灯可能会用在不同的环绕声系统之中(例如音乐会,演唱会,空旷场地等等),我们不能把那个最大的声音设置死了,应该让程序追踪检测并实时更新做出调整。
2 ) 不妨从一首歌入手,歌的前奏间奏和收尾一般比较安静,就必须定期降低maxVol要调整相应的最大音量,调整其视觉强度。
3)代码实现:
将maxVol与gradient已调制的当前音量水平进行平均
二.响度与亮度(Loudness and Brightness)
最初,SparkFun从正比例开始设计:
代码中也就是
(volume / maxVol) * 255
之后调整为
pow(256, volume / maxVol) - 1
最后确定为
代码之中也就是
`pow(volume / maxVol, 2.0) * 255`
在MATLAB之中可以看到最终的确定方案是介于线性和指数之间,也较为合理
三.关于平均(Averaging)
我们选用的是顺序平均,而不是常规的平均。
不是一次平均所有值,而是按顺序计算当前值和上次计算的平均值的平均值
例子:
真实的平均值:
顺序平均值:
我们不难发现,顺序平均值与序列的输入顺序有关,输入的数据越来越大,顺序平均的结果也会越来越大,而输入的数据越来越小,顺序平均的结果也会越来越小(面对的都是同一段序列)
仿真实际:
* **
传感器读数(蓝色)+顺序平均值(橙色)
传感器读数(蓝色)+常规平均值(绿色)
顺序平均值(橙色)+常规平均值(绿色)
我们很容易就可以看出,
1)顺序平均值的方法对声音响度的响应更快,更敏感,同时我们也可以看到,顺序平均值讷讷感让我们很真切的感受到高潮和低谷
2)而常规平均值由于长时间的读取,数据的大量累积,即使大的声音波动也几乎没有变化,系统无法及时的做出响应,是很不利的
四.关于衰退(Fade)
通过每个像素之中每种颜色的指数衰减来完成褪色
该fade()函数仅将每个像素的每个R,G和B值乘以小于1的数字(即,将其除以大于1的数字)。
由于它会乘以每遍,因此灯光会减少相对于当前灯光值的量。
举例:其中我们获取一个从最大亮度(255)开始的光值,并在每次通过后将其衰减0.95(即255×0.95 ^ x,其中x是fade()用相同的小数点调用的次数)
fade函数源码如下所示:
//通过在loop()的每次循环中乘以一个介于0和1之间的值来淡化灯光。
void fade(float damper) {
//“damper”必须在0和1之间,否则你最终会点亮灯或什么都不做,就是每一次都是淡化的
for (int i = 0; i < strand.numPixels(); i++) {
//检索当前位置的颜色。
uint32_t col = strand.getPixelColor(i);
//如果它是黑色的,你就不能再褪色了。
if (col == 0) continue;
float colors[3]; //Array of the three RGB values
//将每个值乘以“damper”
for (int j = 0; j < 3; j++) colors[j] = split(col, j) * damper;
//把dampered color放回原位。
strand.setPixelColor(i, strand.Color(colors[0] , colors[1], colors[2]));
}
}
五.关于颠簸效果(Bumps):
因为我们做的这个项目之中并没有用到频谱分析仪,使用的是音量强度和音量波动,波动可能会出现与拍子不对应的情况,但实际上视觉效果还是挺让人满意的
“bumps” 的基本概念就是一个相对大的,正的音量变化(i.e. 当前音量 - 上次测量的音量 > 0)
颠簸旨在模仿歌曲的节拍,跟随节奏
我们套用“平均”部分中讨论的相似的序列平均,但取而代之的是平均音量的正变化,而不仅仅是音量。
if (volume - last > 0) avgBump = (avgBump + (volume - last)) / 2.0;
本质上就是每一个正的音量变化都会被平均
适当碰撞的标准:
bump = (volume - last > avgBump * .9);
六.PalettePulse()函数
根据音量强度来确定脉冲的起始位置和终止位置
int start = LED_HALF - (LED_HALF * (volume / maxVol));
int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strand.numPixels() % 2;`
利用正弦函数来完成调光效果,
float damp = sin((i - start) * PI / float(finish - start));
由于我们不希望有任何负面的暗淡,我们只使用一个正弦波的波峰(其周期的一半)。这是执行此操作的一种简单方法,因为正弦函数将给出介于0和1之间的常规值
Pulse()函数
//从灯带线的中心发出脉冲
void Pulse() {
fade(0.75); //这个函数只是在循环()的每一次遍历 each pass of loop()中将颜色稍微调暗一点dims the colors a little bit
//如果有一个“颠簸bump”,将调色板推进到下一个值得注意的颜色。
if (bump) gradient += thresholds[palette] / 24;
//如果某时刻是静音的,我们想要淡入效果,因此这个If语句
if (volume > 0) {
uint32_t col = ColorPalette(-1); //我们重新取回的是32-bit color
//这些变量决定了脉冲pulse从哪里开始和结束,因为它开始于链的中间。
// .这些量存储在变量中,因此只需计算一次(加上我们在循环中使用它们)。
int start = LED_HALF - (LED_HALF * (volume / maxVol));
int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strand.numPixels() % 2;
//上面列出的LED_HALF只是您的线段上led数量的一半。
for (int i = start; i < finish; i++) {
//“潮湿damp”会产生一种渐暗效果fade effect,即像素pixel离线束中心越远,亮度就越低。
// 它返回一个介于0和1之间的值,该值在灯带链的中心1处达到峰值,在灯带链的末端为0。
float damp = sin((i - start) * PI / float(finish - start));
//Squaring damp平方潮湿创造了更独特的亮度。
damp = pow(damp, 2.0);
//获取当前像素current pixel 的颜色,这样我们就可以看到它是否暗到可以覆盖。
uint32_t col2 = strand.getPixelColor(i);
//利用一个for循环执行以下操作
// 使用位置location,、音量volume, 和“旋钮knob”调整像素的亮度adjust the brightness of this pixel
// 取预期颜色the intended colorthe existing color和现有颜色的平均RGB值the average RGB value 进行比较 for comparison
uint8_t colors[3];
float avgCol = 0, avgCol2 = 0;
for (int k = 0; k < 3; k++) {
colors[k] = split(col, k) * damp * knob * pow(volume / maxVol, 2);
avgCol += colors[k];
avgCol2 += split(col2, k);
}
avgCol /= 3.0, avgCol2 /= 3.0;
//将平均颜色作为“亮度”进行比较。只覆盖暗淡的颜色,使褪色效果更明显。
if (avgCol > avgCol2) strand.setPixelColor(i, strand.Color(colors[0], colors[1], colors[2]));
}
}
//这个命令实际上显示了灯光。
strand.show();
}
PalettePulse()函数
//PALETTEPULSE 调色板脉冲
//为整个托盘着色,而不是使用单一的纯色
void PalettePulse() {
fade(0.75);
if (bump) gradient += thresholds[palette] / 24;
if (volume > 0) {
int start = LED_HALF - (LED_HALF * (volume / maxVol));
int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strand.numPixels() % 2;
for (int i = start; i < finish; i++) {
float damp = sin((i - start) * PI / float(finish - start));
damp = pow(damp, 2.0);
int val = thresholds[palette] * (i - start) / (finish - start);
val += gradient;
uint32_t col = ColorPalette(val);
uint32_t col2 = strand.getPixelColor(i);
uint8_t colors[3];
float avgCol = 0, avgCol2 = 0;
for (int k = 0; k < 3; k++) {
colors[k] = split(col, k) * damp * knob * pow(volume / maxVol, 2);
avgCol += colors[k];
avgCol2 += split(col2, k);
}
avgCol /= 3.0, avgCol2 /= 3.0;
if (avgCol > avgCol2) strand.setPixelColor(i, strand.Color(colors[0], colors[1], colors[2]));
}
}
strand.show();
}
七.traffic()函数
//fade()实际上在这里创建了每个点后面的轨迹,所以包含它很重要。
void Traffic() {
fade(0.8);
//如果检测到一个bump,创建一个要显示的点dot
if (bump) {
//这个bump只是检查pos[]数组中是否有一个打开位置open slot(-2)。
int8_t slot = 0;
for (slot; slot < sizeof(pos); slot++) {
if (pos[slot] < -1) break;
else if (slot + 1 >= sizeof(pos)) {
slot = -3;
break;
}
}
// 如果有一个open slot,把它设置在灯带线的初始位置。
if (slot != -3) {
//.Evens 偶数向右, odds 奇数向左,所以 evens从0开始,奇数始终最大。
pos[slot] = (slot % 2 == 0) ? -1 : strand.numPixels();
//在它诞生的时候,根据“渐变gradient”的值给它一个颜色color。
uint32_t col = ColorPalette(-1);
gradient += thresholds[palette] / 24;
for (int j = 0; j < 3; j++) {
rgb[slot][j] = split(col, j);
}
}
}
//同样,如果它是静音silent的,我们希望颜色淡出 。
if (volume > 0) {
//如果有声音,沿着这条灯带线适当地迭代每个点dot。
for (int i = 0; i < sizeof(pos); i++) {
//如果一个点是-2,这意味着它是一个开放的位置 open slot,最终另一个点dot会占据这个位置。
if (pos[i] < -1) continue;
//如上所述,偶数向右(+1),奇数向左(-1)
pos[i] += (i % 2) ? -1 : 1;
//通过减法,奇数将达到-2,但如果一个偶数点超过LED条(led strip),它将被清除。
if (pos[i] >= strand.numPixels()) pos[i] = -2;
//.将点dot设置为它的新位置和相应的颜色。
// 由于fade(),i的旧位置的颜色会逐渐淡入,留下痕迹。
strand.setPixelColor( pos[i], strand.Color(
float(rgb[i][0]) * pow(volume / maxVol, 2.0) * knob,
float(rgb[i][1]) * pow(volume / maxVol, 2.0) * knob,
float(rgb[i][2]) * pow(volume / maxVol, 2.0) * knob)
);
}
}
strand.show(); //同样,不要忘记实际显示灯光!(actually show the lights)
}
八.Snake()函数
使点随着节拍前后摆动
void Snake() {
if (bump) {
//在bump上稍微改变一下颜色
gradient += thresholds[palette] / 30;
//改变dot的方向会产生“跳舞”的错觉。
left = !left;
}
fade(0.975); //在dot后面留下痕迹。
uint32_t col = ColorPalette(-1); //获取当前“gradient”的颜色。
//只有在有声音的情况下,dot才会移动。
// 否则,如果噪音noise开始并且一直在移动,它就会出现传送。
if (volume > 0) {
//将dot设置为适当的颜色和强度
strand.setPixelColor(dotPos, strand.Color(
float(split(col, 0)) * pow(volume / maxVol, 1.5) * knob,
float(split(col, 1)) * pow(volume / maxVol, 1.5) * knob,
float(split(col, 2)) * pow(volume / maxVol, 1.5) * knob)
);
//这就是“平均时间avgTime”发挥作用的地方。
// 该变量是检测到的每个“碰撞”之间的“平均”时间量。
// 所以我们可以用它来确定点移动的速度,使它与音乐的节奏the tempo of the music相匹配。
// 点在正常循环速度下移动非常快,所以如果avgTime< 0.15秒,它就是最大速度。
// 慢下来会导致颜色更新,但只会改变每隔一段循环的位置。
if (avgTime < 0.15) dotPos += (left) ? -1 : 1;
else if (avgTime >= 0.15 && avgTime < 0.5 && gradient % 2 == 0) dotPos += (left) ? -1 : 1;
else if (avgTime >= 0.5 && avgTime < 1.0 && gradient % 3 == 0) dotPos += (left) ? -1 : 1;
else if (gradient % 4 == 0) dotPos += (left) ? -1 : 1;
}
strand.show(); // 显示灯,让灯实际亮起来
// 检查点位置dot position是否超出界限。
if (dotPos < 0) dotPos = strand.numPixels() - 1;
else if (dotPos >= strand.numPixels()) dotPos = 0;
}
九.PaletteDance()函数
正弦移位的操作,颜色渲染到整个调色板
// 项目的整个调色板,振荡的节拍,类似于蛇,但整个梯度,而不是一个点
void PaletteDance() {
// 这是计算最密集的可视化,这就是为什么它不需要延迟。
if (bump) left = !left; //改变bump的迭代方向
//只显示如果有声音。
if (volume > avgVol) {
//首先,引入一个sin波函数 来改变所有像素的亮度(存储在“sinVal”中)
//这是为了使舞蹈效果更加明显诀窍是用颜色来改变sin波,这样它就会全部出现
//同样的物体,一个“驼峰”的颜色。这里添加了“dotPos”来实现这个效果。
//其次,整个当前调色板与LED线的长度成比例地匹配(存储在每个像素的val中)。
//这是通过将位置和led总数的比值乘以调色板的阈值来实现的。
//第三,然后通过添加“dotPos”来“移位”调色板(在哪里显示什么颜色)。
//“dotPos”是在除法之前加到这个位置上的,所以这是一个数学上的移位。然而,“dotPos”的范围并非如此
//与位置值(position value)的范围相同,因此使用函数map()。它基本上是一个内置比例调节器(proportion adjuster)。
//最后,把它们乘在一起,在正确的位置得到正确的颜色和强度。
//“gradient”也可以随着时间的推移慢慢地改变颜色。
for (int i = 0; i < strand.numPixels(); i++) {
float sinVal = abs(sin(
(i + dotPos) *
(PI / float(strand.numPixels() / 1.25) )
));
sinVal *= sinVal;
sinVal *= volume / maxVol;
sinVal *= knob;
unsigned int val = float(thresholds[palette] + 1)
//map takes a value between -LED_TOTAL and +LED_TOTAL and returns one between 0 and LED_TOTAL
* (float(i + map(dotPos, -1 * (strand.numPixels() - 1), strand.numPixels() - 1, 0, strand.numPixels() - 1))
/ float(strand.numPixels()))
+ (gradient);
val %= thresholds[palette]; //make sure "val" is within range of the palette
uint32_t col = ColorPalette(val); //get the color at "val"
strand.setPixelColor(i, strand.Color(
float(split(col, 0))*sinVal,
float(split(col, 1))*sinVal,
float(split(col, 2))*sinVal)
);
}
//在这之后,适当地重新定位 reposition “dotPos”。
dotPos += (left) ? -1 : 1;
}
//If there's no sound, fade.
else fade(0.8);
strand.show(); //Show lights.
//Loop循环“dotPos”,如果它超出了界限。
if (dotPos < 0) dotPos = strand.numPixels() - strand.numPixels() / 6;
else if (dotPos >= strand.numPixels() - strand.numPixels() / 6) dotPos = 0;
}
十.Glitter函数
void Glitter() {
//这种视觉效果也适用于整个灯带上的整个调色板
//这只会使调色板循环得更快,因此更美观
gradient += thresholds[palette] / 204;
//“val”再次用作传递到ColorPalette()以适应整个调色板的比例值。
for (int i = 0; i < strand.numPixels(); i++) {
unsigned int val = float(thresholds[palette] + 1) *
(float(i) / float(strand.numPixels()))
+ (gradient);
val %= thresholds[palette];
uint32_t col = ColorPalette(val);
//我们希望亮片是明显的,所以我们调暗了背景颜色。(dim the background color)
strand.setPixelColor(i, strand.Color(
split(col, 0) / 6.0 * knob,
split(col, 1) / 6.0 * knob,
split(col, 2) / 6.0 * knob)
);
}
//Create sparkles every bump 每一个bump都要有sparkles
if (bump) {
//随机生成器(random generator)需要一个种子,而micros()提供了很大范围的值。
// micros()是程序开始运行以来的微秒数。
randomSeed(micros());
//在灯带上随机选一个点。(pick a random spot)
dotPos = random(strand.numPixels() - 1);
// 以适当的亮度在任意位置draw sparkle。
strand.setPixelColor(dotPos, strand.Color(
255.0 * pow(volume / maxVol, 2.0) * knob,
255.0 * pow(volume / maxVol, 2.0) * knob,
255.0 * pow(volume / maxVol, 2.0) * knob
));
}
bleed(dotPos);
strand.show(); //Show the lights.
}
//PAINTBALL//彩弹
//Recycles Glitter()'s random positioning; simulates "paintballs" of
// color splattering randomly on the strand and bleeding together.
回收Glitter()的随机定位;模拟“彩弹”的颜色随机飞溅在灯带上和混在一起。
void Paintball() {
//如果它是自上次“bump”以来“bump”平均时间的两倍,则开始衰减。
if ((millis() / 1000.0) - timeBump > avgTime * 2.0) fade(0.99);
// 颜色混在一起。操作类似于淡入。有关更多信息,请参见下面的定义
bleed(dotPos);
//如果有一个bump,创建一个新的彩弹(如Glitter()中的sparkles)
if (bump) {
//随机生成器(random generator)需要一个种子,而micros()提供了很大范围的值。
//micros()是程序开始运行以来的微秒数microseconds。
randomSeed(micros());
//在灯带线上随便选一个点spot。Random已经在上面重新播种了,所以没有必要再做一次
dotPos = random(strand.numPixels() - 1);
//从调色板(palette)中随机选择一种颜色。(grab a random color)
uint32_t col = ColorPalette(random(thresholds[palette]));
//array数组来保存最终的RGB值
uint8_t colors[3];
//将颜色的亮度与相对体积和电位器值联系起来。
for (int i = 0; i < 3; i++) colors[i] = split(col, i) * pow(volume / maxVol, 2.0) * knob;
//将“paintball”溅射到随机位置。
strand.setPixelColor(dotPos, strand.Color(colors[0], colors[1], colors[2]));
//下面一部分是在原位置的左右两边放置一个不太明亮的同色版本,这样bleed effect更强,颜色更鲜艳。
for (int i = 0; i < 3; i++) colors[i] *= .8;
strand.setPixelColor(dotPos - 1, strand.Color(colors[0], colors[1], colors[2]));
strand.setPixelColor(dotPos + 1, strand.Color(colors[0], colors[1], colors[2]));
}
strand.show(); //Show lights.
}