#include "audio-impl-pw.hpp" #include "audio-impl-shared.hpp" #include "asset-manager.hpp" #include "audio-types.hpp" #include "game-settings.hpp" #include "math-helpers.hpp" #include "pipewire/keys.h" #include "pipewire/thread-loop.h" #include "pk.h" #include "vendor-glm-include.hpp" #include "window.hpp" // see header file #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #include "spa/utils/json.h" #pragma GCC diagnostic pop #define PKE_AUDIO_PW_NAME_RESERVE_LEN 256 pke_audio_implementation_pipewire pke_audio_pw{}; int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value); static const struct pw_stream_events stream_events = { .version = PW_VERSION_STREAM_EVENTS, .destroy = nullptr, .state_changed = nullptr, .control_info = nullptr, .io_changed = nullptr, .param_changed = on_pipewire_stream_param_changed, .add_buffer = nullptr, .remove_buffer = nullptr, .process = on_pipewire_process, .drained = nullptr, .command = nullptr, .trigger_done = nullptr, }; static const struct pw_registry_events registry_events = { .version = PW_VERSION_REGISTRY_EVENTS, .global = on_registry_event_global, .global_remove = on_registry_event_global_removal, }; static const struct pw_metadata_events metadata_events { .version = PW_VERSION_METADATA_EVENTS, .property = metadata_event_property, }; bool pw_objects_find_pw_object_by_id(void *removed_id, void *item) { return reinterpret_cast(item)->id == *(uint32_t *)removed_id; } void pke_audio_pw_init() { int i; pke_audio_pw.bkt = pk_mem_bucket_create("pipewire bucket", PK_MEM_DEFAULT_BUCKET_SIZE, PK_MEMBUCKET_FLAG_NONE); pke_audio_pw.loop = nullptr; pke_audio_pw.pod_buffer = nullptr; pke_audio_pw.stream = nullptr; pke_audio_pw.core = nullptr; pke_audio_pw.registry = nullptr; pke_audio_pw.registry_listener = {}; pke_audio_pw.metadata = nullptr; pke_audio_pw.metadata_listener = {}; pke_audio_pw.pw_objects = {}; pke_audio_pw.pw_objects.bkt = pke_audio_pw.bkt; pke_audio_pw.created_objects = {}; pke_audio_pw.created_objects.bkt = pke_audio_pw.bkt; pke_audio_pw.default_sink_name = pk_new(PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_pw.bkt); pke_audio_pw.default_sink_id = 0; pke_audio_pw.is_needing_output_remapped = true; pke_audio_pw.pod_buffer = pk_new(pke_audio_mstr.channel_count * PKE_AUDIO_POD_BUFFER_LEN, pke_audio_pw.bkt); pw_init(NULL, NULL); pke_audio_pw.loop = pw_thread_loop_new("pke-audio-thrd", NULL); if (pke_audio_pw.loop == NULL) { fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_loop"); return; } pw_properties *props = pw_properties_new( PW_KEY_MEDIA_NAME, "pke-audio-pw", PW_KEY_APP_NAME, "pke", PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", PW_KEY_MEDIA_ROLE, "Music", nullptr); assert(props != NULL); pke_audio_pw.stream = pw_stream_new_simple( pw_thread_loop_get_loop(pke_audio_pw.loop), "pke-audio-pw", props, &stream_events, NULL /* user-data */); if (pke_audio_pw.stream == NULL) { fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_stream"); return; } spa_pod_builder b{}; b.data = pke_audio_pw.pod_buffer; b.size = PKE_AUDIO_POD_BUFFER_LEN * pke_audio_mstr.channel_count; b._padding = 0; b.callbacks.data = NULL; b.callbacks.funcs = NULL; b.state.flags = 0; b.state.offset = 0; b.state.frame = NULL; spa_audio_info_raw spa_audio_info_raw_init{}; spa_audio_info_raw_init.flags = 0; spa_audio_info_raw_init.format = SPA_AUDIO_FORMAT_F32; spa_audio_info_raw_init.rate = 48000; spa_audio_info_raw_init.channels = pke_audio_mstr.channel_count; spa_audio_info_raw_init.position[0] = SPA_AUDIO_CHANNEL_FL; spa_audio_info_raw_init.position[1] = SPA_AUDIO_CHANNEL_FR; const struct spa_pod *params[1]; params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &spa_audio_info_raw_init); if (i = pw_stream_connect(pke_audio_pw.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, pw_stream_flags(PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1), i != 0) { fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to connect to pw_stream"); return; } pke_audio_pw.core = pw_stream_get_core(pke_audio_pw.stream); if (pke_audio_pw.core == NULL) { fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get core"); return; } pke_audio_pw.registry = pw_core_get_registry(pke_audio_pw.core, PW_VERSION_REGISTRY, 0); if (pke_audio_pw.registry == NULL) { fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get registry"); return; } memset(&pke_audio_pw.registry_listener, 0, sizeof(pke_audio_pw.registry_listener)); pw_registry_add_listener(pke_audio_pw.registry, &pke_audio_pw.registry_listener, ®istry_events, NULL); pw_thread_loop_start(pke_audio_pw.loop); return; } void pke_audio_pw_teardown() { uint32_t i; pke_audio_mstr.mtx_buffer.lock(); pw_thread_loop_stop(pke_audio_pw.loop); for (i = pke_audio_pw.created_objects.next; i > 0; --i) { pw_core_destroy(pke_audio_pw.core, ((void **)pke_audio_pw.created_objects.data)[i - 1]); } for (i = pke_audio_pw.pw_objects.next; i > 0; --i) { pw_properties_free(pke_audio_pw.pw_objects[i - 1].props); } if (pke_audio_pw.metadata != NULL) { pw_proxy_destroy((struct pw_proxy*)pke_audio_pw.metadata); } if (pke_audio_pw.registry != NULL) { pw_proxy_destroy((struct pw_proxy*)pke_audio_pw.registry); } if (pke_audio_pw.core != NULL) { pw_core_disconnect(pke_audio_pw.core); } if (pke_audio_pw.stream != NULL) { pw_stream_destroy(pke_audio_pw.stream); } if (pke_audio_pw.loop != NULL) { pw_thread_loop_destroy(pke_audio_pw.loop); } pw_deinit(); pk_delete(pke_audio_pw.default_sink_name, PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_pw.bkt); pk_delete(pke_audio_pw.pod_buffer, pke_audio_mstr.channel_count * PKE_AUDIO_POD_BUFFER_LEN, pke_audio_pw.bkt); pk_arr_reset(&pke_audio_pw.created_objects); pke_audio_pw.created_objects.bkt = nullptr; pk_arr_reset(&pke_audio_pw.pw_objects); pke_audio_pw.pw_objects.bkt = nullptr; pk_mem_bucket_destroy(pke_audio_pw.bkt); pke_audio_pw.bkt = CAFE_BABE(pk_membucket); pke_audio_mstr.mtx_buffer.unlock(); } void pke_audio_pw_remap_outputs() { uint32_t stream_id; uint32_t i; uint32_t port_count = {0}; void *created_obj = {0}; if (pke_audio_pw.pw_objects.data == nullptr) return; if (pke_audio_pw.default_sink_name == nullptr || pke_audio_pw.default_sink_name[0] == '\0') return; pw_properties *props = pw_properties_new(NULL, NULL); pke_audio_pw_object *objs = (pke_audio_pw_object *)pke_audio_pw.pw_objects.data; const char *str; uint32_t *out_ports = pk_new(pke_audio_mstr.channel_count, pkeSettings.mem_bkt.game_transient); uint32_t *in_ports = pk_new(pke_audio_mstr.channel_count, pkeSettings.mem_bkt.game_transient); pke_audio_mstr.mtx_buffer.lock(); pw_thread_loop_lock(pke_audio_pw.loop); // remove all existing links { for (i = pke_audio_pw.created_objects.next; i > 0; --i) { pw_core_destroy(pke_audio_pw.core, pke_audio_pw.created_objects[i - 1]); } pk_arr_clear(&pke_audio_pw.created_objects); } // find node we're going to connect to // + build out_ports and in_ports { stream_id = pw_stream_get_node_id(pke_audio_pw.stream); if (stream_id == 0) { goto remap_done; } for (i = 0; i < pke_audio_pw.pw_objects.next; ++i) { if (objs[i].type != PKE_AUDIO_PW_OBJECT_TYPE_NODE) continue; if ((str = spa_dict_lookup(&objs[i].props->dict, "node.name")), str == NULL) continue; if (strcmp(pke_audio_pw.default_sink_name, str) == 0) { pke_audio_pw.default_sink_id = objs[i].id; break; } } // assert(stream_id != 0); if (pke_audio_pw.default_sink_id == 0) { goto remap_done; } for (i = 0; i < pke_audio_pw.pw_objects.next; ++i) { if (objs[i].type != PKE_AUDIO_PW_OBJECT_TYPE_PORT) continue; if (objs[i].data.port.node != pke_audio_pw.default_sink_id && objs[i].data.port.node != stream_id) continue; if (objs[i].data.port.node == pke_audio_pw.default_sink_id && objs[i].data.port.direction == PW_DIRECTION_OUTPUT) continue; if (objs[i].data.port.node == stream_id) { out_ports[objs[i].data.port.id] = objs[i].id; } else { port_count++; in_ports[objs[i].data.port.id] = objs[i].id; } } } // assert(port_count != 0); if (port_count == 0) { goto remap_done; } // do links { for (i = 0; i < pke_audio_mstr.channel_count; ++i) { if (out_ports[i] == 0) { goto remap_done; } fprintf(stdout, "[pke_audio_pw_remap_outputs] Mapping (out:in): %d:%d\n", out_ports[i], in_ports[i]); pw_properties_clear(props); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%d", out_ports[i]); pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%d", in_ports[i % port_count]); created_obj = pw_core_create_object(pke_audio_pw.core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); if (created_obj != NULL) pk_arr_append(&pke_audio_pw.created_objects, created_obj); } } fprintf(stdout, "[pke_audio_pw_remap_outputs] remapped %i objs\n", pke_audio_pw.created_objects.next); remap_done: if (props != nullptr) pw_properties_free(props); pke_audio_pw.is_needing_output_remapped = pke_audio_pw.created_objects.next != pke_audio_mstr.channel_count; pw_thread_loop_unlock(pke_audio_pw.loop); pke_audio_mstr.mtx_buffer.unlock(); } void on_pipewire_process(void *user_data) { (void)user_data; pw_buffer *b; spa_buffer *buf; int stride; uint8_t pc, pc2; uint32_t i, ii, c; int64_t n_frames, i_frame; float *dst; float val, vol, vol2; float *spatial_volumes = nullptr; glm::vec3 listener_origin; glm::vec3 audio_dir = glm::vec3(0); glm::vec3 *spatial_normals = nullptr; pke_audio_fx_params_reverb *params_reverb; pke_audio_fx_params_delay *params_delay; pke_audio_fx_params_low_pass_filter *params_low_pass_filter; pk_mem_bucket_reset(pke_audio_mstr.bkt_transient); if ((b = pw_stream_dequeue_buffer(pke_audio_pw.stream)) == NULL) { fprintf(stderr, "[" __FILE__ "][on_pipewire_process] out of buffers"); return; } buf = b->buffer; if ((dst = (float*)buf->datas[0].data) == NULL) { fprintf(stderr, "[" __FILE__ "][on_pipewire_process] given buffer was null"); return; } spatial_volumes = pk_new(pke_audio_mstr.channel_count, pke_audio_mstr.bkt_transient); spatial_normals = pk_new(pke_audio_mstr.channel_count, pke_audio_mstr.bkt_transient); params_reverb = pk_new(pke_audio_mstr.channel_count, pke_audio_mstr.bkt_transient); params_delay = pk_new(pke_audio_mstr.channel_count, pke_audio_mstr.bkt_transient); params_low_pass_filter = pk_new(pke_audio_mstr.channel_count, pke_audio_mstr.bkt_transient); // calculate spatial_normals // TODO maybe don't use UBO // TODO project-defined? // TODO instanced audio manager? (easier to handle explicit stereo sources?) listener_origin = glm::inverse(UBO.model) * glm::vec4(0, 0, 0, 1); for (c = 0; c < pke_audio_mstr.channel_count; ++c) { switch (c) { case 0: // left-speaker if (pke_audio_mstr.channel_count > 2) { spatial_normals[c] = glm::normalize(glm::vec3(-1.f, 0.f, 1.f)); } else { spatial_normals[c] = glm::normalize(glm::vec3(-1.f, 0.f, 0.f)); } break; case 1: // right if (pke_audio_mstr.channel_count > 2) { spatial_normals[c] = glm::normalize(glm::vec3( 1.f, 0.f, 1.f)); } else { spatial_normals[c] = glm::normalize(glm::vec3( 1.f, 0.f, 0.f)); } break; case 2: // center spatial_normals[c] = glm::normalize(glm::vec3( 0.f, 0.f, 1.f)); break; case 3: // subwoofer spatial_normals[c] = glm::normalize(glm::vec3( 0.f, 0.f, 0.f)); break; case 4: // left surround spatial_normals[c] = glm::normalize(glm::vec3(-1.f, 0.f, 0.f)); break; case 5: // right surround spatial_normals[c] = glm::normalize(glm::vec3( 1.f, 0.f, 0.f)); break; case 6: // rear left surround spatial_normals[c] = glm::normalize(glm::vec3(-1.f, 0.f, -1.f)); break; case 7: // rear right surround spatial_normals[c] = glm::normalize(glm::vec3( 1.f, 0.f, -1.f)); break; default: break; } } pke_audio_mstr.mtx_buffer.lock(); stride = sizeof(float) * pke_audio_mstr.channel_count; n_frames = buf->datas[0].maxsize / stride; if (b->requested) { n_frames = PK_MIN((int64_t)b->requested, n_frames); } // fprintf(stdout, "[pw] frame count: %li, requested: %lu\n", n_frames, b->requested); buf->datas[0].chunk->offset = 0; buf->datas[0].chunk->stride = stride; buf->datas[0].chunk->size = n_frames * stride; if (buf->datas[0].chunk->size == 0) { goto audio_done; } for (i = 0; i < pke_audio_mstr.playing_objects.next; ++i) { pke_audio_obj *aobj = &pke_audio_mstr.playing_objects[i]; const Asset *a = AM_Get(aobj->handle); vol = 1.0; vol *= pke_audio_mstr.master_volume; vol *= pke_audio_mstr.source_volumes[static_cast(aobj->source)]; for (pc = 0; pc < aobj->play_count; ++pc) { // TODO configurable // >= 5.0 meters is 100% volume // <=50.0 meters is 0% volume float distance_volume = glm::distance(aobj->position_source[pc], listener_origin); distance_volume = 1.0 - ((distance_volume - 5.0) / 50 - 5); distance_volume = glm::clamp(distance_volume, 0.f, 1.f); // calculate panning // TODO handle subwoofer explicitly float sum = 0.0; for (c = 0; c < pke_audio_mstr.channel_count; ++c) { audio_dir = glm::normalize(aobj->position_source[pc] - listener_origin); for (ii = 0; ii < 3; ++ii) { if (glm::isnan(audio_dir[ii])) { audio_dir = glm::vec3(0); spatial_volumes[c] = 1.f; break; } } float dot_dir = glm::dot(spatial_normals[c], audio_dir); if (!glm::isnan(dot_dir) && dot_dir < 0.0) { // fprintf(stderr, "[pw] dot: %f\n", dot_dir); // 20k max, 500 min params_low_pass_filter[c].cutoff_freq = log_interp(500.f, 20000.f, abs(dot_dir)); } else { params_low_pass_filter[c].cutoff_freq = 0.f; } params_reverb[c].reverb_strength = 0.f; // TODO params_delay[c].delay_frames = 0; // TODO // spatial_volumes[c] = glm::clamp(dot_dir, 0.f, 1.f); spatial_volumes[c] = lerp(0.f, 1.f, dot_dir + 0.85f); if (isnan(spatial_volumes[c]) || spatial_volumes[c] == 0.0f) { /* fprintf(stderr, "[pw] NaN or 0: chan: %i, norm: %f,%f,%f, src: %f,%f,%f, origin: %f,%f,%f\n", c, audio_dir.x, audio_dir.y, audio_dir.z, aobj->position_source[pc].x, aobj->position_source[pc].y, aobj->position_source[pc].z, listener_origin.x, listener_origin.y, listener_origin.z ); */ spatial_volumes[c] = 1.0; } sum += spatial_volumes[c]; } // normalize (TODO do we want this?) /* if (sum > 1.f) { for (c = 0; c < pke_audio_mstr.channel_count; ++c) { spatial_volumes[c] /= sum; } } */ /* for (c = 0; c < pke_audio_mstr.channel_count; ++c) { fprintf(stderr, "[pw] obj: %i, chan: %i vol: %f, spatial: %f, dist: %f\n", i, c, vol, spatial_volumes[c], distance_volume ); } */ for (i_frame = 0; i_frame < n_frames; ++i_frame) { dst = (float*)buf->datas[0].data; dst += (i_frame * (uint64_t)pke_audio_mstr.channel_count); // advance for (c = 0; c < pke_audio_mstr.channel_count; ++c) { if (i == 0 && pc == 0) { *dst = 0.f; } // clear buffer as we go vol2 = vol; if (PK_HAS_FLAG(aobj->flags[pc], pke_audio_flag_pos_spatial)) { vol2 *= (spatial_volumes[c] * distance_volume); } if (vol2 <= 0.0) { // fprintf(stderr, "[pw] chan: %i vol2 is <= 0.0\n", c); dst += 1; continue; } /* if (isnan(vol2)) { fprintf(stderr, "[pw] vol2 is NaN\n"); } if (vol2 == 0.0) { fprintf(stderr, "[pw] vol2 is 0, %f, %f, %f\n", pke_audio_mstr.master_volume, pke_audio_mstr.source_volumes[c], spatial_volumes[c]); } */ // val = ((float*)a->ptr)[aobj->play_heads[pc]]; // val is read inside fx_low_pass_filter val = pke_audio_fx_low_pass_filter((float*)a->ptr, a->size / sizeof(float), (uint32_t)aobj->play_heads[pc], ¶ms_low_pass_filter[pc]); val += pke_audio_fx_reverb((float*)a->ptr, a->size / sizeof(float), (uint32_t)aobj->play_heads[pc], ¶ms_reverb[pc]); val += pke_audio_fx_delay((float*)a->ptr, a->size / sizeof(float), (uint32_t)aobj->play_heads[pc], ¶ms_delay[pc]); *dst += val * vol2; /* if (isnan(*dst)) { fprintf(stderr, "[pw] *dst is NaN\n"); } */ dst += 1; } // TODO type-specific attributes for assets so we can pre-calculate this or just KNOW the frame length ahead of time aobj->play_heads[pc] += 1; if (aobj->play_heads[pc] >= a->size / sizeof(float)) { for (pc2 = 0; pc2 < aobj->play_count-1; ++pc2) { aobj->flags[pc2] = aobj->flags[pc2+1]; aobj->play_heads[pc2] = aobj->play_heads[pc2+1]; aobj->position_source[pc2] = aobj->position_source[pc2+1]; } pc -= 1; aobj->play_count -= 1; break; } } } AM_Release(aobj->handle); if (aobj->play_count == 0) { pk_arr_remove_at(&pke_audio_mstr.playing_objects, i); i -= 1; } } if (pke_audio_mstr.playing_objects.next == 0) { memset(dst, 0, sizeof(float) * n_frames * pke_audio_mstr.channel_count); } audio_done: pw_stream_queue_buffer(pke_audio_pw.stream, b); pke_audio_mstr.mtx_buffer.unlock(); return; } void on_pipewire_stream_param_changed(void *, uint32_t id, const struct spa_pod *param) { /* NULL means to clear the format */ if (param == NULL || id != SPA_PARAM_Format) { // memset(&pke_audio_pw.format, 0, sizeof(struct spa_audio_info)); return; } if (spa_format_parse(param, &pke_audio_pw.format.media_type, &pke_audio_pw.format.media_subtype) < 0) { return; } /* only accept raw audio */ if (pke_audio_pw.format.media_type != SPA_MEDIA_TYPE_audio || pke_audio_pw.format.media_subtype != SPA_MEDIA_SUBTYPE_raw) { return; } /* call a helper function to parse the format for us. */ spa_format_audio_raw_parse(param, &pke_audio_pw.format.info.raw); fprintf(stdout, "capturing rate changed:%d channels:%d\n", pke_audio_pw.format.info.raw.rate, pke_audio_pw.format.info.raw.channels); return; } int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value) { if (subject == PW_ID_CORE) { if (key == NULL || spa_streq(key, "default.audio.sink")) { if (pke_audio_pw.default_sink_id != 0) { // the default just changed?? pke_audio_pw.is_needing_output_remapped = true; } if (value == NULL || spa_json_str_object_find(value, strlen(value), "name", pke_audio_pw.default_sink_name, PKE_AUDIO_PW_NAME_RESERVE_LEN) < 0) { pke_audio_pw.default_sink_name[0] = '\0'; } } } fprintf(stdout, "PW_METADATA_CB: data: %p, subject: %d, key: '%s', type: '%s', value: '%s'\n", data, subject, key, type, value); return 0; } void on_registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { (void)data; (void)permissions; const char *str; pke_audio_pw_object obj{}; enum PK_STN_RES res; // if (props == NULL) return; // why if (props == NULL) { printf("PW CALLBACK: NULL PROPS: object: id:%u type:%s/%d\n", id, type, version); return; // why - in what scenario does this happen? } if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { printf("PW CALLBACK NODE: object: id:%u type:%s/%d\n", id, type, version); obj.type = PKE_AUDIO_PW_OBJECT_TYPE_NODE; } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) { obj.type = PKE_AUDIO_PW_OBJECT_TYPE_PORT; if ((str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) == NULL) { return; } if (spa_streq(str, "in")) { obj.data.port.direction = PW_DIRECTION_INPUT; } else if (spa_streq(str, "out")) { obj.data.port.direction = PW_DIRECTION_OUTPUT; } else { return; } if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL) { return; } if (res = pk_stn(&obj.data.port.node, str, 0), res != PK_STN_RES_SUCCESS) { fprintf(stderr, "PW_CALLBACK: failed to parse port node from str: '%s', PK_STN_RES: %i\n", str, res); return; } if ((str = spa_dict_lookup(props, PW_KEY_PORT_ID)) == NULL) { return; } if (res = pk_stn(&obj.data.port.id, str, 0), res != PK_STN_RES_SUCCESS) { fprintf(stderr, "PW_CALLBACK: failed to parse port id from str: '%s', PK_STN_RES: %i\n", str, res); return; } } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) { obj.type = PKE_AUDIO_PW_OBJECT_TYPE_LINK; if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL) { return; } if (res = pk_stn(&obj.data.link.output_port, str, 0), res != PK_STN_RES_SUCCESS) { fprintf(stderr, "PW_CALLBACK: failed to parse link output port from str: '%s', PK_STN_RES: %i\n", str, res); return; } if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL) { return; } if (res = pk_stn(&obj.data.link.input_port, str, 0), res != PK_STN_RES_SUCCESS) { fprintf(stderr, "PW_CALLBACK: failed to parse link input port from str: '%s', PK_STN_RES: %i\n", str, res); return; } } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) { obj.type = PKE_AUDIO_PW_OBJECT_TYPE_METADATA; if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL) { return; } if (!spa_streq(str, "default")) { return; } pke_audio_pw.metadata = (pw_metadata*)pw_registry_bind(pke_audio_pw.registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); spa_zero(pke_audio_pw.metadata_listener); pw_metadata_add_listener(pke_audio_pw.metadata, &pke_audio_pw.metadata_listener, &metadata_events, NULL); } else { return; } #if 0 int i; for (i = 0; i < props->n_items; ++i) { printf("\t'%s': '%s'", props->items[i].key, props->items[i].value); } printf(""); #endif obj.id = id; obj.props = pw_properties_new_dict(props); printf("PW CALLBACK: object: id:%u type:%s/v%d\n", id, type, version); pk_arr_append_t(&pke_audio_pw.pw_objects, obj); // TODO 2025-07-07 JCB // I don't know if this logic is ever true. // Specifically, on startup we don't have default_sink_id yet. // So the only time this catches is if ports on our default node change during runtime? // I feel like this was the right idea, but maybe the wrong place for it? // The bug I was trying to fix at the time was a race condition where we were // trying to link output ports that didn't exist yet. // The correct answer was to validate the data before making the links. /* if (obj.type == PKE_AUDIO_PW_OBJECT_TYPE_PORT && obj.data.port.node == pke_audio_pw.default_sink_id) { pke_audio_pw.is_needing_output_remapped = true; } */ return; } void on_registry_event_global_removal(void *data, uint32_t id) { (void)data; uint32_t i; // printf("PW CALLBACK: object remove: id:%u ...", id); if (i = pk_arr_find_first_index(&pke_audio_pw.pw_objects, &id, pw_objects_find_pw_object_by_id), i != 0xFFFFFFFF) { if (pke_audio_pw.pw_objects[i].id == pke_audio_pw.default_sink_id) { // we just lost our target audio device pke_audio_pw.default_sink_id = 0; pke_audio_pw.default_sink_name[0] = '\0'; pke_audio_pw.is_needing_output_remapped = true; } pk_arr_remove_at(&pke_audio_pw.pw_objects, i); // printf(" removed.\n"); } return; }