AlsaMixerPlugin.cxx 6.75 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 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"
21
#include "lib/alsa/NonBlock.hxx"
Max Kellermann's avatar
Max Kellermann committed
22
#include "mixer/MixerInternal.hxx"
23
#include "mixer/Listener.hxx"
24
#include "output/OutputAPI.hxx"
25
#include "event/MultiSocketMonitor.hxx"
26
#include "event/DeferEvent.hxx"
27
#include "event/Call.hxx"
28
#include "util/ASCII.hxx"
29
#include "util/ReusableArray.hxx"
30
#include "util/Domain.hxx"
31
#include "util/RuntimeError.hxx"
32
#include "Log.hxx"
33

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

#include <alsa/asoundlib.h>

40 41
#include <math.h>

42 43
#define VOLUME_MIXER_ALSA_DEFAULT		"default"
#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT	"PCM"
44
static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0;
45

46 47 48
class AlsaMixerMonitor final : MultiSocketMonitor {
	DeferEvent defer_invalidate_sockets;

Max Kellermann's avatar
Max Kellermann committed
49
	snd_mixer_t *mixer;
50

51 52
	ReusableArray<pollfd> pfd_buffer;

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

62 63 64
	~AlsaMixerMonitor() {
		BlockingCall(MultiSocketMonitor::GetEventLoop(), [this](){
				MultiSocketMonitor::Reset();
65
				defer_invalidate_sockets.Cancel();
66 67 68
			});
	}

69
private:
70 71
	std::chrono::steady_clock::duration PrepareSockets() noexcept override;
	void DispatchSockets() noexcept override;
72 73
};

74
class AlsaMixer final : public Mixer {
75 76
	EventLoop &event_loop;

77 78
	const char *device;
	const char *control;
79
	unsigned int index;
80

81 82
	snd_mixer_t *handle;
	snd_mixer_elem_t *elem;
83

84
	AlsaMixerMonitor *monitor;
85 86

public:
87 88 89
	AlsaMixer(EventLoop &_event_loop, MixerListener &_listener)
		:Mixer(alsa_mixer_plugin, _listener),
		 event_loop(_event_loop) {}
90

91 92
	virtual ~AlsaMixer();

93
	void Configure(const ConfigBlock &block);
94
	void Setup();
95

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

103
static constexpr Domain alsa_mixer_domain("alsa_mixer");
104

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

113
	return PrepareAlsaMixerSockets(*this, mixer, pfd_buffer);
114 115
}

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

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

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

/*
 * libasound callbacks
 *
 */

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

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

	return 0;
}

/*
 * mixer_plugin methods
 *
 */

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

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

183
	return am;
184 185
}

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

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

206
	return nullptr;
207 208
}

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

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

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

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

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

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

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

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

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

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

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

259
	delete monitor;
260

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

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

270
	assert(handle != nullptr);
271

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

277
	return lrint(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT));
278 279
}

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

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

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