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