这次为大家介绍的是一个使用Arduino制作的独特电子作品,分【前篇】和【后篇】两部分进行介绍。
为我们介绍这个非常有趣的电子制作项目的是平原真先生,他是一位以事物之间的关系为主题进行探索的艺术家。平原先生同时也是大阪艺术大学的副教授,迄今为止,他使用计算机和电子器件制作了很多媒体艺术作品。
这次我将为大家介绍一种可以用激光琴弦演奏的电子乐器的制作过程。
在前篇中,我们规划了作品的整体目标,做了制作准备并组装了电子电路。在后篇中,我们将用激光加工机床和3D打印机制作外壳,并使用Mozzi编写程序来最终完成这个作品。让我们一起来见证这个过程!
另外,这次的作品创意,得到了大阪艺术大学的猪熊祐斗先生的协助,在此表示衷心感谢!
3. 制作外壳
3.1. 设计外壳
在前篇“1-3 外壳设计”中,我们通过手绘草图探讨了大体的设计和结构。下面我们根据这个草图来创建数据。如果是复杂的形状,最好用3D CAD软件来进行准确设计,不过这次基本上属于很简单的箱型,所以我是用Adobe Illustrator画的图。请确认分发文件中的CutData.ai。红线是要使用激光加工机床切割的零件,绿线是要使用3D打印机制作的零件。要注意插板处的板厚。这次我设置的是4mm。
3.2. 制作零部件(激光加工机床)
下面用激光加工机床切割材料。外壳材料是厚度为4mm的椴木板。只要厚度一致,用其他材料也可以。请使用分发文件(1.2MB)中的CutData.ai,用激光加工机床进行切割。正面琴板和背面琴板的尺寸约为260mm x 360mm,所以请使用大小合适的激光加工机床进行加工。
用激光加工机床切割椴木板时,表面会变得很脏。如果将机床的输出功率和速度调整到正好可以切断的程度,那么这种情况就可以改善很多。另外还有一种方法,虽然有点费时费力,但可以保护表面基本不会变脏,那就是用美纹纸胶带覆盖表面。从防止在组装作业过程中弄脏的角度考虑,我建议您在作业完成之后再将其撕掉。要粘贴的部分和要安装零件的部分需要根据需要适时去除美纹纸胶带。
3.3. 制作零部件(3D打印机)
需要用3D打印机制作4种零件。
- 激光器的固定机构(LaserMounter.stl) x 12
- 传感器底座(SensorBase.stl) x 1
- 背面琴板顶部固定机构(RearTopFixture.stl) x 2
- 背面琴板底部固定机构(RearBottomFixture.stl) x 2
这些是装在内部的零件,所以您不必担心层纹痕迹。另外,对丝材颜色和树脂类型没有特别的要求。还需要进行相应的设置,以确保用您所选的材料正确地输出想要的尺寸。
3.4. 粘合椴木板
下面需要将切割好的椴木板粘合起来,制作外壳。在要粘合的位置涂上木工胶,对齐粘在一起,然后用美纹纸胶带固定。在干燥前用湿布擦去多余的胶水。
如果您先安装正面琴板内壁上的零部件,然后再粘合侧板和底板,会更容易操作。顶板和背面琴板会在安装电子零部件之后用螺丝拧在一起,所以不要把它们粘合起来。在下面的照片中可以看出来,它们是用胶带暂时固定的。
3.5. 将零部件安装在正面琴板上
下面我们将零部件安装在正面琴板上。先用扬声器支架盖住扬声器,并从外侧穿过M3螺丝,用螺母将其固定。对于可调电阻,先将螺母拧下来,将其将可调电阻从内侧穿过琴板上的孔,然后从外侧再用刚拧下的螺母将其固定。对于传感器底座和背面固定机构,从外侧拧入M3螺丝,将螺纹拧到头就那样保持固定即可。如果孔过大有些松动,就用螺母固定一下。
3.6. 安装传感器
接下来,我们将传感器的面包板固定在底座上。从顶部仔细对准,微调传感器的位置,使其位于激光孔的正下方,然后用面包板背面的双面胶固定住。如果一下子就把整面都粘好,就很难调整位置了,所以最好只撕下一部分双面胶暂时固定。
3.7. 安装激光器
下面需要将激光器连接到它的固定机构上。激光器的前端有一个透镜,可以通过转动螺丝来调节焦距,不过请先把它拧下来,夹进两枚0.8mm厚的M5垫片,然后再把透镜装回去,这样可以固定焦距。如果透镜脱落,请小心将其从里向外放回。然后请将其插入用3D打印机制作的激光器固定机构中。
将激光器固定机构安装在外壳内侧的顶部。请将M3螺丝从下方向上拧入。因为稍后还会调整位置,所以这里请不要拧得太紧,松一点即可。如果您用魔术贴等将导线捆绑在一起,就不容易打结,而且更容易操作。
接下来,需要对准激光。将USB数据线连接电脑并打开电源。这时会发射激光,但应该大部分都没有对准传感器。松开螺丝左右移动,或者在支架和外壳之间夹入M3垫圈前后移动调整,最终使所有的激光都照射到对应的传感器上。您可以通过串口监视器来确认传感器是否有响应。这里需要非常微细的调整,还请付上足够的耐心加细心。
下面是所有接线完成,激光对准工作也完成后的状态。导线比较多,不是很容易处理,还请尽力而为。
3.8. 安装手柄
需要将手柄连接到顶板上。用M4沉头螺丝穿过金属部分和手柄带,用螺母在另一面固定。
3.9. 安装顶板和背面琴板
最后,需要固定顶板和背面琴板。用3D打印机制作的固定机构上有直径为3mm的孔,可以拧入M3螺丝进行固定。
现在,硬件部分完成啦!辛苦大家了!
接下来,让我们用Mozzi创建能够发出声音的程序吧。
4. 程序创建
现在让我们看一下Arduino程序。这次的作品所需的主要功能是以下四个:
- 声音合成库Mozzi
- 读取光电二极管的状态,发现变化时发出声音
- 读取可调电阻的值,改变声音
- 加载音名文件
我先来介绍一下这四个功能,然后再给出整个程序。
4.1. 声音合成库Mozzi
用Arduino发出声音的方法有好几种。一种是可以用Arduino标准的tone()函数指定频率来让它发出声音,但这只是一种简单的嘟嘟声,表现力不足。另外,如果使用能够播放SD卡上保存的音源数据的专用产品,是可以播放丰富音源的,但无法实时更改。因此,这次我们将使用可以实时生成丰富声音的声音合成库“Mozzi”。
>> Mozzi官方网站
关于Mozzi的使用方法,请参阅Device Plus上的这篇文章(日语版):
>> 在Arduino项目中使用组件和传感器~扬声器篇(其2)
安装Mozzi
请从其官网的“download”进入“Download from GitHub”,从GitHub下载Zip文件。然后,从Arduino IDE的菜单中选择 [Sketch] > [Include Library] > [Add .ZIP Library],并加载下载的Zip文件。
用振荡器发出声音
用振荡器生成作为音源的波形。下面我们来了解一下如何加载库、创建对象和提取要输出的声音。请打开分发文件LaserStrings.ino,阅读说明并了解整体情况。
#include <MozziGuts.h>//加载Mozzi库 #include <Oscil.h>//加载振荡器 #include <tables/square_no_alias_2048_int8.h>//矩形波 #include <tables/saw2048_int8.h>//锯齿波 #include <tables/triangle_valve_2048_int8.h>//三角波 #include <tables/sin2048_int8.h>//正弦波 ...省略... Oscil <2048, AUDIO_RATE> oscArray[STRING_NUM];//振荡器数组
首先,加载Mozzi主体、振荡器和波形的表数据。然后,创建振荡器对象。这次我们将12个振荡器视为一个数组。
void setup() { startMozzi(CONTROL_RATE);//启动Mozzi ...省略... }
在setup函数中开始Mozzi处理。
void updateControl() { ...省略... }
Mozzi只能在loop函数中编写音频处理程序。我们取而代之在updateControl()函数中编写处理程序。
int updateAudio() { int out = 0; for ( int i = 0; i < STRING_NUM; i++) { out += (oscArray[i].next() * gain[i]) >> 8;//对振荡器的输出求和 } out /= STRING_NUM;//平均 return out; }
在updateAudio()函数中,通过return返回输出的声音。调用振荡器的next()函数继续进行处理,将12个振荡器的输出相加并求得平均值。
void loop() { audioHook();//播放音频。不可写入其他元素。 }
使用Mozzi时,只能在loop函数中编写audioHook()。
4.2. 读取光电二极管的状态,发现变化时发出声音
当激光被遮挡时,传感器变暗,但如果每次在激光被遮挡时都发出声音,会让人感觉机械和不自然。通过仅在被遮挡的瞬间提高音量并使音量逐渐减弱,可以营造出一种就像在用手指弹奏一样自然的氛围。
int prevState[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};//前一个传感器的状态
首先,需要创建数组来保存每个传感器的先前状态。
for ( int i = 0; i < STRING_NUM; i++) { int crntState = digitalRead(pin[i]);//读取光电二极管的值 if ( crntState == false && prevState[i] == true )//光电二极管的下降沿 { gain[i] = volume;//设置音量 } gain[i] *= decay / 100; //衰减 prevState[i] = crntState; }
在updateControl()中,使用digitalRead()读取传感器的状态。如果当前值为false(暗)而前一个值为true(亮),就知道这是在激光被遮挡的那一瞬间。通过将volume代入gain来提高音量。最后,将crntState代入prevState并更新。
4.3. 读取可调电阻的值,改变声音
给四个可调电阻中的每一个电阻都分配了可以改变声音的不同功能。
- 可调电阻1:音量
- 可调电阻2:音程
- 可调电阻3:音效
- 可调电阻4:波形类型
音量和音效
int volume = map(mozziAnalogRead(A0), 0, 1023, 0, 255);//读取VR1并改变音量 float decay = map(mozziAnalogRead(A2), 0, 1023, 70, 100 );读取VR3并改变衰减系数 for ( int i = 0; i < STRING_NUM; i++) { int crntState = digitalRead(pin[i]);//读取光电二极管的值 if ( crntState == false && prevState[i] == true )//光电二极管的下降沿 { gain[i] = volume;//设置音量 } gain[i] *= decay / 100; //衰减 prevState[i] = crntState; }
用mozziAnalogRead()读取可调电阻1的值,并将其设置为光电二极管响应后的音量(gain)。读取可调电阻3的值并将其设置为衰减系数(decay)。数值越高,声音持续时间越长,数值越低,持续时间越短。
音程和波形类型
int rate = map(mozziAnalogRead(A1), 0, 1024, 1, 5);//读取VR2并改变音程 setFreqs( rate );//设置频率 int waveType = map(mozziAnalogRead(A3), 0, 1024, 0, 4);//读取VR4并改变波形 setWaveType( waveType );//设置波形
读取可调电阻的值并设置振荡器的频率和波形。由于Mozzi已经将Arduino的功能用到了极限,所以不能使用常用的analogRead()函数,请改用mozziAnalogRead()函数。将读取到的值,对音程调整为1~4的整数,对波形调整为0~3的整数。
void setFreqs( int rate )//设置频率 { for ( int i = 0; i < STRING_NUM; i++) { oscArray[i].setFreq(baseFreq[i] * rate); } }
设置12个振荡器的频率。您可以通过基频(baseFreq[])乘以rate来切换4个八度音阶。
void setWaveType( int waveType )//设置波形 { int waveData; switch ( waveType ) { case 0://矩形波 waveData = SQUARE_NO_ALIAS_2048_DATA; break; case 1://锯齿波 waveData = SAW2048_DATA; break; case 2://三角波 waveData = TRIANGLE_VALVE_2048_DATA; break; default://正弦波 waveData = SIN2048_DATA; break; } for ( int i = 0; i < STRING_NUM; i++) { oscArray[i].setTable(waveData);//设置波形 } }
切换12个振荡器的波形类型。矩形波是所谓的“嘟嘟”声,声音比较生硬,而按照锯齿波、三角波、正弦波的顺序,波形变得顺滑,声音变得柔和。即使在相同的音量设置条件下,矩形波听起来比较响亮,正弦波听起来比较低柔。
4.4. 加载音名文件
虽然可以设置振荡器的频率,但是用数字表达很难理解,所以我决定将它们替换为C5等音名。请打开分发文件LaserStrings/pitches.h。“NOTE_B0”等字符表示音名,“31”等字符表示频率。
#define NOTE_B0 31 #define NOTE_C1 33 #define NOTE_CS1 35 #define NOTE_D1 37 #define NOTE_DS1 39 #define NOTE_E1 41 #define NOTE_F1 44 #define NOTE_FS1 46 #define NOTE_G1 49
需要将本文件放在与LaserStrings.ino相同的文件夹中,并在这种状态下读取此文件。
#include "pitches.h"
这样就能够通过音名而不是频率来表示了。
int baseFreq[] = {NOTE_C3, NOTE_CS3, NOTE_D3, NOTE_DS3, NOTE_E3, NOTE_F3, NOTE_FS3, NOTE_G3, NOTE_GS3, NOTE_A3, NOTE_AS3, NOTE_B3};//各琴弦的基频
4.5. 整体草图
请将以下源代码写入Arduino UNO。这是分发文件LaserStrings.ino。
#include <MozziGuts.h>//加载Mozzi库 #include <Oscil.h>//加载振荡器 #include <tables/square_no_alias_2048_int8.h>//矩形波 #include <tables/saw2048_int8.h>//锯齿波 #include <tables/triangle_valve_2048_int8.h>//三角波 #include <tables/sin2048_int8.h>//正弦波 #include "pitches.h" #define CONTROL_RATE 128//更新频率。如果需要更高的频率,就请提高该值。2的乘方 #define STRING_NUM 12//琴弦的根数 Oscil <2048, AUDIO_RATE> oscArray[STRING_NUM];//振荡器数组 byte gain[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};//音量 int pin[] = {1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13};//连接光电二极管的引脚编号 int prevState[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};//前一个传感器的状态 int baseFreq[] = {NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5};//各琴弦的基频 #define DEBUG_MODE false//在通过串口监视器查看传感器和可调电阻值时变为true void setup() { startMozzi(CONTROL_RATE);//启动mozzi for ( int i = 0; i < STRING_NUM; i++) { pinMode(pin[i], INPUT);//通过DigitalRead设置所用的引脚模式 } digitalWrite( A5, LOW );//使放大器ON if (DEBUG_MODE) { Serial.begin(9600);//调试用 } } void updateControl() { // 调试用的监控 ------------------ if ( DEBUG_MODE ) { Serial.print( "PD : " ); for ( int i = 0; i < STRING_NUM; i++) { int pd = digitalRead(pin[i]); Serial.print( pd ); Serial.print( " / " ); } Serial.print( " VR : " ); Serial.print( mozziAnalogRead(A0) ); Serial.print( " / " ); Serial.print( mozziAnalogRead(A1) ); Serial.print( " / " ); Serial.print( mozziAnalogRead(A2) ); Serial.print( " / " ); Serial.print( mozziAnalogRead(A3) ); Serial.println(); } int volume = map(mozziAnalogRead(A0), 0, 1023, 0, 255);//读取VR1并改变音量 float decay = map(mozziAnalogRead(A2), 0, 1023, 70, 100 );//读取VR3并改变衰减系数 for ( int i = 0; i < STRING_NUM; i++) { int crntState = digitalRead(pin[i]);//读取光电二极管的值 if ( crntState == false && prevState[i] == true )//光电二极管的下降沿 { gain[i] = volume;//设置音量 } gain[i] *= decay / 100; //衰减 prevState[i] = crntState; } int rate = map(mozziAnalogRead(A1), 0, 1024, 1, 5);//读取VR2并改变音程 setFreqs( rate );//设置频率 int waveType = map(mozziAnalogRead(A3), 0, 1024, 0, 5);//读取VR4并改变波形 setWaveType( waveType );//设置波形 } int updateAudio() { int out = 0; for ( int i = 0; i < STRING_NUM; i++) { out += (oscArray[i].next() * gain[i]) >> 8;//对振荡器的输出求和 } out /= STRING_NUM;//平均 return out; } void loop() { audioHook();//播放音频。不可写入其他元素。 } void setFreqs( int rate )//设置频率 { for ( int i = 0; i < STRING_NUM; i++) { oscArray[i].setFreq(baseFreq[i] * rate); } } void setWaveType( int waveType )//设置波形 { int waveData; switch ( waveType ) { case 0://矩形波 waveData = SQUARE_NO_ALIAS_2048_DATA; break; case 1://锯齿波 waveData = SAW2048_DATA; break; case 2://三角波 waveData = TRIANGLE_VALVE_2048_DATA; break; default://正弦波 waveData = SIN2048_DATA; break; } for ( int i = 0; i < STRING_NUM; i++) { oscArray[i].setTable(waveData);//设置波形 } }
结语
https://youtu.be/f38Q6aEU6f4
※此链接为Youtube视频
将程序写入Arduino后,就可以演奏了。您的作品成功地发出声音了吗?从正面看,右侧是低音,左侧是高音。从C开始会发出一个八度音阶的声音。通过可调电阻,从左侧开始可以依次调整音量、音程、音效和波形类型。现在,您可以在用手指遮挡激光的同时调整可调电阻,享受各种音色了!
这次我将激光垂直排列,做成了类似竖琴的形状,不过,您还可以改变外壳的设计,做出类似吉他和钢琴等的各种形状的作品。此外,Mozzi有一些我们尚未使用的功能等着您去尝试。欢迎大家尝试改进程序,独立完成更富表现力的作品!