MIDI контроллер на Arduino

Гораздо раньше, чем я приобрёл старую версию Raspberry Pi, я купил небольшую плату на базе Arduino Uno. Поигравшись в электронику и схемотехнику, я отложил микроконтроллер на долгое время, не найдя ему практического применения. Ещё тогда я собрал на его основе контроллер, управляющий музыкальным софтом с помощью MIDI, но дальше 4 потенциометров дело не дошло.

И вот я снова нашёл его. На этот раз я решил, что ему нужно найти практическое применение. Между системой умного сада с автополивом растений и миди-контроллером я остановился на втором, потому же полив растений - процесс медитативный, а на настоящий контроллер я не потрачусь.

Пораскинув гуглом, обнаружил большое множество готовых проектов, с туториалами и без, нашёл даже проект, где продают уже готовый набор с готовой печатной платой, куда останется лишь вставить все детали из комплекта (там же есть собранный контроллер, и тут есть только одна разумная причина для покупки - это гораздо дешевле аналогов). Я решил, что хочу делать всё самостоятельно, но не брать за основу хорошие проекты было бы глупо. К сожалению, единственное, что сработало для меня в должной мере, это всё тот же старый гайд, по которому я собрал контроллер несколько лет назад.

Схема простая:

На плату загружается код (он есть в статье по ссылке, но на всякий случай я сохраню его здесь):

int val = 0; //Our initial pot values. We need one for the first value and a second to test if there has been a change made. This needs to be done for all 3 pots.
int lastVal = 0;
int val2 = 0;
int lastVal2 = 0;
int val3 = 0;
int lastVal3 = 0;

void setup()
{
   Serial.begin(9600);       // Set the speed of the midi port to the same as we will be using in the Hairless Midi software 
}

void loop()
{
   val = analogRead(0)/8;   // Divide by 8 to get range of 0-127 for midi
   if (val != lastVal) // If the value does not = the last value the following command is made. This is because the pot has been turned. Otherwise the pot remains the same and no midi message is output.
   {
   MIDImessage(176,1,val);}         // 176 = CC command (channel 1 control change), 1 = Which Control, val = value read from Potentionmeter 1 NOTE THIS SAYS VAL not VA1 (lowercase of course)
   lastVal = val;

   val2 = analogRead(1)/8;   // Divide by 8 to get range of 0-127 for midi
   if (val2 != lastVal2) 
   {
   MIDImessage(176,2,val2);}         // 176 = CC command, 2 = Which Control, val = value read from Potentionmeter 2
   lastVal2 = val2;

   val3 = analogRead(2)/8;   // Divide by 8 to get range of 0-127 for midi
   if (val3 != lastVal3) 
   {
   MIDImessage(176,3,val3);}         // 176 = CC command, 3 = Which Control, val = value read from Potentionmeter 3
   lastVal3 = val3;
delay(10); //here we add a short delay to help prevent slight fluctuations, knocks on the pots etc. Adding this helped to prevent my pots from jumpin up or down a value when slightly touched or knocked.
}

void MIDImessage(byte command, byte data1, byte data2) //pass values out through standard Midi Command
{
   Serial.write(command);
   Serial.write(data1);
   Serial.write(data2);
}

Суть работы простая: Arduino получает аналоговый сигнал (Analog), затем программа переносит его через последовательный порт (Serial), и так как в Arduino Uno нет встроенного MIDI, нам необходимо использовать софт для преобразования последовательного сигнала (Serial) в MIDI. Я рекомендую Hairless MIDI (Windows & MacOS).

В ней сигнал преобразуется в MIDI, и уже в нужном нам DAW или диджейском софте мы можем мэппить (настраивать соответствие миди-сигнала определённому действию в программе) наши крутилки.

И, казалось бы, контроллер готов, но у него есть только потенциометры. Мне нужно больше. В поисках примеров, где используются и потенциометры, и кнопки, я нашёл много хороших проектов, но все они использовали сторонние библиотеки: MIDI.h или MIDI_controller.h. Это универсальные инструменты для сборки своего контроллера, там прописаны все возможные варианты, и, если я правильно понял, они позволяют пропустить посредника в виде Hairless MIDI. Но в их универсальности их проблема - они громоздкие, сложные, и настроить такую - всё равно что написать свою. Я хотел аккуратный код, который подходит моему проекту.

Не писав ничего на Си уже три года, я взял волю в кулак, и, потратив пару дней на изучение матчасти, написал код, подходящий мне. В нём я учитывал особенности своего проекта:

  • Количество потенциометров и кнопок динамическое - можно настроить в коде
  • Переключатель, перебрасывающий один сенсор между двумя разными значениями, чтобы увеличить возможное количество миди-команд в два раза
  • Рядом с переключателем - светодиод-индикатор

Проект на GitHub

Код и схема с двумя потенциометрами и тремя кнопками (зелёная в качестве переключателя, другие две - в качестве миди-нот) ниже.

На всякий случай код:

/* Arduino MIDI Controller

Created by Viktor G. / https://viktorgordienko.com

This code is in the public domain.

You can: copy it, modify it, share it, buy it, use it, break it, fix it, trash it, change it, mail - upgrade it, etc.

*/

//Potentiometers
int numberOfPots = 2;     //Total number of potentiometers
int potValues[2];         //Array of current values
int potLastValues[2];     //Array of last values (used to compare)

//Buttons
int numberOfButtons = 2;  //Total number of buttons EXCEPT the switch button
int buttonsStates[2];     // the current state of the output pin
int buttonReadings[2];    // the current reading from the input pin
int buttonPrevious[2];    // the previous reading from the input pin
long time = 0;            // the last time the output pin was toggled
long debounce = 200;      // the debounce time, increase if the output flickers
// the last 2 are long’s because the time, measured in msec, will quickly become a bigger number than can be stored in an int.

//Switch button
int controlSwitch = 0;    //used to add another set of controls
int switchState = LOW;
int switchReading;
int switchPrevious = HIGH;

void setup() {
  for (int i = 0; i <= numberOfButtons; i++) {
    pinMode(i + 8, INPUT_PULLUP); //+8 because I started on D8
  }
  pinMode(13, OUTPUT);
  Serial.begin(9600); //Set the speed of the midi port to the same as we will be using in the Hairless Midi software
}

void loop()
{
  //Potentiometesrs on А0-A(numberOfPots-1)
  for (int i = 0; i < numberOfPots; i++) {
    potValues[i] = analogRead(i) / 8;                     // Divide by 8 to get range of 0-127 for MIDI
    if (potValues[i] != potLastValues[i])                 //If the value does not = the last value the following command is made. This is because the pot has been turned. Otherwise the pot remains the same and no midi message is output.
    {
      MIDImessage(176, i + controlSwitch, potValues[i]);  //176 = CC command (channel i control change), (i + controlSwitch) = Which Control, val = value read from Potentionmeter 1
    }
    potLastValues[i] = potValues[i];
  }
  //Potentiometesrs on А0-A(numberOfPots-1)

  //Buttons on D8-D9
  for (int i = 0; i < numberOfButtons; i++) {
    buttonReadings[i] = digitalRead(i + 8);
    if (buttonReadings[i] == HIGH && buttonPrevious[i] == LOW && millis() - time > debounce) {
      if (buttonsStates[i] == HIGH) {
        buttonsStates[i] = LOW;
        MIDImessage(144, i + controlSwitch, 127);         //144 - Note ON, (i + controlSwitch) - note number in MIDI [e.g. 69 = A4], 127 - note velocity
      } else {
        buttonsStates[i] = HIGH;
        MIDImessage(144, i + controlSwitch, 127);
        time = millis();
      }
    }
    buttonPrevious[i] = buttonReadings[i];
  }
  //Buttons on D8-D9

  //The control switch button on D10 and LED on D13
  switchReading = digitalRead(10);
  if (switchReading == HIGH && switchPrevious == LOW && millis() - time > debounce) {
    if (switchState == HIGH) {
      switchState = LOW;
      controlSwitch = 0;
    } else {
      switchState = HIGH;
      controlSwitch = 64;
    }
  }
  digitalWrite(13, switchState);
  switchPrevious = switchReading;
  //The control switch button on D10 and LED on D13

  delay(10); //here we add a short delay to help prevent slight fluctuations, knocks on the pots etc. Adding this helped to prevent my pots from jumpin up or down a value when slightly touched or knocked.
}

void MIDImessage(byte message, byte control, byte value)  //Pass values out through standard Midi Command
{
  Serial.write(message);
  Serial.write(control);
  Serial.write(value);
}

Получился примитивный гранулярный синтезатор с ограниченными возможностями по извлечению звука. Тем не менее, это хороший пример того, как сложна электронная музыка под капотом.

August 1, 2019