CONTROL SURFACE - Change volume of a track with rotary encoder

Hi,
I have 8 rotary encoders connected to an Arduino Pro Micro via an MCP23017 and I am trying to control the volume of DAW tracks using the encoders instead of motorized faders since they are too expensive.
I took a cue from the examples in the CONTROL SURFACE library and wrote the code attached.
Currently, by mapping the Arduino as Mackie Control Universal in Reaper, I can control the track faders but there is a problem.
I create a new project, insert a track and by default it is at 0dB. When I increase the volume with the encoder, the level immediately jumps to -150dB (negative bottom end) and then rises again as I turn the encoder clockwise.
If I turn the encoder counterclockwise the volume decreases correctly.
However, if I change the volume using the daw's fader and then try to vary it again using the encoder, the starting level from which the variation begins is not the level set with the fader but is the old value set with the encoder.
This is probably due to the fact that I am using "absolute" and not relative encoders and so Arduino "remembers" the position of the encoder and uses it as the starting point with each movement.
Is it possible to make the behavior "relative" as in the case of changing the PAN of the tracks?
It is crucial for me that Arduino is mapped as an MCU in the DAW and not with generic MIDI controller.
Thanks! @PieterP


#include <Wire.h>
#include <Control_Surface.h>
#include <AH/Hardware/MCP23017Encoders.hpp>

// Type for the MCP23017 encoders (translates encoder pulses to position)
using WireType = decltype(Wire);     // The type of I²C driver to use
using EncoderPositionType = uint8_t; // The type for saving encoder positions
using MCPEncoderType = MCP23017Encoders<WireType, EncoderPositionType>;

// Type for the MIDI encoders (translates position to MIDI messages)
struct PBMCPEncoder : GenericMIDIAbsoluteEncoder<MCPEncoderType::MCP23017Encoder, PitchBendSender<14>> {
  PBMCPEncoder(MCPEncoderType::MCP23017Encoder enc, MIDIAddress address,
               int16_t multiplier = 512, uint8_t pulsesPerStep = 4)
    : GenericMIDIAbsoluteEncoder(std::move(enc), address, multiplier,
                               pulsesPerStep, {}) {}
};
/*
// Type for the MIDI encoders (translates position to MIDI messages)
struct PBMCPEncoder : GenericMIDIAbsoluteEncoder<MCPEncoderType::MCP23017Encoder,
  PitchBendSender<14>> {
  PBMCPEncoder(MCPEncoderType::MCP23017Encoder enc, MIDIAddress address,
               int16_t multiplier = 512, uint8_t pulsesPerStep = 4)
    : GenericMIDIAbsoluteEncoder(std::move(enc), address, multiplier,
                                 pulsesPerStep, {}) {}
};
*/
USBMIDI_Interface midi;
//USBDebugMIDI_Interface midi;

// Create an object that manages the 8 encoders connected to the MCP23017.
MCPEncoderType encVOL {Wire, 0x27, 4};
//                  │     │    └─ Interrupt pin
//                  │     └────── Address offset
//                  └──────────── I²C interface

// Instantiate 8 MIDI rotary encoders.
PBMCPEncoder pbencodersVOL[] {
  { encVOL[0], MCU::VOLUME_1},
  { encVOL[1], MCU::VOLUME_2},
  { encVOL[2], MCU::VOLUME_3},
  { encVOL[3], MCU::VOLUME_4},
  { encVOL[4], MCU::VOLUME_5},
  { encVOL[5], MCU::VOLUME_6},
  { encVOL[6], MCU::VOLUME_7},
  { encVOL[7], MCU::VOLUME_8}
};

void setup() {
 // RelativeCCSender::setMode(relativeCCmode::MACKIE_CONTROL_RELATIVE);
  Control_Surface.begin();
  Wire.begin(); // Must be called before enc.begin()
  Wire.setClock(800000);
  encVOL.begin(); // Initialize the MCP23017
}

void loop() {
  Control_Surface.loop();
  encVOL.update();
}

You can use the PBValue class to receive the position sent by the DAW. You can then update the "position" of the encoder using PBAbsoluteEncoder::setValue().

You'll probably want to have some kind of delay, e.g. only update the position if you haven't touched the encoder for a couple of milliseconds (but use the millis() function, not delay()).

This looks familiar, and the offered solution a demonstration as to how far into the desert one can wander in ignorance of available means to accomplish a simple goal.

For your amusement

I always suspected it could not have been the first time the problem came up.

Not to worry, I had fun with it and went a bit further after the thread was out of gas.

a7

@PieterP Hello,
I have been trying for quite some time to implement the solution you suggested but have not been able to do it. I am not a good programmer, I am a novice.
Could you give me an example on how to modify my code to implement the solution you suggest?
Thank you!

You could try something like this:

#include <Control_Surface.h>

USBDebugMIDI_Interface midi;
// Sends Pitch Bend messages
PBAbsoluteEncoder enc {{2, 3}, Channel_1, 128};
// Receives Pitch Bend messages
PBValue enc_value {Channel_1};

void setup() {
  Control_Surface.begin();
}

void loop() {
  static auto prev_position = enc.getValue();
  static constexpr decltype(millis()) timeout = 1000;
  static decltype(millis()) prev_enc_move_time = -timeout;

  Control_Surface.loop();

  auto new_position = enc.getValue();
  auto incoming_value = enc_value.getValue();
  bool dirty = incoming_value != new_position;
  // Keep track of when the user last moved the encoder.
  if (new_position != prev_position) {
    prev_position = new_position;
    prev_enc_move_time = millis();
  }
  // If the value last sent by the DAW is different from our local
  // value, and if the user hasn't moved the encoder for a while,
  // update our local encoder position value.
  // Note: this assumes that the DAW always echoes back the value
  //       it received.
  else if (dirty && millis() - prev_enc_move_time >= timeout) {
    enc.setValue(incoming_value);
    enc.forcedUpdate(); // send the new value to the DAW (optional)
  }
}

(This code uses a debug MIDI interface for prototyping, replace it with an actual USBMIDI_Interface to test it with your DAW.)

@PieterP
Thank you very much,
the code works perfectly with my DAW (Reaper 7).
I tried to adapt it to work with 8 encoders connected to an MCP23017 but it doesn't seem to work.
I probably wrote some nonsense!
Where am I going wrong?
Here is my code:

#include <Wire.h>
#include <Control_Surface.h>
#include <AH/Hardware/MCP23017Encoders.hpp>

// Type for the MCP23017 encoders (translates encoder pulses to position)
using WireType = decltype(Wire);     // The type of I²C driver to use
using EncoderPositionType = uint8_t; // The type for saving encoder positions
using MCPEncoderType = MCP23017Encoders<WireType, EncoderPositionType>;

// Type for the MIDI encoders (translates position to MIDI messages)
struct PBMCPEncoder : GenericMIDIAbsoluteEncoder<MCPEncoderType::MCP23017Encoder, PitchBendSender<14>> {
  PBMCPEncoder(MCPEncoderType::MCP23017Encoder enc, MIDIAddress address, int16_t multiplier = 600, uint8_t pulsesPerStep = 4)
    : GenericMIDIAbsoluteEncoder(std::move(enc), address, multiplier, pulsesPerStep, {}) {}
};

USBMIDI_Interface midi;
//USBDebugMIDI_Interface midi;

// Create an object that manages the 8 encoders connected to the MCP23017.
MCPEncoderType encV {Wire, 0x27, 4};
//                  │     │    └─ Interrupt pin
//                  │     └────── Address offset
//                  └──────────── I²C interface

// Sends Pitch Bend messages
PBMCPEncoder pbencodersVOL[] {
  {encV[0], MCU::VOLUME_1},
  {encV[1], MCU::VOLUME_2},
  {encV[2], MCU::VOLUME_3},
  {encV[3], MCU::VOLUME_4},
  {encV[4], MCU::VOLUME_5},
  {encV[5], MCU::VOLUME_6},
  {encV[6], MCU::VOLUME_7},
  {encV[7], MCU::VOLUME_8}
};

// Receives Pitch Bend messages
//Create an array to store pb values
int volumeValues[8];


// Crea un oggetto PBValue per ogni canale
PBValue volumePBValues[8] = {
  {Channel_1},
  {Channel_2},
  {Channel_3},
  {Channel_4},
  {Channel_5},
  {Channel_6},
  {Channel_7},
  {Channel_8},
};

/*PBValue encV1 {Channel_1};
PBValue encV2 {Channel_2};
PBValue encV3 {Channel_3};
PBValue encV4 {Channel_4};
PBValue encV5 {Channel_5};
PBValue encV6 {Channel_6};
PBValue encV7 {Channel_7};
PBValue encV8 {Channel_8};
*/

void setup() {
  Control_Surface.begin();
  Wire.begin(); // Must be called before enc.begin()
  Wire.setClock(800000);
  encV.begin(); // Initialize the MCP23017
}

void loop() {
  // Save the previous positions
  static EncoderPositionType prev_position[8] {};
  // Define a delay
  static constexpr decltype(millis()) timeout = 1000;
  static decltype(millis()) prev_enc_move_time = -timeout;

  Control_Surface.loop();

  // Save the new positions  
  static EncoderPositionType new_position[8] {};

  for (uint8_t i = 0; i < 8; ++i) {
     auto incoming_value = volumePBValues[i].getValue();
     bool dirty = incoming_value != new_position[i];
     // Keep track of when the user last moved the encoder.
       if (new_position != prev_position) {
         prev_position[i] = new_position[i];
         prev_enc_move_time = millis();
        }
       // If the value last sent by the DAW is different from our local
       // value, and if the user hasn't moved the encoder for a while,
       // update our local encoder position value.
       // Note: this assumes that the DAW always echoes back the value
       //       it received.
       else if (dirty && millis() - prev_enc_move_time >= timeout) {
         pbencodersVOL[i].setValue(incoming_value);
         pbencodersVOL[i].forcedUpdate(); // send the new value to the DAW (optional)
      }

  }
  encV.update();
}

@PieterP
Hi,
I solved the problem with this code derived from the suggestion you gave me last time. My old code didn't make sense but I tried to study a little better and got some results.
The code seems to work well: with Reaper I can control the volume of 8 tracks at a time and if I move the faders from the DAW the position is correctly associated with the encoders.
Can you recommend any optimizations to make the code better?

Thanks!

Translated with DeepL.com (free version)

#include <Wire.h>
#include <Control_Surface.h>
#include <AH/Hardware/MCP23017Encoders.hpp>

// Type for the MCP23017 encoders (translates encoder pulses to position)
using WireType = decltype(Wire);     // The type of I²C driver to use
using EncoderPositionType = uint8_t; // The type for saving encoder positions
using MCPEncoderType = MCP23017Encoders<WireType, EncoderPositionType>;
using uint16 = uint16_t;

// Type for the MIDI encoders (translates position to MIDI messages)
struct PBMCPEncoder : GenericMIDIAbsoluteEncoder<MCPEncoderType::MCP23017Encoder, PitchBendSender<14>> {
  PBMCPEncoder(MCPEncoderType::MCP23017Encoder enc, MIDIAddress address, int16_t multiplier = 600, uint8_t pulsesPerStep = 4)
    : GenericMIDIAbsoluteEncoder(std::move(enc), address, multiplier, pulsesPerStep, {}) {}
};

USBMIDI_Interface midi;
//USBDebugMIDI_Interface midi;

// Create an object that manages the 8 encoders connected to the MCP23017.
MCPEncoderType encV {Wire, 0x27, 4};
//                    │     │    └─ Interrupt pin
//                    │     └────── Address offset
//                    └──────────── I²C interface

// Sends Pitch Bend messages
PBMCPEncoder pbencodersVOL[] {
  {encV[0], MCU::VOLUME_1},
  {encV[1], MCU::VOLUME_2},
  {encV[2], MCU::VOLUME_3},
  {encV[3], MCU::VOLUME_4},
  {encV[4], MCU::VOLUME_5},
  {encV[5], MCU::VOLUME_6},
  {encV[6], MCU::VOLUME_7},
  {encV[7], MCU::VOLUME_8},
};

// Create objects that receives Pitch Bend messages from DAW channels
PBValue enc_values[] = {
  {MCU::VOLUME_1},
  {MCU::VOLUME_2},
  {MCU::VOLUME_3},
  {MCU::VOLUME_4},
  {MCU::VOLUME_5},
  {MCU::VOLUME_6},
  {MCU::VOLUME_7},
  {MCU::VOLUME_8}
};

// Create some array to store encoder's position
  uint16 prev_positions [8];
  uint16 incoming_values [8];
  uint16 new_positions [8];


void setup() {
  Control_Surface.begin();
  Wire.begin(); // Must be called before enc.begin()
  Wire.setClock(800000);
  encV.begin(); // Initialize the MCP23017
}

void loop() {
  for (int i = 0; i < 8; i++) {
    prev_positions[i] = pbencodersVOL[i].getValue();
  }
    static constexpr decltype(millis()) timeout = 1000;
    static decltype(millis()) prev_enc_move_time = -timeout;
  
    Control_Surface.loop();
  
  for (int i = 0; i < 8; i++) {
    new_positions[i] = pbencodersVOL[i].getValue();
    incoming_values[i] = enc_values[i].getValue();
    bool dirty = incoming_values[i] != new_positions[i];  
    // Keep track of when the user last moved the encoder.

    if (new_positions[i] != prev_positions[i]) {
      prev_positions[i] = new_positions[i];
      prev_enc_move_time = millis();
    }

    // If the value last sent by the DAW is different from our local
    // value, and if the user hasn't moved the encoder for a while,
    // update our local encoder position value.
    // Note: this assumes that the DAW always echoes back the value
    //       it received.
    else if (dirty && millis() - prev_enc_move_time >= timeout) {
      pbencodersVOL[i].setValue(incoming_values[i]);
      pbencodersVOL[i].forcedUpdate(); // send the new value to the DAW (optional)
    }
    encV.update(); 
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.