1# Copyright (C) 2005-2013 Quentin Sculo <squentin@free.fr> 2# 3# This file is part of Gmusicbrowser. 4# Gmusicbrowser is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License version 3, as 6# published by the Free Software Foundation 7 8package Play_123; 9use strict; 10use warnings; 11use IO::Handle; 12 13use POSIX ':sys_wait_h'; #for WNOHANG in waitpid 14#use IPC::Open3; #for open3 to read STDERR from ogg123 / mpg321 in Play function 15 16#$SIG{CHLD} = 'IGNORE'; # to make sure there are no zombies #cause crash after displaying a file dialog and then runnning an external command with mandriva's gtk2 17#$SIG{CHLD} = sub { while (waitpid(-1, WNOHANG)>0) {} }; 18 19my (@cmd_and_args,$file,$ChildPID,$WatchTag,$WatchTag2,$OUTPUTfh,@pidToKill); 20my ($Paused,$SkipTo); 21my ($CMDfh,$RemoteMode); 22my $alsa09; 23my $Error; 24our %Commands= 25( mpg321 => {type => 'mp3', devices => 'oss alsa esd arts sun', cmdline => \&mpg321_cmdline, }, 26 ogg123 => {type => 'oga flac', devices => 'pulse alsa arts esd oss', cmdline => \&ogg123_cmdline, 27 }, #FIXME could check if flac codec is available 28 mpg123 => 29 { type => 'mp3', devices => sub { return grep $_ ne 'dummy', map m/^(\w+)\s+output*/g, qx/mpg123 --list-modules/; }, 30 remote => { PAUSE => 'P', RESUME => 'P', QUIT => 'Q', 31 LOAD => sub { "L $_[0]" }, 32 JUMP => sub { "J $_[0]s" }, 33 watcher => \&_remotemsg, 34 }, 35 cmdline => \&mpg123_cmdline, 36 priority=> 1, #makes it higher priority than mpg321 37 }, 38 flac123 => 39 { type => 'flac', devices => 'oss esd arts ', 40 remote => { PAUSE => 'P', RESUME => 'P', QUIT => 'Q', 41 LOAD => sub { "L $_[0]" }, JUMP => sub { "J $_[0]" }, watcher => \&_remotemsg, 42 }, 43 cmdline => \&flac123_cmdline, 44 priority=> 1, #makes it higher priority than ogg123 (because ogg123 can't seek in flac files) 45 }, 46); 47our %Supported; 48 49$::PlayPacks{Play_123}=1; #register the package 50 51sub init 52{ my @notfound; my $foundone; 53 for my $cmd (sort {($Commands{$b}{priority}||0) <=> ($Commands{$a}{priority}||0)} keys %Commands) 54 { my ($found)= grep -x, map $_.::SLASH.$cmd, split /:/, $ENV{PATH}; 55 while ($found && -l $found) 56 { my $real= readlink $found; 57 $real='' if $real eq $found; #avoid endless loop 58 $real=~m#([^/]+)$#; 59 if ($cmd ne $1 && $Commands{$1}) {$found=undef} #ignore symbolic links to other known commands (like a mpg123 that is really a symlinked mpg321) 60 else {$found=$real} 61 } 62 for my $ext (split / /,$Commands{$cmd}{type}) { push @{$Supported{$ext}},$cmd if $found; } 63 $Commands{$cmd}{found}=1 if $found; 64 if ($found) {$foundone++} 65 else {push @notfound,$cmd;} 66 } 67 for my $ext (keys %Supported) 68 { my $cmds=$Supported{$ext}; 69 my $priority=$::Options{'123priority_'.$ext}; 70 if ($priority && (grep $priority eq $_, @$cmds)) { $Supported{$ext}=$priority; } 71 else { $Supported{$ext}=$cmds->[0]; } 72 } 73 $Supported{$_}=$Supported{$::Alias_ext{$_}} for grep $Supported{$::Alias_ext{$_}}, keys %::Alias_ext; 74 my @missing= grep !$Supported{$_}, qw/mp3 oga flac/; 75 if (@missing) 76 { warn "These commands were not found : ".join(', ',@notfound)."\n"; 77 warn " => these file types won't be played by the 123 output : ".join(', ',@missing)."\n"; #FIXME include aliases 78 } 79 80 return unless $foundone; 81 return bless {},__PACKAGE__; 82} 83 84sub VolInit 85{ return Play_amixer::init(); #use amixer for volume 86} 87 88sub supported_formats 89{ return grep $Supported{$_}, keys %Supported; 90} 91 92sub mp3_sec_to_frame #mpg321 needs a frame number 93{ my $sec=$_[0]; 94 my ($filetype,$samprate)=Songs::Get($::PlayingID,'filetype','samprate'); 95 my $samperframe=1152; 96 $samperframe= $1==1 ? 384 : $2==2 ? 576 : 1152 if $filetype=~m/mp3 l(\d)v(\d)/; 97 my $framepersec= ($samprate||44100)/$samperframe; 98 return sprintf '%.0f',$sec*$framepersec; 99} 100sub mpg321_cmdline 101{ my ($file,$sec,$out,@opt)=@_; 102 unshift @opt,'-o',$out if $out; 103 push @opt,'-k',mp3_sec_to_frame($sec) if $sec; 104 return 'mpg321',@opt,'-vq','--skip-printing-frames=3','--',$file; 105} 106sub ogg123_cmdline 107{ my ($file,$sec,$out,@opt)=@_; 108 if ($out) 109 { $out=~s/^alsa/alsa09/ if (defined $alsa09 ? $alsa09 : $alsa09=qx(ogg123)=~m/alsa09/); #check if ogg123 calls alsa "alsa09" or "alsa" 110 unshift @opt,'-d',$out; 111 } 112 push @opt,'-k',$sec if $sec; 113 return 'ogg123',@opt,'--',$file; 114} 115sub flac123_cmdline 116{ my ($file,$sec,$out,@opt)=@_; 117 unshift @opt,'-d',$out if $out; 118 return 'flac123',@opt,'-R'; 119} 120sub mpg123_cmdline 121{ my ($file,$sec,$out,@opt)=@_; 122 unshift @opt,'-o',$out if $out; 123 return 'mpg123',@opt,'-R'; 124} 125 126sub Play 127{ (undef,$file,my$sec)=@_; 128 &Stop if $ChildPID; 129 $Error=$SkipTo=undef; 130 @cmd_and_args=(); 131 my $device_option; 132 my $device=$::Options{Device}; 133 my ($type)= $file=~m/\.([^.]*)$/; 134 $type=lc$type; 135 my $cmd=$Supported{$type}; 136 if ($cmd) 137 { my @extra= split / /, $::Options{'123options_'.$cmd}||''; 138 my $out= $::Options{'123device_'.$cmd}; 139 $out=undef if $out && $out eq 'default'; 140 @cmd_and_args= $Commands{$cmd}{cmdline}($file,$sec,$out,@extra); 141 } 142 else 143 { $type= $::Alias_ext{$type} if $::Alias_ext{$type}; 144 my $re= qr/\b$type\b/; 145 my @hints= grep $Commands{$_}{type}=~m/$re/, sort keys %Commands; 146 my $msg= _("Can't play this file."). "\n"; 147 $msg.= @hints>1 ? _"One of these commands is required to play files of type {type} : {cmd}" : 148 @hints ? _"This command is required to play files of type {type} : {cmd}" : 149 _"Don't know how to play files of type {type}" ; 150 ::ErrorPlay( ::__x($msg, type=> $type, cmd=> join(', ',@hints)) ); 151 return undef; 152 } 153 $RemoteMode=$Commands{$cmd}{remote}; 154 155 ################################################# 156 #$ChildPID=open3(my $fh, $OUTPUTfh, $OUTPUTfh, @cmd_and_args, $file); 157 pipe $OUTPUTfh,my$wfh; 158 pipe my($rfh),$CMDfh; 159 $ChildPID=fork; 160 if (!defined $ChildPID) { warn "gmusicbrowser_123 : fork failed : $!\n"; ::ErrorPlay("Fork failed : $!"); return } 161 elsif ($ChildPID==0) #child 162 { close $OUTPUTfh; close $CMDfh; 163 open my($olderr), ">&", \*STDERR; 164 open \*STDIN, '<&='.fileno $rfh; 165 open \*STDOUT,'>&='.fileno $wfh; 166 open \*STDERR,'>&='.fileno $wfh; 167 exec @cmd_and_args or print $olderr "launch failed (@cmd_and_args) : $!\n"; 168 POSIX::_exit(1); 169 } 170 close $wfh; close $rfh; 171 if ($RemoteMode) 172 { $CMDfh->autoflush(1); 173 print $CMDfh $RemoteMode->{LOAD}($file)."\n"; 174 SkipTo(undef,$sec) if $sec; 175 } 176 $OUTPUTfh->blocking(0); #set non-blocking IO 177 warn "playing $file (pid=$ChildPID)\n" if $::Verbose; 178 $WatchTag= Glib::IO->add_watch(fileno($OUTPUTfh),'hup',\&_eos_cb); 179 $WatchTag2= $RemoteMode ? 180 Glib::IO->add_watch(fileno($OUTPUTfh),'in',$RemoteMode->{watcher}) : 181 Glib::Timeout->add(500,\&_UpdateTime) ; 182} 183 184sub _eos_cb 185{ _UpdateTime();#parse last lines 186 #close $OUTPUTfh; 187 if ($ChildPID && $ChildPID==waitpid($ChildPID, WNOHANG)) 188 { $Error||=_"Check your audio settings" if $?; 189 } 190 while (waitpid(-1, WNOHANG)>0) {} #reap dead children 191 Glib::Source->remove($WatchTag); 192 Glib::Source->remove($WatchTag2); 193 $WatchTag=$WatchTag2=$ChildPID=undef; 194 if ($Error) { ::ErrorPlay($Error,_("Command used :")."\n@cmd_and_args"); } 195 ::end_of_file(); 196 return 1; 197} 198 199sub _remotemsg #used by flac123 and mpg123 200{ my $buf; 201 my @line=(<$OUTPUTfh>); 202 my $line=pop @line; #only read the last line 203 chomp $line; 204 if ($line=~m/^\@P 0$/) {print $CMDfh $RemoteMode->{QUIT}."\n"} #finished or stopped 205 elsif ($line=~m/^\@F \d+ \d+ (\d+)\.\d\d \d+\.\d\d$/) 206 { ::UpdateTime( $1 ); 207 } 208 elsif ($line=~m/^\@E(.*)$/) { $Error=$1; print $CMDfh $RemoteMode->{QUIT}."\n"; } #Error 209 #else {warn $line."\n"} 210 return 1; 211} 212 213sub Pause 214{ $Paused=1; 215 if ($RemoteMode) { print $CMDfh $RemoteMode->{PAUSE}."\n" } 216 elsif ($ChildPID) {kill STOP=>$ChildPID}; 217} 218sub Resume 219{ $Paused=0; 220 if ($ChildPID) 221 { if ($RemoteMode) { print $CMDfh $RemoteMode->{RESUME}."\n" } 222 else { kill CONT=>$ChildPID; } 223 } 224 else { SkipTo(undef,$SkipTo); $SkipTo=undef; } 225} 226 227sub SkipTo 228{ my $sec=$_[1]; 229 if ($Paused) { Stop(); $Paused=1; $SkipTo=$sec; } 230 elsif ($RemoteMode && $ChildPID) 231 { ::setlocale(::LC_NUMERIC, 'C'); #flac123 ignores decimals anyway 232 print $CMDfh $RemoteMode->{JUMP}($sec)."\n"; 233 ::setlocale(::LC_NUMERIC, ''); 234 } 235 else { Play(undef,$file,$sec); } 236} 237 238 239sub Stop 240{ $Paused=0; 241 if ($WatchTag) 242 { Glib::Source->remove($WatchTag); 243 Glib::Source->remove($WatchTag2); 244 $WatchTag=$WatchTag2=undef; 245 } 246 if ($ChildPID) 247 { print $CMDfh $RemoteMode->{QUIT}."\n" if $RemoteMode; 248 warn "killing $ChildPID\n" if $::debug; 249 #close $OUTPUTfh; 250 #kill TERM,$ChildPID; 251 kill INT=>$ChildPID; 252 Glib::Timeout->add( 200,\&_Kill_timeout ) unless @pidToKill; 253 push @pidToKill,$ChildPID; 254 undef $ChildPID; 255 } 256} 257sub _Kill_timeout #make sure old children are dead 258{ @pidToKill=grep kill(0,$_), @pidToKill; 259 if (@pidToKill) 260 { warn "killing -9 @pidToKill\n" if $::debug; 261 kill KILL=>@pidToKill; 262 undef @pidToKill; 263 } 264 while (waitpid(-1, WNOHANG)>0) {} #reap dead children 265 return 0; 266} 267 268sub _UpdateTime #used by ogg123 and mpg321 269{ my @lines=(<$OUTPUTfh>); 270 for (reverse @lines) 271 { if (m#\D: +(\d\d):(\d\d)\.(\d\d)#) { ::UpdateTime( $1*60+$2+($3>=50?1:0) ); return 1 } 272 # check if known error message 273 $Error=$1 if m#(Can't find a suitable libao driver)# || 274 m#(No such device \w+)# || 275 m#(Failed to initialize output)# || 276 m#(Cannot open .+)# || 277 m#(.+: No such file or directory)#; 278 } 279 warn join("123:$_",'',@lines) if $::debug; 280 return 1; 281} 282 283sub AdvancedOptions 284{ my $vbox=Gtk2::VBox->new; 285 my $table=Gtk2::Table->new(1,1,::FALSE); 286 my %ext; my %extgroup; 287 $ext{$_}=undef for map split(/ /,$Commands{$_}{type}), keys %Commands; 288 my @ext=sort keys %ext; 289 for my $e (@ext) { $ext{$e}= join '/', $e, sort grep $::Alias_ext{$_} eq $e,keys %::Alias_ext; } 290 my $i=my $j=0; 291 $table->attach_defaults(Gtk2::Label->new($_), $i++,$i,$j,$j+1) for (_"Command", _"Output", _"Options",map " $ext{$_} ", @ext); 292 my $hsize= Gtk2::SizeGroup->new('vertical'); 293 $hsize->add_widget($_) for $table->get_children; 294 for my $cmd (sort keys %Commands) 295 { $i=0; $j++; 296 my $devs= $Commands{$cmd}{devices}; 297 my @widgets; 298 $devs='' if ref $devs && !$Commands{$cmd}{found}; #don't try to find dynamic list of devices if command not found, as the function likely requires the command 299 my @devlist= ref $devs ? $devs->() : split / /,$devs; 300 push @widgets, 301 Gtk2::Label->new($cmd), 302 ::NewPrefCombo('123device_'.$cmd => ['default',@devlist]), 303 ::NewPrefEntry('123options_'.$cmd); 304 $hsize->add_widget($_) for @widgets; 305 my %cando; $cando{$_}=undef for split / /,$Commands{$cmd}{type}; 306 $table->attach_defaults($_, $i++,$i,$j,$j+1) for @widgets; 307 for my $ext (@ext) 308 { if (exists $cando{$ext}) 309 { my $w=Gtk2::RadioButton->new($extgroup{$ext}); 310 $w->set_tooltip_text( ::__x(_"Use {command} to play {ext} files",command=>$cmd, ext=>$ext{$ext}) ); 311 $extgroup{$ext}||=$w; 312 $table->attach($w, $i,$i+1,$j,$j+1,'expand','expand',0,0); 313 push @widgets,$w; 314 $w->set_active(1) if $cmd eq ($Supported{$ext} || ''); 315 $w->signal_connect(toggled => sub { return unless $_[0]->get_active; $Supported{$ext}=$::Options{'123priority_'.$ext}=$cmd; $Supported{$_}=$Supported{$ext} for grep $::Alias_ext{$_} eq $ext, keys %::Alias_ext; }); 316 } 317 $i++; 318 } 319 unless ($Commands{$cmd}{found}) {$_->set_sensitive(0) for @widgets;} 320 } 321 $vbox->pack_start($table,::FALSE,::FALSE,2); 322 my $hbox=Play_amixer->make_option_widget; 323 324 $vbox->pack_start($hbox,::FALSE,::FALSE,2); 325 return $vbox; 326} 327 328package Play_amixer; 329my ($mixer,$Mute,$Volume); 330 331sub init 332{ $Mute||=0; 333 for my $path (split /:/, $ENV{PATH}) 334 { if (-x $path.::SLASH.'amixer') {$mixer=$path.::SLASH.'amixer';last;} 335 } 336 337# if ($mixer) 338# { SetVolume(); 339 #Glib::Timeout->add(5000,\&SetVolume); 340# } 341 unless ($mixer) {warn "amixer not found, won't be able to get/set volume through the 123 or mplayer output.\n"} 342 return bless {},__PACKAGE__; 343} 344 345sub init_volume 346{ $Volume=-1; 347 return unless $mixer; 348 my @list=get_amixer_SMC_list(); 349 my %h; $h{$_}=1 for @list; 350 my $c=\$::Options{amixerSMC}; 351 if ($$c) { SetVolume(); return if $Volume>=0 || $h{$$c}; $$c=''; } 352 if ($h{PCM}) {$$c='PCM'} 353 elsif ($h{Master}) {$$c='Master'} 354 else { warn "Don't know what mixer to choose among : @list\n"; } 355 SetVolume(); 356} 357 358sub GetVolume 359{ init_volume() unless defined $Volume; 360 return $Volume; 361} 362sub GetVolumeError { _"Can't change the volume. Needs amixer (packaged in alsa-utils) to change volume when using this audio backend." } 363sub GetMute {$Mute} 364sub SetVolume 365{ shift; 366 my $inc=$_[0]; 367 return unless $mixer; 368 my $cmd=$mixer; 369 if ($inc) { if ($inc=~m/^([+-])?(\d+)$/) { $inc=$2.'%'.($1||''); } 370 $cmd.=" set '$::Options{amixerSMC}' $inc"; 371 } 372 else { $cmd.=" get '$::Options{amixerSMC}'"; } 373 warn "volume command : $cmd\n" if $::debug; 374 my $oldvol=$Volume; 375 my $oldm=$Mute; 376 open VOL,'-|',$cmd; 377 while (<VOL>) 378 { if (m/ \d+ \[(\d+)%\](?:.*?\[(on|off)\])?/) 379 { $Volume=$1; 380 $Mute=($2 && $2 ne 'on')? 1 : 0; 381 last; 382 } 383 } 384 close VOL; 385 return 1 unless ($oldvol!=$Volume || $oldm!=$Mute); 386 ::HasChanged('Vol'); 387 1; 388} 389 390sub make_option_widget 391{ my $hbox=::NewPrefCombo(amixerSMC => [get_amixer_SMC_list()], text =>_"amixer control :", cb =>sub {SetVolume()}); 392 $hbox->set_sensitive(0) unless $mixer; 393 return $hbox; 394} 395 396sub get_amixer_SMC_list 397{ init() unless $mixer; 398 return () unless $mixer; 399 init_volume() unless defined $Volume; 400 my (@list,$SMC); 401 open VOL,'-|',$mixer; 402 while (<VOL>) 403 { if (m/^Simple mixer control '([^']+)'/) 404 { $SMC=$1; 405 } 406 elsif ($SMC && m/ \d+ \[(\d+)%\](?: \[(\w+)\])?/) 407 { push @list,$SMC; $SMC=undef; 408 } 409 } 410 close VOL; 411 return @list; 412} 413 4141; 415