這幾天在研究如何使用loopMIDI傳送LED訊號到控制板上,加裝LED在之前的DJ控制器上面,效果還真不錯。
如果ATmega328p LED接腳不夠用,可參考以下文章擴充接腳
建議
建議如果還沒有看過以下文章,或者沒有MIDI的基礎概念,請先參閱以下文章:
如果ATmega328p LED接腳不夠用,可參考以下文章擴充接腳
Setup
首先,必須創建兩個虛擬的MIDI端口,loopMIDI Port IN是輸入,loopMIDI Port OUT是輸出。
接下來要定義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>
<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>
<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>
<?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);
}
}
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);
}
#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);
}


沒有留言:
張貼留言