1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * LXQt - a lightweight, Qt based, desktop toolset
5 * https://lxqt.org
6 *
7 * Copyright: 2012 Razor team
8 * Authors:
9 * Johannes Zellner <webmaster@nebulon.de>
10 *
11 * This program or library is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU Lesser General Public
13 * License as published by the Free Software Foundation; either
14 * version 2.1 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Lesser General Public License for more details.
20
21 * You should have received a copy of the GNU Lesser General
22 * Public License along with this library; if not, write to the
23 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301 USA
25 *
26 * END_COMMON_COPYRIGHT_HEADER */
27
28 #include "alsaengine.h"
29
30 #include "alsadevice.h"
31
32 #include <QMetaType>
33 #include <QSocketNotifier>
34 #include <QtDebug>
35
36 AlsaEngine *AlsaEngine::m_instance = nullptr;
37
alsa_elem_event_callback(snd_mixer_elem_t * elem,unsigned int)38 static int alsa_elem_event_callback(snd_mixer_elem_t *elem, unsigned int /*mask*/)
39 {
40 AlsaEngine *engine = AlsaEngine::instance();
41 if (engine)
42 engine->updateDevice(engine->getDeviceByAlsaElem(elem));
43
44 return 0;
45 }
46
alsa_mixer_event_callback(snd_mixer_t *,unsigned int,snd_mixer_elem_t *)47 static int alsa_mixer_event_callback(snd_mixer_t * /*mixer*/, unsigned int /*mask*/, snd_mixer_elem_t * /*elem*/)
48 {
49 return 0;
50 }
51
AlsaEngine(QObject * parent)52 AlsaEngine::AlsaEngine(QObject *parent) :
53 AudioEngine(parent)
54 {
55 discoverDevices();
56 m_instance = this;
57 }
58
instance()59 AlsaEngine *AlsaEngine::instance()
60 {
61 return m_instance;
62 }
63
volumeMax(AudioDevice * device) const64 int AlsaEngine::volumeMax(AudioDevice *device) const
65 {
66 AlsaDevice * alsa_dev = qobject_cast<AlsaDevice *>(device);
67 Q_ASSERT(alsa_dev);
68 return alsa_dev->volumeMax();
69 }
70
getDeviceByAlsaElem(snd_mixer_elem_t * elem) const71 AlsaDevice *AlsaEngine::getDeviceByAlsaElem(snd_mixer_elem_t *elem) const
72 {
73 for (AudioDevice *device : qAsConst(m_sinks)) {
74 AlsaDevice *dev = qobject_cast<AlsaDevice*>(device);
75 if (!dev || !dev->element())
76 continue;
77
78 if (dev->element() == elem)
79 return dev;
80 }
81
82 return nullptr;
83 }
84
commitDeviceVolume(AudioDevice * device)85 void AlsaEngine::commitDeviceVolume(AudioDevice *device)
86 {
87 AlsaDevice *dev = qobject_cast<AlsaDevice*>(device);
88 if (!dev || !dev->element())
89 return;
90
91 long value = dev->volumeMin() + qRound(static_cast<double>(dev->volume()) / 100.0 * (dev->volumeMax() - dev->volumeMin()));
92 snd_mixer_selem_set_playback_volume_all(dev->element(), value);
93 }
94
setMute(AudioDevice * device,bool state)95 void AlsaEngine::setMute(AudioDevice *device, bool state)
96 {
97 AlsaDevice *dev = qobject_cast<AlsaDevice*>(device);
98 if (!dev || !dev->element())
99 return;
100
101 if (snd_mixer_selem_has_playback_switch(dev->element()))
102 snd_mixer_selem_set_playback_switch_all(dev->element(), (int)!state);
103 else if (state)
104 dev->setVolume(0);
105 }
106
updateDevice(AlsaDevice * device)107 void AlsaEngine::updateDevice(AlsaDevice *device)
108 {
109 if (!device)
110 return;
111
112 long value;
113 snd_mixer_selem_get_playback_volume(device->element(), (snd_mixer_selem_channel_id_t)0, &value);
114 // qDebug() << "updateDevice:" << device->name() << value;
115 device->setVolumeNoCommit(qRound((static_cast<double>(value - device->volumeMin()) * 100.0) / (device->volumeMax() - device->volumeMin())));
116
117 if (snd_mixer_selem_has_playback_switch(device->element())) {
118 int mute;
119 snd_mixer_selem_get_playback_switch(device->element(), (snd_mixer_selem_channel_id_t)0, &mute);
120 device->setMuteNoCommit(!(bool)mute);
121 }
122 }
123
driveAlsaEventHandling(int fd)124 void AlsaEngine::driveAlsaEventHandling(int fd)
125 {
126 snd_mixer_handle_events(m_mixerMap.value(fd));
127 }
128
discoverDevices()129 void AlsaEngine::discoverDevices()
130 {
131 int error;
132 int cardNum = -1;
133 const int BUFF_SIZE = 64;
134
135 while (true) {
136 if ((error = snd_card_next(&cardNum)) < 0) {
137 qWarning("Can't get the next card number: %s\n", snd_strerror(error));
138 break;
139 }
140
141 if (cardNum < 0)
142 break;
143
144 char str[BUFF_SIZE];
145 const size_t n = snprintf(str, sizeof(str), "hw:%i", cardNum);
146 if (BUFF_SIZE <= n) {
147 qWarning("AlsaEngine::discoverDevices: Buffer too small\n");
148 continue;
149 }
150
151 snd_ctl_t *cardHandle;
152 if ((error = snd_ctl_open(&cardHandle, str, 0)) < 0) {
153 qWarning("Can't open card %i: %s\n", cardNum, snd_strerror(error));
154 continue;
155 }
156
157 snd_ctl_card_info_t *cardInfo;
158 snd_ctl_card_info_alloca(&cardInfo);
159
160 QString cardName = QString::fromLatin1(snd_ctl_card_info_get_name(cardInfo));
161 if (cardName.isEmpty())
162 cardName = QString::fromLatin1(str);
163
164 if ((error = snd_ctl_card_info(cardHandle, cardInfo)) < 0) {
165 qWarning("Can't get info for card %i: %s\n", cardNum, snd_strerror(error));
166 } else {
167 // setup mixer and iterate over channels
168 snd_mixer_t *mixer = nullptr;
169 snd_mixer_open(&mixer, 0);
170 snd_mixer_attach(mixer, str);
171 snd_mixer_selem_register(mixer, nullptr, nullptr);
172 snd_mixer_load(mixer);
173
174 // setup event handler for mixer
175 snd_mixer_set_callback(mixer, alsa_mixer_event_callback);
176
177 // setup eventloop handling
178 struct pollfd pfd;
179 if (snd_mixer_poll_descriptors(mixer, &pfd, 1)) {
180 QSocketNotifier *notifier = new QSocketNotifier(pfd.fd, QSocketNotifier::Read, this);
181 connect(notifier, &QSocketNotifier::activated, this, [this] (QSocketDescriptor socket, QSocketNotifier::Type) { this->driveAlsaEventHandling(socket); });
182 m_mixerMap.insert(pfd.fd, mixer);
183 }
184
185 snd_mixer_elem_t *mixerElem = nullptr;
186 mixerElem = snd_mixer_first_elem(mixer);
187
188 while (mixerElem) {
189 // check if we have a Sink or Source
190 if (snd_mixer_selem_has_playback_volume(mixerElem)) {
191 AlsaDevice *dev = new AlsaDevice(Sink, this, this);
192 dev->setName(QString::fromLatin1(snd_mixer_selem_get_name(mixerElem)));
193 dev->setIndex(cardNum);
194 dev->setDescription(cardName + QStringLiteral(" - ") + dev->name());
195
196 // set alsa specific members
197 dev->setCardName(QString::fromLatin1(str));
198 dev->setMixer(mixer);
199 dev->setElement(mixerElem);
200
201 // get & store the range
202 long min, max;
203 snd_mixer_selem_get_playback_volume_range(mixerElem, &min, &max);
204 dev->setVolumeMinMax(min, max);
205
206 updateDevice(dev);
207
208 // register event callback
209 snd_mixer_elem_set_callback(mixerElem, alsa_elem_event_callback);
210
211 m_sinks.append(dev);
212 }
213
214 mixerElem = snd_mixer_elem_next(mixerElem);
215 }
216 }
217
218 snd_ctl_close(cardHandle);
219 }
220
221 snd_config_update_free_global();
222 }
223