AlsaMixerPlugin.cxx 6.73 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2020 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13
 * http://www.musicpd.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
 */
19

20
#include "lib/alsa/NonBlock.hxx"
Max Kellermann's avatar
Max Kellermann committed
21
#include "mixer/MixerInternal.hxx"
22
#include "mixer/Listener.hxx"
23
#include "output/OutputAPI.hxx"
24
#include "event/MultiSocketMonitor.hxx"
25
#include "event/DeferEvent.hxx"
26
#include "event/Call.hxx"
27
#include "util/ASCII.hxx"
28
#include "util/Domain.hxx"
29
#include "util/Math.hxx"
30
#include "util/RuntimeError.hxx"
31
#include "Log.hxx"
32

33 34 35
extern "C" {
#include "volume_mapping.h"
}
36 37 38 39 40

#include <alsa/asoundlib.h>

#define VOLUME_MIXER_ALSA_DEFAULT		"default"
#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT	"PCM"
41
static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0;
42

43 44 45
class AlsaMixerMonitor final : MultiSocketMonitor {
	DeferEvent defer_invalidate_sockets;

Max Kellermann's avatar
Max Kellermann committed
46
	snd_mixer_t *mixer;
47

48
	AlsaNonBlockMixer non_block;
49

50 51
public:
	AlsaMixerMonitor(EventLoop &_loop, snd_mixer_t *_mixer)
52 53 54
		:MultiSocketMonitor(_loop),
		 defer_invalidate_sockets(_loop,
					  BIND_THIS_METHOD(InvalidateSockets)),
55
		 mixer(_mixer) {
56
		defer_invalidate_sockets.Schedule();
57
	}
58

59 60 61
	~AlsaMixerMonitor() {
		BlockingCall(MultiSocketMonitor::GetEventLoop(), [this](){
				MultiSocketMonitor::Reset();
62
				defer_invalidate_sockets.Cancel();
63 64 65
			});
	}

66
private:
67 68
	std::chrono::steady_clock::duration PrepareSockets() noexcept override;
	void DispatchSockets() noexcept override;
69 70
};

71
class AlsaMixer final : public Mixer {
72 73
	EventLoop &event_loop;

74 75
	const char *device;
	const char *control;
76
	unsigned int index;
77

78 79
	snd_mixer_t *handle;
	snd_mixer_elem_t *elem;
80

81
	AlsaMixerMonitor *monitor;
82 83

public:
84 85 86
	AlsaMixer(EventLoop &_event_loop, MixerListener &_listener)
		:Mixer(alsa_mixer_plugin, _listener),
		 event_loop(_event_loop) {}
87

88
	~AlsaMixer() override;
89

90
	void Configure(const ConfigBlock &block);
91
	void Setup();
92

93
	/* virtual methods from class Mixer */
94
	void Open() override;
95
	void Close() noexcept override;
96 97
	int GetVolume() override;
	void SetVolume(unsigned volume) override;
98 99
};

100
static constexpr Domain alsa_mixer_domain("alsa_mixer");
101

102
std::chrono::steady_clock::duration
103
AlsaMixerMonitor::PrepareSockets() noexcept
104
{
105 106
	if (mixer == nullptr) {
		ClearSocketList();
107
		return std::chrono::steady_clock::duration(-1);
108
	}
Max Kellermann's avatar
Max Kellermann committed
109

110
	return non_block.PrepareSockets(*this, mixer);
111 112
}

113
void
114
AlsaMixerMonitor::DispatchSockets() noexcept
115
{
Max Kellermann's avatar
Max Kellermann committed
116 117
	assert(mixer != nullptr);

118 119
	non_block.DispatchSockets(*this, mixer);

Max Kellermann's avatar
Max Kellermann committed
120 121
	int err = snd_mixer_handle_events(mixer);
	if (err < 0) {
122 123 124
		FormatError(alsa_mixer_domain,
			    "snd_mixer_handle_events() failed: %s",
			    snd_strerror(err));
Max Kellermann's avatar
Max Kellermann committed
125 126 127 128 129 130 131 132 133

		if (err == -ENODEV) {
			/* the sound device was unplugged; disable
			   this GSource */
			mixer = nullptr;
			InvalidateSockets();
			return;
		}
	}
134 135 136 137 138 139 140 141
}

/*
 * libasound callbacks
 *
 */

static int
142
alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned mask)
143
{
144 145 146 147
	AlsaMixer &mixer = *(AlsaMixer *)
		snd_mixer_elem_get_callback_private(elem);

	if (mask & SND_CTL_EVENT_MASK_VALUE) {
148 149 150
		try {
			int volume = mixer.GetVolume();
			mixer.listener.OnMixerVolumeChanged(mixer, volume);
151
		} catch (...) {
152
		}
153
	}
154 155 156 157 158 159 160 161 162

	return 0;
}

/*
 * mixer_plugin methods
 *
 */

163
inline void
164
AlsaMixer::Configure(const ConfigBlock &block)
165
{
166
	device = block.GetBlockValue("mixer_device",
167
				     VOLUME_MIXER_ALSA_DEFAULT);
168
	control = block.GetBlockValue("mixer_control",
169
				      VOLUME_MIXER_ALSA_CONTROL_DEFAULT);
170
	index = block.GetBlockValue("mixer_index",
171
				    VOLUME_MIXER_ALSA_INDEX_DEFAULT);
172 173
}

174
static Mixer *
Rosen Penev's avatar
Rosen Penev committed
175
alsa_mixer_init(EventLoop &event_loop, [[maybe_unused]] AudioOutput &ao,
176
		MixerListener &listener,
177
		const ConfigBlock &block)
178
{
Max Kellermann's avatar
Max Kellermann committed
179
	auto *am = new AlsaMixer(event_loop, listener);
180
	am->Configure(block);
181

182
	return am;
183 184
}

185
AlsaMixer::~AlsaMixer()
186
{
187 188
	/* free libasound's config cache */
	snd_config_update_free_global();
189 190
}

191
gcc_pure
192
static snd_mixer_elem_t *
193 194
alsa_mixer_lookup_elem(snd_mixer_t *handle,
		       const char *name, unsigned idx) noexcept
195 196
{
	for (snd_mixer_elem_t *elem = snd_mixer_first_elem(handle);
197
	     elem != nullptr; elem = snd_mixer_elem_next(elem)) {
198
		if (snd_mixer_elem_get_type(elem) == SND_MIXER_ELEM_SIMPLE &&
199 200
		    StringEqualsCaseASCII(snd_mixer_selem_get_name(elem),
					  name) &&
201 202 203 204
		    snd_mixer_selem_get_index(elem) == idx)
			return elem;
	}

205
	return nullptr;
206 207
}

208 209
inline void
AlsaMixer::Setup()
210 211 212
{
	int err;

213 214 215
	if ((err = snd_mixer_attach(handle, device)) < 0)
		throw FormatRuntimeError("failed to attach to %s: %s",
					 device, snd_strerror(err));
216

217 218 219
	if ((err = snd_mixer_selem_register(handle, nullptr, nullptr)) < 0)
		throw FormatRuntimeError("snd_mixer_selem_register() failed: %s",
					 snd_strerror(err));
220

221 222 223
	if ((err = snd_mixer_load(handle)) < 0)
		throw FormatRuntimeError("snd_mixer_load() failed: %s\n",
					 snd_strerror(err));
224

225
	elem = alsa_mixer_lookup_elem(handle, control, index);
226 227
	if (elem == nullptr)
		throw FormatRuntimeError("no such mixer control: %s", control);
228

229
	snd_mixer_elem_set_callback_private(elem, this);
230
	snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback);
231

232
	monitor = new AlsaMixerMonitor(event_loop, handle);
233 234
}

235 236
void
AlsaMixer::Open()
237 238 239
{
	int err;

240
	err = snd_mixer_open(&handle, 0);
241 242 243
	if (err < 0)
		throw FormatRuntimeError("snd_mixer_open() failed: %s",
					 snd_strerror(err));
244

245 246 247
	try {
		Setup();
	} catch (...) {
248
		snd_mixer_close(handle);
249
		throw;
250 251 252
	}
}

253
void
254
AlsaMixer::Close() noexcept
255
{
256
	assert(handle != nullptr);
257

258
	delete monitor;
259

260
	snd_mixer_elem_set_callback(elem, nullptr);
261 262
	snd_mixer_close(handle);
}
263

264 265
int
AlsaMixer::GetVolume()
266
{
267 268
	int err;

269
	assert(handle != nullptr);
270

271
	err = snd_mixer_handle_events(handle);
272 273 274
	if (err < 0)
		throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
					 snd_strerror(err));
275

276
	return lround(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT));
277 278
}

279 280
void
AlsaMixer::SetVolume(unsigned volume)
281
{
282
	assert(handle != nullptr);
283

284
	double cur = get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT);
285
	int delta = volume - lround(100.*cur);
286
	int err = set_normalized_playback_volume(elem, cur + 0.01*delta, delta);
287 288 289
	if (err < 0)
		throw FormatRuntimeError("failed to set ALSA volume: %s",
					 snd_strerror(err));
290
}
Viliam Mateicka's avatar
Viliam Mateicka committed
291

292
const MixerPlugin alsa_mixer_plugin = {
293 294
	alsa_mixer_init,
	true,
Viliam Mateicka's avatar
Viliam Mateicka committed
295
};