Volume control with Arduino Leonardo

 

The Arduino Leonardo is an interesting thing. Unlike the Uno, which has an ATmega328P microcontroller, it uses the slightly different 32U4, key features of which include a much higher pin count, four interrupt-capable lines rather than two, and integrated USB support. That last one is a biggie – unlike the 328, which requires a separate USB-to-serial adapter chip (like a Prolific PL2303, the slightly crummy WinChipHead CH340G or the slightly controversial FTDI FT232), the 32U4 is and of itself a USB 2.0 device. This means it can be pretty much any kind of USB device you can program it to be – and Arduino includes Keyboard and Mouse libraries that let you very easily make a Leonardo pretend to be, well, one or both of those things.

So I did.

 

2014-11-15 10.53.22

 

This is a Freetronics Leostick, a Leonardo in Arduino Micro form factor (here’s an equivalent Sparkfun product). Unlike other Micros, it has a full USB plug on one end, so you can plug one straight into your computer’s USB port like a thumbdrive and work with it like that, if you like.

It is also, as you can probably guess from looking at it, one of the first things I ever took a soldering iron to. Shut up. You were a beginner once too.

 

2014-11-14 17.00.33

 

For this little project, my Leostick is tethered to my PC by a basic USB extension cable. I’ve teamed it with a Rotary Encoder LED Ring from Mayhew Labs, which is basically a breakout board for rotary encoders with a brace of 15 serial-addressable LEDs around it. There’s another single LED hiding behind the encoder knob…

 

2014-11-14 17.01.04

 

…which makes for a neat little status light. The board itself has an imposing number of pins, but it’s actually pretty simple to drive once you break it down – it has your usual +5V and GND, two lines for the rotary encoder, one for the push-button (the whole encoder shaft clicks down), three for SPI serial, another line that needs to be grounded, and the tenth isn’t used at all.

If you’ve never played with a rotary encoder before, they’re kind of like digital potentiometers… but not quite. Instead of changing resistance, turning the shaft makes the thing output gray code on the two digital lines in order to track its rotational position. Also unlike a potentiometer, the shaft can spin endlessly through 360 degrees, in either direction, with a certain number of ‘clicks’ or detents per revolution – the one I used here has 24 clicks per full rotation, but fancier ones can get up to over a thousand, making them very precise.

In short, it looks like a pot, but hooks up to digital lines instead of analog, spins forever in either direction, and lets you accurately measure which direction it’s turned in, and how far. Combine a USB-native Arduino with one of these, and with a little help from some additional software on a PC, you have…

 

 

…a USB volume control knob with mute button.

(That track, by the way, is from the excellent album Machinedogs, written by a good friend of mine. My mobile phone’s dinky little microphone does not do it justice.)

You can’t just tell Windows to set the volume to some arbitrary value like 5% or 93% or 76.5% with a Leonardo – at least, not without writing your own driver for it or something – but 3RVX, my volume control program of choice for some time now (get it from here), will interpret certain keyboard and mouse combinations as commands to raise or lower the volume in steps.

 

3rvx2

 

By default, you can raise and lower the volume by pressing Windows and scrolling up and down, and muting/unmuting with Windows/middle (scroll) button, so all my sketch has to do is send a KEY_LEFT_GUI (Windows key, on a Windows PC), followed by a mouse scroll up/down or click, followed by another KEY_LEFT_GUI, and voila – 3RVX makes things happen by magic.

(OemMinus and OemPlus are just the minus and plus keys on your keyboard. I’m not sure what Oem5 is; Winkey + 5 is something different in Windows 7.)

3RVX also lets you set exactly how much the volume changes with each step…

 

3rvx

 

…so I set it to 6.67%, which is roughly 100 divided by 15 (which is the number of LEDs on the rotary encoder board), so volume changes are in lockstep with the LEDs on my little volume control. I can still use Windows+scrolling to change volume – my PC just sees the Leostick as an additional input device alongside my existing keyboard and mouse – but the lights on my controller get out of sync. That’s easy enough to fix, though, by just turning the knob to bring the volume on both the PC and the device to zero (or up to 100%, if you’re feeling masochistic), and turning it back to your desired listening volume.

If you mute the sound, turning the knob will automatically un-mute it. That’s convenient, but can make for a slightly skittish volume control – it’s easy to accidentally turn the knob one click while pressing down on it hard enough to click the switch. The alternative is to make it wait for two volume changes before unmuting, or just refuse to change volume at all until you click the button a second time to un-mute it again, but I didn’t bother doing either of those things.

Here’s the sketch:

 

/*
Tim's USB volume control V1.0

Information
Project page: https://tim.id.au/limejuice/volume-control-with-arduino-leonardo/
Demo video: https://www.youtube.com/watch?v=Ymt3yKxj_h8

Products used
Freetronics Leostick: http://www.freetronics.com/products/leostick
Rotary encoder board: http://mayhewlabs.com/products/rotary-encoder-led-ring

Based on example code from Mayhew Labs (LEDs) and Arduino Playground (rotary encoder)

The rotary encoder I used has 24 steps in 360 degrees, so the original plan was to have
24 volume steps so that 0-100% was one full rotation, but it felt more natural to enable
one LED per detent, so 'numLEDs' is set to 15 as the Mayhew board has 15 LEDs.
*/

// SPI communication pins
const int SDI = 2; // data
const int CLK = 3; // clock
const int LE = 4; // latch

// rotary encoder pins
const int encoderA = 5;
const int encoderB = 6;
const int encoderS = 7;

// volume control parameters
const int numLEDs = 15; // adjust this and the SPI code to use with a different LED product
int position = 0; // reflects the number of LEDs currently lit
int volume = 0;

// flags to deal with muting and button push
bool mute = false;
bool pushed = false;

// array of possible light arrangements
unsigned int array[] =
  {
  0x0,   0x1,   0x3,   0x7,      // 0-3 LEDs
  0xf,   0x1f,   0x3f,   0x7f,   // 4-7 LEDs
  0xff,  0x1ff,  0x3ff,  0x7ff,  // 8-11 LEDs
  0xfff, 0x1fff, 0x3fff, 0x7fff, // 12-16 LEDs
  0x8000                         // mute LED only
  };

// variables to track the state of the rotary encoder
int encoderState = LOW; // encoderState
int encoderLastState = LOW; // PinALast

// setLEDs uses the index of the array of hex numbers to turn on the correct LEDs
void setLEDs(int i)
  {
  digitalWrite(LE, LOW); // start talking to LED ring controller
  shiftOut(SDI, CLK, MSBFIRST, (array[i] >> 8)); // send high byte first
  shiftOut(SDI, CLK, MSBFIRST, array[i]); // then send low byte
  digitalWrite(LE, HIGH); // stop talking to LED ring controller
  }

// muteSound emulates pressing Windows + middle button (3RVX command to mute sound)
void muteSound()
  {
  pushed = true;
  mute = true;
  Keyboard.press(KEY_LEFT_GUI);
  Mouse.click(MOUSE_MIDDLE);
  delay(25); // without this, my PC sometimes out-guesses itself and actually opens the start menu
  Keyboard.release(KEY_LEFT_GUI);
  Serial.println("Sound muted");
  delay(50);
  }

// unmuteSound does the same but with different serial output and mute bool flag
void unmuteSound()
  {
  pushed = true;
  mute = false;
  Keyboard.press(KEY_LEFT_GUI);
  Mouse.click(MOUSE_MIDDLE);
  delay(25);
  Keyboard.release(KEY_LEFT_GUI);
  Serial.println("Sound unmuted");
  delay(50);
  }

// raiseVolume increases the position by 1, turning on another LED, and
// emulates pressing Windows + Scroll Up (3RVX command to raise the volume)
void raiseVolume()
  {
  if (position < numLEDs) // keep position below the maximum
    position++;
  Keyboard.press(KEY_LEFT_GUI);
  Mouse.move(0, 0, 1);
  delay(10);
  Keyboard.release(KEY_LEFT_GUI);
  }

// lowerVolume decreases the position by 1, turning off a LED, and emulates
// pressing Windows + Scroll Down (3RVX command to lower the volume)
void lowerVolume()
  {
  if (position > 0) // keep position above zero
    position--;
  Keyboard.press(KEY_LEFT_GUI);
  Mouse.move(0, 0, -1);
  delay(10);
  Keyboard.release(KEY_LEFT_GUI);
  }

// calculateVolume sets int volume to a percentage based on the current position
// and state of the mute flag
void calculateVolume()
  {
  if (mute)
    volume = 0;
  else
    volume = (int)((float)100/numLEDs*position);
  }

/*
An explanation of the maths involved: 100 divided by 15 is not a whole number so
some translation is involved. Number must be cast to float otherwise it will just
round to 6 and the highest possible volume becomes 90. Final result must be cast
to int to get nice round numbers (4-5% each step) for volume.
*/

// printStatus uses Serial.print/ln to output current position and volume percentage
void printStatus()
  {
  Serial.print("Position: ");
  Serial.print(position);
  Serial.print(", volume: ");
  Serial.print(volume);
  Serial.println("%");
  }

void setup()
  {
  // set SPI pins to output
  pinMode(SDI, OUTPUT);
  pinMode(CLK, OUTPUT);
  pinMode(LE, OUTPUT);
  
  // set encoder pins to input, turn internal pull-ups on
  pinMode(encoderA, INPUT);
  digitalWrite(encoderA, HIGH);
  pinMode(encoderB, INPUT);
  digitalWrite(encoderB, HIGH);
  pinMode(encoderS, INPUT);
  digitalWrite(encoderS, HIGH);
  
  // start various libraries for communication
  Keyboard.begin();
  Mouse.begin();
  Serial.begin(9600);
  }

void loop()
  {
  // check the state of the rotary encoder and adjust position
  encoderState = digitalRead(encoderA);
  if (encoderLastState == LOW && encoderState == HIGH) // encoder has rotated
    {
    if (digitalRead(encoderB) == LOW) // turned counter-clockwise
      lowerVolume();
    else // turned clockwise
      raiseVolume();
    
    // unmute sound if the volume changes
    if (mute == true)
      unmuteSound();
    
    // calculate the volume level
    calculateVolume();
 
    // serial output for debugging
    printStatus();
    }
  encoderLastState = encoderState;
  
  // check to see if the button's been pushed and toggle the mute condition
  if (digitalRead(encoderS) == LOW && mute == false && pushed == false)
    muteSound();
  else if (digitalRead(encoderS) == LOW && mute == true && pushed == false)
    unmuteSound();
  if (digitalRead(encoderS) == HIGH)
    pushed = false;
    
  // finally, set the LEDs
  if (mute)
    setLEDs(16);
  else
    setLEDs(position);
  }

 

Here’s a sample of the output over serial:

 

output

 

I could’ve used interrupts to read the state of the rotary encoder, debounced the button a little better etc, but it works well enough for my own use. Part 2 of this project is to put it in a proper plastic enclosure to make it more solid/resistant to thumping. Once I’ve figured out a permanent solution, I’ll blog that too.

Thanks for reading – leave a comment if you found this interesting/useful/there’s something glaringly wrong with my code/something I could do better!

 

One thought on “Volume control with Arduino Leonardo

Comments are closed.