RFID interface to the Music Player Daemon

Swipe a tag to change the music. It's easier than you think ;)

Introduction

BLOG post in progress. It fit's here very well as introduction.

Motivation

I'm using the music player daemon as an easy to use home-stereo. MPD is a very mature and stable software that /plays/ music, has sufficient features and offers control-clients for many platforms.

What I'm unhappy about is that all user-interfaces for music suck. well some suck less and others are just technically pragmatic. - No use in complaining; I've been working on solutions over the past years and certainly contributed to the mess.

With industrialization and machinery that could not comprehend semantics, the western culture also started to loose a feeling for context. Music transformed from Symphonies to Albums and is these days sold per Track; quite often cited out of context. - Well it can't get much worse, beyond web-2.0 there's hope to get some sense back. - The semantic web won't change the world, but people using it will.

So how does this translate to music? Let's first separate inter-active (playing an instrument, composing music or compositing sound) and active (listening to music, perceiving sound). Here I'm only concerned with the latter and state that it should be as unobtrusive as possible. The user (aka audiophile) should not be concerned with anything but the listening experience; He/she only chooses the environment: fi an opera-hall, a windy boulevard or a sound-studio. Respectively the conductor, wind or operator take care of the rest.

It's freedom of the user to choose between being active or passive to the the music. But currently there's not even a choice: cover-art adds little extra these days; and you-tube is the new pop. Nevertheless there's web-radios and endeavours by enthusiastic people who may end up educating more people than they dream of.

Remote-control the remote-control

I don't need to explain /play/, /next/, /pause/ interfaces so that's a good ;) - playlists representing albums or music collections are also on this list. We can safely add tangible user interfaces, and I'll add MPD to start prototyping…

The idea is to remote control a music player with sensors such as RFID, motion or brightness detectors, not forgetting classical buttons; linked with power of the semantic web.

MPD already offers many user-interfaces from commandline to GUI, joystick and LIRC. While it's not the most flexible and fancy player available (eg. lack of media collection management, connection-encryption, push based API,..) it's good enough for the purpose.

Cutting it down to day-projects:

mprfid is a perl script that allows to flexibly assign RFID tags (read from stdin) to actions controlling a MPD (using Audio::MPD).

Framework

work in progress

Tag-IDs can either be hardcoded or linked using a wiki-page that specifies the command to execute for the given tag. The command set are in XML DokuWiki-extension tag syntax, defined as follows:

<mpd control>(play|pause|stop)</mpd>
<mpd volume>[+-] percent</mpd>
<mpd play [OPTIONS]*>folder/file.mp3 [file/folder.mp3]*</mpd>
<mpd song [OPTIONS]*>name [name]* [/pattern/]* [!/pattern/]*</mpd>
  • control directly controls MPD playback.
  • volume allows to change the volume in steps from 0 to 100. prefixing + or - to the integer performs adjustments relative to the current value.
  • play will clear the playlist, enqueue all space, tab or new-line separated files (relative to MPDs root-folder) to the playlist and start playing.
  • song searches space, tab or new-line separated file or path names in the MPD database; afterwards it optionally filters the search-results with regular-expression patterns.

OPTIONS

  • noplay - don't start playing automatically
  • rand or random - shuffle the playlist after adding files

example commands:

<mpd control>play</mpd>
<mpd ctrl>pause</mpd> 
<mpd control>stop</mpd>
<mpd volume>-5</mpd>
<mpd volume>80</mpd>
<mpd song rand>Neil_Young !/full_album/ /black/ </mpd> 

Software

..just swiped the //Neil Young// Tag.

Besides MPD, you'll need perl, libaudio-mpd-perl (Audio::MPD) for the conductor and libsdl-perl (SDL) for the display screen.

  • mprfid is a perl reads from stdin and sends commands to MPD.
  • perlsdl - excuse the lame name - is a dumb BIG-FONT MPD monitor-window.

Both scripts use the MPD_HOST environment variable or alternatively use the first argument as such.

The software is available from the source repository. You can check out the latest version with direct links:



usage:

# example using ACR/tikitag readers
acr122 | mprfid.pl
# or 
export MPD_HOST="user@host"; acr122 | while [ 1 ]; do mprfid.pl ; done
#
# for sonmirco/mifare readers
tagid.pl | mprfid.pl "password@mpd-host"

# launch the GUI:
./perlsdl.pl "password@mpd-host"
# or
export MPD_HOST="password@mpd-hostname" 
perlsdl.pl


#!/usr/bin/perl
# usage:
#   export MPD_HOST="user@host"
#   acr122 | mprfid.pl
#     or
#  ./tagid.pl | mprfid.pl
 
use warnings;
use strict;
use POSIX 'mktime';
use Audio::MPD q{0.19.0};
use Switch;
use Time::HiRes qw(gettimeofday);
 
##############################################################################
# CONFIGURATION
#
$_=1; # DEBUG
my $cfg_crop = 0;
my $cfg_link_url="http://mir.dnsalias.com/wiki/rfid/";
my $cfg_base_url="http://mir.dnsalias.com/_export/raw/wiki/rfid/";
my $cfg_pext_url="";
my $cfg_msgfile="/tmp/mpdinfo.txt";
 
 
##############################################################################
# SETUP
 
if (@ARGV) {
  $ENV{MPD_HOST}=shift;
}
my $mpd=Audio::MPD->new(conntype => $REUSE);
my $prev="";  # previously read rfid tag (main)
my $prevts=0; # rfid debounce time (sub debounce)
my $msgtme=-1;
my $msgtimeout=5;
 
sub mpc_conn() {
  return;
  # FIXME: does not neccesarily work: MPD.pm calls die() !
  if (!$mpd->ping()) { return; }
  print "NOTE: reconnecting\n";
  $mpd=Audio::MPD->new(conntype => $REUSE);
}
 
##############################################################################
# MPD/MPC callbacks
#
sub mpc_play_or_next() {
  mpc_conn();
  #use Data::Dumper;
  #print Dumper($mpd->status());
  if ($mpd->status()->{'state'} eq "play") {
    print "next\n";
    $mpd->next();
  } else {
    print "play\n";
    $mpd->play();
  }
}
sub mpc_play()    { mpc_conn(); $mpd->play;}
sub mpc_prev()    { mpc_conn(); $mpd->prev();}
sub mpc_next()    { mpc_conn(); $mpd->next();}
sub mpc_stop()    { mpc_conn(); $mpd->stop;}
sub mpc_pause()   { mpc_conn(); $mpd->pause;}
sub mpc_volume($) { mpc_conn(); $mpd->volume(shift); }
sub mpc_shuffle() { mpc_conn(); $mpd->playlist->shuffle(); }
 
sub mpc_collection(@) {
  my %sl;
  my @filter;
  my @filter_n;
  mpc_conn();
  foreach (@_) {
    next if /^$/;
    if (/^!\/(.*)\/$/) {
      push @filter_n, $1;
      next;
    }
    if (/^\/(.*)\/$/) {
      push @filter, $1;
      next;
    }
    #print "search $_\n"; # DEBUG
    foreach ($mpd->collection->all_items_simple($_)) {
      next unless $_->{'file'};
      $sl{$_->{'file'}}=1;
    }
  }
  my @res=keys %sl;
  my %slx;
  my $filtered=0;
  #print "filter in :\n"; print Dumper(@filter); # DEBUG
  foreach (@filter) {
    next if /^$/;
    my $pattern = $_;
    foreach (grep {/$pattern/} keys %sl){
      $slx{$_}=1;
    }
    $filtered=1;
  }
  if ($filtered) {
    @res=keys %slx;
  }
  #print "filter out:\n"; print Dumper(@filter_n); # DEBUG
  foreach (@filter_n) {
    next if /^$/;
    my $pattern = $_;
    @res = grep {!/$pattern/} @res;
  }
  return @res;
}
 
sub mpc_song(@;) {
  mpc_conn();
  if ($cfg_crop) {
    $mpd->playlist->crop();
  } else {
    $mpd->playlist->clear;
  }
  $mpd->playlist->add(@_);
}
 
sub mpc_test($) {
  mpc_conn();
  use Data::Dumper;
  #my @playlists=$mpd->collection->all_playlists;
  print Dumper($mpd->playlist->as_items());
  #print Dumper(@playlists);
  #print Dumper($playlists[rand @playlists]);
  #$mpd->playlist->load($playlists[rand @playlists]);
  #$mpd->play;
}
 
sub mpc_testadd(@) {
  mpc_conn();
  #return map $_->{'file'}, $mpd->collection->items_in_dir(shift);
  #use Data::Dumper;
  #print Dumper($mpd->collection->items_in_dir(shift));
  my %sl;
  foreach (@_) {
    next if /^$/;
    if ($_ =~ m/\.m3u$/) {
      # TODO
      # get paths (abs-file and mpd-root)
      # add files in m3u file order (don't use hash?!)
      next;
    }
    foreach ($mpd->collection->all_items_simple($_)) {
      next unless $_->{'file'};
      $sl{$_->{'file'}}=1;
    }
  }
  return keys %sl;
}
 
##############################################################################
# subroutines
#
# low pass filter action response - debounce buttons
sub debounce($;$;$;) {
  my $cur = shift;
  my $prv = shift;
  my $tim = shift;
 
  if ($tim == -1) { return 1; }
  if ($tim == 0 and $cur eq $prv) { return 0; }
 
  if (!($cur eq $prv)) {
    $prevts = gettimeofday;
    #print "DEBOUNCE TIME: ".$prevts."\n"; # DEBUG
    return 1;
  }
  if (($tim > (gettimeofday - $prevts)) and $cur eq $prv) {
    #print "DEBOUNCE CNT: ".(gettimeofday - $prevts)."\n"; # DEBUG
    return 0;
  }
  $prevts = gettimeofday;
  return 1;
}
 
sub msg_set($;) {
  open (MYMSG, '>'.$cfg_msgfile);
  print MYMSG $_[0];
  close (MYMSG);
  $msgtme=time;
  $msgtimeout=5;
}
 
sub msg_clear() {
  $msgtme=-1;
  unlink($cfg_msgfile);
}
 
# execute optional commands
sub wikiopt(@;) {
  my $flags=0;
  foreach (@_) {
    next if /^$/;
    #print "cmd-opt: '".$_."'\n";  # DEBUG
    switch ($_) {
      case /^rand/   { mpc_shuffle();}
      case /^noplay/ { $flags|=1;}
      case /^.*$/    { print "WARNING: unknown option: '".$_."'\n";}
    }
  }
  if (($flags&1) == 0) { mpc_play();}
}
 
# parse <mpd[:COMMAND]>ARG</mpd> XML
sub wikicmd($;$;) {
  my $cmd = shift;
  my $arg = shift;
  my $rv = 0;
  chomp $cmd;
  chomp $arg;
 
  my @opt = split /[: ]+/, $cmd;
  $arg =~ s/[\n\r\t\f\a\e]+/ /g;
 
  #use Data::Dumper;
  #print "-CMD-<-\n"; print Dumper(@opt); print Dumper($arg); print "->-CMD-\n";
 
  switch ($opt[0]) {
    case /(ctrl|control|command)/ {
      #print "EXE command: '$arg'\n"; # DEBUG
      switch ($arg) {
        case "stop"  { mpc_stop();  $rv|=1;}
        case "play"  { mpc_play();  $rv|=1;}
        case "pause" { mpc_pause(); $rv|=1;}
        case "prev"  { mpc_prev();  $rv|=1;}
        case "next"  { mpc_next();  $rv|=1;}
        case "play_or_next"  { mpc_play_or_next();  $rv|=1;}
      }
    }
    case /^vol/ {
      if    ($arg =~m/^\+/) { mpc_volume("+".int($arg)); $rv|=1;}
      elsif ($arg < 0 )     { mpc_volume(int($arg)); $rv|=1;}
      else                  { mpc_volume(int($arg)); $rv|=1;}
    }
    case /^play/ {
      #print "EXE play-list: '$arg'\n"; # DEBUG
      my @songs = split (/[\s;,]+/ ,$arg);
      mpc_song(@songs);
      $opt[0] = "";
      wikiopt(@opt);
      $rv|=1;
    }
    case /^song/ {
      #print "EXE song-search: '$arg'\n"; # DEBUG
      my @songs = split (/[\s;,]+/ ,$arg);
      mpc_song(mpc_collection(@songs));
      $opt[0] = "";
      wikiopt(@opt);
      $rv|=1;
    }
    case /^.*$/ {
      print "WARNING: unknown command:\n";
      use Data::Dumper;
      print "-CMD-<-\n"; print Dumper(@opt); print Dumper($arg); print "->-CMD-\n";
    }
  }
  return $rv;
}
 
# resolve RFID - lookup MPD/MPC command via curl:
# http://mir.dnsalias.com/wiki/rfid/dae037ef?do=export_raw
sub wikitag($) {
  my $rfid=shift;
  if ($cfg_base_url eq "") {return 0;}
  print 'RPC - looking up: '.$cfg_link_url.$rfid."\n";
  msg_set('RFID: '.$rfid.' (looking up)');
  my $res=`curl -s "$cfg_base_url$rfid$cfg_pext_url"`;
  #print ':::'.$res.":::\n";  # DEBUG
  msg_set('RFID: '.$rfid);
  if ($res =~ m/<mpd[: ]?([^>]*)>(.*)<\/mpd>/s ) {
    return wikicmd($1,$2);
  }
  return 0;
}
 
##############################################################################
#MAIN LOOP
#
 
sub handle_rfid($;) {
  my $rfid= lc shift;
  chomp $rfid;
  return if $rfid =~ m/^#/;
  $rfid =~ s/://g;
  print "rfid: '".$rfid."'\n"; # DEBUG
 
  switch ($rfid) {
    #  -=-=-=-=- HARDCODED TAGS -=-=-=-=-
    case "044260b9212580" { if (debounce($rfid,$prev,0.5)) { print $rfid." pause\n";
      msg_set('RFID: '.$rfid.' (pause)');
      mpc_pause();
    }}
    case "dae037ef" { if (debounce($rfid,$prev,3)) { print $rfid." play/next\n";
      msg_set('RFID: '.$rfid.' (play/next)');
      mpc_play_or_next();
    }}
    case "7e4e605c" { if (debounce($rfid,$prev,1)) { print $rfid." prev\n";
      msg_set('RFID: '.$rfid.' (prev)');
      mpc_play();
      mpc_prev();
    }}
    case "7aac3aef" { if (debounce($rfid,$prev,1)) { print $rfid." stop\n";
      msg_set('RFID: '.$rfid.' (stop)');
      mpc_stop();
    }}
    case "fad937ef" { if (debounce($rfid,$prev,.2)) { print $rfid." vol- up\n";
      msg_set('RFID: '.$rfid.' (vol-up)');
      mpc_volume("+2");
    }}
    case "3e405e5c" { if (debounce($rfid,$prev,.2)) { print $rfid." vol-down\n";
      msg_set('RFID: '.$rfid.' (vol-down)');
      mpc_volume("-2");
    }}
#   case "de4a625c" { if (debounce($rfid,$prev,5)) { print $rfid."\n";
#     mpc_song(mpc_collection("Neil_Young"));
#     mpc_shuffle();
#     mpc_song("Nat_King_Cole/Moonglow_1/crazy_rhythm.mp3");
#     mpc_play();
#   }}
    #  -=-=-=-=- WIKI TAGS -=-=-=-=-
    case /^$/ {last;}
    case /^.*$/ {
      if (debounce($rfid,$prev,5)) {
        if (!wikitag($rfid)) {
          print 'unknown ID: '.$cfg_link_url.$rfid."\n";
          msg_set('unknown ID: '.$rfid); $msgtimeout=120;
        }
      }
    }
  }
  $prev=$rfid;
}
 
#while (<STDIN>) {
#  handle_rfid($_);
#}
 
use IO::Select;
my $select= IO::Select->new();
$select->add(\*STDIN);
msg_clear();
 
while (1) {
  $mpd->ping();
  if ($msgtme >0 && (($msgtme+$msgtimeout) < time)) {
    msg_clear();
  }
  if ($select->can_read($msgtme>0?2:30)) {
    my $line;
    sysread(STDIN, $line,4096);
    foreach (split /[\n\r]+/, $line) {
      handle_rfid($_);
    }
  }
}
 
print "mprfid exiting.\n";
exit(0);

view mprfid.pl source

#!/usr/bin/env perl
#
my $win_width = 640;
my $win_height = 480;
my $font_size=16;
my $line_height=20;
#
use Audio::MPD q{0.19.0};
use SDL;
use SDL::App;
use SDL::Event;
use SDL::Tool::Font;
use SDL::Surface;
use SDL::Color;
use SDL::Cursor;
use SDL::Rect;
use Time::HiRes qw(usleep);
use Time::HiRes qw(gettimeofday);
$SDL::DEBUG =1;
use Data::Dumper;
 
use warnings;
use strict;
 
 
##############################################################################
#MPD Setup and functions
 
if (@ARGV) {
  $ENV{MPD_HOST}=shift;
}
my $mpd=Audio::MPD->new(conntype => $REUSE);
 
my $cur_state; # remember current state
my @plcache;   # playlist cache
my $plcachevs = 0; # cached playlist version
my $plcacheid = -1; # cached currently playing index in playlist
 
sub mpd_status() {
  my $s=$mpd->status();
  my $c=$mpd->current()||{'file'=>""};
  my $song=1+$s->{'song'}||0;
  my $perc=0;
  $cur_state=$s->{'state'};
  if ($s->{'time'}->{'seconds_total'} > $s->{'time'}->{'seconds_sofar'}) {
    $perc =int($s->{'time'}->{'seconds_sofar'}*100.0/$s->{'time'}->{'seconds_total'});
  }
  if ($perc > 0) {
    progressbar($perc);
  }
  return (
          "\001  [".$s->{'state'}."] #".$song."/".$s->{'playlistlength'}."  -  ".
               $s->{'time'}->{'sofar'}."/".$s->{'time'}->{'total'}, #."     (".$perc."%)",
        "  volume: ".$s->{'volume'}."%   repeat: ".($s->{'repeat'}?"on":"off")."   random: ".($s->{'random'}?"on":"off"),
        "\003 ",
          map "            ~  ".$_, split(/\//, $c->{'file'}),
         );
}
 
sub mpd_playlist() {
  # TODO: cache playlist ?!
  my $cutout = int($win_height/$line_height)-12;
  if ($cutout < 3 ) { $cutout=3; }
  my $s=$mpd->status();
  #print Dumper($s);
  my $id=$s->{'song'} || 0;
  my $version=$s->{'playlist'} || -1;
  if ($id == $plcacheid && $plcachevs == $version ) {
    # print " !!! using playlist cache\n";  # DEBUG
    return @plcache;
  }
  my @playlist = map 1+$_->{'pos'}.") ".$_->{'file'}, $mpd->playlist->as_items();
  my $start = $id-1;
  my @rv;
  while ($start > 0 && ($start+$cutout > $#playlist)) { $start--;}
  my $i=$start<0?-1:$start-1;
  while ($i++ < $start+$cutout) {
    next unless $playlist[$i];
    if ($i == $id) {
      push @rv, "\002".$playlist[$i];
    } else {
      push @rv, "\003".$playlist[$i];
    }
  }
  @plcache=@rv;
  $plcachevs = $version;
  $plcacheid=$id;
  #print "CACHE DEBUG $plcachevs\n";
  return @rv;
}
 
sub mpd_info() {
  my @info;
  if ( -e "/tmp/mpdinfo.txt" ) {
    @info = `cat /tmp/mpdinfo.txt`;
    $info[0]="\001 ".$info[0];
  }
  return @info;
}
 
##############################################################################
# SDL SETUP and subroutines
#
my $app = SDL::App->new(
# -flags=>SDL_SWSURFACE,
  -width  => $win_width,
  -height => $win_height,
  -depth  => 8,
);
 
my $background = $SDL::Color::black;
 
my $font_mono = new SDL::Tool::Font
    -ttfont => "/usr/share/fonts/truetype/freefont/FreeMono.ttf",
    -bold => 1,
    -size => $font_size+2,
    -fg => $SDL::Color::white,
    -bg => $background;
 
my $font_sans = new SDL::Tool::Font
    -ttfont => "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
    -size => $font_size,
    -fg => $SDL::Color::white,
    -bg => $background;
 
my $font_mark = new SDL::Tool::Font
    -ttfont => "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
    -size => $font_size,
    -fg => $SDL::Color::blue,
    -bg => $background;
 
 
sub sdlprint($;$;@){
  my $x=shift;
  my $y=shift;
  my $font=$font_sans;
  foreach (@_) {
    next unless $_;
    next if /^$/;
    chomp;
    my $line=$_;
    #$font=$font_sans;
    if (/^\001/) { $line =~ s/\001//; $font=$font_mono; }
    if (/^\002/) { $line =~ s/\002//; $font=$font_mark; }
    if (/^\003/) { $line =~ s/\003//; $font=$font_sans; }
    $font->print($app,$x,$y,$line);
    $y+=$line_height;
  }
}
 
sub progressbar($;) {
  my $perc = shift;
  my $maxwidth = 220;
  my $width = int( $perc*$maxwidth/100.0 );
  my ($x, $y ,$h) = ($win_width-250, 12, $font_size+2);
  my $rect = SDL::Rect->new(
    -height => $h+2,
    -width  => $maxwidth+4,
    -x      => $x,
    -y      => $y,
  );
  $app->fill( $rect, $SDL::Color::blue );
#  $app->update($rect);
  $rect = SDL::Rect->new(
    -height => $h,
    -width  => $width,
    -x      => $x+2,
    -y      => $y+1,
  );
  $app->fill( $rect, $SDL::Color::black );
  sdlprint($x-(3*($font_size+2)/4)+($maxwidth/2)-($perc>=10?(3*($font_size+2)/9):0),$y-2, "\001(".$perc."%)");
}
 
sub myupdate() {
  $app->fill(NULL, $background);
  sdlprint(10,10, mpd_status());
  my @pl = mpd_playlist();
  sdlprint(10,int(7.5*$line_height), @pl);
  sdlprint(0,$win_height-($line_height*2), mpd_info());
  $app->flip();
}
 
##############################################################################
# MAIN
 
SDL::Cursor::show(undef,undef); # hide mouse pointer
my $event = new SDL::Event();
my $timeout=0;
while (1) {
    $event->pump;
    if ($event->poll) {
      my $etype=$event->type;
      last if ($etype eq SDL_QUIT );
      last if (SDL::GetKeyState(SDLK_ESCAPE));
    }
    if (gettimeofday > $timeout ) {
      $timeout=gettimeofday;
      if ($cur_state eq "play") {$timeout+=.25;} else {$timeout+=1.0;}
      myupdate();
    }
    usleep (50000);
}
print "exit.\n";
exit(0);

view perlsdl.pl source

References

 
wiki/rfid/mprfid.txt · Last modified: 23.12.2011 21:26 (external edit)