1# Copyright (C) 2005-2015 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
8BEGIN
9{	require GStreamer;
10	$::gstreamer_version='0.10';
11	die "Needs GStreamer version >= 0.05\n" if GStreamer->VERSION<.05;
12	die "Can't initialize GStreamer.\n" unless GStreamer->init_check;
13	GStreamer->init;
14	my $reg=GStreamer::Registry->get_default;
15	$Play_GST::reg_keep=$reg if GStreamer->CHECK_VERSION(0,10,4); #work-around to keep the register from being finalized in gstreamer<0.10.4 (see http://bugzilla.gnome.org/show_bug.cgi?id=324818)
16	$reg->lookup_feature('playbin') or die "gstreamer plugin 'playbin' not found.\nYou need to install at least gst-plugins-base.\n";
17}
18
19package Play_GST;
20use strict;
21use warnings;
22
23my ($GST_visuals_ok,$GST_EQ_ok,$GST_RG_ok,$playbin2_ok); our $GST_RGA_ok;
24my ($PlayBin,$Sink);
25my ($WatchTag,$Skip,$StateAfterSkip);
26my (%Plugins,%Sinks);
27my ($VSink,$visual_window);
28my $AlreadyNextSong;
29my $RG_dialog;
30my ($VolumeBusy,$VolumeHasChanged);
31
32$::PlayPacks{Play_GST}=1; #register the package
33
34
35BEGIN
36{ %Sinks=
37  (	autoaudio	=> { name => _"auto detect", },
38	oss		=> { option => 'device' },
39	oss4		=> { option => 'device' },
40	esd		=> { option => 'host'},
41	alsa		=> { option => 'device'},
42	artsd		=> {},
43	sdlaudio	=> {},
44	gconfaudio	=> { name => _"use gnome settings"},
45	halaudio	=> { name => "HAL device", option=>'udi'},
46	pulse		=> { name => "PulseAudio", option=>'server device'},
47	jackaudio	=> { name => "JACK", option => 'server' },
48	osxaudio	=> {},
49	directsound	=> {},
50	#alsaspdif	=> { name => "alsa S/PDIF", option => 'card' },
51	#nas		=> {},
52  );
53  %Plugins=(	mp3 => 'flump3dec mad mpg123audiodec avdec_mp3',
54		oga => 'vorbisdec',			flac=> 'flacdec',
55		ape => 'avdec_ape ffdec_ape',		wv  => 'wavpackdec',
56		mpc => 'musepackdec avdec_mpc8',	m4a => 'faad',
57  );
58
59  my $reg=GStreamer::Registry->get_default;
60  $playbin2_ok= $reg->lookup_feature('playbin2');
61  if ($reg->lookup_feature('equalizer-10bands')) { $GST_EQ_ok=1; }
62  else {warn "gstreamer plugin 'equalizer-10bands' not found -> equalizer not available\n";}
63  if ($reg->lookup_feature('rglimiter') && $reg->lookup_feature('rgvolume')) { $GST_RG_ok=1; }
64  else {warn "gstreamer plugins 'rglimiter' and/or 'rgvolume' not found -> replaygain not available\n";}
65  if ($reg->lookup_feature('rganalysis')) { $GST_RGA_ok=1; }
66  else {warn "gstreamer plugins 'rganalysis' not found -> replaygain analysis not available\n";}
67  $GST_visuals_ok=1;
68  eval {require GStreamer::Interfaces};
69  if ($@) {warn "GStreamer::Interfaces perl module not found -> visuals not available\n"; $GST_visuals_ok=0;}
70  unless ($reg->lookup_feature('ximagesink'))
71  {	warn "gstreamer plugin 'ximagesink' not found -> visuals not available\n"; $GST_visuals_ok=0;
72  }
73}
74
75sub supported_formats
76{	my $reg=GStreamer::Registry->get_default;
77	my @found;
78	for my $type (keys %Plugins)
79	{	push @found, $type if grep $reg->lookup_feature($_), split / +/, $Plugins{$type};
80	}
81	return @found;
82}
83sub supported_sinks
84{	my $reg=GStreamer::Registry->get_default;
85	$Sinks{$_}{ok}= ! !$reg->lookup_feature($_.'sink') for keys %Sinks;
86	#$::Options{gst_sink}='autoaudio' unless $Sinks{$::Options{gst_sink}};
87	return {map { $_ => $Sinks{$_}{name}||$_ } grep $Sinks{$_}{ok}, keys %Sinks};
88}
89
90sub init
91{	my $reg=GStreamer::Registry->get_default;
92	$::Options{gst_sink}='' unless $reg->lookup_feature( ($::Options{gst_sink}||'').'sink' );
93	$::Options{gst_sink}||= (grep ($reg->lookup_feature($_.'sink'), qw/autoaudio gconfaudio pulse alsa esd oss oss4/),'autoaudio')[0]; #find a default sink
94	return bless { EQ=>$GST_EQ_ok, EQpre=>$GST_EQ_ok, visuals => $GST_visuals_ok, RG=>$GST_RG_ok },__PACKAGE__;
95}
96
97sub createPlayBin
98{	if ($PlayBin) { $PlayBin->get_bus->remove_signal_watch; }
99	my $pb= $playbin2_ok ? 'playbin2' : 'playbin';
100	$PlayBin=GStreamer::ElementFactory->make($pb => 'playbin'); #FIXME only the first one used works
101	$PlayBin->set('flags' => [qw/audio soft-volume/]) if $playbin2_ok;
102	SetVolume(undef,''); #initialize volume
103	my $bus=$PlayBin->get_bus;
104	$bus->add_signal_watch;
105	$PlayBin->signal_connect("notify::volume" => sub { Glib::Idle->add(\&VolumeChanged) unless $VolumeHasChanged++; },100000) if $Glib::VERSION >= 1.251 && $::Options{gst_monitor_pa_volume}; #not stable with older version of perl-glib due to bug #620099 (https://bugzilla.gnome.org/show_bug.cgi?id=620099), and still not quite stable
106#	$bus->signal_connect('message' => \&bus_message);
107	$bus->signal_connect('message::eos' => \&bus_message_end);
108	$bus->signal_connect('message::error' => \&bus_message_end,1);
109	$bus->signal_connect('message::state-changed' => \&bus_message_state_changed);
110	$PlayBin->signal_connect(about_to_finish => \&about_to_finish) if $::Options{gst_gapless};
111	if ($visual_window) { create_visuals() }
112}
113
114#sub bus_message
115#{	my $msg=$_[1];
116#	warn 'bus: message='.$msg->type."\n" if $::debug;
117#	SkipTo(undef,$Skip) if $Skip && $msg->type & 'state_changed';
118#	return unless $msg->type & ['eos','error'];
119#	if ($Sink->get_name eq 'server') #FIXME
120#	{ $Sink->set_locked_state(1); $PlayBin->set_state('null'); $Sink->set_locked_state(0); }
121#	else { $PlayBin->set_state('null'); }
122#	if ($msg->type & 'error')	{ ::ErrorPlay($msg->error); }
123#	else				{ ::end_of_file(); }
124#}
125
126sub bus_message_end
127{	my ($msg,$error)=($_[1],$_[2]);
128	#error msg if $error is true, else eos
129	if ($Sink->get_name eq 'server') #FIXME
130	{ $Sink->set_locked_state(1); $PlayBin->set_state('null'); $Sink->set_locked_state(0); }
131	else { $PlayBin->set_state('null'); }
132	if ($error)	{ ::ErrorPlay($msg->error); }
133	else		{ ::end_of_file(); }
134}
135
136# when a song starts playing, notify::volume callbacks are called, which causes glib to hang (see https://bugzilla.gnome.org/show_bug.cgi?id=620099#c11) if a skip is done at the same moment
137# using freeze_notify until the skip is done mostly avoid the problem
138# setting state to paused until the skip is done also mostly avoid the problem
139# but the hang still happens in some cases, in particular when using the scroll wheel to change the position in the song, and also very rarely when starting a song
140my $StateChanged;
141sub bus_message_state_changed	# used to wait for the right state to do the skip
142{	return unless $Skip;
143	return if $StateChanged;
144	$StateChanged=1;
145	$PlayBin->freeze_notify unless $PlayBin->{notify_frozen}; $PlayBin->{notify_frozen}=1; #freeze notify until skip is done
146	Glib::Idle->add(sub
147	{	SkipTo(undef,$Skip) if $Skip; # this will only skip if the state is right, else will wait for another state change
148		unless ($Skip) { $PlayBin->thaw_notify if delete $PlayBin->{notify_frozen}; } #if skip is done, unfreeze
149		$StateChanged=0;
150		0;
151	});
152}
153
154sub Close
155{	$Sink=undef;
156}
157
158sub GetVolume	{$::Volume}
159sub GetMute	{$::Mute}
160sub SetVolume
161{	shift;
162	my $set=shift;
163	if	($set eq 'mute')	{ $::Mute=$::Volume; $::Volume=0;}
164	elsif	($set eq 'unmute')	{ $::Volume=$::Mute; $::Mute=0;  }
165	elsif	($set=~m/^\+(\d+)$/)	{ $::Volume+=$1; }
166	elsif	($set=~m/^-(\d+)$/)	{ $::Volume-=$1; }
167	elsif	($set=~m/(\d+)/)	{ $::Volume =$1; }
168	$::Volume=0   if $::Volume<0;
169	$::Volume=100 if $::Volume>100;
170	$VolumeBusy=1;
171	$PlayBin->set(volume => ( ($::Mute||$::Volume) /100)**3, mute => !!$::Mute) if $PlayBin; 	#use a cubic volume scale
172	$VolumeBusy=0;
173	$::Options{Volume}=$::Volume;
174	$::Options{Volume_mute}=$::Mute;
175	::QHasChanged('Vol');
176}
177sub VolumeChanged
178{	$VolumeHasChanged=0;
179	return 0 if $VolumeBusy;
180	return 0 unless $PlayBin;
181	my ($volume,$mute)= $PlayBin->get('volume','mute');
182	$volume= $volume ** (1/3) *100; #use a cubic volume scale
183	$volume= sprintf '%d',$volume;
184	$volume=100 if $volume>100;
185	#return 0 unless $volume!=$::Volume || ($mute xor !!$::Mute);
186	if ($mute)	{ $::Mute=$volume; $::Volume=0; }
187	else		{ $::Mute=0; $::Volume=$volume; }
188	$::Options{Volume}=$::Volume;
189	$::Options{Volume_mute}=$::Mute;
190	::QHasChanged('Vol');
191	0; #called from an idle
192}
193
194sub SkipTo
195{	shift;
196	$Skip=shift;
197	my ($result,$state,$pending)=$PlayBin->get_state(0);
198	return if $result eq 'async'; #when song hasn't started yet, needs to wait until it has started before skipping
199	$PlayBin->seek(1,'time','flush','set', $Skip*1_000_000_000,'none',0);
200	if ($StateAfterSkip) { $PlayBin->set_state($StateAfterSkip); $StateAfterSkip=undef; }
201	$Skip=undef;
202}
203
204sub Pause
205{	$PlayBin->set_state('paused');
206	$StateAfterSkip=undef;
207}
208sub Resume
209{	$PlayBin->set_state('playing');
210	$StateAfterSkip=undef;
211}
212
213sub check_sink
214{	$Sink->get_name eq $::Options{gst_sink};
215}
216sub make_sink
217{	my $sinkname=$::Options{gst_sink};
218	my $sink=GStreamer::ElementFactory->make($sinkname.'sink' => $sinkname);
219	return undef unless $sink;
220	$sink->set(profile => 'music') if $::Options{gst_sink} eq 'gconfaudio';
221	if (my $opts=$Sinks{$sinkname}{option})
222	{	for my $opt (split / /, $opts)
223		{	my $val=$::Options{'gst_'.$sinkname.'_'.$opt};
224			next unless defined $val && $val ne '';
225			$sink->set($opt => $val);
226		}
227	}
228	return $sink;
229}
230
231sub about_to_finish	#GAPLESS
232{	#warn "-------about_to_finish  $::NextFileToPlay\n";
233	return unless $::NextFileToPlay;
234	set_file($::NextFileToPlay);
235	$AlreadyNextSong=$::NextFileToPlay;
236	$::NextFileToPlay=0;
237}
238
239sub Play
240{	(my($package,$file),$Skip)=@_;
241	#warn "------play $file\n";
242	#$PlayBin->set_state('ready');#&Stop;
243	#my ($ext)=$file=~m/\.([^.]*)$/; warn $ext;
244	#::ErrorPlay('not supported') and return undef  unless $Plugins{$ext};
245	my $keep= $Sink && $package->check_sink;
246	my $useEQ= $GST_EQ_ok && $::Options{use_equalizer};
247	my $useRG= $GST_RG_ok && $::Options{use_replaygain};
248	$keep=0 if $Sink->{EQ} xor $useEQ;
249	$keep=0 if $Sink->{RG} xor $useRG;
250	$keep=0 if $package->{modif}; #advanced options changed
251	if ($AlreadyNextSong && $AlreadyNextSong eq $file && $keep && !$Skip)
252	{	$AlreadyNextSong=undef;
253		return;
254	}
255	if ($keep)
256	{	$package->Stop(1);
257	}
258	else
259	{	createPlayBin();
260		warn "Creating new gstreamer sink\n" if $::debug;
261		delete $package->{modif};
262		$Sink=$package->make_sink;
263		unless ($Sink) { ::ErrorPlay( ::__x(_"Can't create sink '{sink}'", sink => $::Options{gst_sink}) );return }
264
265		my @elems;
266		$Sink->{EQ}=$useEQ;
267		if ($useEQ)
268		{	my $preamp=GStreamer::ElementFactory->make('volume' => 'equalizer-preamp');
269			my $equalizer=GStreamer::ElementFactory->make('equalizer-10bands' => 'equalizer');
270			my @val= split /:/, $::Options{equalizer};
271			::setlocale(::LC_NUMERIC, 'C');
272			$equalizer->set( 'band'.$_ => $val[$_]) for 0..9;
273			$preamp->set( volume => $::Options{equalizer_preamp}**3);
274			::setlocale(::LC_NUMERIC, '');
275			push @elems,$preamp,$equalizer;
276		}
277		$Sink->{RG}=$useRG;
278		if ($useRG)
279		{	my ($rgv,$rgl,$ac,$ar)=	map GStreamer::ElementFactory->make($_=>$_),
280					qw/rgvolume rglimiter audioconvert audioresample/;
281			RG_set_options($rgv,$rgl);
282			push @elems, $rgv,$rgl,$ac,$ar;
283		}
284		if (my $custom=$::Options{gst_custom})
285		{	$custom="( $custom )" if $custom=~m/^\s*\w/ && $custom=~m/!/;	#make a Bin by default instead of a pipeline
286			my $elem= eval { GStreamer::parse_launch($custom) };
287			warn "gstreamer custom pipeline error : $@\n" if $@;
288			if ($elem && $elem->isa('GStreamer::Bin'))
289			{	my $first=my $last=$elem;
290				# will work at least for simple cases #FIXME could be better
291				$first=@{ $first->iterate_sorted }[0]  while $first->isa('GStreamer::Bin');
292				$last =@{  $last->iterate_sorted }[-1] while  $last->isa('GStreamer::Bin');
293				$elem->add_pad( GStreamer::GhostPad->new('sink', $last->get_pad('sink') ));
294				$elem->add_pad( GStreamer::GhostPad->new('src', $first->get_pad('src') ));
295			}
296			push @elems, $elem if $elem;
297		}
298		if (@elems)
299		{	my $sink0=GStreamer::Bin->new('sink0');
300			push @elems,$Sink;
301			$sink0->add(@elems);
302			my $first=shift @elems;
303			$first->link(@elems);
304			$sink0->add_pad( GStreamer::GhostPad->new('sink', $first->get_pad('sink') ));
305			$PlayBin->set('audio-sink' => $sink0);
306		}
307		else {$PlayBin->set('audio-sink' => $Sink);}
308	}
309
310	if ($visual_window)
311	{	$visual_window->realize unless $visual_window->window;
312		if (my $w=$visual_window->window) { $VSink->set_xwindow_id($w->XID); }
313	}
314	warn "playing $file\n" if $::Verbose;
315	set_file($file);
316	my $newstate='playing'; $StateAfterSkip=undef;
317	if ($Skip) { $newstate='paused'; $StateAfterSkip='playing'; }
318	$PlayBin->set_state($newstate);
319	$WatchTag=Glib::Timeout->add(500,\&_UpdateTime) unless $WatchTag;
320}
321sub set_file
322{	my $f=shift;
323	if ($f!~m#^([a-z]+)://#)
324	{	$f=~s#([^A-Za-z0-9- /\.])#sprintf('%%%02X', ord($1))#seg;
325		$f='file://'.$f;
326	}
327	$PlayBin -> set(uri => $f);
328}
329
330sub set_equalizer_preamp
331{	my (undef,$volume)=@_;
332	my $preamp= $PlayBin && $PlayBin->get_by_name('equalizer-preamp');
333	$preamp->set( volume => $volume**3) if $preamp;
334}
335sub set_equalizer
336{	my (undef,$values)=@_;
337	my $equalizer= $PlayBin && $PlayBin->get_by_name('equalizer');
338	return unless $equalizer;
339	my @vals= split /:/,$values;
340	$equalizer->set( 'band'.$_ => $vals[$_]) for 0..9;
341}
342sub EQ_Get_Range
343{	createPlayBin() unless $PlayBin;
344	my ($min,$max)=(-1,1);
345	{	my $equalizer=$PlayBin->get_by_name('equalizer')
346		 || GStreamer::ElementFactory->make('equalizer-10bands' => 'equalizer');
347		last unless $equalizer;
348		my $prop= $equalizer->find_property('band0');
349		last unless $prop;
350		$min=$prop->get_minimum;
351		$max=$prop->get_maximum;
352	}
353	my $unit= ($max==1 && $min==-1) ? '' : 'dB';
354	return ($min,$max,$unit);
355}
356sub EQ_Get_Hz
357{	my $i=$_[1];
358	createPlayBin() unless $PlayBin;
359	my $equalizer=$PlayBin->get_by_name('equalizer')
360	 || GStreamer::ElementFactory->make('equalizer-10bands' => 'equalizer');
361	return undef unless $equalizer;
362	my $hz= $equalizer->find_property('band'.$i)->get_nick;
363	if ($hz=~m/^(\d+)\s*(k?)Hz/)
364	{	$hz=$1; $hz*=1000 if $2;
365		$hz= $hz>=1000 ? sprintf '%.1fkHz',$hz/1000 :
366				 sprintf '%dHz',$hz ;
367	}
368	return $hz;
369}
370
371sub create_visuals
372{	unless ($VSink)
373	{	$VSink=GStreamer::ElementFactory->make(ximagesink => 'ximagesink');
374		return unless $VSink;
375		$visual_window->realize unless $visual_window->window;
376		if (my $w=$visual_window->window) { $VSink->set_xwindow_id($w->XID); }
377	}
378	$PlayBin->set('video-sink' => $VSink) if $PlayBin;
379	$PlayBin->set('flags' => [qw/audio vis soft-volume/]) if $PlayBin && $playbin2_ok;
380	set_visual();
381}
382sub add_visuals
383{	remove_visuals() if $visual_window;
384	$visual_window=shift;
385	$visual_window->signal_connect(unrealize => \&remove_visuals);
386	$visual_window->signal_connect(configure_event => sub {$VSink->expose if $VSink});
387	$visual_window->signal_connect(expose_event => sub
388		{	if ($VSink) { $VSink->expose; }
389			else { create_visuals() }
390			1;
391		});
392}
393sub remove_visuals
394{	$VSink->set_xwindow_id(0) if $VSink;
395	$PlayBin->set('flags' => [qw/audio soft-volume/]) if $PlayBin && $playbin2_ok;
396	$PlayBin->set('video-sink' => undef) if $PlayBin;
397	$PlayBin->set('vis-plugin' => undef) if $PlayBin;
398	$visual_window=$VSink=undef;
399}
400sub set_visual
401{	my $visual= shift || $::Options{gst_visual} || '';
402	my @l=list_visuals();
403	return unless @l;
404	if ($visual eq '+') #choose next visual in the list
405	{	$visual=$::Options{gst_visual} || $l[0];
406		my $i=0;
407		for my $v (@l)
408		{	last if $v eq $visual;
409			$i++
410		}
411		$i++; $i=0 if $i>$#l;
412		$visual=$l[$i];
413	}
414	elsif (!(grep $_ eq $visual, @l)) # if visual not found in list
415	{	$visual=undef;
416	}
417	$visual||=$l[0];
418	warn "visual=$visual\n" if $::debug;
419	$::Options{gst_visual}=$visual;
420	$visual=GStreamer::ElementFactory->make($visual => 'visual');
421	$PlayBin->set('vis-plugin' => $visual) if $PlayBin;
422	$VSink->expose;
423}
424
425sub list_visuals
426{	my @visuals;
427	my $reg=GStreamer::Registry->get_default;
428	for my $plugin ($reg->get_plugin_list)
429	{	#warn $plugin;
430		for my $elem ($reg->get_feature_list_by_plugin($plugin->get_name))
431		{	#warn $elem;
432			if ($elem->isa('GStreamer::ElementFactory'))
433			{	my $klass=$elem->get_klass;
434				next unless $klass eq 'Visualization';
435				#warn $elem->get_name."\n";
436				#warn $elem->get_longname."\n";
437				#warn $elem->get_description."\n";
438				#warn $elem->get_element_type."\n";
439				#warn "$klass\n";
440				#warn "\n";
441				push @visuals,$elem->get_name;
442			}
443		}
444	}
445	return @visuals;
446}
447
448sub _UpdateTime
449{	my ($result,$state,$pending)=$PlayBin->get_state(0);
450	if ($AlreadyNextSong) { warn "UpdateTime: gapless change to next song\n" if $::debug; ::end_of_file_faketime(); return 1; }
451	warn "state: $result,$state,$pending\n" if $::debug;
452	return 1 if $result eq 'async';
453	if ($state ne 'playing' && $state ne 'paused')
454	{	return 1 if $pending eq 'playing' || $pending eq 'paused';
455		::ResetTime() unless 'Play_GST' ne ref $::Play_package;
456		$WatchTag=undef;
457		return 0;
458	}
459	my $query=GStreamer::Query::Position->new('time');
460	if ($PlayBin->query($query))
461	{	my (undef, $position)=$query->position;
462		::UpdateTime( $position/1_000_000_000 );
463	}
464	return 1;
465}
466
467sub Stop
468{	#if ($_[1]) { $Sink->set_locked_state(1); $PlayBin->set_state('null'); $Sink->set_locked_state(0);return; }
469	#if ($_[1]) { $PlayBin->set_state('ready'); return;}
470	#my ($result,$state,$pending)=$PlayBin->get_state(0);
471	#warn "stop: state: $result,$state,$pending\n";
472	#return if $state eq 'null' and $pending eq 'void-pending';
473	$PlayBin->set_state('null') if $PlayBin;
474	$StateAfterSkip=undef;
475	#warn "--stop\n";
476}
477
478sub RG_set_options
479{	my ($rgv,$rgl)=@_;
480	return unless $::PlayBin;
481	$rgv||=$PlayBin->get_by_name('rgvolume');
482	$rgl||=$PlayBin->get_by_name('rglimiter');
483	return unless $rgv && $rgl;
484	$rgl->set(enabled	=> $::Options{rg_limiter});
485	$rgv->set('album-mode'	=> !!$::Options{rg_albummode});
486	$rgv->set('pre-amp'	=> $::Options{rg_preamp}||0);
487	$rgv->set('fallback-gain'=>$::Options{rg_fallback}||0);
488	#$rgv->set(headroom => $::Options{gst_rg_headroom}||0);
489}
490
491sub AdvancedOptions
492{	my $self=$_[0];
493	my $vbox=Gtk2::VBox->new(::FALSE, 2);
494	my $modif_cb= sub { $self->{modif}=1 };
495	my $gapless= ::NewPrefCheckButton(gst_gapless => _"enable gapless (experimental)", cb=> $modif_cb);
496	$gapless->set_sensitive(0) unless $playbin2_ok;
497	$vbox->pack_start($gapless,::FALSE,::FALSE,2);
498
499	my $monitor_volume= ::NewPrefCheckButton(gst_monitor_pa_volume => _("Monitor the pulseaudio volume").' '._("(unstable)"), cb=> $modif_cb, tip=>_"Makes gmusicbrowser monitor its pulseaudio volume, so that external changes to its volume are known.");
500	$monitor_volume->set_sensitive(0) unless $Glib::VERSION >= 1.251;
501	$vbox->pack_start($monitor_volume,::FALSE,::FALSE,2);
502
503	my $sg1=Gtk2::SizeGroup->new('horizontal');
504	my $custom= ::NewPrefEntry(gst_custom => _"Custom pipeline", cb=>$modif_cb, sizeg1 => $sg1, expand => 1, tip => _"Insert this pipeline before the audio sink", history => 'gst_custom_history');
505	$vbox->pack_start($custom,::FALSE,::FALSE,2);
506	for my $s (sort grep $Sinks{$_}{ok} && $Sinks{$_}{option}, keys %Sinks)
507	{	my $label= $Sinks{$s}{name}||$s;
508		for my $opt (sort split / /,$Sinks{$s}{option})
509		{	my $hbox=::NewPrefEntry("gst_${s}_$opt", "$s $opt : ", cb => $modif_cb, sizeg1 => $sg1, expand => 1);
510			$vbox->pack_start($hbox,::FALSE,::FALSE,2);
511		}
512	}
513	return $vbox;
514}
515
516package GMB::GST_ReplayGain;
517
518my $RGA_pipeline;
519my (@towrite,$writing);
520my $RGA_songmenu=
521{ label => _"Replaygain analysis",	notempty => 'IDs', notmode => 'P', test => sub {$Play_GST::GST_RGA_ok && $::Options{gst_rg_songmenu}; },
522 submenu =>
523 [	{ label => _"Scan this file",			code => sub { Analyse ($_[0]{IDs}); },		onlyone => 'IDs', },
524	{ label => _"Scan per-file track gain",		code => sub { Analyse ($_[0]{IDs}); },		onlymany=> 'IDs', },
525	{ label => _"Scan using tag-defined album", 	code => sub { Analyse_byAlbum ($_[0]{IDs}); },	onlymany=> 'IDs', },
526	{ label => _"Scan as an album",			code => sub { Analyse([join ' ',@{ $_[0]{IDs} }]); },	onlymany=> 'IDs', },
527 ],
528};
529push @::SongCMenu,$RGA_songmenu;
530
531sub Analyse_full
532{	my $added='';
533	my @todo;
534	my $IDs_in_album=  Filter->new('album:-e:')->filter;	#get songs with an album name
535	my ($again,$apeak,$ids)=Songs::BuildHash('album',$IDs_in_album,undef,'replaygain_album_gain:same','replaygain_album_peak:same','id:list');
536	for my $aid (keys %$again)
537	{	next if @{$ids->{$aid}}<2;	#ignore albums with less than 2 songs
538		my $gain= $again->{$aid};
539		my $peak= $apeak->{$aid};
540		next if $gain==$gain && $peak==$peak;	#NaN : album gain/peak not defined or not the same for all songs from album
541		my $IDs= $ids->{$aid};
542		push @todo,join ' ',@$IDs;
543		vec($added,$_,1)=1 for @$IDs;
544	}
545	my $IDs_no_rg= Filter->newadd(0, 'replaygain_track_gain:-defined:1', 'replaygain_track_peak:-defined:1')->filter;
546	push @todo, grep !vec($added,$_,1), @$IDs_no_rg;
547	Analyse(\@todo) if @todo;
548}
549
550sub Analyse_byAlbum
551{	my @IDs= ::uniq(@{ $_[0] });
552	my $hash= Songs::BuildHash('album',\@IDs,undef,'id:list');
553	my @list;
554	for my $aid (keys %$hash)
555	{	my $IDs= $hash->{$aid};
556		if (@$IDs<2 || Songs::Gid_to_Get('album',$aid) eq '') { push @list, @$IDs; } #no album name or only 1 song in album => push as single songs
557		else { push @list, join ' ',@$IDs; } # push as an album
558	}
559	Analyse(\@list);
560}
561sub Analyse
562{	my $IDs= shift;
563	unless ($RGA_pipeline)
564	{	$RGA_pipeline=GStreamer::Pipeline->new('RGA_pipeline');
565		my $audiobin=GStreamer::Bin->new('RGA_audiobin');
566		#my @elems= qw/filesrc decodebin audioconvert audioresample rganalysis fakesink/;
567		my ($src,$decodebin,$ac,$ar,$rganalysis,$fakesink)=
568			map GStreamer::ElementFactory->make($_ => $_),
569			qw/filesrc decodebin audioconvert audioresample rganalysis fakesink/;
570		$audiobin->add($ac,$ar,$rganalysis,$fakesink);
571		$ac->link($ar,$rganalysis,$fakesink);
572		my $audiopad=$ac->get_pad('sink');
573		$audiobin->add_pad(GStreamer::GhostPad ->new('sink', $audiopad));
574		$RGA_pipeline->add($src,$decodebin,$audiobin);
575		$src->link($decodebin);
576		$decodebin->signal_connect(new_decoded_pad => \&newpad_cb);
577
578		#@elems= map GStreamer::ElementFactory->make($_ => $_), @elems;
579		#$RGA_pipeline->add(@elems);
580		#my $first=shift @elems;
581		#$first->link(@elems);
582		my $bus=$RGA_pipeline->get_bus;
583		$bus->add_signal_watch;
584		$bus->signal_connect('message::error' => sub { warn "ReplayGain analysis error : ".$_[1]->error."\n"; }); #FIXME
585		$bus->signal_connect('message::tag' => \&bus_message_tag);
586		$bus->signal_connect('message::eos' => \&process_next);
587		#FIXME check errors
588	}
589	my $queue= $RGA_pipeline->{queue}||= [];
590	my $nb=0; for my $q (@$queue) { $nb++ for $q=~m/\d+/g; } #count tracks in album lists
591	@$queue= ::uniq(@$queue,@$IDs); #remove redundant IDs
592	$nb=-$nb; for my $q (@$queue) { $nb++ for $q=~m/\d+/g; } #count nb of added tracks
593	::Progress('replaygain', add=>$nb, abortcb=>\&StopAnalysis, title=>_"Replaygain analysis");
594	process_next() unless $::Progress{replaygain}{current}; #FIXME maybe check if $RGA_pipeline is running instead
595}
596sub newpad_cb
597{	my ($decodebin,$pad)=@_;
598	my $audiopad = $RGA_pipeline->get_by_name('RGA_audiobin')->get_pad('sink');
599	return if $audiopad->is_linked;
600	# check media type
601	my $str= $pad->get_caps->get_structure(0)->{name};
602	return unless $str=~m/audio/;
603	$pad->link($audiopad);
604}
605sub process_next
606{	::Progress('replaygain', inc=>1) if $_[0]; #called from callback => one file has been scanned => increment
607	unless ($RGA_pipeline) { ::Progress('replaygain', abort=>1); return; }
608
609	my $rganalysis=$RGA_pipeline->get_by_name('rganalysis');
610	my $ID;
611	if (my $list=$RGA_pipeline->{albumIDs})
612	{	my $i= ++$RGA_pipeline->{album_i};
613		my $left= @$list -$i;
614		$rganalysis->set('num-tracks' => $left);
615		if ($left) {$ID=$list->[$i]; $rganalysis->set_locked_state(1); }
616		else { delete $RGA_pipeline->{$_} for qw/album_i albumIDs album_tosave/; }
617	}
618	$RGA_pipeline->set_state('ready');
619	unless (defined $ID) { $ID=shift @{$RGA_pipeline->{queue}}; };
620	if (defined $ID)
621	{	my @list= split / +/,$ID;
622		if (@list>1) #album mode
623		{	$RGA_pipeline->{albumIDs}= \@list;
624			$rganalysis->set('num-tracks' => scalar @list);
625			$ID=$list[0];
626			$RGA_pipeline->{album_i}=0;
627		}
628		my $f= Songs::Get($ID,'fullfilename_raw');
629		::_utf8_on($f); # pretend it's utf8 to prevent a conversion to utf8 by the bindings
630		$RGA_pipeline->{ID}=$ID;
631		warn "Analysing [$ID] $f\n" if $::Verbose;
632		$RGA_pipeline->get_by_name('filesrc')->set(location => $f);
633		$rganalysis->set_locked_state(0);
634		$RGA_pipeline->set_state('playing');
635	}
636	else
637	{	$RGA_pipeline->set_state('null');
638		$RGA_pipeline=undef;
639	}
640	1;
641}
642sub bus_message_tag
643{	my $msg=$_[1];
644	my $tags=$msg->tag_list;
645	#for my $key (sort keys %$tags) {warn "key=$key => $tags->{$key}\n"}
646	#FIXME should check if the message comes from the rganalysis element, but not supported by the bindings yet, instead check if any non replaygain tags => will re-write replaygain tags _before_ analysis for files without other tags
647	#return if GStreamer->VERSION >=.10 && $msg->src == $RGA_pipeline->get_by_name('rganalysis'); FIXME with Gstreamer 0.10 : $message->src should work, not tested yet !!!! TESTME
648	return unless exists $tags->{'replaygain-track-gain'};
649	return if grep !m/^replaygain-/, keys %$tags; #if other tags than replaygain => doesn't come from the rganalysis element
650
651	my $cID=$RGA_pipeline->{ID};
652	if ($::debug)
653	{	warn "done for ID=$cID\n";
654		warn "done for album IDs=".join(' ',@{$RGA_pipeline->{albumIDs}})."\n" if $RGA_pipeline->{albumIDs} && $RGA_pipeline->get_by_name('rganalysis')->get('num-tracks');
655		for my $f (sort keys %$tags) { my @v=@{$tags->{$f}}; warn " $f : @v\n" }
656	}
657	if ($RGA_pipeline->{albumIDs})
658	{	$RGA_pipeline->{album_tosave}{ $cID }= [@$tags{'replaygain-track-gain','replaygain-track-peak'}];
659		if (exists $tags->{'replaygain-album-gain'} && !$RGA_pipeline->get_by_name('rganalysis')->get('num-tracks'))
660		{	#album done
661			my $IDs= $RGA_pipeline->{albumIDs};
662			my $gainpeak= $RGA_pipeline->{album_tosave};
663			for my $ID (@$IDs)
664			{	@$tags{'replaygain-track-gain','replaygain-track-peak'}= @{$gainpeak->{$ID}};
665				queuewrite($ID,$tags,1);
666			}
667		}
668	}
669	else
670	{	queuewrite($cID,$tags,0);
671	}
672	1;
673}
674sub queuewrite
675{	my ($ID,$tags,$albumtag)=@_;
676	my @keys=qw/replaygain-reference-level replaygain-track-gain replaygain-track-peak/;
677	push @keys, qw/replaygain-album-gain replaygain-album-peak/ if $albumtag;
678	::setlocale(::LC_NUMERIC, 'C');
679	my @modif;
680	for my $key (@keys)
681	{	my $field=$key;
682		$field=~tr/-/_/; # convert replaygain-track-gain to replaygain_track_gain ...
683		push @modif, $field, "$tags->{$key}[0]";#string-ify them with C locale to make sure it's correct
684	}
685	::setlocale(::LC_NUMERIC, '');
686	push @towrite, $ID,\@modif;
687	WriteRGtags() unless $writing;
688}
689sub WriteRGtags
690{	return $writing=0 if !@towrite;
691	$writing=1;
692	my $ID=   shift @towrite;
693	my $modif=shift @towrite;
694	Songs::Set($ID, $modif,
695		abortmsg	=> _"Abort ReplayGain analysis",
696		errormsg	=> _"Error while writing replaygain data",
697		abortcb		=> \&StopAnalysis,
698		callback_finish	=> \&WriteRGtags,
699	);
700}
701sub StopAnalysis
702{	$RGA_pipeline->set_state('null') if $RGA_pipeline;
703	$RGA_pipeline=undef;
704	::Progress('replaygain', abort=>1);
705	@towrite=();
706}
707
708package Play_GST_server;
709use Socket;
710use constant { EOL => "\015\012" };
711our @ISA=('Play_GST');
712
713my (%sockets,$stream,$Server);
714
715$::PlayPacks{Play_GST_server}=1; #register the package
716
717sub init
718{	my $ok=1;
719	my $reg=GStreamer::Registry->get_default;
720	for my $feature (qw/multifdsink lame audioresample audioconvert/)
721	{	next if $reg->lookup_feature($feature);
722		$ok=0;
723		warn "gstreamer plugin '$feature' not found -> gstreamer-server mode not available\n";
724	}
725	return unless $ok;
726	return bless { EQ=>$GST_EQ_ok },__PACKAGE__;
727}
728
729sub Close
730{	close $Server if $Server;
731	$Server=undef;
732}
733
734sub Stop
735{	unless ($_[1])
736	{	for (keys %sockets)
737		{	$sockets{$_}[1]=0; $stream->signal_emit(remove => $_);
738		}
739	}
740	if ($_[1]) { $Sink->set_locked_state(1); $PlayBin->set_state('null'); $Sink->set_locked_state(0);return; }
741	else
742	{	$PlayBin->set_state('null');
743	}
744}
745#was in Play()	#for (keys %sockets) {warn "socket $_ : ".$sockets{$_}[1];;$stream->signal_emit(add => $_ ) if $sockets{$_}[1];}
746
747sub check_sink
748{	$Sink->get_name eq 'server';
749}
750sub make_sink
751{	#return $Sink if $Sink && $Sink->get_name eq 'server';
752	my $sink=GStreamer::Bin->new('server');
753#	my ($aconv,$audioresamp,$vorbisenc,$oggmux,$stream)=
754	(my ($aconv,$audioresamp,$lame),$stream)=
755		GStreamer::ElementFactory -> make
756		(	audioconvert => 'audioconvert',
757			audioresample => 'audioresample',
758			#vorbisenc => 'vorbisenc',
759			#oggmux => 'oggmux',
760			lame => 'lame',
761			multifdsink => 'multifdsink',
762		);
763	#$sink->add($aconv,$audioresamp,$vorbisenc,$oggmux,$stream);
764	$sink->add($aconv,$audioresamp,$lame,$stream);
765	$aconv->link($audioresamp,$lame,$stream);
766	#$aconv->link($audioresamp,$vorbisenc,$oggmux,$stream);
767	$sink->add_pad( GStreamer::GhostPad->new('sink', $aconv->get_pad('sink') ));
768	$stream->set('recover-policy'=>'keyframe');
769	#$stream->signal_connect($_ => sub {warn "@_"},$_) for 'client-removed', 'client_added', 'client-fd-removed';
770	#$stream->signal_connect('client-fd-removed' => sub { Glib::Idle->add(sub {close $sockets{$_[0]}[0]; delete $sockets{$_[0]}},$_[1]); });
771	$stream->signal_connect('client-fd-removed' => sub { close $sockets{$_[1]}[0]; delete $sockets{$_[1]}; ::HasChanged('connections'); }); #FIXME not in main thread, so should be in a Glib::Idle, but doesn't work so ... ?
772	#$stream->signal_connect('client-fd-removed' => sub { warn "@_ ";Glib::Idle->add(sub {warn $_[0];warn "-- $_ ".$sockets{$_} for keys %sockets;unless ($sockets{$_[0]}[1]) { close $sockets{$_[0]}[0] ;warn "closing $_[0]"; delete $sockets{$_[0]};0;} }, $_[1]) });
773	return undef unless Listen();
774	return $sink;
775}
776
777sub Listen
778{	my $proto = getprotobyname('tcp');
779	my $port=$::Options{Icecast_port};
780	my $noerror;
781	{	last unless socket($Server, PF_INET, SOCK_STREAM, $proto);
782		last unless setsockopt($Server, SOL_SOCKET, SO_REUSEADDR,pack('l', 1));
783		last unless bind($Server, sockaddr_in($port, INADDR_ANY));
784		last unless listen($Server,SOMAXCONN);
785		$noerror=1;
786	}
787	unless ($noerror)
788	{	::ErrorPlay("icecast server error : $!");
789		return undef;
790	}
791	Glib::IO->add_watch(fileno($Server),'in', \&Connection);
792	warn "icecast server listening on port $port\n";
793	::HasChanged('connections');
794	return 1;
795}
796
797sub Connection
798{	my $Client;
799	return 0 unless $Server;
800	my $paddr = accept($Client,$Server);
801	return 1 unless $paddr;
802	my($port2,$iaddr) = sockaddr_in($paddr);
803	warn 'Connection from ',inet_ntoa($iaddr), " at port $port2\n";
804	#warn "fileno=".fileno($Client);
805	my $request=<$Client>;warn $request;
806	while (<$Client>)
807	{	warn $_;
808		last if $_ eq EOL;
809	}
810	if ($request=~m#^GET /command\?cmd=(.*?) HTTP/1\.\d\015\012$#)
811	{	my $cmd=::decode_url($1);
812		my $content;
813		if (0) #FIXME add password and disable dangerous commands (RunSysCmd, RunPerlCode and ChangeDisplay)
814		{	::run_command(undef,$cmd);
815			$content='Command sent.';
816		}
817		else {$content='Unauthorized.'}
818		my $answer=
819		'HTTP/1.0 200 OK'.EOL.
820		'Content-Length: '.length($content).EOL.
821		EOL.$content;
822		send $Client,$answer.EOL,0;
823		close $Client;
824		return 1;	#keep listening
825	}
826	my $answer=
827	'HTTP/1.0 200 OK'.EOL.
828	'Server: iceserver/0.2'.EOL.
829	"Content-Type: audio/mpeg".EOL.
830	"x-audiocast-name: gmusicbrowser stream".EOL.
831	'x-audiocast-public: 0'.EOL;
832	send $Client,$answer.EOL,0;
833	#warn $answer;
834	my $fileno=fileno($Client);
835	$sockets{$fileno}=[$Client,1,gethostbyaddr($iaddr,AF_INET)];
836	Glib::IO->add_watch(fileno($Client),'hup',sub {warn "Connection closed"; $sockets{fileno($Client)}[1]=0; ::HasChanged('connections'); $stream->signal_emit(remove => fileno$Client);return 0; }); #FIXME never called
837	$stream->signal_emit(add => fileno$Client);
838	::HasChanged('connections');
839	return 1;	#keep listening
840}
841
842sub get_connections
843{	return map $sockets{$_}[2], grep $sockets{$_}[1],keys %sockets;
844}
845
8461;
847