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