AlsaMixerPlugin.cxx 7.2 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 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 "config.h"
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/DeferredMonitor.hxx"
26
#include "util/ASCII.hxx"
27
#include "util/ReusableArray.hxx"
28
#include "util/Clamp.hxx"
29
#include "util/Domain.hxx"
30
#include "util/RuntimeError.hxx"
31
#include "Log.hxx"
32 33

#include <algorithm>
34 35 36 37 38

#include <alsa/asoundlib.h>

#define VOLUME_MIXER_ALSA_DEFAULT		"default"
#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT	"PCM"
39
static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0;
40

41
class AlsaMixerMonitor final : MultiSocketMonitor, DeferredMonitor {
Max Kellermann's avatar
Max Kellermann committed
42
	snd_mixer_t *mixer;
43

44 45
	ReusableArray<pollfd> pfd_buffer;

46 47
public:
	AlsaMixerMonitor(EventLoop &_loop, snd_mixer_t *_mixer)
48 49 50
		:MultiSocketMonitor(_loop), DeferredMonitor(_loop),
		 mixer(_mixer) {
		DeferredMonitor::Schedule();
51
	}
52

53
private:
54 55
	virtual void RunDeferred() override {
		InvalidateSockets();
56 57
	}

58
	virtual int PrepareSockets() override;
59
	virtual void DispatchSockets() override;
60 61
};

62
class AlsaMixer final : public Mixer {
63 64
	EventLoop &event_loop;

65 66
	const char *device;
	const char *control;
67
	unsigned int index;
68

69 70 71 72 73
	snd_mixer_t *handle;
	snd_mixer_elem_t *elem;
	long volume_min;
	long volume_max;
	int volume_set;
74

75
	AlsaMixerMonitor *monitor;
76 77

public:
78 79 80
	AlsaMixer(EventLoop &_event_loop, MixerListener &_listener)
		:Mixer(alsa_mixer_plugin, _listener),
		 event_loop(_event_loop) {}
81

82 83
	virtual ~AlsaMixer();

84
	void Configure(const ConfigBlock &block);
85
	void Setup();
86

87
	/* virtual methods from class Mixer */
88 89 90 91
	void Open() override;
	void Close() override;
	int GetVolume() override;
	void SetVolume(unsigned volume) override;
92 93
};

94
static constexpr Domain alsa_mixer_domain("alsa_mixer");
95

96 97
int
AlsaMixerMonitor::PrepareSockets()
98
{
99 100
	if (mixer == nullptr) {
		ClearSocketList();
Max Kellermann's avatar
Max Kellermann committed
101
		return -1;
102
	}
Max Kellermann's avatar
Max Kellermann committed
103

104
	int count = snd_mixer_poll_descriptors_count(mixer);
105 106 107
	if (count < 0)
		count = 0;

108 109
	struct pollfd *pfds = pfd_buffer.Get(count);

110
	count = snd_mixer_poll_descriptors(mixer, pfds, count);
111 112 113
	if (count < 0)
		count = 0;

114
	ReplaceSocketList(pfds, count);
115
	return -1;
116 117
}

118 119
void
AlsaMixerMonitor::DispatchSockets()
120
{
Max Kellermann's avatar
Max Kellermann committed
121 122 123 124
	assert(mixer != nullptr);

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

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

/*
 * libasound callbacks
 *
 */

static int
145
alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned mask)
146
{
147 148 149 150
	AlsaMixer &mixer = *(AlsaMixer *)
		snd_mixer_elem_get_callback_private(elem);

	if (mask & SND_CTL_EVENT_MASK_VALUE) {
151 152 153 154 155
		try {
			int volume = mixer.GetVolume();
			mixer.listener.OnMixerVolumeChanged(mixer, volume);
		} catch (const std::runtime_error &) {
		}
156
	}
157 158 159 160 161 162 163 164 165

	return 0;
}

/*
 * mixer_plugin methods
 *
 */

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

177
static Mixer *
178
alsa_mixer_init(EventLoop &event_loop, gcc_unused AudioOutput &ao,
179
		MixerListener &listener,
180
		const ConfigBlock &block)
181
{
182
	AlsaMixer *am = new AlsaMixer(event_loop, listener);
183
	am->Configure(block);
184

185
	return am;
186 187
}

188
AlsaMixer::~AlsaMixer()
189
{
190 191
	/* free libasound's config cache */
	snd_config_update_free_global();
192 193
}

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

207
	return nullptr;
208 209
}

210 211
inline void
AlsaMixer::Setup()
212 213 214
{
	int err;

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

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

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

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

231 232
	snd_mixer_selem_get_playback_volume_range(elem, &volume_min,
						  &volume_max);
233

234
	snd_mixer_elem_set_callback_private(elem, this);
235
	snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback);
236

237
	monitor = new AlsaMixerMonitor(event_loop, handle);
238 239
}

240 241
void
AlsaMixer::Open()
242 243 244
{
	int err;

245
	volume_set = -1;
246

247
	err = snd_mixer_open(&handle, 0);
248 249 250
	if (err < 0)
		throw FormatRuntimeError("snd_mixer_open() failed: %s",
					 snd_strerror(err));
251

252 253 254
	try {
		Setup();
	} catch (...) {
255
		snd_mixer_close(handle);
256
		throw;
257 258 259
	}
}

260
void
261
AlsaMixer::Close()
262
{
263
	assert(handle != nullptr);
264

265
	delete monitor;
266

267
	snd_mixer_elem_set_callback(elem, nullptr);
268 269
	snd_mixer_close(handle);
}
270

271 272
int
AlsaMixer::GetVolume()
273
{
274 275 276 277
	int err;
	int ret;
	long level;

278
	assert(handle != nullptr);
279

280
	err = snd_mixer_handle_events(handle);
281 282 283
	if (err < 0)
		throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
					 snd_strerror(err));
284

285
	err = snd_mixer_selem_get_playback_volume(elem,
286 287
						  SND_MIXER_SCHN_FRONT_LEFT,
						  &level);
288 289 290
	if (err < 0)
		throw FormatRuntimeError("failed to read ALSA volume: %s",
					 snd_strerror(err));
291

292 293 294 295
	ret = ((volume_set / 100.0) * (volume_max - volume_min)
	       + volume_min) + 0.5;
	if (volume_set > 0 && ret == level) {
		ret = volume_set;
296
	} else {
297 298
		ret = (int)(100 * (((float)(level - volume_min)) /
				   (volume_max - volume_min)) + 0.5);
299
	}
300 301 302 303

	return ret;
}

304 305
void
AlsaMixer::SetVolume(unsigned volume)
306 307 308 309 310
{
	float vol;
	long level;
	int err;

311
	assert(handle != nullptr);
312 313 314

	vol = volume;

315
	volume_set = vol + 0.5;
316

317 318
	level = (long)(((vol / 100.0) * (volume_max - volume_min) +
			volume_min) + 0.5);
319
	level = Clamp(level, volume_min, volume_max);
320

321
	err = snd_mixer_selem_set_playback_volume_all(elem, level);
322 323 324
	if (err < 0)
		throw FormatRuntimeError("failed to set ALSA volume: %s",
					 snd_strerror(err));
325
}
Viliam Mateicka's avatar
Viliam Mateicka committed
326

327
const MixerPlugin alsa_mixer_plugin = {
328 329
	alsa_mixer_init,
	true,
Viliam Mateicka's avatar
Viliam Mateicka committed
330
};