3 faders/3 pots external NRPN midi controller

Hello.
Here is a little update of my 3 potentiometers and 3 faders NRPN external midi controller for the CQ range mixer.

-it connect to the CQ via USB midi.

-it use 1 arduino pro micro (leonardo)

-3 potentiometers and 3 faders.

-wiring diagram:

-Arduino sketch:

#include <USB-MIDI.h>

#include <EEPROM.h> 




USBMIDI_CREATE_DEFAULT_INSTANCE();




const byte numPots = 6;

const byte potPins[numPots] = {A0, A1, A2, A3, A7, A8};




const int EEPROM_START_ADDRESS = 0; 




int currentPotValue[numPots] = {0, 0, 0, 0, 0, 0};

int lastPotValue[numPots] = {0, 0, 0, 0, 0, 0};

const int tolerance = 2;




const unsigned int DEFAULT_NRPN[numPots] = {8193, 8253, 8192, 8220, 8252, 8222};




unsigned int nrpnParameters[numPots]; 




const byte midiChannel = 1;




// Define the CC numbers used for configuration

const byte CC_CONFIG_MSB_START = 10; // CC 10-15 pour les MSB des 6 pots

const byte CC_CONFIG_LSB_START = 20; // CC 20-25 pour les LSB des 6 pots




void setup() {

  MIDI.begin(midiChannel);

  MIDI.setHandleControlChange(onControlChange);

  // Serial.begin(9600); 




  loadNRPNConfig();

  

  // ✨ NOUVEAU : Envoi de la configuration enregistrée au démarrage

  // Le navigateur doit être prêt à recevoir cette donnée après la connexion.

  sendCurrentConfigToHost(); 

}




void loop() {

  for (byte i = 0; i < numPots; i++) {

    currentPotValue[i] = analogRead(potPins[i]);




    if (abs(currentPotValue[i] - lastPotValue[i]) > tolerance) {

      unsigned int mappedValue = map(currentPotValue[i], 0, 1023, 0, 16383);

      sendNRPNValue(midiChannel, nrpnParameters[i], mappedValue);

      lastPotValue[i] = currentPotValue[i];

    }

  }




  delay(10);

  MIDI.read();

}




// Fonction de rappel appelée lorsque l'Arduino reçoit un message Control Change

void onControlChange(byte channel, byte control, byte value) {

  if (channel == midiChannel) {

    // Vérifie si c'est un message de configuration MSB (CC 10-15)

    if (control >= CC_CONFIG_MSB_START && control < (CC_CONFIG_MSB_START + numPots)) {

      byte potIndex = control - CC_CONFIG_MSB_START;

      nrpnParameters[potIndex] = (nrpnParameters[potIndex] & 0x007F) | ((unsigned int)value << 7);

    }

    // Vérifie si c'est un message de configuration LSB (CC 20-25)

    else if (control >= CC_CONFIG_LSB_START && control < (CC_CONFIG_LSB_START + numPots)) {

      byte potIndex = control - CC_CONFIG_LSB_START;

      nrpnParameters[potIndex] = (nrpnParameters[potIndex] & 0xFF80) | value;

      

      saveNRPNConfig(potIndex, nrpnParameters[potIndex]);

    }

  }

}




// Fonction utilitaire pour envoyer un message NRPN complet

void sendNRPNValue(byte channel, unsigned int parameterNumber, unsigned int value) {

  MIDI.beginNrpn(parameterNumber, channel);

  MIDI.sendNrpnValue(value, channel);

}




// --- Fonctions EEPROM (inchangées) ---




void loadNRPNConfig() {

  unsigned int checkValue = 0xFFFF;

  

  EEPROM.get(EEPROM_START_ADDRESS, checkValue);

  

  if (checkValue == 0xFFFF) {

    for (byte i = 0; i < numPots; i++) {

      nrpnParameters[i] = DEFAULT_NRPN[i];

      saveNRPNConfig(i, nrpnParameters[i]);

    }

    // Serial.println("EEPROM non initialisée. Chargement et écriture des valeurs par défaut.");

  } else {

    for (byte i = 0; i < numPots; i++) {

      int address = EEPROM_START_ADDRESS + (i * sizeof(unsigned int));

      EEPROM.get(address, nrpnParameters[i]);

    }

    // Serial.println("EEPROM chargée avec les valeurs enregistrées.");

  }

}




void saveNRPNConfig(byte potIndex, unsigned int nrpnValue) {

  int address = EEPROM_START_ADDRESS + (potIndex * sizeof(unsigned int));

  EEPROM.put(address, nrpnValue);

}




// ✨ NOUVELLE FONCTION : Envoie la config NRPN actuelle au logiciel hôte (le navigateur)

void sendCurrentConfigToHost() {

  // L'Arduino va s'envoyer ses propres valeurs enregistrées au navigateur.

  for (byte i = 0; i < numPots; i++) {

    unsigned int nrpnValue = nrpnParameters[i];

    

    // Split 14-bit NRPN into MSB and LSB

    const byte msb_param = (nrpnValue >> 7) & 0x7F;

    const byte lsb_param = nrpnValue & 0x7F;

    

    // Use the custom CCs for configuration (CC 10-15 and CC 20-25)

    const byte cc_msb_number = CC_CONFIG_MSB_START + i;

    const byte cc_lsb_number = CC_CONFIG_LSB_START + i;




    // Envoi du MSB

    MIDI.sendControlChange(cc_msb_number, msb_param, midiChannel);

    // Envoi du LSB (déclenchera l'affichage sur l'interface après réception complète)

    MIDI.sendControlChange(cc_lsb_number, lsb_param, midiChannel);

  }

}

-build your own enclosure, I do not provide 3d files directly here but on demand via MP (for free) for this version only:

-once the Arduino sketch is upload, you can program your midi device directly online at this address:
-https://cq-faders.netlify.app/

If the CQ is connected to the same PC while programming, there is a “learn function” to auto_copy the NRPN value of the CQ fader your move.
Otherwise, choose it via the drop/down menu.

With Mac route midi with midipipe, and with IOS route midi with midiMittr.

Enjoy and report/feedback if you like it.

1 Like