1# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
2#
3# Copyright (C) 2010 Jonathan Matthew
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2, or (at your option)
8# any later version.
9#
10# The Rhythmbox authors hereby grant permission for non-GPL compatible
11# GStreamer plugins to be used and distributed together with GStreamer
12# and Rhythmbox. This permission is above and beyond the permissions granted
13# by the GPL license by which Rhythmbox is covered. If you modify this code
14# you may extend this exception to your version of the code, but you are not
15# obligated to do so. If you do not wish to do so, delete this exception
16# statement from your version.
17#
18# This program is distributed in the hope that it will be useful,
19# but WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21# GNU General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
26#
27
28import rb
29import gi
30
31gi.require_version("Gst", "1.0")
32from gi.repository import RB
33from gi.repository import GObject, Gio, Gst
34
35import config
36
37import gettext
38gettext.install('rhythmbox', RB.locale_dir())
39
40EPSILON = 0.001
41
42class ReplayGainPlayer(object):
43	def __init__(self, shell):
44		# make sure the replaygain elements are available
45		missing = []
46		required = ("rgvolume", "rglimiter")
47		for e in required:
48			if Gst.ElementFactory.find(e) is None:
49				missing.append(e)
50
51		if len(missing) > 0:
52			msg = _("The GStreamer elements required for ReplayGain processing are not available. The missing elements are: %s") % ", ".join(missing)
53			RB.error_dialog(shell.props.window, _("ReplayGain GStreamer plugins not available"), msg)
54			raise Exception(msg)
55
56		self.shell_player = shell.props.shell_player
57		self.player = self.shell_player.props.player
58		self.settings = Gio.Settings.new("org.gnome.rhythmbox.plugins.replaygain")
59
60		self.settings.connect("changed::limiter", self.limiter_changed_cb)
61
62		self.previous_gain = []
63		self.fallback_gain = 0.0
64
65		# we use different means to hook into the playback pipeline depending on
66		# the playback backend in use
67		if GObject.signal_lookup("get-stream-filters", self.player):
68			self.setup_xfade_mode()
69			self.deactivate_backend = self.deactivate_xfade_mode
70		else:
71			self.setup_playbin_mode()
72			self.deactivate_backend = self.deactivate_playbin_mode
73
74
75
76	def deactivate(self):
77		self.deactivate_backend()
78		self.player = None
79		self.shell_player = None
80
81
82	def set_rgvolume(self, rgvolume):
83		# set preamp level
84		preamp = self.settings['preamp']
85		rgvolume.props.pre_amp = preamp
86
87		# set mode
88		# there may eventually be a 'guess' mode here that tries to figure out
89		# what to do based on the upcoming tracks
90		mode = self.settings['mode']
91		if mode == config.REPLAYGAIN_MODE_ALBUM:
92			rgvolume.props.album_mode = 1
93		else:
94			rgvolume.props.album_mode = 0
95
96		# set calculated fallback gain
97		rgvolume.props.fallback_gain = self.fallback_gain
98
99		print("updated rgvolume settings: preamp %f, album-mode %s, fallback gain %f" % (
100			rgvolume.props.pre_amp, str(rgvolume.props.album_mode), rgvolume.props.fallback_gain))
101
102
103	def update_fallback_gain(self, rgvolume):
104		gain = rgvolume.props.target_gain - rgvolume.props.pre_amp
105		# filter out bogus notifications
106		if abs(gain - self.fallback_gain) < EPSILON:
107			print("ignoring gain %f (current fallback gain)" % gain)
108			return False
109		if abs(gain) < EPSILON:
110			print("ignoring zero gain (pretty unlikely)")
111			return False
112
113		# update the running average
114		if len(self.previous_gain) == config.AVERAGE_GAIN_SAMPLES:
115			self.previous_gain.pop(0)
116		self.previous_gain.append(gain)
117		self.fallback_gain = sum(self.previous_gain) / len(self.previous_gain)
118		print("got target gain %f; running average of previous gain values is %f" % (gain, self.fallback_gain))
119		return True
120
121
122
123	### playbin mode (rgvolume ! rglimiter as global filter)
124
125	def playbin_target_gain_cb(self, rgvolume, pspec):
126		self.update_fallback_gain(rgvolume)
127
128	def setup_playbin_mode(self):
129		print("using output filter for rgvolume and rglimiter")
130		self.rgfilter = Gst.Bin()
131
132		self.rgvolume = Gst.ElementFactory.make("rgvolume", None)
133		self.rgvolume.connect("notify::target-gain", self.playbin_target_gain_cb)
134		self.rgfilter.add(self.rgvolume)
135
136		self.rglimiter = Gst.ElementFactory.make("rglimiter", None)
137		self.rgfilter.add(self.rglimiter)
138
139		self.rgfilter.add_pad(Gst.GhostPad.new("sink", self.rgvolume.get_static_pad("sink")))
140		self.rgfilter.add_pad(Gst.GhostPad.new("src", self.rglimiter.get_static_pad("src")))
141		self.rgvolume.link(self.rglimiter)
142
143		self.player.add_filter(self.rgfilter)
144
145	def deactivate_playbin_mode(self):
146		self.player.remove_filter(self.rgfilter)
147		self.rgfilter = None
148
149
150	### xfade mode (rgvolume as stream filter, rglimiter as global filter)
151
152	def xfade_target_gain_cb(self, rgvolume, pspec):
153		if self.update_fallback_gain(rgvolume) is  True:
154			# we don't want any further notifications from this stream
155			rgvolume.disconnect_by_func(self.xfade_target_gain_cb)
156
157	def create_stream_filter_cb(self, player, uri):
158		print("creating rgvolume instance for stream %s" % uri)
159		rgvolume = Gst.ElementFactory.make("rgvolume", None)
160		rgvolume.connect("notify::target-gain", self.xfade_target_gain_cb)
161		self.set_rgvolume(rgvolume)
162		return [rgvolume]
163
164	def limiter_changed_cb(self, settings, key):
165		if self.rglimiter is not None:
166			limiter = settings['limiter']
167			print("limiter setting is now %s" % str(limiter))
168			self.rglimiter.props.enabled = limiter
169
170	def setup_xfade_mode(self):
171		print("using per-stream filter for rgvolume")
172		self.stream_filter_id = self.player.connect("get-stream-filters", self.create_stream_filter_cb)
173
174		# and add rglimiter as an output filter
175		self.rglimiter = Gst.ElementFactory.make("rglimiter", None)
176		self.player.add_filter(self.rglimiter)
177
178	def deactivate_xfade_mode(self):
179		self.player.disconnect(self.stream_filter_id)
180		self.stream_filter_id = None
181		self.player.remove_filter(self.rglimiter)
182		self.rglimiter = None
183