diff --git a/backends/mixer_pulse.cpp b/backends/mixer_pulse.cpp index fd9bc21d..d5aced2b 100644 --- a/backends/mixer_pulse.cpp +++ b/backends/mixer_pulse.cpp @@ -1,1453 +1,1443 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2008 Helio Chissini de Castro * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library 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. */ #include "mixer_pulse.h" #include #include #include #include #include #include "core/mixer.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include #include #if defined(HAVE_CANBERRA) # include #endif // PA_VOLUME_UI_MAX landed in pulseaudio-0.9.23, so this can be removed when/if // minimum requirement is ever bumped up (from 0.9.12 currently) #ifndef PA_VOLUME_UI_MAX #define PA_VOLUME_UI_MAX (pa_sw_volume_from_dB(+11.0)) #endif #define HAVE_SOURCE_OUTPUT_VOLUMES PA_CHECK_VERSION(1,0,0) #define KMIXPA_PLAYBACK 0 #define KMIXPA_CAPTURE 1 #define KMIXPA_APP_PLAYBACK 2 #define KMIXPA_APP_CAPTURE 3 #define KMIXPA_WIDGET_MAX KMIXPA_APP_CAPTURE #define KMIXPA_EVENT_KEY "sink-input-by-media-role:event" static unsigned int refcount = 0; static pa_glib_mainloop *s_mainloop = NULL; static pa_context *s_context = NULL; static enum { UNKNOWN, ACTIVE, INACTIVE } s_pulseActive = UNKNOWN; static int s_outstandingRequests = 0; #if defined(HAVE_CANBERRA) static ca_context *s_ccontext = NULL; #endif QMap s_mixers; static devmap outputDevices; static devmap captureDevices; static QMap clients; static devmap outputStreams; static devmap captureStreams; static devmap outputRoles; typedef struct { pa_channel_map channel_map; pa_cvolume volume; bool mute; QString device; } restoreRule; static QMap s_RestoreRules; static void dec_outstanding(pa_context *c) { if (s_outstandingRequests <= 0) return; if (--s_outstandingRequests == 0) { s_pulseActive = ACTIVE; // If this is our probe phase, exit our context immediately if (s_context != c) { pa_context_disconnect(c); } else qCDebug(KMIX_LOG) << "Reconnected to PulseAudio"; } } static void translateMasksAndMaps(devinfo& dev) { dev.chanMask = Volume::MNONE; dev.chanIDs.clear(); if (dev.channel_map.channels != dev.volume.channels) { qCCritical(KMIX_LOG) << "Hideous Channel mixup map says " << dev.channel_map.channels << ", volume says: " << dev.volume.channels; return; } if (1 == dev.channel_map.channels && PA_CHANNEL_POSITION_MONO == dev.channel_map.map[0]) { // We just use the left channel to represent this. dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MLEFT); dev.chanIDs[0] = Volume::LEFT; } else { for (uint8_t i = 0; i < dev.channel_map.channels; ++i) { switch (dev.channel_map.map[i]) { case PA_CHANNEL_POSITION_MONO: qCWarning(KMIX_LOG) << "Channel Map contains a MONO element but has >1 channel - we can't handle this."; return; case PA_CHANNEL_POSITION_FRONT_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MLEFT); dev.chanIDs[i] = Volume::LEFT; break; case PA_CHANNEL_POSITION_FRONT_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MRIGHT); dev.chanIDs[i] = Volume::RIGHT; break; case PA_CHANNEL_POSITION_FRONT_CENTER: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MCENTER); dev.chanIDs[i] = Volume::CENTER; break; case PA_CHANNEL_POSITION_REAR_CENTER: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARCENTER); dev.chanIDs[i] = Volume::REARCENTER; break; case PA_CHANNEL_POSITION_REAR_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MSURROUNDLEFT); dev.chanIDs[i] = Volume::SURROUNDLEFT; break; case PA_CHANNEL_POSITION_REAR_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MSURROUNDRIGHT); dev.chanIDs[i] = Volume::SURROUNDRIGHT; break; case PA_CHANNEL_POSITION_LFE: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MWOOFER); dev.chanIDs[i] = Volume::WOOFER; break; case PA_CHANNEL_POSITION_SIDE_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARSIDELEFT); dev.chanIDs[i] = Volume::REARSIDELEFT; break; case PA_CHANNEL_POSITION_SIDE_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARSIDERIGHT); dev.chanIDs[i] = Volume::REARSIDERIGHT; break; default: qCWarning(KMIX_LOG) << "Channel Map contains a pa_channel_position we cannot handle " << dev.channel_map.map[i]; break; } } } } static QString getIconNameFromProplist(pa_proplist *l) { const char *t; if ((t = pa_proplist_gets(l, PA_PROP_MEDIA_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_WINDOW_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_APPLICATION_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_MEDIA_ROLE))) { if (strcmp(t, "video") == 0 || strcmp(t, "phone") == 0) return QString::fromUtf8(t); if (strcmp(t, "music") == 0) return "audio"; if (strcmp(t, "game") == 0) return "applications-games"; if (strcmp(t, "event") == 0) return "dialog-information"; } return ""; } static void sink_cb(pa_context *c, const pa_sink_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Sink callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_PLAYBACK)) s_mixers[KMIXPA_PLAYBACK]->triggerUpdate(); return; } devinfo s; s.index = s.device_index = i->index; s.name = QString::fromUtf8(i->name).replace(' ', '_'); s.description = QString::fromUtf8(i->description); s.icon_name = QString::fromUtf8(pa_proplist_gets(i->proplist, PA_PROP_DEVICE_ICON_NAME)); s.volume = i->volume; s.channel_map = i->channel_map; s.mute = !!i->mute; s.stream_restore_rule = ""; s.priority = 0; if (i->active_port != NULL) s.priority = i->active_port->priority; translateMasksAndMaps(s); bool is_new = !outputDevices.contains(s.index); outputDevices[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about sink: " << s.description; if (s_mixers.contains(KMIXPA_PLAYBACK)) { if (is_new) s_mixers[KMIXPA_PLAYBACK]->addWidget(s.index); else { int mid = s_mixers[KMIXPA_PLAYBACK]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_PLAYBACK]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void source_cb(pa_context *c, const pa_source_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Source callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_CAPTURE)) s_mixers[KMIXPA_CAPTURE]->triggerUpdate(); return; } // Do something.... if (PA_INVALID_INDEX != i->monitor_of_sink) { // qCDebug(KMIX_LOG) << "Ignoring Monitor Source: " << i->description; return; } devinfo s; s.index = s.device_index = i->index; s.name = QString::fromUtf8(i->name).replace(' ', '_'); s.description = QString::fromUtf8(i->description); s.icon_name = QString::fromUtf8(pa_proplist_gets(i->proplist, PA_PROP_DEVICE_ICON_NAME)); s.volume = i->volume; s.channel_map = i->channel_map; s.mute = !!i->mute; s.stream_restore_rule = ""; translateMasksAndMaps(s); bool is_new = !captureDevices.contains(s.index); captureDevices[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about source: " << s.description; if (s_mixers.contains(KMIXPA_CAPTURE)) { if (is_new) s_mixers[KMIXPA_CAPTURE]->addWidget(s.index); else { int mid = s_mixers[KMIXPA_CAPTURE]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_CAPTURE]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void client_cb(pa_context *c, const pa_client_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Client callback failure"; return; } if (eol > 0) { dec_outstanding(c); return; } clients[i->index] = QString::fromUtf8(i->name); //qCDebug(KMIX_LOG) << "Got some info about client: " << clients[i->index]; } static void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Sink Input callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) s_mixers[KMIXPA_APP_PLAYBACK]->triggerUpdate(); return; } const char *t; if ((t = pa_proplist_gets(i->proplist, "module-stream-restore.id"))) { if (strcmp(t, KMIXPA_EVENT_KEY) == 0) { //qCDebug(KMIX_LOG) << "Ignoring sink-input due to it being designated as an event and thus handled by the Event slider"; return; } } QString appname = i18n("Unknown Application"); if (clients.contains(i->client)) appname = clients.value(i->client); devinfo s; s.index = i->index; s.device_index = i->sink; s.description = appname % QLatin1String(": ") % QString::fromUtf8(i->name); s.name = QString("stream:") + QString::number(i->index); //appname.replace(' ', '_').toLower(); s.icon_name = getIconNameFromProplist(i->proplist); s.channel_map = i->channel_map; s.volume = i->volume; s.mute = !!i->mute; s.stream_restore_rule = QString::fromUtf8(t); translateMasksAndMaps(s); bool is_new = !outputStreams.contains(s.index); outputStreams[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about sink input (playback stream): " << s.description; if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { if (is_new) s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index, true); else { int mid = s_mixers[KMIXPA_APP_PLAYBACK]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_APP_PLAYBACK]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Source Output callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_APP_CAPTURE)) s_mixers[KMIXPA_APP_CAPTURE]->triggerUpdate(); return; } /* NB Until Source Outputs support volumes, we just use the volume of the source itself */ if (!captureDevices.contains(i->source)) { qCDebug(KMIX_LOG) << "Source Output refers to a Source we don't have any info for (probably just a peak meter or similar)"; return; } QString appname = i18n("Unknown Application"); if (clients.contains(i->client)) appname = clients.value(i->client); devinfo s; s.index = i->index; s.device_index = i->source; s.description = appname % QLatin1String(": ") % QString::fromUtf8(i->name); s.name = QString("stream:") + QString::number(i->index); //appname.replace(' ', '_').toLower(); s.icon_name = getIconNameFromProplist(i->proplist); s.channel_map = i->channel_map; #if HAVE_SOURCE_OUTPUT_VOLUMES s.volume = i->volume; s.mute = !!i->mute; #else s.volume = captureDevices[i->source].volume; s.mute = captureDevices[i->source].mute; #endif s.stream_restore_rule = QString::fromUtf8(pa_proplist_gets(i->proplist, "module-stream-restore.id")); translateMasksAndMaps(s); bool is_new = !captureStreams.contains(s.index); captureStreams[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about source output (capture stream): " << s.description; if (s_mixers.contains(KMIXPA_APP_CAPTURE)) { if (is_new) s_mixers[KMIXPA_APP_CAPTURE]->addWidget(s.index, true); else { int mid = s_mixers[KMIXPA_APP_CAPTURE]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_APP_CAPTURE]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static devinfo create_role_devinfo(QString name) { Q_ASSERT(s_RestoreRules.contains(name)); devinfo s; s.index = s.device_index = PA_INVALID_INDEX; s.description = i18n("Event Sounds"); s.name = QString("restore:") + name; s.icon_name = "dialog-information"; s.channel_map = s_RestoreRules[name].channel_map; s.volume = s_RestoreRules[name].volume; s.mute = s_RestoreRules[name].mute; s.stream_restore_rule = name; translateMasksAndMaps(s); return s; } void ext_stream_restore_read_cb(pa_context *c, const pa_ext_stream_restore_info *i, int eol, void *) { if (eol < 0) { dec_outstanding(c); qCWarning(KMIX_LOG) << "Failed to initialize stream_restore extension: " << pa_strerror(pa_context_errno(s_context)); return; } if (eol > 0) { dec_outstanding(c); // Special case: ensure that our media events exists. // On first login by a new users, this wont be in our database so we should create it. if (!s_RestoreRules.contains(KMIXPA_EVENT_KEY)) { // Create a fake rule restoreRule rule; rule.channel_map.channels = 1; rule.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; rule.volume.channels = 1; rule.volume.values[0] = PA_VOLUME_NORM; rule.mute = false; rule.device = ""; s_RestoreRules[KMIXPA_EVENT_KEY] = rule; qCDebug(KMIX_LOG) << "Initializing restore rule for new user: " << i18n("Event Sounds"); } if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { // If we have rules, it will be created below... but if no rules // then we add it here. if (!outputRoles.contains(PA_INVALID_INDEX)) { devinfo s = create_role_devinfo(KMIXPA_EVENT_KEY); outputRoles[s.index] = s; s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index); } s_mixers[KMIXPA_APP_PLAYBACK]->triggerUpdate(); } return; } QString name = QString::fromUtf8(i->name); // qCDebug(KMIX_LOG) << QString("Got some info about restore rule: '%1' (Device: %2)").arg(name).arg(i->device ? i->device : "None"); restoreRule rule; rule.channel_map = i->channel_map; rule.volume = i->volume; rule.mute = !!i->mute; rule.device = i->device; if (rule.channel_map.channels < 1 && name == KMIXPA_EVENT_KEY) { // Stream restore rules may not have valid volumes/channel maps (as these are optional) // but we need a valid volume+channelmap for our events sounds so fix it up. rule.channel_map.channels = 1; rule.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; rule.volume.channels = 1; rule.volume.values[0] = PA_VOLUME_NORM; } s_RestoreRules[name] = rule; if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { // We only want to know about Sound Events for now... if (name == KMIXPA_EVENT_KEY) { devinfo s = create_role_devinfo(name); bool is_new = !outputRoles.contains(s.index); outputRoles[s.index] = s; if (is_new) s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index, true); } } } static void ext_stream_restore_subscribe_cb(pa_context *c, void *) { Q_ASSERT(c == s_context); pa_operation *o; if (!(o = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_ext_stream_restore_read() failed"; return; } pa_operation_unref(o); } static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *) { Q_ASSERT(c == s_context); switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { case PA_SUBSCRIPTION_EVENT_SINK: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_PLAYBACK)) s_mixers[KMIXPA_PLAYBACK]->removeWidget(index); } else { pa_operation *o; if (!(o = pa_context_get_sink_info_by_index(c, index, sink_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_sink_info_by_index() failed"; return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SOURCE: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_CAPTURE)) s_mixers[KMIXPA_CAPTURE]->removeWidget(index); } else { pa_operation *o; if (!(o = pa_context_get_source_info_by_index(c, index, source_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_source_info_by_index() failed"; return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SINK_INPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) s_mixers[KMIXPA_APP_PLAYBACK]->removeWidget(index); } else { pa_operation *o; if (!(o = pa_context_get_sink_input_info(c, index, sink_input_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_sink_input_info() failed"; return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_APP_CAPTURE)) s_mixers[KMIXPA_APP_CAPTURE]->removeWidget(index); } else { pa_operation *o; if (!(o = pa_context_get_source_output_info(c, index, source_output_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_sink_input_info() failed"; return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { clients.remove(index); } else { pa_operation *o; if (!(o = pa_context_get_client_info(c, index, client_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_client_info() failed"; return; } pa_operation_unref(o); } break; } } static void context_state_callback(pa_context *c, void *) { pa_context_state_t state = pa_context_get_state(c); if (state == PA_CONTEXT_READY) { // Attempt to load things up pa_operation *o; // 1. Register for the stream changes (except during probe) if (s_context == c) { pa_context_set_subscribe_callback(c, subscribe_cb, NULL); if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t) (PA_SUBSCRIPTION_MASK_SINK| PA_SUBSCRIPTION_MASK_SOURCE| PA_SUBSCRIPTION_MASK_CLIENT| PA_SUBSCRIPTION_MASK_SINK_INPUT| PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL))) { qCWarning(KMIX_LOG) << "pa_context_subscribe() failed"; return; } pa_operation_unref(o); } if (!(o = pa_context_get_sink_info_list(c, sink_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_sink_info_list() failed"; return; } pa_operation_unref(o); s_outstandingRequests++; if (!(o = pa_context_get_source_info_list(c, source_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_source_info_list() failed"; return; } pa_operation_unref(o); s_outstandingRequests++; if (!(o = pa_context_get_client_info_list(c, client_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_client_info_list() failed"; return; } pa_operation_unref(o); s_outstandingRequests++; if (!(o = pa_context_get_sink_input_info_list(c, sink_input_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_sink_input_info_list() failed"; return; } pa_operation_unref(o); s_outstandingRequests++; if (!(o = pa_context_get_source_output_info_list(c, source_output_cb, NULL))) { qCWarning(KMIX_LOG) << "pa_context_get_source_output_info_list() failed"; return; } pa_operation_unref(o); s_outstandingRequests++; /* These calls are not always supported */ if ((o = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL))) { pa_operation_unref(o); s_outstandingRequests++; pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, NULL); if ((o = pa_ext_stream_restore_subscribe(c, 1, NULL, NULL))) pa_operation_unref(o); } else { qCWarning(KMIX_LOG) << "Failed to initialize stream_restore extension: " << pa_strerror(pa_context_errno(s_context)); } } else if (!PA_CONTEXT_IS_GOOD(state)) { // If this is our probe phase, exit our context immediately if (s_context != c) { pa_context_disconnect(c); } else { // If we're not probing, it means we've been disconnected from our // glib context pa_context_unref(s_context); s_context = NULL; // Remove all GUI elements QMap::iterator it; for (it = s_mixers.begin(); it != s_mixers.end(); ++it) { (*it)->removeAllWidgets(); } // This one is not handled above. clients.clear(); if (s_mixers.contains(KMIXPA_PLAYBACK)) { qCWarning(KMIX_LOG) << "Connection to PulseAudio daemon closed. Attempting reconnection."; s_pulseActive = UNKNOWN; QTimer::singleShot(50, s_mixers[KMIXPA_PLAYBACK], SLOT(reinit())); } } } } static void setVolumeFromPulse(Volume& volume, const devinfo& dev) { chanIDMap::const_iterator iter; for (iter = dev.chanIDs.begin(); iter != dev.chanIDs.end(); ++iter) { //qCDebug(KMIX_LOG) << "Setting volume for channel " << iter.value() << " to " << (long)dev.volume.values[iter.key()] << " (" << ((100*(long)dev.volume.values[iter.key()]) / PA_VOLUME_NORM) << "%)"; volume.setVolume(iter.value(), (long)dev.volume.values[iter.key()]); } } static pa_cvolume genVolumeForPulse(const devinfo& dev, Volume& volume) { pa_cvolume cvol = dev.volume; chanIDMap::const_iterator iter; for (iter = dev.chanIDs.begin(); iter != dev.chanIDs.end(); ++iter) { cvol.values[iter.key()] = (uint32_t)volume.getVolume(iter.value()); //qCDebug(KMIX_LOG) << "Setting volume for channel " << iter.value() << " to " << cvol.values[iter.key()] << " (" << ((100*cvol.values[iter.key()]) / PA_VOLUME_NORM) << "%)"; } return cvol; } static devmap* get_widget_map(int type, QString id = QString()) { Q_ASSERT(type >= 0 && type <= KMIXPA_WIDGET_MAX); if (KMIXPA_PLAYBACK == type) return &outputDevices; else if (KMIXPA_CAPTURE == type) return &captureDevices; else if (KMIXPA_APP_PLAYBACK == type) { if (id.startsWith(QLatin1String("restore:"))) return &outputRoles; return &outputStreams; } else if (KMIXPA_APP_CAPTURE == type) return &captureStreams; Q_ASSERT(0); return NULL; } static devmap* get_widget_map(int type, int index) { if (PA_INVALID_INDEX == (uint32_t)index) return get_widget_map(type, "restore:"); return get_widget_map(type); } void Mixer_PULSE::emitControlsReconfigured() { // emit controlsReconfigured(_mixer->id()); // Do not emit directly to ensure all connected slots are executed // in their own event loop. /* * Bug 309464: * * Comment by cesken: I am not really sure what the comment above means. * 1) IIRC coling told me "otherwise KMix crashes". * 2) There are also bug reports that heavily indicate the crash when operation the "move stream" from a popup * menu. * 3) I don't know what the "executed in their own event loop" means. Are we in a "wrong" thread here (PA), * which is not suitable for GUI code?!? * * Work note: Ouch. it means PA thread makes direct calls via announce(), and do even GUI code. OUCH. Redo this comments! * * Conclusions: * a) It seems there seems to be some object deletion hazard with a QMenu (the one for "move stream") * b) I do not see why executing it Queued is better, because you can never know when it is actually being * executed: it could be "right now". It looks like Qt currently executes it after the QMenu hazard has * resolved itself miraculously. * c) I am definitely strongly opposed on this "execute later" approach. It is pure gambling IMO and might be * broken any time (from DEBUG to RELEASE build, or by a new Qt or KDE version). * * TODO Somebody with more Qt and PA internal insight might help to clear up things here. * * Temporary solution: Do the QueuedConnection until we really know hat is going on. But the called code * pulseControlsReconfigured() will then do the standard announce() so that every part of * KMix automatically gets updated. * */ QMetaObject::invokeMethod(this, "pulseControlsReconfigured", Qt::QueuedConnection); // QMetaObject::invokeMethod(this, // "pulseControlsReconfigured", // Qt::QueuedConnection, // Q_ARG(QString, _mixer->id())); } void Mixer_PULSE::pulseControlsReconfigured() { qCDebug(KMIX_LOG) << "Reconfigure " << _mixer->id(); ControlManager::instance().announce(_mixer->id(), ControlManager::ControlList, getDriverName()); } void Mixer_PULSE::pulseControlsReconfigured(QString mixerId) { qCDebug(KMIX_LOG) << "Reconfigure " << mixerId; ControlManager::instance().announce(mixerId, ControlManager::ControlList, getDriverName()); } void Mixer_PULSE::updateRecommendedMaster(devmap* map) { unsigned int prio = 0; shared_ptr res; MixSet::iterator iter; for (iter = m_mixDevices.begin(); iter != m_mixDevices.end(); ++iter) { unsigned int devprio = map->value( id2num((*iter)->id()) ).priority; if (( devprio > prio ) || !res ) { prio = devprio; res = *iter; } } if (res) qCDebug(KMIX_LOG) << "Selecting master " << res->id() << " for type " << m_devnum; m_recommendedMaster = res; } void Mixer_PULSE::addWidget(int index, bool isAppStream) { devmap* map = get_widget_map(m_devnum, index); if (!map->contains(index)) { qCWarning(KMIX_LOG) << "New " << m_devnum << " widget notified for index " << index << " but I cannot find it in my list :s"; return; } if (addDevice((*map)[index], isAppStream)) updateRecommendedMaster(map); emitControlsReconfigured(); } void Mixer_PULSE::removeWidget(int index) { devmap* map = get_widget_map(m_devnum); if (!map->contains(index)) { qCDebug(KMIX_LOG) << "Removing " << m_devnum << " widget notified for index " << index << " but I cannot find it in my list :s"; // Sometimes we ignore things (e.g. event sounds) so don't be too noisy here. return; } QString id = (*map)[index].name; map->remove(index); // We need to find the MixDevice that goes with this widget and remove it. MixSet::iterator iter; shared_ptr md; for (iter = m_mixDevices.begin(); iter != m_mixDevices.end(); ++iter) { if ((*iter)->id() == id) { md = m_mixDevices.get(id); qCDebug(KMIX_LOG) << "MixDevice 1 useCount=" << md.use_count(); md->close(); qCDebug(KMIX_LOG) << "MixDevice 2 useCount=" << md.use_count(); m_mixDevices.erase(iter); qCDebug(KMIX_LOG) << "MixDevice 3 useCount=" << md.use_count(); break; } } if (md) updateRecommendedMaster(map); emitControlsReconfigured(); qCDebug(KMIX_LOG) << "MixDevice 4 useCount=" << md.use_count(); } void Mixer_PULSE::removeAllWidgets() { devmap* map = get_widget_map(m_devnum); map->clear(); // Special case if (KMIXPA_APP_PLAYBACK == m_devnum) outputRoles.clear(); freeMixDevices(); emitControlsReconfigured(); } bool Mixer_PULSE::addDevice(devinfo& dev, bool isAppStream) { - if (dev.chanMask == Volume::MNONE) return false; + if (dev.chanMask==Volume::MNONE) return (false); MixSet *ms = nullptr; - if (m_devnum == KMIXPA_APP_PLAYBACK && s_mixers.contains(KMIXPA_PLAYBACK)) + if (m_devnum==KMIXPA_APP_PLAYBACK && s_mixers.contains(KMIXPA_PLAYBACK)) + { ms = s_mixers[KMIXPA_PLAYBACK]->getMixSet(); - else if (m_devnum == KMIXPA_APP_CAPTURE && s_mixers.contains(KMIXPA_CAPTURE)) + } + else if (m_devnum==KMIXPA_APP_CAPTURE && s_mixers.contains(KMIXPA_CAPTURE)) + { ms = s_mixers[KMIXPA_CAPTURE]->getMixSet(); + } + + const bool isCapture = (m_devnum==KMIXPA_APP_CAPTURE || m_devnum==KMIXPA_CAPTURE); - int maxVol = GlobalConfig::instance().data.volumeOverdrive ? PA_VOLUME_UI_MAX : PA_VOLUME_NORM; + const int maxVol = (!isCapture && GlobalConfig::instance().data.volumeOverdrive) ? PA_VOLUME_UI_MAX : PA_VOLUME_NORM; Volume v(maxVol, PA_VOLUME_MUTED, true, false); v.addVolumeChannels(dev.chanMask); setVolumeFromPulse(v, dev); + MixDevice* md = new MixDevice( _mixer, dev.name, dev.description, dev.icon_name, ms); - if (isAppStream) - md->setApplicationStream(true); - -// qCDebug(KMIX_LOG) << "Adding Pulse volume " << dev.name << ", isCapture= " -// << (m_devnum == KMIXPA_CAPTURE || m_devnum == KMIXPA_APP_CAPTURE) -// << ", isAppStream= " << isAppStream << "=" << md->isApplicationStream() << ", devnum=" << m_devnum; - md->addPlaybackVolume(v); - md->setMuted(dev.mute); + if (isAppStream) md->setApplicationStream(true); + + //qCDebug(KMIX_LOG) << "Adding Pulse volume" << dev.name + // << "isCapture" << isCapture + // << "isAppStream" << isAppStream << "=" << md->isApplicationStream() + // << "devnum" << m_devnum; + + if (isCapture) + { + md->addCaptureVolume(v); + md->setRecSource(!dev.mute); + } + else + { + md->addPlaybackVolume(v); + md->setMuted(dev.mute); + } + m_mixDevices.append(md->addToPool()); - return true; + return (true); } Mixer_Backend* PULSE_getMixer( Mixer *mixer, int devnum ) { Mixer_Backend *l_mixer; l_mixer = new Mixer_PULSE( mixer, devnum ); return l_mixer; } bool Mixer_PULSE::connectToDaemon() { Q_ASSERT(NULL == s_context); qCDebug(KMIX_LOG) << "Attempting connection to PulseAudio sound daemon"; pa_mainloop_api *api = pa_glib_mainloop_get_api(s_mainloop); Q_ASSERT(api); s_context = pa_context_new(api, "KMix"); Q_ASSERT(s_context); if (pa_context_connect(s_context, NULL, PA_CONTEXT_NOFAIL, 0) < 0) { pa_context_unref(s_context); s_context = NULL; return false; } pa_context_set_state_callback(s_context, &context_state_callback, NULL); return true; } Mixer_PULSE::Mixer_PULSE(Mixer *mixer, int devnum) : Mixer_Backend(mixer, devnum) { if ( devnum == -1 ) m_devnum = 0; QString pulseenv = qgetenv("KMIX_PULSEAUDIO_DISABLE"); if (pulseenv.toInt()) s_pulseActive = INACTIVE; // We require a glib event loop if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib") && !QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("GlibEventDispatcher")) { qCDebug(KMIX_LOG) << "Disabling PulseAudio integration for lack of GLib event loop"; s_pulseActive = INACTIVE; } ++refcount; if (INACTIVE != s_pulseActive && 1 == refcount) { // First of all conenct to PA via simple/blocking means and if that succeeds, // use a fully async integrated mainloop method to connect and get proper support. pa_mainloop *p_test_mainloop; if (!(p_test_mainloop = pa_mainloop_new())) { qCDebug(KMIX_LOG) << "PulseAudio support disabled: Unable to create mainloop"; s_pulseActive = INACTIVE; goto endconstruct; } pa_context *p_test_context; if (!(p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "kmix-probe"))) { qCDebug(KMIX_LOG) << "PulseAudio support disabled: Unable to create context"; pa_mainloop_free(p_test_mainloop); s_pulseActive = INACTIVE; goto endconstruct; } qCDebug(KMIX_LOG) << "Probing for PulseAudio..."; // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required if (pa_context_connect(p_test_context, NULL, static_cast(0), NULL) < 0) { qCDebug(KMIX_LOG) << QString("PulseAudio support disabled: %1").arg(pa_strerror(pa_context_errno(p_test_context))); pa_context_disconnect(p_test_context); pa_context_unref(p_test_context); pa_mainloop_free(p_test_mainloop); s_pulseActive = INACTIVE; goto endconstruct; } // Assume we are inactive, it will be set to active if appropriate s_pulseActive = INACTIVE; pa_context_set_state_callback(p_test_context, &context_state_callback, NULL); for (;;) { pa_mainloop_iterate(p_test_mainloop, 1, NULL); if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(p_test_context))) { qCDebug(KMIX_LOG) << "PulseAudio probe complete."; break; } } pa_context_disconnect(p_test_context); pa_context_unref(p_test_context); pa_mainloop_free(p_test_mainloop); if (INACTIVE != s_pulseActive) { // Reconnect via integrated mainloop s_mainloop = pa_glib_mainloop_new(NULL); Q_ASSERT(s_mainloop); connectToDaemon(); #if defined(HAVE_CANBERRA) int ret = ca_context_create(&s_ccontext); if (ret < 0) { qCDebug(KMIX_LOG) << "Disabling Sound Feedback. Canberra context failed."; s_ccontext = NULL; } else ca_context_set_driver(s_ccontext, "pulse"); #endif } qCDebug(KMIX_LOG) << "PulseAudio status: " << (s_pulseActive==UNKNOWN ? "Unknown (bug)" : (s_pulseActive==ACTIVE ? "Active" : "Inactive")); } endconstruct: s_mixers[m_devnum] = this; } Mixer_PULSE::~Mixer_PULSE() { s_mixers.remove(m_devnum); if (refcount > 0) { --refcount; if (0 == refcount) { #if defined(HAVE_CANBERRA) if (s_ccontext) { ca_context_destroy(s_ccontext); s_ccontext = NULL; } #endif if (s_context) { pa_context_unref(s_context); s_context = NULL; } if (s_mainloop) { pa_glib_mainloop_free(s_mainloop); s_mainloop = NULL; } } } closeCommon(); } int Mixer_PULSE::open() { //qCDebug(KMIX_LOG) << "Trying Pulse sink"; if (ACTIVE == s_pulseActive && m_devnum <= KMIXPA_APP_CAPTURE) { // Make sure the GUI layers know we are dynamic so as to always paint us _mixer->setDynamic(); devmap::iterator iter; if (KMIXPA_PLAYBACK == m_devnum) { _id = "Playback Devices"; registerCard(i18n("Playback Devices")); for (iter = outputDevices.begin(); iter != outputDevices.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&outputDevices); } else if (KMIXPA_CAPTURE == m_devnum) { _id = "Capture Devices"; registerCard(i18n("Capture Devices")); for (iter = captureDevices.begin(); iter != captureDevices.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&outputDevices); } else if (KMIXPA_APP_PLAYBACK == m_devnum) { _id = "Playback Streams"; registerCard(i18n("Playback Streams")); for (iter = outputRoles.begin(); iter != outputRoles.end(); ++iter) addDevice(*iter, true); updateRecommendedMaster(&outputRoles); for (iter = outputStreams.begin(); iter != outputStreams.end(); ++iter) addDevice(*iter, true); updateRecommendedMaster(&outputStreams); } else if (KMIXPA_APP_CAPTURE == m_devnum) { _id = "Capture Streams"; registerCard(i18n("Capture Streams")); for (iter = captureStreams.begin(); iter != captureStreams.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&captureStreams); } qCDebug(KMIX_LOG) << "Using PulseAudio for mixer: " << getName(); m_isOpen = true; } return 0; } int Mixer_PULSE::close() { closeCommon(); return 1; } int Mixer_PULSE::id2num(const QString& id) { int num = -1; // todo: Store this in a hash or similar int i; for (i = 0; i < m_mixDevices.size(); ++i) { if (m_mixDevices[i]->id() == id) { num = i; break; } } //qCDebug(KMIX_LOG) << "id2num() num=" << num; return num; } -int Mixer_PULSE::readVolumeFromHW( const QString& id, shared_ptr md ) -{ - devmap *map = get_widget_map(m_devnum, id); - devmap::iterator iter; - for (iter = map->begin(); iter != map->end(); ++iter) +static int doForDevice(const QString &id, const devmap *devices, shared_ptr md, + int (*func)(const devinfo &, shared_ptr)) +{ + for (devmap::const_iterator iter = devices->constBegin(); iter!=devices->constEnd(); ++iter) { - if (iter->name == id) - { - setVolumeFromPulse(md->playbackVolume(), *iter); - md->setMuted(iter->mute); - break; - } + if (iter->name==id) return ((*func)(*iter, md)); } - return 0; + return (Mixer::OK); } -int Mixer_PULSE::writeVolumeToHW( const QString& id, shared_ptr md ) -{ - devmap::iterator iter; - if (KMIXPA_PLAYBACK == m_devnum) - { - for (iter = outputDevices.begin(); iter != outputDevices.end(); ++iter) - { - if (iter->name == id) - { - pa_operation *o; - pa_cvolume volume = genVolumeForPulse(*iter, md->playbackVolume()); - if (!(o = pa_context_set_sink_volume_by_index(s_context, iter->index, &volume, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_sink_volume_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - if (!(o = pa_context_set_sink_mute_by_index(s_context, iter->index, (md->isMuted() ? 1 : 0), NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_sink_mute_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - -#if defined(HAVE_CANBERRA) - if (s_ccontext && Mixer::getBeepOnVolumeChange() ) { - int playing = 0; - int cindex = 2; // Note "2" is simply the index we've picked. It's somewhat irrelevant. - - - ca_context_playing(s_ccontext, cindex, &playing); - - // NB Depending on how this is desired to work, we may want to simply - // skip playing, or cancel the currently playing sound and play our - // new one... for now, let's do the latter. - if (playing) { - ca_context_cancel(s_ccontext, cindex); - playing = 0; - } - - if (!playing) { - char dev[64]; - - snprintf(dev, sizeof(dev), "%lu", (unsigned long) iter->index); - ca_context_change_device(s_ccontext, dev); - - // Ideally we'd use something like ca_gtk_play_for_widget()... - ca_context_play( - s_ccontext, - cindex, - CA_PROP_EVENT_DESCRIPTION, i18n("Volume Control Feedback Sound").toUtf8().constData(), - CA_PROP_EVENT_ID, "audio-volume-change", - CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", - CA_PROP_CANBERRA_ENABLE, "1", - NULL - ); - - ca_context_change_device(s_ccontext, NULL); - } - } -#endif - - return 0; - } - } - } - else if (KMIXPA_CAPTURE == m_devnum) +static bool checkOpResult(pa_operation *op, const QString &explain) +{ + if (op==nullptr) { - for (iter = captureDevices.begin(); iter != captureDevices.end(); ++iter) - { - if (iter->name == id) - { - pa_operation *o; + qCWarning(KMIX_LOG) << "PulseAudio operation" << qPrintable(explain) << "failed"; + return (false); + } + pa_operation_unref(op); + return (true); +} - pa_cvolume volume = genVolumeForPulse(*iter, md->playbackVolume()); - if (!(o = pa_context_set_source_volume_by_index(s_context, iter->index, &volume, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_volume_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - if (!(o = pa_context_set_source_mute_by_index(s_context, iter->index, (md->isMuted() ? 1 : 0), NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_mute_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); +int Mixer_PULSE::readVolumeFromHW(const QString &id, shared_ptr md) +{ + const devmap *map = get_widget_map(m_devnum, id); + return (doForDevice(id, map, md, + [](const devinfo &dev, shared_ptr md) -> int + { + setVolumeFromPulse(md->playbackVolume(), dev); + md->setMuted(dev.mute); // to cover both playback + md->setRecSource(!dev.mute); // and capture channels + return (0); + })); +} - return 0; - } - } - } - else if (KMIXPA_APP_PLAYBACK == m_devnum) + +int Mixer_PULSE::writeVolumeToHW(const QString &id, shared_ptr md) +{ + switch (m_devnum) { +case KMIXPA_PLAYBACK: + return (doForDevice(id, &outputDevices, md, + [](const devinfo &dev, shared_ptr md) -> int + { + pa_cvolume volume = genVolumeForPulse(dev, md->playbackVolume()); + pa_operation *op = pa_context_set_sink_volume_by_index(s_context, dev.index, &volume, NULL, NULL); + if (!checkOpResult(op,"pa_context_set_sink_volume_by_index")) return (Mixer::ERR_WRITE); + + op = pa_context_set_sink_mute_by_index(s_context, dev.index, (md->isMuted() ? 1 : 0), NULL, NULL); + if (!checkOpResult(op, "pa_context_set_sink_mute_by_index")) return (Mixer::ERR_WRITE); +#ifdef HAVE_CANBERRA + if (s_ccontext!=nullptr && Mixer::getBeepOnVolumeChange()) + { + int playing = 0; + // Note that '2' is simply an index we've picked. + // It's mostly irrelevant. + int cindex = 2; + + ca_context_playing(s_ccontext, cindex, &playing); + + // Note: Depending on how this is desired to work, + // we may want to simply skip playing, or cancel the + // currently playing sound and play our + // new one... for now, let's do the latter. + if (playing) + { + ca_context_cancel(s_ccontext, cindex); + playing = 0; + } + + if (playing==0) + { + char devnum[64]; + snprintf(devnum, sizeof(devnum), "%lu", (unsigned long) dev.index); + ca_context_change_device(s_ccontext, devnum); + + // Ideally we'd use something like ca_gtk_play_for_widget()... + ca_context_play( + s_ccontext, + cindex, + CA_PROP_EVENT_DESCRIPTION, i18n("Volume Control Feedback Sound").toUtf8().constData(), + CA_PROP_EVENT_ID, "audio-volume-change", + CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", + CA_PROP_CANBERRA_ENABLE, "1", + NULL); + + ca_context_change_device(s_ccontext, NULL); + } + } +#endif // HAVE_CANBERRA + return (Mixer::OK); + })); + +case KMIXPA_CAPTURE: + return (doForDevice(id, &captureDevices, md, + [](const devinfo &dev, shared_ptr md) -> int + { + pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); + pa_operation *op = pa_context_set_source_volume_by_index(s_context, dev.index, &volume, NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_volume_by_index")) return (Mixer::ERR_WRITE); + + op = pa_context_set_source_mute_by_index(s_context, dev.index, (md->isRecSource() ? 0 : 1), NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_mute_by_index")) return (Mixer::ERR_WRITE); + + return (Mixer::OK); + })); + +case KMIXPA_APP_PLAYBACK: if (id.startsWith(QLatin1String("stream:"))) { - for (iter = outputStreams.begin(); iter != outputStreams.end(); ++iter) - { - if (iter->name == id) - { - pa_operation *o; - - pa_cvolume volume = genVolumeForPulse(*iter, md->playbackVolume()); - if (!(o = pa_context_set_sink_input_volume(s_context, iter->index, &volume, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_sink_input_volume() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - - if (!(o = pa_context_set_sink_input_mute(s_context, iter->index, (md->isMuted() ? 1 : 0), NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_sink_input_mute() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - - return 0; - } - } + return (doForDevice(id, &outputStreams, md, + [](const devinfo &dev, shared_ptr md) -> int + { + pa_cvolume volume = genVolumeForPulse(dev, md->playbackVolume()); + pa_operation *op = pa_context_set_sink_input_volume(s_context, dev.index, &volume, NULL, NULL); + if (!checkOpResult(op, "pa_context_set_sink_input_volume")) return (Mixer::ERR_WRITE); + + op = pa_context_set_sink_input_mute(s_context, dev.index, (md->isMuted() ? 1 : 0), NULL, NULL); + if (!checkOpResult(op, "pa_context_set_sink_input_mute")) return (Mixer::ERR_WRITE); + + return (Mixer::OK); + })); } else if (id.startsWith(QLatin1String("restore:"))) { - for (iter = outputRoles.begin(); iter != outputRoles.end(); ++iter) - { - if (iter->name == id) - { - restoreRule &rule = s_RestoreRules[iter->stream_restore_rule]; - pa_ext_stream_restore_info info; - info.name = iter->stream_restore_rule.toUtf8().constData(); - info.channel_map = rule.channel_map; - info.volume = genVolumeForPulse(*iter, md->playbackVolume()); - info.device = rule.device.isEmpty() ? NULL : rule.device.toUtf8().constData(); - info.mute = (md->isMuted() ? 1 : 0); - - pa_operation* o; - if (!(o = pa_ext_stream_restore_write(s_context, PA_UPDATE_REPLACE, &info, 1, true, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_ext_stream_restore_write() failed" << info.channel_map.channels << info.volume.channels << info.name; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - - return 0; - } - } + return (doForDevice(id, &outputRoles, md, + [](const devinfo &dev, shared_ptr md) -> int + { + restoreRule &rule = s_RestoreRules[dev.stream_restore_rule]; + pa_ext_stream_restore_info info; + info.name = dev.stream_restore_rule.toUtf8().constData(); + info.channel_map = rule.channel_map; + info.volume = genVolumeForPulse(dev, md->playbackVolume()); + info.device = rule.device.isEmpty() ? NULL : rule.device.toUtf8().constData(); + info.mute = (md->isMuted() ? 1 : 0); + + pa_operation *op = pa_ext_stream_restore_write(s_context, PA_UPDATE_REPLACE, &info, 1, true, NULL, NULL); + if (!checkOpResult(op, "pa_ext_stream_restore_write")) return (Mixer::ERR_WRITE); + + return (Mixer::OK); + })); } - } - else if (KMIXPA_APP_CAPTURE == m_devnum) - { - for (iter = captureStreams.begin(); iter != captureStreams.end(); ++iter) - { - if (iter->name == id) - { - pa_operation *o; + else return (Mixer::OK); +case KMIXPA_APP_CAPTURE: + return (doForDevice(id, &captureStreams, md, + [](const devinfo &dev, shared_ptr md) -> int + { #if HAVE_SOURCE_OUTPUT_VOLUMES - pa_cvolume volume = genVolumeForPulse(*iter, md->playbackVolume()); - if (!(o = pa_context_set_source_output_volume(s_context, iter->index, &volume, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_output_volume_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); + pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); + pa_operation *op = pa_context_set_source_output_volume(s_context, dev.index, &volume, NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_output_volume")) return (Mixer::ERR_WRITE); - if (!(o = pa_context_set_source_output_mute(s_context, iter->index, (md->isMuted() ? 1 : 0), NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_output_mute_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); + op = pa_context_set_source_output_mute(s_context, dev.index, (md->isRecSource() ? 0 : 1), NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_output_mute")) return (Mixer::ERR_WRITE); #else - // NB Note that this is different from APP_PLAYBACK in that we set the volume on the source itself. - pa_cvolume volume = genVolumeForPulse(*iter, md->playbackVolume()); - if (!(o = pa_context_set_source_volume_by_index(s_context, iter->device_index, &volume, NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_volume_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); - - if (!(o = pa_context_set_source_mute_by_index(s_context, iter->device_index, (md->isMuted() ? 1 : 0), NULL, NULL))) { - qCWarning(KMIX_LOG) << "pa_context_set_source_mute_by_index() failed"; - return Mixer::ERR_READ; - } - pa_operation_unref(o); + // Note that this is different from APP_PLAYBACK in that + // we set the volume on the source itself. + pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); + pa_operation *op = pa_context_set_source_volume_by_index(s_context, dev.device_index, &volume, NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_volume_by_index")) return (Mixer::ERR_WRITE); + + op = pa_context_set_source_mute_by_index(s_context, dev.device_index, (md->isRecSource() ? 0 : 1), NULL, NULL); + if (!checkOpResult(op, "pa_context_set_source_mute_by_index")) return (Mixer::ERR_WRITE); #endif + return (Mixer::OK); + })); - return 0; - } - } +default: + qCWarning(KMIX_LOG) << "Unknown device index" << m_devnum; } - return 0; + return (Mixer::OK); } + /** * Move the stream to a new destination */ bool Mixer_PULSE::moveStream( const QString& id, const QString& destId ) { Q_ASSERT(KMIXPA_APP_PLAYBACK == m_devnum || KMIXPA_APP_CAPTURE == m_devnum); qCDebug(KMIX_LOG) << "Mixer_PULSE::moveStream(): Move Stream Requested - Stream: " << id << ", Destination: " << destId; // Lookup the stream index. uint32_t stream_index = PA_INVALID_INDEX; QString stream_restore_rule = ""; devmap::iterator iter; devmap *map = get_widget_map(m_devnum); for (iter = map->begin(); iter != map->end(); ++iter) { if (iter->name == id) { stream_index = iter->index; stream_restore_rule = iter->stream_restore_rule; break; } } if (PA_INVALID_INDEX == stream_index) { qCCritical(KMIX_LOG) << "Mixer_PULSE::moveStream(): Cannot find stream index"; return false; } if (destId.isEmpty()) { // We want to remove any specific device in the stream restore rule. if (stream_restore_rule.isEmpty() || !s_RestoreRules.contains(stream_restore_rule)) { qCWarning(KMIX_LOG) << "Mixer_PULSE::moveStream(): Trying to set Automatic on a stream with no rule"; } else { restoreRule &rule = s_RestoreRules[stream_restore_rule]; pa_ext_stream_restore_info info; info.name = stream_restore_rule.toUtf8().constData(); info.channel_map = rule.channel_map; info.volume = rule.volume; info.device = NULL; info.mute = rule.mute ? 1 : 0; pa_operation* o; if (!(o = pa_ext_stream_restore_write(s_context, PA_UPDATE_REPLACE, &info, 1, true, NULL, NULL))) { qCWarning(KMIX_LOG) << "pa_ext_stream_restore_write() failed" << info.channel_map.channels << info.volume.channels << info.name; return false; } pa_operation_unref(o); } } else { pa_operation* o; if (KMIXPA_APP_PLAYBACK == m_devnum) { if (!(o = pa_context_move_sink_input_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL))) { qCWarning(KMIX_LOG) << "pa_context_move_sink_input_by_name() failed"; return false; } } else { if (!(o = pa_context_move_source_output_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL))) { qCWarning(KMIX_LOG) << "pa_context_move_source_output_by_name() failed"; return false; } } pa_operation_unref(o); } return true; } void Mixer_PULSE::reinit() { // We only support reinit on our primary mixer. Q_ASSERT(KMIXPA_PLAYBACK == m_devnum); connectToDaemon(); } void Mixer_PULSE::triggerUpdate() { readSetFromHWforceUpdate(); readSetFromHW(); } // Please see KMixWindow::initActionsAfterInitMixer(), it uses the driverName QString PULSE_getDriverName() { return "PulseAudio"; } QString Mixer_PULSE::getDriverName() { return "PulseAudio"; }