Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Using a rotary encoder as a volume control for the Raspberry Pi

Using a rotary encoder as a volume control

On my RetroPie machine I wanted a hardware volume knob — the games I play use a handful of emulators, and there's no unified software interface for controlling the volume. The speakers I got for my cabinet are great, but don't have their own hardware volume knob. So with a bunch of googling and trial and error, I figured out what I need to pull this off: a rotary encoder and a daemon that listens for the signals it sends.

Rotary encoder

A rotary encoder is like the standard potentiometer (i.e., analog volume knob) we all know, except (a) you can keep turning it in either direction for as long as you want, and thus (b) it talks to the RPi differently than a potentiometer would.

I picked up this one from Adafruit, but there are plenty others available. This rotary encoder also lets you push the knob in and treats that like a button press, so I figured that would be useful for toggling mute on and off.

So we've got 5 wires to hook up: three for the knob part (A, B, and ground), and two for the button part (common and ground). Here's how I hooked them up (reference):

Description BCM # Board #
knob A GPIO 26 37
knob B GPIO 19 35
knob ground ground pin below GPIO 26 39
button common GPIO 13 33
button ground ground pin opposite GPIO 13 34

You can use whichever pins you want; just update the script if you change them.

Volume daemon

Since the GPIO pins are just for arbitrary signals, you need a script on the Pi that knows what to do with them. The builtin GPIO library for the Pi will do nicely for this.

You'll see the script below, but here's how it works: it listens on the specified pins, and when the knob is turned one way or another, it uses the states of the A and B pins to figure out whether the knob was turned to the left or to the right. That way it knows whether to increase or decrease the system volume in response, which it does with the command-line program amixer.

First, make sure amixer is present and install it if it isn’t.

which amixer || sudo apt-get install alsa-utils

(If you’re not using the default analog audio output, consult @thijstriemstra’s comment below for some additional steps that you may or may not need to do.)

Create a bin directory in your pi folder if it doesn't exist already, then drop the script below into it.

mkdir ~/bin

If it's not there yet, I'd also put /home/pi/bin somewhere in your PATH:

echo $PATH
# don't see "/home/pi/bin" there? then run...
echo "export PATH=$HOME/bin:$PATH" >> ~/.bashrc
# ...and restart your shell

Then drop the script into the /home/pi/bin folder:

nano ~/bin/monitor-volume # (or however you do it)
chmod +x ~/bin/monitor-volume

You can run this script in the foreground just to test it out. Edit the script and temporarily change DEBUG to True so that you can see what's going on, then simply run it with monitor-volume. When you turn or press the knob, the script should report what it's doing. (Make sure that the volume increases rather than decreases when you turn it to the right, and if it doesn't, swap your A and B pins, or just swap their numbers in the script.)

You should also play around with the constants defined at the top of the script. Naturally, if you picked other pins, you'll want to tell the script which GPIO pins to use. If your rotary encoder doesn't act like a button, or if you didn't hook up the button and don't care about it, you can set GPIO_BUTTON to None. But you may also want to change the minimum and maximum volumes (they're percentages, so they should be between 1 and 100) and the increment (how many percentage points the volume increases/decreases with each "click" of the knob).

If it's working the way you want, you can proceed to the next step: running monitor-volume automatically in the background whenever your Pi starts.

Creating a systemd service

NOTE: If you're on a version of Raspbian before Jessie, these instructions won't work for you. Hopefully someone can pipe up with a version of this for init.d in the comments.

systemd is the new way of managing startup daemons in Raspbian Jessie. Because it's new, there's not much RPi-specific documentation on it, and to find out how to use it you have to sift through a bunch of Google results from people who hate systemd and wish it didn't exist. After much trial and error, here's what worked for me:

nano ~/monitor-volume.service
# paste in the contents of monitor-volume.service, save/exit nano
chmod +x ~/monitor-volume.service
sudo mv ~/monitor-volume.service /etc/systemd/system
sudo systemctl enable monitor-volume
sudo systemctl start monitor-volume

If that worked right, then you just told Raspbian to start up that script in the background on every boot (enable), and also to start it right now (start). At this point, and on every boot after this, your volume knob should Just Work.

FAQ

This didn't work!

I got this working on an RPi3 running RetroPie 3.x, so all I can say is “works on my machine.” Some things to try:

  • You might not have Python 3; if which python3 turns up nothing, try this:

    sudo apt-get install python3 python3-rpi.gpio
    
  • I've heard that in earlier versions of Raspbian, the pi user isn't automatically allowed to access the GPIO pins, so you need to run scripts like this as root. If you're running into permissions errors when you try to run the script from your shell, then that's your problem, most likely. There's no particular reason why you shouldn't run this script as root, except on the general principle that you shouldn't really trust code that you didn't write. Make good choices and have backups.

  • I might have made a typo in the gist. Wouldn't be the first time.

If you run into trouble, leave a comment and the internet can help you figure it out.

#!/usr/bin/env python3
"""
The daemon responsible for changing the volume in response to a turn or press
of the volume knob.
The volume knob is a rotary encoder. It turns infinitely in either direction.
Turning it to the right will increase the volume; turning it to the left will
decrease the volume. The knob can also be pressed like a button in order to
turn muting on or off.
The knob uses two GPIO pins and we need some extra logic to decode it. The
button we can just treat like an ordinary button. Rather than poll
constantly, we use threads and interrupts to listen on all three pins in one
script.
"""
import os
import signal
import subprocess
import sys
import threading
from RPi import GPIO
from queue import Queue
DEBUG = False
# SETTINGS
# ========
# The two pins that the encoder uses (BCM numbering).
GPIO_A = 26
GPIO_B = 19
# The pin that the knob's button is hooked up to. If you have no button, set
# this to None.
GPIO_BUTTON = 13
# The minimum and maximum volumes, as percentages.
#
# The default max is less than 100 to prevent distortion. The default min is
# greater than zero because if your system is like mine, sound gets
# completely inaudible _long_ before 0%. If you've got a hardware amp or
# serious speakers or something, your results will vary.
VOLUME_MIN = 60
VOLUME_MAX = 96
# The amount you want one click of the knob to increase or decrease the
# volume. I don't think that non-integer values work here, but you're welcome
# to try.
VOLUME_INCREMENT = 1
# (END SETTINGS)
#
# When the knob is turned, the callback happens in a separate thread. If
# those turn callbacks fire erratically or out of order, we'll get confused
# about which direction the knob is being turned, so we'll use a queue to
# enforce FIFO. The callback will push onto a queue, and all the actual
# volume-changing will happen in the main thread.
QUEUE = Queue()
# When we put something in the queue, we'll use an event to signal to the
# main thread that there's something in there. Then the main thread will
# process the queue and reset the event. If the knob is turned very quickly,
# this event loop will fall behind, but that's OK because it consumes the
# queue completely each time through the loop, so it's guaranteed to catch up.
EVENT = threading.Event()
def debug(str):
if not DEBUG:
return
print(str)
class RotaryEncoder:
"""
A class to decode mechanical rotary encoder pulses.
Ported to RPi.GPIO from the pigpio sample here:
http://abyz.co.uk/rpi/pigpio/examples.html
"""
def __init__(self, gpioA, gpioB, callback=None, buttonPin=None, buttonCallback=None):
"""
Instantiate the class. Takes three arguments: the two pin numbers to
which the rotary encoder is connected, plus a callback to run when the
switch is turned.
The callback receives one argument: a `delta` that will be either 1 or -1.
One of them means that the dial is being turned to the right; the other
means that the dial is being turned to the left. I'll be damned if I know
yet which one is which.
"""
self.lastGpio = None
self.gpioA = gpioA
self.gpioB = gpioB
self.callback = callback
self.gpioButton = buttonPin
self.buttonCallback = buttonCallback
self.levA = 0
self.levB = 0
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.gpioA, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.gpioB, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(self.gpioA, GPIO.BOTH, self._callback)
GPIO.add_event_detect(self.gpioB, GPIO.BOTH, self._callback)
if self.gpioButton:
GPIO.setup(self.gpioButton, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(self.gpioButton, GPIO.FALLING, self._buttonCallback, bouncetime=500)
def destroy(self):
GPIO.remove_event_detect(self.gpioA)
GPIO.remove_event_detect(self.gpioB)
GPIO.cleanup()
def _buttonCallback(self, channel):
self.buttonCallback(GPIO.input(channel))
def _callback(self, channel):
level = GPIO.input(channel)
if channel == self.gpioA:
self.levA = level
else:
self.levB = level
# Debounce.
if channel == self.lastGpio:
return
# When both inputs are at 1, we'll fire a callback. If A was the most
# recent pin set high, it'll be forward, and if B was the most recent pin
# set high, it'll be reverse.
self.lastGpio = channel
if channel == self.gpioA and level == 1:
if self.levB == 1:
self.callback(1)
elif channel == self.gpioB and level == 1:
if self.levA == 1:
self.callback(-1)
class VolumeError(Exception):
pass
class Volume:
"""
A wrapper API for interacting with the volume settings on the RPi.
"""
MIN = VOLUME_MIN
MAX = VOLUME_MAX
INCREMENT = VOLUME_INCREMENT
def __init__(self):
# Set an initial value for last_volume in case we're muted when we start.
self.last_volume = self.MIN
self._sync()
def up(self):
"""
Increases the volume by one increment.
"""
return self.change(self.INCREMENT)
def down(self):
"""
Decreases the volume by one increment.
"""
return self.change(-self.INCREMENT)
def change(self, delta):
v = self.volume + delta
v = self._constrain(v)
return self.set_volume(v)
def set_volume(self, v):
"""
Sets volume to a specific value.
"""
self.volume = self._constrain(v)
output = self.amixer("set 'PCM' unmute {}%".format(v))
self._sync(output)
return self.volume
def toggle(self):
"""
Toggles muting between on and off.
"""
if self.is_muted:
output = self.amixer("set 'PCM' unmute")
else:
# We're about to mute ourselves, so we should remember the last volume
# value we had because we'll want to restore it later.
self.last_volume = self.volume
output = self.amixer("set 'PCM' mute")
self._sync(output)
if not self.is_muted:
# If we just unmuted ourselves, we should restore whatever volume we
# had previously.
self.set_volume(self.last_volume)
return self.is_muted
def status(self):
if self.is_muted:
return "{}% (muted)".format(self.volume)
return "{}%".format(self.volume)
# Read the output of `amixer` to get the system volume and mute state.
#
# This is designed not to do much work because it'll get called with every
# click of the knob in either direction, which is why we're doing simple
# string scanning and not regular expressions.
def _sync(self, output=None):
if output is None:
output = self.amixer("get 'PCM'")
lines = output.readlines()
if DEBUG:
strings = [line.decode('utf8') for line in lines]
debug("OUTPUT:")
debug("".join(strings))
last = lines[-1].decode('utf-8')
# The last line of output will have two values in square brackets. The
# first will be the volume (e.g., "[95%]") and the second will be the
# mute state ("[off]" or "[on]").
i1 = last.rindex('[') + 1
i2 = last.rindex(']')
self.is_muted = last[i1:i2] == 'off'
i1 = last.index('[') + 1
i2 = last.index('%')
# In between these two will be the percentage value.
pct = last[i1:i2]
self.volume = int(pct)
# Ensures the volume value is between our minimum and maximum.
def _constrain(self, v):
if v < self.MIN:
return self.MIN
if v > self.MAX:
return self.MAX
return v
def amixer(self, cmd):
p = subprocess.Popen("amixer {}".format(cmd), shell=True, stdout=subprocess.PIPE)
code = p.wait()
if code != 0:
raise VolumeError("Unknown error")
sys.exit(0)
return p.stdout
if __name__ == "__main__":
gpioA = GPIO_A
gpioB = GPIO_B
gpioButton = GPIO_BUTTON
v = Volume()
def on_press(value):
v.toggle()
print("Toggled mute to: {}".format(v.is_muted))
EVENT.set()
# This callback runs in the background thread. All it does is put turn
# events into a queue and flag the main thread to process them. The
# queueing ensures that we won't miss anything if the knob is turned
# extremely quickly.
def on_turn(delta):
QUEUE.put(delta)
EVENT.set()
def consume_queue():
while not QUEUE.empty():
delta = QUEUE.get()
handle_delta(delta)
def handle_delta(delta):
if v.is_muted:
debug("Unmuting")
v.toggle()
if delta == 1:
vol = v.up()
else:
vol = v.down()
print("Set volume to: {}".format(vol))
def on_exit(a, b):
print("Exiting...")
encoder.destroy()
sys.exit(0)
debug("Volume knob using pins {} and {}".format(gpioA, gpioB))
if gpioButton != None:
debug("Volume button using pin {}".format(gpioButton))
debug("Initial volume: {}".format(v.volume))
encoder = RotaryEncoder(GPIO_A, GPIO_B, callback=on_turn, buttonPin=GPIO_BUTTON, buttonCallback=on_press)
signal.signal(signal.SIGINT, on_exit)
while True:
# This is the best way I could come up with to ensure that this script
# runs indefinitely without wasting CPU by polling. The main thread will
# block quietly while waiting for the event to get flagged. When the knob
# is turned we're able to respond immediately, but when it's not being
# turned we're not looping at all.
#
# The 1200-second (20 minute) timeout is a hack; for some reason, if I
# don't specify a timeout, I'm unable to get the SIGINT handler above to
# work properly. But if there is a timeout set, even if it's a very long
# timeout, then Ctrl-C works as intended. No idea why.
EVENT.wait(1200)
consume_queue()
EVENT.clear()
[Unit]
Description=Volume knob monitor
[Service]
User=pi
Group=pi
ExecStart=/home/pi/bin/monitor-volume
[Install]
WantedBy=multi-user.target
@dh04000

This comment has been minimized.

Copy link

dh04000 commented Oct 3, 2016

Great idea savetheclocktower! I have been looking for something JUST LIKE THIS for my standup arcade machine for ages. I run my machine on recalbox (a retropi competitor), but they appearently run python 2.7 and this is python 3 code. I found this project https://wiki.python.org/moin/3to2 which on converts python 3 code to python 2.7 code, but I am a "user" , not a coder, so I can't figure out how to use it.

Would you please consider creating a python 2.7 compatible version, so I could use your wonderful idea in my arcade cabinet too?

Thank you for your time and consideration.

@jazzcat007

This comment has been minimized.

Copy link

jazzcat007 commented Dec 6, 2016

If I wanted to use 3 push button rotary encoders to perform say volume/mute, up-down/enter, track/pause, would I need multiple scripts and monitor services?

@michaeldrouin

This comment has been minimized.

Copy link

michaeldrouin commented Jan 26, 2017

@dh04000 the only things that need to be adjusted in this to make it python 2 compatible is change line #1 from: #!/usr/bin/env python3 to: #!/usr/bin/env python2, and change line #25 from from queue import Queue to from multiprocessing import Queue

@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Feb 27, 2017

@jazzcat007 I keep forgetting that GitHub doesn't send emails when people comment on my gists. (Which means that you might not see this either.)

The answer is no — you wouldn't need multiple scripts and monitor services. The add_event_detect approach I use here works for an arbitrary number of GPIO pins. (Whereas if I used wait_for_edge I'd be able to listen to only one pin per script.)

However, you might nonetheless choose to use multiple scripts/services. Since we're not constantly asking for the status of the pins, and are instead relying on the interrupt to tell us when the pin status changes, running several scripts at once would have a negligible effect on performance. And the code above is complicated enough that you probably won't have fun hacking it to listen on nine pins instead of three.

If I were in your position and had multiple rotary encoders on the same machine I'd probably clean this code up and hide the ugliness in a module that each script could import. That way the volume-changing code wouldn't be all tangled up in the rotary-encoder-interpreting code.

@thijstriemstra

This comment has been minimized.

Copy link

thijstriemstra commented Jun 28, 2017

Trying to get this to work with an USB audio card @savetheclocktower but:

/bin/sh: 1: amixer: not found
Traceback (most recent call last):
  File "./monitor-volume", line 271, in <module>
    v = Volume()
  File "./monitor-volume", line 164, in __init__
    self._sync()
  File "./monitor-volume", line 223, in _sync
    output = self.amixer("get 'PCM'")
  File "./monitor-volume", line 259, in amixer
    raise VolumeError("Unknown error")
__main__.VolumeError: Unknown error

Install following:

sudo apt-get install alsa-utils jackd oss-compat pulseaudio

Then:

amixer: Unable to find simple control 'PCM',0

Traceback (most recent call last):
  File "./monitor-volume", line 271, in <module>
    v = Volume()
  File "./monitor-volume", line 164, in __init__
    self._sync()
  File "./monitor-volume", line 223, in _sync
    output = self.amixer("get 'PCM'")
  File "./monitor-volume", line 259, in amixer
    raise VolumeError("Unknown error")
__main__.VolumeError: Unknown error

My /etc/asound.conf with an USB audio card attached:

pcm.!default  {
 type hw card 1
}
ctl.!default {
 type hw card 1
}

Available cards:

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 7/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio]
  Subdevices: 0/1
  Subdevice #0: subdevice #0

And:

$ cat  /proc/asound/pcm
00-00: bcm2835 ALSA : bcm2835 ALSA : playback 8
00-01: bcm2835 ALSA : bcm2835 IEC958/HDMI : playback 1
01-00: USB Audio : USB Audio : playback 1 : capture 1

Turns out there's no PCM control, it's called Master:

$ amixer scontrols
Simple mixer control 'Master',0
Simple mixer control 'Capture',0

Changing the PCM references to Master in the script fixed the problem.

$ ./monitor-volume 
OUTPUT:
Simple mixer control 'Master',0
  Capabilities: pvolume pswitch pswitch-joined
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 65536
  Mono:
  Front Left: Playback 49145 [75%] [on]
  Front Right: Playback 49145 [75%] [on]

Volume knob using pins 26 and 19
Volume button using pin 13
Initial volume: 75
@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Jun 28, 2017

@thijstriemstra Glad you got it working! Now I vaguely recall having to install alsa-utils. I don't think I had to install those other three. I'll make a note in the gist.

@thijstriemstra

This comment has been minimized.

Copy link

thijstriemstra commented Jul 3, 2017

I ended up using a potentiometer with an MCP3008. Modified your gist with instructions here: https://gist.github.com/thijstriemstra/6396142f426aeffb0c1c6507fb2acd7b, cheers.

@jonnydcakes

This comment has been minimized.

Copy link

jonnydcakes commented Oct 13, 2017

@savetheclocktower - I'm using the same rotary encoder you recommended above, but am having problems with the mute function. here's what debug is spiting out:

Traceback (most recent call last):
   File "/home/pi/bin/monitor-volume", line 126, in _buttonCallback
     self.buttonCallback(GPIO.input(channel))
   File "/home/pi/bin/monitor-volume", line 274, in on_press
      v.toggle()
   File "/home/pi/bin/monitor-volume", line 202, in toggle
      output = self.amixer("set 'PCM' mute")
   File "/home/pi/bin/monitor-volume", line 259, in amixer
      raise VolumeError("Unknown error")
__main__.VolumeError: Unknown error

Does anyone have any help? I'm kinda new to python and physical computing. And thanks so much for putting this out there! I'm building an audio book player for my kids, and this is perfect!

@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Oct 13, 2017

@johnnydcakes Try running amixer set 'PCM' mute in a terminal and see what it does. For whatever reason, that command seems to be returning an error for you when the script invokes it.

@jonnydcakes

This comment has been minimized.

Copy link

jonnydcakes commented Oct 14, 2017

@savetheclocktower I tried and got this response:

amixer set 'PCM' mute
amixer: Invalid command!

The volume knob is working fine adjusting up and down, it's just the mute. I can't figure out what's up here. Just so you know, I'm using a RPi Zero W with an Adafruit Audio Bonnet. I don't seem to be able to use any command to mute the volume through amixer at the command line.

Thanks again for your help!

@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Oct 14, 2017

@jonnydcakes Try running just amixer and looking at the group names. One of them is probably what you should be using instead of "PCM."

@jonnydcakes

This comment has been minimized.

Copy link

jonnydcakes commented Oct 14, 2017

I was able to get the button to mute the sound by replacing line 202 with output = self.amixer("cset numid=1 0%) but obviously pressing it again won't return it to the previous volume because the program can't tell it is muted. For whatever reason, I don't seem to have the mute function available in amixer.

@jonnydcakes

This comment has been minimized.

Copy link

jonnydcakes commented Oct 14, 2017

Here is the output of amixer:

Simple mixer control 'PCM',0
  Capabilities: volume
  Playback channels: Front Left - Front Right
  Capture channels: Front Left - Front Right
  Limits: 0 - 255
  Front Left: 156 [61%]
  Front Right: 156 [61%]

So it looks like PCM is the only card available. I wonder if it has something to do with running audio out I2S and the custom stuff you have to add to get the audio bonnet to work.

@jonnydcakes

This comment has been minimized.

Copy link

jonnydcakes commented Oct 14, 2017

Here is the special audio conf code for the bonnet from /etc/asound.conf. I'm not really familiar with what this code does, so maybe the muting function is not available because it isn't allowed here?

pcm.speakerbonnet {
   type hw card 0
}

pcm.dmixer {
   type dmix
   ipc_key 1024
   ipc_perm 0666
   slave {
     pcm "speakerbonnet"
     period_time 0
     period_size 1024
     buffer_size 8192
     rate 44100
     channels 2
   }
}

ctl.dmixer {
    type hw card 0
}

pcm.softvol {
    type softvol
    slave.pcm "dmixer"
    control.name "PCM"
    control.card 0
}

ctl.softvol {
    type hw card 0
}

pcm.!default {
    type             plug
    slave.pcm       "softvol"
}
@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Oct 14, 2017

Yeah, I'm out of ideas — audio config isn't my strong suit either. At this point I'd suggest disabling the mute function and just turning the knob really fast to the left when you want to mute it. :-)

@tony1550

This comment has been minimized.

Copy link

tony1550 commented Jan 10, 2018

Hi, I really like this project so I decided to give it a try for my raspberry pi radio I am working on. I used the same rotary encoder as mentioned in the instruction and I also used the same GPIO as in the script. When I initially ran the script I had to change PCM to Master because I have a Pimoroni Phat DAC as my sound adapter. Needless to say it all works fine. However, when I run the script it mutes my audio completely, the encoder reports that the volume increase and decrease as I turn the knob but there is no audio coming from my speakers. The only way to restore my audio is to reboot. Has anyone had this happen to them and can anyone give me a hand. Thanks in advance.

@mizzoudavis

This comment has been minimized.

Copy link

mizzoudavis commented Feb 28, 2018

Hey @savetheclocktower Any chance you could post a pick of your setup? The link to it is dead.

@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented Feb 28, 2018

@mizzoudavis The dead link was just a diagram of the RPi GPIO pins. I updated the link to point to pinout.xyz. Thanks!

@satstar69

This comment has been minimized.

Copy link

satstar69 commented Mar 23, 2018

Hello @savetheclocktower,
I've tried your code and works perfectly.
My objective is using a rotary encoder to control 2 Led's , the left led lights up during the period that the encoder rotates left, right led lights up during the period that the encoder rotates right. Can you help me ?
Thanks.

@sHooPmyWooP

This comment has been minimized.

Copy link

sHooPmyWooP commented Mar 23, 2018

Hello @savetheclocktower,
thanks for this awesome tutorial. I'm very new to programming in general and python/raspberry in detail. Your code works fine for me except one problem. The volume will only vary between two values (e.g. 31 to 32). I'm not sure if this explains my problem well enough. I hope you have an idea how to fix this, since I've checked various sites and didn't find a trace to a solution.
Greetings and thanks in advance!

@JamesGKent

This comment has been minimized.

Copy link

JamesGKent commented Apr 6, 2018

for anyone interested i wrote a kernel driver to use a rotary encoder as a volume control by translating it's events into keyboard volume up and down keypresses. for anyone interested the source and instructions is here

@Onextw

This comment has been minimized.

Copy link

Onextw commented Apr 30, 2018

Hi all,
I'm more of a duct tape and cable ties kind of coder but I thought I would share in case it helps someone else.

Came across this project while building a standalone AV Reciever sort of thing, hit some of the same hiccups as others along the way but managed to get it working for now. Writing this up from memory from last night so I might do a clean install and make sure I've covered everything later on but I'm pretty sure this is everything I did.

As I was using it on OSMC (latest stable build) with a HifiBerry Amp2 there were a few tweaks that I needed to do to get it up and running.

First off, as suggested by @savetheclocktower OSMC doesnt have alsa-utils installed by default so

sudo apt-get install alsa-utils

I am also using an OLED Display and I don't believe OSMC has GPIO functionality off the bat so I had also used the following instructions installing various libraries taken from here

sudo apt-get install build-essential python-dev python-pip
sudo pip install RPi.GPIO

I have heard that using pip to install RPi.GPIO is the wrong way to go about it, but it did work, maybe someone can clear that up.

It became a bit of a mess using Python3 so following @michaeldrouin's suggestion I changed

line #1 from: #!/usr/bin/env python3 to: #!/usr/bin/env python2
line #25 from from queue import Queue to from multiprocessing import Queue

As the Berry Amp2's use pin 19 switched that up to pin 16 (HIFIBERRY DAC+, DIGI+ AND AMP+ also look to use the same pins)

line #34 from GPIO_B = 19 to GPIO_B = 16

I also had a similar problem as @jonnydcakes with the audio sink so after a quick google I came across this which flagged that the audio sink was 'Digital' so essentially replacing "PCM" within the following lines with "Digital"

Line 185: output = self.amixer("set 'Digital' unmute {}%".format(v))
Line 194: output = self.amixer("set 'Digital' unmute")
Line 199: output = self.amixer("set 'Digital' mute")
Line 220: output = self.amixer("get 'Digital'")

Lastly to get it running as a service, OSMC has a problem allowing r/w/x access to GPIO pins to the main user osmc, you could add the user to the GPIO group permissions but i chose to run it as root instead as it will be a core system function for my project. so changing the lines in the monitor-volume.service script

Line 5: User=root
Line 6: Group=root

That might be a security risk, maybe someone wants to correct me on that but for now it works. Again I'll probably be doing this on a clean install to see if I missed anything.

There are a few things that I might try and do (albeit with duct tape and cable ties)

  • The Hifiberry amp2 has a mute line on GPIO pin 4, so make use of that over the rotary encoders button
  • Use the button on the rotary encoder to facilitate a menu on the OLED
  • As this doesn't show volume changes on the GUI I might look into @JamesGKent's approach once the Kernel within the stable branch of OSMC reach the requirements (4.20 I think?) as I'm assuming that as it replicates (+/-) keystrokes it would.

Anyway awesome project, appreciate you making it easier for dumbasses like me to make things work. hopefully this helps other people

Snap of the Rpi im running atm

@ghost

This comment has been minimized.

Copy link

ghost commented May 1, 2018

Hello,
Getting error noted below, help please!

pi@Pi0:~ $ DEBUG=1 monitor-volume
Knob using pins 26 and 19
amixer: Unable to find simple control 'PCM',0

Traceback (most recent call last):
File "/home/pi/bin/monitor-volume", line 276, in
v = Volume()
File "/home/pi/bin/monitor-volume", line 132, in init
self._sync()
File "/home/pi/bin/monitor-volume", line 193, in _sync
output = self._amixer("get 'PCM'")
File "/home/pi/bin/monitor-volume", line 224, in _amixer
raise VolumeError("Unknown error")
main.VolumeError: Unknown error

@savetheclocktower

This comment has been minimized.

Copy link
Owner Author

savetheclocktower commented May 1, 2018

@GRigdon see this comment for some guidance.

@AEChadwick

This comment has been minimized.

Copy link

AEChadwick commented Jun 19, 2018

first off, thank you so much for this project--i appreciate all the work, it's been super helpful.

everything runs perfectly from the command line, but i cannot get it to work as a service. it claims to be running, but there is no effect--turning the encoder does nothing.

checking service monitor-volume status gets this

   Loaded: loaded (/etc/systemd/system/monitor-volume.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2018-06-19 23:34:14 UTC; 41s ago
 Main PID: 528 (python3)
   CGroup: /system.slice/monitor-volume.service
           ├─528 python3 /home/pi/bin/monitor-volume
           └─657 /usr/bin/pulseaudio --start --log-target=syslog

Jun 19 23:34:14 raspberrypi systemd[1]: Started Volume knob monitor.
Jun 19 23:34:16 raspberrypi pulseaudio[657]: [pulseaudio] server-lookup.c: Unable to contact D-Bus: org.freedesktop.DBus.Error.NotSupported: Unable to autolaunch a dbus-daemon without a $DISPL
Jun 19 23:34:16 raspberrypi pulseaudio[657]: [pulseaudio] main.c: Unable to contact D-Bus: org.freedesktop.DBus.Error.NotSupported: Unable to autolaunch a dbus-daemon without a $DISPLAY for X1

RaspberryPi 3 with HifiBerry Amp2, and currently using a monitor via HDMI (but i hope to go headless)

i am just wondering if anyone else is encountering this. Thanks!

@klettervirus

This comment has been minimized.

Copy link

klettervirus commented Sep 26, 2018

Awesome project, thanks! I got it working so that the mute button works and I can change the volume. Only problem left: It only changes volume down to 74% (which funnily is the initial Volume) to 100%. I did change the maximal/minimal Volume in the script (ln 46/47), but that did nothing. Can somebody please point me in the right direction? Using a usb-soundcard and python 2 (2.7 I believe).

Set volume to: 74
OUTPUT:
Simple mixer control 'PCM',0
Capabilities: pvolume pswitch pswitch-joined
Playback channels: Front Left - Front Right
Limits: Playback 0 - 151
Mono:
Front Left: Playback 111 [74%] [-7.56dB] [on]
Front Right: Playback 111 [74%] [-7.56dB] [on]

@panzo19

This comment has been minimized.

Copy link

panzo19 commented Jan 31, 2019

I'm following all the steps unfortunately I take the following output but the knob seems not to be responded.
im also running it as root and already try to run it with python2

any help?

OUTPUT:
Simple mixer control 'PCM',0
Capabilities: pvolume pvolume-joined pswitch pswitch-joined
Playback channels: Mono
Limits: Playback -10239 - 400
Mono: Playback -1089 [86%] [-10.89dB] [on]

Volume knob using pins 26 and 19
Volume button using pin 13
Initial volume: 86

@panzo19

This comment has been minimized.

Copy link

panzo19 commented Feb 1, 2019

Problem solved as i found out my rotary encoder has different pin outs!! But another issu raised !!! When i turn the encoder in any way goes from 86% to 85% and stays in 85% which is the minimun. Randomly goes to 87% and then back to 85%. Push works like a chrarm as mute

@nettings

This comment has been minimized.

Copy link

nettings commented Feb 16, 2019

Hi @savetheclocktower, thanks for this nice script. I linked to you from my own similar project, because I find yours easier. The reason I wrote my own is that I need it to also be able to create JACK MIDI messages, which means it has to run under realtime constraints. The Python JACK API is very nice, but it wouldn't give me the performance to run at low latency. If someone here has a similar itch to scratch, can I humbly suggest to check out https://github.com/nettings/gpioctl . Otherwise, by all means stick to Python!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.