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