#! /usr/bin/perl -w # Music Player Daemon Joystick Interface # 2018 by L. Ross Raszewski # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. use Linux::Joystick; use Time::HiRes qw/time/; # Seek distance in seconds my $SEEKBACK = 10; my $SEEKFWD = 10; # Some configuration settings # Auto sleep mode my $TIMEOUT = 60; # MPD host. Change this to run the remote on a different box from the # music player my $HOST = 'localhost'; # Restrict max volume my $MAXVOL = 100; # Regex required on playlists my $plregex = qr/^DYLAN/; # The joystick my $js = new Linux::Joystick; # Last direction we detected motion in. Because we don't fire until # the joystick is released, we need to remember which way was pushed, # then honor that when the joystick position goes back to 0 my $lastdir = undef; my $lasttime = 0; # Mapping of buttons to events. This is based on an SNES two-axis # 8-button controller. # Negative numbers are used for the D-pad. my %events = ( -5 => \&prevPL, #UP -1 => \&nextPL, #DOWN -4 => \&prevSong, #LEFT -2 => \&nextSong, #RIGHT 0 => \&playPause, # A 1 => \&toBeginning, # B 2 => \&forwardSome, # X 3 => \&backwardSome, # Y 4 => \&volDown, # L 5 => \&volUp, # R 6 => \&stop, # SELECT 7 => \&play, # START ); # Current playlist. Used to cycle through them. my $playlist = "Unknown"; # Returns a list giving the previous and next entries in the # playlist. If the current playlist is not known, it returns the last # and first playlist alphabetically. sub getPlaylistPos { my $res = `mpc --host $HOST lsplaylists`; my $pos = -1; my @l = (); foreach (split /\n/, $res) { push @l, $_ if $_ =~ m/$plregex/; $pos = $#l if $playlist eq $_; } my $ni = $pos+1; $pos = 0 if $pos<0; my $pi = $pos-1; $pi = $#l if $pi < 0; $ni = 0 if $ni > $#l; return @l[$pi, $ni]; } # Switch to the previous playlist. When switching playlists, we have # to do 3 steps: clear the old playlist, load the new one, then start # playing. # Note that switching playlists implictly starts playing. I think this # makes sense for my application since it doesn't have a screen, so # otherwise, you'd just be blindly cycling not knowing what you're doing. sub prevPL { ($playlist, $_) = getPlaylistPos(); `logger "MPC: Switch to playlist $playlist"`; `mpc --host $HOST clear`; `mpc --host $HOST load "$playlist"`; `mpc --host $HOST play`; } # Switch to the next playlist sub nextPL { ($_, $playlist) = getPlaylistPos(); `logger "MPC: Switch to playlist $playlist"`; `mpc --host $HOST clear`; `mpc --host $HOST load "$playlist"`; `mpc --host $HOST play`; } # Switch to the previous song. Does nothing if we're stopped because # MPC does not actually return a playlist position when we're stopped # (though it does seem to remember one, weirdly.) sub prevSong { return if ($_[0]->{status} == 0); `mpc --host $HOST prev`; my $rv = getMPCStat(); `logger "MPC: Previous $_[0]->{playing} -> $rv->{playing}"`; } # Switch to the next song. sub nextSong { return if ($_[0]->{status} == 0); `mpc --host $HOST next`; my $rv = getMPCStat(); `logger "MPC: Next $_[0]->{playing} -> $rv->{playing}"`; } # Restart the current song if playing sub toBeginning { return if ($_[0]->{status} == 0); `logger "MPC: Restart $_[0]->{playing}"`; `mpc --host $HOST seek 0`; } # Seek bakward if playing sub backwardSome { return if ($_[0]->{status} == 0); `logger "MPC: Seek Back $_[0]->{playing}"`; `mpc --host $HOST seek -$SEEKBACK`; } # Seek forward if playing sub forwardSome { return if ($_[0]->{status} == 0); `logger "MPC: Seek Forward $_[0]->{playing}"`; `mpc --host $HOST seek +$SEEKFWD`; } # Play/Pause sub playPause { my $rv = shift; my $act = $rv->{status}==1 ? "Pause":"Play"; `logger "MPC: $act $rv->{playing}"`; `mpc --host $HOST toggle`; } # Turn volume down sub volDown { my $rv = shift; if (defined $rv->{volume} && $rv->{volume} > 0) { `logger "MPC: Volume down from $rv->{volume}"`; `mpc --host $HOST volume -5`; } } # Turn volume up sub volUp { my $rv = shift; if (defined $rv->{volume} && $rv->{volume} <= $MAXVOL-5) { `logger "MPC: Volume up from $rv->{volume}"`; `mpc --host $HOST volume +5`; } } # Start playing unconditionally sub play { `logger "MPC: Play $_[0]->{playing}"`; `mpc --host $HOST play`; } # Stop playing unconditionally (Loses position within song) sub stop { `logger "MPC: Stop"`; `mpc --host $HOST stop`; } # Ask MPD what it's doing and turn it into a hash. sub getMPCStat { my $status = `mpc --host $HOST`; my @lines = split /\n/, $status; my %rv = ( volume => undef, repeat => 0, random => 0, consume => 0, single => 0, playing => '(no song)', status => 0, pos => undef, plpos => undef, length => undef, ); foreach my $line (@lines) { if ($line =~ m/volume:\s*([^\s]+)\s+repeat:\s*(off|on)\s+random:\s*(off|on)\s+single:\s*(off|on)\s+consume:\s*(off|on)/) { $rv{volume} = $1 eq 'n/a' ? undef:$1; $rv{repeat} = $2 eq 'off' ? 0:1; $rv{random} = $3 eq 'off' ? 0:1; $rv{single} = $4 eq 'off' ? 0:1; $rv{consume} = $4 eq 'off' ? 0:1; $rv{volume} =~ s/%// if defined $rv{volume}; } elsif ($line =~ m/\[(playing|paused)\]\s+#(\d+)\/\d+\s+([\d+:]+)\/([\d+:]+)/) { $rv{status} = $1 eq 'playing' ? 1:2; $rv{pos} = $3; $rv{plpos} = $2; $rv{length} = $4; } else { chomp $line; $rv{playing} = $line; } } return { %rv} ; } # Main loop. while (1) { my $rin = ''; vec($rin, fileno($js->fileHandle), 1) = 1; my $nfound = select($rin, undef, undef, $TIMEOUT*60); my $status = getMPCStat(); if ($nfound) { # A joystick button was pressed my $event = $js->nextEvent(); my $action = undef; if ($event->isButton) { # We only react when the button is released. if (!$event->buttonDown) { $action = $event->button(); } $lastdir = undef; } elsif ($event->isAxis) { # To react when the dpad is released, we are looking for # it to go to Zero, but the action we take is based on # what value we got in the earlier event when the dpad was pressed if ($event->axisValue) { # Math skullduggery to produce a set of numbers # disjoint from the buttons. $lastdir = ($event->axisValue/32767)*($event->axis+1)-3; $lasttime = time; # We ignore axis events when the time between down and # up is less than a tenth of a second because there # seems to be some jitter on the D-pad. I don't know # if this is just my cheap D-pad, or if it's a common # issue that you always have to account for } else { $action = $lastdir if $lastdir && !$event->axisValue && time-$lasttime > 0.1; $lastdir = undef; } } # Run the proper code for the button that was pressed. $events{$action}->($status) if (defined $action && defined $events{$action}) } else { # Select timed out - no button was pressed within the # timeout. Pause playback on the assumption that the user has # fallen asleep. if ($status->{status}==1) { `logger "MPC: Sleep timeout"`; `mpc --host $HOST pause`; } } }