From 277b80c90307ca22104945777a47246db3e87d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Wed, 5 Mar 2025 08:01:11 +0100 Subject: [PATCH 01/98] bluez5: backend-native: Fix ECNR support in HFP HF SDP record Sending AT+ECNR is supported by the native backend --- spa/plugins/bluez5/backend-native.c | 1 + 1 file changed, 1 insertion(+) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 359696903..65cb764cd 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -3318,6 +3318,7 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag } else if (profile == SPA_BT_PROFILE_HFP_AG) { /* Start SLC connection */ unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_CLIP | SPA_BT_HFP_HF_FEATURE_3WAY | + SPA_BT_HFP_HF_FEATURE_ECNR | SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS | SPA_BT_HFP_HF_FEATURE_ESCO_S4; bool has_msbc = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC); From 79d232c1f2fbecf445cd4cf09e3f2e6ca2ff2753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Wed, 5 Mar 2025 08:13:01 +0100 Subject: [PATCH 02/98] bluez5: backend-native: Fix incoming call crash HFP/HF/TWC/BV-01-C test creates an incoming call as soon as the SLC is completed, i.e. a +CIEV: ,1 event just after AT+CHLD=? reply has been received. This try to parse the rfcomm->telephony_ag->call_list which has not yet been created. This commit move the telephony_ag creation to the SLC completed event. --- spa/plugins/bluez5/backend-native.c | 39 +++++++++++++++-------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 65cb764cd..74c58d296 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -2286,6 +2286,26 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) } SPA_FALLTHROUGH; case hfp_hf_chld: + rfcomm->slc_configured = true; + + if (!rfcomm->codec_negotiation_supported) { + if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) { + // TODO: We should manage the missing transport + } else { + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + + rfcomm->telephony_ag = telephony_ag_new(backend->telephony, 0); + rfcomm->telephony_ag->address = strdup(rfcomm->device->address); + telephony_ag_set_callbacks(rfcomm->telephony_ag, + &telephony_ag_callbacks, rfcomm); + if (rfcomm->transport) { + rfcomm->telephony_ag->transport.codec = rfcomm->transport->codec; + rfcomm->telephony_ag->transport.state = rfcomm->transport->state; + } + telephony_ag_register(rfcomm->telephony_ag); + rfcomm_send_cmd(rfcomm, "AT+CLIP=1"); rfcomm->hf_state = hfp_hf_clip; break; @@ -2312,25 +2332,6 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) SPA_FALLTHROUGH; case hfp_hf_nrec: rfcomm->hf_state = hfp_hf_slc1; - rfcomm->slc_configured = true; - - if (!rfcomm->codec_negotiation_supported) { - if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) { - // TODO: We should manage the missing transport - } else { - spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); - } - } - - rfcomm->telephony_ag = telephony_ag_new(backend->telephony, 0); - rfcomm->telephony_ag->address = strdup(rfcomm->device->address); - telephony_ag_set_callbacks(rfcomm->telephony_ag, - &telephony_ag_callbacks, rfcomm); - if (rfcomm->transport) { - rfcomm->telephony_ag->transport.codec = rfcomm->transport->codec; - rfcomm->telephony_ag->transport.state = rfcomm->transport->state; - } - telephony_ag_register(rfcomm->telephony_ag); if (rfcomm->hfp_hf_clcc) { rfcomm_send_cmd(rfcomm, "AT+CLCC"); From 5c1be35018caca8fdfe1abc86a2ee04237fb1bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Wed, 5 Mar 2025 12:19:59 +0100 Subject: [PATCH 03/98] bluez5: backend-native: Fix 3way active call hangup HFP/HF/TWC/BV-03-C test, which setup an active and a held calls, expects to receive AT+CHLD=1 (release and swap calls) instead of AT+CHUP on active call hang up request. As this changes the active call to disconnected and held call to being active, the call states should be managed in hfp_hf_hangup instead of waiting for +CIEV (callheld=0) event which will drop the previously held call before AT+CLCC reply can inform this call is now active. --- spa/plugins/bluez5/backend-native.c | 46 ++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 74c58d296..8d5354d31 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -1406,18 +1406,44 @@ static void hfp_hf_hangup(void *data, enum spa_bt_telephony_error *err, uint8_t struct rfcomm_call_data *call_data = data; struct rfcomm *rfcomm = call_data->rfcomm; struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call, *tcall; + bool found_held = false; + bool hfp_hf_in_progress = false; char reply[20]; bool res; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) + found_held = true; + } + switch (call_data->call->state) { case CALL_STATE_ACTIVE: case CALL_STATE_DIALING: case CALL_STATE_ALERTING: case CALL_STATE_INCOMING: - rfcomm_send_cmd(rfcomm, "AT+CHUP"); + if (found_held) { + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); + hfp_hf_in_progress = true; + } else { + rfcomm_send_cmd(rfcomm, "AT+CHUP"); + } break; case CALL_STATE_WAITING: + if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } rfcomm_send_cmd(rfcomm, "AT+CHLD=0"); + hfp_hf_in_progress = true; break; default: spa_log_info(backend->log, "Call not incoming, waiting or active: skip hangup"); @@ -1435,6 +1461,24 @@ static void hfp_hf_hangup(void *data, enum spa_bt_telephony_error *err, uint8_t return; } + if (hfp_hf_in_progress) { + if (call_data->call->state != CALL_STATE_WAITING) { + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) { + call->state = CALL_STATE_ACTIVE; + telephony_call_notify_updated_props(call); + } + } + } + rfcomm->hfp_hf_in_progress = true; + } *err = BT_TELEPHONY_ERROR_NONE; } From 901401e6f1105269d53f0e66e0dd54016852f963 Mon Sep 17 00:00:00 2001 From: Jan Palus Date: Thu, 6 Mar 2025 13:57:21 +0100 Subject: [PATCH 04/98] module-roc: require roc >= 0.4.0 3270bd4 introduced changes reyling on features from roc 0.4.0 (upstream commit: https://github.com/roc-streaming/roc-toolkit/commit/d18d342) --- src/modules/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/meson.build b/src/modules/meson.build index 51db77334..be9912dd6 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -620,7 +620,7 @@ if build_module_raop endif summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') -roc_dep = dependency('roc', version: '>= 0.3.0', required: get_option('roc')) +roc_dep = dependency('roc', version: '>= 0.4.0', required: get_option('roc')) summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons') pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source', From 78981a8d9b98892bae70da9e2213fb546f3af499 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 11:57:08 +0200 Subject: [PATCH 05/98] spa: acp: in SplitPCM probe channel count and allow excess In SplitPCM mode, Focusrite Scarlett Gen 4 (USB 1235:8218) UCM profile specifies "CaptureChannels 2" for the Mic1/2 inputs, but snd_pcm_hw_params_set_channels(2) fails for the HW device. Fix by not requiring the channel count to be exact for SplitPCM, but also allow larger numbers of channels than what UCM profile specifies. --- spa/plugins/alsa/acp/alsa-ucm.c | 45 ++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index 9061a1f26..77e476eb8 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -2383,7 +2383,7 @@ static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm) dev->eld_device = pcm_device; } -static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) { +static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode, bool max_channels) { snd_pcm_t* pcm; pa_sample_spec try_ss = ucm->default_sample_spec; pa_channel_map try_map; @@ -2391,6 +2391,11 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, bool exact_channels = m->channel_map.channels > 0; if (!m->split) { + if (max_channels) { + errno = EINVAL; + return NULL; + } + if (exact_channels) { try_map = m->channel_map; try_ss.channels = try_map.channels; @@ -2402,8 +2407,8 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, return NULL; } - exact_channels = true; - try_ss.channels = m->split->hw_channels; + exact_channels = false; + try_ss.channels = max_channels ? PA_CHANNELS_MAX : m->split->hw_channels; pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX); } @@ -2416,15 +2421,34 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, NULL, NULL, exact_channels); if (pcm) { - if (!exact_channels) + if (m->split) { + if (try_map.channels < m->split->hw_channels) { + pa_alsa_close(&pcm); + + pa_logl((max_channels ? PA_LOG_WARN : PA_LOG_DEBUG), + "Too few channels in %s for ALSA UCM SplitPCM: avail %d < required %d", + m->device_strings[0], try_map.channels, m->split->hw_channels); + + /* Retry with max channel count, in case ALSA rounded down */ + if (!max_channels) + return mapping_open_pcm(ucm, m, mode, true); + + return NULL; + } else if (try_map.channels > m->split->hw_channels) { + pa_log_debug("Update split PCM channel count for %s: %d -> %d", + m->device_strings[0], m->split->hw_channels, try_map.channels); + m->split->hw_channels = try_map.channels; + } + } else if (!exact_channels) { m->channel_map = try_map; + } mapping_init_eld(m, pcm); } return pcm; } -static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction) +static void pa_alsa_init_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction) { pa_proplist *props = pa_proplist_new(); uint32_t idx; @@ -2445,6 +2469,9 @@ static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props); else pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props); + + /* Update HW channel count to match probed one */ + m->split->hw_channels = leader->split->hw_channels; } pa_proplist_free(props); @@ -2464,7 +2491,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) { if (!m->split) pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); else - pa_alsa_init_proplist_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT); + pa_alsa_init_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT); pa_alsa_close(&m->output_pcm); } @@ -2479,7 +2506,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) { if (!m->split) pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); else - pa_alsa_init_proplist_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT); + pa_alsa_init_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT); pa_alsa_close(&m->input_pcm); } @@ -2536,7 +2563,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (m->split && !m->split->leader) continue; - m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK); + m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK, false); if (!m->output_pcm) { p->supported = false; break; @@ -2554,7 +2581,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (m->split && !m->split->leader) continue; - m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE); + m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE, false); if (!m->input_pcm) { p->supported = false; break; From ae16463c3c7f7d755b471edb3e4c8e051d2569fe Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 13:04:36 +0200 Subject: [PATCH 06/98] spa: acp: be more noisy when UCM profiles fail to be supported Generally ALSA UCM profiles should all work as they're supposed to be device-specific, so be more noisy when the profile fails to be supported due to the PCM device failing to open. Some logging on the probe outcome in failure case also makes spa-acp-tool etc. log output easier to read. --- spa/plugins/alsa/acp/alsa-ucm.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index 77e476eb8..f64c573a4 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -2548,7 +2548,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * pa_log_info("Set ucm verb to %s", verb_name); if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) { - pa_log("Failed to set verb %s", verb_name); + pa_log("Profile '%s': failed to set verb %s", p->name, verb_name); p->supported = false; continue; } @@ -2565,6 +2565,8 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK, false); if (!m->output_pcm) { + pa_log_info("Profile '%s' mapping '%s': output PCM open failed", + p->name, m->name); p->supported = false; break; } @@ -2583,6 +2585,8 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE, false); if (!m->input_pcm) { + pa_log_info("Profile '%s' mapping '%s': input PCM open failed", + p->name, m->name); p->supported = false; break; } @@ -2591,6 +2595,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (!p->supported) { profile_finalize_probing(p); + pa_log_info("Profile %s not supported", p->name); continue; } From 06438cbc9af84faad5507903aae295d6ed855e21 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 16:14:56 +0200 Subject: [PATCH 07/98] spa: acp: make spa-acp-tool debug output easier to read Include log level, file/line numbers, and indent messages for debug levels. --- spa/plugins/alsa/acp-tool.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c index cff9edb80..c12401100 100644 --- a/spa/plugins/alsa/acp-tool.c +++ b/spa/plugins/alsa/acp-tool.c @@ -132,6 +132,18 @@ static ACP_PRINTF_FUNC(6,0) void log_func(void *data, int level, const char *file, int line, const char *func, const char *fmt, va_list arg) { + static const char * const levels[] = { "E", "W", "N", "I", "D", "T" }; + const char *level_str = levels[SPA_CLAMP(level, 0, (int)SPA_N_ELEMENTS(levels) - 1)]; + + if (file) { + const char *p = strrchr(file, '/'); + if (p) + file = p + 1; + } + + fprintf(stderr, "%s %16s:%-5d ", level_str, file ? file : "", line); + while (level-- > 1) + fprintf(stderr, " "); vfprintf(stderr, fmt, arg); fprintf(stderr, "\n"); } From 8d55213288e95eff8f8ac95a7990eee77732e9ce Mon Sep 17 00:00:00 2001 From: Jan Grulich Date: Mon, 10 Mar 2025 13:23:10 +0100 Subject: [PATCH 08/98] stream: don't emit process when disconnecting Commit b160a72018656505e4ae4ec26308177c033da627 introduced this change before, but it was omitted in e1e0a886d524cbd3cbd339c79b4c38514cb11e39. This makes again sure we don't call process callback while disconnecting stream. Fixes #3314 --- src/pipewire/stream.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c index 5e5aed3b7..4af3a3afa 100644 --- a/src/pipewire/stream.c +++ b/src/pipewire/stream.c @@ -446,7 +446,7 @@ static inline void call_process(struct stream *impl) if (impl->n_buffers == 0 || (impl->direction == SPA_DIRECTION_OUTPUT && update_requested(impl) <= 0)) return; - if (impl->rt_callbacks.funcs) + if (impl->rt_callbacks.funcs && !impl->disconnecting) spa_callbacks_call_fast(&impl->rt_callbacks, struct pw_stream_events, process, 0); } From 17eaf83fe80ad7b70ec14b1b92515f8291594f96 Mon Sep 17 00:00:00 2001 From: msizanoen Date: Mon, 10 Mar 2025 20:06:58 +0700 Subject: [PATCH 09/98] systemd: Disable pipewire user services for root The `access(2)` based multi-user mediation mechanism doesn't quite work for the root user, which may cause it to conflict with a running foreground user session. Prevent this by not running the user service at all for the root user, which nobody should be doing anyway. --- src/daemon/systemd/user/pipewire.service.in | 1 + src/daemon/systemd/user/pipewire.socket | 1 + 2 files changed, 2 insertions(+) diff --git a/src/daemon/systemd/user/pipewire.service.in b/src/daemon/systemd/user/pipewire.service.in index b9b137351..4236c6bd4 100644 --- a/src/daemon/systemd/user/pipewire.service.in +++ b/src/daemon/systemd/user/pipewire.service.in @@ -14,6 +14,7 @@ Description=PipeWire Multimedia Service # After=pipewire.socket is not needed, as it is already implicit in the # socket-service relationship, see systemd.socket(5). Requires=pipewire.socket +ConditionUser=!root [Service] LockPersonality=yes diff --git a/src/daemon/systemd/user/pipewire.socket b/src/daemon/systemd/user/pipewire.socket index 16e23a7b6..890342abb 100644 --- a/src/daemon/systemd/user/pipewire.socket +++ b/src/daemon/systemd/user/pipewire.socket @@ -1,5 +1,6 @@ [Unit] Description=PipeWire Multimedia System Sockets +ConditionUser=!root [Socket] Priority=6 From 2397a984f747271f75218ed727e1f6700dd729b4 Mon Sep 17 00:00:00 2001 From: Nikolay Borodin Date: Sun, 9 Mar 2025 18:52:49 +0200 Subject: [PATCH 10/98] spa: alsa: allow building without UMP in libalsa Allow building with older libalsa releases that don't have UMP. --- meson.build | 2 +- spa/meson.build | 2 +- spa/plugins/alsa/alsa-seq.c | 63 ++++++++++++++++++++++++++++++++++++- spa/plugins/alsa/alsa-seq.h | 2 ++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 3dfdc1842..7afffda19 100644 --- a/meson.build +++ b/meson.build @@ -481,7 +481,7 @@ endif summary({'intl support': libintl_dep.found()}, bool_yn: true) need_alsa = get_option('pipewire-alsa').enabled() or 'media-session' in get_option('session-managers') -alsa_dep = dependency('alsa', version : '>=1.2.10', required: need_alsa) +alsa_dep = dependency('alsa', version : '>=1.2.6', required: need_alsa) summary({'pipewire-alsa': alsa_dep.found()}, bool_yn: true) if host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd' diff --git a/spa/meson.build b/spa/meson.build index 9b5f89604..9d0009868 100644 --- a/spa/meson.build +++ b/spa/meson.build @@ -43,7 +43,7 @@ if get_option('spa-plugins').allowed() endif # plugin-specific dependencies - alsa_dep = dependency('alsa', version : '>=1.2.10', required: get_option('alsa')) + alsa_dep = dependency('alsa', version : '>=1.2.6', required: get_option('alsa')) summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend') bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5')) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 2a4ebd2c4..52e869c29 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -37,12 +37,14 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu 0)) < 0) return res; +#ifdef ALSA_UMP if ((res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0)) < 0) { snd_seq_close(conn->hndl); spa_log_info(state->log, "%p: ALSA failed to enable UMP MIDI: %s", state, snd_strerror(res)); return res; } +#endif return 0; } @@ -172,7 +174,11 @@ static void init_ports(struct seq_state *state) } } +#ifdef ALSA_UMP static void debug_event(struct seq_state *state, snd_seq_ump_event_t *ev) +#else +static void debug_event(struct seq_state *state, snd_seq_event_t *ev) +#endif { if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) return; @@ -199,10 +205,18 @@ static void debug_event(struct seq_state *state, snd_seq_ump_event_t *ev) static void alsa_seq_on_sys(struct spa_source *source) { struct seq_state *state = source->data; +#ifdef ALSA_UMP snd_seq_ump_event_t *ev; +#else + snd_seq_event_t *ev; +#endif int res; +#ifdef ALSA_UMP while (snd_seq_ump_event_input(state->sys.hndl, &ev) > 0) { +#else + while (snd_seq_event_input(state->sys.hndl, &ev) > 0) { +#endif const snd_seq_addr_t *addr = &ev->data.addr; if (addr->client == state->event.addr.client) @@ -529,15 +543,27 @@ static int process_recycle(struct seq_state *state) static int process_read(struct seq_state *state) { +#ifdef ALSA_UMP snd_seq_ump_event_t *ev; +#else + snd_seq_event_t *ev; +#endif struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; uint32_t i; +#ifdef ALSA_UMP uint32_t *data; +#else + uint8_t data[MAX_EVENT_SIZE]; +#endif long size; - int res; + int res = -1; /* copy all new midi events into their port buffers */ +#ifdef ALSA_UMP while ((res = snd_seq_ump_event_input(state->event.hndl, &ev)) > 0) { +#else + while (snd_seq_event_input(state->event.hndl, &ev) > 0) { +#endif const snd_seq_addr_t *addr = &ev->source; struct seq_port *port; uint64_t ev_time, diff; @@ -559,8 +585,16 @@ static int process_read(struct seq_state *state) continue; } +#ifdef ALSA_UMP data = (uint32_t*)&ev->ump[0]; size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; +#else + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, data, MAX_EVENT_SIZE, ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } +#endif /* queue_time is the estimated current time of the queue as calculated by * the DLL. Calculate the age of the event. */ @@ -661,7 +695,11 @@ static int process_write(struct seq_state *state) struct spa_pod_sequence *pod; struct spa_data *d; struct spa_pod_control *c; +#ifdef ALSA_UMP snd_seq_ump_event_t ev; +#else + snd_seq_event_t ev; +#endif uint64_t out_time; snd_seq_real_time_t out_rt; @@ -688,6 +726,7 @@ static int process_write(struct seq_state *state) SPA_POD_SEQUENCE_FOREACH(pod, c) { size_t body_size; +#ifdef ALSA_UMP uint8_t *body; if (c->type != SPA_CONTROL_UMP) @@ -698,6 +737,21 @@ static int process_write(struct seq_state *state) spa_zero(ev); memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); +#else + if (c->type != SPA_CONTROL_Midi) + continue; + + snd_seq_ev_clear(&ev); + + snd_midi_event_reset_encode(stream->codec); + if ((body_size = snd_midi_event_encode(stream->codec, + SPA_POD_BODY(&c->value), + SPA_POD_BODY_SIZE(&c->value), &ev)) <= 0) { + spa_log_warn(state->log, "failed to encode event: %s", + snd_strerror(body_size)); + continue; + } +#endif snd_seq_ev_set_source(&ev, state->event.addr.port); snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); @@ -711,10 +765,17 @@ static int process_write(struct seq_state *state) spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%zd port:%d.%d", ev.type, out_time, c->offset, body_size, port->addr.client, port->addr.port); +#ifdef ALSA_UMP if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); } +#else + if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } +#endif } } snd_seq_drain_output(state->event.hndl); diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 0f2c192c3..906f3c813 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -13,7 +13,9 @@ extern "C" { #include #include +#ifdef ALSA_UMP #include +#endif #include #include From 2167945eff99c8042cff42221c2603904ef96068 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 19:38:01 +0200 Subject: [PATCH 11/98] spa: alsa: support also MIDI-1.0 IO for ALSA seq Support also non-UMP IO with ALSA seq, in case either alsa-lib or the kernel does not have UMP enabled. Add configuration option "api.alsa.seq.ump" for optionally turning UMP I/O off, for easier debugging. --- spa/plugins/alsa/alsa-seq-bridge.c | 4 + spa/plugins/alsa/alsa-seq.c | 243 +++++++++++++++++++---------- spa/plugins/alsa/alsa-seq.h | 1 + 3 files changed, 163 insertions(+), 85 deletions(-) diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c index f85b41e28..7ec39321c 100644 --- a/spa/plugins/alsa/alsa-seq-bridge.c +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -931,6 +931,7 @@ impl_init(const struct spa_handle_factory *factory, this->quantum_limit = 8192; this->min_pool_size = 500; this->max_pool_size = 2000; + this->ump = true; for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; @@ -949,6 +950,8 @@ impl_init(const struct spa_handle_factory *factory, spa_atou32(s, &this->min_pool_size, 0); } else if (spa_streq(k, "api.alsa.seq.max-pool")) { spa_atou32(s, &this->max_pool_size, 0); + } else if (spa_streq(k, "api.alsa.seq.ump")) { + this->ump = spa_atob(s); } } @@ -992,6 +995,7 @@ static const struct spa_dict_item info_items[] = { "["SPA_KEY_API_ALSA_DISABLE_LONGNAME"=] " "[ api.alsa.seq.min-pool=] " "[ api.alsa.seq.max-pool=]" + "[ api.alsa.seq.ump = ]" }, }; diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 52e869c29..8ca7405d8 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -24,7 +24,7 @@ #define CHECK(s,msg,...) if ((res = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(res)); return res; } -static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue) +static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue, bool probe_ump) { struct props *props = &state->props; int res; @@ -37,14 +37,29 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu 0)) < 0) return res; -#ifdef ALSA_UMP - if ((res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0)) < 0) { - snd_seq_close(conn->hndl); - spa_log_info(state->log, "%p: ALSA failed to enable UMP MIDI: %s", - state, snd_strerror(res)); - return res; + if (!state->ump) { + spa_log_info(state->log, "%p: ALSA UMP MIDI disabled", state); + return 0; } + +#ifdef ALSA_UMP + res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0); +#else + res = -EOPNOTSUPP; #endif + if (res < 0) { + spa_log_lev(state->log, (probe_ump ? SPA_LOG_LEVEL_INFO : SPA_LOG_LEVEL_ERROR), + "%p: ALSA failed to enable UMP MIDI: %s", state, snd_strerror(res)); + if (!probe_ump) { + snd_seq_close(conn->hndl); + return res; /* either all are UMP or none are UMP */ + } + + state->ump = false; + } else { + spa_log_debug(state->log, "%p: ALSA UMP MIDI enabled", state); + state->ump = true; + } return 0; } @@ -174,11 +189,7 @@ static void init_ports(struct seq_state *state) } } -#ifdef ALSA_UMP -static void debug_event(struct seq_state *state, snd_seq_ump_event_t *ev) -#else static void debug_event(struct seq_state *state, snd_seq_event_t *ev) -#endif { if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) return; @@ -205,19 +216,33 @@ static void debug_event(struct seq_state *state, snd_seq_event_t *ev) static void alsa_seq_on_sys(struct spa_source *source) { struct seq_state *state = source->data; -#ifdef ALSA_UMP - snd_seq_ump_event_t *ev; -#else snd_seq_event_t *ev; -#endif int res; -#ifdef ALSA_UMP - while (snd_seq_ump_event_input(state->sys.hndl, &ev) > 0) { + while (1) { + const snd_seq_addr_t *addr; + + if (state->ump) { +#if ALSA_UMP + snd_seq_ump_event_t *ump_ev; + + res = snd_seq_ump_event_input(state->sys.hndl, &ump_ev); + if (res <= 0) + break; + + /* Structures are layout-compatible */ + ev = (snd_seq_event_t *)ump_ev; + addr = &ump_ev->data.addr; #else - while (snd_seq_event_input(state->sys.hndl, &ev) > 0) { + spa_assert_not_reached(); #endif - const snd_seq_addr_t *addr = &ev->data.addr; + } else { + res = snd_seq_event_input(state->sys.hndl, &ev); + if (res <= 0) + break; + + addr = &ev->data.addr; + } if (addr->client == state->event.addr.client) continue; @@ -283,8 +308,8 @@ int spa_alsa_seq_open(struct seq_state *state) spa_zero(reserve); for (i = 0; i < 16; i++) { - spa_log_debug(state->log, "close %d", i); - if ((res = seq_open(state, &reserve[i], false)) < 0) + spa_log_debug(state->log, "open %d", i); + if ((res = seq_open(state, &reserve[i], false, (i == 0))) < 0) break; } if (i >= 2) { @@ -543,31 +568,47 @@ static int process_recycle(struct seq_state *state) static int process_read(struct seq_state *state) { -#ifdef ALSA_UMP - snd_seq_ump_event_t *ev; -#else snd_seq_event_t *ev; -#endif struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; uint32_t i; -#ifdef ALSA_UMP uint32_t *data; -#else - uint8_t data[MAX_EVENT_SIZE]; -#endif + uint8_t midi1_data[MAX_EVENT_SIZE]; + uint32_t ump_data[MAX_EVENT_SIZE]; long size; int res = -1; /* copy all new midi events into their port buffers */ -#ifdef ALSA_UMP - while ((res = snd_seq_ump_event_input(state->event.hndl, &ev)) > 0) { -#else - while (snd_seq_event_input(state->event.hndl, &ev) > 0) { -#endif - const snd_seq_addr_t *addr = &ev->source; + while (1) { + const snd_seq_addr_t *addr; struct seq_port *port; uint64_t ev_time, diff; uint32_t offset; +#ifdef ALSA_UMP + snd_seq_ump_event_t *ump_ev; +#endif + uint8_t *midi1_ptr; + size_t midi1_size; + uint64_t ump_state = 0; + + if (state->ump) { +#ifdef ALSA_UMP + res = snd_seq_ump_event_input(state->event.hndl, &ump_ev); + if (res <= 0) + break; + + /* Structures are layout-compatible */ + ev = (snd_seq_event_t *)ump_ev; + addr = &ump_ev->source; +#else + spa_assert_not_reached(); +#endif + } else { + res = snd_seq_event_input(state->event.hndl, &ev); + if (res <= 0) + break; + + addr = &ev->source; + } debug_event(state, ev); @@ -585,16 +626,23 @@ static int process_read(struct seq_state *state) continue; } + if (state->ump) { #ifdef ALSA_UMP - data = (uint32_t*)&ev->ump[0]; - size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; + data = (uint32_t*)&ump_ev->ump[0]; + size = spa_ump_message_size(snd_ump_msg_hdr_type(ump_ev->ump[0])) * 4; #else - snd_midi_event_reset_decode(stream->codec); - if ((size = snd_midi_event_decode(stream->codec, data, MAX_EVENT_SIZE, ev)) < 0) { - spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); - continue; - } + spa_assert_not_reached(); #endif + } else { + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } + + midi1_ptr = midi1_data; + midi1_size = size; + } /* queue_time is the estimated current time of the queue as calculated by * the DLL. Calculate the age of the event. */ @@ -611,19 +659,32 @@ static int process_read(struct seq_state *state) else offset = 0; - spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", - ev->type, ev_time, offset, size, addr->client, addr->port); + do { + if (!state->ump) { + data = ump_data; + size = spa_ump_from_midi(&midi1_ptr, &midi1_size, + ump_data, sizeof(ump_data), 0, &ump_state); + if (size <= 0) + break; + } - spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&port->builder, data, size); + spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", + ev->type, ev_time, offset, size, addr->client, addr->port); - /* make sure we can fit at least one control event of max size otherwise - * we keep the event in the queue and try to copy it in the next cycle */ - if (port->builder.state.offset + - sizeof(struct spa_pod_control) + - MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) - break; + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&port->builder, data, size); + + /* make sure we can fit at least one control event of max size otherwise + * we keep the event in the queue and try to copy it in the next cycle */ + if (port->builder.state.offset + + sizeof(struct spa_pod_control) + + MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) + goto done; + + } while (!state->ump); } + +done: if (res < 0 && res != -EAGAIN) spa_log_warn(state->log, "event read failed: %s", snd_strerror(res)); @@ -695,11 +756,6 @@ static int process_write(struct seq_state *state) struct spa_pod_sequence *pod; struct spa_data *d; struct spa_pod_control *c; -#ifdef ALSA_UMP - snd_seq_ump_event_t ev; -#else - snd_seq_event_t ev; -#endif uint64_t out_time; snd_seq_real_time_t out_rt; @@ -725,8 +781,12 @@ static int process_write(struct seq_state *state) } SPA_POD_SEQUENCE_FOREACH(pod, c) { - size_t body_size; + snd_seq_event_t *ev; + snd_seq_event_t midi1_ev; #ifdef ALSA_UMP + snd_seq_ump_event_t ump_ev; +#endif + size_t body_size; uint8_t *body; if (c->type != SPA_CONTROL_UMP) @@ -734,48 +794,61 @@ static int process_write(struct seq_state *state) body = SPA_POD_BODY(&c->value); body_size = SPA_POD_BODY_SIZE(&c->value); - spa_zero(ev); - memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); + if (state->ump) { +#ifdef ALSA_UMP + spa_zero(ump_ev); + memcpy(ump_ev.ump, body, SPA_MIN(sizeof(ump_ev.ump), (size_t)body_size)); + ev = (snd_seq_event_t *)&ump_ev; #else - if (c->type != SPA_CONTROL_Midi) - continue; - - snd_seq_ev_clear(&ev); - - snd_midi_event_reset_encode(stream->codec); - if ((body_size = snd_midi_event_encode(stream->codec, - SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), &ev)) <= 0) { - spa_log_warn(state->log, "failed to encode event: %s", - snd_strerror(body_size)); - continue; - } + spa_assert_not_reached(); #endif + } else { + uint8_t data[MAX_EVENT_SIZE]; + int size; - snd_seq_ev_set_source(&ev, state->event.addr.port); - snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) + continue; + + snd_seq_ev_clear(&midi1_ev); + + snd_midi_event_reset_encode(stream->codec); + if ((size = snd_midi_event_encode(stream->codec, data, size, &midi1_ev)) <= 0) { + spa_log_warn(state->log, "failed to encode event: %s", snd_strerror(size)); + continue; + } + + ev = &midi1_ev; + body_size = size; + } + + snd_seq_ev_set_source(ev, state->event.addr.port); + snd_seq_ev_set_dest(ev, port->addr.client, port->addr.port); out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset); out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC; out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC; - snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + snd_seq_ev_schedule_real(ev, state->event.queue_id, 0, &out_rt); spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%zd port:%d.%d", - ev.type, out_time, c->offset, body_size, port->addr.client, port->addr.port); + ev->type, out_time, c->offset, body_size, port->addr.client, port->addr.port); + if (state->ump) { #ifdef ALSA_UMP - if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { - spa_log_warn(state->log, "failed to output event: %s", - snd_strerror(err)); - } + if ((err = snd_seq_ump_event_output(state->event.hndl, &ump_ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } #else - if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) { - spa_log_warn(state->log, "failed to output event: %s", - snd_strerror(err)); - } + spa_assert_not_reached(); #endif + } else { + if ((err = snd_seq_event_output(state->event.hndl, ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } + } } } snd_seq_drain_output(state->event.hndl); diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 906f3c813..0546459db 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -155,6 +155,7 @@ struct seq_state { unsigned int opened:1; unsigned int started:1; unsigned int following:1; + unsigned int ump:1; struct seq_stream streams[2]; From 4f077e1f275189d1e70d8ecca89f9cebae514c55 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 20:33:39 +0200 Subject: [PATCH 12/98] spa: fix ALSA UMP support detection in meson --- spa/meson.build | 4 ++++ spa/plugins/alsa/alsa-seq.c | 16 ++++++++-------- spa/plugins/alsa/alsa-seq.h | 4 +++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/spa/meson.build b/spa/meson.build index 9d0009868..9faaae48a 100644 --- a/spa/meson.build +++ b/spa/meson.build @@ -46,6 +46,10 @@ if get_option('spa-plugins').allowed() alsa_dep = dependency('alsa', version : '>=1.2.6', required: get_option('alsa')) summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend') + if alsa_dep.version().version_compare('>=1.2.10') + cdata.set('HAVE_ALSA_UMP', true) + endif + bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5')) bluez_gio_dep = dependency('gio-2.0', required : get_option('bluez5')) bluez_gio_unix_dep = dependency('gio-unix-2.0', required : get_option('bluez5')) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 8ca7405d8..fcf6dd09a 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -42,7 +42,7 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu return 0; } -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0); #else res = -EOPNOTSUPP; @@ -223,7 +223,7 @@ static void alsa_seq_on_sys(struct spa_source *source) const snd_seq_addr_t *addr; if (state->ump) { -#if ALSA_UMP +#ifdef HAVE_ALSA_UMP snd_seq_ump_event_t *ump_ev; res = snd_seq_ump_event_input(state->sys.hndl, &ump_ev); @@ -583,7 +583,7 @@ static int process_read(struct seq_state *state) struct seq_port *port; uint64_t ev_time, diff; uint32_t offset; -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP snd_seq_ump_event_t *ump_ev; #endif uint8_t *midi1_ptr; @@ -591,7 +591,7 @@ static int process_read(struct seq_state *state) uint64_t ump_state = 0; if (state->ump) { -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP res = snd_seq_ump_event_input(state->event.hndl, &ump_ev); if (res <= 0) break; @@ -627,7 +627,7 @@ static int process_read(struct seq_state *state) } if (state->ump) { -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP data = (uint32_t*)&ump_ev->ump[0]; size = spa_ump_message_size(snd_ump_msg_hdr_type(ump_ev->ump[0])) * 4; #else @@ -783,7 +783,7 @@ static int process_write(struct seq_state *state) SPA_POD_SEQUENCE_FOREACH(pod, c) { snd_seq_event_t *ev; snd_seq_event_t midi1_ev; -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP snd_seq_ump_event_t ump_ev; #endif size_t body_size; @@ -796,7 +796,7 @@ static int process_write(struct seq_state *state) body_size = SPA_POD_BODY_SIZE(&c->value); if (state->ump) { -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP spa_zero(ump_ev); memcpy(ump_ev.ump, body, SPA_MIN(sizeof(ump_ev.ump), (size_t)body_size)); ev = (snd_seq_event_t *)&ump_ev; @@ -835,7 +835,7 @@ static int process_write(struct seq_state *state) ev->type, out_time, c->offset, body_size, port->addr.client, port->addr.port); if (state->ump) { -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP if ((err = snd_seq_ump_event_output(state->event.hndl, &ump_ev)) < 0) { spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 0546459db..274311c70 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -12,8 +12,10 @@ extern "C" { #include #include +#include "config.h" + #include -#ifdef ALSA_UMP +#ifdef HAVE_ALSA_UMP #include #endif From f7b1ba2a406deb1de3ee2550a60437b8eaef61f9 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 9 Mar 2025 21:24:47 +0200 Subject: [PATCH 13/98] spa: avoid casting between snd_seq_event_t & snd_seq_ump_event_t Although the two structs have same initial sequence, it's not really correct to cast between their pointers. Alsa-lib also does this only internally, but not in API. --- spa/plugins/alsa/alsa-seq.c | 209 +++++++++++++++++++++--------------- 1 file changed, 122 insertions(+), 87 deletions(-) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index fcf6dd09a..3e6f5c29f 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -213,43 +213,74 @@ static void debug_event(struct seq_state *state, snd_seq_event_t *ev) ev->queue); } +#ifdef HAVE_ALSA_UMP +static void debug_ump_event(struct seq_state *state, snd_seq_ump_event_t *ev) +{ + if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) + return; + + spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags); + switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) { + case SND_SEQ_TIME_STAMP_TICK: + spa_log_trace(state->log, " time: %d ticks", ev->time.tick); + break; + case SND_SEQ_TIME_STAMP_REAL: + spa_log_trace(state->log, " time = %d.%09d", + (int)ev->time.time.tv_sec, + (int)ev->time.time.tv_nsec); + break; + } + spa_log_trace(state->log, " source:%d.%d dest:%d.%d queue:%d", + ev->source.client, + ev->source.port, + ev->dest.client, + ev->dest.port, + ev->queue); +} +#endif + static void alsa_seq_on_sys(struct spa_source *source) { struct seq_state *state = source->data; - snd_seq_event_t *ev; + const bool ump = state->ump; int res; while (1) { const snd_seq_addr_t *addr; + snd_seq_event_type_t type; - if (state->ump) { + if (ump) { #ifdef HAVE_ALSA_UMP - snd_seq_ump_event_t *ump_ev; + snd_seq_ump_event_t *ev; - res = snd_seq_ump_event_input(state->sys.hndl, &ump_ev); + res = snd_seq_ump_event_input(state->sys.hndl, &ev); if (res <= 0) break; - /* Structures are layout-compatible */ - ev = (snd_seq_event_t *)ump_ev; - addr = &ump_ev->data.addr; + debug_ump_event(state, ev); + + addr = &ev->data.addr; + type = ev->type; #else spa_assert_not_reached(); #endif } else { + snd_seq_event_t *ev; + res = snd_seq_event_input(state->sys.hndl, &ev); if (res <= 0) break; + debug_event(state, ev); + addr = &ev->data.addr; + type = ev->type; } if (addr->client == state->event.addr.client) continue; - debug_event(state, ev); - - switch (ev->type) { + switch (type) { case SND_SEQ_EVENT_CLIENT_START: case SND_SEQ_EVENT_CLIENT_CHANGE: spa_log_info(state->log, "client add/change %d", addr->client); @@ -283,7 +314,7 @@ static void alsa_seq_on_sys(struct spa_source *source) break; default: spa_log_info(state->log, "unhandled event %d: %d:%d", - ev->type, addr->client, addr->port); + type, addr->client, addr->port); break; } @@ -568,8 +599,8 @@ static int process_recycle(struct seq_state *state) static int process_read(struct seq_state *state) { - snd_seq_event_t *ev; struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; + const bool ump = state->ump; uint32_t i; uint32_t *data; uint8_t midi1_data[MAX_EVENT_SIZE]; @@ -583,34 +614,43 @@ static int process_read(struct seq_state *state) struct seq_port *port; uint64_t ev_time, diff; uint32_t offset; -#ifdef HAVE_ALSA_UMP - snd_seq_ump_event_t *ump_ev; -#endif + void *event; uint8_t *midi1_ptr; size_t midi1_size; uint64_t ump_state = 0; + snd_seq_event_type_t SPA_UNUSED type; - if (state->ump) { + if (ump) { #ifdef HAVE_ALSA_UMP - res = snd_seq_ump_event_input(state->event.hndl, &ump_ev); + snd_seq_ump_event_t *ev; + + res = snd_seq_ump_event_input(state->event.hndl, &ev); if (res <= 0) break; - /* Structures are layout-compatible */ - ev = (snd_seq_event_t *)ump_ev; - addr = &ump_ev->source; + debug_ump_event(state, ev); + + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; #else spa_assert_not_reached(); #endif } else { + snd_seq_event_t *ev; + res = snd_seq_event_input(state->event.hndl, &ev); if (res <= 0) break; - addr = &ev->source; - } + debug_event(state, ev); - debug_event(state, ev); + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; + } if ((port = find_port(state, stream, addr)) == NULL) { spa_log_debug(state->log, "unknown port %d.%d", @@ -626,27 +666,8 @@ static int process_read(struct seq_state *state) continue; } - if (state->ump) { -#ifdef HAVE_ALSA_UMP - data = (uint32_t*)&ump_ev->ump[0]; - size = spa_ump_message_size(snd_ump_msg_hdr_type(ump_ev->ump[0])) * 4; -#else - spa_assert_not_reached(); -#endif - } else { - snd_midi_event_reset_decode(stream->codec); - if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { - spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); - continue; - } - - midi1_ptr = midi1_data; - midi1_size = size; - } - /* queue_time is the estimated current time of the queue as calculated by * the DLL. Calculate the age of the event. */ - ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); if (state->queue_time > ev_time) diff = state->queue_time - ev_time; else @@ -659,8 +680,30 @@ static int process_read(struct seq_state *state) else offset = 0; + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev = event; + + data = (uint32_t*)&ev->ump[0]; + size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev = event; + + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } + + midi1_ptr = midi1_data; + midi1_size = size; + } + do { - if (!state->ump) { + if (!ump) { data = ump_data; size = spa_ump_from_midi(&midi1_ptr, &midi1_size, ump_data, sizeof(ump_data), 0, &ump_state); @@ -669,7 +712,7 @@ static int process_read(struct seq_state *state) } spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", - ev->type, ev_time, offset, size, addr->client, addr->port); + type, ev_time, offset, size, addr->client, addr->port); spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); spa_pod_builder_bytes(&port->builder, data, size); @@ -681,7 +724,7 @@ static int process_read(struct seq_state *state) MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) goto done; - } while (!state->ump); + } while (!ump); } done: @@ -746,6 +789,7 @@ done: static int process_write(struct seq_state *state) { struct seq_stream *stream = &state->streams[SPA_DIRECTION_INPUT]; + const bool ump = state->ump; uint32_t i; int err, res = 0; @@ -781,11 +825,6 @@ static int process_write(struct seq_state *state) } SPA_POD_SEQUENCE_FOREACH(pod, c) { - snd_seq_event_t *ev; - snd_seq_event_t midi1_ev; -#ifdef HAVE_ALSA_UMP - snd_seq_ump_event_t ump_ev; -#endif size_t body_size; uint8_t *body; @@ -795,48 +834,25 @@ static int process_write(struct seq_state *state) body = SPA_POD_BODY(&c->value); body_size = SPA_POD_BODY_SIZE(&c->value); - if (state->ump) { -#ifdef HAVE_ALSA_UMP - spa_zero(ump_ev); - memcpy(ump_ev.ump, body, SPA_MIN(sizeof(ump_ev.ump), (size_t)body_size)); - ev = (snd_seq_event_t *)&ump_ev; -#else - spa_assert_not_reached(); -#endif - } else { - uint8_t data[MAX_EVENT_SIZE]; - int size; - - if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) - continue; - - snd_seq_ev_clear(&midi1_ev); - - snd_midi_event_reset_encode(stream->codec); - if ((size = snd_midi_event_encode(stream->codec, data, size, &midi1_ev)) <= 0) { - spa_log_warn(state->log, "failed to encode event: %s", snd_strerror(size)); - continue; - } - - ev = &midi1_ev; - body_size = size; - } - - snd_seq_ev_set_source(ev, state->event.addr.port); - snd_seq_ev_set_dest(ev, port->addr.client, port->addr.port); - out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset); - out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC; out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC; - snd_seq_ev_schedule_real(ev, state->event.queue_id, 0, &out_rt); - spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%zd port:%d.%d", - ev->type, out_time, c->offset, body_size, port->addr.client, port->addr.port); + spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%zd port:%d.%d", + out_time, c->offset, body_size, port->addr.client, port->addr.port); - if (state->ump) { + if (ump) { #ifdef HAVE_ALSA_UMP - if ((err = snd_seq_ump_event_output(state->event.hndl, &ump_ev)) < 0) { + snd_seq_ump_event_t ev; + + spa_zero(ev); + memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); + + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); } @@ -844,7 +860,26 @@ static int process_write(struct seq_state *state) spa_assert_not_reached(); #endif } else { - if ((err = snd_seq_event_output(state->event.hndl, ev)) < 0) { + snd_seq_event_t ev; + uint8_t data[MAX_EVENT_SIZE]; + int size; + + if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) + continue; + + snd_seq_ev_clear(&ev); + + snd_midi_event_reset_encode(stream->codec); + if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) <= 0) { + spa_log_warn(state->log, "failed to encode event: %s", snd_strerror(size)); + continue; + } + + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) { spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); } From edffc87ebf179002d1573af02b3bd069e919e514 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 11:50:48 +0100 Subject: [PATCH 14/98] audioconvert: configure resample channels correctly Depending on the direction of the conversion, we run the resampler before or after the channelmix. This means we need to use the channel count before or after the channelmixer instead of always using the channels after channelmixing. Fixes #4595 --- spa/plugins/audioconvert/audioconvert.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index 47859e654..9726842cd 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -1971,13 +1971,19 @@ static int setup_resample(struct impl *this) struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; int res; + uint32_t channels; + + if (this->direction == SPA_DIRECTION_INPUT) + channels = in->format.info.raw.channels; + else + channels = out->format.info.raw.channels; spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this, spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), - out->format.info.raw.channels, + channels, in->format.info.raw.rate, spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), - out->format.info.raw.channels, + channels, out->format.info.raw.rate); if (this->props.resample_disabled && !this->resample_peaks && @@ -1987,7 +1993,7 @@ static int setup_resample(struct impl *this) if (this->resample.free) resample_free(&this->resample); - this->resample.channels = out->format.info.raw.channels; + this->resample.channels = channels; this->resample.i_rate = in->format.info.raw.rate; this->resample.o_rate = out->format.info.raw.rate; this->resample.log = this->log; From f4417f4e2c57d52d9fd8356bef4f472d558a1dcf Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 11:52:52 +0100 Subject: [PATCH 15/98] audioconvert: remove some unused fields --- spa/plugins/audioconvert/audioconvert.c | 31 +++++-------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index 9726842cd..a244294e6 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -210,7 +210,6 @@ struct stage_context { uint32_t src_idx; uint32_t dst_idx; uint32_t final_idx; - uint32_t n_datas; struct port *ctrlport; }; @@ -219,8 +218,6 @@ struct stage { bool passthrough; uint32_t in_idx; uint32_t out_idx; - uint32_t n_in; - uint32_t n_out; void *data; void (*run) (struct stage *stage, struct stage_context *c); }; @@ -3223,8 +3220,6 @@ static void add_wav_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->src_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_wav_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3236,7 +3231,7 @@ static void run_dst_remap_stage(struct stage *s, struct stage_context *c) struct impl *impl = s->impl; struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT]; uint32_t i; - for (i = 0; i < s->n_in; i++) { + for (i = 0; i < dir->conv.n_channels; i++) { c->datas[s->out_idx][i] = c->datas[s->in_idx][dir->remap[i]]; spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]); } @@ -3248,8 +3243,6 @@ static void add_dst_remap_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->dst_idx; s->out_idx = CTX_DATA_REMAP_DST; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_dst_remap_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3275,8 +3268,6 @@ static void add_src_remap_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = CTX_DATA_REMAP_SRC; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_src_remap_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3310,8 +3301,6 @@ static void add_src_convert_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_src_convert_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3340,8 +3329,6 @@ static void add_resample_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_resample_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3389,8 +3376,6 @@ static void add_filter_stage(struct impl *impl, uint32_t i, struct filter_graph s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = fg; s->run = run_filter_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3405,8 +3390,6 @@ static void add_channelmix_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_channelmix_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3440,8 +3423,6 @@ static void add_dst_convert_stage(struct impl *impl, struct stage_context *ctx) s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->final_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_dst_convert_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3834,14 +3815,14 @@ static int impl_node_process(void *object) ctx.in_samples = n_samples; ctx.n_samples = n_samples; ctx.n_out = n_out; - ctx.src_idx = CTX_DATA_SRC; - ctx.dst_idx = CTX_DATA_DST; - ctx.final_idx = CTX_DATA_DST; - ctx.n_datas = dir->conv.n_channels; ctx.ctrlport = ctrlport; - if (this->recalc) + if (SPA_UNLIKELY(this->recalc)) { + ctx.src_idx = CTX_DATA_SRC; + ctx.dst_idx = CTX_DATA_DST; + ctx.final_idx = CTX_DATA_DST; recalc_stages(this, &ctx); + } for (i = 0; i < this->n_stages; i++) { struct stage *s = &this->stages[i]; From 15e200846a99d89f4fbd2f3859072536fdea265b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 12:32:28 +0100 Subject: [PATCH 16/98] alsa-seq: avoid uninitialized warning --- spa/plugins/alsa/alsa-seq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 3e6f5c29f..42b59cc00 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -616,7 +616,7 @@ static int process_read(struct seq_state *state) uint32_t offset; void *event; uint8_t *midi1_ptr; - size_t midi1_size; + size_t midi1_size = 0; uint64_t ump_state = 0; snd_seq_event_type_t SPA_UNUSED type; From b4d88143da8f7c1c70ffe89f70b9102513909c38 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 13:29:38 +0100 Subject: [PATCH 17/98] alsa-seq: fix UMP output We need to set the UMP flag, which is done with snd_seq_ev_set_ump_data() to make it output the midi data. --- spa/plugins/alsa/alsa-seq.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 42b59cc00..4f64ee5ba 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -845,9 +845,8 @@ static int process_write(struct seq_state *state) #ifdef HAVE_ALSA_UMP snd_seq_ump_event_t ev; - spa_zero(ev); - memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); - + snd_seq_ump_ev_clear(&ev); + snd_seq_ev_set_ump_data(&ev, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); snd_seq_ev_set_source(&ev, state->event.addr.port); snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); From 9696e2c62b56779a74ceb07d3bf20ed3f006748e Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 16:34:28 +0100 Subject: [PATCH 18/98] loop: remove return from function returning void Fixes #4603 --- src/pipewire/loop.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipewire/loop.h b/src/pipewire/loop.h index 9de1cbf3f..8bb762170 100644 --- a/src/pipewire/loop.h +++ b/src/pipewire/loop.h @@ -141,7 +141,7 @@ pw_loop_add_signal(struct pw_loop *object, int signal_number, PW_API_LOOP_IMPL void pw_loop_destroy_source(struct pw_loop *object, struct spa_source *source) { - return spa_loop_utils_destroy_source(object->utils, source); + spa_loop_utils_destroy_source(object->utils, source); } /** From e16c184228da9d2eb86f035f9415a1dcb59be413 Mon Sep 17 00:00:00 2001 From: sunyuechi Date: Thu, 13 Mar 2025 01:01:19 +0800 Subject: [PATCH 19/98] Add x86 CPU check in meson.build for clang build fix have_avx = cc.has_argument(avx_args) gcc generates an error when AVX is unsupported, whereas clang only produces a warning. Thus, using Clang on the RISC-V platform incorrectly enables AVX file compilation, leading to build failures. --- meson.build | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/meson.build b/meson.build index 7afffda19..d568e9d8b 100644 --- a/meson.build +++ b/meson.build @@ -127,21 +127,30 @@ if have_cpp add_project_arguments(cxx.get_supported_arguments(cxx_flags), language: 'cpp') endif -sse_args = '-msse' -sse2_args = '-msse2' -ssse3_args = '-mssse3' -sse41_args = '-msse4.1' -fma_args = '-mfma' -avx_args = '-mavx' -avx2_args = '-mavx2' +have_sse = false +have_sse2 = false +have_ssse3 = false +have_sse41 = false +have_fma = false +have_avx = false +have_avx2 = false +if host_machine.cpu_family() in ['x86', 'x86_64'] + sse_args = '-msse' + sse2_args = '-msse2' + ssse3_args = '-mssse3' + sse41_args = '-msse4.1' + fma_args = '-mfma' + avx_args = '-mavx' + avx2_args = '-mavx2' -have_sse = cc.has_argument(sse_args) -have_sse2 = cc.has_argument(sse2_args) -have_ssse3 = cc.has_argument(ssse3_args) -have_sse41 = cc.has_argument(sse41_args) -have_fma = cc.has_argument(fma_args) -have_avx = cc.has_argument(avx_args) -have_avx2 = cc.has_argument(avx2_args) + have_sse = cc.has_argument(sse_args) + have_sse2 = cc.has_argument(sse2_args) + have_ssse3 = cc.has_argument(ssse3_args) + have_sse41 = cc.has_argument(sse41_args) + have_fma = cc.has_argument(fma_args) + have_avx = cc.has_argument(avx_args) + have_avx2 = cc.has_argument(avx2_args) +endif have_neon = false if host_machine.cpu_family() == 'aarch64' From ba3a36b3d1ed57ae6a022050e7a7b2199a693e36 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Wed, 12 Mar 2025 18:40:14 +0200 Subject: [PATCH 20/98] spa: acp: allow also too few channels in SplitPCM configs GoXLR Mini has different numbers of channels actually available (21, 23, or 25) depending on its firmware/etc, but its UCM profile specifies always 23. The count can then be bigger or smaller than what is actually available. Fail a bit more gracefully in the case of too few channels: create all the split devices specified by the profile. The channels that aren't actually available in HW just won't get routed anywhere. ALSA upstream IIUC is saying that the channel counts should be fixed, so spew warnings that say the UCM profiles are wrong if they look wrong. --- spa/plugins/alsa/acp/alsa-ucm.c | 45 +++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index f64c573a4..41e0a1874 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -353,6 +353,15 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd const char *device_name; int i; uint32_t hw_channels; + const char *pcm_name; + const char *rule_name; + + if (spa_streq(prefix, "Playback")) + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK); + else + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (!pcm_name) + pcm_name = ""; device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); if (!device_name) @@ -373,15 +382,20 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd break; if (idx >= hw_channels) - goto fail; + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannel%d=%d >= %sChannels=%d", + pcm_name, device_name, prefix, i, idx, prefix, hw_channels); value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name); - if (!value) + if (!value) { + rule_name = "ChannelPos"; goto fail; + } map = snd_pcm_chmap_parse_string(value); - if (!map) + if (!map) { + rule_name = "ChannelPos value"; goto fail; + } if (map->channels == 1) { pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)", @@ -391,6 +405,7 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd free(map); } else { free(map); + rule_name = "channel map parsing"; goto fail; } } @@ -405,7 +420,7 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd return split; fail: - pa_log_warn("Invalid SplitPCM ALSA UCM rule for device %s", device_name); + pa_log_warn("Invalid SplitPCM ALSA UCM %s for device %s (%s)", rule_name, pcm_name, device_name); pa_xfree(split); return NULL; } @@ -2422,21 +2437,25 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, if (pcm) { if (m->split) { - if (try_map.channels < m->split->hw_channels) { - pa_alsa_close(&pcm); + const char *mode_name = mode == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture"; - pa_logl((max_channels ? PA_LOG_WARN : PA_LOG_DEBUG), - "Too few channels in %s for ALSA UCM SplitPCM: avail %d < required %d", - m->device_strings[0], try_map.channels, m->split->hw_channels); + if (try_map.channels < m->split->hw_channels) { + pa_logl((max_channels ? PA_LOG_NOTICE : PA_LOG_DEBUG), + "Error in ALSA UCM profile for %s (%s): %sChannels=%d > avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); /* Retry with max channel count, in case ALSA rounded down */ - if (!max_channels) + if (!max_channels) { + pa_alsa_close(&pcm); return mapping_open_pcm(ucm, m, mode, true); + } - return NULL; + /* Just accept whatever we got... Some of the routings won't get connected + * anywhere */ + m->split->hw_channels = try_map.channels; } else if (try_map.channels > m->split->hw_channels) { - pa_log_debug("Update split PCM channel count for %s: %d -> %d", - m->device_strings[0], m->split->hw_channels, try_map.channels); + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannels=%d < avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); m->split->hw_channels = try_map.channels; } } else if (!exact_channels) { From d58d2a2375856666e1aced19a60db9a716784a5f Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Thu, 13 Mar 2025 19:23:32 +0200 Subject: [PATCH 21/98] spa: acp: indicate ALSA UCM profile errors in UIs Add "[ALSA UCM error]" to the end of card description, to indicate something is wrong. --- spa/plugins/alsa/acp/acp.c | 23 +++++++++++++++++++++++ spa/plugins/alsa/acp/alsa-ucm.c | 6 +++++- spa/plugins/alsa/acp/alsa-ucm.h | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 7b49c8be6..63a32eafb 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -498,6 +498,7 @@ static void add_profiles(pa_card *impl) int n_profiles, n_ports, n_devices; uint32_t idx; const char *arr; + bool broken_ucm = false; n_devices = 0; pa_dynarray_init(&impl->out.devices, device_free); @@ -541,6 +542,9 @@ static void add_profiles(pa_card *impl) dev->ports, NULL); pa_dynarray_append(&ap->out.devices, dev); + + if (m->split && m->split->broken) + broken_ucm = true; } } @@ -564,6 +568,9 @@ static void add_profiles(pa_card *impl) dev->ports, NULL); pa_dynarray_append(&ap->out.devices, dev); + + if (m->split && m->split->broken) + broken_ucm = true; } } cp->n_devices = pa_dynarray_size(&ap->out.devices); @@ -571,6 +578,22 @@ static void add_profiles(pa_card *impl) pa_hashmap_put(impl->profiles, ap->name, cp); } + + /* Add a conspicuous notice if there are errors in the UCM profile */ + if (broken_ucm) { + const char *desc; + char *new_desc = NULL; + + desc = pa_proplist_gets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION); + if (!desc) + desc = ""; + new_desc = spa_aprintf(_("%s [ALSA UCM error]"), desc); + pa_log_notice("Errors in ALSA UCM profile for card %s", desc); + if (new_desc) + pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION, new_desc); + free(new_desc); + } + pa_dynarray_init(&impl->out.ports, NULL); n_ports = 0; PA_HASHMAP_FOREACH(dp, impl->ports, state) { diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index 41e0a1874..73466f004 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -381,9 +381,11 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd if (pa_atou(value, &idx) < 0) break; - if (idx >= hw_channels) + if (idx >= hw_channels) { pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannel%d=%d >= %sChannels=%d", pcm_name, device_name, prefix, i, idx, prefix, hw_channels); + split->broken = true; + } value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name); if (!value) { @@ -2453,10 +2455,12 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, /* Just accept whatever we got... Some of the routings won't get connected * anywhere */ m->split->hw_channels = try_map.channels; + m->split->broken = true; } else if (try_map.channels > m->split->hw_channels) { pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannels=%d < avail %d", m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); m->split->hw_channels = try_map.channels; + m->split->broken = true; } } else if (!exact_channels) { m->channel_map = try_map; diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h index 74dbe8219..8e4c1a7d1 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.h +++ b/spa/plugins/alsa/acp/alsa-ucm.h @@ -185,6 +185,7 @@ struct pa_alsa_ucm_split { int channels; int idx[PA_CHANNELS_MAX]; enum snd_pcm_chmap_position pos[PA_CHANNELS_MAX]; + bool broken; }; struct pa_alsa_ucm_device { From fb43eb8ee29c3875f950d526909765ebada40fcc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 14 Mar 2025 10:03:44 +0100 Subject: [PATCH 22/98] audioconvert: fix the filter-graph samplerate The filter graph runs before or after the resampler depending on the direction of the conversion. --- spa/plugins/audioconvert/audioconvert.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index a244294e6..a18e08629 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -1000,13 +1000,13 @@ static int setup_filter_graph(struct impl *this, struct spa_filter_graph *graph) { int res; char rate_str[64]; - struct dir *in; + struct dir *dir; if (graph == NULL) return 0; - in = &this->dir[SPA_DIRECTION_INPUT]; - snprintf(rate_str, sizeof(rate_str), "%d", in->format.info.raw.rate); + dir = &this->dir[SPA_DIRECTION_REVERSE(this->direction)]; + snprintf(rate_str, sizeof(rate_str), "%d", dir->format.info.raw.rate); spa_filter_graph_deactivate(graph); res = spa_filter_graph_activate(graph, From 2eb8cf5dc780ca22b94545f1254497a428c412f5 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 11 Mar 2025 15:09:58 +0100 Subject: [PATCH 23/98] 1.4.1 --- NEWS | 43 ++++++++++++++++++++++++++++++++++++++++--- meson.build | 2 +- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 66ce22fcb..e154347f5 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,43 @@ +# PipeWire 1.4.1 (2025-03-14) + +This is a quick bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Handle SplitPCM wrong channels specifications. This fixes some + problems with disappearing devices. + - Add backwards compatibility support for when the kernel does not + support UMP. Also fix UMP output. This restores MIDI support for + older kernels/ALSA. + - Fix a crash in audioconvert because the resampler was not using the + right number of channels. + - Some compilation fixes and small improvements. + + +## PipeWire + - Don't emit events when disconnecting a stream. (#3314) + - Fix some compilation problems. (#4603) + +## Modules + - Bump the ROC requirement to version 0.4.0 + +## SPA + - Handle SplitPCM too few or too many channels. Add an error string + to the device names when the UCM config has an error. + - Add backwards compatibility support for when the kernel does not + support UMP. + - Configure the channels in the resampler correctly in all + cases. (#4595) + - Fix UMP output. + - Use the right samplerate of the filter-graph in audioconvert in + all cases. + +## Bluetooth + - Fix a crash with an incomming call. + + +Older versions: + # PipeWire 1.4.0 (2025-03-06) This is the 1.4 release that is API and ABI compatible with previous @@ -99,9 +139,6 @@ the 1.2 release last year, including: ## JACK - Add an option to disable the MIDI2 port flags. (#4584) - -Older versions: - # PipeWire 1.3.83 (2025-02-20) This is the third and hopefully last 1.4 release candidate that diff --git a/meson.build b/meson.build index d568e9d8b..28909b200 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.4.0', + version : '1.4.1', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', From b87764bd0765b591a179a3b2ed91cadc23cd58e5 Mon Sep 17 00:00:00 2001 From: Robert Mader Date: Wed, 19 Mar 2025 08:25:34 +0100 Subject: [PATCH 24/98] systemd: Depend on dbus.service Solution suggested by Xi Ruoyao. The dbus user service is required for various features - the summary says: 'dbus (Bluetooth, rt, portal, pw-reserve)' On session logout the dbus service gets shut down while the Pipewire one relies on a timeout. If a user logs in again before PW timed out, the later stays alive but doesn't handle re-connecting to the dbus service of the new session, breaking the camera portal and potentially other features. Thus hard-depend on the dbus service (if enabled at build time) and thus shut down together with it. (cherry picked from commit 2625983a236ca34e5ca18fb12ad4f52166a1b4bd) --- src/daemon/systemd/user/meson.build | 6 ++++++ src/daemon/systemd/user/pipewire.service.in | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/daemon/systemd/user/meson.build b/src/daemon/systemd/user/meson.build index 10227629d..a96409f2b 100644 --- a/src/daemon/systemd/user/meson.build +++ b/src/daemon/systemd/user/meson.build @@ -11,6 +11,12 @@ systemd_config = configuration_data() systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire') systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse') +pw_service_reqs = '' +if get_option('dbus').enabled() + pw_service_reqs += 'dbus.service ' +endif +systemd_config.set('PW_SERVICE_REQS', pw_service_reqs) + configure_file(input : 'pipewire.service.in', output : 'pipewire.service', configuration : systemd_config, diff --git a/src/daemon/systemd/user/pipewire.service.in b/src/daemon/systemd/user/pipewire.service.in index 4236c6bd4..f0c8d9cd5 100644 --- a/src/daemon/systemd/user/pipewire.service.in +++ b/src/daemon/systemd/user/pipewire.service.in @@ -13,7 +13,7 @@ Description=PipeWire Multimedia Service # # After=pipewire.socket is not needed, as it is already implicit in the # socket-service relationship, see systemd.socket(5). -Requires=pipewire.socket +Requires=pipewire.socket @PW_SERVICE_REQS@ ConditionUser=!root [Service] From 4176caca30d10417004c55b744d71083857f3e77 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Thu, 20 Mar 2025 20:35:46 +0200 Subject: [PATCH 25/98] alsa: seq: double-check midi client version Check midi client version after setting it, to see if it was really successfully set. Old kernels without UMP don't know about the midi version fields, so snd_seq_set_client_midi_version() appears to fail silently there. --- spa/plugins/alsa/alsa-seq.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 4f64ee5ba..eb12bde29 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -44,9 +44,27 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu #ifdef HAVE_ALSA_UMP res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0); + if (!res) { + snd_seq_client_info_t *info = NULL; + + /* Double check client version */ + res = snd_seq_client_info_malloc(&info); + if (!res) + res = snd_seq_get_client_info(conn->hndl, info); + if (!res) { + res = snd_seq_client_info_get_midi_version(info); + if (res == SND_SEQ_CLIENT_UMP_MIDI_2_0) + res = 0; + else + res = -EIO; + } + if (info) + snd_seq_client_info_free(info); + } #else res = -EOPNOTSUPP; #endif + if (res < 0) { spa_log_lev(state->log, (probe_ump ? SPA_LOG_LEVEL_INFO : SPA_LOG_LEVEL_ERROR), "%p: ALSA failed to enable UMP MIDI: %s", state, snd_strerror(res)); From 081cb6848e4ea75303896e894c651c916ad90496 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 24 Mar 2025 17:54:32 +0100 Subject: [PATCH 26/98] pw-cat: prefer AU format when using stdin/stdout WAV is actually not usable for streaming output by sndfile. See #4629 --- src/tools/pw-cat.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index f485a08f1..c81ae0f8c 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -1343,7 +1343,7 @@ static void format_from_filename(SF_INFO *info, const char *filename) } } if (format == -1) - format = SF_FORMAT_WAV; + format = spa_streq(filename, "-") ? SF_FORMAT_AU : SF_FORMAT_WAV; if (format == SF_FORMAT_WAV && info->channels > 2) format = SF_FORMAT_WAVEX; From 58c412c9dcee6549b49779862d5b8e52ac5fc96c Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 24 Mar 2025 18:44:21 +0100 Subject: [PATCH 27/98] pw-cat: improve sndfile file format debug info Print the endianness, container name and the sample format nicely instead of dumping the hex values. --- src/tools/pw-cat.c | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index c81ae0f8c..36a78981e 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -1436,6 +1436,21 @@ static int setup_encodedfile(struct data *data) } #endif +static const char *endianness_to_name(int format) +{ + switch (format & SF_FORMAT_ENDMASK) { + case SF_ENDIAN_FILE: + return "Default Endian"; + case SF_ENDIAN_LITTLE: + return "Little Endian"; + case SF_ENDIAN_BIG: + return "Big Endian"; + case SF_ENDIAN_CPU: + return "CPU Endian"; + } + return "unknown"; +} + static int setup_sndfile(struct data *data) { const struct format_info *fi = NULL; @@ -1473,9 +1488,21 @@ static int setup_sndfile(struct data *data) return -EIO; } - if (data->verbose) - fprintf(stderr, "sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n", - data->filename, info.format, info.channels, info.samplerate); + if (data->verbose) { + SF_FORMAT_INFO ti, sti; + spa_zero(ti); + ti.format = info.format & SF_FORMAT_TYPEMASK; + if (sf_command(NULL, SFC_GET_FORMAT_INFO, &ti, sizeof(ti)) != 0) + ti.name = "unknown"; + spa_zero(sti); + sti.format = info.format & SF_FORMAT_SUBMASK; + if (sf_command(NULL, SFC_GET_FORMAT_INFO, &sti, sizeof(sti)) != 0) + sti.name = "unknown"; + + fprintf(stderr, "sndfile: opened file \"%s\" format \"%s %s %s\" channels:%d rate:%d\n", + data->filename, endianness_to_name(info.format), + ti.name, sti.name, info.channels, info.samplerate); + } if (data->channels > 0 && info.channels != (int)data->channels) { fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n", data->channels, info.channels); @@ -1511,7 +1538,6 @@ static int setup_sndfile(struct data *data) /* try native format first, else decode to float */ if ((fi = format_info_by_sf_format(info.format)) == NULL) fi = format_info_by_sf_format(SF_FORMAT_FLOAT); - } if (fi == NULL) return -EIO; From 81408597f496941684db2f058ef9a1dd55954b8d Mon Sep 17 00:00:00 2001 From: Philippe Normand Date: Sat, 22 Mar 2025 18:33:12 +0000 Subject: [PATCH 28/98] gst: deviceprovider: Fix a leak and a heap-use-after-free The device passed to gst_device_provider_device_add() is transfer:floating, so we need increase its ref, otherwise the pointer we keep internally will be a dangling ref. Also gst_device_provider_device_remove() doesn't actually release the device, so we have to do it ourselves. Fixes #4616 --- src/gst/gstpipewiredeviceprovider.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gst/gstpipewiredeviceprovider.c b/src/gst/gstpipewiredeviceprovider.c index c9d0d7ad7..5bdd09b7d 100644 --- a/src/gst/gstpipewiredeviceprovider.c +++ b/src/gst/gstpipewiredeviceprovider.c @@ -327,6 +327,7 @@ static void do_add_nodes(GstPipeWireDeviceProvider *self) gst_object_ref_sink (device), compare_device_session_priority); } else { + gst_object_ref (device); gst_device_provider_device_add (GST_DEVICE_PROVIDER (self), device); } } @@ -484,7 +485,8 @@ destroy_node (void *data) } if (nd->dev != NULL) { - gst_device_provider_device_remove (provider, GST_DEVICE (nd->dev)); + gst_device_provider_device_remove (provider, nd->dev); + gst_clear_object (&nd->dev); } if (nd->caps) gst_caps_unref(nd->caps); From 8d7175a1e766e2047ece6c2f470c77e1c843fd55 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 26 Mar 2025 17:22:11 +0100 Subject: [PATCH 29/98] adapter: add Header metadata by default Firefox needs this but we should eventually check the Meta Params to decide on this. --- spa/plugins/videoconvert/videoadapter.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index 8c1c8de77..a32302d3d 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -403,6 +403,7 @@ static int negotiate_buffers(struct impl *this) uint32_t i, size, buffers, blocks, align, flags, stride = 0; uint32_t *aligns; struct spa_data *datas; + struct spa_meta metas[1]; uint64_t follower_flags, conv_flags; spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); @@ -484,9 +485,11 @@ static int negotiate_buffers(struct impl *this) datas[i].maxsize = size; aligns[i] = align; } + metas[0].type = SPA_META_Header; + metas[0].size = sizeof(struct spa_meta_header); free(this->buffers); - this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns); + this->buffers = spa_buffer_alloc_array(buffers, flags, 1, metas, blocks, datas, aligns); if (this->buffers == NULL) return -errno; this->n_buffers = buffers; From 2d26fbcddff42464ed4c5b4722d688ca3ca02e57 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 28 Mar 2025 16:00:51 +0100 Subject: [PATCH 30/98] v4l2: handle nearest set_format The set_format function can return 1 when the format was adjusted to the nearest supported format so return this from the port_set_param function. This instructs the adapter to recheck the configured format so that it can store the adjuted format on the converter. --- spa/plugins/v4l2/v4l2-source.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c index f19dffde8..b06dda994 100644 --- a/spa/plugins/v4l2/v4l2-source.c +++ b/spa/plugins/v4l2/v4l2-source.c @@ -675,7 +675,7 @@ static int port_set_format(struct impl *this, struct port *port, const struct spa_pod *format) { struct spa_video_info info; - int res; + int res = 0; spa_zero(info); @@ -755,7 +755,7 @@ static int port_set_format(struct impl *this, struct port *port, emit_port_info(this, port, false); emit_node_info(this, false); - return 0; + return res; } static int impl_node_port_set_param(void *object, From a9eba98e8efaf75ee5cdb45ac727d959cfc39e0c Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 1 Apr 2025 10:18:05 +0200 Subject: [PATCH 31/98] pod: handle builder overflows When the builder is overflowed, we might get a NULL pod. This is a valid situation that we need to handle because it can be used to get the required builder buffer size. --- spa/include/spa/pod/filter.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h index 6a47472e4..2cfc7bf08 100644 --- a/spa/include/spa/pod/filter.h +++ b/spa/include/spa/pod/filter.h @@ -139,7 +139,7 @@ spa_pod_filter_prop(struct spa_pod_builder *b, const struct spa_pod_prop *p2) { const struct spa_pod *v1, *v2; - struct spa_pod_choice *nc; + struct spa_pod_choice *nc, dummy; uint32_t j, k, nalt1, nalt2; void *alt1, *alt2, *a1, *a2; uint32_t type, size, p1c, p2c; @@ -175,6 +175,10 @@ spa_pod_filter_prop(struct spa_pod_builder *b, spa_pod_builder_prop(b, p1->key, p1->flags & p2->flags); spa_pod_builder_push_choice(b, &f, 0, 0); nc = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f); + /* write to dummy value when builder overflows. We don't want to error + * because overflowing is a way to determine the required buffer size. */ + if (nc == NULL) + nc = &dummy; /* default value */ spa_pod_builder_primitive(b, v1); From 98e7ae39c6584309a167bca5ee613c9917509154 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 1 Apr 2025 10:19:40 +0200 Subject: [PATCH 32/98] impl-link: improve debug log Log the format after we patch it up and log some context lines. Move some info log to debug. --- src/pipewire/impl-link.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index 3df092ad8..a9de783e3 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -388,11 +388,10 @@ static int do_negotiate(struct pw_impl_link *this) } } - pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); - SPA_POD_OBJECT_ID(format) = SPA_PARAM_Format; pw_log_debug("%p: doing set format %p fixated:%d", this, format, spa_pod_is_fixated(format)); + pw_log_pod(SPA_LOG_LEVEL_INFO, format); if (out_state == PW_IMPL_PORT_STATE_CONFIGURE) { pw_log_debug("%p: doing set format on output", this); @@ -405,7 +404,7 @@ static int do_negotiate(struct pw_impl_link *this) goto error; } if (SPA_RESULT_IS_ASYNC(res)) { - pw_log_info("output set format %d", res); + pw_log_debug("output set format %d", res); busy_id = pw_work_queue_add(impl->work, &this->output_link, spa_node_sync(output->node->node, res), complete_ready, this); @@ -425,7 +424,7 @@ static int do_negotiate(struct pw_impl_link *this) goto error; } if (SPA_RESULT_IS_ASYNC(res2)) { - pw_log_info("input set format %d", res2); + pw_log_debug("input set format %d", res2); busy_id = pw_work_queue_add(impl->work, &this->input_link, spa_node_sync(input->node->node, res2), complete_ready, this); From ce6d2fce9ab1a39640647bf8c5f64bb6a373730e Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 1 Apr 2025 16:23:34 +0200 Subject: [PATCH 33/98] audioadapter: init the builder for each param Or else we keep on adding items until we overflow. --- spa/plugins/audioconvert/audioadapter.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 6e393bd0d..85e3fb8da 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -1612,13 +1612,13 @@ port_enum_formats_for_convert(struct impl *this, int seq, enum spa_direction dir uint32_t count = 0; struct spa_result_node_params result; - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - result.id = id; result.next = start; next: result.index = result.next; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if (result.next < 0x100000) { /* Enumerate follower formats first, until we have enough or we run out */ if ((res = node_port_enum_params_sync(this, this->follower, direction, port_id, id, From 1388f16c473a059a0c9ca2a88886952937d168e0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 2 Apr 2025 10:39:34 +0200 Subject: [PATCH 34/98] adapter: handle builder overflow --- spa/plugins/audioconvert/audioadapter.c | 2 ++ spa/plugins/videoconvert/videoadapter.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 85e3fb8da..5c0eb4d19 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -997,6 +997,8 @@ found: format = merge_objects(this, &b, SPA_PARAM_Format, (struct spa_pod_object*)format, (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; spa_pod_fixate(format); diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index a32302d3d..ebc11e1f0 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -964,6 +964,8 @@ found: format = merge_objects(this, &b, SPA_PARAM_Format, (struct spa_pod_object*)format, (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; spa_pod_fixate(format); From 48a959bf2e7df97ed12da220cd623047e1f8d5c1 Mon Sep 17 00:00:00 2001 From: Sanchayan Maity Date: Thu, 20 Mar 2025 19:30:03 +0530 Subject: [PATCH 35/98] gst: pipewireformat: Do not use RANGE if values are equal This fixes assertion from the underlying gst_value_collect_int_range when using gst_caps_set_simple with range types when the values are equal. --- src/gst/gstpipewireformat.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/gst/gstpipewireformat.c b/src/gst/gstpipewireformat.c index 24115325e..903d216e2 100644 --- a/src/gst/gstpipewireformat.c +++ b/src/gst/gstpipewireformat.c @@ -1065,8 +1065,19 @@ handle_rect_prop (const struct spa_pod_prop *prop, const char *width, const char { if (n_items < 3) return; - gst_caps_set_simple (res, width, GST_TYPE_INT_RANGE, rect[1].width, rect[2].width, - height, GST_TYPE_INT_RANGE, rect[1].height, rect[2].height, NULL); + + if (rect[1].width == rect[2].width && + rect[1].height == rect[2].height) { + gst_caps_set_simple (res, + width, G_TYPE_INT, rect[1].width, + height, G_TYPE_INT, rect[1].height, + NULL); + } else { + gst_caps_set_simple (res, + width, GST_TYPE_INT_RANGE, rect[1].width, rect[2].width, + height, GST_TYPE_INT_RANGE, rect[1].height, rect[2].height, + NULL); + } break; } case SPA_CHOICE_Enum: @@ -1117,8 +1128,17 @@ handle_fraction_prop (const struct spa_pod_prop *prop, const char *key, GstCaps { if (n_items < 3) return; - gst_caps_set_simple (res, key, GST_TYPE_FRACTION_RANGE, fract[1].num, fract[1].denom, - fract[2].num, fract[2].denom, NULL); + + if (fract[1].num == fract[2].num && + fract[1].denom == fract[2].denom) { + gst_caps_set_simple (res, key, GST_TYPE_FRACTION, + fract[1].num, fract[1].denom, NULL); + } else { + gst_caps_set_simple (res, key, GST_TYPE_FRACTION_RANGE, + fract[1].num, fract[1].denom, + fract[2].num, fract[2].denom, + NULL); + } break; } case SPA_CHOICE_Enum: From 99b94015a75e04f02acb7189e8b697fd0f605f4d Mon Sep 17 00:00:00 2001 From: Philippe Normand Date: Tue, 25 Mar 2025 10:16:47 +0000 Subject: [PATCH 36/98] gst: src: Fix buffer pool handling in case of caps renegotiation In case negotiation is first attempted with unfixed caps, bufferpool support was unconditionally disabled. Then at a second caps negotiation attempt it wasn't restored according to the property value. --- src/gst/gstpipewiresrc.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index 1cd049418..05d759011 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -1127,14 +1127,15 @@ handle_format_change (GstPipeWireSrc *pwsrc, #ifdef HAVE_GSTREAMER_DMA_DRM } #endif + } else { + /* Don't provide bufferpool for audio if not requested by the + * application/user */ + if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES) + pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; } } else { pwsrc->negotiated = FALSE; pwsrc->is_video = FALSE; - - /* Don't provide bufferpool for audio if not requested by the application/user */ - if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES) - pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; } if (pwsrc->caps) { From f571253ff3a4fd51eb8724b66e19eb44b0922d6d Mon Sep 17 00:00:00 2001 From: Sanchayan Maity Date: Wed, 19 Mar 2025 19:54:46 +0530 Subject: [PATCH 37/98] gst: pipewiresrc: Fixate caps if intersect did not return fixated caps We might end up in a situation where depending on the pipeline, intersect might not give us fixated caps. Possible example of such a pipeline can be below. gst-launch-1.0 -e pipewiresrc target-object= ! audioconvert ! audio/x-raw,format=S16LE,rate=48000,channels=2 ! lamemp3enc ! filesink location=test.mp3 This results in non-fixated caps like below when intersecting caps from format param and possible_caps which depends on what we have downstream in the pipeline. audio/x-raw, layout=(string)interleaved, format=(string)S16LE, rate=(int)48000, channels=(int)2, channel-mask=(bitmask)0x0000000000000003; audio/x-raw, layout=(string)interleaved, format=(string)S16LE, rate=(int)48000, channels=(int)2 To fix this, fixate the caps explicitly. --- src/gst/gstpipewiresrc.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index 05d759011..ea63e6192 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -1091,14 +1091,34 @@ handle_format_change (GstPipeWireSrc *pwsrc, } pw_peer_caps = gst_caps_from_format (param); + if (pw_peer_caps && pwsrc->possible_caps) { + GST_DEBUG_OBJECT (pwsrc, "peer caps %" GST_PTR_FORMAT, pw_peer_caps); + GST_DEBUG_OBJECT (pwsrc, "possible caps %" GST_PTR_FORMAT, pwsrc->possible_caps); + pwsrc->caps = gst_caps_intersect_full (pw_peer_caps, pwsrc->possible_caps, GST_CAPS_INTERSECT_FIRST); + + /* + * We expect pw_peer_caps to be fixed caps as we receive that from + * PipeWire. See pw_context_find_format() and SPA_PARAM_Format. + * possible_caps can be non-fixated caps based on what is downstream + * in the pipeline. + * + * The intersection result above might give us non-fixated caps. A + * possible scenario for this is the below pipeline. + * pipewiresrc ! audioconvert ! audio/x-raw,rate=44100,channels=2 ! .. + * + * So we fixate the caps explicitly here. + */ + pwsrc->caps = gst_caps_fixate (pwsrc->caps); gst_caps_maybe_fixate_dma_format (pwsrc->caps); } - if (pwsrc->caps && gst_caps_is_fixed (pwsrc->caps)) { + if (pwsrc->caps) { + g_return_if_fail (gst_caps_is_fixed (pwsrc->caps)); + pwsrc->negotiated = TRUE; structure = gst_caps_get_structure (pwsrc->caps, 0); From f0a432118a697f358243407a3f6b07ca869c2238 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 26 Mar 2025 09:52:22 +0100 Subject: [PATCH 38/98] gst: require a buffer size of at least 1 Setting the default size to 0 and outside of the min/max range now means that there is no suggestion for the size and it should use the suggestion of the peer. --- src/gst/gstpipewiresrc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index ea63e6192..694cbea0e 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -1179,7 +1179,7 @@ handle_format_change (GstPipeWireSrc *pwsrc, pwsrc->min_buffers, pwsrc->max_buffers), SPA_PARAM_BUFFERS_blocks, SPA_POD_CHOICE_RANGE_Int(0, 1, INT32_MAX), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 1, INT32_MAX), SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes)); From cf5db17aa6fa4081203fd80d8f701d8019e83d27 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Fri, 28 Mar 2025 09:48:35 -0400 Subject: [PATCH 39/98] gst: sink: Only add VideoCrop meta for video --- src/gst/gstpipewiresink.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c index aa9d94b99..b79ae6a14 100644 --- a/src/gst/gstpipewiresink.c +++ b/src/gst/gstpipewiresink.c @@ -305,6 +305,7 @@ gst_pipewire_sink_update_params (GstPipeWireSink *sink) struct spa_pod_builder b = { NULL }; uint8_t buffer[1024]; struct spa_pod_frame f; + guint n_params = 0; config = gst_buffer_pool_get_config (GST_BUFFER_POOL (pool)); gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers); @@ -325,20 +326,22 @@ gst_pipewire_sink_update_params (GstPipeWireSink *sink) (1<is_video) { + port_params[n_params++] = spa_pod_builder_add_object (&b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), + SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region))); + } pw_thread_loop_lock (sink->stream->core->loop); - pw_stream_update_params (sink->stream->pwstream, port_params, 3); + pw_stream_update_params (sink->stream->pwstream, port_params, n_params); pw_thread_loop_unlock (sink->stream->core->loop); } From 07f49a75590bef7012e7b0af242c24745d61bf5e Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 4 Apr 2025 11:24:39 +0200 Subject: [PATCH 40/98] raop: fix byte array initialization Initialize the byte array with bytes instead of a string because the 0 byte at the end of the string does not fit in the array and causes a compiler warning. --- src/modules/module-raop-sink.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index 9d9406811..8eef429f0 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -1331,10 +1331,12 @@ static int rtsp_post_auth_setup_reply(void *data, int status, const struct spa_d static int rtsp_do_post_auth_setup(struct impl *impl) { - static const unsigned char content[33] = - "\x01" - "\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07" - "\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e"; + static const uint8_t content[33] = { + 0x01, + 0x59, 0x02, 0xed, 0xe9, 0x0d, 0x4e, 0xf2, 0xbd, + 0x4c, 0xb6, 0x8a, 0x63, 0x30, 0x03, 0x82, 0x07, + 0xa9, 0x4d, 0xbd, 0x50, 0xd8, 0xaa, 0x46, 0x5b, + 0x5d, 0x8c, 0x01, 0x2a, 0x0c, 0x7e, 0x1d, 0x4e }; return pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict, "application/octet-stream", content, sizeof(content), From f120b595f0cb5c6daf9bf5ce58f310ea612d4b60 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 6 Apr 2025 11:55:44 +0300 Subject: [PATCH 41/98] pw-loopback: add missing --channel-map long option --- src/tools/pw-loopback.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c index 65a613c15..1d76eb284 100644 --- a/src/tools/pw-loopback.c +++ b/src/tools/pw-loopback.c @@ -103,6 +103,7 @@ int main(int argc, char *argv[]) { "group", required_argument, NULL, 'g' }, { "name", required_argument, NULL, 'n' }, { "channels", required_argument, NULL, 'c' }, + { "channel-map", required_argument, NULL, 'm' }, { "latency", required_argument, NULL, 'l' }, { "delay", required_argument, NULL, 'd' }, { "capture", required_argument, NULL, 'C' }, From 35591922ceab4d3c35704194e708ac2eaab39c4f Mon Sep 17 00:00:00 2001 From: fossdd Date: Sun, 6 Apr 2025 21:16:37 +0200 Subject: [PATCH 42/98] doc: fix typo in pw-link(1) --- doc/dox/programs/pw-link.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dox/programs/pw-link.1.md b/doc/dox/programs/pw-link.1.md index aff53d600..83a1d8372 100644 --- a/doc/dox/programs/pw-link.1.md +++ b/doc/dox/programs/pw-link.1.md @@ -37,7 +37,7 @@ output ports and their links. List output ports \par -i | \--input -List output ports +List input ports \par -l | \--links List links From a0be431c7face0dd3e9ba3cae54e446609936b95 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Fri, 11 Apr 2025 21:42:23 +0200 Subject: [PATCH 43/98] audioconvert: Increase param length limit to 4096 Parameter values read into a 512 byte long buffer, which is insufficient for medium to long filter-graph parameters. Increase the buffer to 4096 bytes to give some wiggle-room. --- spa/plugins/audioconvert/audioconvert.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index a18e08629..c1fa022d1 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -1214,7 +1214,7 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params) while (true) { const char *name; struct spa_pod *pod; - char value[512]; + char value[4096]; if (spa_pod_parser_get_string(&prs, &name) < 0) break; From ac42f559163c11eedeb2f9a6879e6a7a9ff6dc05 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 14 Apr 2025 11:19:44 +0200 Subject: [PATCH 44/98] ebur128: work around libebur128 bug Versions 1.2.5 and 1.2.6 don't scale the window in ms correctly, leading to excessive memory allocation, so work around this here. See https://github.com/jiixyj/libebur128/pull/132 Fixes #4646 --- spa/plugins/filter-graph/ebur128_plugin.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/ebur128_plugin.c index 22bf5f513..92904e102 100644 --- a/spa/plugins/filter-graph/ebur128_plugin.c +++ b/spa/plugins/filter-graph/ebur128_plugin.c @@ -235,6 +235,8 @@ static void ebur128_cleanup(void * Instance) static void ebur128_activate(void * Instance) { struct ebur128_impl *impl = Instance; + unsigned long max_window; + int major, minor, patch; int mode = 0, i; int modes[] = { EBUR128_MODE_M, @@ -264,12 +266,17 @@ static void ebur128_activate(void * Instance) mode |= modes[i]; } + ebur128_get_version(&major, &minor, &patch); + max_window = impl->max_window; + if (major == 1 && minor == 2 && (patch == 5 || patch == 6)) + max_window = (max_window + 999) / 1000; + for (i = 0; i < 7; i++) { impl->st[i] = ebur128_init(1, impl->rate, mode); if (impl->st[i]) { ebur128_set_channel(impl->st[i], i, channels[i]); ebur128_set_max_history(impl->st[i], impl->max_history); - ebur128_set_max_window(impl->st[i], impl->max_window); + ebur128_set_max_window(impl->st[i], max_window); } } } From d20a1523b6770dfa93a270bdda5d7c800d7ec191 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 2 Apr 2025 15:41:29 +0200 Subject: [PATCH 45/98] 1.4.2 --- NEWS | 37 ++++++++++++++++++++++++++++++++++--- meson.build | 2 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index e154347f5..ea4317e1d 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,37 @@ +# PipeWire 1.4.2 (2025-04-14) + +This is a bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Do extra checks for MIDI to avoid 100% CPU usage on older kernels. + - Fix some potential crashes in POD builder. + - pw-cat streaming improvements on stdout/stdin. + - Small fixes and improvements. + + +## PipeWire + - Make the service files depend on DBus to avoid startup races. + +## SPA + - Do extra checks for MIDI to avoid 100% CPU usage on older kernels. + - Use Header metadata by default in videoadapter. + - Handle set_format result from v4l2 better. + - Handle crash when POD builder overflows in the filter. + - Work around a libebur128 bug. (#4646) + +## Tools + - pw-cat prefers AU format when streaming on stdout/stdin. (#4629) + - Improve pw-cat verbose sndfile format debug. + - Add the missing --channel-map long option to pw-loopback. + +## GStreamer + - Fix a leak in the deviceprovider. (#4616) + - Fix negotiation and make renegotiation better. + + +Older versions: + # PipeWire 1.4.1 (2025-03-14) This is a quick bugfix release that is API and ABI compatible with @@ -35,9 +69,6 @@ previous 1.x releases. ## Bluetooth - Fix a crash with an incomming call. - -Older versions: - # PipeWire 1.4.0 (2025-03-06) This is the 1.4 release that is API and ABI compatible with previous diff --git a/meson.build b/meson.build index 28909b200..8b5728eea 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.4.1', + version : '1.4.2', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', From 3abda54d80cb02cacb25b9a66ce2808dc4615b34 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 19 Mar 2025 17:04:38 +0100 Subject: [PATCH 46/98] pod: use default value of filter When using a filter, it makes more sense to use the default value of the filter as a first attempt. One case is in adapter when we try to find a passthrough format first. The audioconverter suggests a default rate of the graph rate but the follower filters this out for another unrelated default value and passthrough is not possible (altough it would be because the default value of the filter is in the supported follower range). Fixes #4619 --- spa/include/spa/pod/filter.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h index 2cfc7bf08..9e265f9b7 100644 --- a/spa/include/spa/pod/filter.h +++ b/spa/include/spa/pod/filter.h @@ -181,7 +181,7 @@ spa_pod_filter_prop(struct spa_pod_builder *b, nc = &dummy; /* default value */ - spa_pod_builder_primitive(b, v1); + spa_pod_builder_primitive(b, v2); if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_None) || (p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Enum) || @@ -189,10 +189,10 @@ spa_pod_filter_prop(struct spa_pod_builder *b, (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Enum)) { int n_copied = 0; /* copy all equal values but don't copy the default value again */ - for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1, size, void)) { - for (k = 0, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) { + for (j = 0, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a2, size, void)) { + for (k = 0, a1 = alt1; k < nalt1; k++, a1 = SPA_PTROFF(a1,size,void)) { if (spa_pod_compare_value(type, a1, a2, size) == 0) { - if (p1c == SPA_CHOICE_Enum || j > 0) + if (p2c == SPA_CHOICE_Enum || j > 0) spa_pod_builder_raw(b, a1, size); n_copied++; } From 63bf21d7b8a22675fa640554d8f031fdd3b71209 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Wed, 16 Apr 2025 17:57:44 +0200 Subject: [PATCH 47/98] midifile: decode UMP SysRT messages --- src/tools/midifile.c | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/tools/midifile.c b/src/tools/midifile.c index 80c5bbb1c..cd1b6ec52 100644 --- a/src/tools/midifile.c +++ b/src/tools/midifile.c @@ -940,7 +940,43 @@ static int dump_event_ump(FILE *out, const struct midi_event *ev) dump_mem(out, "Utility", ev->data, ev->size); break; case 0x1: - dump_mem(out, "SysRT", ev->data, ev->size); + switch (ev->data[2]) { + case 0xf1: + fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d", + ev->data[0] >> 4, ev->data[0] & 0xf); + break; + case 0xf2: + fprintf(out, "Song Position Pointer: value %d", + ((int)ev->data[1] << 7 | ev->data[0])); + break; + case 0xf3: + fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f)); + break; + case 0xf6: + fprintf(out, "Tune Request"); + break; + case 0xf8: + fprintf(out, "Timing Clock"); + break; + case 0xfa: + fprintf(out, "Start Sequence"); + break; + case 0xfb: + fprintf(out, "Continue Sequence"); + break; + case 0xfc: + fprintf(out, "Stop Sequence"); + break; + case 0xfe: + fprintf(out, "Active Sensing"); + break; + case 0xff: + fprintf(out, "System Reset"); + break; + default: + dump_mem(out, "SysRT", ev->data, ev->size); + break; + } break; case 0x2: { From 8c1e1ea17ff51ac588193bc704ceee98c01fcd90 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 16 Apr 2025 18:33:36 +0200 Subject: [PATCH 48/98] midifile: unpack the UMP SysRT bytes correctly They are packed in a native endian uint32_t so read it like that and then use shifts to get the right bytes. --- src/tools/midifile.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tools/midifile.c b/src/tools/midifile.c index cd1b6ec52..a58a1bc97 100644 --- a/src/tools/midifile.c +++ b/src/tools/midifile.c @@ -940,17 +940,19 @@ static int dump_event_ump(FILE *out, const struct midi_event *ev) dump_mem(out, "Utility", ev->data, ev->size); break; case 0x1: - switch (ev->data[2]) { + { + uint8_t b[3] = { (d[0] >> 16) & 0x7f, (d[0] >> 8) & 0x7f, d[0] & 0x7f }; + switch (b[0]) { case 0xf1: fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d", - ev->data[0] >> 4, ev->data[0] & 0xf); + b[1] >> 4, b[1] & 0xf); break; case 0xf2: fprintf(out, "Song Position Pointer: value %d", - ((int)ev->data[1] << 7 | ev->data[0])); + ((int)b[2] << 7 | b[1])); break; case 0xf3: - fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f)); + fprintf(out, "Song Select: value %d", b[1]); break; case 0xf6: fprintf(out, "Tune Request"); @@ -978,20 +980,18 @@ static int dump_event_ump(FILE *out, const struct midi_event *ev) break; } break; + } case 0x2: { struct midi_event ev1; - uint8_t msg[4]; + uint8_t b[3] = { d[0] >> 16, d[0] >> 8, d[0] }; ev1 = *ev; - msg[0] = (d[0] >> 16); - msg[1] = (d[0] >> 8); - msg[2] = (d[0]); - if (msg[0] >= 0xc0 && msg[0] <= 0xdf) + if (b[0] >= 0xc0 && b[0] <= 0xdf) ev1.size = 2; else ev1.size = 3; - ev1.data = msg; + ev1.data = b; dump_event_midi1(out, &ev1); break; } From f857a5073416f40a95215f9e05332fdea440f256 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 24 Apr 2025 10:25:39 +0200 Subject: [PATCH 49/98] ump: fix program change 2.0 to 1.0 conversion The program change byte should not be shifted an extra bit, unlike all the other messages. Fixes #4664 --- spa/include/spa/control/ump-utils.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/spa/include/spa/control/ump-utils.h b/spa/include/spa/control/ump-utils.h index 9f57efb93..463b12d14 100644 --- a/spa/include/spa/control/ump-utils.h +++ b/spa/include/spa/control/ump-utils.h @@ -96,9 +96,17 @@ SPA_API_CONTROL_UMP_UTILS int spa_ump_to_midi(uint32_t *ump, size_t ump_size, if (ump_size < 8) return 0; midi[size++] = (ump[0] >> 16) | 0x80; - if (midi[0] < 0xc0 || midi[0] > 0xdf) + switch (midi[0] & 0xf0) { + case 0xc0: + midi[size++] = (ump[1] >> 24); + break; + default: midi[size++] = (ump[0] >> 8) & 0x7f; - midi[size++] = (ump[1] >> 25); + SPA_FALLTHROUGH; + case 0xd0: + midi[size++] = (ump[1] >> 25); + break; + } break; case 0x0: /* Utility Messages */ From 957bde02cd1f69ef69ba5caa0206ca582edf0b4b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 23 Apr 2025 11:09:54 +0200 Subject: [PATCH 50/98] v4l2: only skip buffer for raw formats We don't want to skip the first buffer for encoded formats because it can contain the encoding parameters, in the h264 case, for example. --- spa/plugins/v4l2/v4l2-utils.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c index be38ab32d..96ce68ff7 100644 --- a/spa/plugins/v4l2/v4l2-utils.c +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -1873,7 +1873,10 @@ static int spa_v4l2_stream_on(struct impl *this) spa_log_debug(this->log, "starting"); - port->first_buffer = true; + if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw) + port->first_buffer = true; + else + port->first_buffer = false; mmap_read(this); type = V4L2_BUF_TYPE_VIDEO_CAPTURE; From 49c4c44dcea0bdcbadc421b6d5dc80360b01548a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 23 Apr 2025 11:14:34 +0200 Subject: [PATCH 51/98] videoconvert: consume input buffer in all cases Return the input buffer when we get some error so that the other side can send new data. --- spa/plugins/videoconvert/videoconvert-ffmpeg.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spa/plugins/videoconvert/videoconvert-ffmpeg.c b/spa/plugins/videoconvert/videoconvert-ffmpeg.c index 955bfa93a..ddb810be5 100644 --- a/spa/plugins/videoconvert/videoconvert-ffmpeg.c +++ b/spa/plugins/videoconvert/videoconvert-ffmpeg.c @@ -1812,6 +1812,7 @@ static int impl_node_process(void *object) } sbuf = &in_port->buffers[input->buffer_id]; + input->status = SPA_STATUS_NEED_DATA; if ((dbuf = peek_buffer(this, out_port)) == NULL) { spa_log_error(this->log, "%p: out of buffers", this); @@ -1902,8 +1903,6 @@ static int impl_node_process(void *object) output->buffer_id = dbuf->id; output->status = SPA_STATUS_HAVE_DATA; - input->status = SPA_STATUS_NEED_DATA; - return SPA_STATUS_HAVE_DATA; } From 0135baa60fe79e11080d5a3c92c1396b61044b5b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 24 Apr 2025 13:21:07 +0200 Subject: [PATCH 52/98] alsa-seq: add the source only on success Otherwise if we have an error with the timerfd, we have the source fd added and we close the device. --- spa/plugins/alsa/alsa-seq.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index eb12bde29..dac268376 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -404,7 +404,6 @@ int spa_alsa_seq_open(struct seq_state *state) state->sys.source.func = alsa_seq_on_sys; state->sys.source.data = state; - spa_loop_add_source(state->main_loop, &state->sys.source); /* increase event queue timer resolution */ snd_seq_queue_timer_alloca(&timer); @@ -449,6 +448,8 @@ int spa_alsa_seq_open(struct seq_state *state) state->timerfd = res; + spa_loop_add_source(state->main_loop, &state->sys.source); + state->opened = true; return 0; From 7d27668fb29c2a0a817d4e948dd3c59e9da4fcd1 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 28 Apr 2025 09:51:20 +0200 Subject: [PATCH 53/98] ebur128: fix port name Fixes #4667 --- spa/plugins/filter-graph/ebur128_plugin.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/ebur128_plugin.c index 92904e102..a7aeeef99 100644 --- a/spa/plugins/filter-graph/ebur128_plugin.c +++ b/spa/plugins/filter-graph/ebur128_plugin.c @@ -356,7 +356,7 @@ static struct spa_fga_port ebur128_ports[] = { .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, }, { .index = PORT_OUT_SHORTTERM, - .name = "Shorttem LUFS", + .name = "Shortterm LUFS", .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, }, { .index = PORT_OUT_GLOBAL, From 5d741a2b3c4327cae4a387fc0856c051cf03d0cc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 30 Apr 2025 12:20:31 +0200 Subject: [PATCH 54/98] filter-chain: add props only once Only add the properties offset in the builder after we actually got a property or else we end up with the same property twice. --- src/modules/module-filter-chain.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 1446cc437..537b8c7d0 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -1153,10 +1153,11 @@ static int setup_streams(struct impl *impl) SPA_PARAM_EnumFormat, &impl->capture_info); for (i = 0;; i++) { - if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) - *offs = b.b.state.offset; + uint32_t save = b.b.state.offset; if (spa_filter_graph_enum_prop_info(graph, i, &b.b, NULL) != 1) break; + if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) + *offs = save; } if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) From 2d4af3ced511d8579da0cb6b8ad107f0a3a3c790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Mon, 5 May 2025 12:44:20 +0200 Subject: [PATCH 55/98] spa: Add default: statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows to use the library in projects that use `-Wswitch-default` without any #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-default" #pragma GCC diagnostic pop This is useful as as the header is being pulled in via pipewire/wireplumber headers into projects that might have this warning enabled and would otherwise fail to build with -Werror. Signed-off-by: Guido Günther --- spa/include/spa/utils/json-core.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spa/include/spa/utils/json-core.h b/spa/include/spa/utils/json-core.h index 31bf772f1..10c87e68c 100644 --- a/spa/include/spa/utils/json-core.h +++ b/spa/include/spa/utils/json-core.h @@ -268,6 +268,8 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) if (--utf8_remain == 0) iter->state = __STRING | flag; continue; + default: + break; } _SPA_ERROR(CHARACTERS_NOT_ALLOWED); case __ESC: @@ -276,12 +278,17 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) case 'n': case 'r': case 't': case 'u': iter->state = __STRING | flag; continue; + default: + break; } _SPA_ERROR(INVALID_ESCAPE); case __COMMENT: switch (cur) { case '\n': case '\r': iter->state = __STRUCT | flag; + break; + default: + break; } break; default: @@ -299,6 +306,8 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) case __COMMENT: /* trailing comment */ return 0; + default: + break; } if ((iter->state & __SUB_FLAG) && (iter->state & __KEY_FLAG)) { From 37dbf93cc4a00b06675e38ff6c9a7c0cfa698f6f Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 7 May 2025 12:55:00 +0200 Subject: [PATCH 56/98] filter-graph: remove useless check --- spa/plugins/filter-graph/filter-graph.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index bc9ff154a..91afd62a5 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -256,9 +256,8 @@ static int impl_process(void *object, if (out[i] == NULL) continue; - port = i < graph->n_output ? &graph->output[i] : NULL; - - if (port && port->desc) + port = &graph->output[i]; + if (port->desc) port->desc->connect_port(*port->hndl, port->port, out[i]); else memset(out[i], 0, n_samples * sizeof(float)); From 750b88c3ca8a6aa932d5710a7833f7d8c2950483 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 9 May 2025 09:33:05 +0200 Subject: [PATCH 57/98] control: only convert midi/UMP, pass other control types We should only try to convert MIDI/UMP when the port doesn't support it but let all the other control types pass through. Fixes #4692 --- spa/plugins/control/mixer.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c index 439bea76b..e97072d45 100644 --- a/spa/plugins/control/mixer.c +++ b/spa/plugins/control/mixer.c @@ -671,6 +671,14 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control * } } +static inline bool control_needs_conversion(struct port *port, uint32_t type) +{ + /* we only converter between midi and UMP and only when the port + * does not support the current type */ + return (type == SPA_CONTROL_Midi || type == SPA_CONTROL_UMP) && + port->types && (port->types & (1u << type)) == 0; +} + static int impl_node_process(void *object) { struct impl *this = object; @@ -782,7 +790,7 @@ static int impl_node_process(void *object) if (next == NULL) break; - if (outport->types && (outport->types & (1u << next->type)) == 0) { + if (control_needs_conversion(outport, next->type)) { uint8_t *data = SPA_POD_BODY(&next->value); size_t size = SPA_POD_BODY_SIZE(&next->value); From 1cbe4e17821ecfd510a6eaa7290cdb9f7cd11544 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 17 Mar 2025 18:05:06 +0100 Subject: [PATCH 58/98] ump: handle sysex from UMP to MIDI1 better SysEx in UMP can span multiple packets. In MIDI1 we can't split them up into multiple events so we need to collect the complete sysex and then write out the event. Fixes SysEx writes to ALSA seq by running the event encoder until a valid packet is completed. Also fixes split MIDI1 packets in the JACK API when going through the tunnel or via netjack. --- pipewire-jack/src/pipewire-jack.c | 43 +++++++++++++- spa/plugins/alsa/alsa-seq.c | 16 ++++- src/modules/module-jack-tunnel.c | 24 ++++++-- src/modules/module-netjack2/peer.c | 94 +++++++++++++++++++++++------- 4 files changed, 143 insertions(+), 34 deletions(-) diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index 839a9fc24..e85947124 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -1546,14 +1546,37 @@ static inline jack_midi_data_t* midi_event_reserve(void *port_buffer, return res; } +static inline int midi_event_append(void *port_buffer, const jack_midi_data_t *data, size_t data_size) +{ + struct midi_buffer *mb = port_buffer; + struct midi_event *events = SPA_PTROFF(mb, sizeof(*mb), struct midi_event); + struct midi_event *ev; + size_t old_size; + uint8_t *old, *buf; + + ev = &events[--mb->event_count]; + mb->write_pos -= ev->size; + old_size = ev->size; + if (old_size <= MIDI_INLINE_MAX) + old = ev->inline_data; + else + old = SPA_PTROFF(mb, ev->byte_offset, uint8_t); + buf = midi_event_reserve(port_buffer, ev->time, old_size + data_size); + if (SPA_UNLIKELY(buf == NULL)) + return -ENOBUFS; + memmove(buf, old, old_size); + memcpy(buf+old_size, data, data_size); + return 0; +} + static inline int midi_event_write(void *port_buffer, jack_nframes_t time, const jack_midi_data_t *data, size_t data_size, bool fix) { jack_midi_data_t *retbuf = midi_event_reserve (port_buffer, time, data_size); - if (SPA_UNLIKELY(retbuf == NULL)) - return -ENOBUFS; + if (SPA_UNLIKELY(retbuf == NULL)) + return -ENOBUFS; memcpy (retbuf, data, data_size); if (fix) fix_midi_event(retbuf, data_size); @@ -1566,6 +1589,7 @@ static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void uint64_t state = 0; uint32_t i; int res = 0; + bool in_sysex = false; for (i = 0; i < n_seq; i++) c[i] = spa_pod_control_first(&seq[i]->body); @@ -1620,17 +1644,30 @@ static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void void *data = SPA_POD_BODY(&next->value); size_t size = SPA_POD_BODY_SIZE(&next->value); uint8_t ev[32]; + bool was_sysex = in_sysex; if (type == TYPE_ID_MIDI) { int ev_size = spa_ump_to_midi(data, size, ev, sizeof(ev)); if (ev_size <= 0) break; + size = ev_size; data = ev; + + if (!in_sysex && ev[0] == 0xf0) + in_sysex = true; + if (in_sysex && ev[ev_size-1] == 0xf7) + in_sysex = false; + } else if (type != TYPE_ID_UMP) break; - if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0) + if (was_sysex) + res = midi_event_append(midi, data, size); + else + res = midi_event_write(midi, next->offset, data, size, fix); + + if (res < 0) pw_log_warn("midi %p: can't write event: %s", midi, spa_strerror(res)); } diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index dac268376..b0be92098 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -78,6 +78,7 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu spa_log_debug(state->log, "%p: ALSA UMP MIDI enabled", state); state->ump = true; } + state->ump = false; return 0; } @@ -821,6 +822,7 @@ static int process_write(struct seq_state *state) struct spa_pod_control *c; uint64_t out_time; snd_seq_real_time_t out_rt; + bool first = true; if (!port->valid || io == NULL) continue; @@ -885,13 +887,20 @@ static int process_write(struct seq_state *state) if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) continue; - snd_seq_ev_clear(&ev); + if (first) + snd_seq_ev_clear(&ev); - snd_midi_event_reset_encode(stream->codec); - if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) <= 0) { + if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) < 0) { spa_log_warn(state->log, "failed to encode event: %s", snd_strerror(size)); + snd_midi_event_reset_encode(stream->codec); + first = true; continue; } + first = false; + if (ev.type == SND_SEQ_EVENT_NONE) + /* this can happen when the event is not complete yet, like + * a sysex message and we need to encode some more data. */ + continue; snd_seq_ev_set_source(&ev, state->event.addr.port); snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); @@ -901,6 +910,7 @@ static int process_write(struct seq_state *state) spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); } + first = true; } } } diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 48e8c6f8e..7ab4c9faf 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -247,6 +247,9 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s struct spa_pod_sequence *seq; struct spa_pod_control *c; int res; + bool in_sysex = false; + uint8_t tmp[n_samples * 4]; + size_t tmp_size = 0; jack.midi_clear_buffer(dst); if (src == NULL) @@ -260,23 +263,32 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s seq = (struct spa_pod_sequence*)pod; SPA_POD_SEQUENCE_FOREACH(seq, c) { - uint8_t data[16]; int size; if (c->type != SPA_CONTROL_UMP) continue; size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), data, sizeof(data)); + SPA_POD_BODY_SIZE(&c->value), &tmp[tmp_size], sizeof(tmp) - tmp_size); if (size <= 0) continue; if (impl->fix_midi) - fix_midi_event(data, size); + fix_midi_event(&tmp[tmp_size], size); - if ((res = jack.midi_event_write(dst, c->offset, data, size)) < 0) - pw_log_warn("midi %p: can't write event: %s", dst, - spa_strerror(res)); + if (!in_sysex && tmp[tmp_size] == 0xf0) + in_sysex = true; + + tmp_size += size; + if (in_sysex && tmp[tmp_size-1] == 0xf7) + in_sysex = false; + + if (!in_sysex) { + if ((res = jack.midi_event_write(dst, c->offset, tmp, tmp_size)) < 0) + pw_log_warn("midi %p: can't write event: %s", dst, + spa_strerror(res)); + tmp_size = 0; + } } } diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index bac6f1322..8016a048a 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -242,14 +242,71 @@ static inline void fix_midi_event(uint8_t *data, size_t size) } } +static inline void *n2j_midi_buffer_reserve(struct nj2_midi_buffer *buf, + uint32_t offset, uint32_t size) +{ + struct nj2_midi_event *ev; + void *ptr; + + if (size <= 0) + return NULL; + + size_t used_size = sizeof(*buf) + buf->write_pos + + ((buf->event_count + 1) * sizeof(struct nj2_midi_event)); + + ev = &buf->event[buf->event_count]; + ev->time = offset; + ev->size = size; + if (size <= MIDI_INLINE_MAX) { + ptr = ev->buffer; + } else { + if (used_size + size > buf->buffer_size) + return NULL; + buf->write_pos += size; + ev->offset = buf->buffer_size - 1 - buf->write_pos; + ptr = SPA_PTROFF(buf, ev->offset, void); + } + buf->event_count++; + return ptr; +} + +static inline void n2j_midi_buffer_write(struct nj2_midi_buffer *buf, + uint32_t offset, void *data, uint32_t size) +{ + void *ptr = n2j_midi_buffer_reserve(buf, offset, size); + if (ptr != NULL) + memcpy(ptr, data, size); + else + buf->lost_events++; +} + +static inline void n2j_midi_buffer_append(struct nj2_midi_buffer *buf, + void *data, uint32_t size) +{ + struct nj2_midi_event *ev; + uint32_t old_size; + uint8_t *old_ptr, *new_ptr; + + ev = &buf->event[--buf->event_count]; + old_size = ev->size; + if (old_size <= MIDI_INLINE_MAX) { + old_ptr = ev->buffer; + } else { + buf->write_pos -= old_size; + old_ptr = SPA_PTROFF(buf, ev->offset, void); + } + new_ptr = n2j_midi_buffer_reserve(buf, ev->time, old_size + size); + memmove(new_ptr, old_ptr, old_size); + memcpy(new_ptr+old_size, data, size); +} + static void midi_to_netjack2(struct netjack2_peer *peer, struct nj2_midi_buffer *buf, float *src, uint32_t n_samples) { struct spa_pod *pod; struct spa_pod_sequence *seq; struct spa_pod_control *c; - struct nj2_midi_event *ev; - int free_size; + bool in_sysex = false; buf->magic = MIDI_BUFFER_MAGIC; buf->buffer_size = peer->quantum_limit * sizeof(float); @@ -269,12 +326,10 @@ static void midi_to_netjack2(struct netjack2_peer *peer, seq = (struct spa_pod_sequence*)pod; - free_size = buf->buffer_size - sizeof(*buf); - SPA_POD_SEQUENCE_FOREACH(seq, c) { int size; uint8_t data[16]; - void *ptr; + bool was_sysex = in_sysex; if (c->type != SPA_CONTROL_UMP) continue; @@ -284,29 +339,24 @@ static void midi_to_netjack2(struct netjack2_peer *peer, if (size <= 0) continue; - if (c->offset >= n_samples || - size >= free_size) { + if (c->offset >= n_samples) { buf->lost_events++; continue; } - if (peer->fix_midi) + if (!in_sysex && data[0] == 0xf0) + in_sysex = true; + + if (!in_sysex && peer->fix_midi) fix_midi_event(data, size); - ev = &buf->event[buf->event_count]; - ev->time = c->offset; - ev->size = size; - if (size <= MIDI_INLINE_MAX) { - ptr = ev->buffer; - } else { - buf->write_pos += size; - ev->offset = buf->buffer_size - 1 - buf->write_pos; - free_size -= size; - ptr = SPA_PTROFF(buf, ev->offset, void); - } - memcpy(ptr, data, size); - buf->event_count++; - free_size -= sizeof(*ev); + if (in_sysex && data[size-1] == 0xf7) + in_sysex = false; + + if (was_sysex) + n2j_midi_buffer_append(buf, data, size); + else + n2j_midi_buffer_write(buf, c->offset, data, size); } if (buf->write_pos > 0) memmove(SPA_PTROFF(buf, sizeof(*buf) + buf->event_count * sizeof(struct nj2_midi_event), void), From 3d33acce1d33ef4485a1b1af45a872cf9f2ecf6a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 18 Mar 2025 09:38:55 +0100 Subject: [PATCH 59/98] netjack: handle overflow in midi buffer append --- src/modules/module-netjack2/peer.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index 8016a048a..a3a20f294 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -296,8 +296,12 @@ static inline void n2j_midi_buffer_append(struct nj2_midi_buffer *buf, old_ptr = SPA_PTROFF(buf, ev->offset, void); } new_ptr = n2j_midi_buffer_reserve(buf, ev->time, old_size + size); - memmove(new_ptr, old_ptr, old_size); - memcpy(new_ptr+old_size, data, size); + if (new_ptr == NULL) { + buf->lost_events++; + } else { + memmove(new_ptr, old_ptr, old_size); + memcpy(new_ptr+old_size, data, size); + } } static void midi_to_netjack2(struct netjack2_peer *peer, From 9b52c078710fbec6ad5951f601c4d613814c0385 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 19 Mar 2025 12:57:44 +0100 Subject: [PATCH 60/98] jack: write midi events to end of buffer There is no need to keep one extra byte at the end of the buffer, we can write the event up to the last byte. --- pipewire-jack/src/pipewire-jack.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index e85947124..14c062a21 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -1538,7 +1538,7 @@ static inline jack_midi_data_t* midi_event_reserve(void *port_buffer, res = ev->inline_data; } else { mb->write_pos += data_size; - ev->byte_offset = mb->buffer_size - 1 - mb->write_pos; + ev->byte_offset = mb->buffer_size - mb->write_pos; res = SPA_PTROFF(mb, ev->byte_offset, uint8_t); } mb->event_count += 1; @@ -5791,8 +5791,8 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames) convert_to_event(seq, n_seq, mb, p->client->fix_midi_events, p->object->port.type_id); memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count * sizeof(struct midi_event))); - if (mb->write_pos) { - size_t offs = mb->buffer_size - 1 - mb->write_pos; + if (mb->write_pos > 0) { + size_t offs = mb->buffer_size - mb->write_pos; memcpy(SPA_PTROFF(ptr, offs, void), SPA_PTROFF(mb, offs, void), mb->write_pos); } return ptr; From 28109587fd272b0bbbca9b2179418cdf605a26bb Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 19 Mar 2025 13:06:15 +0100 Subject: [PATCH 61/98] netjack2: copy large midi events to the end of the buffer There is no need to keep an extra free byte at the end and it will cause us to lose a byte when we copy the large midi events down. --- src/modules/module-netjack2/peer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index a3a20f294..d0e819847 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -263,7 +263,7 @@ static inline void *n2j_midi_buffer_reserve(struct nj2_midi_buffer *buf, if (used_size + size > buf->buffer_size) return NULL; buf->write_pos += size; - ev->offset = buf->buffer_size - 1 - buf->write_pos; + ev->offset = buf->buffer_size - buf->write_pos; ptr = SPA_PTROFF(buf, ev->offset, void); } buf->event_count++; From d28d1957455c3241678bc1c4b5c38f88fb85ad8d Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 19 Mar 2025 13:09:07 +0100 Subject: [PATCH 62/98] netjack2: set correct max midi buffer size It depends on the negotiated period size, not the graph quantum. --- src/modules/module-netjack2/peer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index d0e819847..319ae164c 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -313,7 +313,7 @@ static void midi_to_netjack2(struct netjack2_peer *peer, bool in_sysex = false; buf->magic = MIDI_BUFFER_MAGIC; - buf->buffer_size = peer->quantum_limit * sizeof(float); + buf->buffer_size = peer->params.period_size * sizeof(float); buf->nframes = n_samples; buf->write_pos = 0; buf->event_count = 0; From 3aaf91d9c695f4e0a235a8d804e69f8e84dca378 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 19 Mar 2025 13:10:15 +0100 Subject: [PATCH 63/98] netjack2: fix the large midi events offset The midi events have their large data offsets relative to the start of the buffer and the large data is at the end of the buffer. Because we copied it down, right after the events, but we didn't adjust the offsets, calculate a correction offset when unpacking the events. --- src/modules/module-netjack2/peer.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index 319ae164c..411bd6abc 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -373,6 +373,8 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b struct spa_pod_builder b = { 0, }; uint32_t i; struct spa_pod_frame f; + size_t offset = size - buf->write_pos - + sizeof(*buf) - (buf->event_count * sizeof(struct nj2_midi_event)); spa_pod_builder_init(&b, dst, size); spa_pod_builder_push_sequence(&b, &f, 0); @@ -384,8 +386,8 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b if (ev->size <= MIDI_INLINE_MAX) data = ev->buffer; - else if (ev->offset > buf->write_pos) - data = SPA_PTROFF(buf, ev->offset - buf->write_pos, void); + else if (ev->offset > offset) + data = SPA_PTROFF(buf, ev->offset - offset, void); else continue; @@ -393,9 +395,10 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b while (s > 0) { uint32_t ump[4]; int ump_size = spa_ump_from_midi(&data, &s, ump, sizeof(ump), 0, &state); - if (ump_size <= 0) + if (ump_size <= 0) { + pw_log_warn("invalid MIDI received: %s", spa_strerror(ump_size)); break; - + } spa_pod_builder_control(&b, ev->time, SPA_CONTROL_UMP); spa_pod_builder_bytes(&b, ump, ump_size); } From 3a231a807df6d86e240e4e2f1d086bdca41413c3 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 28 Apr 2025 11:12:06 +0200 Subject: [PATCH 64/98] netjack2: Improve docs Remove unused option. Clarify that channels and midi port are chosen by the server. See #4666 --- src/modules/module-netjack2-driver.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 3675a33f7..ce889cd4a 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -63,11 +63,12 @@ * - `source.ip =`: IP address to bind to, default "0.0.0.0" * - `source.port =`: port to bind to, default 0 (allocate) * - `netjack2.client-name`: the name of the NETJACK2 client. - * - `netjack2.save`: if jack port connections should be save automatically. Can also be - * placed per stream. * - `netjack2.latency`: the latency in cycles, default 2 * - `audio.channels`: the number of audio ports. Can also be added to the stream props. + * Will always be configured to the channel count of the manager. The audio.position + * can however be used to assign an audio position. * - `midi.ports`: the number of midi ports. Can also be added to the stream props. + * Will always be configured to the number of midi ports on the manager. * - `source.props`: Extra properties for the source filter. * - `sink.props`: Extra properties for the sink filter. * @@ -95,7 +96,6 @@ * args = { * #driver.mode = duplex * #netjack2.client-name = PipeWire - * #netjack2.save = false * #netjack2.latency = 2 * #midi.ports = 0 * #audio.channels = 2 @@ -151,7 +151,6 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( source.ip= ) " \ "( source.port= ) " \ "( netjack2.client-name= ) " \ - "( netjack2.save= ) " \ "( netjack2.latency= ) " \ "( midi.ports= ) " \ "( audio.channels= ) " \ From 5a00232cdf80b05266dbaf88fa25e0cee2466093 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 28 Apr 2025 11:13:14 +0200 Subject: [PATCH 65/98] netjack2: implement ifname in the driver See #4666 --- src/modules/module-netjack2-driver.c | 14 +++++++++++--- src/modules/module-netjack2-manager.c | 3 +-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index ce889cd4a..199dbe9cf 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -705,7 +705,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen) static int make_socket(struct sockaddr_storage *src, socklen_t src_len, struct sockaddr_storage *dst, socklen_t dst_len, - bool loop, int ttl, int dscp) + bool loop, int ttl, int dscp, const char *ifname) { int af, fd, val, res; struct timeval timeout; @@ -721,7 +721,13 @@ static int make_socket(struct sockaddr_storage *src, socklen_t src_len, pw_log_error("setsockopt failed: %m"); goto error; } - +#ifdef SO_BINDTODEVICE + if (ifname && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) { + res = -errno; + pw_log_error("setsockopt(SO_BINDTODEVICE) failed: %m"); + goto error; + } +#endif #ifdef SO_PRIORITY val = 6; if (setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0) @@ -981,9 +987,11 @@ static int create_netjack2_socket(struct impl *impl) impl->ttl = pw_properties_get_uint32(impl->props, "net.ttl", DEFAULT_NET_TTL); impl->loop = pw_properties_get_bool(impl->props, "net.loop", DEFAULT_NET_LOOP); impl->dscp = pw_properties_get_uint32(impl->props, "net.dscp", DEFAULT_NET_DSCP); + str = pw_properties_get(impl->props, "local.ifname"); fd = make_socket(&impl->src_addr, impl->src_len, - &impl->dst_addr, impl->dst_len, impl->loop, impl->ttl, impl->dscp); + &impl->dst_addr, impl->dst_len, impl->loop, impl->ttl, impl->dscp, + str); if (fd < 0) { res = -errno; pw_log_error("can't create socket: %s", spa_strerror(res)); diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 8229b5d5f..dfbce12e1 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -1082,8 +1082,7 @@ static int create_netjack2_socket(struct impl *impl) impl->dscp = pw_properties_get_uint32(impl->props, "net.dscp", DEFAULT_NET_DSCP); str = pw_properties_get(impl->props, "local.ifname"); - fd = make_announce_socket(&impl->src_addr, impl->src_len, - pw_properties_get(impl->props, "local.ifname")); + fd = make_announce_socket(&impl->src_addr, impl->src_len, str); if (fd < 0) { res = fd; pw_log_error("can't create socket: %s", spa_strerror(res)); From f3cb35dad339b0e7f0448714efcff34a66e383ad Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 28 Apr 2025 12:23:15 +0200 Subject: [PATCH 66/98] netjack2: keep position io per stream Keep a position info for the stream it was set and then use the position info for the stream that is driving the graph. Otherwise we might use a destroyed position info. --- src/modules/module-netjack2-driver.c | 41 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 199dbe9cf..fbb687dd5 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -181,6 +181,8 @@ struct stream { struct pw_filter *filter; struct spa_hook listener; + struct spa_io_position *position; + struct spa_audio_info_raw info; uint32_t n_midi; @@ -221,8 +223,6 @@ struct impl { struct spa_hook core_proxy_listener; struct spa_hook core_listener; - struct spa_io_position *position; - struct stream source; struct stream sink; @@ -368,11 +368,10 @@ static void source_process(void *d, struct spa_io_position *position) static void stream_io_changed(void *data, void *port_data, uint32_t id, void *area, uint32_t size) { struct stream *s = data; - struct impl *impl = s->impl; if (port_data == NULL) { switch (id) { case SPA_IO_Position: - impl->position = area; + s->position = area; break; default: break; @@ -615,6 +614,24 @@ static inline uint64_t get_time_nsec(struct impl *impl) return nsec; } +static void update_clock(struct impl *impl, struct stream *s, uint64_t nsec, uint32_t nframes) +{ + if (s->position) { + struct spa_io_clock *c = &s->position->clock; + + c->nsec = nsec; + c->rate = SPA_FRACTION(1, impl->samplerate); + c->position = impl->frame_time; + c->duration = nframes; + c->delay = 0; + c->rate_diff = 1.0; + c->next_nsec = nsec; + + c->target_rate = c->rate; + c->target_duration = c->duration; + } +} + static void on_data_io(void *data, int fd, uint32_t mask) { @@ -654,20 +671,6 @@ on_data_io(void *data, int fd, uint32_t mask) pw_log_warn("Xrun netjack2:%u PipeWire:%u", impl->nj2_xrun, impl->pw_xrun); impl->new_xrun = false; } - if (impl->position) { - struct spa_io_clock *c = &impl->position->clock; - - c->nsec = nsec; - c->rate = SPA_FRACTION(1, impl->samplerate); - c->position = impl->frame_time; - c->duration = nframes; - c->delay = 0; - c->rate_diff = 1.0; - c->next_nsec = nsec; - - c->target_rate = c->rate; - c->target_duration = c->duration; - } if (!source_running) netjack2_recv_data(&impl->peer, NULL, 0, NULL, 0); @@ -675,11 +678,13 @@ on_data_io(void *data, int fd, uint32_t mask) impl->done = false; impl->triggered = true; impl->driving = MODE_SOURCE; + update_clock(impl, &impl->source, nsec, nframes); pw_filter_trigger_process(impl->source.filter); } else if (impl->mode == MODE_SINK && sink_running) { impl->done = false; impl->triggered = true; impl->driving = MODE_SINK; + update_clock(impl, &impl->sink, nsec, nframes); pw_filter_trigger_process(impl->sink.filter); } else { sink_running = false; From dc1f73ceafebeecb4713c39f52c638fec25cbf4b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 09:16:01 +0200 Subject: [PATCH 67/98] netjack2: warn when the trigger fails --- src/modules/module-netjack2-driver.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index fbb687dd5..be7af2749 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -679,13 +679,15 @@ on_data_io(void *data, int fd, uint32_t mask) impl->triggered = true; impl->driving = MODE_SOURCE; update_clock(impl, &impl->source, nsec, nframes); - pw_filter_trigger_process(impl->source.filter); + if (pw_filter_trigger_process(impl->source.filter) < 0) + pw_log_warn("source not ready"); } else if (impl->mode == MODE_SINK && sink_running) { impl->done = false; impl->triggered = true; impl->driving = MODE_SINK; update_clock(impl, &impl->sink, nsec, nframes); - pw_filter_trigger_process(impl->sink.filter); + if (pw_filter_trigger_process(impl->sink.filter) < 0) + pw_log_warn("sink not ready"); } else { sink_running = false; impl->done = true; From c2c255b5f9f690f5492970e32c640a8a7c574ea2 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 09:16:54 +0200 Subject: [PATCH 68/98] netjack2: keep per stream io_position And handle the trigger failure with a warning and fallback. --- src/modules/module-netjack2-manager.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index dfbce12e1..5c2bb854e 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -183,6 +183,8 @@ struct stream { struct pw_filter *filter; struct spa_hook listener; + struct spa_io_position *position; + struct spa_audio_info_raw info; uint32_t n_midi; @@ -202,8 +204,6 @@ struct follower { struct spa_list link; struct impl *impl; - struct spa_io_position *position; - struct stream source; struct stream sink; @@ -354,9 +354,8 @@ static void sink_process(void *d, struct spa_io_position *position) pw_loop_update_io(s->impl->data_loop, follower->socket, SPA_IO_IN); } -static void source_process(void *d, struct spa_io_position *position) +static inline void handle_source_process(struct stream *s, struct spa_io_position *position) { - struct stream *s = d; struct follower *follower = s->follower; uint32_t nframes = position->clock.duration; struct data_info midi[s->n_ports]; @@ -369,6 +368,12 @@ static void source_process(void *d, struct spa_io_position *position) netjack2_recv_data(&follower->peer, midi, n_midi, audio, n_audio); } +static void source_process(void *d, struct spa_io_position *position) +{ + struct stream *s = d; + handle_source_process(s, position); +} + static void follower_free(struct follower *follower) { struct impl *impl = follower->impl; @@ -473,18 +478,20 @@ on_data_io(void *data, int fd, uint32_t mask) if (mask & SPA_IO_IN) { pw_loop_update_io(impl->data_loop, follower->socket, 0); - pw_filter_trigger_process(follower->source.filter); + if (pw_filter_trigger_process(follower->source.filter) < 0) { + pw_log_warn("source not ready"); + handle_source_process(&follower->source, follower->source.position); + } } } static void stream_io_changed(void *data, void *port_data, uint32_t id, void *area, uint32_t size) { struct stream *s = data; - struct follower *follower = s->follower; if (port_data == NULL) { switch (id) { case SPA_IO_Position: - follower->position = area; + s->position = area; break; default: break; From 85479cf2035ca83c92e4c2483dc2e501a6d6a55a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 09:17:35 +0200 Subject: [PATCH 69/98] netjack2: copy the node.group to streams Just in case we want them to be scheduled in the same group. --- src/modules/module-netjack2-manager.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 5c2bb854e..432fb6e47 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -1309,6 +1309,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_NODE_NETWORK); + copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_LINK_GROUP); copy_props(impl, props, PW_KEY_NODE_ALWAYS_PROCESS); copy_props(impl, props, PW_KEY_NODE_LOCK_QUANTUM); From b9bad88eed17bc1c76f811e465f51b9fae54094a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 16:26:30 +0200 Subject: [PATCH 70/98] netjack2: make function to clear events Make a separate function to clear events instead of passing NULL as the midi buffer and segfaulting. --- src/modules/module-netjack2/peer.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index 411bd6abc..3870b3817 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -368,17 +368,27 @@ static void midi_to_netjack2(struct netjack2_peer *peer, buf->write_pos); } +static inline void netjack2_clear_midi(float *dst, uint32_t size) +{ + struct spa_pod_builder b = { 0, }; + struct spa_pod_frame f; + spa_pod_builder_init(&b, dst, size); + spa_pod_builder_push_sequence(&b, &f, 0); + spa_pod_builder_pop(&b, &f); +} + static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_buffer *buf) { struct spa_pod_builder b = { 0, }; uint32_t i; struct spa_pod_frame f; - size_t offset = size - buf->write_pos - - sizeof(*buf) - (buf->event_count * sizeof(struct nj2_midi_event)); + size_t offset = size - buf->write_pos - sizeof(*buf) - + (buf->event_count * sizeof(struct nj2_midi_event)); spa_pod_builder_init(&b, dst, size); spa_pod_builder_push_sequence(&b, &f, 0); - for (i = 0; buf != NULL && i < buf->event_count; i++) { + + for (i = 0; i < buf->event_count; i++) { struct nj2_midi_event *ev = &buf->event[i]; uint8_t *data; size_t s; @@ -1088,7 +1098,7 @@ static int netjack2_recv_data(struct netjack2_peer *peer, } for (i = 0; i < n_midi; i++) { if (!midi[i].filled && midi[i].data != NULL) - netjack2_to_midi(midi[i].data, peer->params.period_size * sizeof(float), NULL); + netjack2_clear_midi(midi[i].data, peer->params.period_size * sizeof(float)); } peer->sync.cycle = ntohl(header.cycle); return 0; From 0ea1bc384170591e631e0a23bc7f9800c1ad0297 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 17:17:14 +0200 Subject: [PATCH 71/98] netjack2: implement driver and manager roles correctly The manager is actually not supposed to decide much about the number of audio and midi ports. It should just suggest a default when connecting driver doesn't know. Add a audio.ports parameters to manager and driver to suggest/ask for the amount of audio ports. Let the audio.position/audio.channels be a specification of the channel mask in case it matches the requested channels, otherwise use AUX channels for the ports. This means that we must derive the mode (sink/source/audio/midi) from the ports that are negotiated in the manager and the driver, so delay this until after negotiation. Make sure all the possible modes work. For midi only streams, we can't wait for the session manager to perform a PortConfig so do that ourselves. Make sure we only use a source trigger when we have a sink. Fixes #4666 --- src/modules/module-netjack2-driver.c | 132 ++++++++++--------- src/modules/module-netjack2-manager.c | 178 ++++++++++++++++---------- 2 files changed, 180 insertions(+), 130 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index be7af2749..c2f1148ed 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -53,7 +53,6 @@ * * ## Module Options * - * - `driver.mode`: the driver mode, sink|source|duplex, default duplex * - `local.ifname = `: interface name to use * - `net.ip =`: multicast IP address, default "225.3.19.154" * - `net.port =`: control port, default 19000 @@ -64,11 +63,10 @@ * - `source.port =`: port to bind to, default 0 (allocate) * - `netjack2.client-name`: the name of the NETJACK2 client. * - `netjack2.latency`: the latency in cycles, default 2 - * - `audio.channels`: the number of audio ports. Can also be added to the stream props. - * Will always be configured to the channel count of the manager. The audio.position - * can however be used to assign an audio position. + * - `audio.ports`: the number of audio ports. Can also be added to the stream props. + * A value of -1 will configure to the number of audio ports on the manager. * - `midi.ports`: the number of midi ports. Can also be added to the stream props. - * Will always be configured to the number of midi ports on the manager. + * A value of -1 will configure to the number of midi ports on the manager. * - `source.props`: Extra properties for the source filter. * - `sink.props`: Extra properties for the sink filter. * @@ -94,10 +92,10 @@ * context.modules = [ * { name = libpipewire-module-netjack2-driver * args = { - * #driver.mode = duplex * #netjack2.client-name = PipeWire * #netjack2.latency = 2 * #midi.ports = 0 + * #audio.ports = -1 * #audio.channels = 2 * #audio.position = [ FL FR ] * source.props = { @@ -133,15 +131,13 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define NETWORK_MAX_LATENCY 30 #define DEFAULT_CLIENT_NAME "PipeWire" -#define DEFAULT_CHANNELS 2 -#define DEFAULT_POSITION "[ FL FR ]" -#define DEFAULT_MIDI_PORTS 1 +#define DEFAULT_MIDI_PORTS -1 +#define DEFAULT_AUDIO_PORTS -1 -#define FOLLOWER_INIT_TIMEOUT 1 +#define FOLLOWER_INIT_TIMEOUT 10000 #define FOLLOWER_INIT_RETRY -1 #define MODULE_USAGE "( remote.name= ) " \ - "( driver.mode= ) " \ "( local.ifname= ) " \ "( net.ip= ) " \ "( net.port= ) " \ @@ -152,9 +148,10 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( source.port= ) " \ "( netjack2.client-name= ) " \ "( netjack2.latency= ) " \ - "( midi.ports= ) " \ - "( audio.channels= ) " \ - "( audio.position= ) " \ + "( audio.ports= ) " \ + "( midi.ports= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ "( source.props= ) " \ "( sink.props= ) " @@ -181,11 +178,13 @@ struct stream { struct pw_filter *filter; struct spa_hook listener; + int32_t wanted_n_midi; + int32_t wanted_n_audio; + struct spa_io_position *position; struct spa_audio_info_raw info; - uint32_t n_midi; uint32_t n_ports; struct port *ports[MAX_PORTS]; @@ -456,6 +455,7 @@ static void make_stream_ports(struct stream *s) s->ports[i] = port; } + pw_filter_set_active(s->filter, true); } static struct spa_pod *make_props_param(struct spa_pod_builder *b, @@ -521,7 +521,6 @@ static void stream_param_changed(void *data, void *port_data, uint32_t id, case SPA_PARAM_PortConfig: pw_log_debug("PortConfig"); make_stream_ports(s); - pw_filter_set_active(s->filter, true); break; case SPA_PARAM_Props: pw_log_debug("Props"); @@ -556,6 +555,7 @@ static int make_stream(struct stream *s, const char *name) const struct spa_pod *params[4]; uint8_t buffer[1024]; struct spa_pod_builder b; + int res; n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); @@ -581,12 +581,18 @@ static int make_stream(struct stream *s, const char *name) SPA_PARAM_Format, &s->info); params[n_params++] = make_props_param(&b, &s->volume); - return pw_filter_connect(s->filter, + if ((res = pw_filter_connect(s->filter, PW_FILTER_FLAG_INACTIVE | PW_FILTER_FLAG_DRIVER | PW_FILTER_FLAG_RT_PROCESS | PW_FILTER_FLAG_CUSTOM_LATENCY, - params, n_params); + params, n_params)) < 0) + return res; + + if (s->info.channels == 0) + make_stream_ports(s); + + return res; } static int create_filters(struct impl *impl) @@ -801,13 +807,12 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p int res; struct netjack2_peer *peer = &impl->peer; uint32_t i; + const char *media; pw_log_info("got follower setup"); nj2_dump_session_params(params); nj2_session_params_ntoh(&peer->params, params); - SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); - SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); if (peer->params.send_audio_channels < 0 || peer->params.recv_audio_channels < 0 || @@ -822,20 +827,28 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p pw_loop_update_io(impl->main_loop, impl->setup_socket, 0); - impl->source.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; - impl->source.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.send_audio_channels != impl->source.info.channels) { - impl->source.info.channels = peer->params.send_audio_channels; - for (i = 0; i < SPA_MIN(impl->source.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) - impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + impl->sink.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; + if (impl->sink.n_ports > MAX_PORTS) { + pw_log_warn("Too many follower sink ports %d > %d", impl->sink.n_ports, MAX_PORTS); + return -EINVAL; } - impl->sink.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; impl->sink.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.recv_audio_channels != impl->sink.info.channels) { - impl->sink.info.channels = peer->params.recv_audio_channels; - for (i = 0; i < SPA_MIN(impl->sink.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + if ((uint32_t)peer->params.send_audio_channels != impl->sink.info.channels) { + impl->sink.info.channels = SPA_MIN(peer->params.send_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + for (i = 0; i < impl->sink.info.channels; i++) impl->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } + impl->source.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; + if (impl->source.n_ports > MAX_PORTS) { + pw_log_warn("Too many follower source ports %d > %d", impl->source.n_ports, MAX_PORTS); + return -EINVAL; + } + impl->source.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.recv_audio_channels != impl->source.info.channels) { + impl->source.info.channels = SPA_MIN(peer->params.recv_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + for (i = 0; i < impl->source.info.channels; i++) + impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } impl->samplerate = peer->params.sample_rate; impl->period_size = peer->params.period_size; @@ -855,6 +868,20 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p pw_properties_setf(impl->source.props, PW_KEY_NODE_FORCE_QUANTUM, "%u", impl->period_size); + media = impl->sink.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get(impl->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(impl->sink.props, PW_KEY_MEDIA_CLASS, "%s/Sink", media); + + media = impl->source.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get(impl->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(impl->source.props, PW_KEY_MEDIA_CLASS, "%s/Source", media); + + impl->mode = 0; + if (impl->source.n_ports > 0) + impl->mode |= MODE_SOURCE; + if (impl->sink.n_ports > 0) + impl->mode |= MODE_SINK; + if ((res = create_filters(impl)) < 0) return res; @@ -954,10 +981,10 @@ static int send_follower_available(struct impl *impl) snprintf(params.follower_name, sizeof(params.follower_name), "%s", pw_get_host_name()); params.mtu = htonl(impl->mtu); params.transport_sync = htonl(0); - params.send_audio_channels = htonl(-1); - params.recv_audio_channels = htonl(-1); - params.send_midi_channels = htonl(-1); - params.recv_midi_channels = htonl(-1); + params.send_audio_channels = htonl(impl->sink.wanted_n_audio); + params.recv_audio_channels = htonl(impl->source.wanted_n_audio); + params.send_midi_channels = htonl(impl->sink.wanted_n_midi); + params.recv_midi_channels = htonl(impl->source.wanted_n_midi); params.sample_encoder = htonl(NJ2_ENCODER_FLOAT); params.follower_sync_mode = htonl(1); params.network_latency = htonl(impl->latency); @@ -1165,8 +1192,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio { spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), - SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, SPA_KEY_AUDIO_CHANNELS, SPA_KEY_AUDIO_POSITION, NULL); @@ -1234,20 +1260,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->sink.impl = impl; impl->sink.direction = PW_DIRECTION_INPUT; - impl->mode = MODE_DUPLEX; - if ((str = pw_properties_get(props, "driver.mode")) != NULL) { - if (spa_streq(str, "source")) { - impl->mode = MODE_SOURCE; - } else if (spa_streq(str, "sink")) { - impl->mode = MODE_SINK; - } else if (spa_streq(str, "duplex")) { - impl->mode = MODE_DUPLEX; - } else { - pw_log_error("invalid driver.mode '%s'", str); - res = -EINVAL; - goto error; - } - } impl->latency = pw_properties_get_uint32(impl->props, "netjack2.latency", DEFAULT_NETWORK_LATENCY); @@ -1259,11 +1271,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if (pw_properties_get(props, PW_KEY_NODE_ALWAYS_PROCESS) == NULL) pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); - pw_properties_set(impl->sink.props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_DRIVER, "40000"); pw_properties_set(impl->sink.props, PW_KEY_NODE_NAME, "netjack2_driver_send"); - pw_properties_set(impl->source.props, PW_KEY_MEDIA_CLASS, "Audio/Source"); pw_properties_set(impl->source.props, PW_KEY_PRIORITY_DRIVER, "40001"); pw_properties_set(impl->source.props, PW_KEY_NODE_NAME, "netjack2_driver_receive"); @@ -1278,22 +1288,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_ALWAYS_PROCESS); copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, "midi.ports"); + copy_props(impl, props, "audio.ports"); parse_audio_info(impl->source.props, &impl->source.info); parse_audio_info(impl->sink.props, &impl->sink.info); - impl->source.n_midi = pw_properties_get_uint32(impl->source.props, + impl->source.wanted_n_midi = pw_properties_get_int32(impl->source.props, "midi.ports", DEFAULT_MIDI_PORTS); - impl->sink.n_midi = pw_properties_get_uint32(impl->sink.props, + impl->sink.wanted_n_midi = pw_properties_get_int32(impl->sink.props, "midi.ports", DEFAULT_MIDI_PORTS); - - impl->source.n_ports = impl->source.n_midi + impl->source.info.channels; - impl->sink.n_ports = impl->sink.n_midi + impl->sink.info.channels; - if (impl->source.n_ports > MAX_PORTS || impl->sink.n_ports > MAX_PORTS) { - pw_log_error("too many ports"); - res = -EINVAL; - goto error; - } + impl->source.wanted_n_audio = pw_properties_get_int32(impl->source.props, + "audio.ports", DEFAULT_AUDIO_PORTS); + impl->sink.wanted_n_audio = pw_properties_get_int32(impl->sink.props, + "audio.ports", DEFAULT_AUDIO_PORTS); impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 432fb6e47..30cedb795 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -67,8 +67,11 @@ * - `netjack2.period-size`: the buffer size to use, default 1024 * - `netjack2.encoding`: the encoding, float|opus|int, default float * - `netjack2.kbps`: the number of kilobits per second when encoding, default 64 - * - `audio.channels`: the number of audio ports. Can also be added to the stream props. - * - `midi.ports`: the number of midi ports. Can also be added to the stream props. + * - `audio.ports`: the number of audio ports. Can also be added to the stream props. This + * is the default suggestion for drivers that don't specify any number of audio channels. + * - `midi.ports`: the number of midi ports. Can also be added to the stream props. This + * is the default suggestion for drivers that don't specify any number of midi channels. + * - `audio.position`: default channel position for the number of audio.ports. * - `source.props`: Extra properties for the source filter. * - `sink.props`: Extra properties for the sink filter. * @@ -99,6 +102,7 @@ * #netjack2.period-size = 1024 * #netjack2.encoding = float # float|opus * #netjack2.kbps = 64 + * #audio.ports = 0 * #midi.ports = 0 * #audio.channels = 2 * #audio.position = [ FL FR ] @@ -137,8 +141,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_PERIOD_SIZE 1024 #define DEFAULT_ENCODING "float" #define DEFAULT_KBPS 64 -#define DEFAULT_CHANNELS 2 -#define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_AUDIO_PORTS 2 #define DEFAULT_MIDI_PORTS 1 #define MODULE_USAGE "( remote.name= ) " \ @@ -151,8 +154,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( netjack2.connect= ) " \ "( netjack2.sample-rate= ) "\ "( netjack2.period-size= ) " \ - "( midi.ports= ) " \ - "( audio.channels= ) " \ + "( midi.ports= ) " \ + "( audio.channels= ) " \ "( audio.position= ) " \ "( source.props= ) " \ "( sink.props= ) " @@ -186,8 +189,9 @@ struct stream { struct spa_io_position *position; struct spa_audio_info_raw info; - + uint32_t n_audio; uint32_t n_midi; + uint32_t n_ports; struct port *ports[MAX_PORTS]; @@ -204,6 +208,11 @@ struct follower { struct spa_list link; struct impl *impl; +#define MODE_SINK (1<<0) +#define MODE_SOURCE (1<<1) +#define MODE_DUPLEX (MODE_SINK|MODE_SOURCE) + uint32_t mode; + struct stream source; struct stream sink; @@ -235,10 +244,6 @@ struct impl { struct pw_loop *data_loop; struct spa_system *system; -#define MODE_SINK (1<<0) -#define MODE_SOURCE (1<<1) -#define MODE_DUPLEX (MODE_SINK|MODE_SOURCE) - uint32_t mode; struct pw_properties *props; struct pw_properties *sink_props; struct pw_properties *source_props; @@ -371,6 +376,11 @@ static inline void handle_source_process(struct stream *s, struct spa_io_positio static void source_process(void *d, struct spa_io_position *position) { struct stream *s = d; + struct follower *follower = s->follower; + + if (!(follower->mode & MODE_SINK)) + sink_process(&follower->sink, position); + handle_source_process(s, position); } @@ -478,9 +488,15 @@ on_data_io(void *data, int fd, uint32_t mask) if (mask & SPA_IO_IN) { pw_loop_update_io(impl->data_loop, follower->socket, 0); - if (pw_filter_trigger_process(follower->source.filter) < 0) { - pw_log_warn("source not ready"); - handle_source_process(&follower->source, follower->source.position); + if (follower->mode & MODE_SOURCE) { + if (pw_filter_trigger_process(follower->source.filter) < 0) { + pw_log_warn("source not ready"); + handle_source_process(&follower->source, follower->source.position); + } + } else { + /* There is no source, handle the source receive side (without ports) + * with the sink position io */ + handle_source_process(&follower->source, follower->sink.position); } } } @@ -528,6 +544,9 @@ static void make_stream_ports(struct stream *s) struct spa_latency_info latency; const struct spa_pod *params[1]; + if (s->ready) + return; + for (i = 0; i < s->n_ports; i++) { struct port *port = s->ports[i]; if (port != NULL) { @@ -578,6 +597,9 @@ static void make_stream_ports(struct stream *s) s->ports[i] = port; } + s->ready = true; + if (s->follower->started) + pw_filter_set_active(s->filter, true); } static struct spa_pod *make_props_param(struct spa_pod_builder *b, @@ -643,9 +665,6 @@ static void stream_param_changed(void *data, void *port_data, uint32_t id, case SPA_PARAM_PortConfig: pw_log_debug("PortConfig"); make_stream_ports(s); - s->ready = true; - if (s->follower->started) - pw_filter_set_active(s->filter, true); break; case SPA_PARAM_Props: pw_log_debug("Props"); @@ -681,6 +700,7 @@ static int make_stream(struct stream *s, const char *name) uint8_t buffer[1024]; struct spa_pod_builder b; uint32_t flags; + int res; n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); @@ -700,7 +720,8 @@ static int make_stream(struct stream *s, const char *name) } else { pw_filter_add_listener(s->filter, &s->listener, &source_events, s); - flags |= PW_FILTER_FLAG_TRIGGER; + if (s->follower->mode & MODE_SINK) + flags |= PW_FILTER_FLAG_TRIGGER; } reset_volume(&s->volume, s->info.channels); @@ -712,18 +733,23 @@ static int make_stream(struct stream *s, const char *name) SPA_PARAM_Format, &s->info); params[n_params++] = make_props_param(&b, &s->volume); - return pw_filter_connect(s->filter, flags, params, n_params); + if ((res = pw_filter_connect(s->filter, flags, params, n_params)) < 0) + return res; + + if (s->info.channels == 0) + make_stream_ports(s); + + return res; } static int create_filters(struct follower *follower) { - struct impl *impl = follower->impl; int res = 0; - if (impl->mode & MODE_SINK) + if (follower->mode & MODE_SINK) res = make_stream(&follower->sink, "NETJACK2 Send"); - if (impl->mode & MODE_SOURCE) + if (follower->mode & MODE_SOURCE) res = make_stream(&follower->source, "NETJACK2 Receive"); return res; @@ -867,6 +893,8 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param struct follower *follower; char buffer[256]; struct netjack2_peer *peer; + uint32_t i; + const char *media; pw_log_info("got follower available"); nj2_dump_session_params(params); @@ -898,6 +926,12 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param parse_audio_info(follower->source.props, &follower->source.info); parse_audio_info(follower->sink.props, &follower->sink.info); + follower->source.n_audio = pw_properties_get_uint32(follower->source.props, + "audio.ports", follower->source.info.channels ? + follower->source.info.channels : DEFAULT_AUDIO_PORTS); + follower->sink.n_audio = pw_properties_get_uint32(follower->sink.props, + "audio.ports", follower->sink.info.channels ? + follower->sink.info.channels : DEFAULT_AUDIO_PORTS); follower->source.n_midi = pw_properties_get_uint32(follower->source.props, "midi.ports", DEFAULT_MIDI_PORTS); follower->sink.n_midi = pw_properties_get_uint32(follower->sink.props, @@ -932,29 +966,65 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param peer->params.sample_encoder = impl->encoding; peer->params.kbps = impl->kbps; + /* params send and recv are from the client point of view and reversed for the + * manager, so when the client sends, we receive in a source etc. We swap the params + * later after we replied to the client. */ if (peer->params.send_audio_channels < 0) - peer->params.send_audio_channels = follower->sink.info.channels; + peer->params.send_audio_channels = follower->source.n_audio; if (peer->params.recv_audio_channels < 0) - peer->params.recv_audio_channels = follower->source.info.channels; + peer->params.recv_audio_channels = follower->sink.n_audio; if (peer->params.send_midi_channels < 0) - peer->params.send_midi_channels = follower->sink.n_midi; + peer->params.send_midi_channels = follower->source.n_midi; if (peer->params.recv_midi_channels < 0) - peer->params.recv_midi_channels = follower->source.n_midi; + peer->params.recv_midi_channels = follower->sink.n_midi; follower->source.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; - follower->source.info.rate = peer->params.sample_rate; - follower->source.info.channels = peer->params.send_audio_channels; + follower->source.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.send_audio_channels != follower->source.info.channels) { + follower->source.info.channels = SPA_MIN(peer->params.send_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + for (i = 0; i < follower->source.info.channels; i++) + follower->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } follower->sink.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; - follower->sink.info.rate = peer->params.sample_rate; - follower->sink.info.channels = peer->params.recv_audio_channels; + follower->sink.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.recv_audio_channels != follower->sink.info.channels) { + follower->sink.info.channels = SPA_MIN(peer->params.recv_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + for (i = 0; i < follower->sink.info.channels; i++) + follower->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } - follower->source.n_ports = follower->source.n_midi + follower->source.info.channels; - follower->sink.n_ports = follower->sink.n_midi + follower->sink.info.channels; if (follower->source.n_ports > MAX_PORTS || follower->sink.n_ports > MAX_PORTS) { - pw_log_error("too many ports"); + pw_log_error("too many ports source:%d sink:%d max:%d", follower->source.n_ports, + follower->sink.n_ports, MAX_PORTS); res = -EINVAL; goto cleanup; } + media = follower->sink.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get_bool(follower->sink.props, "netjack2.connect", DEFAULT_CONNECT)) { + if (pw_properties_get(follower->sink.props, PW_KEY_NODE_AUTOCONNECT) == NULL) + pw_properties_set(follower->sink.props, PW_KEY_NODE_AUTOCONNECT, "true"); + if (pw_properties_get(follower->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->sink.props, PW_KEY_MEDIA_CLASS, "Stream/Input/%s", media); + } else { + if (pw_properties_get(follower->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->sink.props, PW_KEY_MEDIA_CLASS, "%s/Sink", media); + } + media = follower->source.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get_bool(follower->source.props, "netjack2.connect", DEFAULT_CONNECT)) { + if (pw_properties_get(follower->source.props, PW_KEY_NODE_AUTOCONNECT) == NULL) + pw_properties_set(follower->source.props, PW_KEY_NODE_AUTOCONNECT, "true"); + if (pw_properties_get(follower->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->source.props, PW_KEY_MEDIA_CLASS, "Stream/Output/%s", media); + } else { + if (pw_properties_get(follower->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->source.props, PW_KEY_MEDIA_CLASS, "%s/Source", media); + } + + follower->mode = 0; + if (follower->sink.n_ports > 0) + follower->mode |= MODE_SINK; + if (follower->source.n_ports > 0) + follower->mode |= MODE_SOURCE; if ((res = create_filters(follower)) < 0) goto create_failed; @@ -1009,6 +1079,10 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param nj2_dump_session_params(params); send(follower->socket->fd, params, sizeof(*params), 0); + /* now swap send and recv to make it match our point of view */ + SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); + SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); + return 0; create_failed: @@ -1179,8 +1253,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio { spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), - SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, SPA_KEY_AUDIO_CHANNELS, SPA_KEY_AUDIO_POSITION, NULL); @@ -1244,20 +1317,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->main_loop = pw_context_get_main_loop(context); impl->system = impl->main_loop->system; - impl->mode = MODE_DUPLEX; - if ((str = pw_properties_get(props, "tunnel.mode")) != NULL) { - if (spa_streq(str, "source")) { - impl->mode = MODE_SOURCE; - } else if (spa_streq(str, "sink")) { - impl->mode = MODE_SINK; - } else if (spa_streq(str, "duplex")) { - impl->mode = MODE_DUPLEX; - } else { - pw_log_error("invalid tunnel.mode '%s'", str); - res = -EINVAL; - goto error; - } - } impl->samplerate = pw_properties_get_uint32(impl->props, "netjack2.sample-rate", DEFAULT_SAMPLE_RATE); impl->period_size = pw_properties_get_uint32(impl->props, "netjack2.period-size", @@ -1316,27 +1375,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_LOCK_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, "audio.ports"); + copy_props(impl, props, "midi.ports"); copy_props(impl, props, "netjack2.connect"); - if (pw_properties_get_bool(impl->sink_props, "netjack2.connect", DEFAULT_CONNECT)) { - if (pw_properties_get(impl->sink_props, PW_KEY_NODE_AUTOCONNECT) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_NODE_AUTOCONNECT, "true"); - if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Stream/Input/Audio"); - } else { - if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); - } - if (pw_properties_get_bool(impl->source_props, "netjack2.connect", DEFAULT_CONNECT)) { - if (pw_properties_get(impl->source_props, PW_KEY_NODE_AUTOCONNECT) == NULL) - pw_properties_set(impl->source_props, PW_KEY_NODE_AUTOCONNECT, "true"); - if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Stream/Output/Audio"); - } else { - if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source"); - } - impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); From 515de13b8aa3573f94e54058bb13eeb10ec13214 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 17:35:40 +0200 Subject: [PATCH 72/98] netjack2: set timeout to sane value again --- src/modules/module-netjack2-driver.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index c2f1148ed..7946821f4 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -134,7 +134,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_MIDI_PORTS -1 #define DEFAULT_AUDIO_PORTS -1 -#define FOLLOWER_INIT_TIMEOUT 10000 +#define FOLLOWER_INIT_TIMEOUT 1 #define FOLLOWER_INIT_RETRY -1 #define MODULE_USAGE "( remote.name= ) " \ From c3e08ad9c977af3172a3f8faec6914795984e36b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 18:09:02 +0200 Subject: [PATCH 73/98] netjack2: handle connection errors in more cases --- src/modules/module-netjack2-manager.c | 18 +++++++++++++++--- src/modules/module-netjack2/peer.c | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 30cedb795..b690ec771 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -359,6 +359,15 @@ static void sink_process(void *d, struct spa_io_position *position) pw_loop_update_io(s->impl->data_loop, follower->socket, SPA_IO_IN); } +static int stop_follower(struct follower *follower); + +static int do_stop_follower(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + stop_follower(user_data); + return 0; +} + static inline void handle_source_process(struct stream *s, struct spa_io_position *position) { struct follower *follower = s->follower; @@ -369,7 +378,10 @@ static inline void handle_source_process(struct stream *s, struct spa_io_positio set_info(s, nframes, midi, &n_midi, audio, &n_audio); - netjack2_manager_sync_wait(&follower->peer); + if (netjack2_manager_sync_wait(&follower->peer) < 0) { + pw_loop_invoke(s->impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); + return; + } netjack2_recv_data(&follower->peer, midi, n_midi, audio, n_audio); } @@ -481,8 +493,8 @@ on_data_io(void *data, int fd, uint32_t mask) if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { pw_log_warn("error:%08x", mask); - pw_loop_destroy_source(impl->data_loop, follower->socket); - follower->socket = NULL; + pw_loop_update_io(impl->data_loop, follower->socket, 0); + pw_loop_invoke(impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); return; } if (mask & SPA_IO_IN) { diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index 3870b3817..833b22723 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -758,7 +758,7 @@ static inline int32_t netjack2_driver_sync_wait(struct netjack2_peer *peer) receive_error: pw_log_warn("recv error: %m"); - return 0; + return -errno; } static inline int32_t netjack2_manager_sync_wait(struct netjack2_peer *peer) @@ -802,7 +802,7 @@ static inline int32_t netjack2_manager_sync_wait(struct netjack2_peer *peer) receive_error: pw_log_warn("recv error: %m"); - return 0; + return -errno; } static int netjack2_recv_midi(struct netjack2_peer *peer, struct nj2_packet_header *header, uint32_t *count, From 4e0137696f08e193d8cdcf8cf17ca26bd965b8a4 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 29 Apr 2025 18:36:54 +0200 Subject: [PATCH 74/98] netjack2: fix trace_fp compilation --- src/modules/module-netjack2-driver.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 7946821f4..f85ece7a8 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -670,8 +670,8 @@ on_data_io(void *data, int fd, uint32_t mask) impl->frame_time += nframes; - pw_log_trace_fp("process %d %u %u %p %"PRIu64, nframes, source_running, - sink_running, impl->position, impl->frame_time); + pw_log_trace_fp("process %d %u %u %"PRIu64, nframes, source_running, + sink_running, impl->frame_time); if (impl->new_xrun) { pw_log_warn("Xrun netjack2:%u PipeWire:%u", impl->nj2_xrun, impl->pw_xrun); From e040430967b4684c23bb27665376a3589688a7b0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 30 Apr 2025 11:00:42 +0200 Subject: [PATCH 75/98] netjack2: improve shutdown Destroy the sources from the io handler immediately when there is an error so that we don't end up in endless error wakeups. Schedule the free from the main loop and make sure only one can ever run. --- src/modules/module-netjack2-manager.c | 39 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index b690ec771..64ad7fb3b 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -236,6 +236,7 @@ struct follower { unsigned int done:1; unsigned int new_xrun:1; unsigned int started:1; + unsigned int freeing:1; }; struct impl { @@ -289,6 +290,7 @@ static void stream_destroy(void *d) struct stream *s = d; uint32_t i; + s->running = false; spa_hook_remove(&s->listener); for (i = 0; i < s->n_ports; i++) s->ports[i] = NULL; @@ -400,20 +402,35 @@ static void follower_free(struct follower *follower) { struct impl *impl = follower->impl; + if (follower->freeing) + return; + + follower->freeing = true; + spa_list_remove(&follower->link); - if (follower->source.filter) + if (follower->socket) { + pw_loop_destroy_source(impl->data_loop, follower->socket); + follower->socket = NULL; + } + if (follower->setup_socket) { + pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->setup_socket = NULL; + } + + if (follower->source.filter) { pw_filter_destroy(follower->source.filter); - if (follower->sink.filter) + follower->source.filter = NULL; + } + if (follower->sink.filter) { pw_filter_destroy(follower->sink.filter); + follower->sink.filter = NULL; + } pw_properties_free(follower->source.props); + follower->source.props = NULL; pw_properties_free(follower->sink.props); - - if (follower->socket) - pw_loop_destroy_source(impl->data_loop, follower->socket); - if (follower->setup_socket) - pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->sink.props = NULL; netjack2_cleanup(&follower->peer); free(follower); @@ -447,10 +464,13 @@ static void on_setup_io(void *data, int fd, uint32_t mask) { struct follower *follower = data; + struct impl *impl = follower->impl; if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { pw_log_warn("error:%08x", mask); - stop_follower(follower); + pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->setup_socket = NULL; + pw_loop_invoke(impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); return; } if (mask & SPA_IO_IN) { @@ -493,7 +513,8 @@ on_data_io(void *data, int fd, uint32_t mask) if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { pw_log_warn("error:%08x", mask); - pw_loop_update_io(impl->data_loop, follower->socket, 0); + pw_loop_destroy_source(impl->data_loop, follower->socket); + follower->socket = NULL; pw_loop_invoke(impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); return; } From f7f2e3e52a0b236bf380f43343d5693124c158b3 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 30 Apr 2025 11:02:58 +0200 Subject: [PATCH 76/98] netjack2: use strncpy to copy the header It pads the remaining bytes in the header with 0 bytes so that the memory doesn't contain uninitialized data. --- src/modules/module-netjack2/peer.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index 833b22723..b289c9425 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -429,7 +429,7 @@ static int netjack2_send_sync(struct netjack2_peer *peer, uint32_t nframes) is_last = peer->params.send_midi_channels == 0 && peer->params.send_audio_channels == 0 ? 1 : 0; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('s'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -483,7 +483,7 @@ static int netjack2_send_midi(struct netjack2_peer *peer, uint32_t nframes, max_size = peer->params.mtu - sizeof(header); num_packets = (midi_size + max_size-1) / max_size; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('m'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -535,7 +535,7 @@ static int netjack2_send_float(struct netjack2_peer *peer, uint32_t nframes, sub_period_bytes = sub_period_size * sizeof(float) + sizeof(int32_t); num_packets = nframes / sub_period_size; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -610,7 +610,7 @@ static int netjack2_send_opus(struct netjack2_peer *peer, uint32_t nframes, } } - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -678,7 +678,7 @@ static int netjack2_send_int(struct netjack2_peer *peer, uint32_t nframes, memset(ap, 0, max_encoded); } - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); From 835d8b7801f497834c2cdafdb38ea64ae72ac3c1 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 6 May 2025 10:44:14 +0200 Subject: [PATCH 77/98] netjack2: add driver.mode again This configures the default number of audio and midi ports per stream to 0 depending on the mode. Improve docs a little. See #4666 --- src/modules/module-netjack2-driver.c | 42 +++++++++++++++++++++++++-- src/modules/module-netjack2-manager.c | 15 +++++++++- src/modules/module-netjack2/packets.h | 2 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index f85ece7a8..e1f5eb6ca 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -45,7 +45,26 @@ /** \page page_module_netjack2_driver Netjack2 driver * * The netjack2-driver module provides a source or sink that is following a - * netjack2 manager. + * netjack2 manager. It is meant to be used over stable (ethernet) network + * connections with minimal latency and jitter. + * + * The driver normally decides how many ports it will send and receive from the + * manager. By default however, these values are set to -1 so that the manager + * decides on the number of ports. + * + * With the global or per stream audio.port and midi.ports properties this + * behaviour can be adjusted. + * + * The driver will send out UDP messages on a (typically) multicast address to + * inform the manager of the available driver. This will then instruct the manager + * to configure and start the driver. + * + * On the driver side, a sink and/or source with the specified numner of audio and + * midi ports will be created. On the manager side there will be a corresponding + * source and/or sink created respectively. + * + * The driver will be scheduled with exactly the same period as the manager but with + * a configurable number of periods of delay (see netjack2.latency, default 2). * * ## Module Name * @@ -53,6 +72,10 @@ * * ## Module Options * + * - `driver.mode`: the driver mode, sink|source|duplex, default duplex. This set the + * per stream audio.port and midi.ports default from -1 to 0. sink mode defaults to + * no source ports, source mode to no sink ports and duplex leaves the defaults as + * they are. * - `local.ifname = `: interface name to use * - `net.ip =`: multicast IP address, default "225.3.19.154" * - `net.port =`: control port, default 19000 @@ -99,7 +122,7 @@ * #audio.channels = 2 * #audio.position = [ FL FR ] * source.props = { - * # extra sink properties + * # extra source properties * } * sink.props = { * # extra sink properties @@ -138,6 +161,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define FOLLOWER_INIT_RETRY -1 #define MODULE_USAGE "( remote.name= ) " \ + "( driver.mode= ) " \ "( local.ifname= ) " \ "( net.ip= ) " \ "( net.port= ) " \ @@ -1260,6 +1284,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->sink.impl = impl; impl->sink.direction = PW_DIRECTION_INPUT; + if ((str = pw_properties_get(props, "driver.mode")) != NULL) { + if (spa_streq(str, "source")) { + pw_properties_set(impl->sink.props, "audio.ports", "0"); + pw_properties_set(impl->sink.props, "midi.ports", "0"); + } else if (spa_streq(str, "sink")) { + pw_properties_set(impl->source.props, "audio.ports", "0"); + pw_properties_set(impl->source.props, "midi.ports", "0"); + } else if (!spa_streq(str, "duplex")) { + pw_log_error("invalid driver.mode '%s'", str); + res = -EINVAL; + goto error; + } + } + impl->latency = pw_properties_get_uint32(impl->props, "netjack2.latency", DEFAULT_NETWORK_LATENCY); diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 64ad7fb3b..634a42d58 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -49,6 +49,19 @@ * The netjack2 manager module listens for new netjack2 driver messages and will * start a communication channel with them. * + * Messages are received on a (typically) multicast address. + * + * Normally, the driver will specify the number of send and receive channels it + * wants to set up with the manager. If the driver however specifies a don't-care + * value of -1, the audio.ports and midi.ports configuration values of the manager + * are used. + * + * The manager will create the corresponding streams to send and receive data + * to/from the drivers. These are usually sink and sources but with the + * netjack2.connect property, these will be streams that will be autoconnected to + * the default source and sink by the session manager. + * + * * ## Module Name * * `libpipewire-module-netjack2-manager` @@ -107,7 +120,7 @@ * #audio.channels = 2 * #audio.position = [ FL FR ] * source.props = { - * # extra sink properties + * # extra source properties * } * sink.props = { * # extra sink properties diff --git a/src/modules/module-netjack2/packets.h b/src/modules/module-netjack2/packets.h index af60a6c78..69791cd11 100644 --- a/src/modules/module-netjack2/packets.h +++ b/src/modules/module-netjack2/packets.h @@ -120,7 +120,7 @@ struct nj2_packet_header { uint32_t cycle; /* process cycle counter */ uint32_t sub_cycle; /* midi/audio subcycle counter */ int32_t frames; /* process cycle size in frames (can be -1 to indicate entire buffer) */ - uint32_t is_last; /* is it the last packet of a given cycle ('y' or 'n') */ + uint32_t is_last; /* is it the last packet of a given cycle (1=yes or 0=no) */ } __attribute__ ((packed)); #define UDP_HEADER_SIZE 64 /* 40 bytes for IP header in IPV6, 20 in IPV4, 8 for UDP, so take 64 */ From f5c9944e611ec35d817bb3f7199ce66efa57d3c6 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 12 May 2025 12:57:11 +0200 Subject: [PATCH 78/98] netjack2: reverse send/recv roles in driver/manager The params contain the send/recv streams from the point of view of the manager (and not the driver as was assumed before). This means we need to swap send/recv in the driver, not the manager. This makes things interoperate with JACK/netjack2. See #4666 --- src/modules/module-netjack2-driver.c | 14 +++++++++---- src/modules/module-netjack2-manager.c | 29 +++++++++++---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index e1f5eb6ca..0aa9a85cd 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -848,6 +848,10 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p pw_log_warn("invalid follower setup"); return -EINVAL; } + /* the params are from the perspective of the manager, so send is our + * receive (source) and recv is our send (sink) */ + SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); + SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); pw_loop_update_io(impl->main_loop, impl->setup_socket, 0); @@ -1005,10 +1009,12 @@ static int send_follower_available(struct impl *impl) snprintf(params.follower_name, sizeof(params.follower_name), "%s", pw_get_host_name()); params.mtu = htonl(impl->mtu); params.transport_sync = htonl(0); - params.send_audio_channels = htonl(impl->sink.wanted_n_audio); - params.recv_audio_channels = htonl(impl->source.wanted_n_audio); - params.send_midi_channels = htonl(impl->sink.wanted_n_midi); - params.recv_midi_channels = htonl(impl->source.wanted_n_midi); + /* send/recv is from the perspective of the manager, so what we send (sink) + * is recv on the manager and vice versa. */ + params.recv_audio_channels = htonl(impl->sink.wanted_n_audio); + params.send_audio_channels = htonl(impl->source.wanted_n_audio); + params.recv_midi_channels = htonl(impl->sink.wanted_n_midi); + params.send_midi_channels = htonl(impl->source.wanted_n_midi); params.sample_encoder = htonl(NJ2_ENCODER_FLOAT); params.follower_sync_mode = htonl(1); params.network_latency = htonl(impl->latency); diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 634a42d58..2cef87218 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -1012,29 +1012,28 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param peer->params.sample_encoder = impl->encoding; peer->params.kbps = impl->kbps; - /* params send and recv are from the client point of view and reversed for the - * manager, so when the client sends, we receive in a source etc. We swap the params - * later after we replied to the client. */ + /* params send and recv are from the manager point of view and reversed for the + * driver. So, for us send = sink and recv = source */ if (peer->params.send_audio_channels < 0) - peer->params.send_audio_channels = follower->source.n_audio; + peer->params.send_audio_channels = follower->sink.n_audio; if (peer->params.recv_audio_channels < 0) - peer->params.recv_audio_channels = follower->sink.n_audio; + peer->params.recv_audio_channels = follower->source.n_audio; if (peer->params.send_midi_channels < 0) - peer->params.send_midi_channels = follower->source.n_midi; + peer->params.send_midi_channels = follower->sink.n_midi; if (peer->params.recv_midi_channels < 0) - peer->params.recv_midi_channels = follower->sink.n_midi; + peer->params.recv_midi_channels = follower->source.n_midi; - follower->source.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; + follower->source.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; follower->source.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.send_audio_channels != follower->source.info.channels) { - follower->source.info.channels = SPA_MIN(peer->params.send_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + if ((uint32_t)peer->params.recv_audio_channels != follower->source.info.channels) { + follower->source.info.channels = SPA_MIN(peer->params.recv_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); for (i = 0; i < follower->source.info.channels; i++) follower->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } - follower->sink.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; + follower->sink.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; follower->sink.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.recv_audio_channels != follower->sink.info.channels) { - follower->sink.info.channels = SPA_MIN(peer->params.recv_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); + if ((uint32_t)peer->params.send_audio_channels != follower->sink.info.channels) { + follower->sink.info.channels = SPA_MIN(peer->params.send_audio_channels, (int)SPA_AUDIO_MAX_CHANNELS); for (i = 0; i < follower->sink.info.channels; i++) follower->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } @@ -1125,10 +1124,6 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param nj2_dump_session_params(params); send(follower->socket->fd, params, sizeof(*params), 0); - /* now swap send and recv to make it match our point of view */ - SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); - SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); - return 0; create_failed: From 6b9f34021968274a0d2585df1b15bff04f71c56a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 13 May 2025 11:20:55 +0200 Subject: [PATCH 79/98] alsa-seq: remove leftover debug line --- spa/plugins/alsa/alsa-seq.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index b0be92098..04280f291 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -78,8 +78,6 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu spa_log_debug(state->log, "%p: ALSA UMP MIDI enabled", state); state->ump = true; } - state->ump = false; - return 0; } From 2a8d5c1f40ffcc16c15bc2bd5b6700ccb59b6b3e Mon Sep 17 00:00:00 2001 From: Robert Mader Date: Thu, 15 May 2025 21:38:35 +0200 Subject: [PATCH 80/98] v4l2: Re-enable first buffer skip for jpeg types The workaround is typically needed with usb-cameras using jpeg streams. Re-enabling the skip shouldn't affect what the commit was trying to do, i.e. fix encoded streams like h264 etc. Fixes: bcf0c0cf8 (v4l2: only skip buffer for raw formats) (cherry picked from commit e387772dc2a4316d88d41b528b6b2b16eccf32ba) --- spa/plugins/v4l2/v4l2-utils.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c index 96ce68ff7..887094eff 100644 --- a/spa/plugins/v4l2/v4l2-utils.c +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -1873,7 +1873,9 @@ static int spa_v4l2_stream_on(struct impl *this) spa_log_debug(this->log, "starting"); - if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw) + if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw || + port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_mjpg || + port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_jpeg) port->first_buffer = true; else port->first_buffer = false; From e7610de30536d66745b0a26296fe3abf8e977d46 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 8 May 2025 18:14:02 +0200 Subject: [PATCH 81/98] alsa: clamp audio.channels to MAX_CHANNELS So that we don't end up trying to use too many channels later on. --- spa/plugins/alsa/alsa-pcm.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c index 36834eaed..f8db5eb0d 100644 --- a/spa/plugins/alsa/alsa-pcm.c +++ b/spa/plugins/alsa/alsa-pcm.c @@ -162,6 +162,11 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) int fmt_change = 0; if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { state->default_channels = atoi(s); + if (state->default_channels > SPA_AUDIO_MAX_CHANNELS) { + spa_log_warn(state->log, "%p: %s: %s > %d, clamping", + state, k, s, SPA_AUDIO_MAX_CHANNELS); + state->default_channels = SPA_AUDIO_MAX_CHANNELS; + } fmt_change++; } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { state->default_rate = atoi(s); From a66377cf42a9aad1bdb0940247319659451e7097 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 8 May 2025 18:16:53 +0200 Subject: [PATCH 82/98] alsa: only use default rate and channels when valid Check the user provided rate and channels and only use them to limit the rate and channels when they are valid. --- spa/plugins/alsa/alsa-pcm.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c index f8db5eb0d..4c159e020 100644 --- a/spa/plugins/alsa/alsa-pcm.c +++ b/spa/plugins/alsa/alsa-pcm.c @@ -1568,15 +1568,18 @@ static int add_channels(struct state *state, bool all, uint32_t index, uint32_t spa_log_debug(state->log, "channels (%d %d) default:%d all:%d", min, max, state->default_channels, all); - if (state->default_channels != 0 && !all) { - if (min < state->default_channels) - min = state->default_channels; - if (max > state->default_channels) - max = state->default_channels; - } min = SPA_MIN(min, SPA_AUDIO_MAX_CHANNELS); max = SPA_MIN(max, SPA_AUDIO_MAX_CHANNELS); + if (state->default_channels != 0 && !all) { + if (min > state->default_channels || + max < state->default_channels) + spa_log_warn(state->log, "given audio.channels %d out of range:%d-%d", + state->default_channels, min, max); + else + min = max = state->default_channels; + } + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_channels, 0); if (state->props.use_chmap && (maps = snd_pcm_query_chmaps(hndl)) != NULL) { @@ -1847,10 +1850,12 @@ static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *ne spa_log_debug(state->log, "rate (%d %d)", rmin, rmax); if (state->default_rate != 0) { - if (rmin < state->default_rate) - rmin = state->default_rate; - if (rmax > state->default_rate) - rmax = state->default_rate; + if (rmin > state->default_rate || + rmax < state->default_rate) + spa_log_warn(state->log, "given audio.rate %d out of range:%d-%d", + state->default_rate, rmin, rmax); + else + rmin = rmax = state->default_rate; } spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_iec958Codec, 0); From 9207fea99220dc5ab040efc91908421335ee5d0e Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Sat, 17 May 2025 14:05:23 +0200 Subject: [PATCH 83/98] libcemara: take care of index offset when enumerating controls Add an index offset when enumerating controls. We insert 2 properties before enumerating the controls so the index of the first control needs to have an offset of 2. --- spa/plugins/libcamera/libcamera-source.cpp | 4 ++-- spa/plugins/libcamera/libcamera-utils.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp index f570499d1..0dffbc92b 100644 --- a/spa/plugins/libcamera/libcamera-source.cpp +++ b/spa/plugins/libcamera/libcamera-source.cpp @@ -273,7 +273,7 @@ next: default: return spa_libcamera_enum_controls(impl, GET_OUT_PORT(impl, 0), - seq, result.index - 2, num, filter); + seq, result.index, 2, num, filter); } break; } @@ -535,7 +535,7 @@ next: switch (id) { case SPA_PARAM_PropInfo: - return spa_libcamera_enum_controls(impl, port, seq, start, num, filter); + return spa_libcamera_enum_controls(impl, port, seq, start, 0, num, filter); case SPA_PARAM_EnumFormat: return spa_libcamera_enum_format(impl, port, seq, start, num, filter); diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp index 92185c4b0..bbd351afe 100644 --- a/spa/plugins/libcamera/libcamera-utils.cpp +++ b/spa/plugins/libcamera/libcamera-utils.cpp @@ -495,7 +495,7 @@ static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id) static int spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq, - uint32_t start, uint32_t num, + uint32_t start, uint32_t offset, uint32_t num, const struct spa_pod *filter) { const ControlInfoMap &info = impl->camera->controls(); @@ -513,7 +513,7 @@ spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq, result.next = start; auto it = info.begin(); - for (skip = result.next; skip; skip--) + for (skip = result.next - offset; skip; skip--) it++; if (false) { From a968027bdc467fae7ab2a4aa115c9e3d554215c0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 19 May 2025 16:48:04 +0200 Subject: [PATCH 84/98] adapter: handle -ENOTSUP for commands When using custom commands, the converter might return -ENOTSUP and we should ignore this. --- spa/plugins/audioconvert/audioadapter.c | 5 ++++- spa/plugins/videoconvert/videoadapter.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 5c0eb4d19..d12665180 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -1045,7 +1045,10 @@ static int impl_node_send_command(void *object, const struct spa_command *comman break; } - if ((res = spa_node_send_command(this->target, command)) < 0) { + res = spa_node_send_command(this->target, command); + if (res == -ENOTSUP) + res = 0; + if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", this, SPA_NODE_COMMAND_ID(command), spa_strerror(res)); diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index ebc11e1f0..4e0110cb5 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -1012,7 +1012,10 @@ static int impl_node_send_command(void *object, const struct spa_command *comman break; } - if ((res = spa_node_send_command(this->target, command)) < 0) { + res = spa_node_send_command(this->target, command); + if (res == -ENOTSUP) + res = 0; + if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", this, SPA_NODE_COMMAND_ID(command), spa_strerror(res)); From 603aae2dc855d7920284bad8078af5d726b5bf1d Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Mon, 19 May 2025 17:24:13 +0200 Subject: [PATCH 85/98] daemon/pipewire.conf.avail: Add snippet enabling module-raop --- src/daemon/pipewire.conf.avail/50-raop.conf.in | 4 ++++ src/daemon/pipewire.conf.avail/meson.build | 1 + 2 files changed, 5 insertions(+) create mode 100644 src/daemon/pipewire.conf.avail/50-raop.conf.in diff --git a/src/daemon/pipewire.conf.avail/50-raop.conf.in b/src/daemon/pipewire.conf.avail/50-raop.conf.in new file mode 100644 index 000000000..088f4a597 --- /dev/null +++ b/src/daemon/pipewire.conf.avail/50-raop.conf.in @@ -0,0 +1,4 @@ +context.modules = [ + # Use mDNS to detect and load module-raop-sink + { name = libpipewire-module-raop-discover } +] diff --git a/src/daemon/pipewire.conf.avail/meson.build b/src/daemon/pipewire.conf.avail/meson.build index 8b071ba42..b936a217b 100644 --- a/src/daemon/pipewire.conf.avail/meson.build +++ b/src/daemon/pipewire.conf.avail/meson.build @@ -1,6 +1,7 @@ conf_files = [ '10-rates.conf', '20-upmix.conf', + '50-raop.conf', ] foreach c : conf_files From 61168adb924887a955c46ca389a81001c6582875 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 20 May 2025 10:41:59 +0200 Subject: [PATCH 86/98] adapter: log command errors when no converter --- spa/plugins/audioconvert/audioadapter.c | 2 +- spa/plugins/videoconvert/videoadapter.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index d12665180..43a47e25c 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -1046,7 +1046,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman } res = spa_node_send_command(this->target, command); - if (res == -ENOTSUP) + if (res == -ENOTSUP && this->target != this->follower) res = 0; if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index 4e0110cb5..078c90553 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -1013,7 +1013,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman } res = spa_node_send_command(this->target, command); - if (res == -ENOTSUP) + if (res == -ENOTSUP && this->target != this->follower) res = 0; if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", From 2a3f92e67fcc7f20e435a1c6eca145c3e10bfaba Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 20 May 2025 11:47:10 +0200 Subject: [PATCH 87/98] client-node: let all events go to the node Just pass all events to the node and only error out when they return an error value. Make sure we pass the result values of the command around. --- src/modules/module-client-node/remote-node.c | 17 +++++++++-------- src/pipewire/impl-node.c | 7 ++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c index 8b5c7daf3..52134eaa4 100644 --- a/src/modules/module-client-node/remote-node.c +++ b/src/modules/module-client-node/remote-node.c @@ -481,15 +481,16 @@ static int client_node_command(void *_data, const struct spa_command *command) pw_proxy_error(proxy, res, "suspend failed"); } break; - case SPA_NODE_COMMAND_RequestProcess: - res = pw_impl_node_send_command(node, command); - break; default: - pw_log_warn("unhandled node command %d (%s)", id, - spa_debug_type_find_name(spa_type_node_command_id, id)); - res = -ENOTSUP; - pw_proxy_errorf(proxy, res, "command %d (%s) not supported", id, - spa_debug_type_find_name(spa_type_node_command_id, id)); + res = pw_impl_node_send_command(node, command); + if (res < 0) { + pw_log_warn("node command %d (%s) error: %s (%d)", id, + spa_debug_type_find_name(spa_type_node_command_id, id), + spa_strerror(res), res); + pw_proxy_errorf(proxy, res, "command %d (%s) error: %s (%d)", id, + spa_debug_type_find_name(spa_type_node_command_id, id), + spa_strerror(res), res); + } } return res; } diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c index 351f98b06..e5636d561 100644 --- a/src/pipewire/impl-node.c +++ b/src/pipewire/impl-node.c @@ -660,19 +660,20 @@ static int node_send_command(void *object, const struct spa_command *command) struct resource_data *data = object; struct pw_impl_node *node = data->node; uint32_t id = SPA_NODE_COMMAND_ID(command); + int res; pw_log_debug("%p: got command %d (%s)", node, id, spa_debug_type_find_name(spa_type_node_command_id, id)); switch (id) { case SPA_NODE_COMMAND_Suspend: - suspend_node(node); + res = suspend_node(node); break; default: - spa_node_send_command(node->node, command); + res = spa_node_send_command(node->node, command); break; } - return 0; + return res; } static const struct pw_node_methods node_methods = { From 6115a240d1b486ed5e208541e10b570ffc87134c Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 May 2025 15:23:00 +0200 Subject: [PATCH 88/98] filter-chain: manage graph from source only Only react to the capture stream state change and format changes. The playback and capture streams change state somewhat concurrently and so the graph state might not be consistent. Because only the capture stream will trigger the playback stream and start the processing, we can safely react to capture stream changes only. --- src/modules/module-filter-chain.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 537b8c7d0..50075a779 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -972,7 +972,7 @@ static void param_tag_changed(struct impl *impl, const struct spa_pod *param) pw_stream_update_params(impl->playback, params, 1); } -static void state_changed(void *data, enum pw_stream_state old, +static void capture_state_changed(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct impl *impl = data; @@ -1048,7 +1048,9 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param, struct spa_audio_info_raw info; spa_zero(info); if (param == NULL) { - spa_filter_graph_deactivate(graph); + pw_log_info("module %p: filter deactivate", impl); + if (capture) + spa_filter_graph_deactivate(graph); impl->rate = 0; } else { if ((res = spa_format_audio_raw_parse(param, &info)) < 0) @@ -1087,7 +1089,7 @@ static const struct pw_stream_events in_stream_events = { .destroy = capture_destroy, .process = capture_process, .io_changed = io_changed, - .state_changed = state_changed, + .state_changed = capture_state_changed, .param_changed = capture_param_changed }; @@ -1108,7 +1110,6 @@ static const struct pw_stream_events out_stream_events = { .destroy = playback_destroy, .process = playback_process, .io_changed = io_changed, - .state_changed = state_changed, .param_changed = playback_param_changed, }; From b24ceda8b22b26582462cd7b8e2822b3d81939e9 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 May 2025 15:34:40 +0200 Subject: [PATCH 89/98] filter-graph: lv2 features need a NULL terminator --- spa/plugins/filter-graph/lv2_plugin.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spa/plugins/filter-graph/lv2_plugin.c b/spa/plugins/filter-graph/lv2_plugin.c index a2af22d6a..63c83caaa 100644 --- a/spa/plugins/filter-graph/lv2_plugin.c +++ b/spa/plugins/filter-graph/lv2_plugin.c @@ -234,7 +234,7 @@ struct instance { LV2_Options_Option options[6]; LV2_Feature options_feature; - const LV2_Feature *features[7]; + const LV2_Feature *features[8]; const LV2_Worker_Interface *work_iface; @@ -328,9 +328,11 @@ static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct s c->atom_Float, &fsample_rate }; i->options[5] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL }; - i->options_feature.URI = LV2_OPTIONS__options; - i->options_feature.data = i->options; - i->features[n_features++] = &i->options_feature; + i->options_feature.URI = LV2_OPTIONS__options; + i->options_feature.data = i->options; + i->features[n_features++] = &i->options_feature; + i->features[n_features++] = NULL; + spa_assert(n_features <= SPA_N_ELEMENTS(i->features)); i->instance = lilv_plugin_instantiate(p->p, SampleRate, i->features); if (i->instance == NULL) { From 331d5e03516a99c56b3064dbbbd639a3ae848d36 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 13 May 2025 11:18:58 +0200 Subject: [PATCH 90/98] 1.4.3 --- NEWS | 46 +++++++++++++++++++++++++++++++++++++++++++--- meson.build | 2 +- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index ea4317e1d..9d4af5cde 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,46 @@ +# PipeWire 1.4.3 (2025-05-22) + +This is a bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Many netjack2 improvements. The driver/manager roles were fixed, + MIDI is written correctly and errors are handled better. + - Improvements to UMP sysex handling. + - More small bug fixes and improvements. + + +## PipeWire + - Let all commands go to the node. This makes it possible to send + custom commands. + +## Modules + - Many netjack2 improvements. The driver/manager roles were fixed, + MIDI is written correctly and errors are handled better. + - Improve the filter-graph state management in filter-chain. + +## SPA + - Use default value of filter. (#4619) + - Fix UMP program change conversion to MIDI 1.0. (#4664) + - Skip only the first buffer for raw formats in v4l2 to avoid + dropping important headers when dealing with encoded formats. + - Fix ebur128 port name. (#4667) + - Only convert UMP/MIDI, pass other controls. Fixes OSC and other + control types in the mixer. (#4692) + - Improve UMP sysex handling in alsa seq. + - Improve ALSA audio.channels support. Only use this when the value + is within the valid range. + +## Tools + - debug UMP SysRT messages correctly in pw-mididump. + +## JACK + - Handle sysex in UMP better by appending the converted midi1 + sysex. + + +Older versions: + # PipeWire 1.4.2 (2025-04-14) This is a bugfix release that is API and ABI compatible with @@ -29,9 +72,6 @@ previous 1.x releases. - Fix a leak in the deviceprovider. (#4616) - Fix negotiation and make renegotiation better. - -Older versions: - # PipeWire 1.4.1 (2025-03-14) This is a quick bugfix release that is API and ABI compatible with diff --git a/meson.build b/meson.build index 8b5728eea..12b255ab1 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.4.2', + version : '1.4.3', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', From 483b59a9d95aa084dfcd1c17e13ee27bd106d4b0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 23 May 2025 16:41:00 +0200 Subject: [PATCH 91/98] pod: add bytes start/append/end functions Add functions to dynamically start and build a bytes pod. --- spa/include/spa/pod/builder.h | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h index 553f75512..3dc4a4f1c 100644 --- a/spa/include/spa/pod/builder.h +++ b/spa/include/spa/pod/builder.h @@ -326,6 +326,31 @@ spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len) return SPA_POD_BODY(spa_pod_builder_deref(builder, offset)); } +SPA_API_POD_BUILDER uint32_t +spa_pod_builder_bytes_start(struct spa_pod_builder *builder) +{ + uint32_t offset = builder->state.offset; + const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(0); + spa_pod_builder_raw(builder, &p, sizeof(p)); + return offset; +} +SPA_API_POD_BUILDER int +spa_pod_builder_bytes_append(struct spa_pod_builder *builder, uint32_t offset, + const void *data, uint32_t size) +{ + int res = spa_pod_builder_raw(builder, data, size); + struct spa_pod *pod = spa_pod_builder_deref(builder, offset); + if (pod) + pod->size += size; + return res; +} + +SPA_API_POD_BUILDER int +spa_pod_builder_bytes_end(struct spa_pod_builder *builder, uint32_t offset) +{ + return spa_pod_builder_pad(builder, builder->state.offset); +} + #define SPA_POD_INIT_Pointer(type,value) ((struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { (type), 0, (value) } }) SPA_API_POD_BUILDER int From 76db05a0f8edb8b7d4d896d3860bc555934ad383 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 23 May 2025 16:46:13 +0200 Subject: [PATCH 92/98] Use "8 bit raw midi" for control ports again There is no need to encode the potential format in the format.dsp of control ports, this is just for legacy compatibility with JACK apps. The actual format can be negotiated with the types field. Fixes midi port visibility with apps compiled against 1.2, such as JACK apps in flatpaks. --- pipewire-jack/src/pipewire-jack.c | 20 +------------------ spa/plugins/alsa/alsa-seq-bridge.c | 2 +- spa/plugins/audioconvert/audioconvert.c | 2 +- spa/plugins/bluez5/midi-node.c | 4 ++-- .../videoconvert/videoconvert-ffmpeg.c | 2 +- src/examples/midi-src.c | 2 +- src/modules/module-ffado-driver.c | 2 +- src/modules/module-jack-tunnel.c | 2 +- src/modules/module-netjack2-driver.c | 2 +- src/modules/module-netjack2-manager.c | 2 +- src/modules/module-rtp/stream.c | 2 +- src/modules/module-vban/stream.c | 2 +- src/pipewire/filter.c | 4 +++- src/pipewire/stream.c | 2 +- src/tools/pw-cat.c | 2 +- 15 files changed, 18 insertions(+), 34 deletions(-) diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index 14c062a21..b859b64b0 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -3466,24 +3466,6 @@ static const char* type_to_string(jack_port_type_id_t type_id) } } -static const char* type_to_format_dsp(jack_port_type_id_t type_id) -{ - switch(type_id) { - case TYPE_ID_AUDIO: - return JACK_DEFAULT_AUDIO_TYPE; - case TYPE_ID_VIDEO: - return JACK_DEFAULT_VIDEO_TYPE; - case TYPE_ID_OSC: - return JACK_DEFAULT_OSC_TYPE; - case TYPE_ID_MIDI: - return JACK_DEFAULT_MIDI_TYPE; - case TYPE_ID_UMP: - return JACK_DEFAULT_UMP_TYPE; - default: - return NULL; - } -} - static bool type_is_dsp(jack_port_type_id_t type_id) { switch(type_id) { @@ -5544,7 +5526,7 @@ jack_port_t * jack_port_register (jack_client_t *client, spa_list_init(&p->mix); - pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_format_dsp(type_id)); + pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_string(type_id)); pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name); if (flags > 0x1f) { pw_properties_setf(p->props, PW_KEY_PORT_EXTRA, diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c index 7ec39321c..68e6c91a8 100644 --- a/spa/plugins/alsa/alsa-seq-bridge.c +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -275,7 +275,7 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f snprintf(alias, sizeof(alias), "%s:%s", client_name, port_name); clean_name(alias); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias); diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index c1fa022d1..c36028bcd 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -362,7 +362,7 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); } if (this->group_name[0] != '\0') items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c index b5fd179ea..cfba2bfcf 100644 --- a/spa/plugins/bluez5/midi-node.c +++ b/spa/plugins/bluez5/midi-node.c @@ -2024,13 +2024,13 @@ impl_init(const struct spa_handle_factory *factory, for (i = 0; i < N_PORTS; ++i) { struct port *port = &this->ports[i]; static const struct spa_dict_item in_port_items[] = { - SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), }; static const struct spa_dict_item out_port_items[] = { - SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), diff --git a/spa/plugins/videoconvert/videoconvert-ffmpeg.c b/spa/plugins/videoconvert/videoconvert-ffmpeg.c index ddb810be5..3da0e5677 100644 --- a/spa/plugins/videoconvert/videoconvert-ffmpeg.c +++ b/spa/plugins/videoconvert/videoconvert-ffmpeg.c @@ -228,7 +228,7 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); } if (this->group_name[0] != '\0') items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); diff --git a/src/examples/midi-src.c b/src/examples/midi-src.c index edcaa0f08..12f86d6f2 100644 --- a/src/examples/midi-src.c +++ b/src/examples/midi-src.c @@ -213,7 +213,7 @@ int main(int argc, char *argv[]) PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(struct port), pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_NAME, "output", NULL), NULL, 0); diff --git a/src/modules/module-ffado-driver.c b/src/modules/module-ffado-driver.c index 4345e5a19..1c9c7c590 100644 --- a/src/modules/module-ffado-driver.c +++ b/src/modules/module-ffado-driver.c @@ -772,7 +772,7 @@ static int make_stream_ports(struct stream *s) break; case ffado_stream_type_midi: props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_NAME, port->name, PW_KEY_PORT_PHYSICAL, "true", PW_KEY_PORT_TERMINAL, "true", diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 7ab4c9faf..76ac73c8d 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -515,7 +515,7 @@ static void make_stream_ports(struct stream *s) } else { snprintf(name, sizeof(name), "%s_%d", prefix, i - s->info.channels); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_NAME, name, PW_KEY_PORT_PHYSICAL, "true", NULL); diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 0aa9a85cd..011052e7b 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -452,7 +452,7 @@ static void make_stream_ports(struct stream *s) } else { snprintf(name, sizeof(name), "midi%d", i - s->info.channels); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_AUDIO_CHANNEL, name, PW_KEY_PORT_PHYSICAL, "true", NULL); diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 2cef87218..fe482fd94 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -614,7 +614,7 @@ static void make_stream_ports(struct stream *s) } else { snprintf(name, sizeof(name), "midi%d", i - s->info.channels); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_PHYSICAL, "true", PW_KEY_AUDIO_CHANNEL, name, NULL); diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c index f4d3fa3b8..cbd3ee994 100644 --- a/src/modules/module-rtp/stream.c +++ b/src/modules/module-rtp/stream.c @@ -390,7 +390,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, res = -EINVAL; goto out; } - pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); impl->stride = impl->format_info->size; impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000); if (impl->rate == 0) diff --git a/src/modules/module-vban/stream.c b/src/modules/module-vban/stream.c index efa5af370..10eb34a82 100644 --- a/src/modules/module-vban/stream.c +++ b/src/modules/module-vban/stream.c @@ -307,7 +307,7 @@ struct vban_stream *vban_stream_new(struct pw_core *core, res = -EINVAL; goto out; } - pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); impl->stride = impl->format_info->size; impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000); if (impl->rate == 0) diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c index 519c1e2f7..03706e98a 100644 --- a/src/pipewire/filter.c +++ b/src/pipewire/filter.c @@ -1855,8 +1855,10 @@ void *pw_filter_add_port(struct pw_filter *filter, add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_Midi); else if (spa_streq(str, "8 bit raw control")) add_control_dsp_port_params(impl, p, 0); - else if (spa_streq(str, "32 bit raw UMP")) + else if (spa_streq(str, "32 bit raw UMP")) { add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_UMP); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); + } } /* then override with user provided if any */ if (update_params(impl, p, SPA_ID_INVALID, params, n_params) < 0) diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c index 4af3a3afa..f5684d5e7 100644 --- a/src/pipewire/stream.c +++ b/src/pipewire/stream.c @@ -2053,7 +2053,7 @@ pw_stream_connect(struct pw_stream *stream, pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, str); else if (impl->media_type == SPA_MEDIA_TYPE_application && impl->media_subtype == SPA_MEDIA_SUBTYPE_control) - pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); if (pw_properties_get(impl->port_props, PW_KEY_PORT_GROUP) == NULL) pw_properties_set(impl->port_props, PW_KEY_PORT_GROUP, "stream.0"); diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index 36a78981e..928afed0e 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -1985,7 +1985,7 @@ int main(int argc, char *argv[]) SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); - pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); break; case TYPE_DSD: { From 06941f7315c3ca814771cc8b6612c3d01a8bf968 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 23 May 2025 16:53:42 +0200 Subject: [PATCH 93/98] alsa: remove UMP flag from control format Don't set the UMP type flag on the format. Use the negotiated types flag to decide what format to output. Add support for output to old style midi. Set the UMP type flag only on the new mixer and JACK when UMP is enabled. This ensures that only new (or explicitly requesting) apps get UMP and old apps receive old midi. This makes JACK running on 1.2 in flatpaks work with midi again. --- pipewire-jack/src/pipewire-jack.c | 23 ++++- spa/plugins/alsa/alsa-seq-bridge.c | 13 +-- spa/plugins/alsa/alsa-seq.c | 159 ++++++++++++++++++++--------- spa/plugins/alsa/alsa-seq.h | 3 + spa/plugins/control/mixer.c | 3 +- src/modules/module-jack-tunnel.c | 33 +++--- 6 files changed, 157 insertions(+), 77 deletions(-) diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index b859b64b0..695938f90 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -2549,11 +2549,28 @@ static int param_enum_format(struct client *c, struct port *p, case TYPE_ID_UMP: case TYPE_ID_OSC: case TYPE_ID_MIDI: - *param = spa_pod_builder_add_object(b, - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + { + struct spa_pod_frame f; + int32_t types = 0; + + spa_pod_builder_push_object(b, &f, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + 0); + if (p->object->port.type_id == TYPE_ID_UMP) + types |= 1u<object->port.type_id == TYPE_ID_OSC) + types |= 1u<have_format = false; } else { struct spa_audio_info info = { 0 }; - uint32_t types; + uint32_t types = 0; if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) return err; @@ -646,13 +644,12 @@ static int port_set_format(void *object, struct seq_port *port, if ((err = spa_pod_parse_object(format, SPA_TYPE_OBJECT_Format, NULL, - SPA_FORMAT_CONTROL_types, SPA_POD_Int(&types))) < 0) + SPA_FORMAT_CONTROL_types, SPA_POD_OPT_Int(&types))) < 0) return err; - if (types != 1u << SPA_CONTROL_UMP) - return -EINVAL; port->current_format = info; port->have_format = true; + port->control_types = types; } port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 04280f291..75d9c604a 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -586,7 +586,8 @@ static int prepare_buffer(struct seq_state *state, struct seq_port *port) spa_pod_builder_init(&port->builder, port->buffer->buf->datas[0].data, port->buffer->buf->datas[0].maxsize); - spa_pod_builder_push_sequence(&port->builder, &port->frame, 0); + spa_pod_builder_push_sequence(&port->builder, &port->frame, 0); + port->ev_offset = SPA_IDX_INVALID; return 0; } @@ -620,10 +621,8 @@ static int process_read(struct seq_state *state) struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; const bool ump = state->ump; uint32_t i; - uint32_t *data; uint8_t midi1_data[MAX_EVENT_SIZE]; uint32_t ump_data[MAX_EVENT_SIZE]; - long size; int res = -1; /* copy all new midi events into their port buffers */ @@ -631,10 +630,11 @@ static int process_read(struct seq_state *state) const snd_seq_addr_t *addr; struct seq_port *port; uint64_t ev_time, diff; - uint32_t offset; + uint32_t offset, ev_type; void *event; - uint8_t *midi1_ptr; - size_t midi1_size = 0; + uint8_t *data_ptr; + size_t data_size = 0; + long size; uint64_t ump_state = 0; snd_seq_event_type_t SPA_UNUSED type; @@ -679,7 +679,7 @@ static int process_read(struct seq_state *state) continue; if ((res = prepare_buffer(state, port)) < 0) { - spa_log_debug(state->log, "can't prepare buffer port:%p %d.%d: %s", + spa_log_warn(state->log, "can't prepare buffer port:%p %d.%d: %s", port, addr->client, addr->port, spa_strerror(res)); continue; } @@ -702,8 +702,8 @@ static int process_read(struct seq_state *state) #ifdef HAVE_ALSA_UMP snd_seq_ump_event_t *ev = event; - data = (uint32_t*)&ev->ump[0]; - size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; + data_ptr = (uint8_t*)&ev->ump[0]; + data_size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; #else spa_assert_not_reached(); #endif @@ -712,40 +712,69 @@ static int process_read(struct seq_state *state) snd_midi_event_reset_decode(stream->codec); if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { - spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + spa_log_warn(state->log, "decode failed: %s", snd_strerror(data_size)); continue; } - - midi1_ptr = midi1_data; - midi1_size = size; + data_ptr = midi1_data; + data_size = size; } - do { - if (!ump) { - data = ump_data; - size = spa_ump_from_midi(&midi1_ptr, &midi1_size, + ev_type = (port->control_types & (1u << SPA_CONTROL_UMP)) ? + SPA_CONTROL_UMP : SPA_CONTROL_Midi; + + spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", + type, ev_time, offset, data_size, addr->client, addr->port); + + if ((ump && ev_type == SPA_CONTROL_UMP) || + (!ump && ev_type == SPA_CONTROL_Midi)) { + /* no conversion needed */ + spa_pod_builder_control(&port->builder, offset, ev_type); + spa_pod_builder_bytes(&port->builder, data_ptr, data_size); + } + else if (ump) { + bool continued = port->ev_offset != SPA_IDX_INVALID; + + /* UMP -> MIDI */ + size = spa_ump_to_midi((uint32_t*)data_ptr, data_size, + midi1_data, sizeof(midi1_data)); + if (size < 0) + continue; + + if (!continued) { + spa_pod_builder_control(&port->builder, offset, ev_type); + port->ev_offset = spa_pod_builder_bytes_start(&port->builder); + if (midi1_data[0] == 0xf0) + continued = true; + } else { + if (midi1_data[size-1] == 0xf7) + continued = false; + } + spa_pod_builder_bytes_append(&port->builder, port->ev_offset, midi1_data, size); + + if (!continued) { + spa_pod_builder_bytes_end(&port->builder, port->ev_offset); + port->ev_offset = SPA_IDX_INVALID; + } + } else { + /* MIDI -> UMP */ + while (data_size > 0) { + size = spa_ump_from_midi(&data_ptr, &data_size, ump_data, sizeof(ump_data), 0, &ump_state); if (size <= 0) break; + + spa_pod_builder_control(&port->builder, offset, ev_type); + spa_pod_builder_bytes(&port->builder, ump_data, size); } + } - spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", - type, ev_time, offset, size, addr->client, addr->port); - - spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&port->builder, data, size); - - /* make sure we can fit at least one control event of max size otherwise - * we keep the event in the queue and try to copy it in the next cycle */ - if (port->builder.state.offset + - sizeof(struct spa_pod_control) + - MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) - goto done; - - } while (!ump); + /* make sure we can fit at least one control event of max size otherwise + * we keep the event in the queue and try to copy it in the next cycle */ + if (port->builder.state.offset + + sizeof(struct spa_pod_control) + + MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) + break; } - -done: if (res < 0 && res != -EAGAIN) spa_log_warn(state->log, "event read failed: %s", snd_strerror(res)); @@ -760,6 +789,8 @@ done: continue; if (prepare_buffer(state, port) >= 0) { + if (port->ev_offset != SPA_IDX_INVALID) + spa_pod_builder_bytes_end(&port->builder, port->ev_offset); spa_pod_builder_pop(&port->builder, &port->frame); port->buffer->buf->datas[0].chunk->offset = 0; @@ -846,9 +877,7 @@ static int process_write(struct seq_state *state) SPA_POD_SEQUENCE_FOREACH(pod, c) { size_t body_size; uint8_t *body; - - if (c->type != SPA_CONTROL_UMP) - continue; + int size; body = SPA_POD_BODY(&c->value); body_size = SPA_POD_BODY_SIZE(&c->value); @@ -862,33 +891,67 @@ static int process_write(struct seq_state *state) if (ump) { #ifdef HAVE_ALSA_UMP + uint8_t *ump_data; + uint32_t data[MAX_EVENT_SIZE]; snd_seq_ump_event_t ev; - snd_seq_ump_ev_clear(&ev); - snd_seq_ev_set_ump_data(&ev, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); - snd_seq_ev_set_source(&ev, state->event.addr.port); - snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); - snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + do { + switch (c->type) { + case SPA_CONTROL_UMP: + ump_data = body; + size = body_size; + body_size = 0; + break; + case SPA_CONTROL_Midi: + size = spa_ump_from_midi(&body, &body_size, + data, sizeof(data), 0, &port->ump_state); + ump_data = (uint8_t*)data; + break; + default: + size = 0; + body_size = 0; + continue; + } + if (size <= 0) + break; - if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { - spa_log_warn(state->log, "failed to output event: %s", - snd_strerror(err)); - } + snd_seq_ump_ev_clear(&ev); + snd_seq_ev_set_ump_data(&ev, ump_data, SPA_MIN(sizeof(ev.ump), (size_t)size)); + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } + } while (body_size > 0); #else spa_assert_not_reached(); #endif } else { snd_seq_event_t ev; uint8_t data[MAX_EVENT_SIZE]; - int size; + uint8_t *midi_data; - if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) + switch (c->type) { + case SPA_CONTROL_UMP: + if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) + continue; + midi_data = data; + break; + case SPA_CONTROL_Midi: + midi_data = body; + size = body_size; + break; + default: continue; + } if (first) snd_seq_ev_clear(&ev); - if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) < 0) { + if ((size = snd_midi_event_encode(stream->codec, midi_data, size, &ev)) < 0) { spa_log_warn(state->log, "failed to encode event: %s", snd_strerror(size)); snd_midi_event_reset_encode(stream->codec); first = true; diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 274311c70..7e9fc4297 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -80,7 +80,10 @@ struct seq_port { struct buffer *buffer; struct spa_pod_builder builder; struct spa_pod_frame frame; + uint32_t ev_offset; + uint64_t ump_state; + uint32_t control_types; struct spa_audio_info current_format; unsigned int have_format:1; unsigned int valid:1; diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c index e97072d45..cfe3c394b 100644 --- a/spa/plugins/control/mixer.c +++ b/spa/plugins/control/mixer.c @@ -298,7 +298,8 @@ static int port_enum_formats(void *object, struct port *port, *param = spa_pod_builder_add_object(builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(SPA_ID_INVALID)); break; default: return 0; diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 76ac73c8d..7cce66b0b 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -247,7 +247,6 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s struct spa_pod_sequence *seq; struct spa_pod_control *c; int res; - bool in_sysex = false; uint8_t tmp[n_samples * 4]; size_t tmp_size = 0; @@ -267,23 +266,23 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s if (c->type != SPA_CONTROL_UMP) continue; + switch (c->type) { + case SPA_CONTROL_UMP: + size = spa_ump_to_midi(SPA_POD_BODY(&c->value), + SPA_POD_BODY_SIZE(&c->value), &tmp[tmp_size], sizeof(tmp) - tmp_size); + if (size <= 0) + continue; + tmp_size += size; + break; + case SPA_CONTROL_Midi: + tmp_size = SPA_POD_BODY_SIZE(&c->value); + memcpy(tmp, SPA_POD_BODY(&c->value), SPA_MIN(sizeof(tmp), tmp_size)); + break; + } - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), &tmp[tmp_size], sizeof(tmp) - tmp_size); - if (size <= 0) - continue; - - if (impl->fix_midi) - fix_midi_event(&tmp[tmp_size], size); - - if (!in_sysex && tmp[tmp_size] == 0xf0) - in_sysex = true; - - tmp_size += size; - if (in_sysex && tmp[tmp_size-1] == 0xf7) - in_sysex = false; - - if (!in_sysex) { + if (tmp[0] != 0xf0 || tmp[tmp_size-1] == 0xf7) { + if (impl->fix_midi) + fix_midi_event(tmp, tmp_size); if ((res = jack.midi_event_write(dst, c->offset, tmp, tmp_size)) < 0) pw_log_warn("midi %p: can't write event: %s", dst, spa_strerror(res)); From c507c4b0ffa094d92c76ad4589ad776b677d94d2 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 26 May 2025 15:44:51 +0200 Subject: [PATCH 94/98] adapter: negotiate from target to follower Since 3abda54d80cb02cacb25b9a66ce2808dc4615b34 we prefer the format of the filter. This reverses the selection of the default value when negotiating buffers from the target to the follower. If the follower does not select a reasonable value for the buffer size, for example, this then results in wrongly sized buffers. Fix this by reversing the order of allocation from target to follower where we let the target (converter) select a default value, which is more likely to be correct. See #4713, #4619 --- spa/plugins/audioconvert/audioadapter.c | 39 ++++++++----------------- spa/plugins/videoconvert/videoadapter.c | 39 ++++++++----------------- 2 files changed, 24 insertions(+), 54 deletions(-) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 43a47e25c..f15d5a4ca 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -442,8 +442,8 @@ static int negotiate_buffers(struct impl *this) state = 0; param = NULL; - if ((res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, + if ((res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) < 0) { if (res == -ENOENT) @@ -456,8 +456,8 @@ static int negotiate_buffers(struct impl *this) } state = 0; - if ((res = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + if ((res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { debug_params(this, this->target, @@ -497,7 +497,7 @@ static int negotiate_buffers(struct impl *this) if (this->async) buffers = SPA_MAX(2u, buffers); - spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + spa_log_info(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); align = SPA_MAX(align, this->max_align); @@ -941,27 +941,13 @@ static int negotiate_format(struct impl *this) spa_node_send_command(this->follower, &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); - /* first try the ideal converter format, which is likely passthrough */ - tstate = 0; - fres = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, - NULL, &format, &b); - if (fres == 1) { - fstate = 0; - res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, - format, &format, &b); - if (res == 1) - goto found; - } - - /* then try something the follower can accept */ + /* The target has been negotiated on its other ports and so it can propose + * a passthrough format or an ideal conversion. We use the suggestions of the + * target to find the best follower format */ for (fstate = 0;;) { format = NULL; - res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, + res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_EnumFormat, &fstate, NULL, &format, &b); @@ -971,8 +957,8 @@ static int negotiate_format(struct impl *this) break; tstate = 0; - fres = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + fres = node_port_enum_params_sync(this, this->follower, + this->direction, 0, SPA_PARAM_EnumFormat, &tstate, format, &format, &b); if (fres == 0 && res == 1) @@ -981,7 +967,6 @@ static int negotiate_format(struct impl *this) res = fres; break; } -found: if (format == NULL) { debug_params(this, this->follower, this->direction, 0, SPA_PARAM_EnumFormat, format, "follower format", res); diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index 078c90553..e10a8adf5 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -416,8 +416,8 @@ static int negotiate_buffers(struct impl *this) state = 0; param = NULL; - if ((res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, + if ((res = spa_node_port_enum_params_sync(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) < 0) { if (res == -ENOENT) @@ -430,8 +430,8 @@ static int negotiate_buffers(struct impl *this) } state = 0; - if ((res = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { debug_params(this, this->target, @@ -471,7 +471,7 @@ static int negotiate_buffers(struct impl *this) if (this->async) buffers = SPA_MAX(2u, buffers); - spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + spa_log_info(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); align = SPA_MAX(align, this->max_align); @@ -908,27 +908,13 @@ static int negotiate_format(struct impl *this) spa_node_send_command(this->follower, &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); - /* first try the ideal converter format, which is likely passthrough */ - tstate = 0; - fres = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, - NULL, &format, &b); - if (fres == 1) { - fstate = 0; - res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, - format, &format, &b); - if (res == 1) - goto found; - } - - /* then try something the follower can accept */ + /* The target has been negotiated on its other ports and so it can propose + * a passthrough format or an ideal conversion. We use the suggestions of the + * target to find the best follower format */ for (fstate = 0;;) { format = NULL; - res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, + res = spa_node_port_enum_params_sync(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_EnumFormat, &fstate, NULL, &format, &b); @@ -938,8 +924,8 @@ static int negotiate_format(struct impl *this) break; tstate = 0; - fres = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + fres = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, SPA_PARAM_EnumFormat, &tstate, format, &format, &b); if (fres == 0 && res == 1) @@ -948,7 +934,6 @@ static int negotiate_format(struct impl *this) res = fres; break; } -found: if (format == NULL) { debug_params(this, this->follower, this->direction, 0, SPA_PARAM_EnumFormat, format, "follower format", res); From eda42ef2fc9eaa41dd7dc1582ae062d2ce727612 Mon Sep 17 00:00:00 2001 From: Robert Mader Date: Sun, 25 May 2025 03:57:38 +0200 Subject: [PATCH 95/98] gst: src: Change DEFAULT_MIN_BUFFERS back to 1 The change from 1 to 8 was done without justification in the commit message and possibly for debug purposes. Unfortunately it breaks negotiation with the libcamera virtual pipeline, which defaults to 4 buffers. Set the the value to 1 again as successful negotiation - even with an unusually low number of buffers - is usually more desirable than an error. Fixes: 98b7dc7c0 ("gst: don't do set_caps from the pipewire callback") (cherry picked from commit e81fb773224fb5cd4b9399393ca2d2bfab7f4273) --- src/gst/gstpipewiresrc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index 694cbea0e..994534c60 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -41,7 +41,7 @@ GST_DEBUG_CATEGORY_STATIC (pipewire_src_debug); #define GST_CAT_DEFAULT pipewire_src_debug #define DEFAULT_ALWAYS_COPY false -#define DEFAULT_MIN_BUFFERS 8 +#define DEFAULT_MIN_BUFFERS 1 #define DEFAULT_MAX_BUFFERS INT32_MAX #define DEFAULT_RESEND_LAST false #define DEFAULT_KEEPALIVE_TIME 0 From 62a719d71a2c37e618379a2156d80bfc1ade9bcc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 27 May 2025 15:00:43 +0200 Subject: [PATCH 96/98] adapter:handle -ENOENT when enumerating buffers When the follower has no buffer suggestion, it can return -ENOENT, which should not generate an error but simply use the converter buffer suggestion instead. --- spa/plugins/audioconvert/audioadapter.c | 16 ++++++++++------ spa/plugins/videoconvert/videoadapter.c | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index f15d5a4ca..95ec85e0d 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -449,8 +449,9 @@ static int negotiate_buffers(struct impl *this) if (res == -ENOENT) param = NULL; else { - debug_params(this, this->follower, this->direction, 0, - SPA_PARAM_Buffers, param, "follower buffers", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "target buffers", res); return res; } } @@ -460,10 +461,13 @@ static int negotiate_buffers(struct impl *this) this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { - debug_params(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_Buffers, param, "convert buffers", res); - return -ENOTSUP; + if (res == -ENOENT) + res = 0; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_Buffers, param, "follower buffers", res); + return res < 0 ? res : -ENOTSUP; + } } if (param == NULL) return -ENOTSUP; diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index e10a8adf5..5111b9380 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -423,8 +423,9 @@ static int negotiate_buffers(struct impl *this) if (res == -ENOENT) param = NULL; else { - debug_params(this, this->follower, this->direction, 0, - SPA_PARAM_Buffers, param, "follower buffers", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "target buffers", res); return res; } } @@ -434,10 +435,13 @@ static int negotiate_buffers(struct impl *this) this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { - debug_params(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_Buffers, param, "convert buffers", res); - return -ENOTSUP; + if (res == -ENOENT) + res = 0; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_Buffers, param, "follower buffers", res); + return res < 0 ? res : -ENOTSUP; + } } if (param == NULL) return -ENOTSUP; From fc9aa5161961f6e25f13117ee186dd944389f693 Mon Sep 17 00:00:00 2001 From: Jonas Holmberg Date: Wed, 28 May 2025 09:50:43 +0200 Subject: [PATCH 97/98] pod: declare offset as unused in spa_pod_builder_bytes_end() Fixes compiler warning: /usr/include/spa-0.2/spa/pod/builder.h:357:69: error: unused parameter 'offset' [-Werror=unused-parameter] --- spa/include/spa/pod/builder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h index 3dc4a4f1c..46b2f64c1 100644 --- a/spa/include/spa/pod/builder.h +++ b/spa/include/spa/pod/builder.h @@ -346,7 +346,7 @@ spa_pod_builder_bytes_append(struct spa_pod_builder *builder, uint32_t offset, } SPA_API_POD_BUILDER int -spa_pod_builder_bytes_end(struct spa_pod_builder *builder, uint32_t offset) +spa_pod_builder_bytes_end(struct spa_pod_builder *builder, uint32_t offset SPA_UNUSED) { return spa_pod_builder_pad(builder, builder->state.offset); } From 3f79bcae5d4415f82907b49221ca05241a7f263c Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 29 May 2025 09:17:47 +0200 Subject: [PATCH 98/98] 1.4.4 --- NEWS | 28 +++++++++++++++++++++++++--- meson.build | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 9d4af5cde..84569fe10 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,28 @@ +# PipeWire 1.4.4 (2025-05-29) + +This is a quick bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Provide better compatibility with 1.2 for MIDI. + - Fix mpv buffer negotiation regression. + - Improve GStreamer compatibility with libcamera. + + +## SPA + - Provide conversions to old style midi in the ALSA sequencer. + - Negotiate only to UMP when using a newer library. + - Fix negotiation direction for buffers, prefer the converter + suggestion instead of the application until we can be sure + applications make good suggestions. + +## GStreamer + - Allow a minimum of 1 buffers again instead of 8. libcamera will + allocate only 4 buffers so we need to support this. + + +Older versions: + # PipeWire 1.4.3 (2025-05-22) This is a bugfix release that is API and ABI compatible with @@ -38,9 +63,6 @@ previous 1.x releases. - Handle sysex in UMP better by appending the converted midi1 sysex. - -Older versions: - # PipeWire 1.4.2 (2025-04-14) This is a bugfix release that is API and ABI compatible with diff --git a/meson.build b/meson.build index 12b255ab1..bca05ecd6 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.4.3', + version : '1.4.4', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3',