2019年9月22日 星期日

【單晶片】如何製自MIDI arduino Virtual DJ 控制板




市面上的DJ板太貴了怎麼辦,有興趣但買不起。沒關係,你可以自己搞一個出來。


需要安裝以下應用程式


請先研究以下重點技術:

結構




電路








loopMIDI

一個虛擬MIDI環境,因為應用程式不能直接傳送或接收MIDI信號到另一個應用程式,必須使用loopMIDI建立一個虛擬MIDI環境,可以讓hairless MIDI所傳送的信息送到VirtualDJ。


hairless MIDI

將UART TX/RX信號轉換為MIDI信息。選擇arduino的COM,選擇loopMIDI Port。鮑率設定115200。


VirtualDJ

電腦的混音應用程式,可接收MIDI信號。



CH340G

CH340G是一個ic,將單晶片輸出的UART TX/RX信號轉成USB D+/D-介面。


CD4051

CD4051是一個ic,是一個8ch的類比多工器,有三條選擇線。使用CD4051是怕ATmega328的IO接腳不夠用,透過選擇的方式輸入資料,8個數據資料只需要占用到一個A0就可以讀到8筆資料。


Button

電子元件,按鈕。


Potentiometer

電子元件,電位器。


環境建置


1. 製作控制板

2. 調整loopMIDI

建立一個loopMIDI Port。




3. 設定hairless MIDI

通常MIDI設備就是使用UART在傳輸資料的,鮑率是31250。但是我們因為使用的是hairless MIDI,並非正統的MIDI裝置,所以還是可以把鮑率設在115200、9600。

將控制板接到電腦上,在Serial port上選擇控制板對應的位置,MIDI Out請選擇你剛剛建立的loopMIDI Port。接下來選File>Preferences,將鮑率設定成115200。設定完成後再將Serial<->MIDI Bridge On打勾。*開啟的時候不可以上傳程式碼到ATmega328上面,因為Port被hairless MIDI佔用了,需要把Serial<->MIDI Bridge On關掉才能使用。




4. 設定MIDI-OX

在製作MIDI裝置時會遇到的問題很多,所以還要再安裝一個MIDI信息監聽工具MIDI-OX。
MIDI-OX可以監控MIDI封包,確保資料的正確性。

點選Menu上面的Options>MIDI Devices。在MIDI inputs的清單下面選擇之前建立的loopMIDI Port。





5. 測試UART。

將以下程式上傳到ATmega328上,並打開RealTerm。將鮑率設定成115200,DisplayAs設定成Hex或Hex+[space]。輸出你會看到一行90507F805000,在Debug的時候比較不容易閱讀,可以在Display的Data Frames 調整輸出的樣式。如果要讓3Bytes作為一個Frame,每個Frame均會換行,將Single打勾,並點擊Gulp。這樣的話看起來就清晰多了。
90507F MIDI資料,可以看成90 50 7F,arduino的byte可以表示0~255(0xFF)之間的數字。

程式碼:

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

void setup() {
  //設定鮑率成115200
  Serial.begin(115200);

  }

void loop() {
  //送出MIDI NoteOn Message
  sendNoteOn(0x50,127,1);
  delay(1000);

  //送出MIDI NoteOff Message
  sendNoteOff(0x50,0,1);
  delay(5000);

}

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 status, byte data1, byte data2) {
  //送出MIDI NoteOff Message
  Serial.write(status);
  Serial.write(data1);
  Serial.write(data2);
}








6. 測試MIDI Input。

確定UART的原始資料正確後,開啟Serial<->MIDI Bridge On,並打開MIDI-OX做進一步的確認。點擊上方的Display Raw MIDI Input,視窗上顯示收到的MIDI封包。

正常資料會顯示像以下表格:

STATUSDATA1DATA2CHANEVENT
90507F1Note On
8050001Note Off




arduino實現


使用arduino傳送Channel Voice Messages

Channel Voice Messages的封包排列方式為:

Status ByteData Byte 1Data Byte 2

90H50H7F

Status Byte = 90H  -->Note On
Chennal=(Status Byte - 90H) + 1   --> Chennal 1
Key number=50H
Note velocity=7FH

所以輸出所代表的意義是:
90507F:通道1;Note On;音高80;強度127
805000:通道1;Note Off;音高80;強度0

STATUS、DATA1、DATA2各自為一個位元組,每個位元組使用16進制表示。函式sendChannelVoiceMsg的第一個參數status就是拿來送STATUS Byte的。

程式碼:

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

void sendChannelVoiceMsg(byte status, byte data1, byte data2) {
  Serial.write(status);
  Serial.write(data1);
  Serial.write(data2);
}


使用arduino傳送Note On、Note Off

Note On、Note Off的封包排列方式為:

Note on/off +chennalKey numberNote on/off velocity

  • Note on及Note off可比喻為按下琴鍵和放開琴鍵。
  • Key number意思就是鋼琴的音高(可比照前圖),數字範圍為0~127。
  • velocity即鋼琴按下按鍵後發出聲音的強度,數字越大琴聲越強,數字範圍為0~127。

Note On、Note Off分別是按下按鈕、放開按鈕這兩個動作,Virtual DJ的Note On、Note Off很單純,Note On的velocity為127,Note Off的velocity為0。
因為Note On、Note Off皆為Channel Voice Messages,所以sendNoteOn、sendNoteOff皆會呼叫sendChannelVoiceMsg,差別只有在STATUS Byte要送出MIDI_MSG_NOTE_OFF 還是MIDI_MSG_NOTE_ON 而已。


程式碼:

sendNoteOn(0x50,127,1);//壓下按鈕
sendNoteOff(0x50,0,1);//放開按鈕

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);
  }


使用arduino傳送Control Change

Control Change的封包排列方式為:

Control Change +chennalController numberController value

  • Controller number就是單指那個旋鈕的編號,數字範圍為0~127。
  • Controller value即是代表位資料,數字範圍為0~127。

Control Change相當於控制板的可變電阻被旋轉改變電位時的動作。Controller number主要用途是定義是是哪個旋鈕電位改變,而Controller value就是改變的值。Control Change也是Channel Voice Messages,所以也會呼叫sendChannelVoiceMsg。

程式碼:

sendCtrlChange(0x53,110,1);

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

程式碼上傳

接下來,把程式碼上傳到arduino,記得把Serial<->MIDI Bridge On關掉。

程式碼:

#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 MIDI_MSG_NOTE_OFF 128 //or 0x80
#define MIDI_MSG_NOTE_ON 144 //or 0x90
#define MIDI_MSG_CTRL_CHANGE 176 //or 0xB0

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);

}

void loop() {

  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 (i != 3 && 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 (i == 3) {
        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);
}


定義MIDI裝置

接下來要將這塊板子定義成一個MIDI裝置,好讓Virtual DJ可以辨識這個裝置。Virtual DJ有一個檔案在C:\Users\%USERNAME%\Documents\VirtualDJ\Devices\devices.xml
這個檔案是用來定義裝置所送出的MIDI Messages在程式裡是對應哪個代號,例如:

devices.xml:<button name="BUTTON-L0" channel="0" note="0x39"/>

Arduino:sendNoteOn(0x39,127,1);
sendNoteOff(0x39,0,1);

這段的意思就是宣告一個按鈕,這個按鈕所對應到的note是0x39。
當在Chennal 1收到Note On 或 Note Off而且note為0x39的時候,Virtual DJ會接收到BUTTON-L0已被按下的事件。


devices.xml:<slider name="SLIDER-M0" channel="0" cc="0x57"/>

Arduino:
sendCtrlChange(0x57,110,1);

這段的意思就是宣告一個滑標,這個按鈕所對應到的cc是0x57。
當在Chennal 1收到Control Change而且cc為0x57的時候,Virtual DJ會接收到SLIDER-M0已被改變的事件,並且回傳改變後的值110。


當我們在定義一個裝置時,需要標註裝置的特性,特別是這個裝置的資料來源,根據我們之前所看到的結構圖hairless MIDI才是主要的信號源,但是hairless MIDI與Virtual DJ 並無法互相傳輸資料,所以才要透過loopMIDI這個虛擬MIDI端口做資料傳輸的媒介,所以對於Virtual DJ來說loopMIDI才是信號源

信號源是透過identifier來定義的,所以如果沒有指定好identifier,Virtual DJ是收不到信號的。請參考Virtual DJ定義虛擬MIDI端口 loopMIDI devices.xml此篇文章,找到MIDI端口的識別碼,按照原本步驟做的話,應該會找到類似\\?\root#media#0000#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\tevmidi0的字樣。完整的開發指南請參考Definition file for a MIDI Controller in Version 8

請複製以下程式碼到devices.xml

devices.xml

<device name="arduino VDJ Controller" author="VincentYeh" description="arduino VDJ Controller" version="800" decks="1" type="MIDI" identifier="\\?\root#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>


Mappers

不對啊,Virtual DJ光只有接收資料還是不會動阿!!
當然不會動,你目前只是把原始資料轉成代號而已,並沒有向Virtual DJ定義當按下或滑動時要做什麼動作。

而做什麼全看你,你可以隨心所欲駕馭它。要怎麼定義呢,可以用設定-->控制,選擇你剛剛在devices.xml定義的裝置名稱。接下來選擇元件,例如BUTTON-L0是個按鈕,你可以在指令欄輸入deck left play_button,讓左盤的音樂開始撥放。至於指令的部分,相信大家都心累了,所以等等會附上程式碼,如果後續想做更個人化的控制,請參考VDJscript





當你按下、滑動某個控制元件時,左上角會顯示最近的事件,所以你可以邊按按鈕邊定義動作,就不用回去看哪個代碼是對應哪個按鈕,很方便。



以下附上已完成的Mapper,請將檔案放在C:\Users\%USERNAME%\Documents\VirtualDJ\Mappers資料夾裡。

[你在devices.xml定義的裝置名稱] - [Mapper名稱 可自訂].xml
如:arduino VDJ Controller - arduino VDJ Controller.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper device="arduino VDJ Controller" version="830" date="2019-06-10">
<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" />
<map value="PAD-L0" action="deck left hot_cue 1" />
<map value="PAD-L1" action="deck left hot_cue 2" />
<map value="PAD-L2" action="deck left hot_cue 3" />
<map value="PAD-R0" action="deck right hot_cue 1" />
<map value="PAD-R1" action="deck right hot_cue 2" />
<map value="PAD-R2" action="deck right hot_cue 3" />
</mapper>


Arduino Project Hub



沒有留言:

張貼留言