Rotary volume control for the Raspberry Pi

Last modified date

Comments: 8

If you have checked out my previous post, you know I want to create a clock radio powered by the Raspberry Pi with the audio coming out of a JustBoom AMP Zero pHAT.  One of the things that I thought would be quite handy (probably necessary) would be volume control.  Ideally that would be some kind of hardware rotation controller, but looking around online on how one might control the JustBoom amp with a rotary encoder really only brought back results regarding full OS solutions such as Moode Audio.

As it turns out, controlling the volume for the JustBoom when using just Raspbian is actually pretty simple.

The hardware

We’re using a simple rotary encoder (this particular one was purchased from ModMyPi) which also has a momentary push button functionality.

ModMyPi has a great article on just what a rotary encoder is and how it works, so I won’t bother repeating it all here.  Suffice to say, you can determine which way the rotary encoder is rotating and from there you can determine if you want to move the volume up or down.

The encoder has five pins; a positive and ground, one for the button and two for the rotary encoding.  I hooked those up to pins higher up on the end of the GPIO because I knew that JustBoom weren’t using them (nor would be the screen I want to use).  The pins I used were:

  • GND: Physical pin 39
  • +: Physical pin 2
  • SW: Physical pin 37/BCM 26
  • DT: Physical pin 31/BCM 6
  • CLK: Physical pin 29/BCM 5

Setting the volume

The first thing I needed to do was just get a little information about my audio card, such as what is the number of the audio card and what is the maximum volume.

In order to do that we first want to check out the proc system so that we can see the audio card information.  That can be done with the command line:

 cat /proc/asound/cards

With that I get the output:

 0 [sndrpijustboomd]: snd_rpi_justboo - snd_rpi_justboom_dac
                      snd_rpi_justboom_dac

So it can be seen that the JustBoom card is number 0.  This was going to be pretty obvious as it’s the only card that I have attached to the Raspberry Pi, but it’s good to see it written down.

The audio amp card is an ALSA-based card and as such the amixer command can be used to get the information or set the value of audio controls.  I’ve tried to use the name of the control but that never worked well for me, but using the id of the control worked well.  To get the list of controls and their respective ids, I just run the command:

 amixer -c 0 controls

That brought back the results:

numid=6,iface=MIXER,name='DSP Program'
numid=3,iface=MIXER,name='Analogue Playback Boost Volume'
numid=2,iface=MIXER,name='Analogue Playback Volume'
numid=10,iface=MIXER,name='Auto Mute Mono Switch'
numid=11,iface=MIXER,name='Auto Mute Switch'
numid=8,iface=MIXER,name='Auto Mute Time Left'
numid=9,iface=MIXER,name='Auto Mute Time Right'
numid=7,iface=MIXER,name='Clock Missing Period'
numid=5,iface=MIXER,name='Deemphasis Switch'
numid=4,iface=MIXER,name='Digital Playback Switch'
numid=1,iface=MIXER,name='Digital Playback Volume'
numid=20,iface=MIXER,name='Max Overclock DAC'
numid=19,iface=MIXER,name='Max Overclock DSP'
numid=18,iface=MIXER,name='Max Overclock PLL'
numid=16,iface=MIXER,name='Volume Ramp Down Emergency Rate'
numid=17,iface=MIXER,name='Volume Ramp Down Emergency Step'
numid=12,iface=MIXER,name='Volume Ramp Down Rate'
numid=13,iface=MIXER,name='Volume Ramp Down Step'
numid=14,iface=MIXER,name='Volume Ramp Up Rate'
numid=15,iface=MIXER,name='Volume Ramp Up Step'

The volume control I want to use is the ‘Digital Playback Volume’ which has the numid of 1.

That numid can now be used to get information about that control.  To do that, the command is:

 amixer cget numid=1

Which gives the result:

 numid=1,iface=MIXER,name='Digital Playback Volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=207,step=0
  : values=0,0
  | dBscale-min=-103.50dB,step=0.50dB,mute=1

With the above, the minimum audio value is 0 and the maximum value that can be set is 207.  The values=0,0 shows that the volume value is currently at 0 (for left and right channels, presumably).

The volume can easily be adjusted with the command like:

 amixer -c 0 cset numid=1 175

The value that returns from the cget lookup is now:

numid=1,iface=MIXER,name='Digital Playback Volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=207,step=0
  : values=175,175
  | dBscale-min=-103.50dB,step=0.50dB,mute=1

Hooking it all up

Now that the hardware is attached and we know a little more information about the card we can write a little code:

I saved that into a file called volume.py and ran it with the command:

python volume.py

(You could put it in the background if you want, but I had another terminal window up where I start some music playing with mpg321).

And it works!  Pretty simple, but the results are effective.

What next?

The rotary encoding is just determined by a simple check of the clk and dt values being different and using those to determine which way it’s rotating.  However, it’s a little susceptible to bounce and can quite often go down when it’s meant to go up, and it’s certainly not very good at rotating at high speeds.  So something about that needs to be sorted out.  However, it’s not a bad first step.

Share

8 Responses

  1. Honestly, I don’t know for sure – never tried. However, potentiometers only have a certain range that they can travel through – a min and max rotation before they stop. Rotary encoders, on the other hand, have infinite rotation so would allow you to scroll as much you want to either way. Also, rotary encoders produces digital signals whereas potentiometers would be analogue. So I suppose it depends on your need.

    This thread may be of some info to you: https://www.element14.com/community/thread/55374/l/potentiometer-instead-of-rotary-encoder?displayFullThread=true

  2. Awesome, easy and very well explained! Thank you so much… I ran into one Prolem though:
    I get a negative Volume for minimum volume (-10239) when I do “amixer cget numid=1″. I cannot call ” amixer -c 0 cset numid=1 -175″ with a negative number, for it gets misinterpreted…. I have no soundcard, but use regular output via build in sound: “cat /proc/asound/cards”
    gives
    ” 0 [ALSA ]: bcm2835_alsa – bcm2835 ALSA
    bcm2835 ALSA”

    How can I use the negative value for volume-min. so I can make the volume lower than now? Right now I can only go from 400 to 0, leaving a considerable amount of volume left…..

  3. Hi Andy,

    I was wondering if you could help to understand my problem is, as I followed your tuto and the rotary is working but not acting on sound at all.
    (Raspberry 3)

    Here is the output of with the above commands :

    pi@raspberrypi:~ $ cat /proc/asound/cards
    0 [Headphones ]: bcm2835_headphonbcm2835 Headphones – bcm2835 Headphones
    bcm2835 Headphones

    pi@raspberrypi:~ $ amixer -c 0 controls
    numid=2,iface=MIXER,name=’Headphone Playback Switch’
    numid=1,iface=MIXER,name=’Headphone Playback Volume’

    pi@raspberrypi:~ $ amixer cget numid=1
    numid=1,iface=MIXER,name=’Capture Volume’
    ; type=INTEGER,access=rw——,values=1,min=0,max=65536,step=1
    : values=65536

    And once I launch the script I can see the knob acting

    pi@raspberrypi:~ $ python volume.py
    105 (51%)
    Muted
    Unmuted
    100 (48%)
    105 (51%)
    110 (53%)
    115 (56%)
    110 (53%)
    105 (51%)
    100 (48%)
    95 (46%)
    90 (43%)
    85 (41%)
    80 (39%)

    I have not modify your script.

    Sound is coming in the raspberry through Bluetooth (A2DP) and out via the jack plug.
    Thank you for your help

    cheers

  4. @srvduplex you say you haven’t modified the script at all, but I think you may need to. In the script there is a min/max value:

    # vals from output of amixer cget numid=1
    min = 0
    max = 207
    

    which I got from the values output by using amixer cget numid=1.

    When you ran that same command, your min/max was shown as min=0,max=65536. So in the script, make the lines look like:

    min = 0
    max = 65536
    

    and see how that works out. You may also need to adjust the line:

    preVolume = volume = 100  # give it some volume to start with
    

    So that the 100 is around 3200 or something, given that your max is reported as 65536.

  5. Hello!

    Thank you for a very interesting site!

    I have the same question as klettervirus.

    I get a negative Volume for minimum volume (-10239) when I do “amixer cget numid=1″. I cannot call ” amixer -c 0 cset numid=1 -175″ with a negative number, for it gets misinterpreted…. I have no soundcard, but use regular output via build in sound: “cat /proc/asound/cards”
    gives
    ” 0 [ALSA ]: bcm2835_alsa – bcm2835 ALSA
    bcm2835 ALSA”

    How can I use the negative value for volume-min. so I can make the volume lower than now? Right now I can only go from 400 to 0, leaving a considerable amount of volume left…..

    Can you please help me?

    Best regards
    Johan

  6. Hi Johan, thanks for posting a comment.

    To be honest, it’s been a really long time since I’ve played around with this as the project I was going to use it on didn’t go anywhere, so this answer is just a guess…

    I had a look at the amixer man page, and it’s possible to give a percentage value, such as:

    amixer -c 2 cset numid=1 40%
    

    So instead of incrementing a numeric count based on min/max values from a call to amixer, you could try just going up and down from 0 to 100%, something like (the very, very untested code as I’m just winging it right now):

    
    from RPi import GPIO
    from time import sleep
    import subprocess
    
    clk = 5
    dt = 6
    btn = 26
    
    min = 0
    max = 100
    
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(clk, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(dt, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(btn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    
    isMuted = False
    preVolume = volume = 40
    clkLastState = GPIO.input(clk)
    btnLastState = GPIO.input(btn)
    
    def set_volume(level):
        subprocess.call(['amixer', '-q', '-c', '0', 'cset', 'numid=1', f'{level}%'])
    
    try:
        set_volume(volume)
        while True:
            btnPushed = GPIO.input(btn)
            if ((not btnLastState) and btnPushed):
                if isMuted:
                    volume = preVolume
                    isMuted = False
                    print "Unmuted"
                else:
                    preVolume = volume
                    volume = 0
                    isMuted = True
                    print "Muted"
                set_volume(volume)
                sleep(0.05)
            else:
                clkState = GPIO.input(clk)
                dtState = GPIO.input(dt)
                if clkState != clkLastState:
                    if isMuted:
                        isMuted = False
                        volume = 0
                    if dtState != clkState:
                        volume += 1
                        if volume > max:
                            volume = max
                    else:
                        volume -= 1
                        if volume < min:
                            volume = min
                    print(f"Volume at {volume}%")
                    set_volume(volume)
                clkLastState = clkState
            btnLastState = btnPushed
    finally:
        GPIO.cleanup()
    

    No idea if it'll work, and I cannot try it out as my pi and sound card are scattered around in a box or two somewhere - no idea where right now! 😀 But hopefully it'll point you in close enough to the right direction!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.