Данный проект начался с углублённого изучения видеомикшера vMix. Когда начинаешь активно использовать хотя бы 50% этого ПО, само собой как-то приходит осознание, что управлять всем этим разнообразием через клавиатуру и мышь не очень удобно. Эта мысль не обошла и меня стороной. Захотелось чего-то удобного, оперативного. Побегав по поисковикам, я обнаружил в продаже готовые решения. Все они работают по протоколу MIDI, так что разнообразие моделей, годных для наших целей, просто огромное. Начиная от именитых брендов типа AKAI и заканчивая разработками самой компании vMix. Даже любимый «народный китайский» магазин выдавал по запросу vMix controller много предложений начиная от 150$ и выше. И, казалось бы, выбирай и пользуй, но то клавиш слишком мало, то слишком много, то подсветка яркая, то одноцветная, то ценник кусается. И вообще, это же не наши методы. Получается, как бы, подсознание само заставило прийти к этому… Ну что же, для таких отбитых как я, ардуина тоже имеет библиотеку протокола MIDI, так что примеров самоделок так же масса. Значит пришло время…
Из головы на бумагу
Для начала мне пришлось определиться с количеством клавиш и прочих органов управления. В нашей выездной системе 4 камеры и 6 индикаторов Tally, которые я делал специально с запасом +2, потому что иногда кроме камер используются источники из вне, да и порты на ардуинке оставлять пустые не хотелось. Ну вот цифра 6 мне вполне нравится. Не заблудишься в количестве и в то же время их хватит. Так же не плохо сделать управление выходами (запись, стрим, рендер) и конечно же клавиши перехода и Т-бар (нет, это не про штанги и спорт. Это тот самый рычажок на режиссерском пульте, что позволяет управлять переходами). Ну и хотелось бы иметь под рукой несколько «крутилок» (как вам такое слово из уст инженера?) под управление звуком.
вот такой классический макет у меня вышел.
хммм. какой же контроллер подобрать для наших непотребств? Дайте подумать: шина преднабора – PVW, шина программы – PGM, 3 перехода, 3 управления выходами, 4 звука и Т-бар, в качестве которого я решил использовать резистор фейдера от старого неисправного звукового пульта. Значит 23 порта под управление и ещё подсветка на каждую кнопку. А для того, чтобы наша библиотека MIDI работала и дружила наш микшер со всем этим управлением, нам потребуется контроллер с аппаратным USB. Так что нанка наизнанку тут точно не пройдет. Нам нужна ардуина на базе, например, ATMega32U4. Вполне подойдет Leonardo или китайские клоны типа ProMicro.
Такие ардуинки бывают 3.3в и 5в. Нам нужна последняя. А что бы уместить все наши хотелки в этот контроллер, немного прооптимизируем.
Будем использовать матричное подключение кнопок, а подсветку сделаем из адресных светодиодов WS2812
Да, опытный читатель сейчас шмякнет в меня помидором. Знаю, матрица 6х3 далеко не самая оптимальная по занимаемым портам, но большее количество нам просто не нужно. А если потребуется, то всегда можно добавить позже. Обратите внимание на количество керамических конденсаторов. WS2812 довольно шумная микросхема и если не ставить фильтры, шум лезет в аналоговые порты и значения регуляторов начинают прыгать. Так что настоятельно рекомендую не пренебрегать фильтрами.
И так, определившись со схемой, и уже имея в голове макет внешнего вида (и даже попробовав его на макетнице), садимся за печатные платы.
Плат будет 3. Основная и две вспомогательные, соединенные шлейфом. Не задействованные порты я вывел на верхний шлейф, что бы можно было их использовать в дальнейшем, без серьезных переделок. А так как я стал ленив и немного криворук с возрастом, платы я решил заказать на JLCPCB. Эти ребята воплощают любой каприз на основе файлов gerber за приемлемую цену и с отличным качеством. Сам проект gerber я оставлю в конце статьи, как обычно.
Сборка
Не сочтите за рекламу, платы и впрямь доставляют эстетический экстаз. И как только платы приехали, бросаемся собрать девайс. Но для начала нужно уделить внимание основному органу управления – кнопкам. Дело в том, что в качестве кнопок мне хотелось видеть те, что используются в настоящих «взрослых» видеомикшерах типа Sony или Guramex. И такие кнопки я нашел в Китае.
PB06 продают разных размеров и цветов. Я взял 15х15 мм, хотя, по опыту, рекомендовал бы брать 17х17 мм. Большая кнопка дает меньший шанс промазать по ней в запале, но они дороже. Тем не менее, расстояние между кнопками на плате я сделал такое, чтобы можно было разместить 17х17мм. Цвет индикатора неважен, можно брать самый дешевый. Нам их всё равно придется переделать на адресные диоды. Ну и раз уж мы заговорили о переделке:
разбираем кнопку и вынимаем оттуда индикатор.
Обратите внимание на маленькие направляющие на стенках корпуса. Они держатся на смазке и их легко потерять.
Далее берем адресный диод и припаиваем к нему 4 ножки
В качестве ножек я использовал зачищенную витую пару UTP5e. Теперь сверлим 4 отверстия в корпусе кнопки и пропускаем через них ножки нашего диода
Не забываем приклеить их там. Собираем кнопку
И так 18 раз. После этих издевательств останется сущий пустяк, повтыкать всё в плату и собрать платы между собой.
Отдельная история – это корпус. Можно выпилить из металла, собрать из стеклотекстолита, а можно распечатать на 3D принтере. В моём случае, мои хорошие друзья, смоделировали по моим чертежам такую вот коробку и распечатали для меня.
В качестве лицевой панели решили использовать оргстекло, нарезанное на лазере с ЧПУ и окрашенное изнутри. Такое решение принято потому, что только так можно обеспечить точные размеры «окон» под кнопки. Ну и, если уж друзья мои, естественно, не жадные люди, модели корпуса аккуратно сложу в том же архиве в конце статьи. Короче говоря, запихиваем все наши изыскания в это чудо корпус:
Обратите внимание, что разъем microUSB был выпилен из конструкции, как богомерзкий и ненадежный. Термоклей и паяльник – наши друзья.
Получаем вот такой прототип:
Кодим
В основе скетча лежит библиотека MIDIUSB.h которая и позволяет нам общаться с внешним миром, Keypad.h для реализации матричной клавиатуры и Adafruit_NeoPixel.h для работы с адресными светодиодами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
#include <MIDIUSB.h> #include <Keypad.h> #include <Adafruit_NeoPixel.h> #define PIN 0 //пин адресного диода #define NUMPIXELS 18 //количество диодов #define ROWS 3 //количество строк в матрице #define COLS 6 //количество столбцов в матрице #define FADES 5 //количество аналогов #define CH 0 //MIDI канал byte faders[FADES] = {A0, A1, A2, A3, A10}; //перечисление аналогов int Val[FADES]; //значения аналогов int potCState[FADES] = {0}; //Текущее состояние аналогов int potPState[FADES] = {0}; //Предыдущее состояние аналогов int potVar = 0; //Разница между состояниями int midiCState[FADES] = {0}; //Текущее значение MIDI int midiPState[FADES] = {0}; //Предыдущее значение MIDI const int TIMEOUT = 300; //Время, в течение которого потенциометр будет считываться после того, как он превысит значение varThreshold const int varThreshold = 10; //Порог изменения сигнала потенциометра boolean potMoving = true; //Если потенциометр двигается unsigned long PTime[FADES] = {0}; //Предыдущее время unsigned long timer[FADES] = {0}; //Текущее время char keys[ROWS][COLS] = { //перечисление клавиш {'1', '2', '3', '4', '5', '6'}, {'a', 'b', 'c', 'd', 'e', 'f'}, {'g', 'h', 'i', 'j', 'k', 'l'} }; byte rowPins[ROWS] = { 8, 9, 16 }; //перечисление строк byte colPins[COLS] = { 2, 3, 4, 5, 6, 7 }; //перечисление столбцов byte brightness = 64; //общий уровень яркости bool ledState = false; //текущее состояние мигающих диодов unsigned long currentMillis = 0; //текущее время unsigned long previousMillis = 0; //предыдущее время int blinkLeds[NUMPIXELS][4] = { //перечисление мигающих диодов {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }; Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); void noteOn(byte channel, byte pitch, byte velocity) { midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity}; MidiUSB.sendMIDI(noteOn); } void noteOff(byte channel, byte pitch, byte velocity) { midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, velocity}; MidiUSB.sendMIDI(noteOff); } void controlChange(byte channel, byte control, byte value) { midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value}; MidiUSB.sendMIDI(event); } void timerLoop() //цикл таймера { currentMillis = millis(); //основное время if (currentMillis - previousMillis >= 1000) { previousMillis = currentMillis; ledState = !ledState; } for ( int i = 0; i < NUMPIXELS; ++i ) { if (blinkLeds[i][0] == 1) { if (ledState) { pixels.setPixelColor(i, blinkLeds[i][1], blinkLeds[i][2], blinkLeds[i][3]); pixels.show(); } else { pixels.setPixelColor(i, 0, 0, 0); pixels.show(); } } } } void midiControls() //цикл обработки аналогов { for (int i = 0; i < FADES; i++) { potCState[i] = analogRead(faders[i]); midiCState[i] = map(potCState[i], 10, 1000, 0, 127); //от 10 до 1000 для люфта потенциометров midiCState[i] = constrain(midiCState[i], 0, 127); potVar = abs(potCState[i] - potPState[i]); if (potVar > varThreshold) PTime[i] = millis(); timer[i] = millis() - PTime[i]; if (timer[i] < TIMEOUT) potMoving = true; else potMoving = false; if (potMoving == true) { if (midiPState[i] != midiCState[i]) { controlChange(CH, ROWS * COLS + i, midiCState[i]); MidiUSB.flush(); potPState[i] = potCState[i]; midiPState[i] = midiCState[i]; } } } } void shortcuts() //цикл обработки кнопок { if (kpd.getKeys()) { for (int i = 0; i < LIST_MAX; i++) { if ( kpd.key[i].stateChanged ) { switch (kpd.key[i].kstate) // IDLE, PRESSED, HOLD, RELEASED { case PRESSED: noteOn(CH, kpd.key[i].kcode, 64); MidiUSB.flush(); break; case RELEASED: noteOff(CH, kpd.key[i].kcode, 64); MidiUSB.flush(); break; } } } } } void activators() //цикл обработки диодов { midiEventPacket_t rx; do { rx = MidiUSB.read(); if (rx.header != 0) { for (int i = 0; i < NUMPIXELS; i++) { if (rx.byte2 == i) { blinkLeds[i][0] = 0; if (rx.byte3 == 0) pixels.setPixelColor(i, 0, 0, 0); //off if (rx.byte3 == 1) pixels.setPixelColor(i, 255, 255, 255); //white if (rx.byte3 == 2) pixels.setPixelColor(i, 32, 32, 32); //white 50% if (rx.byte3 == 3) //white blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 255; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } if (rx.byte3 == 4) pixels.setPixelColor(i, 255, 0, 0); //red if (rx.byte3 == 5) pixels.setPixelColor(i, 64, 0, 0); //red 50% if (rx.byte3 == 6) //red blink { blinkLeds[i][3] = 0; blinkLeds[i][2] = 0; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } if (rx.byte3 == 7) pixels.setPixelColor(i, 255, 128, 0); //Orange if (rx.byte3 == 8) pixels.setPixelColor(i, 64, 32, 0); //Orange 50% if (rx.byte3 == 9) //Orange blink { blinkLeds[i][3] = 0; blinkLeds[i][2] = 128; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } if (rx.byte3 == 10) pixels.setPixelColor(i, 255, 255, 0); //Yellow if (rx.byte3 == 11) pixels.setPixelColor(i, 64, 64, 0); //Yellow 50% if (rx.byte3 == 12) //Yellow blink { blinkLeds[i][3] = 0; blinkLeds[i][2] = 255; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } if (rx.byte3 == 13) pixels.setPixelColor(i, 128, 255, 0); //Lime if (rx.byte3 == 14) pixels.setPixelColor(i, 32, 64, 0); //Lime 50% if (rx.byte3 == 15) //Lime blink { blinkLeds[i][3] = 0; blinkLeds[i][2] = 255; blinkLeds[i][1] = 128; blinkLeds[i][0] = 1; } if (rx.byte3 == 16) pixels.setPixelColor(i, 0, 255, 0); //Green if (rx.byte3 == 17) pixels.setPixelColor(i, 0, 64, 0); //Green 50% if (rx.byte3 == 18) //Green blink { blinkLeds[i][3] = 0; blinkLeds[i][2] = 255; blinkLeds[i][1] = 0; blinkLeds[i][0] = 1; } if (rx.byte3 == 19) pixels.setPixelColor(i, 0, 255, 128); //SpringGreen if (rx.byte3 == 20) pixels.setPixelColor(i, 0, 64, 32); //SpringGreen 50% if (rx.byte3 == 21)//SpringGreen blink { blinkLeds[i][3] = 128; blinkLeds[i][2] = 255; blinkLeds[i][1] = 0; blinkLeds[i][0] = 1; } if (rx.byte3 == 22) pixels.setPixelColor(i, 0, 255, 255); //Aqua if (rx.byte3 == 23) pixels.setPixelColor(i, 0, 64, 64); //Aqua 50% if (rx.byte3 == 24) //Aqua blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 255; blinkLeds[i][1] = 0; blinkLeds[i][0] = 1; } if (rx.byte3 == 25) pixels.setPixelColor(i, 0, 128, 255); //DodgerBlue if (rx.byte3 == 26) pixels.setPixelColor(i, 0, 32, 64); //DodgerBlue 50% if (rx.byte3 == 27) //DodgerBlue blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 128; blinkLeds[i][1] = 0; blinkLeds[i][0] = 1; } if (rx.byte3 == 28) pixels.setPixelColor(i, 0, 0, 255); //Blue if (rx.byte3 == 29) pixels.setPixelColor(i, 0, 0, 64); //Blue 50% if (rx.byte3 == 30) //Blue blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 0; blinkLeds[i][1] = 0; blinkLeds[i][0] = 1; } if (rx.byte3 == 31) pixels.setPixelColor(i, 128, 0, 255); //Indigo if (rx.byte3 == 32) pixels.setPixelColor(i, 32, 0, 64); //Indigo 50% if (rx.byte3 == 33) //Indigo blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 0; blinkLeds[i][1] = 128; blinkLeds[i][0] = 1; } if (rx.byte3 == 34) pixels.setPixelColor(i, 255, 0, 255); //Fuchsia if (rx.byte3 == 35) pixels.setPixelColor(i, 64, 0, 64); //Fuchsia 50% if (rx.byte3 == 36) //Fuchsia blink { blinkLeds[i][3] = 255; blinkLeds[i][2] = 0; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } if (rx.byte3 == 37) pixels.setPixelColor(i, 255, 0, 128); //Pink if (rx.byte3 == 38) pixels.setPixelColor(i, 64, 0, 32); //Pink 50% if (rx.byte3 == 39) //Pink blink { blinkLeds[i][3] = 128; blinkLeds[i][2] = 0; blinkLeds[i][1] = 255; blinkLeds[i][0] = 1; } } pixels.show(); } } } while (rx.header != 0); } void logo() { pixels.clear(); for (int x = 0; x <= 254; x++) { for (int y = 0; y < NUMPIXELS; y++)pixels.setPixelColor(y, x, x, x); pixels.show(); delay(4); } for (int x = 254; x >= 0; x--) { for (int y = 0; y < NUMPIXELS; y++)pixels.setPixelColor(y, x, x, x); pixels.show(); delay(2); } } void setup() { pinMode(8, INPUT_PULLUP); pinMode(9, INPUT_PULLUP); pinMode(16, INPUT_PULLUP); for (int i = 0; i < FADES; i++) pinMode(faders[i], INPUT); pixels.setBrightness(brightness); pixels.begin(); logo(); pixels.clear(); pixels.show(); } void loop() { timerLoop(); shortcuts(); midiControls(); activators(); } |
Сам код довольно простой. Остановлюсь на некоторых моментах. Во первых функция timerLoop() существует для мигающего режима индикаторов. Естественно никаких delay! Задержка – наш враг. Во вторых- в midiControls() применены программные фильтры, что бы регуляторы не «колбасило». Не буду строить из себя дофига программиста, просто признаюсь, что после нескольких попыток написать фильтр, я плюнул и стащил этот кусок кода из интернетов (говорил же сто раз: не программист я!). И ещё не маловажный момент: переменная brightness, устанавливающая общую яркость светодиодов, подобрана так, чтобы индикаторы не слепили в условиях темноты и в то же время не перегружали USB порт по току потребления.
Вздумаете прибавить яркости, обязательно учитывайте, что USB2.0 не даст более 500мА по току.
Отдельно хотел затронуть тему компиляции. Мне хотелось спрятать торчащие из проекта «рожки» ардуины, чтобы выглядеть немного солидней (Да, да. Тщеславный инженер. Эка невидаль). Для этого я переделал описание плат Pro micro и Leonardo и сохранил их отдельно в папке c:\Users\%username%\Documents\Arduino\hardware\. Эти описания я так же оставлю внизу в архиве. Теперь при запуске Arduino IDE мы можем выбрать такие платы:
В моём случае я использовал китайский клон платы и определялся он изначально как Leonardo. Её я и выбрал. Теперь, если мы прошьем наш скетч, то увидим в диспетчере устройств гордое название:
программная часть
Думали безумие закончилось? А вот и нет! В продолжении подпитки собственного эго и тщеславия, я переписал названия в драйверах ардуины и конечно же, пришлось переподписывать их собственным сертификатом. О том, как это сделать, можно спросить в интернетах. Это конечно не обязательно, но выглядит красиво. Всё это безумие, вместе с файлами описания активаторов и шорткатов я упаковал в установочный пакет, который так же лежит в архиве в конце статьи. Опустим, пожалуй, скучную историю написания всего этого непотребства (я же говорю – больной ублюдок тщеславный инженер), а перейдем к самому интересному. После установки собранного мной пакета, установки драйверов на наш контроллер, открываем vMix и идём в настройки, вкладка shortcuts, кнопка templates
применяем нашу заготовку и получаем список назначенных и настроенных шорткатов. Не забываем нажать на MIDI Settings, найти там наше устройство и активировать его. Далее идем во вкладку Activators, естественно, нажимаем Enable Device и включаем наш контроллер в списке. Нажимаем на Import и идем в папку C:\Program Files (x86)\vMix\shortcuts\templates где нас ждет уже установленный файл UNICASTER.vMixActivators. Теперь в списке активаторов у нас всё что нужно
Конечно это только пример, как можно назначить кнопки и индикаторы, и, если Вам захотелось, к примеру, сделать общую подсветку не белой, а любым другим цветом, просто открываем активатор default, в разделе type видим UNICASTER и открываем цвета.
Тут можно назначать практически всю палитру в вариантах яркости 50%, 100% и мигающие.
Заключение
Немного о недостатках конструкции: пожалуй, единственный недостаток использования любого MIDI устройства – если в процессе работы он «отрыгнул» (плохой контакт USB или ещё чего), то при переподключении устройства, нужно перезагружать программу vMix. Производитель данного ПО писал на форуме, что это ограничения Windows и сделать с этим они ничего не могли. Но, в версии vMix 24 на базе Windows 10 x64, у меня данная проблема исчезла. Устройство само переподвязывается “на лету”. В остальном устройство проходило тест на uptime в 4 дня, и ни единого сбоя не замечено. В общем чего тут воду в ступе толочь, пользуйтесь своими мозгами правильно!