Kristian Lyngstøl's Blog

Hacking my AV Receiver

Posted on 2012-05-02

I'm a "HiFi-idiot". I don't believe $400 power cables give me better sound, but I'm not that far off either.

So a while back I bought a new AV receiver, a Denon 4311 to be precise, and it came with a few nifty hack-worthy features. The biggest one being the serial interface… which is also available over regular TCP/IP using a regular Ethernet connection.

The interface is meant for systems integration. It does offer a simple web interface that allows me to do most basic tasks too, but what's really interesting is the "Serial" interface. You talk to the receiver using a bastardized telnet-interface. It says TCP port 23, but don't expect telnet to "just work".

Denon kindly offers a full documentation of the interface, and it gives you all the features of the amplifier, from basic volume control to complete control over the iPod docking, including navigation of said iPod. I just HAD to do something with it.

The first thing I did was of course to write a munin plugin to graph the volume. This is an old (obviously out-dated) example:

Munin graph of volume

(main reason it's out of date: I've been fiddling with the munin install for a while and never bothered fixing it).

The code for that is available on Munin exchange, or my github page.

However, the Denon interface has a major drawback: It can only handle one connection at any given time. This means that if I'm doing something else with it, like adjusting the volume, the munin plugin wont execute. There are other problems too: If you try to bind "Volume up" to a key that spews the right command at the interface, it'll fail because chances are you'll want to adjust the volume for more than 0.5dBa at a time.

So I wrote a tiny little perl daemon:

#!/usr/bin/perl -w
# vold.pl, Volume control "daemon" for Denon x311 AVR
# Copyright (C) 2011-2012 Kristian Lyngstol <kristian@bohemians.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

# vold.pl talks to a denon x311 AV receiver over TCP/IP and offers a simple
# interface you can program against. It also talks to Spotify over D-Bus
# and hijacks the 'sleep'-function on the receiver, allowing you to use
# your regular remote control to pause Spotify and skip forward. If you
# want to use this, I suggest you read the code to hardcode some of this to
# your own needs.

use strict;
use IO::Socket;
use IO::Select;
use IO::Socket::INET6;

# The receiver
my $remote;

# The select() struct
my $sel;

# Read from the receiver. Assume unmuted at start-up (worst case scenario:
# Hit mute twice to unmute so vold catches the MUON in return).
my $mute = 0;

# No volume at start-up.
my $vol = "NA";

# for readin' stuff
my $line;

# listen-socket.
my $listen;

my $target = $ARGV[0];

if (!defined($target)) {
        print "Usage: vold.pl <IP of denon receiver>\n";
        exit(1);
}
# (Re-)connect to the receiver and re-add it to the select-queue.
sub recon {
        if (defined($remote)) {
        $sel->remove($remote);
        }
        $remote = IO::Socket::INET6->new(Proto => "tcp", PeerAddr => $target, PeerPort => "23", Timeout => 5,) or die "cannot connect to avr";
        $sel->add($remote);
        print "Re-opened connection to AVR\n";
}

# Handle data from the receiver, possibly reconnecting if needed.
sub handle_remote {
        local $/ = "\r";
        if (!$remote->connected) {
                recon();
                return;
        }

        $line = <$remote>;
        if ($line =~ m/MV[0-9][0-9][0-9]?/) {
                $vol = $line;
                $vol =~ s/[^0-9]//g;
                if (length($vol)>2) {
                        $vol =~ s/[0-9]$//;
                }
                if ($vol == "99") {
                        $vol = "0";
                } else {
                        $vol -= 1;
                }
        } elsif ($line =~ m/MUON/) {
                my $blah = `dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous`;
                $mute = 1;
        } elsif ($line =~ m/MUOFF/) {
                $mute = 0;
                my $blah = `dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play`;
        } elsif ($line =~ m/SLP[0-9]/) {
                my $blah = `dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next`;
                print $remote "SLPOFF\r";
        }
}

# Close a peer and remove it from the select queue
sub go_away {
        my $fh = $_[0];
        if (defined $fh) {
                $sel->remove($fh);
                $fh->close;
        }
        return;
}

# Handle client-data, defaulting to getting rid of it.
sub handle_client {
        my $fh = $_[0];
        if (!$fh->connected) {
                go_away($fh);
                return;
        }
        $line = <$fh>;
        if (!defined $line) {
                go_away($fh);
                return;
        }
        $line =~ s/[^a-zA-Z0-9]//g;
        if ($line =~ m/UP/) {
                print $remote "MVUP\r";
        } elsif ($line =~ m/DOWN/) {
                print $remote "MVDOWN\r";
        } elsif ($line =~ m/VOL/) {
                print $fh "". $vol . "\n";
        } elsif ($line =~ m/PC/) {
                print $remote "SIDVR\r";
        } elsif ($line =~ m/MUTE/) {
                if ($mute == 0) {
                        print $remote "MUON\r";
                } else {
                        print $remote "MUOFF\r";
                }
        } else {
                print $fh "What you say?\n";
                print $fh "Use: DOWN, UP, MUTE, PC or VOL\n";
                go_away($fh);
        }
}

$listen = new IO::Socket::INET6(Listen => 1, LocalPort => 1337, Timeout=>0,ReuseAddr =>1) or die "WHAT?";
$sel = new IO::Select( $listen );
recon();

# Let the games begin.
# Short: Both accepts new connections on $listen and adds them to $sel,
# reads client-connections and reads the denon-receiver, hopefully catching
# disconnects.
while(my @ready = $sel->can_read) {
        if (!defined $remote || !$remote->connected) {
                recon();
        }
        foreach my $fh (@ready) {
                if($fh == $listen) {
                        # Create a new socket
                        my $new = $listen->accept;
                        $sel->add($new);
                } elsif ($fh == $remote) {
                        handle_remote();
                } else {
                        handle_client($fh);
                }
        }
}

(let me know if it's interesting and I'll move it to GitHub)

The features are simple: vold.pl opens a single connection to the AVR and forwards a handful commands, like UP/DOWN/MUTE and allows switching to the "PC" input, which I've got configured as DVR on the receiver.

It also taps into the sleep-function, which I never use. Why? Because sleep is the only button on the remote control which:

  • Doesn't do something I use
  • Sends SOMETHING over the telnet interface
  • Doesn't change state of the receiver immediately

I also hooked into mute. If I hit mute on my receiver, it not only mutes, but it pauses Spotify.

If I hit sleep twice (The first hit just shows the sleep timer, which is off), it will skip forward to the next song, then reset the sleep timer. If I hit sleep three times, it will skip two songs and reset etc.

It's a fairly fugly interface, but fun. The ugliest part of the interface is perhaps that it uses carriage returns and goes rather bonkers if you try to send a newline. This makes it impossible to use the interface by just telneting directly to it (unless you find some knob to make telnet only send carriage returns AND behave generally nice. Keep in mind it also doesn't SEND any line feeds, so your terminal will keep overwriting the old text.)