/* PipeWire */ /* SPDX-FileCopyrightText: Copyright © 2026 PipeWire authors */ /* SPDX-License-Identifier: MIT */ #include "config.h" #include "pwtest.h" #include #include #include #include #include #include #include #include #define EXPORT_RAMP_CURRENT "\"ramp:Current\" " #define EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT EXPORT_RAMP_CURRENT \ EXPORT_RAMP_CURRENT EXPORT_RAMP_CURRENT \ EXPORT_RAMP_CURRENT EXPORT_RAMP_CURRENT \ EXPORT_RAMP_CURRENT EXPORT_RAMP_CURRENT \ EXPORT_RAMP_CURRENT EXPORT_RAMP_CURRENT #define EXPORT_RAMP_CURRENT_100 EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 #define EXPORT_RAMP_CURRENT_160 EXPORT_RAMP_CURRENT_100 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 EXPORT_RAMP_CURRENT_10 \ EXPORT_RAMP_CURRENT_10 static bool is_current_notify_info(struct spa_pod *pod) { const char *name = NULL; if (pod == NULL || !spa_pod_is_object(pod)) return false; if (SPA_POD_OBJECT_ID(pod) != SPA_PARAM_PropInfo) return false; if (spa_pod_parse_object(pod, SPA_TYPE_OBJECT_PropInfo, NULL, SPA_PROP_INFO_name, SPA_POD_OPT_String(&name)) < 0) return false; return spa_streq(name, "ramp:Current"); } static bool has_current_notify(struct spa_pod *pod) { struct spa_pod_object *obj = (struct spa_pod_object *)pod; const struct spa_pod_prop *prop; SPA_POD_OBJECT_FOREACH(obj, prop) { struct spa_pod_parser prs; struct spa_pod_frame f; if (prop->key != SPA_PROP_params) continue; spa_pod_parser_pod(&prs, &prop->value); if (spa_pod_parser_push_struct(&prs, &f) < 0) return false; while (true) { const char *name; float value; if (spa_pod_parser_get_string(&prs, &name) < 0) break; if (spa_pod_parser_get_float(&prs, &value) < 0) break; if (spa_streq(name, "ramp:Current") && value > 0.0f) return true; } } return false; } PWTEST(export_controls) { struct pw_loop *loop; struct pw_context *context; struct pw_properties *props; struct spa_handle *handle; struct spa_filter_graph *graph; struct spa_pod_dynamic_builder b; struct spa_pod *pod = NULL; float out[64]; void *outputs[1] = { out }; int res; pw_init(0, NULL); loop = pw_loop_new(NULL); pwtest_ptr_notnull(loop); context = pw_context_new(loop, pw_properties_new( PW_KEY_CONFIG_NAME, "null", "context.modules.allow-empty", "true", "support.dbus", "false", NULL), 0); pwtest_ptr_notnull(context); pw_context_add_spa_lib(context, "^filter\\.graph\\.plugin\\.builtin$", "filter-graph/libspa-filter-graph-plugin-builtin"); pw_context_add_spa_lib(context, "^filter\\.graph$", "filter-graph/libspa-filter-graph"); props = pw_properties_new( "clock.quantum-limit", "128", "filter-graph.n_inputs", "0", "filter-graph.n_outputs", "1", SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph", "filter.graph", "{ nodes = [ { type = builtin name = ramp label = ramp " "control = { Start = 0.0 Stop = 1.0 \"Duration (s)\" = 0.001 } } ] " "outputs = [ \"ramp:Out\" ] " "export.controls = [ \"ramp:Current\" ] }", NULL); pwtest_ptr_notnull(props); handle = pw_context_load_spa_handle(context, "filter.graph", &props->dict); pwtest_ptr_notnull(handle); res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_FilterGraph, (void **)&graph); pwtest_neg_errno_ok(res); pwtest_ptr_notnull(graph); spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); res = spa_filter_graph_enum_notify_info(graph, 0, &b.b, &pod); pwtest_int_eq(res, 1); pwtest_ptr_notnull(pod); pwtest_bool_true(is_current_notify_info(pod)); spa_pod_dynamic_builder_clean(&b); spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); res = spa_filter_graph_enum_notify_info(graph, 1, &b.b, &pod); pwtest_int_eq(res, 0); spa_pod_dynamic_builder_clean(&b); res = spa_filter_graph_activate(graph, &SPA_DICT_ITEMS(SPA_DICT_ITEM(PW_KEY_AUDIO_RATE, "48000"))); pwtest_neg_errno_ok(res); spa_zero(out); res = spa_filter_graph_process(graph, NULL, outputs, SPA_N_ELEMENTS(out)); pwtest_neg_errno_ok(res); spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); res = spa_filter_graph_get_notify(graph, &b.b, &pod); pwtest_int_eq(res, 1); pwtest_ptr_notnull(pod); pwtest_bool_true(has_current_notify(pod)); spa_pod_dynamic_builder_clean(&b); spa_filter_graph_deactivate(graph); pw_unload_spa_handle(handle); pw_properties_free(props); pw_context_destroy(context); pw_loop_destroy(loop); pw_deinit(); return PWTEST_PASS; } PWTEST(export_controls_large_payload) { struct pw_loop *loop; struct pw_context *context; struct pw_properties *props; struct spa_handle *handle; struct spa_filter_graph *graph; struct spa_pod_dynamic_builder b; struct spa_pod *pod = NULL; float out[64]; void *outputs[1] = { out }; int res; pw_init(0, NULL); loop = pw_loop_new(NULL); pwtest_ptr_notnull(loop); context = pw_context_new(loop, pw_properties_new( PW_KEY_CONFIG_NAME, "null", "context.modules.allow-empty", "true", "support.dbus", "false", NULL), 0); pwtest_ptr_notnull(context); pw_context_add_spa_lib(context, "^filter\\.graph\\.plugin\\.builtin$", "filter-graph/libspa-filter-graph-plugin-builtin"); pw_context_add_spa_lib(context, "^filter\\.graph$", "filter-graph/libspa-filter-graph"); props = pw_properties_new( "clock.quantum-limit", "128", "filter-graph.n_inputs", "0", "filter-graph.n_outputs", "1", SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph", "filter.graph", "{ nodes = [ { type = builtin name = ramp label = ramp " "control = { Start = 0.0 Stop = 1.0 \"Duration (s)\" = 0.001 } } ] " "outputs = [ \"ramp:Out\" ] " "export.controls = [ " EXPORT_RAMP_CURRENT_160 "] }", NULL); pwtest_ptr_notnull(props); handle = pw_context_load_spa_handle(context, "filter.graph", &props->dict); pwtest_ptr_notnull(handle); res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_FilterGraph, (void **)&graph); pwtest_neg_errno_ok(res); pwtest_ptr_notnull(graph); res = spa_filter_graph_activate(graph, &SPA_DICT_ITEMS(SPA_DICT_ITEM(PW_KEY_AUDIO_RATE, "48000"))); pwtest_neg_errno_ok(res); spa_zero(out); res = spa_filter_graph_process(graph, NULL, outputs, SPA_N_ELEMENTS(out)); pwtest_neg_errno_ok(res); spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); res = spa_filter_graph_get_notify(graph, &b.b, &pod); pwtest_int_eq(res, 1); pwtest_ptr_notnull(pod); pwtest_int_gt((int)b.b.state.offset, 4096); pwtest_bool_true(has_current_notify(pod)); spa_pod_dynamic_builder_clean(&b); spa_filter_graph_deactivate(graph); pw_unload_spa_handle(handle); pw_properties_free(props); pw_context_destroy(context); pw_loop_destroy(loop); pw_deinit(); return PWTEST_PASS; } PWTEST(export_controls_duplicated_graph_uses_scalar_values) { struct pw_loop *loop; struct pw_context *context; struct pw_properties *props; struct spa_handle *handle; struct spa_filter_graph *graph; struct spa_pod_dynamic_builder b; struct spa_pod *pod = NULL; float in[2][64], out[2][64]; const void *inputs[2] = { in[0], in[1] }; void *outputs[2] = { out[0], out[1] }; int res; pw_init(0, NULL); loop = pw_loop_new(NULL); pwtest_ptr_notnull(loop); context = pw_context_new(loop, pw_properties_new( PW_KEY_CONFIG_NAME, "null", "context.modules.allow-empty", "true", "support.dbus", "false", NULL), 0); pwtest_ptr_notnull(context); pw_context_add_spa_lib(context, "^filter\\.graph\\.plugin\\.builtin$", "filter-graph/libspa-filter-graph-plugin-builtin"); pw_context_add_spa_lib(context, "^filter\\.graph$", "filter-graph/libspa-filter-graph"); props = pw_properties_new( "clock.quantum-limit", "128", "filter-graph.n_inputs", "2", "filter-graph.n_outputs", "2", SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph", "filter.graph", "{ nodes = [ " "{ type = builtin name = copy label = copy } " "{ type = builtin name = ramp label = ramp " "control = { Start = 0.0 Stop = 1.0 \"Duration (s)\" = 0.001 } } ] " "inputs = [ \"copy:In\" ] " "outputs = [ \"copy:Out\" ] " "export.controls = [ \"ramp:Current\" ] }", NULL); pwtest_ptr_notnull(props); handle = pw_context_load_spa_handle(context, "filter.graph", &props->dict); pwtest_ptr_notnull(handle); res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_FilterGraph, (void **)&graph); pwtest_neg_errno_ok(res); pwtest_ptr_notnull(graph); res = spa_filter_graph_activate(graph, &SPA_DICT_ITEMS(SPA_DICT_ITEM(PW_KEY_AUDIO_RATE, "48000"))); pwtest_neg_errno_ok(res); spa_zero(in); spa_zero(out); res = spa_filter_graph_process(graph, inputs, outputs, SPA_N_ELEMENTS(out[0])); pwtest_neg_errno_ok(res); spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); res = spa_filter_graph_get_notify(graph, &b.b, &pod); pwtest_int_eq(res, 1); pwtest_ptr_notnull(pod); pwtest_bool_true(has_current_notify(pod)); spa_pod_dynamic_builder_clean(&b); spa_filter_graph_deactivate(graph); pw_unload_spa_handle(handle); pw_properties_free(props); pw_context_destroy(context); pw_loop_destroy(loop); pw_deinit(); return PWTEST_PASS; } PWTEST_SUITE(filter_graph) { pwtest_add(export_controls, PWTEST_NOARG); pwtest_add(export_controls_large_payload, PWTEST_NOARG); pwtest_add(export_controls_duplicated_graph_uses_scalar_values, PWTEST_NOARG); return PWTEST_PASS; } #undef EXPORT_RAMP_CURRENT_160 #undef EXPORT_RAMP_CURRENT_100 #undef EXPORT_RAMP_CURRENT_10 #undef EXPORT_RAMP_CURRENT