See the related article, ALSA Dummy Driver Events as MPD Hooks for additional context.
This article describes how to use a Soekris dam1021 DAC with Volumio. In particular, it allows you to use MPD's volume control to use the dam1021's built-in hardware volume control. It does so by adding a "hook" to the ALSA dummy mixer device that actually sends volume control commands to the DAC.
To give credit where credit is due, this is basically a poor man's version of Muffinman's Synchronator. From Synchronator's README:
Synchronator brings bit perfect volume control to Hi-Fi systems
with Linux as source.
This enables control of your Hi-Fi amplifier volume level for
Airplay, DLNA, OpenHome, MPD, Squeezelite, and Roon a.o.
...In Linux it is not uncommon that audio applications, such as
MPD and Shairport, allow audio data and mixer/volume data to be
send to different (audio) devices. By sending mixer data to a
dummy/virtual soundcard, volume control can be enabled without
touching the audio data. Synchronator in turn can synchronise
that volume level with any Hi-Fi system/amplifier that can be
externally controlled (RS232/I2C/TCP/IR). In addition, changes
in volume level at the amplifier side are synced back.
Synchronator is indeed a more robust, full-featured version of what is being done here. You could presumably install, build and configure Synchronator to accomplish the same goal, and learn a tool that might be useful in other applications as well. The approach described here uses the same exact concept, but it is simpler since it is designed to be very specific, i.e. link the dam1021's builtin hardware volume control to MPD.
NOTE: I originally had this set up on Archphile, which is now defunct. I haven't had time to re-create the setup under a new distribution. So these instructions haven't been well-tested. But at a minimum they hopefully provide enough foundational info.
Python packages: serial, xmodem, select.
Note: the listener.py script is written for Python version 2.
I expect it could be easily ported to Python3, but it has not even been attempted at this point. Your distribution may or may not provide native packages for these modules. But presumably all distributions support pip, which can be used to easily install Python packages. Examples:
pacman -Sy python2-pip apt-get install python-pip
pyalsaaudio: there seems to be at least three different ALSA wrappers for Python. This is the only one that has been tested. Most distributions probably don't package pyalsaaudio natively. With a working pip, you should be able to do:
pip install pyalsaaudio
You may have to use "pip2" instead of "pip" if your distribution is Python3-native (e.g. Arch).
Note: some examples of the wrong ALSA packages include python-pyalsa, python-alsaaudio.
python dam1021 library: Requires serial and xmodem Python packages described above. You can install via pip or also from source:
git clone https://github.com/fortaa/dam1021 cd dam1021 python setup.py install
wiringpi: only necessary if using the Normunds switch board. But a very handy general tool for playing with GPIO pins on the Raspberry Pi.
If you are using the Normunds Input/Switch Board, then you will need to set the I2S select pin appropriately. I did this with the gpio utility from the wiringpi_ package. I wrapped it up in a little script I call fixup.sh, see below. (Note pin15 == GPIO 22 == wiringPi pin 3, see explanation of Raspberry Pi GIO pin numbering.)
#!/bin/bash
GPIO=/usr/bin/gpio
$GPIO mode 3 out
$GPIO mode 3 down
Might need to install ALSA packages, depending on distribution: alsa-utils alsa-lib alsa-firmware
install alsa dummy module: modrobe snd_dummy auto-load at boot, add "snd-dummy" to: /etc/modules-load.d/modules.conf
Download here: listener.py, or copy and paste:
#!/usr/bin/python2
import alsaaudio as alsa
import select, dam1021
##############################################################################
# * dam1021 true range: [-80,10]
# -> treat as [-80,0]
# * alsa range is [-50,100]
# -> treat as [0,100] because this is what MPD and alsamixer
# produce when changing volume (FIXME: why the discrepancy?)
# * limit dam1021 volume to -30 in case of mistakes, save
# ears/speakers
def vol_convert_dam1021(alsavol):
LIMIT = -10
pct = float(alsavol)/100.0
damvol = int(pct*80.0+0.5) - 80
if (damvol > LIMIT): damvol = LIMIT
return damvol
##############################################################################
if __name__ == '__main__':
conn = dam1021.Connection('/dev/ttyAMA0')
mixer = alsa.Mixer(device='hw:Dummy', control='Master')
poll = select.poll()
descriptors = mixer.polldescriptors()
poll.register(descriptors[0][0])
print 'begin poll...'
while True:
events = poll.poll()
mixer.handleevents()
print ' event:'
for e in events:
fd = e[0]
event = e[1]
alsavol = mixer.getvolume()
damvol = vol_convert_dam1021(alsavol[0])
print ' fd=%d event=%d alsavol=%s damvol=%d' % \
(fd, event, alsavol, damvol)
conn.set_current_volume_level(damvol)
You will need something like this in /etc/mpd.conf::
audio_output {
enabled "yes"
type "alsa"
name "MY DAC"
device "hw:1,0"
format "*:32:*"
auto_resample "no"
auto_channels "no"
auto_format "no"
dop "no" # default yes
mixer_type "hardware"
mixer_device "hw:Dummy"
mixer_control "Master"
}