MIDI клавиатура на базе Roland K-25M

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Собственно, не так давно обнаружил устройство, которое может представлять некоторый интерес для DIY - клавиатура K-25M от Roland.

Производителем она позиционируется как составная часть набора модульных синтезаторов Boutique. Т.е. имеется около дюжины разных моделей синтезаторов, которые могут использоваться либо отдельно (как "мозги"), либо быть вставленными в эту клавиатуру.

Ссылку не привожу, дабы не сочли за рекламу, но лично я заказывал в Музторге курьером.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Сама по себе клавиатура является исключительно пассивной и содержит лишь группы контактов (по две группы на клавишу), объединенные в матрицу 8х8 и защищенные диодами. Но сам производитель схемы не приводит, так что любители DIY вынуждены восстанавливать схему методом прозвонки.

Собственно, две группы нужны для того, чтобы по разнице времени их срабатывания вычислять параметр velocity, т.е. громкость ноты.

Естественно, сначала попытался поискать что-нибудь по поводу указанной клавиатуры в контексте DIY. Иногда попадаются весьма любопытные варианты: https://www.youtube.com/watch?v=8PREjlg1tss

На Гитхабе нашлось пару статей. В одной из них автор сразу указывал, что динамика (т.е. вычисление velocity) не поддерживается. Другую просмотрел более внимательно, но у меня сразу создалось впечатление, что скетч неработоспособен. Сборка конструкции подтвердила мои предположения. Точнее, он мог бы быть работоспособен, если бы подтянуть входы матрицы клавиатуры к земле. Но автор ни о каких внешних элементах не написал. 

В общем, ознакомившись с тем, что есть, пришел к выводу, что проще всего писать самому с нуля.

Пока публикую самый первый работоспособный прототип:

- диапазон фиксированный: от До малой до До второй,

- Active Sensing не поддерживается,

- Running Status не поддерживается.

Дальше предполагается добавить кнопки для перемещения строя по октавам и реализовать упомянутые Active Sensing и Running Status.

Для начала. Потом, возможно и еще какие органы управления, а также возможность настройки внешнего синтезатора в специальном режиме клавиатуры.

Но самое главное, как выяснилось, для клавиатуры подобного типа вполне хватает контроллеров AVR без каких-бы то ни было дополнительных внешних микросхем.

// MIDI клавиатура на базе Roland K-25M и Arduino

#define DEBUG false

#define COL0A 2 // 
#define COL0B 3 // пины колонок матрицы, с которых снимают сигнал:
#define COL1A 4 //  А - срабатывание верхней контактной группы клавиши
#define COL1B 5 //  В - срабатывание нижней контактной группы клавиши
#define COL2A 6 // по разнице времени срабатывания вычисляется скорость 
#define COL2B 7 // движения клавиши - velocity
#define COL3A 8 //    по сути эти константы используются только при инициализации, 
#define COL3B 9 // обращение к пинам происходит непосредственно через порты

#define ROW0 A5 // ряды матрицы
#define ROW1 12 // на которые при сканировании
#define ROW2 A4 // подается мпульс низкого напряжения
#define ROW3 11
#define ROW4 A3
#define ROW5 10
#define ROW6 A2
#define ROW7 A1 //

#define numCols  4
#define numRows  8

const byte colA[] = {COL0A, COL1A, COL2A, COL3A};
const byte colB[] = {COL0B, COL1B, COL2B, COL3B};

const byte rows[] = {ROW0, ROW1, ROW2, ROW3, ROW4, ROW5, ROW6, ROW7};

const int size_revTime = 320;

const int revTime[] = {
127, 127, 127, 127, 125, 122, 120, 118, 115, 113, 111, 109, 107, 105, 103, 101,   //  0 - 12 ms
99, 97, 95, 93, 91, 90, 88, 86, 85, 83, 82, 80, 79, 77, 76, 74,   //  12.8 - 24.8 ms
73, 72, 70, 69, 68, 67, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56,   //  25.6 - 37.6 ms
55, 54, 53, 52, 51, 50, 49, 48, 48, 47, 46, 45, 45, 44, 43, 42,   //  38.4 - 50.4 ms
42, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 33,   //  51.2 - 63.2 ms
33, 32, 32, 31, 31, 30, 30, 30, 29, 29, 28, 28, 28, 27, 27, 27,   //  64 - 76 ms
26, 26, 26, 25, 25, 25, 24, 24, 24, 24, 23, 23, 23, 23, 22, 22,   //  76.8 - 88.8 ms
22, 22, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 19, 19, 19, 19,   //  89.6 - 101.6 ms
19, 19, 18, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 17, 17, 17,   //  102.4 - 114.4 ms
17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 15,   //  115.2 - 127.2 ms
15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14,   //  128 - 140 ms
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13,   //  140.8 - 152.8 ms
13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,   //  153.6 - 165.6 ms
13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,   //  166.4 - 178.4 ms
13, 13, 13, 13, 13, 13, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,   //  179.2 - 191.2 ms
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,   //  192 - 204 ms
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,   //  204.8 - 216.8 ms
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,   //  217.6 - 229.6 ms
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,   //  230.4 - 242.4 ms
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1};   //  243.2 - 255.2 ms


struct event { // структура, описывающая состояние клавиши
  unsigned int Apr = 0; // время срабатывания контактной группы А
  unsigned int Bpr = 0; // время срабатывания контактной группы В
  bool active = false;  // клавиша отмечена как нажатая
  byte key;             // номер клавиши
};

struct event Ev[numCols][numRows]; // массив клавиш - всего рассчитано на 32
int numNotes = 0; 
unsigned int timer2count = 0; // это у нас основной счетчик времени (в 800 мкс интервалах)

void setup() {
  for(int i=0; i < numCols; i++){
    pinMode(colA[i], INPUT_PULLUP);
    pinMode(colB[i], INPUT_PULLUP);
    for(int j=0; j<numRows; j++){
      Ev[i][j].key = (i * numRows) + j;
    }
  }
  for(int j=0; j<numRows; j++){
    pinMode(rows[j], OUTPUT);
    digitalWrite(rows[j], HIGH);
  }
  if(DEBUG) {
    Serial.begin(115200);
    while(!Serial);
    Serial.println("Roland K25-M Example");
  } else {
    Serial.begin(31250);
  }
  pinMode(LED_BUILTIN, OUTPUT);

  TCCR2A = 0;
  TCCR2B = 0;
  TCNT2  = 0; // обнуляем счетчик таймера
  OCR2A = 199;// = (16*10^6) / 64 / (199+1)  == 1250 Hz (800ms)
  TCCR2A |= (1 << WGM21);    // CTC mode
  TIMSK2 |= (1 << OCIE2A);   // разрешаем прерывание по сравнению
  TCCR2B = 4; //(1 << CS22);  // прескалер 64
}

ISR(TIMER2_COMPA_vect){ // разрешение millis мало, а micros - велико, поэтому мы вводим собственные часы, тикающие 1250 раз в секунду
  timer2count++;
}

void inline checkLED() { // обычный blink without delay - показывает, что контроллер работает, не завис
  static int ledState = LOW;
  static unsigned long previousMillis = 0;
  const long interval = 250;
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    digitalWrite(LED_BUILTIN, ledState = !ledState);
  }  
}

void inline checkContacts() {
  unsigned int counter = timer2count; //millis();
  for(int j=0; j < numRows; j++) { // перебираем 8 групп по 4 клавиши
    digitalWrite(rows[j], LOW);
    delayMicroseconds(2);
    bool trig;
    trig = !(PIND & 0x04);    // c1-g1
    if(trig && !Ev[0][j].Apr) {        Ev[0][j].Apr = counter;      } else if(!trig) Ev[0][j].Apr = 0; // эта группа замыкается первой
    trig = !(PIND & 0x08);
    if(trig && !Ev[0][j].Bpr) {        Ev[0][j].Bpr = counter;      } else if(!trig) Ev[0][j].Bpr = 0; // а эта - второй

    trig = !(PIND & 0x10);    // g#1-d#2
    if(trig && !Ev[1][j].Apr) {        Ev[1][j].Apr = counter;      } else if(!trig) Ev[1][j].Apr = 0;
    trig = !(PIND & 0x20);
    if(trig && !Ev[1][j].Bpr) {        Ev[1][j].Bpr = counter;      } else if(!trig) Ev[1][j].Bpr = 0;

    trig = !(PIND & 0x40);    // e2-h2
    if(trig && !Ev[2][j].Apr) {        Ev[2][j].Apr = counter;      } else if(!trig) Ev[2][j].Apr = 0;
    trig = !(PIND & 0x80);
    if(trig && !Ev[2][j].Bpr) {        Ev[2][j].Bpr = counter;      } else if(!trig) Ev[2][j].Bpr = 0;

    trig = !(PINB & 0x01);    // c3 only
    if(trig && !Ev[3][j].Apr) {        Ev[3][j].Apr = counter;      } else if(!trig) Ev[3][j].Apr = 0;
    trig = !(PINB & 0x02);
    if(trig && !Ev[3][j].Bpr) {        Ev[3][j].Bpr = counter;      } else if(!trig) Ev[3][j].Bpr = 0;

    digitalWrite(rows[j], HIGH);
  }
}

void inline checkEvents() {
  for(int i=0; i<numCols; i++){
    for(int j=0; j<numRows; j++){
      struct event *e = &Ev[i][j];
      if(e->Apr && e->Bpr && !e->active){ // обе контактные группы сработали, а нота еще не отмечена как включенная
        e->active = true;                 // помечаем ноту включенной
        int vel = e->Bpr - e->Apr;                       // 
        if(vel >= size_revTime) vel = size_revTime - 1;  // рассчитываем velocity ноты
        vel = revTime[vel];                              // 
        if(DEBUG) {
          Serial.print(e->key);
          Serial.print(" on ");
          Serial.println(vel);
        } else {
          Serial.write(0x95);
          Serial.write(e->key + 48);
          Serial.write(vel & 0x7F);
        }
        numNotes++;
      }
      else if(!e->Apr && e->active){ // нота помечена включенной, а верхняя контактная группа уже разомкнута
        e->active = false;           // помечаем ноту выключенной
        if(DEBUG) {
          Serial.print(e->key);
          Serial.println(" off");
        } else {
          Serial.write(0x95);
          Serial.write(e->key + 48);
          Serial.write(0);
        }
      }
    }
  }
}

uint16_t stat[256]; // для отладки - статистика по времени цикла обработки нот

void loop() {
  unsigned long t0 = micros();
  checkLED();
  unsigned long t1 = micros();
  checkContacts();
  unsigned long t2 = micros();
  checkEvents();
  if(DEBUG) {
    unsigned long t3 = micros();
    unsigned int d23 = (t3-t1)/4;
    if(d23 > 255) d23 = 255;
    if(stat[d23] < 65535) stat[d23]++;
    if(Serial.available()) {
      Serial.read();
      for(int i = 0; i < 256; i++) {
        if(stat[i]) {
          Serial.print(i*4);
          Serial.print('\t');
          Serial.println(stat[i]);
        }
      }
      Serial.print("nn: ");
      Serial.print(numNotes);
      Serial.print(", ms: ");
      Serial.print(millis());
      Serial.print(", counter: ");
      Serial.println(timer2count);
    }
  }
} 

 

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Появились некоторые идеи, какие функции должен обеспечивать инструмент и вытекающая из этих функций схемотехника.

 

Музыканту доступны 25 чувствительных с динамике клавиш, колеса тона и модуляции, а также слайдер Data, 8 кнопок, подсвеченных светодиодами и два гнезда для ножных педалей.

4 из 8 кнопок обеспечивают сдвиг строя инструмента: 

- на две октавы вниз,

- на одну октаву вниз, 

- на одну октаву вверх,

- на две октавы вверх,

таким образом можно сдвигать строй инструмента до 3 октав в каждую сторону, перекрывая весь музыкальный диапазон от До субконтроктавы до До пятой.

Остальные 4 кнопки включают управляющие режимы, в которых музыкальные клавиши используются для ввода значений. Управляющие режимы позволяют как изменять внутренние настройки самого инструмента (изменение кривой чувствительности, закрепление за слайдером определенного MIDI контроллера, управление режимами Active Sensing и Running Status, назначение номера MIDI канала), а также выдачу наружу некоторых MIDI команд (переключение банков и инструментов).

PS. Исправил на схеме ошибки и изменил тип контроллера (с Mini на Nano).