2019年11月3日 星期日

【單晶片】利用Virtual DJ MIDI端口控制arduino上的LED



這幾天在研究如何使用loopMIDI傳送LED訊號到控制板上,加裝LED在之前的DJ控制器上面,效果還真不錯。



建議


建議如果還沒有看過以下文章,或者沒有MIDI的基礎概念,請先參閱以下文章:

Setup


首先,必須創建兩個虛擬的MIDI端口,loopMIDI Port IN是輸入,loopMIDI Port OUT是輸出。



接上控制板,並打開hairless MIDI,設定
MIDI Out:loopMIDI Port IN
MIDI In:loopMIDI Port OUT


定義Virtual DJ 裝置


接下來要定義Virtual DJ輸出裝置也就是loopMIDI Port OUT,loopMIDI Port OUT主要是拿來回傳Virtual DJ的訊息給arduino。記得,輸入、輸出裝置是分開的,所以需要定義兩個Virtual DJ裝置。
裝置定義的方法請參考此篇:Virtual DJ定義虛擬MIDI端口 loopMIDI devices.xml

新建兩個XML檔在C:\Users\%USERNAME%\Documents\VirtualDJ\Devices裡,檔案名稱可自訂,但是IN跟OUT最好在檔案上面標明,這邊用arduino VDJ Controller MIDI IN Device.xml及arduino VDJ Controller MIDI OUT Device.xml舉例。

Virtual DJ會將從軟體收到的LED訊號利用傳送Note On(亮燈)、Note Off(熄燈)到loopMIDI Port OUT,再由hairless MIDI將從loopMIDI Port OUT收到的MIDI信號轉成UART並送回arduino。

arduino VDJ Controller MIDI OUT Device.xml

<device name="arduino VDJ Controller MIDI OUT" author="VincentYeh" description="arduino VDJ Controller MIDI OUT" version="800" decks="2" type="MIDI" identifier ="#media#0000#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\tevmidi1" motor="no">
<led note="0x00" name="deck_left_beat_1" channel="1"/>
<led note="0x01" name="deck_left_beat_2" channel="1"/>
<led note="0x02" name="deck_left_beat_3" channel="1"/>
<led note="0x03" name="deck_left_beat_4" channel="1"/>

<led note="0x04" name="deck_right_beat_1" channel="1"/>
<led note="0x05" name="deck_right_beat_2" channel="1"/>
<led note="0x06" name="deck_right_beat_3" channel="1"/>
<led note="0x07" name="deck_right_beat_4" channel="1"/>

</device>



arduino VDJ Controller MIDI IN Device.xml <device name="arduino VDJ Controller MIDI IN" author="VincentYeh" description="arduino VDJ Controller MIDI IN" version="800" decks="2" type="MIDI" identifier ="#media#0000#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\tevmidi0" motor="no">

<button note="0x39" name="BUTTON-L0" channel="0" />
<button note="0x3A" name="BUTTON-R0" channel="0" />

<button note="0x3B" name="BUTTON-L1" channel="0" />
<button note="0x3C" name="BUTTON-R1" channel="0" />

<button note="0x3D" name="BUTTON-L2" channel="0" />
<button note="0x3E" name="BUTTON-R2" channel="0" />

<slider cc="0x3F" name="SLIDER-L0" channel="0"/>
<slider cc="0x40" name="SLIDER-R0" channel="0"/>

<button note="0x41" name="BUTTON-L3" channel="0" />
<button note="0x42" name="BUTTON-R3" channel="0" />

<button note="0x43" name="BUTTON-L4" channel="0" />
<button note="0x44" name="BUTTON-R4" channel="0" />

<button note="0x45" name="BUTTON-L5" channel="0" />
<button note="0x46" name="BUTTON-R5" channel="0" />

<button note="0x47" name="BUTTON-L6" channel="0" />
<button note="0x48" name="BUTTON-R6" channel="0" />


<slider cc="0x57" name="SLIDER-M0" channel="0"/>


</device>

Mapping


裝置定義好後,接下來要來Mapping,指定Virtual DJ介面的某個元件,這篇文章以介面上方的LED為例,如果想在板子上做出一樣的效果的話,我們需要兩組4顆同顏色的LED,我們使用4顆綠色LED和4顆紅色LED。



接下來要將藍色上排的那四個LED分別定義,第一個框我們在arduino VDJ Controller MIDI OUT Device.xml檔案就已經定義過了名為deck_left_beat_1,以此列推。但是要如何將將介面上面的指示燈連結到deck_left_beat_1這個名稱,這時就要使用Mapper,Mapper可以指定介面上的原件,對映定義好的Device檔案元件,做下一步的傳輸動作。

藍色上排的第一個LED指令為:deck left get_beat_num 1 4?on:off
意思是指左盤所撥放的音樂節拍到達第1個beat時LED亮(on),剩下其他的節拍不會亮(off)。

依照上述的規則,只要更改紅色標示成1、2、3、4,就可以達到此功能。

如果要研究Mapper的寫法,請參考此篇:Controller Mapping files in VirtualDJ 8

新建兩個XML檔在C:\Users\%USERNAME%\Documents\VirtualDJ\Mappers裡,檔案名稱可自訂,但是IN跟OUT最好在檔案上面標明,這邊用arduino VDJ Controller MIDI IN Mapper.xml及arduino VDJ Controller MIDI OUT Mapper.xml舉例。

arduino VDJ Controller MIDI OUT Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper device="arduino VDJ Controller MIDI OUT" version="830" date="2019-10-22">
<map value="deck_left_beat_1" action="deck left get_beat_num 1 4?on:off" />
<map value="deck_left_beat_2" action="deck left get_beat_num 2 4?on:off" />
<map value="deck_left_beat_3" action="deck left get_beat_num 3 4?on:off" />
<map value="deck_left_beat_4" action="deck left get_beat_num 4 4?on:off" />

<map value="deck_right_beat_1" action="deck right get_beat_num 1 4?on:off" />
<map value="deck_right_beat_2" action="deck right get_beat_num 2 4?on:off" />
<map value="deck_right_beat_3" action="deck right get_beat_num 3 4?on:off" />
<map value="deck_right_beat_4" action="deck right get_beat_num 4 4?on:off" />
</mapper>


arduino VDJ Controller MIDI IN Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper device="arduino VDJ Controller MIDI IN" version="830" date="2019-10-22">
<map value="BUTTON-L0" action="deck left play_button" />
<map value="BUTTON-L1" action="deck left stop_button" />
<map value="BUTTON-L2" action="deck left sync" />
<map value="BUTTON-L3" action="deck left pitch_reset" />
<map value="BUTTON-L4" action="deck left loop_half" />
<map value="BUTTON-L5" action="deck left loop" />
<map value="BUTTON-L6" action="deck left loop_double" />
<map value="BUTTON-R0" action="deck right play_button" />
<map value="BUTTON-R1" action="deck right stop_button" />
<map value="BUTTON-R2" action="deck right sync" />
<map value="BUTTON-R3" action="deck right pitch_reset" />
<map value="BUTTON-R4" action="deck right loop_half" />
<map value="BUTTON-R5" action="deck right loop" />
<map value="BUTTON-R6" action="deck right loop_double" />
<map value="SLIDER-M0" action="crossfader" />
<map value="SLIDER-L0" action="deck left filter" />
<map value="SLIDER-R0" action="deck right filter" />
</mapper>


撰寫Arduino程式、焊上八顆LED


最後,準備4顆綠色LED和4顆紅色LED焊上板子,如下圖。



紅色LED從左到右分別為arduino的第2, 3, 4, 8
綠色LED從左到右分別為arduino的第9, 10, 11, 12

介面上的燈號被觸發時會發送Note On信號,不會發送Note Off信號,所以判別燈的亮暗使用MIDI Note On的DataByte,當DataByte等於127時代表燈亮,當DataByte等於0時代表燈滅。

MIDI封包由三個byte組成,使用以下程式來控制LED。

byte input[3] = {0, 0, 0};
  if (Serial.available() > 2) {//必須接到兩個byte以上才會成立
    Serial.readBytes(input, 3);//一次讀取三個byte到陣列input上
    byte statusByte = input[0];//input[0]即為MIDI的 statusByte
    if (statusByte >= 0x90 && statusByte < 0xA0) {//當statusByte為Note On時成立
      byte chennal = statusByte - 0x90;
      digitalWrite(LED_PIN[input[1]],input[2]==127?HIGH:LOW);
    }
  }

由於我們在Device檔案定義LED是由0x00~0x07,而且LED腳位順序與陣列LED_PIN[]一致,陣列的Index索引值即為對應的LED腳位。

VDJ_Controller_2019-10-23.ino

#define DEBUG false
//You can use Arduino Serial Plotter for debug by changing DEBUG to true,but it will make your MIDI board unable to send MIDI Message.

#define VALUE_MIN_RANGE 1
#define DEBOUNCE_MS 100
#define INPUT_TYPE_ANALOG 0
#define INPUT_TYPE_DIGITAL 127

#define MIDI_MSG_NOTE_OFF 128 //or 0x80
#define MIDI_MSG_NOTE_ON 144 //or 0x90
#define MIDI_MSG_CTRL_CHANGE 176 //or 0xB0

const byte LED_PIN[] = {2, 3, 4, 8, 9, 10, 11, 12};

const byte INPUT_TYPE_MAP[2][8] = {
  {
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_ANALOG,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL
  },
  {
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_ANALOG,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL,
    INPUT_TYPE_DIGITAL
  }
};

byte previous_values[2][8];
byte previous_M_slide_value;
unsigned long btn_timer[2][8];
//unsigned long timer = 0;

void setup() {

  Serial.begin(115200);
  previous_M_slide_value = -1;
  for (int i = 0; i < 8; i++) {
    previous_values[0][i] = -1;
    previous_values[1][i] = -1;
    btn_timer[0][i] = 0;
    btn_timer[1][i] = 0;
  }

  pinMode(7, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(5, OUTPUT);


  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(8, OUTPUT);

  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);

}

void loop() {
  byte input[3] = {0, 0, 0};
  if (Serial.available() > 2) {
    Serial.readBytes(input, 3);
    byte statusByte = input[0];
    if (statusByte >= 0x90 && statusByte < 0xA0) {
      byte chennal = statusByte - 0x90;
      digitalWrite(LED_PIN[input[1]],input[2]==127?HIGH:LOW);
    }
  }
  for (int i = 0; i < 8; i++) {
    select(i);
    int curr_value[2];
    curr_value[0] = analogRead(A0);
    curr_value[1] = analogRead(A1);
    for (byte side = 0; side < 2; side++) {
      if (INPUT_TYPE_MAP[side][i] == INPUT_TYPE_DIGITAL) {
        if (previous_values[side][i] != (ATD(curr_value[side]) == 1 ? 0 : 127) && millis() >= btn_timer[side][i]) {

          //        When button is pressed.
          byte val = ATD(curr_value[side]) == 1 ? 0 : 127;
          previous_values[side][i] = val;
          if (val == 127) {
            if (!DEBUG)sendNoteOn(0x39 + side + i * 2, val, 1);
          } else if (val == 0) {
            if (!DEBUG)sendNoteOff(0x39 + side + i * 2, val, 1);
            btn_timer[side][i] = millis() + DEBOUNCE_MS;
          }
        }

      } else if (INPUT_TYPE_MAP[side][i] == INPUT_TYPE_ANALOG) {

        int analog_val = map(curr_value[side], 0, 1023, 0, 127);
        if (range(previous_values[side][i], analog_val, VALUE_MIN_RANGE) != previous_values[side][i]) {
          //        When slider value is changed.
          previous_values[side][i] = analog_val;
          if (!DEBUG)sendCtrlChange(0x39 + side + i * 2, previous_values[side][i], 1);
        }
      }
    }
    if (DEBUG)for (byte si = 0; si < 2; si++) {
        Serial.print(String(previous_values[si][i] + i * 150 + si * 150 * 8));
        Serial.print(",");
      }
  }
  if (DEBUG)Serial.println();

  int analog_val = map(analogRead(A2), 0, 1023, 0, 127);
  if (range(previous_M_slide_value, analog_val, VALUE_MIN_RANGE) != previous_M_slide_value) {
    previous_M_slide_value = analog_val;
    if (!DEBUG)sendCtrlChange(0x57, previous_M_slide_value, 1);
  }


}


void printChart(int val) {
  Serial.print(val);
  Serial.print(",");
}
int range(int previous, int current, int d) {
  //  This method compare previous value and current value.
  //  If current value only increase or decrease "d",this method will return previous value.
  //  If current value increase or decrease "d" + >0 value ,this method will return current value.

  //example:
  //  previous=5;current=4;d=1
  //  return 5

  //  previous=5;current=3;d=1
  //  return 3

  if (current == 127)return 127;
  else if (current == 0)return 0;
  if (current > previous && current - d == previous)return previous;
  else if (current < previous && previous - d == current)return previous;
  else if (current == previous)return previous;
  return current;
}

bool ATD(int analog_val) {
  //  Analog to digital
  //  analog_val:0~1023
  //  return 0 or 1.
  if (((float)analog_val * 0.0048828125) > 2.94) {
    return 1;
  } else if (((float)analog_val * 0.0048828125) <= 1.47) {
    return 0;
  }
}

void select(int select) {
  //  CD4051 select
  //  C (MSB) : S2  13  D7
  //  B       : S1  12  D6
  //  A (LSB) : S0  11  D5

  for (int inputPin = 0; inputPin < 3; inputPin++)
  {
    int pinState = bitRead(select, inputPin);
    digitalWrite(inputPin + 5, pinState);
  }
}

//MIDI------------------------------------------
void sendNoteOn(byte note, byte velocity, byte channel) {
  sendChannelVoiceMsg(MIDI_MSG_NOTE_ON | (channel - 1), note, velocity);

}

void sendNoteOff(byte note, byte velocity, byte channel) {
  sendChannelVoiceMsg(MIDI_MSG_NOTE_OFF | (channel - 1), note, velocity);
}

void sendCtrlChange(byte cc, byte value, int channel) {
  sendChannelVoiceMsg(MIDI_MSG_CTRL_CHANGE | (channel - 1), cc, value);
}

void sendChannelVoiceMsg(byte command, byte note, byte velocity) {
  //send MIDI Channel Voice Message
  Serial.write(command);
  Serial.write(note);
  Serial.write(velocity);
}

沒有留言:

張貼留言