"JackHWMute" is a UCM device value that, if set, indicates that the jack of the device mutes some other device at the hardware or driver level. A common example would be a headphone jack that mutes built-in speakers. PulseAudio should show such auto-muted devices as unavailable. Previously there was only a simple relationship: each UCM device was related to one jack and vice versa. Now each device is still related to exactly one jack, but each jack can be related to two devices: one that the jack enables, and one that the jack disables (mutes). The patch adds pa_alsa_jack_set_plugged_in() to encapsulate the logic of marking the appropriate ports available/unavailable. pa_alsa_jack_set_plugged_in() propagates the status to UCM devices, and the devices propagate the status further to ports. Previously the logic of mapping jack status to port availability was done by module-alsa-card.c, which I think is not the right place, so I didn't want to add the UCM "mute jack" logic there. --- src/modules/alsa/alsa-mixer.c | 15 ++ src/modules/alsa/alsa-mixer.h | 14 ++ src/modules/alsa/alsa-ucm.c | 286 +++++++++++++++++++++++++++++++++++- src/modules/alsa/alsa-ucm.h | 37 +++++ src/modules/alsa/module-alsa-card.c | 18 ++- src/pulsecore/device-port.c | 14 +- 6 files changed, 362 insertions(+), 22 deletions(-) diff --git a/src/modules/alsa/alsa-mixer.c b/src/modules/alsa/alsa-mixer.c index 2fe2ae4..423832f 100644 --- a/src/modules/alsa/alsa-mixer.c +++ b/src/modules/alsa/alsa-mixer.c @@ -2768,6 +2768,21 @@ int pa_alsa_path_probe(pa_alsa_path *p, snd_mixer_t *m, bool ignore_dB) { return 0; } +void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in) { + pa_assert(jack); + + if (plugged_in == jack->plugged_in) + return; + + jack->plugged_in = plugged_in; + + if (jack->device) + pa_alsa_ucm_device_set_available(jack->device, plugged_in); + + if (jack->hw_mute) + pa_alsa_ucm_device_set_available(jack->hw_mute, !plugged_in); +} + void pa_alsa_setting_dump(pa_alsa_setting *s) { pa_assert(s); diff --git a/src/modules/alsa/alsa-mixer.h b/src/modules/alsa/alsa-mixer.h index ec39fab..dad1ea2 100644 --- a/src/modules/alsa/alsa-mixer.h +++ b/src/modules/alsa/alsa-mixer.h @@ -168,8 +168,22 @@ struct pa_alsa_jack { pa_alsa_required_t required; pa_alsa_required_t required_any; pa_alsa_required_t required_absent; + + /* UCM specific: this is the "owner" device of this jack. When the jack is + * plugged in, the device referenced here gets marked as available, and + * when the jack is unplugged, the device gets marked as unavailable. */ + pa_alsa_ucm_device *device; + + /* UCM specific: if this is non-NULL, then the referenced device gets muted + * by the hardware (or driver) when this jack is plugged in. The muting can't + * be overridden by PulseAudio, so the device gets marked as + * unavailable (so if a device is referenced by this field, the effect is + * inverse to being referenced by the "device" field). */ + pa_alsa_ucm_device *hw_mute; }; +void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in); + /* A path wraps a series of elements into a single entity which can be * used to control it as if it had a single volume slider, a single * mute switch and a single list of selectable options. */ diff --git a/src/modules/alsa/alsa-ucm.c b/src/modules/alsa/alsa-ucm.c index ef6adcd..061b8f7 100644 --- a/src/modules/alsa/alsa-ucm.c +++ b/src/modules/alsa/alsa-ucm.c @@ -66,6 +66,17 @@ #ifdef HAVE_ALSA_UCM +static void device_set_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack); + +struct port { + pa_alsa_ucm_config *ucm; + pa_device_port *core_port; + + /* Usually a port is associated with only one device, but ports can also be + * a combination of several devices. */ + pa_dynarray *devices; /* pa_alsa_ucm_device */ +}; + struct ucm_items { const char *id; const char *property; @@ -91,6 +102,7 @@ static struct ucm_items item[] = { {"CaptureChannels", PA_ALSA_PROP_UCM_CAPTURE_CHANNELS}, {"TQ", PA_ALSA_PROP_UCM_QOS}, {"JackControl", PA_ALSA_PROP_UCM_JACK_CONTROL}, + {"JackHWMute", PA_ALSA_PROP_UCM_JACK_HW_MUTE}, {NULL, NULL}, }; @@ -394,9 +406,12 @@ static int ucm_get_devices(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) { for (i = 0; i < num_dev; i += 2) { pa_alsa_ucm_device *d = pa_xnew0(pa_alsa_ucm_device, 1); + d->verb = verb; d->proplist = pa_proplist_new(); pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(dev_list[i])); pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(dev_list[i + 1])); + d->ports = pa_dynarray_new(NULL); + d->available = true; PA_LLIST_PREPEND(pa_alsa_ucm_device, verb->devices, d); } @@ -553,6 +568,8 @@ int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) { const char **verb_list; int num_verbs, i, err = 0; + ucm->ports = pa_dynarray_new(NULL); + /* is UCM available for this card ? */ err = snd_card_get_name(card_index, &card_name); if (err < 0) { @@ -670,6 +687,67 @@ static int pa_alsa_ucm_device_cmp(const void *a, const void *b) { return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME)); } +static void device_add_port(pa_alsa_ucm_device *device, struct port *port) { + pa_assert(device); + pa_assert(port); + + pa_dynarray_append(device->ports, port); +} + +static void device_remove_port(pa_alsa_ucm_device *device, struct port *port) { + pa_assert(device); + pa_assert(port); + + pa_dynarray_remove_by_data(device->ports, port); +} + +static struct port *port_new(pa_alsa_ucm_config *ucm, pa_device_port *core_port, pa_alsa_ucm_device **devices, + unsigned n_devices) { + struct port *port; + unsigned i; + pa_available_t available = PA_AVAILABLE_YES; + + pa_assert(ucm); + pa_assert(core_port); + pa_assert(devices); + + port = pa_xnew0(struct port, 1); + port->ucm = ucm; + port->core_port = core_port; + port->devices = pa_dynarray_new(NULL); + + for (i = 0; i < n_devices; i++) { + device_add_port(devices[i], port); + pa_dynarray_append(port->devices, devices[i]); + + if (!devices[i]->available) + available = PA_AVAILABLE_NO; + } + + pa_device_port_set_available(core_port, available); + + pa_dynarray_append(ucm->ports, port); + + return port; +} + +static void port_free(struct port *port) { + pa_assert(port); + + pa_dynarray_remove_by_data(port->ucm->ports, port); + + if (port->devices) { + pa_alsa_ucm_device *device; + + while ((device = pa_dynarray_steal_last(port->devices))) + device_remove_port(device, port); + + pa_dynarray_free(port->devices); + } + + pa_xfree(port); +} + static void ucm_add_port_combination( pa_hashmap *hash, pa_alsa_ucm_mapping_context *context, @@ -688,6 +766,9 @@ static void ucm_add_port_combination( const char *dev_name; const char *direction; pa_alsa_ucm_device *sorted[num], *dev; + pa_alsa_ucm_config *ucm; + + ucm = context->ucm; for (i = 0; i < num; i++) sorted[i] = pdevices[i]; @@ -735,16 +816,21 @@ static void ucm_add_port_combination( port = pa_hashmap_get(ports, name); if (!port) { + struct port *ucm_port; + pa_device_port_new_data port_data; pa_device_port_new_data_init(&port_data); pa_device_port_new_data_set_name(&port_data, name); pa_device_port_new_data_set_description(&port_data, desc); pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT); + pa_device_port_new_data_set_available(&port_data, PA_AVAILABLE_YES); - port = pa_device_port_new(core, &port_data, 0); + port = pa_device_port_new(core, &port_data, sizeof(struct port *)); pa_device_port_new_data_done(&port_data); - pa_assert(port); + + ucm_port = port_new(ucm, port, pdevices, num); + *((struct port **) PA_DEVICE_PORT_DATA(port)) = ucm_port; pa_hashmap_put(ports, port->name, port); pa_log_debug("Add port %s: %s", port->name, port->description); @@ -1263,16 +1349,67 @@ static int ucm_create_mapping( return ret; } -static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device, const char *pre_tag) { +static void device_set_output_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(!jack || !device->mute_jack); + + if (jack == device->output_jack) + return; + + device->output_jack = jack; + + if (jack) + pa_alsa_ucm_device_set_available(device, jack->plugged_in); + else + pa_alsa_ucm_device_set_available(device, true); +} + +static void device_set_input_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(!jack || !device->mute_jack); + + if (jack == device->input_jack) + return; + + device->input_jack = jack; + + if (jack) + pa_alsa_ucm_device_set_available(device, jack->plugged_in); + else + pa_alsa_ucm_device_set_available(device, true); +} + +static void device_set_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(!jack || !device->output_jack); + pa_assert(!jack || !device->input_jack); + + if (jack == device->mute_jack) + return; + + device->mute_jack = jack; + + if (jack) + pa_alsa_ucm_device_set_available(device, !jack->plugged_in); + else + pa_alsa_ucm_device_set_available(device, true); +} + +static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device, const char *pre_tag, bool implicit) { + const char *jack_control; pa_alsa_jack *j; const char *device_name; char *name; - const char *jack_control; + const char *jack_hw_mute; pa_assert(ucm); pa_assert(device); pa_assert(pre_tag); + jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL); + if (!jack_control && !implicit) + return NULL; + device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); name = pa_sprintf_malloc("%s%s", pre_tag, device_name); @@ -1281,6 +1418,7 @@ static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *d goto out; j = pa_xnew0(pa_alsa_jack, 1); + j->device = device; j->state_unplugged = PA_AVAILABLE_NO; j->state_plugged = PA_AVAILABLE_YES; j->name = pa_xstrdup(name); @@ -1288,9 +1426,38 @@ static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *d jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL); if (jack_control) j->alsa_name = pa_xstrdup(jack_control); - else + else if (implicit) j->alsa_name = pa_sprintf_malloc("%s Jack", device_name); + jack_hw_mute = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_HW_MUTE); + if (jack_hw_mute) { + pa_alsa_ucm_device *device2; + bool device_found = false; + + PA_LLIST_FOREACH(device2, device->verb->devices) { + const char *device2_name; + + device2_name = pa_proplist_gets(device2->proplist, PA_ALSA_PROP_UCM_NAME); + + if (pa_streq(device2_name, jack_hw_mute)) { + device_found = true; + + if (!device2->output_jack && !device2->input_jack) { + j->hw_mute = device2; + device_set_mute_jack(device2, j); + } else { + pa_log("[%s] Ignoring \"JackHWMute\", because device availability is already governed by jack \"%s\".", + device2_name, device2->output_jack ? device2->output_jack->name : device2->input_jack->name); + } + + break; + } + } + + if (!device_found) + pa_log("[%s] JackHWMute references unknown device: %s", device_name, jack_hw_mute); + } + PA_LLIST_PREPEND(pa_alsa_jack, ucm->jacks, j); out: @@ -1349,7 +1516,12 @@ static int ucm_create_profile( if (verb_info[i].id == NULL) p->priority = 1000; + /* Get jacks for devices (and also associate the device with mappings). No + * implicit jacks are allowed (that is, if "JackControl" is not set, a jack + * will not be created). */ PA_LLIST_FOREACH(dev, verb->devices) { + pa_alsa_jack *jack; + name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); @@ -1358,9 +1530,57 @@ static int ucm_create_profile( ucm_create_mapping(ucm, ps, p, dev, verb_name, name, sink, source); if (sink) - dev->output_jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_OUTPUT); + jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_OUTPUT, false); + + if (source) + jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_INPUT, false); + + if (jack) { + if (dev->mute_jack) { + pa_log("[%s] Overriding \"JackHWMute\", because \"JackControl\" is set and takes precedence.", name); + device_set_mute_jack(dev, NULL); + } + } + + if (sink) + device_set_output_jack(dev, jack); + if (source) - dev->input_jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_INPUT); + device_set_input_jack(dev, jack); + } + + /* Now go over the device list again and get implicit jacks for devices + * that don't have "JackControl" set nor are referenced by any other + * device's "JackHWMute". "Implicit" means that the kcontrol name is not + * explicitly configured; instead, we generate the kcontrol name from the + * device name. + * + * We need a separate loop instead of creating the implicit jacks in the + * same loop as the other jacks, because the full set of mute jack + * relationships isn't known before we have iterated over all devices, + * and the relationships need to be known before creating the implicit + * jacks, since otherwise the implicit jacks could override the mute jacks, + * which we don't want. */ + PA_LLIST_FOREACH(dev, verb->devices) { + pa_alsa_jack *jack; + + if (dev->output_jack || dev->input_jack || dev->mute_jack) + continue; + + name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (sink) { + jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_OUTPUT, true); + device_set_output_jack(dev, jack); + } + + if (source) { + jack = ucm_get_jack(ucm, dev, PA_UCM_PRE_TAG_INPUT, true); + device_set_input_jack(dev, jack); + } } /* Now find modifiers that have their own PlaybackPCM and create @@ -1451,7 +1671,10 @@ static void ucm_mapping_jack_probe(pa_alsa_mapping *m) { PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { pa_alsa_jack *jack; jack = m->direction == PA_ALSA_DIRECTION_OUTPUT ? dev->output_jack : dev->input_jack; - pa_assert (jack); + + if (!jack) + continue; + jack->has_control = pa_alsa_mixer_find(mixer_handle, jack->alsa_name, 0) != NULL; pa_log_info("UCM jack %s has_control=%d", jack->name, jack->has_control); } @@ -1565,6 +1788,12 @@ static void free_verb(pa_alsa_ucm_verb *verb) { PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) { PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di); + + if (di->ports) { + pa_assert(pa_dynarray_size(di->ports) == 0); + pa_dynarray_free(di->ports); + } + pa_proplist_free(di->proplist); if (di->conflicting_devices) pa_idxset_free(di->conflicting_devices, NULL); @@ -1591,6 +1820,15 @@ void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) { pa_alsa_ucm_verb *vi, *vn; pa_alsa_jack *ji, *jn; + if (ucm->ports) { + struct port *port; + + while ((port = pa_dynarray_last(ucm->ports))) + port_free(port); + + pa_dynarray_free(ucm->ports); + } + PA_LLIST_FOREACH_SAFE(vi, vn, ucm->verbs) { PA_LLIST_REMOVE(pa_alsa_ucm_verb, ucm->verbs, vi); free_verb(vi); @@ -1685,6 +1923,35 @@ void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_ } } +void pa_alsa_ucm_device_set_available(pa_alsa_ucm_device *device, bool available) { + struct port *port; + unsigned idx; + + pa_assert(device); + + if (available == device->available) + return; + + device->available = available; + + PA_DYNARRAY_FOREACH(port, device->ports, idx) { + pa_alsa_ucm_device *device2; + unsigned idx2; + pa_available_t port_available = PA_AVAILABLE_YES; + + /* If there are any devices associated with the port that are + * unavailable, then the port is considered unavailable. */ + PA_DYNARRAY_FOREACH(device2, port->devices, idx2) { + if (!device2->available) { + port_available = PA_AVAILABLE_NO; + break; + } + } + + pa_device_port_set_available(port->core_port, port_available); + } +} + #else /* HAVE_ALSA_UCM */ /* Dummy functions for systems without UCM support */ @@ -1739,4 +2006,7 @@ void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, p void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { } +void pa_alsa_ucm_device_set_available(pa_alsa_ucm_device *device, bool available) { +} + #endif diff --git a/src/modules/alsa/alsa-ucm.h b/src/modules/alsa/alsa-ucm.h index a8c8090..1d76889 100644 --- a/src/modules/alsa/alsa-ucm.h +++ b/src/modules/alsa/alsa-ucm.h @@ -30,6 +30,8 @@ typedef void snd_use_case_mgr_t; #include "alsa-mixer.h" +#include <pulsecore/dynarray.h> + /** For devices: List of verbs, devices or modifiers available */ #define PA_ALSA_PROP_UCM_NAME "alsa.ucm.name" @@ -87,6 +89,9 @@ typedef void snd_use_case_mgr_t; /* Corresponds to the "JackControl" UCM value. */ #define PA_ALSA_PROP_UCM_JACK_CONTROL "alsa.ucm.jack_control" +/* Corresponds to the "JackHWMute" UCM value. */ +#define PA_ALSA_PROP_UCM_JACK_HW_MUTE "alsa.ucm.jack_hw_mute" + typedef struct pa_alsa_ucm_verb pa_alsa_ucm_verb; typedef struct pa_alsa_ucm_modifier pa_alsa_ucm_modifier; typedef struct pa_alsa_ucm_device pa_alsa_ucm_device; @@ -122,7 +127,15 @@ void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_ /* UCM - Use Case Manager is available on some audio cards */ +struct pa_alsa_ucm_port { + pa_device_port *core_port; + pa_dynarray *devices; /* pa_alsa_ucm_device */ + bool available; +}; + struct pa_alsa_ucm_device { + pa_alsa_ucm_verb *verb; + PA_LLIST_FIELDS(pa_alsa_ucm_device); pa_proplist *proplist; @@ -142,10 +155,33 @@ struct pa_alsa_ucm_device { pa_idxset *conflicting_devices; pa_idxset *supported_devices; + pa_dynarray *ports; /* pa_alsa_ucm_port */ + + /* These jacks are owned by this device. When these jacks are plugged in, + * the device is marked as available, and when these jacks are unplugged, + * the device is marked as unavailable. Not all devices have jacks + * associated with them; those devices are always marked as available. + * + * XXX: It's probably pointless to have separate jacks for input and + * output. Both jack objects will anyway reference the same alsa + * kcontrol, so they will always have the same state. */ pa_alsa_jack *input_jack; pa_alsa_jack *output_jack; + + /* The "mute jack" is owned by some other device. The mute jack makes this + * device unavailable when it's plugged in. A device can't have both a + * "normal" jack (meaning the output_jack or input_jack field) and a mute + * jack, because there would be ambiguity about which jack is controlling + * the device availability. If the configuration tries to set both types of + * jacks for one device, the normal jack has precedence and mute_jack gets + * set to NULL. */ + pa_alsa_jack *mute_jack; + + bool available; }; +void pa_alsa_ucm_device_set_available(pa_alsa_ucm_device *device, bool available); + struct pa_alsa_ucm_modifier { PA_LLIST_FIELDS(pa_alsa_ucm_modifier); @@ -185,6 +221,7 @@ struct pa_alsa_ucm_config { PA_LLIST_HEAD(pa_alsa_ucm_verb, verbs); PA_LLIST_HEAD(pa_alsa_jack, jacks); + pa_dynarray *ports; /* struct port */ }; struct pa_alsa_ucm_mapping_context { diff --git a/src/modules/alsa/module-alsa-card.c b/src/modules/alsa/module-alsa-card.c index a7fec04..7ac750f 100644 --- a/src/modules/alsa/module-alsa-card.c +++ b/src/modules/alsa/module-alsa-card.c @@ -384,16 +384,18 @@ static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask) { PA_HASHMAP_FOREACH(jack, u->jacks, state) if (jack->melem == melem) { - jack->plugged_in = plugged_in; + pa_alsa_jack_set_plugged_in(jack, plugged_in); + if (u->use_ucm) { - pa_assert(u->card->ports); - port = pa_hashmap_get(u->card->ports, jack->name); - pa_assert(port); - } - else { - pa_assert(jack->path); - pa_assert_se(port = jack->path->port); + /* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack + * state to port availability. */ + continue; } + + /* When not using UCM, we have to do the jack state -> port + * availability mapping ourselves. */ + pa_assert(jack->path); + pa_assert_se(port = jack->path->port); report_port_state(port, u); } return 0; diff --git a/src/pulsecore/device-port.c b/src/pulsecore/device-port.c index cfe2a80..906ab1f 100644 --- a/src/pulsecore/device-port.c +++ b/src/pulsecore/device-port.c @@ -66,8 +66,6 @@ void pa_device_port_new_data_done(pa_device_port_new_data *data) { } void pa_device_port_set_available(pa_device_port *p, pa_available_t status) { - pa_core *core; - pa_assert(p); if (p->available == status) @@ -80,10 +78,14 @@ void pa_device_port_set_available(pa_device_port *p, pa_available_t status) { status == PA_AVAILABLE_NO ? "no" : "unknown"); /* Post subscriptions to the card which owns us */ - pa_assert_se(core = p->core); - pa_subscription_post(core, PA_SUBSCRIPTION_EVENT_CARD|PA_SUBSCRIPTION_EVENT_CHANGE, p->card->index); - - pa_hook_fire(&core->hooks[PA_CORE_HOOK_PORT_AVAILABLE_CHANGED], p); + /* XXX: We need to check p->card, because this function may be called + * before the card object has been created. The card object should probably + * be created before port objects, and then p->card could be non-NULL for + * the whole lifecycle of pa_device_port. */ + if (p->card) { + pa_subscription_post(p->core, PA_SUBSCRIPTION_EVENT_CARD|PA_SUBSCRIPTION_EVENT_CHANGE, p->card->index); + pa_hook_fire(&p->core->hooks[PA_CORE_HOOK_PORT_AVAILABLE_CHANGED], p); + } } static void device_port_free(pa_object *o) { -- 1.9.3