1# Copyright (C) 2015 Dustin Spicuzza
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2, or (at your option)
6# any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16#
17#
18# The developers of the Exaile media player hereby grant permission
19# for non-GPL compatible GStreamer and Exaile plugins to be used and
20# distributed together with GStreamer and Exaile. This permission is
21# above and beyond the permissions granted by the GPL license by which
22# Exaile is covered. If you modify this code, you may extend this
23# exception to your version of the code, but you are not obligated to
24# do so. If you do not wish to do so, delete this exception statement
25# from your version.
26
27from typing import Tuple
28
29from gi.repository import GLib
30
31from xl import common
32
33import logging
34
35FadeState = common.enum(NoFade=1, FadingIn=2, Normal=3, FadingOut=4)
36
37
38class TrackFader:
39    """
40    This object manages the volume of a track, and fading it in/out via
41    volume. As a bonus, this object can manage the start/stop offset of
42    a track also.
43
44    * Fade in only happens once per track play
45    * Fade out can happen multiple times
46
47    This fader can be used on any engine, as long as it implements the
48    following functions:
49
50    * get_position: return current track position in nanoseconds
51    * get_volume/set_volume: volume is 0 to 1.0
52    * stop: stops playback
53
54    There is a user volume and a fade volume. The fade volume is used
55    internally and the output volume is set by multiplying both of
56    them together.
57
58    .. note:: Only intended to be used by engine implementations
59    """
60
61    SECOND = 1000000000.0
62
63    def __init__(self, stream, on_fade_out, name):
64        self.name = name
65
66        self.logger = logging.getLogger('%s [%s]' % (__name__, name))
67
68        self.stream = stream
69        self.state = FadeState.NoFade
70        self.on_fade_out = on_fade_out
71        self.timer_id = None
72
73        self.fade_volume = 1.0
74        self.user_volume = 1.0
75
76        self.fade_in_start = None
77        self.fade_out_start = None
78
79    def calculate_fades(self, track, fade_in, fade_out):
80        ''' duration is in seconds'''
81
82        start_offset = track.get_tag_raw('__startoffset') or 0
83        stop_offset = track.get_tag_raw('__stopoffset') or 0
84        tracklen = track.get_tag_raw('__length') or 0
85
86        if stop_offset < 1:
87            stop_offset = tracklen
88
89        # Deal with bad values..
90        start_offset = min(start_offset, tracklen)
91        stop_offset = min(stop_offset, tracklen)
92
93        # Give up
94        if stop_offset <= start_offset:
95            return (tracklen,) * 4
96
97        if fade_in is None:
98            fade_in = 0
99        else:
100            fade_in = max(0, fade_in)
101
102        if fade_out is None:
103            fade_out = 0
104        else:
105            fade_out = max(0, fade_out)
106
107        playlen = stop_offset - start_offset
108
109        total_fade = max(fade_in + fade_out, 0.1)
110
111        if total_fade > playlen:
112            fade_in = playlen * (float(fade_in) / total_fade)
113            fade_out = playlen * (float(fade_out) / total_fade)
114
115        return (
116            start_offset,
117            start_offset + fade_in,
118            stop_offset - fade_out,
119            stop_offset,
120        )
121
122    def calculate_user_volume(self, real_volume) -> Tuple[float, bool]:
123        """Given the 'real' output volume, calculate what the user
124        volume should be and whether they are identical"""
125
126        vol = self.user_volume * self.fade_volume
127        real_is_same = abs(real_volume - vol) < 0.01
128
129        if real_is_same:
130            return real_volume, True
131
132        if self.fade_volume < 0.01:
133            return real_volume, True
134        else:
135            user_vol = real_volume / self.fade_volume
136            is_same = abs(user_vol - self.user_volume) < 0.01
137            return user_vol, is_same
138
139    def fade_out_on_play(self):
140
141        if self.fade_out_start is None:
142            self.logger.debug("foop: no fade out defined, stopping")
143            self.stream.stop()
144            return
145
146        self.now = self.stream.get_position() / self.SECOND - 0.010
147        fade_len = self.fade_out_end - self.fade_out_start
148
149        # If playing, and is not fading out, then force a fade out
150        if self.state == FadeState.Normal:
151            start = self.now
152
153        elif self.state == FadeState.FadingIn:
154            # Calculate an optimal fadeout given the current volume
155            volume = self.fade_volume
156            start = -((volume * fade_len) - self.now)
157            self.state = FadeState.FadingOut
158
159        else:
160            return
161
162        self.logger.debug(
163            "foop: starting fade (now: %s, start: %s, len: %s)",
164            self.now,
165            start,
166            fade_len,
167        )
168
169        self._cancel()
170        if self._execute_fade(start, fade_len):
171            self.timer_id = GLib.timeout_add(10, self._execute_fade, start, fade_len)
172
173    def get_user_volume(self):
174        return self.user_volume
175
176    def is_fading_out(self):
177        return self.state == FadeState.FadingOut
178
179    def setup_track(self, track, fade_in, fade_out, is_update=False, now=None):
180        """
181        Call this function either when a track first starts, or if the
182        crossfade period has been updated.
183
184        As a bonus, calling this function with crossfade_duration=None
185        will automatically stop the track at its stop_offset
186
187        :param fade_in: Set to None or fade duration in seconds
188        :param fade_out: Set to None or fade duration in seconds
189        :param is_update: Set True if this is a settings update
190        """
191
192        # If user disables crossfade during a transition, then don't
193        # cancel the transition
194
195        has_fade = fade_in is not None or fade_out is not None
196
197        if is_update and has_fade:
198
199            if self.state == FadeState.FadingOut:
200                # Don't cancel the current fade out
201                return
202
203            elif self.state == FadeState.FadingIn:
204                # Don't cancel the current fade in, but adjust the
205                # current fade out
206                _, _, self.fade_out_start, self.fade_out_end = self.calculate_fades(
207                    track, fade_in, fade_out
208                )
209
210                self._next(now=now)
211                return
212
213        if has_fade:
214            self.play(*self.calculate_fades(track, fade_in, fade_out))
215        else:
216            stop_offset = track.get_tag_raw('__stopoffset') or 0
217            if stop_offset > 0:
218                self.play(None, None, stop_offset, stop_offset, now=now)
219            else:
220                self.play(now=now)
221
222    def play(
223        self,
224        fade_in_start=None,
225        fade_in_end=None,
226        fade_out_start=None,
227        fade_out_end=None,
228        now=None,
229    ):
230        '''Don't call this when doing crossfading'''
231
232        self.fade_in_start = fade_in_start
233        self.fade_in_end = fade_in_end
234
235        self.fade_out_start = fade_out_start
236        self.fade_out_end = fade_out_end
237
238        if self.fade_in_start is not None:
239            self.state = FadeState.FadingIn
240        elif self.fade_out_start is None:
241            self.state = FadeState.NoFade
242        else:
243            self.state = FadeState.Normal
244
245        self._next(now=now)
246
247    def pause(self):
248        self._cancel()
249
250    def seek(self, to):
251        self._next(now=to)
252
253    def set_user_volume(self, volume):
254        self.user_volume = volume
255        self.stream.set_volume(self.user_volume * self.fade_volume)
256
257    def set_fade_volume(self, volume):
258        self.fade_volume = volume
259        self.stream.set_volume(self.user_volume * self.fade_volume)
260
261    def unpause(self):
262        self._next()
263
264    def stop(self):
265        self.state = FadeState.NoFade
266        self._cancel()
267
268    def _next(self, now=None):
269
270        self._cancel()
271
272        if self.state == FadeState.NoFade:
273            self.set_fade_volume(1.0)
274            return
275
276        if now is None:
277            now = self.stream.get_position() / self.SECOND
278
279        msg = "Fade data: now: %.2f; in: %s,%s; out: %s,%s"
280        self.logger.debug(
281            msg,
282            now,
283            self.fade_in_start,
284            self.fade_in_end,
285            self.fade_out_start,
286            self.fade_out_end,
287        )
288
289        if self.state == FadeState.FadingIn:
290            if now < self.fade_in_end:
291                self._on_fade_start(now=now)
292                return
293
294            self.state = FadeState.Normal
295
296        if self.fade_out_start is None:
297            self.state = FadeState.NoFade
298            self.set_fade_volume(1.0)
299            return
300
301        fade_tm = int((self.fade_out_start - now) * 1000)
302        if fade_tm > 0:
303            self.logger.debug("- Will fade out in %.2f seconds", fade_tm / 1000.0)
304            self.timer_id = GLib.timeout_add(fade_tm, self._on_fade_start)
305            self.set_fade_volume(1.0)
306        else:
307            # do the fade now
308            self._on_fade_start(now=now)
309
310    def _cancel(self):
311        if self.timer_id:
312            GLib.source_remove(self.timer_id)
313            self.timer_id = None
314
315    def _on_fade_start(self, now=None):
316
317        if now is None:
318            now = self.stream.get_position() / self.SECOND
319
320        self.now = now - 0.010
321
322        if self.state == FadeState.FadingIn:
323            self.logger.debug("Fade in begins")
324            end = self.fade_in_end
325            start = self.fade_in_start
326        else:
327            end = self.fade_out_end
328            start = self.fade_out_start
329
330            if self.state != FadeState.FadingOut:
331                self.logger.debug("Fade out begins at %s", self.now)
332                self.state = FadeState.FadingOut
333                self.on_fade_out()
334
335        fade_len = float(end - start)
336
337        if self._execute_fade(start, fade_len):
338            self.timer_id = GLib.timeout_add(10, self._execute_fade, start, fade_len)
339        return False
340
341    def _execute_fade(self, fade_start, fade_len):
342        """
343        Executes a fade for a period of time, then ends
344
345        :param fade_start:  When the fade should have started
346        :param fade_len:    _total_ length of fade, regardless of start
347        """
348
349        # Don't query the stream, just assume this is close enough
350        self.now += 0.010
351        fading_in = self.state == FadeState.FadingIn
352
353        if fade_len < 0.01:
354            volume = 0.0
355        else:
356            volume = (self.now - fade_start) / fade_len
357
358        if not fading_in:
359            volume = 1.0 - volume
360
361        volume = min(max(0.0, volume), 1.0)
362        self.set_fade_volume(volume)
363
364        if self.now > fade_start + fade_len:
365            self.timer_id = None
366
367            if fading_in:
368                self.logger.debug("Fade in ends")
369                self.state = FadeState.Normal
370                self._next()
371            else:
372                self.logger.debug("Fade out ends")
373                self.state = FadeState.NoFade
374                self.stream.stop()
375
376            return False
377
378        return True
379