pipewire/test/test-filter-graph.c
2026-05-10 05:33:47 +02:00

344 lines
9.6 KiB
C

/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire authors */
/* SPDX-License-Identifier: MIT */
#include "config.h"
#include "pwtest.h"
#include <string.h>
#include <pipewire/pipewire.h>
#include <spa/filter-graph/filter-graph.h>
#include <spa/param/props.h>
#include <spa/pod/dynamic.h>
#include <spa/pod/iter.h>
#include <spa/pod/parser.h>
#include <spa/utils/keys.h>
#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