Midi Controller for Darktable

Here’s an unusual midi controller idea.

Quick update:

I played a bit with MIDIUSB, a rotary encoder and a linear fader and so far everything is working nicely together.

Not so quick details ahead for those who want to play with that, too.

I didn’t have time for an Advent calendar but let’s fill the time with some small pieces of information. Today I would like to give a brief introduction about the two kinds of input hardware that I used, the rotary encoder and the fader, and how to make use of them.

A fader is basically a linear potentiometer, that means, it’s a variable voltage divider. Details can be found on Wikipedia. For usage with an Arduino a 10k fader works nicely. Of course, a regular potentiometer works just as well, it doesn’t look as fancy though. :slight_smile:

The rotary encoder is a little more complicated. While the fader is an analog device, the encoder is working digitally. To the Arduino it looks like two buttons. When turning the shaft only one of the two changes its state at the same time. That way it’s possible to determine the direction of the rotation, and by counting you can also keep track of the position. Still, it’s a relative measurement and not absolute as the potentiometer. The nice thing about that is that you can turn more than 360° and keep going. You also don’t have to care about the initial position.

Connecting those to an Arduino is simple with some jumper cables and a breadboard.

The colors in the photo are really bad (and the soldering of the header pins is much better than the image might make you think :wink:).
The two orange ones are connecting to GND to the - lane and VCC to the + lane. The encoder is connected to +with its middle pin, the other two are on A2 and A3 of the Arduino. If it turns the wrong way later just flip those two. The fader is connected to - with the black wire, + with the white one and A1 with the grey cable. That’s all.

Ignore the resistor please, that’s for connecting a WS2812B LED stripe. I will show how that works and how to control them with MIDI messages from the computer in another post.

On the Arduino I am running this code:

#include "MIDIUSB.h"

#define ENCODER_PIN_A A3
#define ENCODER_PIN_B A2
#define NUMBER_CLICKS 30 // the number of clicks per rotation of the encoder

#define SLIDER_PIN A1
#define SLIDER_MAX 1015 // my fader has a tiny resistance when completely open

unsigned long loopTime;
uint8_t encoder_state_prev;
uint8_t encoder_pos_prev = 0;
uint8_t slider_7bit_prev = 0;

static inline void midi_control(uint8_t channel, uint8_t control, uint8_t value)
{
  midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
  MidiUSB.sendMIDI(event);
}

void setup()
{
  // we are using internal pullup resistors to make external wiring simpler
  pinMode(ENCODER_PIN_A, INPUT_PULLUP);
  pinMode(ENCODER_PIN_B, INPUT_PULLUP);

  uint8_t encoder_A = digitalRead(ENCODER_PIN_A);
  uint8_t encoder_B = digitalRead(ENCODER_PIN_B);
  encoder_state_prev = (encoder_A << 1) | encoder_B;

  loopTime = millis();
}

void loop()
{
  int8_t encoder_pos = encoder_pos_prev;

  unsigned long currentTime = millis();
  if(currentTime >= (loopTime + 5)) // 5ms since last check of encoder/fader = 200Hz
  {
    uint8_t midi_sent = 0; // only call MidiUSB.flush() when needed

    // the rotary encoder
    // blending everything together into this state variable might seem like overkill for now, but once we
    // use the GPIO extender and want to check a bunch of inputs this might be handy.
    const uint8_t encoder_A = digitalRead(ENCODER_PIN_A);
    const uint8_t encoder_B = digitalRead(ENCODER_PIN_B);
    const uint8_t state = (encoder_A << 1) | encoder_B;
    if(state != encoder_state_prev)
    {
      switch(state | (encoder_state_prev << 2))
      {
        // some states can't happen as only one input can change at a time.
        // some are not interesting with an encoder that has twice as many steps as clicks.
        //
        // old_A  old_B  A  B    case
        // 0      0      0  0    0        can't happen
        // 0      0      0  1    1        not interesting - inbetween turning left
        // 0      0      1  0    2        not interesting - inbetween turning right
        // 0      0      1  1    3        can't happen
        // 0      1      0  0    4        turning right
        // 0      1      0  1    5        can't happen
        // 0      1      1  0    6        can't happen
        // 0      1      1  1    7        turning left
        // 1      0      0  0    8        turning left
        // 1      0      0  1    9        can't happen
        // 1      0      1  0    10       can't happen
        // 1      0      1  1    11       turning right
        // 1      1      0  0    12       can't happen
        // 1      1      0  1    13       not interesting - inbetween turning right
        // 1      1      1  0    14       not interesting - inbetween turning left
        // 1      1      1  1    15       can't happen

        // 0 1 0 0
        // 1 0 1 1
        case 4:
        case 11:
          midi_control(0x00, 0x01, 0x7f); // send an indication of CW rotation
          midi_sent = 1;
          encoder_pos = (encoder_pos + 1) % NUMBER_CLICKS;
          break;

        // 0 1 1 1
        // 1 0 0 0
        case 7:
        case 8:
          midi_control(0x00, 0x01, 0x00); // send an indication of CCW rotation
          midi_sent = 1;
          encoder_pos = (encoder_pos - 1 + NUMBER_CLICKS) % NUMBER_CLICKS;
          break;

        default:  break;
      }

      if(encoder_pos != encoder_pos_prev)
      {
        midi_control(0x00, 0x02, encoder_pos); // send the current position
        midi_sent = 1;
        encoder_pos_prev = encoder_pos;
      }
      encoder_state_prev = state;
    }

    // the fader
    const unsigned short slider_raw = analogRead(SLIDER_PIN);
    const unsigned short slider = map(slider_raw, 0, SLIDER_MAX, 0, 127);
    const uint8_t slider_7bit = (uint8_t)constrain(slider, 0, 127);
    if(slider_7bit != slider_7bit_prev)
    {
      midi_control(0x00, 0x03, slider_7bit); // send the current position
      midi_sent = 1;
      slider_7bit_prev = slider_7bit;
    }

    if(midi_sent) MidiUSB.flush();

    loopTime = currentTime;
  }
}

This code will check for input changes every 5ms. That way we can avoid explicit debouncing of the encoder.

When connected to the PC we should see a new MIDI endpoint:

> amidi -l
Dir Device    Name
IO  hw:1,0,0  SparkFun Pro Micro MIDI 1

Which we can listen on:

> amidi -d -p hw:1,0,0

B0 01 7F
B0 02 01
B0 01 7F
B0 02 02
B0 01 7F
B0 02 03

This is me turning the encoder three clicks clockwise. For every click two MIDI control events (indicated by the B in the first byte) are sent on channel 0 (indicated by the 0 in B0): One from controller 1 (01 as the 2nd byte) with value 127 (7F) telling us that the rotation was clockwise, and once from controller 2 (02 as the 2nd byte) with the current position of the encoder. It starts with 0, so the first click brings it to 01, then 02 and the third is reporting 03.
More on the format of MIDI messages can be found here.

And that’s it. The next steps for the hardware is adding more encoders, buttons and – should they arrive some day – the I2C GPIO extenders to connect even more things. Once they are added we will no longer read digital pins manually but get them as bitsfields from I2C. I will show how that works once I have it implemented.

What’s left now is changing darktable’s code to make it easy to trigger callbacks from such MIDI events. The code to receive them is already done (and quite boring).

3 Likes

Not sure if it will be helpful, but I have a project on github for an open source midi wind controller using the Teensy 3.0, which is Arduino compatible for the most part. Some of that code might be useful for you…

I’ve pointed the formatting issue out on meta.discourse.org:

Cool, I got infected and searching for the parts now.

  • Arduino micro is cheap - thats nice
  • Wires, breadboard okay

But:

This puzzles me a bit. They are not so cheap, depending where i’m looking. Is yours something like a “ALPS STEC11B04” encoder?

  • Faders okay, but for which modules in darktable they will be helpfull?
    I guess the position would be synced with the assigned slider.

About the parts I used:

  • I ordered the Arduino Pro Micro from China (via Ebay). Dirt cheap, but takes a while to arrive.
  • Wires and breadboard are cheap, too, and re-usable, so having those around is never a bad idea.
  • The fader shown is this one. It would be mapped to a slider in darktable. However, given that the initial position when entering darkroom, switching to another image, resetting a module, … wouldn’t match I will probably not use those in the final design. I just wanted to play with them to get a feel for it.
  • The encoder is indeed the one you linked. In the final design I will however use this one as I want to mount it on the PCB and not screw it in. And it features an extra button when pressing the shaft down. I just didn’t use it in the photo shown because I didn’t want to confuse with the extra pins from the button.

I STILL haven’t received my MCP23018 which I requested samples for. Reichelt doesn’t have them so I will probably buy some when being near a Conrad the next time.

About the encoder being complicated, I was mostly referring to reading them in the code, not in wiring them up or buying them. :slight_smile:

I’m also trying to control dt with encoders, my initial aim was to use midi and the Behringer X-Touch Mini, more specific, something like this.

I first checked issue #10919 some months ago and tried the recommend lua approach, but didn’t find a better way to use lua then to call a keyboard short-cut, so decided skip the lua part for now and use an arduino as as USB keyboard and defining the short-cuts in dt,

I have this proto working with some funky knobs.

The five encoders are working not by increasing the values, but sending an edit value short-cut, then the value and enter. This allow you to use an encoder library that has acceleration so you can jump your exposure from 0 to 1 fast, but still have 0.01 precision. I’m also using a single click to enable/disable the module, double click to show it and a long press to reset it.

I’ve set it for Exposure, Contrast, Saturation, Shadows and Highlights and nice to use and test, but now I really need lua, as I need a way to send the current values to the arduino, to use it for WB temperature or more important when you change images or edit previously edited pics.

I was hoping to find some sort of trigger on the API for when a value was changed, exposure for example, but was unable to do it, can any one help me with that?

Speaking of help, the arduino leonardo and pro micro share the same ATmega32U4 processor so I can share what have so far if you find it useful to test (just need to clean it a bit)

I still find that midi is the way to go, because from my experience it’s much easier to build a prototype that works, than to make it practical and reliable. You can get a Behringer for 60 bucks that does that from the start.

As this is my first (long) post, I’m a software engineer by trade, and amateur musician (with midi experience)/photographer/electronics eng just to do different more inspired stuff.

3 Likes

Hi @danielblues and welcome! I’d suggest you coordinate with @houz on writing code, I thought he mentioned he had some midi stuff going. I’m sure he slowed down a bit on this since darktable just released a major version recently.

Hi @danielblues I have a bcf2000 by behringer so I hope your work and @houz fleshes out. Thanks to both of you’s for all your work

I always wish to do a control of midi with reactivision

https://www.flickr.com/photos/el_bazza/5027117557/in/album-72157624590555210/

My Version was a so much imprecise for are litle and low quality

https://www.flickr.com/photos/el_bazza/5009346196/in/dateposted-public/

Just as a small update: I never got the samples I ordered (even though I got a shipping confirmation) so I didn’t proceed with this. Once I have a little more free time again I will go and buy what I need and get back to it.

1 Like

I believe it would be so much faster to edit photos with the 32 knobs of my Behringer controller. I do a live graphics set with it, and it’s really amazing how tweaking visuals live becomes something physical, where you can use your muscle memory. Much more direct than mouse/keyboard interaction, because you can turn and feel knobs while you keep your eyes on the result, no need to look at a gui.

Anything we can do to make this happen? :slight_smile:

3 Likes

Any progress on that side … It would be SOOOOOOO awesome

1 Like

By thw way … is anybody aware of that project?

3 Likes

and anotherone which can (as I understand it) simulate keystrokes on midi Samuel Nicholas (Enetheru) / midi2input · GitLab

Hi @houz, I am interested in collaborating with Midi support. How can we coordinate so I can help with the effort already done?

2 Likes

@houz is at the moment here and in the darktable mailing list in hibernation mode, but you can still catch him in the darktable or gimp irc channels.

any news on this subject, i think would be a great addition a way how to work with that!

There is a proof of concept implementation of a stand-alone module that just provides a mapping of midi controller knobs to darkroom sliders on [WIP][RFC] Add support for MIDI controllers mapping to sliders by dterrahe · Pull Request #3722 · darktable-org/darktable · GitHub

As is clear from the discussion, this is highly unlikely to be merged into darktable, because it should really be properly integrated in the input layer, which should be made more configurable for that (and other) purpose(s). That’s why I made no attempt at all yet to map note keys to commands, but it is possible to achieve this using external programs.

For Windows, there is a stand-alone dll for version 3.0.1 and same additional configuration for behringer x-touch mini.

1 Like

I’ll contribute to a loupedeck fund for you if it means there would be support for it in the future. I got one the other day and I absolutely love it, but unfortunately I had to switch to Lightroom classic in order to use it