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