HOWTO - Soekris dam1021 DAC and MPD on Raspberry Pi

HOWTO: Soekris dam1021 DAC and MPD on Raspberry Pi

Introduction

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.

HOWTO

Needed Packages

  • 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.

Normunds Input/Switch Board Fixup

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

ALSA Dummy Module

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

listener.py script

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)

mpd.conf

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"
}