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