基于 Arduino 的 MIDI 表达踏板
用 Arduino 电路板创建一个 MIDI 表达踏板
文章 1:Arduino 硬件平台介绍
文章 2:Arduino 与 LCD 接口
引言
这是我关于 Arduino 硬件平台 的第三篇文章,也是本系列的结论。在本文中,我将介绍如何使用 Arduino 微控制器构建一个 MIDI 音量踏板。
背景
我有一个效果器,围绕它创作了一首歌曲,并在演奏时调整其中一个参数。该设备有 MIDI 端口,但它不响应 MIDI 连续控制器 (CC) 消息,但是可以通过发送 MIDI 系统独占 (SysEx) 消息来更改参数。我本可以实现一个纯软件的解决方案,但我想要一个独立的设备,而且不想将电脑连接到我的设备上。我也可以使用现成的组件实现一个纯硬件的解决方案,包括一个 表情踏板、一个 MIDI Solutions Pedal Controller 和一个 MIDI Solutions Event Processor,但这仍然会有点笨重,我想要更紧凑一些。有一天,我偶然发现了 Small Bear Electronics 网站,该网站 销售表情踏板套件,我萌生了将踏板外壳与微控制器结合起来构建我的第一个电子设备——一个输出 SysEx 的自定义表情踏板。我知道这是一个非常专业化的案例,MIDI 音量踏板会更受欢迎,所以我还包含了一个 MIDI 音量踏板的草图。
Metronome2
在我开始谈论表情踏板草图之前,让我们回顾一下我在第一篇文章中介绍的节拍器草图。如果您还记得,节奏在程序中被硬编码了,如果需要更改节奏,必须修改代码并重新编译。在 Metronome2 中,我添加了一个 10K 欧姆的电位器,连接到模拟引脚 0
模拟引脚需要 10K 欧姆的设备,范围是 0 - 1023。下面的草图显示了电位器变化时的值
// Raw potentiometer value viewer
// Constants
const int POT_PIN = 0; // Pot connected to analog pin 0
const int SERIAL_PORT_RATE = 9600;
void setup() // Run once, when the sketch starts
{
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
}
void loop() // Run over and over again
{
int nValue = analogRead(POT_PIN);
Serial.println(nValue);
}
如果您在串口监视器中运行,您会发现一个问题——即使您不触摸电位器,值仍然在变化。对于 Ski 草图来说这不是问题,因为它只有五个值(-2 到 +2),但对于 Metronome2 草图(以及后面的音量踏板草图)来说,这将是一个问题——设置完节奏后,您希望节奏保持恒定而不波动。为了解决这个问题,添加了代码来支持阈值,并拒绝与最后一个已知良好值过于接近的值。
// Potentiometer viewer with threshold
// Constants
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 3; // Threshold amount to guard against false values
const int SERIAL_PORT_RATE = 9600;
void setup() // Run once, when the sketch starts
{
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
}
void loop() // Run over and over again
{
static int s_nLastValue = 0;
int nValue = analogRead(POT_PIN);
if(abs(nValue - s_nLastValue) < POT_THRESHOLD)
return;
s_nLastValue = nValue;
Serial.println(nValue);
}
这样会好一些,但代码仍需进一步改进。代码需要映射到一个我们想要的范围内的值,并且代码需要防止设置相同的值。
//
// Check floating potentiometer value against threshold
//
// Constants
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 3; // Threshold amount to guard against false values
const int SERIAL_PORT_RATE = 9600;
void setup() // Run once, when the sketch starts
{
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
}
void loop() // Run over and over again
{
static int s_nLastPotValue = 0;
static int s_nLastMappedValue = 0;
int nCurrentPotValue = analogRead(POT_PIN);
if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
return;
s_nLastPotValue = nCurrentPotValue;
int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 255); // Map the value to 0-255
if(nMappedValue == s_nLastMappedValue)
return;
s_nLastMappedValue = nMappedValue;
Serial.println(nMappedValue);
}
如果您觉得这仍然太嘈杂(我就是,因为我在一个需要 10K 的引脚上使用了一个 200K 的哇哇踏板电位器),另一种方法是将值与历史缓冲区进行比较。
//
// Check floating potentiometer value against history buffer
//
// Constants
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 3; // Threshold amount to guard against false values
const int HISTORY_BUFFER_LENGTH = 6; // History buffer length
// (to guard against noise being sent)
const int SERIAL_PORT_RATE = 9600;
// Globals
static int s_history[HISTORY_BUFFER_LENGTH];
void setup() // Run once, when the sketch starts
{
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
// Initialize he buffer
for(int i=0; i<HISTORY_BUFFER_LENGTH; i++)
{
s_history[i] = -1;
}
}
void loop() // Run over and over again
{
static int s_nLastPotValue = 0;
static int s_nLastMappedValue = 0;
int nCurrentPotValue = analogRead(POT_PIN);
if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
return;
s_nLastPotValue = nCurrentPotValue;
int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 255); // Map the value
// to 0-255
if(nMappedValue == s_nLastMappedValue)
return;
for(int i=0; i<HISTORY_BUFFER_LENGTH; i++)
{
if(s_history[i] == nMappedValue)
return;
}
memcpy(&s_history[0], &s_history[1], sizeof(int) * (HISTORY_BUFFER_LENGTH - 1));
s_history[HISTORY_BUFFER_LENGTH - 1] = nMappedValue;
s_nLastMappedValue = nMappedValue;
Serial.println(nMappedValue);
}
(当前值重复发送不是节拍器草图的问题,但稍后我们将需要这个用于表情踏板。)
现在,将这个支持电位器的功能添加到节拍器草图中,代码看起来是这样的:
/*
* Metronome2
*
* Based on the basic Arduino example, Blink:
* http://www.arduino.cc/en/Tutorial/Blink
* Operates as a visual metronome.
*/
// Constants
const int LED_PIN = 13; // LED connected to digital pin 13
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 3; // Threshold amount to guard against false values
void setup() // Run once, when the sketch starts
{
pinMode(LED_PIN, OUTPUT); // Sets the LED as output
}
void loop() // Run over and over again
{
static int s_nLastPotValue = 0;
static int s_nTempo = 0;
int nCurrentPotValue = analogRead(POT_PIN); // Has a range of 0 - 1023
if(abs(nCurrentPotValue - s_nLastPotValue) >= POT_THRESHOLD)
{
s_nLastPotValue = nCurrentPotValue;
int nTempo = map(nCurrentPotValue, 0, 1023, 50, 255); // Map the value to 50-255
if(nTempo != s_nTempo)
{
s_nTempo = nTempo;
}
}
// Delay in milliseconds = 1 minute 60 seconds 1000 milliseconds
// --------- * ---------- * -----------------
// (X) beats minute second
int nDelay = (int)((60.0 * 1000.0) / (float)s_nTempo);
PlayNote(nDelay);
}
void PlayNote(int nDuration)
{
nDuration = (nDuration / 2);
digitalWrite(LED_PIN, HIGH); // Set the LED on
delay(nDuration); // Wait for half the (original) duration
digitalWrite(LED_PIN, LOW); // Set the LED off
delay(nDuration); // Wait for half the (original) duration
}
VolumePedal
市面上已经有几种 MIDI 脚踏控制器,例如 Roland FC-300 和 Behringer FCB1010,但这些脚踏控制器体积庞大,如果您只需要一个表情踏板,它们就有点大材小用。不幸的是,目前市场上没有单独的 MIDI 音量踏板,所以我决定使用 Arduino 来创建一个。
发送 MIDI CC 音量更改消息非常简单,只需要发送三个字节。
0xB0 - CC change command (0xB0 - 0xBF depending on the MIDI channel)
0x07 - Volume command
the value - The value of the volume, between 0 and 127
……所以实际发送的 MIDI 命令并不复杂或冗长,但是支持的硬件和软件确实花了一些工夫……(不算太多,但仍然有一些工作和一些反复试验)
在查看源代码之前,让我们先了解一下零件。最少,您需要:
- 一个 Arduino 处理器。我的表情踏板我用的是 Arduino Pro Mini(5V 版本)。
- 一个表情踏板外壳。
- 一个哇哇电位器(踏板外壳不包含)。
- 一个 3 极脚踏开关。
- 一个 LED(可选)。
- 一个 DC 电源插头。
- 一个 9VDC 电源。
- 一个 MIDI 插头。
- MIDI 端口需要一个 220 欧姆电阻。
- LED 需要一个 1K 欧姆电阻。
- 一块面包板,用于预先组装电路并验证草图是否正常工作。
- 一块空白电路板,用于焊接所有元件。
- 一套排针,用于将 Arduino 焊接到电路板上。
- 导线
Small Bear Electronics 提供的表情踏板外壳是未完成的,是裸铝的。在组装您的项目之前,您可能想要给外壳喷漆。我把我的送到了 PedalEnclosures.com,并让外壳喷成了红色锤纹效果。
关于接线,我们已经讲了如何接 LED 和电位器。剩下的组件是 9VDC 输入、脚踏开关和 MIDI 输出端口。对于 MIDI 输出端口,您需要连接:
- 引脚 5 到 TX0
- 引脚 2 到 GND
- 引脚 4 到一个 220 欧姆电阻,然后连接到 +5VDC 电源 (VCC)。
对于 9VDC 输入插头和脚踏开关,将脚踏开关焊接到在输入插头的 +V 之后断开电路。
……现在将所有东西连接到面包板上。
我使用的相机无法拍出最终组装的好照片,所以这是 Arduino Mini 每个引脚的简要说明:
顶行 | |
RAW | 脚踏开关 +V |
GND | 电源插头 GND |
RST | |
VCC | +V 轨 |
A3 | |
A2 | |
A1 | |
A0 | 电位器可变电阻(中间连接器) |
13 | LED |
12 | |
11 | |
10 |
底行 | |
TX0 | MIDI 引脚 5 |
RXI | |
RST | |
GND | 地轨 |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 |
+V 轨 | 220 欧姆电阻(连接到 MIDI 引脚 4) |
电位器 +V |
GND 轨 | MIDI 引脚 2 |
1K 电阻(连接到 LED 地极) |
MIDI 音量踏板的源代码如下:
//#define DEBUG 1
// Constants
const int LED_PIN = 13; // LED connected to digital pin 13
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 7; // Threshold amount to guard against false values
const int MIDI_CHANNEL = 0; // MIDI Channel 1
#ifdef DEBUG
const int DEBUG_RATE = 9600; // Serial debugging communicates at 9600 baud
const int SERIAL_PORT_RATE = DEBUG_RATE;
#else
const int MIDI_BAUD_RATE = 31250; // MIDI communicates at 31250 baud
const int SERIAL_PORT_RATE = MIDI_BAUD_RATE;
#endif
void setup()
{
pinMode(LED_PIN, OUTPUT); // Sets the digital pin as output
digitalWrite(LED_PIN, HIGH); // Turn the LED on
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
}
void loop()
{
static int s_nLastPotValue = 0;
static int s_nLastMappedValue = 0;
int nCurrentPotValue = analogRead(POT_PIN);
if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
return;
s_nLastPotValue = nCurrentPotValue;
int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
if(nMappedValue == s_nLastMappedValue)
return;
s_nLastMappedValue = nMappedValue;
MidiVolume(MIDI_CHANNEL, nMappedValue);
}
void MidiVolume(byte channel, byte volume)
{
#ifdef DEBUG
Serial.println(volume, DEC);
#else
Serial.print(0xB0 | (channel & 0x0F), BYTE); // Control change command
Serial.print(0x07, BYTE); // Volume command
Serial.print(volume & 0x7F, BYTE); // Volume 0-127
#endif
}
遍历代码,首先是:
//#define DEBUG 1
我使用此常量来定义构建是否为调试构建。要以调试模式运行,只需取消注释此行。
接下来是常量:
// Constants
const int LED_PIN = 13; // LED connected to digital pin 13
const int POT_PIN = 0; // Pot connected to analog pin 0
const int POT_THRESHOLD = 7; // Threshold amount to guard against false values
const int MIDI_CHANNEL = 0; // MIDI Channel 1
#ifdef DEBUG
const int DEBUG_RATE = 9600; // Serial debugging communicates at 9600 baud
const int SERIAL_PORT_RATE = DEBUG_RATE;
#else
const int MIDI_BAUD_RATE = 31250; // MIDI communicates at 31250 baud
const int SERIAL_PORT_RATE = MIDI_BAUD_RATE;
#endif
LED 指示设备已开启。它是可选的,但如果您连接了 LED,LED 需要连接到数字引脚 13(别忘了电阻和 LED 的方向)。
电位器连接到模拟引脚 0。Arduino 规格规定模拟引脚需要电阻仅为 10K 欧姆的设备,但是哇哇电位器(我们将与表情踏板一起使用的)仅提供 100K 和 200K 的规格。我购买了一个 200K 电位器,发现它虽然有点嘈杂,但仍然可用,并且需要将阈值设置为 7。
const int POT_THRESHOLD = 7; // Threshold amount to guard against false values
下一个值,MIDI 通道,被保留为一个硬编码的常量。就像节拍器草图一样,我们可以添加一个电位器来选择 MIDI 通道,但对我来说,只需要将 MIDI 通道固定到一个通道。
下一组常量与串口速率有关。MIDI 端口和用于调试的 print()
命令都使用串口,并且不能很好地共存。我们需要在调试模式或发布模式之间进行选择,所以我使用 #ifdef DEBUG
检查在两者之间切换。
接下来是代码。setup()
函数很小,是我们迄今为止已经看到过的典型用法。
void setup()
{
pinMode(LED_PIN, OUTPUT); // Sets the digital pin as output
digitalWrite(LED_PIN, HIGH); // Turn the LED on
Serial.begin(SERIAL_PORT_RATE); // Starts communication with the serial port
}
下一个函数 loop()
,包含了大部分代码。
void loop()
{
static int s_nLastPotValue = 0;
static int s_nLastMappedValue = 0;
int nCurrentPotValue = analogRead(POT_PIN);
if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
return;
s_nLastPotValue = nCurrentPotValue;
int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
if(nMappedValue == s_nLastMappedValue)
return;
s_nLastMappedValue = nMappedValue;
MidiVolume(MIDI_CHANNEL, nMappedValue);
}
s_nLastPotValue
和 if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
检查可以防止电位器产生噪声,而 s_nLastMappedValue
和 if(nMappedValue == s_nLastMappedValue)
检查可以防止在发送了上一个值后再次发送相同的值。
MidiVolume()
是发送字节的地方。如我之前提到的,发送 MIDI CC 音量消息只需要三个字节。
void MidiVolume(byte channel, byte volume)
{
#ifdef DEBUG
Serial.println(volume, DEC);
#else
Serial.print(0xB0 | (channel & 0x0F), BYTE); // Control change command
Serial.print(0x07, BYTE); // Volume command
Serial.print(volume & 0x7F, BYTE); // Volume 0-127
#endif
}
就是这样。如果您正确连接了硬件,一切应该都能正常工作。在 Windows 上,有一个名为 MIDI-OX 的免费程序,可以显示所有传入的 MIDI 消息,在 Mac OSX 上有一个名为 MIDI Monitor 的免费程序,功能相同。如果您运行其中一个程序(并将您的表情踏板的 MIDI 输出端口连接到您的电脑),您应该会在移动哇哇电位器时看到 MIDI CC 音量消息传入。
组装最终组件
这时,我已经准备好将设计提交到最终电路板并组装组件了。我将面包板上的设计转移到了电路板上,并添加了连接器,以便我可以轻松地添加和移除不在电路板上的组件。
调整范围
硬件完全组装后,哇哇电位器不再有完整的运动范围(由于顶板的范围有限),因此需要进行一些小的代码更改。当哇哇电位器有完整运动范围时,s_nLastPotValue
的范围是 0-1023,s_nLastMappedValue
的范围是 0-127。由于范围有限,s_nLastPotValue
现在范围是 0-1002,s_nLastMappedValue
的范围是 0-125。我用一个小改动更新了 loop()
,它变成了:
void loop()
{
static int s_nLastPotValue = 0;
static int s_nLastMappedValue = 0;
int nCurrentPotValue = analogRead(POT_PIN);
if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
return;
s_nLastPotValue = nCurrentPotValue;
//int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
int nMappedValue = map(nCurrentPotValue, 0, 1002, 0, 127); // Map the value to 0-127
if(nMappedValue > 127)
nMappedValue = 127;
if(nMappedValue == s_nLastMappedValue)
return;
s_nLastMappedValue = nMappedValue;
MidiVolume(MIDI_CHANNEL, nMappedValue);
}
SysExPedal
一些 MIDI 消息,如 MIDI Note On/Off 命令和 Program Change 命令是特定的、封闭的、严格定义的。然而,制造商需要一种机制来将自定义数据从一个设备传输到另一个设备,因此将开放式的 System Exclusive (SysEx) 消息添加到 MIDI 规范中。SysEx 消息以 0xF0 开始,以 0xF7 结束。通常,这些标记内的格式是:
0xF0 - SysEx start
xx - Manufacturer ID
xx - Model ID
xx - MIDI channel
xx - data 1
xx - data 2
...
xx - data N
0xF7 - SysEx end
……但格式由制造商自行定义。正如我在本文开头提到的,这个项目以及硬件编程的整个原因是我有一个效果器,它不响应 MIDI CC 消息,但会响应 SysEx 消息。从发送 CC 消息切换到发送 SysEx 消息非常直接。如果您比较 SysExPedal 草图和 VolumePedal 草图,您会发现这两个草图几乎是相同的。当电位器改变时调用 MidiVolume()
的地方现在变成了 SendSysEx()
。
void SendSysEx(byte channel, byte volume)
{
#ifdef DEBUG
Serial.println(volume, DEC);
#else
SerialOutput(0xF0); // SysEx start
SerialOutput(0x00); // Manufacturer 0
SerialOutput(0x01); // Model 1
SerialOutput(channel); // MIDI channel
SerialOutput(0x42); // Data
SerialOutput(0x42); // Data
SerialOutput(volume); // Data
SerialOutput(0x42); // Data
SerialOutput(0x42); // Data
SerialOutput(0xF7); // SysEx end
#endif
}
这里呈现的是一个虚构设备的示例。由于 SysEx 消息的格式对于特定设备(在我的例子中,是特定的补丁)来说是唯一的,所以我决定只发布 SysEx 消息的原始轮廓。
结论
这结束了第三篇文章,也是我对 Arduino 硬件平台系列的总结。我们从连接一个简单的 LED 到开发一个功能齐全的产品。希望您能将这里提出的一些想法应用到您自己的 Arduino 微控制器项目中,构建出独特的设备。
历史
- 2009 年 7 月 16 日:初始发布
- 2009 年 7 月 16 日:文章更新