diff --git a/backends/mixer_pulse.cpp b/backends/mixer_pulse.cpp index 86e6374c..8b834502 100644 --- a/backends/mixer_pulse.cpp +++ b/backends/mixer_pulse.cpp @@ -1,1431 +1,1429 @@ /* * 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 "qtpamainloop.h" #include #include #include #include #include #include "core/mixer.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #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_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.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.chanMask |= Volume::MLEFT; dev.chanIDs[i] = Volume::LEFT; break; case PA_CHANNEL_POSITION_FRONT_RIGHT: - dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MRIGHT); + 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.chanMask |= Volume::MCENTER; dev.chanIDs[i] = Volume::CENTER; break; case PA_CHANNEL_POSITION_REAR_CENTER: - dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARCENTER); + 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.chanMask |= Volume::MSURROUNDLEFT; dev.chanIDs[i] = Volume::SURROUNDLEFT; break; case PA_CHANNEL_POSITION_REAR_RIGHT: - dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MSURROUNDRIGHT); + dev.chanMask |= Volume::MSURROUNDRIGHT; dev.chanIDs[i] = Volume::SURROUNDRIGHT; break; case PA_CHANNEL_POSITION_LFE: - dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MWOOFER); + 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.chanMask |= Volume::MREARSIDELEFT; dev.chanIDs[i] = Volume::REARSIDELEFT; break; case PA_CHANNEL_POSITION_SIDE_RIGHT: - dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARSIDERIGHT); + 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; } /** * Helper for performing an operation on the device specified by @p id. * Search for the device by name in the specified @p devices map, perform * the function @c func on it, and return the result of that. If the * device is not found then do nothing. */ 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) return ((*func)(*iter, md)); } qCDebug(KMIX_LOG) << "Device" << id << "not in map"; return (Mixer::OK); } /** * Helper for checking the result of a PulseAudio operation. * If the operation @p op failed, log a message identifying the * call @p explain, then return @c false. Return @c true if the * operation succeeeded, after dereferencing the operation. */ static bool checkOpResult(pa_operation *op, const char *explain) { if (op==nullptr) { qCWarning(KMIX_LOG) << "PulseAudio operation" << explain << "failed," << pa_strerror(pa_context_errno(s_context)); return (false); } pa_operation_unref(op); return (true); } 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 user this won't 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 '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 *op = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL); checkOpResult(op, "pa_ext_stream_restore_read"); } 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 *op = pa_context_get_sink_info_by_index(c, index, sink_cb, NULL); checkOpResult(op, "pa_context_get_sink_info_by_index"); } 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 *op = pa_context_get_source_info_by_index(c, index, source_cb, NULL); checkOpResult(op, "pa_context_get_source_info_by_index"); } 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 *op = pa_context_get_sink_input_info(c, index, sink_input_cb, NULL); checkOpResult(op, "pa_context_get_sink_input_info"); } 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 *op = pa_context_get_source_output_info(c, index, source_output_cb, NULL); checkOpResult(op, "pa_context_get_sink_input_info"); } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { clients.remove(index); } else { pa_operation *op = pa_context_get_client_info(c, index, client_cb, NULL); checkOpResult(op, "pa_context_get_client_info"); } 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 *op; // 1. Register for the stream changes (except during probe) if (s_context == c) { pa_context_set_subscribe_callback(c, subscribe_cb, NULL); - op = pa_context_subscribe(c, (pa_subscription_mask_t) - (PA_SUBSCRIPTION_MASK_SINK| + op = pa_context_subscribe(c, static_cast( + PA_SUBSCRIPTION_MASK_SINK| PA_SUBSCRIPTION_MASK_SOURCE| PA_SUBSCRIPTION_MASK_CLIENT| PA_SUBSCRIPTION_MASK_SINK_INPUT| PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL); if (!checkOpResult(op, "pa_context_subscribe")) return; } op = pa_context_get_sink_info_list(c, sink_cb, NULL); if (!checkOpResult(op, "pa_context_get_sink_info_list")) return; s_outstandingRequests++; op = pa_context_get_source_info_list(c, source_cb, NULL); if (!checkOpResult(op, "pa_context_get_source_info_list")) return; s_outstandingRequests++; op = pa_context_get_client_info_list(c, client_cb, NULL); if (!checkOpResult(op, "pa_context_client_info_list")) return; s_outstandingRequests++; op = pa_context_get_sink_input_info_list(c, sink_input_cb, NULL); if (!checkOpResult(op, "pa_context_get_sink_input_info_list")) return; s_outstandingRequests++; op = pa_context_get_source_output_info_list(c, source_output_cb, NULL); if (!checkOpResult(op, "pa_context_get_source_output_info_list")) return; s_outstandingRequests++; /* These calls are not always supported */ op = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL); if (checkOpResult(op, "pa_ext_stream_restore_read")) { s_outstandingRequests++; pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, NULL); pa_ext_stream_restore_subscribe(c, 1, NULL, NULL); } } 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 { 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()]); + volume.setVolume(iter.value(), static_cast(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()); + cvol.values[iter.key()] = static_cast(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); + if (static_cast(index)==PA_INVALID_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); MixSet *ms = nullptr; 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)) { ms = s_mixers[KMIXPA_CAPTURE]->getMixSet(); } const bool isCapture = (m_devnum==KMIXPA_APP_CAPTURE || m_devnum==KMIXPA_CAPTURE); 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" << 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); } 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"; // No need to create this until necessary if (!m_mainloop) { // When bumping c++ requirement to c++14, // replace with `= std::make_unique();` m_mainloop.reset(new QtPaMainLoop); } s_context = pa_context_new(&m_mainloop->pa_vtable, "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; ++refcount; if (INACTIVE != s_pulseActive && 1 == refcount) { // First of all connect 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 = pa_mainloop_new(); if (p_test_mainloop==nullptr) { qCDebug(KMIX_LOG) << "PulseAudio support disabled, unable to create mainloop"; s_pulseActive = INACTIVE; goto endconstruct; } pa_context *p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "kmix-probe"); if (p_test_context==nullptr) { 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 connectToDaemon(); #if defined(HAVE_CANBERRA) int ret = ca_context_create(&s_ccontext); if (ret < 0) { qCDebug(KMIX_LOG) << "Disabling sound feedback, Canberra context create 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; } } } 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) { 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); })); } 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); + const QByteArray devnum = QByteArray::number(dev.index); + ca_context_change_device(s_ccontext, devnum.constData()); // 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:"))) { 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:"))) { 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 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(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); 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 // 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); })); default: qCWarning(KMIX_LOG) << "Unknown device index" << m_devnum; } return (Mixer::OK); } static const devinfo *getStreamInfo(int devnum, const QString &id) { //qCDebug(KMIX_LOG) << "dev" << devnum << "id" << id; const devmap *map = get_widget_map(devnum); for (devmap::const_iterator iter = map->constBegin(); iter!=map->constEnd(); ++iter) { // I think this will work! '*iter' dereferences the iterator and obtains // a reference to the actual map item. '&' then takes the address of that // and returns a pointer. if (iter->name==id) return (&*iter); } qCWarning(KMIX_LOG) << "Cannot find stream index for" << id; return (nullptr); } /** * Move the stream to a new destination. */ bool Mixer_PULSE::moveStream(const QString &id, const QString &destId) { Q_ASSERT(m_devnum==KMIXPA_APP_PLAYBACK || m_devnum==KMIXPA_APP_CAPTURE); qCDebug(KMIX_LOG) << "dev" << m_devnum << "move" << id << "->" << destId; const devinfo *info = getStreamInfo(m_devnum, id); if (info==nullptr) return (false); if (destId.isEmpty()) { // Reset the stream to its automatic destination, using the previously saved // restore rule. const QString stream_restore_rule = info->stream_restore_rule; if (stream_restore_rule.isEmpty() || !s_RestoreRules.contains(stream_restore_rule)) { qCWarning(KMIX_LOG) << "Stream has no restore rule"; } else { // We want to remove any specific device in the stream restore rule. const 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 *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 (false); } } else { // Move the stream to the specified destination. const uint32_t stream_index = info->index; if (m_devnum==KMIXPA_APP_PLAYBACK) { pa_operation *op = pa_context_move_sink_input_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL); if (!checkOpResult(op, "pa_context_move_sink_input_by_name")) return (false); } else { pa_operation *op = pa_context_move_source_output_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL); if (!checkOpResult(op, "pa_context_move_source_output_by_name")) return (false); } } return (true); } QString Mixer_PULSE::currentStreamDevice(const QString &id) const { Q_ASSERT(m_devnum==KMIXPA_APP_PLAYBACK || m_devnum==KMIXPA_APP_CAPTURE); //qCDebug(KMIX_LOG) << "dev" << m_devnum << "id" << id; const devinfo *info = getStreamInfo(m_devnum, id); if (info==nullptr) return (QString()); const int dev = info->device_index; // Look in 'outputDevices' for KMIXPA_APP_PLAYBACK, or 'captureDevices' for KMIXPA_APP_CAPTURE const devmap *deviceMap = get_widget_map(m_devnum==KMIXPA_APP_PLAYBACK ? KMIXPA_PLAYBACK : KMIXPA_CAPTURE); if (deviceMap->contains(dev)) { const devinfo &s = deviceMap->value(dev); //qDebug() << " dev" << s.device_index << "desc" << s.description << "->" << s.name; return (s.name); } //else qDebug() << " no device info"; return (QString()); } 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"; } diff --git a/core/volume.cpp b/core/volume.cpp index 469c1069..5815df2e 100644 --- a/core/volume.cpp +++ b/core/volume.cpp @@ -1,381 +1,381 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 1996-2004 Christian Esken * * 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 "core/volume.h" #include "kmix_debug.h" // for operator<<() #include #include float Volume::VOLUME_STEP_DIVISOR = 20; float Volume::VOLUME_PAGESTEP_DIVISOR = 10; static Volume::ChannelMask channelMask[] = { Volume::MLEFT, Volume::MRIGHT, Volume::MCENTER, Volume::MWOOFER, Volume::MSURROUNDLEFT, Volume::MSURROUNDRIGHT, Volume::MREARSIDELEFT, Volume::MREARSIDERIGHT, Volume::MREARCENTER }; -QString Volume::channelNameReadable(ChannelID id) +QString Volume::channelNameReadable(Volume::ChannelID id) { switch (id) { case LEFT: return (i18nc("Channel name", "Left")); case RIGHT: return (i18nc("Channel name", "Right")); case CENTER: return (i18nc("Channel name", "Center")); case WOOFER: return (i18nc("Channel name", "Subwoofer")); case SURROUNDLEFT: return (i18nc("Channel name", "Surround Left")); case SURROUNDRIGHT: return (i18nc("Channel name", "Surround Right")); case REARSIDELEFT: return (i18nc("Channel name", "Side Left")); case REARSIDERIGHT: return (i18nc("Channel name", "Side Right")); case REARCENTER: return (i18nc("Channel name", "Rear Center")); default: qCWarning(KMIX_LOG) << "called for unknown ID" << id; return (i18nc("Channel name", "Unknown")); } } -QString Volume::channelNameForPersistence(ChannelID id) +QString Volume::channelNameForPersistence(Volume::ChannelID id) { switch (id) { case LEFT: return ("volumeL"); case RIGHT: return ("volumeR"); case CENTER: return ("volumeCenter"); case WOOFER: return ("volumeWoofer"); case SURROUNDLEFT: return ("volumeSurroundL"); case SURROUNDRIGHT: return ("volumeSurroundR"); case REARSIDELEFT: return ("volumeSideL"); case REARSIDERIGHT: return ("volumeSideR"); case REARCENTER: return ("volumeRearCenter"); default: qCWarning(KMIX_LOG) << "called for unknown ID" << id; return ("unknown"); } } // Forbidden/private. Only here because if there is no CaptureVolume we need the values initialized // And also QMap requires it. Volume::Volume() : _chmask(MNONE) , _minVolume(0) , _maxVolume(0) , _hasSwitch(false) , _switchActivated(false) , _switchType(None) , _isCapture(false) { } /** * Do not use. Only implicitely required for QMap. * * @deprecated Do not use */ VolumeChannel::VolumeChannel() : volume(0) , chid(Volume::NOCHANNEL) { } VolumeChannel::VolumeChannel(Volume::ChannelID c) : volume(0) , chid(c) { } Volume::Volume(long maxVolume, long minVolume, bool hasSwitch, bool isCapture ) { - init(static_cast(0), maxVolume, minVolume, hasSwitch, isCapture ); + init(Volume::MNONE, maxVolume, minVolume, hasSwitch, isCapture ); } /** * @deprecated */ -void Volume::addVolumeChannels(ChannelMask chmask) +void Volume::addVolumeChannels(Volume::ChannelMask chmask) { for ( Volume::ChannelID chid=Volume::CHIDMIN; chid<= Volume::CHIDMAX; ) { if ( chmask & channelMask[chid] ) { addVolumeChannel(VolumeChannel(chid)); } chid = static_cast(1+static_cast(chid)); } // for all channels } void Volume::addVolumeChannel(VolumeChannel vc) { _volumesL.insert(vc.chid, vc); // Add the corresponding "muted version" of the channel. // VolumeChannel* zeroChannel = new VolumeChannel(vc.chid); // zeroChannel->volume = 0; // _volumesMuted.insert(zeroChannel->chid, *zeroChannel); // TODO remove _volumesMuted } -void Volume::init( ChannelMask chmask, long maxVolume, long minVolume, bool hasSwitch, bool isCapture) +void Volume::init( Volume::ChannelMask chmask, long maxVolume, long minVolume, bool hasSwitch, bool isCapture) { _chmask = chmask; _maxVolume = maxVolume; _minVolume = minVolume; _hasSwitch = hasSwitch; _isCapture = isCapture; //_muted = false; // Presume that the switch is active. This will always work: // a) Physical switches will be updated after start from the hardware. // b) Emulated virtual/switches will not receive updates from the hardware, so they shouldn't disable the channels. _switchActivated = true; } QMap Volume::getVolumesWhenActive() const { return _volumesL; } QMap Volume::getVolumes() const { return _volumesL; } /** * Returns the absolute change to do one "step" for this volume. This is similar to a page step in a slider, * namely a fixed percentage of the range. * One step is the percentage given by 100/VOLUME_STEP_DIVISOR. The * default VOLUME_STEP_DIVISOR is 20, so default change is 5% of the volumeSpan(). * * This method guarantees a minimum absolute change of 1, zero is never returned. * * It is NOT verified, that such a volume change would actually be possible. You might hit the upper or lower bounds * of the volume range. * * * @param decrease true, if you want a volume step that decreases the volume by one page step * @return The volume step. It will be negative if you have used decrease==true * */ long Volume::volumeStep(bool decrease) const { long inc = volumeSpan() / Volume::VOLUME_STEP_DIVISOR; if ( inc == 0 ) inc = 1; if ( decrease ) inc *= -1; return inc; } // @ compatibility void Volume::setAllVolumes(long vol) { long int finalVol = volrange(vol); QMap::iterator it = _volumesL.begin(); while (it != _volumesL.end()) { it.value().volume = finalVol; //it.value().unmutedVolume= finalVol; ++it; } } void Volume::changeAllVolumes( long step ) { QMap::iterator it = _volumesL.begin(); while (it != _volumesL.end()) { long int finalVol = volrange(it.value().volume + step); it.value().volume = finalVol; // it.value().unmutedVolume= finalVol; ++it; } } /** * Sets the volume for the given Channel * @ compatibility */ -void Volume::setVolume( ChannelID chid, long vol) +void Volume::setVolume( Volume::ChannelID chid, long vol) { QMap::iterator it = _volumesL.find(chid); if ( it != _volumesL.end()) { it.value().volume = vol; //it.value().unmutedVolume = vol; } } /** * Copy the volume elements contained in v to this Volume object. */ // void Volume::setVolume(const Volume &v) // { // foreach (VolumeChannel vc, _volumesL ) // { -// ChannelID chid = vc.chid; +// Volume::ChannelID chid = vc.chid; // v.getVolumes()[chid].volume = vc.volume; // //v.getVolumes()[chid].unmutedVolume = vc.volume; // } // } void Volume::setSwitch( bool active ) { _switchActivated = active; // if ( isCapture() ) // return; // // for playback volumes we will not only do the switch, but also set the volume to 0 // QMap::iterator it = _volumesL.begin(); // if ( active ) // { // while (it != _volumesL.end()) // { // VolumeChannel& vc = it.value(); // vc.volume = vc.unmutedVolume; // ++it; // } // } // else // { // while (it != _volumesL.end()) // { // VolumeChannel& vc = it.value(); // vc.unmutedVolume = vc.volume; // vc.volume = 0; // ++it; // } // } } /** * Returns the volume of the given channel. */ -long Volume::getVolume(ChannelID chid) const +long Volume::getVolume(Volume::ChannelID chid) const { return _volumesL.value(chid).volume; } /** * Returns the volume of the given channel. If this Volume is inactive (switched off), 0 is returned. */ -long Volume::getVolumeForGUI(ChannelID chid) const +long Volume::getVolumeForGUI(Volume::ChannelID chid) const { if (! isSwitchActivated() ) return 0; return _volumesL.value(chid).volume; } -qreal Volume::getAvgVolume(ChannelMask chmask) const +qreal Volume::getAvgVolume(Volume::ChannelMask chmask) const { int avgVolumeCounter = 0; long long sumOfActiveVolumes = 0; foreach (VolumeChannel vc, _volumesL ) { if (channelMask[vc.chid] & chmask ) { sumOfActiveVolumes += vc.volume; ++avgVolumeCounter; } } if (avgVolumeCounter != 0) { qreal sumOfActiveVolumesQreal = sumOfActiveVolumes; sumOfActiveVolumesQreal /= avgVolumeCounter; return sumOfActiveVolumesQreal; } else return 0; } -int Volume::getAvgVolumePercent(ChannelMask chmask) const +int Volume::getAvgVolumePercent(Volume::ChannelMask chmask) const { qreal volume = getAvgVolume(chmask); // min=-100, max=200 => volSpan = 301 // volume = -50 => volShiftedToZero = -50+min = 50 qreal volSpan = volumeSpan(); qreal volShiftedToZero = volume - _minVolume; qreal percentReal = ( volSpan == 0 ) ? 0 : ( 100 * volShiftedToZero ) / ( volSpan - 1); int percent = qRound(percentReal); //qCDebug(KMIX_LOG) << "volSpan=" << volSpan << ", volume=" << volume << ", volShiftedToPositive=" << volShiftedToZero << ", percent=" << percent; return percent; } /** * returns a "sane" volume level. This means, it is a volume level inside the * valid bounds */ long Volume::volrange( long vol ) { if ( vol < _minVolume ) { return _minVolume; } else if ( vol < _maxVolume ) { return vol; } else { return _maxVolume; } } std::ostream& operator<<(std::ostream& os, const Volume& vol) { os << "("; bool first = true; foreach ( const VolumeChannel vc, vol.getVolumes() ) { if ( !first ) os << ","; else first = false; os << vc.volume; } // all channels os << ")"; os << " [" << vol._minVolume << "-" << vol._maxVolume; if ( vol._switchActivated ) { os << " : switch active ]"; } else { os << " : switch inactive ]"; } return os; } QDebug operator<<(QDebug os, const Volume& vol) { os << "("; bool first = true; foreach ( VolumeChannel vc, vol.getVolumes() ) { if ( !first ) os << ","; else first = false; os << vc.volume; } // all channels os << ")"; os << " [" << vol._minVolume << "-" << vol._maxVolume; if ( vol._switchActivated ) { os << " : switch active ]"; } else { os << " : switch inactive ]"; } return os; } diff --git a/core/volume.h b/core/volume.h index 133c65e8..c20f1e75 100644 --- a/core/volume.h +++ b/core/volume.h @@ -1,200 +1,219 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * Copyright Christian Esken * * 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. */ #ifndef RADOMPREFIX_VOLUME_H #define RADOMPREFIX_VOLUME_H #include #include #include #include #include "kmixcore_export.h" class VolumeChannel; class KMIXCORE_EXPORT Volume { friend class MixDevice; public: // Channel definition: // For example a 2.0 system just has MLEFT and MRIGHT. // A 5.1 system adds MCENTER, MWOOFER, MSURROUNDLEFT and MSURROUNDRIGHT. // A 7.1 system furthermore adds MREARLEFT and MREARRIGHT. - enum ChannelMask { MNONE = 0, + enum ChannelMaskBits + { + MNONE = 0x000, - MLEFT = 1, MRIGHT = 2, MCENTER = 4, - MMAIN = 3, MFRONT = 7, + MLEFT = 0x001, + MRIGHT = 0x002, + MCENTER = 0x004, + MMAIN = 0x003, + MFRONT = 0x007, - MWOOFER = 8, + MWOOFER = 0x008, - // SURROUND (4.0 or 4.1 or in higher - like 5.1) - MSURROUNDLEFT = 0x10, MSURROUNDRIGHT = 0x20, - // MSURROUND - MSURROUND = 0x30, + // SURROUND (4.0 or 4.1 or in higher - like 5.1) + MSURROUNDLEFT = 0x010, + MSURROUNDRIGHT = 0x020, + // all SURROUND + MSURROUND = 0x030, - // REARSIDE (Usually only in 7.1) - MREARSIDELEFT = 0x40, MREARSIDERIGHT = 0x80, - // REARCENTER (Usually only in 6.1) - MREARCENTER = 0x100, - // MREAR - MREAR = 0x1C0, + // REARSIDE (Usually only in 7.1) + MREARSIDELEFT = 0x040, + MREARSIDERIGHT = 0x080, + // REARCENTER (Usually only in 6.1) + MREARCENTER = 0x100, + // all REAR + MREAR = 0x1C0, - MALL=0xFFFF }; + MALL = 0xFFF + }; + Q_DECLARE_FLAGS(ChannelMask, ChannelMaskBits) + enum ChannelID + { + NOCHANNEL = -1, + CHIDMIN = 0, - enum ChannelID { NOCHANNEL =-1, CHIDMIN = 0, - LEFT = 0, RIGHT = 1, CENTER = 2, + LEFT = 0, + RIGHT = 1, + CENTER = 2, - WOOFER = 3, + WOOFER = 3, - SURROUNDLEFT = 4, SURROUNDRIGHT = 5, + SURROUNDLEFT = 4, + SURROUNDRIGHT = 5, - REARSIDELEFT = 6, REARSIDERIGHT = 7, + REARSIDELEFT = 6, + REARSIDERIGHT = 7, - REARCENTER = 8, + REARCENTER = 8, - CHIDMAX = 8 }; + CHIDMAX = 8 + }; - static QString channelNameForPersistence(ChannelID id); - static QString channelNameReadable(ChannelID id); + static QString channelNameForPersistence(Volume::ChannelID id); + static QString channelNameReadable(Volume::ChannelID id); enum VolumeType { PlaybackVT = 0 , CaptureVT = 1 }; enum VolumeTypeFlag { Playback = 1, Capture = 2, Both = 3 }; // regular constructor (old, deprecated) //Volume( ChannelMask chmask, long maxVolume, long minVolume, bool hasSwitch, bool isCapture ); // regular constructor Volume(long maxVolume, long minVolume, bool hasSwitch, bool isCapture ); void addVolumeChannel(VolumeChannel ch); /// @Deprecated - void addVolumeChannels(ChannelMask chmask); + void addVolumeChannels(Volume::ChannelMask chmask); // Set all volumes as given by vol void setAllVolumes(long vol); // Set all volumes to the ones given in vol //void setVolume(const Volume &vol ); // Set volumes as specified by the channel mask - //void setVolume( const Volume &vol, ChannelMask chmask); - void setVolume( ChannelID chid, long volume); + //void setVolume( const Volume &vol, Volume::ChannelMask chmask); + void setVolume( Volume::ChannelID chid, long volume); // Increase or decrease all volumes by step void changeAllVolumes( long step ); - long getVolume(ChannelID chid) const; - long getVolumeForGUI(ChannelID chid) const; - qreal getAvgVolume(ChannelMask chmask) const; - int getAvgVolumePercent(ChannelMask chmask) const; + long getVolume(Volume::ChannelID chid) const; + long getVolumeForGUI(Volume::ChannelID chid) const; + qreal getAvgVolume(Volume::ChannelMask chmask) const; + int getAvgVolumePercent(Volume::ChannelMask chmask) const; //long operator[](int); long maxVolume() const { return (_maxVolume); } long minVolume() const { return (_minVolume); } /** * The number of valid volume levels */ long volumeSpan() const { return (_maxVolume-_minVolume+1); } int count() const { return (getVolumes().count()); } bool hasSwitch() const { return (_hasSwitch); } bool hasVolume() const { return (_maxVolume!=_minVolume); } /** * Returns whether this is a playback or capture volume. * * @return true, if it is a capture volume */ bool isCapture() const { return (_isCapture); } // Some playback switches control playback, and some are special. // ALSA doesn't differentiate between playback, OnOff and special, so users can add this information in the profile. // It is only used for GUI things, like showing a "Mute" text or tooltip // Capture is not really used, and has only been added for completeness and future extensibility. enum SwitchType { None, PlaybackSwitch, CaptureSwitch, OnSwitch, OffSwitch, SpecialSwitch }; void setSwitchType(SwitchType type) { _switchType = type; } Volume::SwitchType switchType() const { return (_switchType); } friend std::ostream& operator<<(std::ostream& os, const Volume& vol); friend QDebug operator<<(QDebug os, const Volume& vol); // _channelMaskEnum[] and the following elements moved to public section. operator<<() could not // access it, when private. Strange, as operator<<() is declared friend. QMap getVolumes() const; QMap getVolumesWhenActive() const; long volumeStep(bool decrease) const; static float VOLUME_STEP_DIVISOR; // The divisor for defining volume control steps (for mouse-wheel, DBUS and Normal step for Sliders ) static float VOLUME_PAGESTEP_DIVISOR; // The divisor for defining volume control steps (page-step for sliders) protected: long _chmask; QMap _volumesL; // QMap _volumesMuted; long _minVolume; long _maxVolume; // setSwitch() and isSwitchActivated() are tricky. No regular class (including the Backends) shall use // these functions. Our friend class MixDevice will handle that gracefully for us. void setSwitch( bool active ); bool isSwitchActivated() const // TODO rename to isActive() { return _switchActivated; }; private: // constructor for dummy volumes Volume(); - void init( ChannelMask chmask, long maxVolume, long minVolume, bool hasSwitch, bool isCapture); + void init( Volume::ChannelMask chmask, long maxVolume, long minVolume, bool hasSwitch, bool isCapture); long volrange( long vol ); bool _hasSwitch; bool _switchActivated; SwitchType _switchType; bool _isCapture; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(Volume::ChannelMask) + class KMIXCORE_EXPORT VolumeChannel { public: VolumeChannel(); /** * Construct a channel for the given channel id. * * @param chid */ VolumeChannel(Volume::ChannelID chid); long volume; Volume::ChannelID chid; }; std::ostream& operator<<(std::ostream& os, const Volume& vol); QDebug operator<<(QDebug os, const Volume& vol); #endif // RADOMPREFIX_VOLUME