1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
|
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/pulse_audio_mixer.h"
#include <pulse/pulseaudio.h>
#include "base/logging.h"
namespace chromeos {
// TODO(davej): Do asynchronous versions by using the threaded PulseAudio API
// so gets, sets, and init sequence do not block calling thread. GetVolume()
// and IsMute() may still need to be synchronous since caller is asking for the
// value specifically, which we will not know until the operation completes.
// So new asynchronous versions of the Get routines could be added with a
// callback parameter. Currently, all Get, Set and Init calls are blocking on
// PulseAudio.
//
// Set calls can just return without waiting. When a set operation completes,
// we could proxy to the UI thread to notify there was a volume change update
// so the new volume can be displayed to the user.
// TODO(davej): Serialize volume/mute to preserve settings when restarting?
// TODO(davej): Check if we need some thread safety mechanism (will someone be
// calling GetVolume while another process is calling SetVolume?)
namespace {
const int kInvalidDeviceId = -1;
} // namespace
// AudioInfo contains all the values we care about when getting info for a
// Sink (output device) used by GetAudioInfo()
struct PulseAudioMixer::AudioInfo {
pa_cvolume cvolume;
bool muted;
};
// This class will set volume using PulseAudio to adjust volume and mute.
PulseAudioMixer::PulseAudioMixer()
: device_id_(kInvalidDeviceId),
pa_mainloop_(NULL),
pa_context_(NULL),
connect_started_(false),
last_channels_(0) {
}
PulseAudioMixer::~PulseAudioMixer() {
PulseAudioFree();
}
bool PulseAudioMixer::Init() {
// Find main device for changing 'master' default volume.
if (!PulseAudioInit())
return false;
device_id_ = GetDefaultPlaybackDevice();
last_channels_ = 0;
LOG(INFO) << "PulseAudioMixer initialized OK";
return true;
}
double PulseAudioMixer::GetVolumeDb() const {
AudioInfo data;
if (!GetAudioInfo(&data))
return pa_sw_volume_to_dB(0); // this returns -inf
return pa_sw_volume_to_dB(data.cvolume.values[0]);
}
void PulseAudioMixer::SetVolumeDb(double vol_db) {
if (!PulseAudioValid()) {
DLOG(ERROR) << "Called SetVolume when mixer not valid";
return;
}
// last_channels_ determines the number of channels on the main output device,
// and is used later to set the volume on all channels at once.
if (!last_channels_) {
AudioInfo data;
if (!GetAudioInfo(&data))
return;
last_channels_ = data.cvolume.channels;
}
pa_operation* pa_op;
pa_cvolume cvolume;
pa_cvolume_set(&cvolume, last_channels_, pa_sw_volume_from_dB(vol_db));
pa_op = pa_context_set_sink_volume_by_index(pa_context_, device_id_,
&cvolume, NULL, NULL);
CompleteOperationHelper(pa_op);
}
bool PulseAudioMixer::IsMute() const {
AudioInfo data;
if (!GetAudioInfo(&data))
return false;
return data.muted;
}
void PulseAudioMixer::SetMute(bool mute) {
if (!PulseAudioValid()) {
DLOG(ERROR) << "Called SetMute when mixer not valid";
return;
}
pa_operation* pa_op;
pa_op = pa_context_set_sink_mute_by_index(pa_context_, device_id_,
mute ? 1 : 0, NULL, NULL);
CompleteOperationHelper(pa_op);
}
void PulseAudioMixer::ToggleMute() {
SetMute(!IsMute());
}
bool PulseAudioMixer::IsValid() const {
if (device_id_ == kInvalidDeviceId)
return false;
if (!pa_context_)
return false;
if (pa_context_get_state(pa_context_) != PA_CONTEXT_READY)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Private functions that deal with PulseAudio directly
bool PulseAudioMixer::PulseAudioInit() {
pa_mainloop_api* pa_mlapi;
pa_context_state_t state;
// It's OK to call Init again to re-initialize. This could be useful if
// !IsValid() and we want to try reconnecting.
if (pa_mainloop_)
PulseAudioFree();
while (true) {
// Create connection to default server.
pa_mainloop_ = pa_mainloop_new();
if (!pa_mainloop_) {
LOG(ERROR) << "Can't create PulseAudio mainloop";
break;
}
pa_mlapi = pa_mainloop_get_api(pa_mainloop_);
if (!pa_mlapi) {
LOG(ERROR) << "Can't get PulseAudio mainloop api";
break;
}
pa_context_ = pa_context_new(pa_mlapi, "ChromeAudio");
if (!pa_context_) {
LOG(ERROR) << "Can't create new PulseAudio context";
break;
}
// Using simpler method of just checking state after each iterate.
// Connect to PulseAudio sound server.
if (pa_context_connect(pa_context_, NULL,
PA_CONTEXT_NOAUTOSPAWN, NULL) != 0) {
LOG(ERROR) << "Can't start connection to PulseAudio sound server";
break;
}
connect_started_ = true;
// Wait until we have a completed connection or fail.
// TODO(davej): Remove blocking waits during init as well.
do {
pa_mainloop_iterate(pa_mainloop_, 1, NULL); // blocking wait
state = pa_context_get_state(pa_context_);
if (state == PA_CONTEXT_FAILED) {
LOG(ERROR) << "PulseAudio context connection failed";
break;
}
if (state == PA_CONTEXT_TERMINATED) {
LOG(ERROR) << "PulseAudio connection terminated early";
break;
}
} while (state != PA_CONTEXT_READY);
if (state != PA_CONTEXT_READY)
break;
return true;
}
// Failed startup sequence, clean up now.
PulseAudioFree();
return false;
}
void PulseAudioMixer::PulseAudioFree() {
if (pa_context_) {
if (connect_started_)
pa_context_disconnect(pa_context_);
pa_context_unref(pa_context_);
pa_context_ = NULL;
}
connect_started_ = false;
if (pa_mainloop_) {
pa_mainloop_free(pa_mainloop_);
pa_mainloop_ = NULL;
}
}
bool PulseAudioMixer::PulseAudioValid() const {
if (!pa_context_) {
DLOG(ERROR) << "Trying to use PulseAudio when no context";
return false;
}
if (pa_context_get_state(pa_context_) != PA_CONTEXT_READY) {
LOG(ERROR) << "PulseAudio context not ready";
return false;
}
if (device_id_ == kInvalidDeviceId) {
DLOG(ERROR) << "Trying to use PulseAudio when no device id";
return false;
}
return true;
}
bool PulseAudioMixer::CompleteOperationHelper(pa_operation* pa_op) {
// After starting any operation, this helper checks if it started OK, then
// waits for it to complete by iterating through the mainloop until the
// operation is not running anymore.
if (!pa_op) {
LOG(ERROR) << "Failed to start operation";
return false;
}
while (pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING) {
pa_mainloop_iterate(pa_mainloop_, 1, NULL);
}
pa_operation_unref(pa_op);
return true;
}
int PulseAudioMixer::GetDefaultPlaybackDevice() {
int device = kInvalidDeviceId;
pa_operation* pa_op;
if (!pa_context_) {
DLOG(ERROR) << "Trying to use PulseAudio when no context";
return kInvalidDeviceId;
}
if (pa_context_get_state(pa_context_) != PA_CONTEXT_READY) {
LOG(ERROR) << "PulseAudio context not ready";
return kInvalidDeviceId;
}
pa_op = pa_context_get_sink_info_list(pa_context_,
EnumerateDevicesCallback,
&device);
CompleteOperationHelper(pa_op);
return device;
}
// static
void PulseAudioMixer::EnumerateDevicesCallback(pa_context* unused,
const pa_sink_info* sink_info,
int eol,
void* userdata) {
int* pa_device = static_cast<int*>(userdata);
// If eol is set to a positive number, you're at the end of the list.
if (eol > 0)
return;
// TODO(davej): Should we handle cases of more than one output sink device?
if (*pa_device == kInvalidDeviceId)
*pa_device = sink_info->index;
}
bool PulseAudioMixer::GetAudioInfo(AudioInfo* info) const {
DCHECK(info);
if (!PulseAudioValid()) {
DLOG(ERROR) << "Called GetAudioInfo when mixer not valid";
return false;
}
if (!info)
return false;
pa_operation* pa_op;
pa_op = pa_context_get_sink_info_by_index(pa_context_,
device_id_,
GetAudioInfoCallback,
info);
// Duplicating some code in CompleteOperationHelper because this function
// needs to stay 'const', and this isn't allowed if that is called.
if (!pa_op) {
LOG(ERROR) << "Failed to start operation";
return false;
}
while (pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING) {
pa_mainloop_iterate(pa_mainloop_, 1, NULL);
}
pa_operation_unref(pa_op);
return true;
}
// static
void PulseAudioMixer::GetAudioInfoCallback(pa_context* unused,
const pa_sink_info* sink_info,
int eol,
void* userdata) {
if (!userdata) {
DLOG(ERROR) << "userdata NULL";
return;
}
AudioInfo* data = static_cast<AudioInfo*>(userdata);
// Copy just the information we care about.
if (eol == 0) {
data->cvolume = sink_info->volume;
data->muted = sink_info->mute ? true : false;
}
}
} // namespace chromeos
|