市面上的DJ板太貴了怎麼辦,有興趣但買不起。沒關係,你可以自己搞一個出來。
需要安裝以下應用程式
- VirtualDJ (須購買完整版,或者...網路找一下破解)
- MIDI-OX
- loopMIDI
- hairless MIDI
- RealTerm: Serial/TCP Terminal
請先研究以下重點技術:
結構
一個虛擬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
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封包。
正常資料會顯示像以下表格:
使用arduino傳送Channel Voice Messages
Channel Voice Messages的封包排列方式為:
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、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
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封包。
正常資料會顯示像以下表格:
| STATUS | DATA1 | DATA2 | CHAN | EVENT |
| 90 | 50 | 7F | 1 | Note On |
| 80 | 50 | 00 | 1 | Note Off |
arduino實現
使用arduino傳送Channel Voice Messages
Channel Voice Messages的封包排列方式為:
| Status Byte | Data Byte 1 | Data Byte 2 |
| 90H | 50H | 7F |
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 +chennal | Key number | Note on/off velocity |
- Note on及Note off可比喻為按下琴鍵和放開琴鍵。
- Key number意思就是鋼琴的音高(可比照前圖),數字範圍為0~127。
- velocity即鋼琴按下按鍵後發出聲音的強度,數字越大琴聲越強,數字範圍為0~127。
因為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 +chennal | Controller number | Controller 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>







沒有留言:
張貼留言