1/* 2 * Copyright (c) 2016 gnome-pomodoro contributors 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 * 17 */ 18 19using GLib; 20 21 22namespace SoundsPlugin 23{ 24 public errordomain SoundPlayerError 25 { 26 FAILED_TO_INITIALIZE 27 } 28 29 /** 30 * Preset sounds are defined relative to data directory, 31 * and used URIs are not particulary valid. 32 */ 33 private string get_absolute_uri (string uri) 34 { 35 var scheme = GLib.Uri.parse_scheme (uri); 36 37 if (scheme == null && uri != "") 38 { 39 var path = GLib.Path.build_filename (Config.PACKAGE_DATA_DIR, 40 "sounds", 41 uri); 42 43 try { 44 return GLib.Filename.to_uri (path); 45 } 46 catch (GLib.ConvertError error) { 47 GLib.warning ("Failed to convert \"%s\" to uri: %s", path, error.message); 48 } 49 } 50 51 return uri; 52 } 53 54 public interface SoundPlayer : GLib.Object 55 { 56 public abstract GLib.File? file { get; set; } 57 58 public abstract double volume { get; set; } 59 60 public abstract void play (); 61 62 public abstract void stop (); 63 64 public virtual string[] get_supported_mime_types () 65 { 66 string[] mime_types = { 67 "audio/*" 68 }; 69 70 return mime_types; 71 } 72 } 73 74 private interface Fadeable 75 { 76 public abstract void fade_in (uint duration); 77 78 public abstract void fade_out (uint duration); 79 } 80 81 private class GStreamerPlayer : GLib.Object, SoundPlayer, Fadeable 82 { 83 public GLib.File? file { 84 get { 85 return this._file; 86 } 87 set { 88 this._file = value; 89 90 var uri = get_absolute_uri (this._file != null ? this._file.get_uri () : ""); 91 92 if (uri == "") { 93 this.stop (); 94 } 95 else { 96 Gst.State state; 97 Gst.State pending_state; 98 99 this.pipeline.get_state (out state, 100 out pending_state, 101 Gst.CLOCK_TIME_NONE); 102 103 if (pending_state != Gst.State.VOID_PENDING) { 104 state = pending_state; 105 } 106 107 if (state == Gst.State.PLAYING || 108 state == Gst.State.PAUSED) 109 { 110 this.is_about_to_finish = false; 111 112 this.pipeline.set_state (Gst.State.READY); 113 this.pipeline.uri = uri; 114 this.pipeline.set_state (state); 115 } 116 } 117 } 118 } 119 120 public double volume { 121 get { 122 if (this.pipeline != null && this.pipeline.volume != null) { 123 return this.pipeline.volume; 124 } else { 125 return 1.0; 126 } 127 } 128 set { 129 this.pipeline.volume = value.clamp (0.0, 1.0); 130 } 131 } 132 133 public double volume_fade { 134 get { 135 if (this.volume_filter != null && this.volume_filter.volume != null) { 136 return this.volume_filter.volume; 137 } else { 138 return 0.0; 139 } 140 } 141 set { 142 this.volume_filter.volume = value.clamp (0.0, 1.0); 143 } 144 } 145 146 public bool repeat { get; set; default = false; } 147 148 private GLib.File _file; 149 private dynamic Gst.Element pipeline; 150 private dynamic Gst.Element volume_filter; 151 private Pomodoro.Animation volume_animation; 152 private bool is_about_to_finish = false; 153 154 [Flags] 155 private enum GstPlayFlags { 156 VIDEO = 0x00000001, 157 AUDIO = 0x00000002, 158 TEXT = 0x00000004, 159 VIS = 0x00000008, 160 SOFT_VOLUME = 0x00000010, 161 NATIVE_AUDIO = 0x00000020, 162 NATIVE_VIDEO = 0x00000040, 163 DOWNLOAD = 0x00000080, 164 BUFFERING = 0x00000100, 165 DEINTERLACE = 0x00000200, 166 SOFT_COLORBALANCE = 0x00000400, 167 FORCE_FILTERS = 0x00000800 168 } 169 170 private const uint FADE_FRAMES_PER_SECOND = 20; 171 172 public GStreamerPlayer () throws SoundPlayerError 173 { 174 dynamic Gst.Element pipeline = Gst.ElementFactory.make ("playbin", "player"); 175 dynamic Gst.Element volume_filter = Gst.ElementFactory.make ("volume", "volume"); 176 177 if (pipeline == null) { 178 throw new SoundPlayerError.FAILED_TO_INITIALIZE ("Failed to initialize \"playbin\" element"); 179 } 180 181 if (volume_filter == null) { 182 throw new SoundPlayerError.FAILED_TO_INITIALIZE ("Failed to initialize \"volume\" element"); 183 } 184 185 pipeline.flags = GstPlayFlags.AUDIO; 186 pipeline.audio_filter = volume_filter; 187 pipeline.about_to_finish.connect (this.on_about_to_finish); 188 pipeline.get_bus ().add_watch (GLib.Priority.DEFAULT, 189 this.on_bus_callback); 190 191 pipeline.volume = 1.0; 192 volume_filter.volume = 0.0; 193 194 this.volume_filter = volume_filter; 195 this.pipeline = pipeline; 196 } 197 198 ~GStreamerPlayer () 199 { 200 if (this.pipeline != null) { 201 this.pipeline.set_state (Gst.State.NULL); 202 } 203 } 204 205 public void play () 206 requires (this.pipeline != null) 207 { 208 this.fade_in (0); 209 } 210 211 public void stop () 212 requires (this.pipeline != null) 213 { 214 this.fade_out (0); 215 } 216 217 public void fade_in (uint duration) 218 { 219 if (this.volume_animation != null) { 220 this.volume_animation.stop (); 221 this.volume_animation = null; 222 } 223 224 if (duration > 0) { 225 this.volume_animation = new Pomodoro.Animation (Pomodoro.AnimationMode.EASE_OUT, 226 duration, 227 FADE_FRAMES_PER_SECOND); 228 this.volume_animation.add_property (this, 229 "volume-fade", 230 1.0); 231 this.volume_animation.start (); 232 } 233 else { 234 this.volume_fade = 1.0; 235 } 236 237 var uri = get_absolute_uri (this._file != null ? this._file.get_uri () : ""); 238 239 if (uri != "") { 240 this.pipeline.uri = uri; 241 this.pipeline.set_state (Gst.State.PLAYING); 242 } 243 } 244 245 public void fade_out (uint duration) 246 { 247 Gst.State state; 248 Gst.State pending_state; 249 250 if (this.volume_animation != null) { 251 this.volume_animation.stop (); 252 this.volume_animation = null; 253 } 254 255 this.pipeline.get_state (out state, 256 out pending_state, 257 Gst.CLOCK_TIME_NONE); 258 259 if (pending_state != Gst.State.VOID_PENDING) { 260 state = pending_state; 261 } 262 263 if (duration > 0 && state == Gst.State.PLAYING) { 264 this.volume_animation = new Pomodoro.Animation (Pomodoro.AnimationMode.EASE_IN_OUT, 265 duration, 266 FADE_FRAMES_PER_SECOND); 267 this.volume_animation.add_property (this, 268 "volume-fade", 269 0.0); 270 this.volume_animation.complete.connect (() => { 271 this.stop (); 272 }); 273 274 this.volume_animation.start (); 275 } 276 else { 277 if (state != Gst.State.NULL && state != Gst.State.READY) { 278 this.pipeline.set_state (Gst.State.READY); 279 } 280 281 this.volume_fade = 0.0; 282 } 283 } 284 285 private bool on_bus_callback (Gst.Bus bus, 286 Gst.Message message) 287 { 288 GLib.Error error; 289 Gst.State state; 290 Gst.State pending_state; 291 292 this.pipeline.get_state (out state, 293 out pending_state, 294 Gst.CLOCK_TIME_NONE); 295 296 switch (message.type) 297 { 298 case Gst.MessageType.EOS: 299 if (this.is_about_to_finish) { 300 this.is_about_to_finish = false; 301 } 302 else { 303 this.finished (); 304 } 305 306 if (pending_state != Gst.State.PLAYING) { 307 this.pipeline.set_state (Gst.State.READY); 308 } 309 310 break; 311 312 case Gst.MessageType.ERROR: 313 if (this.is_about_to_finish) { 314 this.is_about_to_finish = false; 315 } 316 317 message.parse_error (out error, null); 318 GLib.critical (error.message); 319 320 this.pipeline.set_state (Gst.State.NULL); 321 322 this.finished (); 323 break; 324 325 default: 326 break; 327 } 328 329 return true; 330 } 331 332 /** 333 * Try emit "finished" signal before the end of stream. 334 * If play gets called during then 335 */ 336 private void on_about_to_finish () 337 { 338 this.is_about_to_finish = true; 339 340 this.finished (); 341 } 342 343 public virtual signal void finished () 344 { 345 string current_uri; 346 347 if (this.repeat) { 348 this.pipeline.get ("current-uri", out current_uri); 349 350 if (current_uri != "") { 351 this.pipeline.set ("uri", current_uri); 352 } 353 } 354 } 355 } 356 357 private class CanberraPlayer : GLib.Object, SoundPlayer 358 { 359 public GLib.File? file { 360 get { 361 return this._file; 362 } 363 set { 364 this._file = value != null 365 ? GLib.File.new_for_uri (get_absolute_uri (value.get_uri ())) 366 : null; 367 368 if (this.is_cached) { 369 /* there is no way to invalidate old value, so at least refresh cache */ 370 this.cache_file (); 371 } 372 } 373 } 374 375 public string event_id { get; private construct set; } 376 public double volume { get; set; default = 1.0; } 377 378 private GLib.File _file; 379 private Canberra.Context context; 380 private bool is_cached = false; 381 382 public CanberraPlayer (string? event_id) throws SoundPlayerError 383 { 384 Canberra.Context context; 385 386 /* Create context */ 387 var status = Canberra.Context.create (out context); 388 var application = GLib.Application.get_default (); 389 390 if (status != Canberra.SUCCESS) { 391 throw new SoundPlayerError.FAILED_TO_INITIALIZE ( 392 "Failed to initialize canberra context - %s".printf (Canberra.strerror (status))); 393 } 394 395 /* Set properties about application */ 396 status = context.change_props ( 397 Canberra.PROP_APPLICATION_ID, application.application_id, 398 Canberra.PROP_APPLICATION_NAME, Config.PACKAGE_NAME, 399 Canberra.PROP_APPLICATION_ICON_NAME, Config.PACKAGE_NAME); 400 401 if (status != Canberra.SUCCESS) { 402 throw new SoundPlayerError.FAILED_TO_INITIALIZE ( 403 "Failed to set context properties - %s".printf (Canberra.strerror (status))); 404 } 405 406 /* Connect to the sound system */ 407 status = context.open (); 408 409 if (status != Canberra.SUCCESS) { 410 throw new SoundPlayerError.FAILED_TO_INITIALIZE ( 411 "Failed to open canberra context - %s".printf (Canberra.strerror (status))); 412 } 413 414 this.context = (owned) context; 415 this.event_id = event_id; 416 } 417 418 ~CanberraPlayer () 419 { 420 if (this.context != null) { 421 this.stop (); 422 } 423 } 424 425 private static double amplitude_to_decibels (double amplitude) 426 { 427 return 20.0 * Math.log10 (amplitude); 428 } 429 430 public void play () 431 requires (this.context != null) 432 { 433 if (this._file != null) 434 { 435 if (this.context != null) 436 { 437 Canberra.Proplist properties = null; 438 439 var status = Canberra.Proplist.create (out properties); 440 properties.sets (Canberra.PROP_MEDIA_ROLE, "alert"); 441 properties.sets (Canberra.PROP_MEDIA_FILENAME, this._file.get_path ()); 442 properties.sets (Canberra.PROP_CANBERRA_VOLUME, 443 ((float) amplitude_to_decibels (this.volume)).to_string ()); 444 445 if (this.event_id != null) { 446 properties.sets (Canberra.PROP_EVENT_ID, this.event_id); 447 448 if (!this.is_cached) { 449 this.cache_file (); 450 } 451 } 452 453 status = this.context.play_full (0, 454 properties, 455 this.on_play_callback); 456 457 if (status != Canberra.SUCCESS) { 458 GLib.warning ("Couldn't play sound '%s' - %s", 459 this._file.get_uri (), 460 Canberra.strerror (status)); 461 } 462 } 463 else { 464 GLib.warning ("Couldn't play sound '%s'", 465 this._file.get_uri ()); 466 } 467 } 468 } 469 470 public void stop () 471 requires (this.context != null) 472 { 473 /* we dont need it for event sounds */ 474 } 475 476 public string[] get_supported_mime_types () 477 { 478 string[] mime_types = { 479 "audio/x-vorbis+ogg", 480 "audio/x-wav" 481 }; 482 483 return mime_types; 484 } 485 486 private void cache_file () 487 { 488 Canberra.Proplist properties = null; 489 490 if (this.context != null && this.event_id != null && this._file != null) 491 { 492 var status = Canberra.Proplist.create (out properties); 493 properties.sets (Canberra.PROP_EVENT_ID, this.event_id); 494 properties.sets (Canberra.PROP_MEDIA_FILENAME, this._file.get_path ()); 495 496 status = this.context.cache_full (properties); 497 498 if (status != Canberra.SUCCESS) { 499 GLib.warning ("Couldn't clear libcanberra cache - %s", 500 Canberra.strerror (status)); 501 } 502 else { 503 this.is_cached = true; 504 } 505 } 506 } 507 508 private void on_play_callback (Canberra.Context context, 509 uint32 id, 510 int code) 511 { 512 } 513 } 514 515 private class DummyPlayer : GLib.Object, SoundPlayer 516 { 517 public GLib.File? file { 518 get { 519 return this._file; 520 } 521 set { 522 this._file = value != null 523 ? GLib.File.new_for_uri (get_absolute_uri (value.get_uri ())) 524 : null; 525 } 526 } 527 528 public double volume { get; set; default = 1.0; } 529 530 private GLib.File _file; 531 532 public void play () { 533 } 534 535 public void stop () { 536 } 537 } 538} 539