1# Copyright (C) 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 Glib::Object::Introspection;
10	warn "Using Glib::Object::Introspection version ".Glib::Object::Introspection->VERSION."\n" if $::debug;
11	Glib::Object::Introspection->setup(basename => 'Gst', version => '1.0', package => 'GStreamer1');
12	$::gstreamer_version='1.x';
13	GStreamer1::init_check([ $0, @ARGV ]) or die "Can't initialize gstreamer-1.x\n";
14	my $reg= GStreamer1::Registry::get();
15	$reg->lookup_feature('playbin') or die "gstreamer-1.x plugin 'playbin' not found.\n";
16}
17
18package Play_GST;
19use strict;
20use warnings;
21
22my ($GST_visuals_ok,$GST_EQ_ok,$GST_RG_ok); our $GST_RGA_ok;
23my ($VolumeBusy,$VolumeHasChanged);
24my (%Plugins,%Sinks);
25$::PlayPacks{Play_GST}=1; #register the package
26my $support_install_missing;
27
28BEGIN
29{ %Sinks=
30  (	autoaudio	=> { name => _"auto detect", },
31	oss		=> { option => 'device' },
32	oss4		=> { option => 'device' },
33	alsa		=> { option => 'device' },
34	openal		=> {},
35	pulse		=> { name => "PulseAudio", option=>'server device'},
36	jackaudio	=> { name => "JACK", option => 'server' },
37	osxaudio	=> { option => 'device' },
38	directsound	=> {},
39  );
40  %Plugins=(	mp3 => 'flump3dec mad mpg123audiodec avdec_mp3',
41		oga => 'vorbisdec',			flac=> 'flacdec',
42		ape => 'avdec_ape ffdec_ape',		wv  => 'wavpackdec',
43		mpc => 'musepackdec avdec_mpc8',	m4a => 'faad',
44  );
45
46  my $reg= GStreamer1::Registry::get();
47  $Sinks{$_}{ok}= ! !$reg->lookup_feature($_.'sink') for keys %Sinks;
48  if ($reg->lookup_feature('equalizer-10bands')) { $GST_EQ_ok=1; }
49  else {warn "gstreamer1 plugin 'equalizer-10bands' not found -> equalizer not available\n";}
50  if ($reg->lookup_feature('rglimiter') && $reg->lookup_feature('rgvolume')) { $GST_RG_ok=1; }
51  else {warn "gstreamer1 plugins 'rglimiter' and/or 'rgvolume' not found -> replaygain not available\n";}
52  if ($reg->lookup_feature('rganalysis')) { $GST_RGA_ok=1; }
53  else {warn "gstreamer1 plugins 'rganalysis' not found -> replaygain analysis not available\n";}
54
55  #some functions that should be accessible from perl but are not currently (Glib::Object::Introspection-0.027)
56  *GStreamer1::Bin::add_many= sub {my $b=shift;$b->add($_) for @_} unless *GStreamer1::Bin::add_many{CODE};
57  *GStreamer1::Element::link_many= sub {while (@_>1){my $e=shift;$e->link($_[0])} } unless *GStreamer1::Element::link_many{CODE};
58
59  $GST_visuals_ok=1;
60  # don't know what features are needed for visuals in gstreamer-1.x
61  #unless ($reg->lookup_feature('????'))
62  #{	warn "gstreamer plugin '????' not found -> visuals not available\n"; $GST_visuals_ok=0;
63  #}
64  # setup the gstvideooverlay interface (used for telling playbin the window ID to use for visuals)
65  eval { Glib::Object::Introspection->setup(basename => 'GstVideo', version => '1.0', package => 'GStreamer1::Video'); }; #unless *GStreamer1::Video::VideoOverlay::set_window_handle{CODE};
66  if ($@)
67  {	$GST_visuals_ok=0;
68	warn "Can't setup GStreamer1::Video::VideoOverlay -> visuals not available:\n $@\n";
69  }
70
71  if (1)
72  {	eval { Glib::Object::Introspection->setup(basename => 'GstPbutils', version => '1.0', package => 'GStreamer1::Pbutils') }; # unless *GStreamer1::Pbutils::pb_utils_init{CODE};
73	if (!$@)
74	{	GStreamer1::Pbutils::pb_utils_init();
75		$support_install_missing=1 if GStreamer1::Pbutils::install_plugins_supported();
76		warn "gstreamer says Installing missing plugins not supported by this system\n" if $::debug;
77	}
78	else { warn "Can't setup GStreamer1::Pbutils -> Installing missing plugins not supported\n"; }
79  }
80}
81
82sub supported_formats
83{	my $reg= GStreamer1::Registry::get;
84	my @found;
85	for my $type (keys %Plugins)
86	{	push @found, $type if grep $reg->lookup_feature($_), split / +/, $Plugins{$type};
87	}
88	return @found;
89}
90sub supported_sinks
91{	my $reg= GStreamer1::Registry::get;
92	$Sinks{$_}{ok}= ! !$reg->lookup_feature($_.'sink') for keys %Sinks;
93	return {map { $_ => $Sinks{$_}{name}||$_ } grep $Sinks{$_}{ok}, keys %Sinks};
94}
95
96sub init
97{	my $reg= GStreamer1::Registry::get();
98	$::Options{gst_sink}='' unless $reg->lookup_feature( ($::Options{gst_sink}||'').'sink' );
99	$::Options{gst_sink}||= (grep ($reg->lookup_feature($_.'sink'), qw/autoaudio pulse alsa oss oss4/),'autoaudio')[0]; #find a default sink
100	return bless { EQ=>$GST_EQ_ok, EQpre=>$GST_EQ_ok, visuals => $GST_visuals_ok, RG=>$GST_RG_ok },__PACKAGE__;
101}
102
103
104sub create_playbin
105{	my $self=shift;
106	my $pb= GStreamer1::ElementFactory::make('playbin' => 'playbin');
107	if ($self->{playbin})
108	{	$self->{playbin}->get_bus->remove_signal_watch; #not sure if needed
109		$self->{playbin}->set_state('null');
110	}
111	$self->{playbin}=$pb;
112	$pb->set('flags' => [qw/audio soft-volume/]);
113	$self->SetVolume(''); #initialize volume
114	my $bus=$pb->get_bus;
115	::weaken( $bus->{self}=$self );
116	::weaken( $pb->{self} =$self );
117	$bus->add_signal_watch;
118	$pb->signal_connect("notify::volume" => sub { Glib::Idle->add(\&VolumeChanged,$_[0]) unless $VolumeHasChanged++; },100000) if $::Options{gst_monitor_pa_volume}; # can cause a freeze in some circumstances, in particular when using the scroll wheel on the time bar, see perl-glib bug #620099 (https://bugzilla.gnome.org/show_bug.cgi?id=620099)
119#	$bus->signal_connect('message' => \&bus_message);
120	$bus->signal_connect('message::element' => \&bus_message_missing_plugin) if $support_install_missing;
121	$bus->signal_connect('message::eos' => \&bus_message_end,0);
122	$bus->signal_connect('message::error' => \&bus_message_end,1);
123	$bus->signal_connect('message::state-changed' => \&bus_message_state_changed);
124	$pb->signal_connect(about_to_finish => \&about_to_finish) if $::Options{gst_gapless};
125	$self->connect_visuals if $self->{has_visuals};
126}
127
128sub _parse_error
129{	my $msg=shift;
130	my $s=$msg->get_structure;
131	return $s->get_value('gerror')->message, $s->get_string('debug');
132}
133
134sub bus_message_end
135{	my ($msg,$error)=($_[1],$_[2]);
136	my $self=$_[0]{self};
137	#error msg if $error is true, else eos
138	if ($self->{continuous}) { $self->{sink}->set_locked_state(1); $self->{playbin}->set_state('null'); $self->{sink}->set_locked_state(0); }
139	else { $self->{playbin}->set_state('null'); }
140	if ($error)	{ ::ErrorPlay(_parse_error($msg)); } #can't use $msg->parse_error as it doesn't work currently : "FIXME - GI_TYPE_TAG_ERROR" (Glib::Object::Introspection-0.027)
141	else		{ ::end_of_file(); }
142}
143
144sub bus_message_missing_plugin
145{	my ($bus,$msg)=@_;
146	return unless GStreamer1::Pbutils::is_missing_plugin_message($msg);
147	my $details=GStreamer1::Pbutils::missing_plugin_message_get_installer_detail($msg);
148	warn "missing plugin details: ".$details."\n" if $::debug;
149	GStreamer1::Pbutils::install_plugins_async([$details],undef,
150	 sub
151	 {	return unless $_[0]=~/success/; # success or partial_success
152		GStreamer1::update_registry(); #FIXME "Applications should assume that the registry update is neither atomic nor thread-safe and should therefore not have any dynamic pipelines running (including the playbin and decodebin elements) and should also not create any elements or access the GStreamer registry while the update is in progress"
153	 });
154}
155
156# 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
157# using freeze_notify until the skip is done mostly avoid the problem
158# setting state to paused until the skip is done also mostly avoid the problem
159# 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
160my $StateChanged;
161sub bus_message_state_changed	# used to wait for the right state to do the skip
162{	my $self=$_[0]{self};
163	return unless $self || $self->{skip};
164	return if $self->{state_changed};
165	my $playbin= $self->{playbin};
166	$self->{state_changed}=1;
167	$self->{playbin}->freeze_notify unless $self->{notify_frozen}; $self->{notify_frozen}=1; #freeze notify until skip is done
168	Glib::Idle->add(sub
169	{	$self->SkipTo($self->{skip}) if $self->{skip}; # this will only skip if the state is right, else will wait for another state change
170		unless ($self->{skip}) { $self->{playbin}->thaw_notify if delete $self->{notify_frozen}; } #if skip is done, unfreeze
171		$self->{state_changed}=0;
172		0;
173	});
174}
175
176
177sub check_sink
178{	my $self=shift;
179	$self->{sink}->get_name eq $::Options{gst_sink};
180}
181
182sub create_sink
183{	my $self=shift;
184	my $sinkname=$::Options{gst_sink};
185	my $sink=GStreamer1::ElementFactory::make($sinkname.'sink' => $sinkname);
186	return undef unless $sink;
187	#$sink->set(profile => 'music') if $::Options{gst_sink} eq 'gconfaudio';
188	if (my $opts=$Sinks{$sinkname}{option})
189	{	for my $opt (split / /, $opts)
190		{	my $val=$::Options{'gst_'.$sinkname.'_'.$opt};
191			next unless defined $val && $val ne '';
192			$sink->set($opt => $val);
193		}
194	}
195	$self->{sink}=$sink;
196}
197
198sub init_sink
199{	my $self=shift;
200	delete $self->{modif};
201	$self->create_sink;
202	my $sink= $self->{sink};
203	unless ($sink) { ::ErrorPlay( ::__x(_"Can't create sink '{sink}'", sink => $::Options{gst_sink}) );return }
204
205	my @elems;
206	$sink->{EQ}= $GST_EQ_ok && $::Options{use_equalizer};
207	if ($sink->{EQ})
208	{	my $preamp=   GStreamer1::ElementFactory::make('volume' => 'equalizer-preamp');
209		my $equalizer=GStreamer1::ElementFactory::make('equalizer-10bands' => 'equalizer');
210		my @val= split /:/, $::Options{equalizer};
211		::setlocale(::LC_NUMERIC, 'C');
212		$equalizer->set( 'band'.$_ => $val[$_]) for 0..9;
213		$preamp->set( volume => $::Options{equalizer_preamp}**3);
214		::setlocale(::LC_NUMERIC, '');
215		push @elems,$preamp,$equalizer;
216	}
217	$sink->{RG}= $GST_RG_ok && $::Options{use_replaygain};
218	if ($sink->{RG})
219	{	my ($rgv,$rgl,$ac,$ar)=	map GStreamer1::ElementFactory::make($_=>$_),
220				qw/rgvolume rglimiter audioconvert audioresample/;
221		$self->RG_set_options($rgv,$rgl);
222		push @elems, $rgv,$rgl,$ac,$ar;
223	}
224	if (my $custom=$::Options{gst_custom})
225	{	$custom="( $custom )" if $custom=~m/^\s*\w/ && $custom=~m/!/;	#make a Bin by default instead of a pipeline
226		my $elem= eval { GStreamer1::parse_launch($custom) };
227		warn "gstreamer custom pipeline error : $@\n" if $@;
228		if ($elem && $elem->isa('GStreamer1::Bin'))
229		{	my $first=my $last=$elem;
230			# will work at least for simple cases #FIXME could be better
231			$first=($first->list_iterate_sorted)[0]  while $first->isa('GStreamer1::Bin');
232			$last =($last->list_iterate_sorted)[-1] while  $last->isa('GStreamer1::Bin');
233			$elem->add_pad( GStreamer1::GhostPad->new('sink', $last->get_static_pad('sink') ));
234			$elem->add_pad( GStreamer1::GhostPad->new('src', $first->get_static_pad('src') ));
235		}
236		push @elems, $elem if $elem;
237	}
238	my $playbin= $self->{playbin};
239	if (@elems)
240	{	my $sink0=GStreamer1::Bin->new('sink0');
241		push @elems,$sink;
242		$sink0->add_many(@elems);
243		my $pad= $elems[0]->get_static_pad('sink');
244		GStreamer1::Element::link_many(@elems);
245		$sink0->add_pad( GStreamer1::GhostPad->new('sink',$pad));
246		$playbin->set('audio-sink' => $sink0);
247	}
248	else {$playbin->set('audio-sink' => $sink);}
249}
250
251#convenience function for getting a list instead of having to deal with iterators
252sub GStreamer1::Bin::list_iterate_sorted
253{	my $bin=shift;
254	my $iter= $bin->iterate_sorted;
255	my @children;
256	$iter->foreach(sub {push @children,$_[0]});
257	return @children;
258}
259
260sub about_to_finish	#GAPLESS
261{	#warn "-------about_to_finish  $::NextFileToPlay\n";
262	my $self=$_[0]{self};
263	return unless $self && $::NextFileToPlay;
264	$self->set_file($::NextFileToPlay);
265	$self->{already_next_song}=$::NextFileToPlay;
266	$::NextFileToPlay=0;
267}
268
269sub Play
270{	my($self,$file,$skip)=@_;
271	$self->{skip}=$skip;
272	my $sink= $self->{sink};
273	my $keep= $sink && $self->check_sink;
274	if ($keep)
275	{	my $useEQ= $GST_EQ_ok && $::Options{use_equalizer};
276		my $useRG= $GST_RG_ok && $::Options{use_replaygain};
277		$keep=0 if $sink->{EQ} xor $useEQ;
278		$keep=0 if $sink->{RG} xor $useRG;
279		$keep=0 if $self->{modif}; #advanced options changed
280	}
281	if ($self->{already_next_song} && $self->{already_next_song} eq $file && $keep && !$skip)
282	{	$self->{already_next_song}=undef;
283		return;
284	}
285	$self->{already_next_song}=undef;
286	if ($keep)
287	{	$self->Stop(1);
288	}
289	else
290	{	$self->create_playbin;
291		warn "Creating new gstreamer sink\n" if $::debug;
292		$self->init_sink;
293	}
294
295	warn "playing $file\n" if $::Verbose;
296	$self->set_file($file);
297	my $newstate='playing'; $self->{state_after_skip}=undef;
298	if ($skip) { $newstate='paused'; $self->{state_after_skip}='playing'; }
299	$self->{playbin}->set_state($newstate);
300	$self->{watch_tag} ||= Glib::Timeout->add(500,\&UpdateTime,$self);
301}
302sub set_file
303{	my ($self,$f)=@_;
304	if ($f!~m#^([a-z]+)://#)
305	{	$f=~s#([^A-Za-z0-9-/\.])#sprintf('%%%02X', ord($1))#seg;
306		$f='file://'.$f;
307	}
308	$self->{playbin}->set(uri => $f);
309}
310
311sub UpdateTime
312{	my $self=shift;
313	my $playbin= $self->{playbin};
314	my ($result,$state,$pending)= $playbin->get_state(0);
315	warn "state: $result,$state,$pending\n" if $::debug;
316	return 1 if $result eq 'async';
317	if ($state ne 'playing' && $state ne 'paused')
318	{	return 1 if $pending eq 'playing' || $pending eq 'paused';
319		::ResetTime() unless $self->{continuous};
320		$self->{watch_tag}=undef;
321		return 0;
322	}
323	my $query=GStreamer1::Query->new_position('time');
324	if ($playbin->query($query))
325	{	my (undef, $position)=$query->parse_position;
326		$position/=1_000_000_000;
327		if ($self->{already_next_song} && $position<$::PlayTime)
328		{	warn "UpdateTime: gapless change to next song\n" if $::debug;
329			::end_of_file_faketime();
330		}
331		::UpdateTime($position);
332	}
333	return 1;
334}
335
336sub Stop
337{	my $self=shift;
338	if (my $playbin= $self->{playbin})
339	{	if ($self->{continuous}) { $self->{sink}->set_locked_state(1); $playbin->set_state('null'); $self->{sink}->set_locked_state(0); }
340		else { $playbin->set_state('null'); }
341	}
342	$self->{state_after_skip}=undef;
343	$self->{already_next_song}=undef;
344	if (my $w=$self->{visual_window}) { $w->queue_draw }
345}
346
347sub SkipTo
348{	my ($self,$skip)=@_;
349	$self->{skip}=$skip;
350	my $playbin= $self->{playbin};
351	my ($result,$state,$pending)=$playbin->get_state(0);
352	return if $result eq 'async'; #when song hasn't started yet, needs to wait until it has started before skipping
353	$playbin->seek(1,'time','flush','set', $skip*1_000_000_000,'none',0);
354	if (my $new=delete $self->{state_after_skip}) { $playbin->set_state($new); }
355	delete $self->{skip};
356}
357
358sub Pause
359{	my $self=shift;
360	$self->{playbin}->set_state('paused');
361	$self->{state_after_skip}=undef;
362}
363sub Resume
364{	my $self=shift;
365	$self->{playbin}->set_state('playing');
366	$self->{state_after_skip}=undef;
367}
368
369sub GetVolume	{$::Volume}
370sub GetMute	{$::Mute}
371sub SetVolume
372{	my ($self,$set)=@_;
373	if	($set eq 'mute')	{ $::Mute=$::Volume; $::Volume=0;}
374	elsif	($set eq 'unmute')	{ $::Volume=$::Mute; $::Mute=0;  }
375	elsif	($set=~m/^\+(\d+)$/)	{ $::Volume+=$1; }
376	elsif	($set=~m/^-(\d+)$/)	{ $::Volume-=$1; }
377	elsif	($set=~m/(\d+)/)	{ $::Volume =$1; }
378	$::Volume=0   if $::Volume<0;
379	$::Volume=100 if $::Volume>100;
380	$self->{volume_busy}=1;
381	my $pb= $self->{playbin};
382	$pb->set(volume => ( ($::Mute||$::Volume) /100)**3, mute => !!$::Mute) if $pb; 	#use a cubic volume scale
383	$self->{volume_busy}=0;
384	$::Options{Volume}=$::Volume;
385	$::Options{Volume_mute}=$::Mute;
386	::QHasChanged('Vol');
387}
388sub VolumeChanged
389{	$VolumeHasChanged=0;
390	my $self=$_[0]{self};
391	return 0 if $self->{volume_busy};
392	return 0 unless $self->{playbin};
393	my ($volume,$mute)= $self->{playbin}->get('volume','mute');
394	$volume= $volume ** (1/3) *100; #use a cubic volume scale
395	$volume= sprintf '%d',$volume;
396	$volume=100 if $volume>100;
397	#return 0 unless $volume!=$::Volume || ($mute xor !!$::Mute);
398	if ($mute)	{ $::Mute=$volume; $::Volume=0; }
399	else		{ $::Mute=0; $::Volume=$volume; }
400	$::Options{Volume}=$::Volume;
401	$::Options{Volume_mute}=$::Mute;
402	::QHasChanged('Vol');
403	0; #called from an idle
404}
405
406sub set_equalizer_preamp
407{	my ($self,$volume)=@_;
408	my $preamp= $self->{playbin} && $self->{playbin}->get_by_name('equalizer-preamp');
409	$preamp->set( volume => $volume**3) if $preamp;
410}
411sub set_equalizer
412{	my ($self,$values)=@_;
413	my $equalizer= $self->{playbin} && $self->{playbin}->get_by_name('equalizer');
414	return unless $equalizer;
415	my @vals= split /:/,$values;
416	$equalizer->set( 'band'.$_ => $vals[$_] ) for 0..9;
417}
418
419sub _throwaway_equalizer #return false if option to sync is disabled
420{	$::Options{gst_sync_EQpresets} && GStreamer1::ElementFactory::make('equalizer-10bands' => 'equalizer');
421}
422sub EQ_Import_Presets
423{	my $equalizer= _throwaway_equalizer;
424	return unless $equalizer;
425	my $new;
426	for my $name (@{ $equalizer->get_preset_names })
427	{	next if $::Options{equalizer_presets}{$name}; #ignore if one by that name already exist
428		$new++;
429		$equalizer->load_preset($name);
430		$::Options{equalizer_presets}{$name}= join ':',map $equalizer->get('band'.$_), 0..9;
431	}
432	::HasChanged('Equalizer','presetlist') if $new;
433}
434sub EQ_Save_Preset
435{	my (undef,$name,$values)=@_;
436	my $equalizer= _throwaway_equalizer;
437	return unless $equalizer;
438	if ($values)
439	{	my @vals= split /:/,$values;
440		$equalizer->set( 'band'.$_ => $vals[$_]) for 0..9;
441		$equalizer->save_preset($name);
442	}
443	else
444	{	$equalizer->delete_preset($name)
445	}
446}
447
448sub EQ_Get_Range
449{	my $self=shift;
450	$self->create_playbin unless $self->{playbin};
451	my ($min,$max)=(-1,1);
452	{	my $equalizer= $self->{playbin}->get_by_name('equalizer')
453		 || GStreamer1::ElementFactory::make('equalizer-10bands' => 'equalizer');
454		last unless $equalizer;
455		my $prop= $equalizer->find_property('band0');
456		last unless $prop;
457		$min=$prop->get_minimum;
458		$max=$prop->get_maximum;
459	}
460	my $unit= ($max==1 && $min==-1) ? '' : 'dB';
461	return ($min,$max,$unit);
462}
463sub EQ_Get_Hz
464{	my ($self,$i)=@_;
465	$self->create_playbin unless $self->{playbin};
466	my $equalizer=  $self->{playbin}->get_by_name('equalizer')
467	 || GStreamer1::ElementFactory::make('equalizer-10bands' => 'equalizer');
468	return undef unless $equalizer;
469	my $hz= $equalizer->find_property('band'.$i)->get_nick;
470	if ($hz=~m/^(\d+)\s*(k?)Hz/)
471	{	$hz=$1; $hz*=1000 if $2;
472		$hz= $hz>=1000 ? sprintf '%.1fkHz',$hz/1000 :
473				 sprintf '%dHz',$hz ;
474	}
475	return $hz;
476}
477
478sub RG_set_options
479{	my ($self,$rgv,$rgl)=@_;
480	my $playbin= $self->{playbin};
481	return unless $playbin;
482	$rgv||=$playbin->get_by_name('rgvolume');
483	$rgl||=$playbin->get_by_name('rglimiter');
484	return unless $rgv && $rgl;
485	$rgl->set(enabled	=> $::Options{rg_limiter});
486	$rgv->set('album-mode'	=> !!$::Options{rg_albummode});
487	$rgv->set('pre-amp'	=> $::Options{rg_preamp}||0);
488	$rgv->set('fallback-gain'=>$::Options{rg_fallback}||0);
489	#$rgv->set(headroom => $::Options{gst_rg_headroom}||0);
490}
491
492sub AdvancedOptions
493{	my $self=shift;
494	my $vbox=Gtk2::VBox->new(::FALSE, 2);
495	my $modif_cb= sub { $self->{modif}=1 };
496	my $gapless= ::NewPrefCheckButton(gst_gapless => _"enable gapless (experimental)", cb=> $modif_cb);
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	$vbox->pack_start($monitor_volume,::FALSE,::FALSE,2);
501
502	my $sync_EQpresets= ::NewPrefCheckButton(gst_sync_EQpresets => _"Synchronize equalizer presets", cb=> sub { EQ_Import_Presets(); $modif_cb->() }, tip=>_"Imports gstreamer presets, and synchronize modifications made with gmusicbrowser");
503	$vbox->pack_start($sync_EQpresets,::FALSE,::FALSE,2);
504
505	my $sg1=Gtk2::SizeGroup->new('horizontal');
506	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');
507	$vbox->pack_start($custom,::FALSE,::FALSE,2);
508	for my $s (sort grep $Sinks{$_}{ok} && $Sinks{$_}{option}, keys %Sinks)
509	{	my $label= $Sinks{$s}{name}||$s;
510		for my $opt (sort split / /,$Sinks{$s}{option})
511		{	my $hbox=::NewPrefEntry("gst_${s}_$opt", "$s $opt : ", cb => $modif_cb, sizeg1 => $sg1, expand => 1);
512			$vbox->pack_start($hbox,::FALSE,::FALSE,2);
513		}
514	}
515	return $vbox;
516}
517
518sub add_visuals
519{	my ($self,$window)=@_;
520	::weaken( $window->{playobject}=$self );
521	unshift @{$self->{visual_windows}}, $window; ::weaken($self->{visual_windows}[0]);
522	$self->{has_visuals}++;
523	$window->set_double_buffered(0);
524	$window->signal_connect(unrealize => sub { my $self=$_[0]{playobject}; $self->remove_visuals($_[0]) if $self; });
525	$window->signal_connect(expose_event => sub
526	 {	my $self=$_[0]{playobject};
527		if ($self && $_[0]{visuals_on} && defined $::TogPlay) { $self->{playbin}->expose }
528		else #not connected or stopped -> draw black background
529		{	my ($widget,$event)=@_;
530			$widget->window->draw_rectangle($widget->style->black_gc,::TRUE,$event->area->values);
531		}
532		1;
533	 });
534	if ($window->window) { $self->connect_visuals }
535	else
536	{	$window->signal_connect(realize => sub { my $self=$_[0]{playobject}; $self->connect_visuals if $self; });
537	}
538}
539
540sub connect_visuals
541{	my $self=shift;
542	my ($window)= grep $_ && $_->window, @{$self->{visual_windows}};
543	return unless $window && $self->{playbin};
544	if (my $w=$self->{visual_window}) { return if $w==$window; $w->{visuals_on}=0; $w->queue_draw; }
545	$self->{visual_window}= $window;
546	$window->{visuals_on}=1;
547	$self->create_playbin unless $self->{playbin};
548	$self->{playbin}->set_window_handle($window->window->XID);
549	$self->{playbin}->set('flags' => [qw/audio soft-volume vis/]);
550	$self->{playbin}->set('force-aspect-ratio',0);
551	$self->set_visual;
552}
553sub remove_visuals
554{	my ($self,$window)=@_;
555	my $wlist= $self->{visual_windows};
556	@$wlist= grep $_ && $_!=$window, @$wlist;
557	::weaken($_) for @$wlist; #re-weaken references (the grep above made them strong again)
558	$self->{has_visuals}= @$wlist;
559	return unless $window->{visuals_on};
560	$window->{visuals_on}=0;
561	my $new= $wlist->[0];
562	if ($new && $new->window) { $self->connect_visuals($new) }
563	elsif ($self->{playbin})
564	{	$self->{playbin}->set('flags' => [qw/audio soft-volume/]);
565		$self->{playbin}->set_window_handle(0);
566		$self->{visual_window}= undef;
567	}
568}
569sub set_visual
570{	my $self=shift;
571	my $visual= shift || $::Options{gst_visual} || '';
572	my @l=list_visuals();
573	return unless @l;
574	if ($visual eq '+') #choose next visual in the list
575	{	$visual=$::Options{gst_visual} || $l[0];
576		my $i=0;
577		for my $v (@l)
578		{	last if $v eq $visual;
579			$i++
580		}
581		$i++; $i=0 if $i>$#l;
582		$visual=$l[$i];
583	}
584	elsif (!(grep $_ eq $visual, @l)) # if visual not found in list
585	{	$visual=undef;
586	}
587	$visual||=$l[0];
588	warn "visual=$visual\n" if $::debug;
589	$::Options{gst_visual}=$visual;
590	$visual= GStreamer1::ElementFactory::make($visual => 'visual');
591	if (my $pb=$self->{playbin})
592	{	$pb->set('vis-plugin' => $visual);
593		$pb->expose;
594	}
595}
596sub list_visuals
597{	my @visuals;
598	my $reg= GStreamer1::Registry::get;
599	for my $plugin (@{$reg->get_plugin_list})
600	{	#warn $plugin->get_name;
601		my $list=$reg->get_feature_list_by_plugin($plugin->get_name);
602		next unless $list;
603		for my $elem (@$list)
604		{	if ($elem->isa('GStreamer1::ElementFactory'))
605			{	my $klass= $elem->get_metadata('klass');
606				next unless $klass eq 'Visualization';
607				push @visuals,$elem->get_name;
608			}
609		}
610	}
611	return @visuals;
612}
613
614
615package GMB::GST_ReplayGain;
616
617my $RGA_pipeline;
618my (@towrite,$writing);
619my $RGA_songmenu=
620{ label => _"Replaygain analysis",	notempty => 'IDs', notmode => 'P', test => sub {$Play_GST::GST_RGA_ok && $::Options{gst_rg_songmenu}; },
621 submenu =>
622 [	{ label => _"Scan this file",			code => sub { Analyse ($_[0]{IDs}); },		onlyone => 'IDs', },
623	{ label => _"Scan per-file track gain",		code => sub { Analyse ($_[0]{IDs}); },		onlymany=> 'IDs', },
624	{ label => _"Scan using tag-defined album", 	code => sub { Analyse_byAlbum ($_[0]{IDs}); },	onlymany=> 'IDs', },
625	{ label => _"Scan as an album",			code => sub { Analyse([join ' ',@{ $_[0]{IDs} }]); },	onlymany=> 'IDs', },
626 ],
627};
628push @::SongCMenu,$RGA_songmenu;
629
630sub Analyse_full
631{	my $added='';
632	my @todo;
633	my $IDs_in_album=  Filter->new('album:-e:')->filter;	#get songs with an album name
634	my ($again,$apeak,$ids)=Songs::BuildHash('album',$IDs_in_album,undef,'replaygain_album_gain:same','replaygain_album_peak:same','id:list');
635	for my $aid (keys %$again)
636	{	next if @{$ids->{$aid}}<2;	#ignore albums with less than 2 songs
637		my $gain= $again->{$aid};
638		my $peak= $apeak->{$aid};
639		next if $gain==$gain && $peak==$peak;	#NaN : album gain/peak not defined or not the same for all songs from album
640		my $IDs= $ids->{$aid};
641		push @todo,join ' ',@$IDs;
642		vec($added,$_,1)=1 for @$IDs;
643	}
644	my $IDs_no_rg= Filter->newadd(0, 'replaygain_track_gain:-defined:1', 'replaygain_track_peak:-defined:1')->filter;
645	push @todo, grep !vec($added,$_,1), @$IDs_no_rg;
646	Analyse(\@todo) if @todo;
647}
648
649sub Analyse_byAlbum
650{	my @IDs= ::uniq(@{ $_[0] });
651	my $hash= Songs::BuildHash('album',\@IDs,undef,'id:list');
652	my @list;
653	for my $aid (keys %$hash)
654	{	my $IDs= $hash->{$aid};
655		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
656		else { push @list, join ' ',@$IDs; } # push as an album
657	}
658	Analyse(\@list);
659}
660sub Analyse
661{	my $IDs= shift;
662	unless ($RGA_pipeline)
663	{	$RGA_pipeline=GStreamer1::Pipeline->new('RGA_pipeline');
664		my $audiobin=GStreamer1::Bin->new('RGA_audiobin');
665		my ($src,$decodebin,$ac,$ar,$rganalysis,$fakesink)=
666			map GStreamer1::ElementFactory::make($_ => $_),
667			qw/filesrc decodebin audioconvert audioresample rganalysis fakesink/;
668		$audiobin->add_many($ac,$ar,$rganalysis,$fakesink);
669		$ac->link_many($ar,$rganalysis,$fakesink);
670		my $audiopad=$ac->get_static_pad('sink');
671		$audiobin->add_pad(GStreamer1::GhostPad ->new('sink', $audiopad));
672		$RGA_pipeline->add_many($src,$decodebin,$audiobin);
673		$src->link($decodebin);
674		$decodebin->signal_connect(pad_added => \&newpad_cb);
675
676		my $bus=$RGA_pipeline->get_bus;
677		$bus->add_signal_watch;
678		$bus->signal_connect('message::error' => sub { warn "ReplayGain analysis error : ".join(":\n ",Play_GST::_parse_error($_[1]))."\n"; }); #can't use $msg->parse_error as it doesn't work currently : "FIXME - GI_TYPE_TAG_ERROR" (Glib::Object::Introspection-0.027)
679		$bus->signal_connect('message::tag' => \&bus_message_tag);
680		$bus->signal_connect('message::eos' => \&process_next);
681		#FIXME check errors
682	}
683	my $queue= $RGA_pipeline->{queue}||= [];
684	my $nb=0; for my $q (@$queue) { $nb++ for $q=~m/\d+/g; } #count tracks in album lists
685	@$queue= ::uniq(@$queue,@$IDs); #remove redundant IDs
686	$nb=-$nb; for my $q (@$queue) { $nb++ for $q=~m/\d+/g; } #count nb of added tracks
687	::Progress('replaygain', add=>$nb, abortcb=>\&StopAnalysis, title=>_"Replaygain analysis");
688	process_next() unless $::Progress{replaygain}{current}; #FIXME maybe check if $RGA_pipeline is running instead
689}
690sub newpad_cb
691{	my ($decodebin,$pad)=@_;
692	my $audiopad = $RGA_pipeline->get_by_name('RGA_audiobin')->get_static_pad('sink');
693	return if $audiopad->is_linked;
694	# check media type
695	my $str= $pad->get_current_caps->get_structure(0)->get_name;
696	return unless $str=~m/audio/;
697	$pad->link($audiopad);
698}
699sub process_next
700{	::Progress('replaygain', inc=>1) if $_[0]; #called from callback => one file has been scanned => increment
701	unless ($RGA_pipeline) { ::Progress('replaygain', abort=>1); return; }
702
703	my $rganalysis= $RGA_pipeline->get_by_name('rganalysis');
704	my $ID;
705	if (my $list=$RGA_pipeline->{albumIDs})
706	{	my $i= ++$RGA_pipeline->{album_i};
707		my $left= @$list -$i;
708		$rganalysis->set('num-tracks' => $left);
709		if ($left) {$ID=$list->[$i]; $rganalysis->set_locked_state(1); }
710		else { delete $RGA_pipeline->{$_} for qw/album_i albumIDs album_tosave/; }
711	}
712	$RGA_pipeline->set_state('ready');
713	unless (defined $ID) { $ID=shift @{$RGA_pipeline->{queue}}; };
714	if (defined $ID)
715	{	my @list= split / +/,$ID;
716		if (@list>1) #album mode
717		{	$RGA_pipeline->{albumIDs}= \@list;
718			$rganalysis->set('num-tracks' => scalar @list);
719			$ID=$list[0];
720			$RGA_pipeline->{album_i}=0;
721		}
722		my $f= Songs::Get($ID,'fullfilename_raw');
723		::_utf8_on($f); # pretend it's utf8 to prevent a conversion to utf8 by the bindings
724		$RGA_pipeline->{ID}=$ID;
725		warn "Analysing [$ID] $f\n" if $::Verbose;
726		$RGA_pipeline->get_by_name('filesrc')->set(location => $f);
727		$RGA_pipeline->set_state('playing');
728		if ($rganalysis->is_locked_state) # for album mode: unlock $rganalysis
729		{	#For some reason, GStreamer 1.0's rganalysis element produces an error here unless a flush has been performed
730			# http://66125.n4.nabble.com/Problem-with-GStreamer-1-0-EOS-and-set-locked-state-tp4656994.html
731			# work-around found in https://bitbucket.org/fk/rgain thanks
732			my $pad= $rganalysis->get_static_pad('src');
733			$pad->send_event( GStreamer1::Event->new_flush_start );
734			$pad->send_event( GStreamer1::Event->new_flush_stop(1) );
735			$rganalysis->set_locked_state(0);
736		}
737	}
738	else
739	{	$RGA_pipeline->set_state('null');
740		$RGA_pipeline=undef;
741	}
742	1;
743}
744sub bus_message_tag
745{	my $msg=$_[1];
746	my $taglist=$msg->parse_tag;
747	#warn $tags->to_string;
748	#warn GStreamer1::tag_get_type('replaygain-track-gain');
749	my (%tags,$count);
750	for my $field (qw/replaygain-reference-level replaygain-track-gain replaygain-track-peak replaygain-album-gain replaygain-album-peak/)
751	{	my ($nvalues,$firstvalue)= $taglist->get_double($field);
752		next unless $nvalues;
753		$count++;
754		$tags{$field}= $firstvalue;
755		#warn "$field: $firstvalue\n";
756	}
757	return unless $count && $count == $taglist->n_tags; #if other tags than replaygain => doesn't come from the rganalysis element
758
759	my $cID=$RGA_pipeline->{ID};
760	if ($::debug)
761	{	warn "done for ID=$cID\n";
762		warn "done for album IDs=".join(' ',@{$RGA_pipeline->{albumIDs}})."\n" if $RGA_pipeline->{albumIDs} && $RGA_pipeline->get_by_name('rganalysis')->get('num-tracks');
763		for my $f (sort keys %tags) { warn " $f : $tags{$f}\n" }
764	}
765	if ($RGA_pipeline->{albumIDs})
766	{	# album mode: store the values for when album is done
767		$RGA_pipeline->{album_tosave}{ $cID }= [@tags{'replaygain-track-gain','replaygain-track-peak'}];
768		if (exists $tags{'replaygain-album-gain'} && !$RGA_pipeline->get_by_name('rganalysis')->get('num-tracks'))
769		{	#album done
770			my $IDs= $RGA_pipeline->{albumIDs};
771			my $gainpeak= $RGA_pipeline->{album_tosave};
772			for my $ID (@$IDs)
773			{	@tags{'replaygain-track-gain','replaygain-track-peak'}= @{$gainpeak->{$ID}};
774				queuewrite($ID,\%tags,1);
775			}
776		}
777	}
778	else
779	{	queuewrite($cID,\%tags,0);
780	}
781	1;
782}
783sub queuewrite
784{	my ($ID,$tags,$albumtag)=@_;
785	my @keys=qw/replaygain-reference-level replaygain-track-gain replaygain-track-peak/;
786	push @keys, qw/replaygain-album-gain replaygain-album-peak/ if $albumtag;
787	::setlocale(::LC_NUMERIC, 'C');
788	my @modif;
789	for my $key (@keys)
790	{	my $field=$key;
791		$field=~tr/-/_/; # convert replaygain-track-gain to replaygain_track_gain ...
792		push @modif, $field, "$tags->{$key}";#string-ify them with C locale to make sure it's correct
793	}
794	::setlocale(::LC_NUMERIC, '');
795	push @towrite, $ID,\@modif;
796	WriteRGtags() unless $writing;
797}
798sub WriteRGtags
799{	return $writing=0 if !@towrite;
800	$writing=1;
801	my $ID=   shift @towrite;
802	my $modif=shift @towrite;
803	Songs::Set($ID, $modif,
804		abortmsg	=> _"Abort ReplayGain analysis",
805		errormsg	=> _"Error while writing replaygain data",
806		abortcb		=> \&StopAnalysis,
807		callback_finish	=> \&WriteRGtags,
808	);
809}
810sub StopAnalysis
811{	$RGA_pipeline->set_state('null') if $RGA_pipeline;
812	$RGA_pipeline=undef;
813	::Progress('replaygain', abort=>1);
814	@towrite=();
815}
816
817
818
819package Play_GST_server;
820use Socket;
821use constant { EOL => "\015\012" };
822our @ISA=('Play_GST');
823
824my %Encodings;
825
826BEGIN
827{ %Encodings=
828  (	vorbis	=> { pipeline=>'vorbisenc oggmux',	mime=>'application/ogg',},
829	mp3	=> { pipeline=>'lamemp3enc',		mime=>'audio/mpeg'},
830  );
831}
832
833$::PlayPacks{Play_GST_server}=1; #register the package
834
835sub init
836{	my $ok=1;
837	my $reg= GStreamer1::Registry::get;
838	for my $feature (qw/multifdsink lamemp3enc audioresample audioconvert/)
839	{	next if $reg->lookup_feature($feature);
840		$ok=0;
841		warn "gstreamer plugin '$feature' not found -> gstreamer-server mode not available\n";
842	}
843	return unless $ok;
844	return bless { EQ=>$GST_EQ_ok, visuals => $GST_visuals_ok, RG=>$GST_RG_ok },__PACKAGE__;
845}
846
847sub Close
848{	my $self=shift;
849	close $self->{server} if $self->{server};
850	$self->{server}=$self->{sink}=undef;
851}
852
853sub Play
854{	my $self=shift;
855	$self->{continuous}=1;
856	Play_GST::Play($self,@_);
857}
858
859sub Stop
860{	my ($self,$partialstop)=@_;
861	unless ($partialstop)
862	{	my $sockets= $self->{stream}{sockets};
863		for (keys %$sockets)
864		{	$sockets->{$_}[1]=0; $self->{stream}->signal_emit(remove => $_);
865		}
866		$self->{continuous}=0;
867	}
868	Play_GST::Stop($self);
869}
870
871sub check_sink {1}
872sub create_sink
873{	my $self=shift;
874	my $sink=GStreamer1::Bin->new('server');
875	my @pipeline=(qw/audioconvert audioresample/);
876	my $encoding= $::Options{gst_server_encoding}||'';
877	$encoding= 'mp3' unless $Encodings{$encoding};
878	$self->{encoding}= $encoding;
879	push @pipeline, split / +/, $Encodings{$encoding}{pipeline};
880	push @pipeline, 'multifdsink';
881	my ($aconv,@elems)= map GStreamer1::ElementFactory::make($_=>$_), @pipeline;
882	$sink->add_many($aconv,@elems);
883	$aconv->link_many(@elems);
884	my $stream= $self->{stream}= pop @elems;
885	$stream->{sockets}={};
886	$sink->add_pad( GStreamer1::GhostPad->new('sink', $aconv->get_static_pad('sink') ));
887	$stream->set('recover-policy'=>'keyframe');
888	#$stream->signal_connect($_ => sub {warn "@_"},$_) for 'client-removed', 'client_added', 'client-fd-removed';
889	$stream->signal_connect('client-fd-removed' => sub { my $sockets=$_[0]{sockets}; close $sockets->{$_[1]}[0]; delete $sockets->{$_[1]}; ::QHasChanged('connections'); });
890	return undef unless $self->Listen;
891	return $self->{sink}=$sink;
892}
893
894sub Listen
895{	my $self=shift;
896	my $server;
897	my $proto = getprotobyname('tcp');
898	my $port=$::Options{Icecast_port};
899	my $noerror;
900	{	last unless socket($server, PF_INET, SOCK_STREAM, $proto);
901		last unless setsockopt($server, SOL_SOCKET, SO_REUSEADDR,pack('l', 1));
902		last unless bind($server, sockaddr_in($port, INADDR_ANY));
903		last unless listen($server,SOMAXCONN);
904		$noerror=1;
905	}
906	unless ($noerror)
907	{	::ErrorPlay("icecast server error : $!");
908		return undef;
909	}
910	$self->{server}= $server;
911	Glib::IO->add_watch(fileno($server),'in', \&Connection,$self);
912	warn "icecast server listening on port $port\n";
913	::HasChanged('connections');
914	return 1;
915}
916
917sub Connection
918{	my $self=$_[2];
919	my $client;
920	return 0 unless $self->{server};
921	my $paddr = accept($client,$self->{server});
922	return 1 unless $paddr;
923	my($port2,$iaddr) = sockaddr_in($paddr);
924	warn 'Connection from ',inet_ntoa($iaddr), " at port $port2\n" if $::Verbose;
925	my $request=<$client>;
926	warn " $request" if $::debug;
927	while (<$client>)
928	{	warn " $_" if $::debug;
929		last if $_ eq EOL;
930	}
931	if ($request=~m#^GET /command\?cmd=(.*?) HTTP/1\.\d\015\012$#)
932	{	my $cmd=::decode_url($1);
933		my $content;
934		if (0) #FIXME add password and disable dangerous commands (RunSysCmd, RunPerlCode and ChangeDisplay)
935		{	::run_command(undef,$cmd);
936			$content='Command sent.';
937		}
938		else {$content='Unauthorized.'}
939		my $answer=
940		'HTTP/1.0 200 OK'.EOL.
941		'Content-Length: '.length($content).EOL.
942		EOL.$content;
943		send $client,$answer.EOL,0;
944		close $client;
945		return 1;	#keep listening
946	}
947	my $answer=
948	'HTTP/1.0 200 OK'.EOL.
949	'Server: iceserver/0.2'.EOL.
950	"Content-Type: ".$Encodings{$self->{encoding}}{mime}.EOL.
951	"x-audiocast-name: gmusicbrowser stream".EOL.
952	'x-audiocast-public: 0'.EOL;
953	send $client,$answer.EOL,0;
954	#warn $answer;
955	my $stream=$self->{stream};
956	my $fileno=fileno($client);
957	$stream->{sockets}{$fileno}=[$client,1,gethostbyaddr($iaddr,AF_INET)];
958	Glib::IO->add_watch($fileno,'hup',sub {warn "Connection closed"; my $stream=$_[2]; $stream->{sockets}{$fileno}[1]=0; ::HasChanged('connections'); $stream->signal_emit(remove => $fileno);return 0; },$stream); #FIXME never called
959	$stream->signal_emit(add => $fileno);
960	::HasChanged('connections');
961	return 1;	#keep listening
962}
963
964sub get_connections
965{	my $self=shift;
966	return () unless $self->{stream};
967	my $s= $self->{stream}{sockets};
968	return map $s->{$_}[2], grep $s->{$_}[1],keys %$s;
969}
970
9711;
972