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