65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 Arduino 的 MIDI 表达踏板

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (45投票s)

2009年7月16日

CPOL

10分钟阅读

viewsIcon

170439

downloadIcon

828

用 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

(这张图有点难看清。如果您放大图片,就能更清楚地看到 Arduino 的引脚。)

模拟引脚需要 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-300Behringer 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_nLastPotValueif(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD) 检查可以防止电位器产生噪声,而 s_nLastMappedValueif(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 音量消息传入。

组装最终组件

这时,我已经准备好将设计提交到最终电路板并组装组件了。我将面包板上的设计转移到了电路板上,并添加了连接器,以便我可以轻松地添加和移除不在电路板上的组件。

这张照片是从近距离拍摄的。因此,图像会产生扭曲。底部的绿色线将 MIDI 连接器的引脚 5 连接到 TX0,但在图像中看起来像是连接到 RXI,而不是 TX0。

电路板水平翻转以显示背面(顶部轨道是 +V,底部轨道是 GND)。

带组件的电路板。

电路板连接特写。

组件的内部组装。

调整范围

硬件完全组装后,哇哇电位器不再有完整的运动范围(由于顶板的范围有限),因此需要进行一些小的代码更改。当哇哇电位器有完整运动范围时,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 日:文章更新
© . All rights reserved.