GStreamer 简明教程(九):插件开发,以一个音频特效插件为例

devtools/2025/1/24 10:24:21/

系列文章目录

  • GStreamer 简明教程(一):环境搭建,运行 Basic Tutorial 1 Hello world!
  • GStreamer 简明教程(二):基本概念介绍,Element 和 Pipeline
  • GStreamer 简明教程(三):动态调整 Pipeline
  • GStreamer 简明教程(四):Seek 以及获取文件时长
  • GStreamer 简明教程(五):Pad 相关概念介绍,Pad Capabilities/Templates
  • GStreamer 简明教程(六):利用 Tee 复制流数据,巧用 Queue 实现多线程
  • GStreamer 简明教程(七):实现管道的动态数据流
  • GStreamer 简明教程(八):常用工具介绍
  • GStreamer 简明教程(九):Seek 与跳帧

文章目录

  • 系列文章目录
  • 前言
  • 一、目标
  • 二、准备工作
    • 2.1 生成插件模板代码
    • 2.2 理解 gstmyemptyfilter 代码
  • 三、开发一个音频插件
    • 3.1 与 C++ 混编
    • 3.2 设置 Caps 模板
    • 3.3 插件属性与算法参数
    • 3.4 创建 Delay Line
    • 3.3 处理音频数据
    • 3.4 Impl 资源的创建与销毁
    • 3.5 测试我们的插件
  • 总结
  • 参考


前言

GStreamer 简明教程系列已经更新了 9 期,这些教程基本是我个人在学习官方教程中的一些理解和总结。官方的基础教程中远不止 9 期,但后续的基础教程我决定不再更新了,因为后面的内容基本还是围绕如何使用 GStreamer 中的某种功能来展开的,它不涉及 GStreamer 底层代码的实现逻辑。

回看我最初学习 GStreamer 的目的,最重要是掌握 GStreamer 的设计思想,学习消化后以便自己能设计出一套类似的音视频框架,因此我对 GStreamer 底层的实现逻辑更感兴趣。

此外,在学习基础教程的过程中,越来越多疑问出现在我的脑海中:GStreamer 中的线程模型是怎么样的?资源的声明周期如何控制?线程安全如何保证?状态流转有什么实现技巧?Element 应该处理不同状态的?Element 之间的negotiate(协商)的逻辑是怎么样的?音画同步是怎么做的?Push 模式和Pull 模式之间如何切换?如何处理各类时间?Element 之间如何进行消息通信?…

这些疑问在基础教程并不能给我很好的答案,GStreamer 的官方文档中虽然对这些内容或多或少有所提及,但看过一遍后仍然不得要领,无法准确理解官方文档中所说内容。

那么怎么办?我想到的办法是写更多的代码,更多涉及底层逻辑的代码,最简单的方法就是写 GStreamer 插件。因此,本章我将说明如何写一个 GStreamer 插件。

本文所有代码你可以在 my_plugin - github 找到。

本文基本参考官方教程 Writing a Plugin。更多细节欢迎读者自行阅读官方教程。

一、目标

  1. 写一个音频音效插件,实现音频 Delay(回声) 效果。算法实现可以参考 【音效处理】Delay/Echo 简介,但并不重要,我们只需要知道有这么个算法就行。
  2. 可以通过参数对算法效果进行控制。就像其他 Element 有很多 Property(属性)一样,我们算法可以通过参数来控制行为。
  3. 正确的处理插件资源的生命周期。
  4. 用 C++ 语法实现。这个纯粹是因为写 C++ 代码比较方便,比如可以用智能指针、atomic 等特性。
  5. 提供一个可以运行的示例,方便验证效果。

二、准备工作

2.1 生成插件模板代码

Gstreamer 提供了一些工具,方便我们编写插件,我们可以生成一个插件代码模板,具体步骤如下:

  1. 下载 gst-template.git 仓库
git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git
  1. 生成模板文件,执行下面命令,会生成 gstmyemptyfilter.c/h 两个文件 ,将 .c 文件后缀修改为 .cpp 文件,以便进行 c++ 编译
cd /path/to/gst-template/gst-plugin/src
../tools/make_element MyEmptyFilter
  1. 拷贝模板文件到我们工程下,添加如下 messon.build 即可进行编译。具体代码参考 meson.build
plugin_c_args = ['-DHAVE_CONFIG_H']
plugin_cpp_args = ['-DHAVE_CONFIG_H', '-std=c++17']
plugins_install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0')gstmyemptyfilter_sources = ['gstmyemptyfilter.cpp',
]gstmyemptyfilter = library('gstmyemptyfilter',gstmyemptyfilter_sources,c_args: plugin_c_args,cpp_args: plugin_cpp_args,dependencies : [gst_dep, gstbase_dep, gstaudio_dep],install : true,install_dir : plugins_install_dir,
)
  1. 搭建测试环境。编译 gstmyemptyfilter 后会在某个目录下生成一个动态库,你需要将动态库所在目录添加到 GST_PLUGIN_PATH 环境变量下。具体步骤:
    1. 参考 GStreamer 源码编译,在 Clion 下搭建调试环境 生成调试用的环境变量,例如存放在 env.sh 文件中
    2. 在 env.sh 文件中,找到 GST_PLUGIN_PATH 变量,并在前添加 gstmyemptyfilter 动态库的目录,在本人机器上这个目录是 /Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/
  2. 运行测试示例,验证插件是否运行正常。我提供了一个 gstmyfilter_example 示例,修改几处代码进行运行:
    1. 创建 my_empty_filter。创建 data.my_filter 时使用 “my_empty_filter”,而不是 “my_filter”
    2. 修改 data.source 的 uri 参数为你本地的测试视频路径。完成后,运行示例测试音频是否正常播放。注意记得导入测试环境的环境变量。

2.2 理解 gstmyemptyfilter 代码

我们可以先用 gst-inspect-1.0 查询下 gstmyemptyfilter 基本信息。如果能正常查询,说明插件正确生成了。

./gst-inspect-1.0 myemptyfilter
Factory Details:Rank                     none (0)Long-name                MyEmptyFilterKlass                    FIXME:GenericDescription              FIXME:Generic Template ElementAuthor                    <<user@hostname.org>>Documentation            https://gstreamer.freedesktop.org/documentation/myemptyfilter/#my_empty_filter-pagePlugin Details:Name                     myemptyfilterDescription              my_empty_filterFilename                 /Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/libgstmyemptyfilter.dylibVersion                  1.25.0.1License                  LGPLSource module            gstreamerDocumentation            https://gstreamer.freedesktop.org/documentation/myemptyfilter/Binary package           GStreamer gitOrigin URL               Unknown package originGObject+----GInitiallyUnowned+----GstObject+----GstElement+----GstMyEmptyFilterPad Templates:SINK template: 'sink'Availability: AlwaysCapabilities:ANYSRC template: 'src'Availability: AlwaysCapabilities:ANYElement has no clocking capabilities.
Element has no URI handling capabilities.Pads:SINK: 'sink'Pad Template: 'sink'SRC: 'src'Pad Template: 'src'Element Properties:name                : The name of the objectflags: readable, writableString. Default: "myemptyfilter0"parent              : The parent of the objectflags: readable, writableObject of type "GstObject"silent              : Produce verbose output ?flags: readable, writableBoolean. Default: false

myemptyfilter_init 函数中有插件的信息,其中 my_empty_filter 是元素的名字,myemptyfilter 是插件名字。

static gboolean
myemptyfilter_init (GstPlugin * myemptyfilter)
{return GST_ELEMENT_REGISTER (my_empty_filter, myemptyfilter);
}

gst_my_empty_filter_class_init 函数中,进行了类的初始化,做了这么几个事情:

  1. 覆写 set_propertyget_property 函数,以便控制属性变化的行为。
  2. 设置属性。示例代码中只添加了一个 silent 参数
  3. gst_element_class_set_details_simple 设置元素的一些元数据信息。
  4. gst_element_class_add_pad_template 添加 Pads 的模板信息。

gst_my_empty_filter_init 函数中,进行了实例的初始化,做了这么几个事情:

  1. 为插件添加一个 sink pad,设置 sink pad 的 event function 和 chain function。其中 event function 负责处理各种事件,例如 EOS 或者格式信息等;chain function 负责处理各种 buffer 数据,buffer 中包含视频、音频等数据。并设置 GST_PAD_SET_PROXY_CAPS
  2. 为插件添加一个 src pad,并设置 GST_PAD_SET_PROXY_CAPS
  3. 初始化 silent 的值。

gst_my_empty_filter_set_propertygst_my_empty_filter_get_property 是属性的设置和获取,这块代码很简单,不再说明。

以上就是插件模板代码的基本内容,内容不多,但有挺多细节。以及最重要的 event function 和 chain function 到底要怎么写,目前我们还不知道。

三、开发一个音频插件

这部分完整代码参考 gstmyfilter.cpp

3.1 与 C++ 混编

音频特效算法部分,我准备复用之前写的一些代码,这些代码是 C++ 的,它们是一些类。为了让 gstmyfilter.h 头文件保持纯粹的 C 代码,需要将引入的 C++ 代码隐藏在 .cpp 文件中。因此可以用类似与 C++ Impl 的实现方式,在 GstMyFilter 添加一个指针:

struct _GstMyFilter
{GstElement element;GstPad *sinkpad, *srcpad;void* impl;
};

接着,在 .cpp 文件中,构建一个 Impl 类,将必要的数据放到这个类中进行管理

class GstMyFilterImpl {
public:std::atomic<float> delay = {kDefaultDelay};std::atomic<float> feedback = {kDefaultFeedback};std::atomic<float> dry = {kDefaultDry};std::atomic<float> wet = {kDefaultWet};GstAudioInfo info;std::unique_ptr<libaa::DelayLine<float>> delay_line;
};
  • delay、feedback 等是算法参数的值,使用了 atomic 确保线程安全。
  • GstAudioInfo 用来存放音频参数,包括采样率、声道数等等。
  • delay_line 是一个 libaa::DelayLine 的一个智能指针,用它来实现音频特效算法。libaa::DelayLine 源码也加入到了工程中,你可以参看 aa_delay_line.h 文件。另外,再说明一次,音频特效算法原理不重要,刚兴趣可以参考 【音效处理】Delay/Echo 简介,不感兴趣跳过即可。

3.2 设置 Caps 模板

为了简单起见,我们限定 Caps 之间只能处理 F32LE 单声道数据。这是因为如果有多种格式要处理,那么 delay_line 的需要设置不同的模板值,可能是 int16 或者 double 之类的,此外还需要处理多声道的情况,对于我们这个简单的教程来说这些情况过于复杂了。

const char *ALLOWED_CAPS = R"(audio/x-raw,
format=(string) {F32LE},
rate=(int)[1,48000],
channels=(int)1,
layout=(string) interleaved)";static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(ALLOWED_CAPS));static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS(ALLOWED_CAPS));

3.3 插件属性与算法参数

gst_my_filter_class_init 类初始化函数中,我们添加了 4 个属性,分别对应算法 4 个参数

  g_object_class_install_property(gobject_class, PROP_DELAY,g_param_spec_float("delay", "Delay", "Delay time in milliseconds", 0.0f, kMaxDelayMs, kDefaultDelay,static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |GST_PARAM_CONTROLLABLE)));g_object_class_install_property(gobject_class, PROP_FEEDBACK,g_param_spec_float("feedback", "Feedback", "Feedback factor", 0.0f, 1.0f, kDefaultFeedback,static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |GST_PARAM_CONTROLLABLE)));
// ....

同样地,我们覆写 set_propertyget_property 方法,以便控制算法参数的值

static void gst_my_filter_class_init(GstMyFilterClass *klass) {// ....gobject_class->set_property = gst_my_filter_set_property;gobject_class->get_property = gst_my_filter_get_property;//...
}
static void gst_my_filter_set_property(GObject *object, guint prop_id,const GValue *value, GParamSpec *pspec) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);switch (prop_id) {case PROP_DELAY:priv->delay = g_value_get_float(value);break;case PROP_FEEDBACK:priv->feedback = g_value_get_float(value);break;case PROP_DRY:priv->dry = g_value_get_float(value);break;case PROP_WET:priv->wet = g_value_get_float(value);break;default:G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);break;}
}
static void gst_my_filter_get_property(GObject *object, guint prop_id,GValue *value, GParamSpec *pspec) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);switch (prop_id) {case PROP_DELAY:g_value_set_float(value, priv->delay.load());break;case PROP_FEEDBACK:g_value_set_float(value, priv->feedback.load());break;case PROP_DRY:g_value_set_float(value, priv->dry.load());break;case PROP_WET:g_value_set_float(value, priv->wet.load());break;default:G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);break;}
}

3.4 创建 Delay Line

我们需要知道音频的采样率才能初始化 Delay Line,音频格式信息在 caps 之间协商确定后,通过 GST_EVENT_CAPS 事件发送给 Pipeline 进行处理,因此我们可以在 event function 中拿到音频格式信息,从而初始化 Delay Line

static gboolean gst_my_filter_sink_event(GstPad *pad, GstObject *parent,GstEvent *event) {//....switch (GST_EVENT_TYPE(event)) {case GST_EVENT_CAPS: {GstCaps *caps;gst_event_parse_caps(event, &caps);/* do something with the caps */if(!gst_audio_info_from_caps(&priv->info, caps)) {g_printerr("Failed to parse caps\n");return FALSE;}if(priv->delay_line == nullptr) {priv->delay_line = std::make_unique<libaa::DelayLine<float>>();}const auto max_delay_samples = static_cast<size_t>(kMaxDelayMs / 1000.0f * priv->info.rate);priv->delay_line->resize(max_delay_samples);/* and forward */ret = gst_pad_event_default(pad, parent, event);break;}//....
}

我们根据 kMaxDelayMs 和采样率计算出最大的 buffer size,并对 delay_line 进行设置。

3.3 处理音频数据

chain function 中处理具体的音频数据,拿到数据后进行音频算法处理,接着将数据送给 src pad

static GstFlowReturn gst_my_filter_chain(GstPad *pad, GstObject *parent,GstBuffer *buf) {GstMyFilter *filter;filter = GST_MYFILTER(parent);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);// get bufferGstMapInfo map;gst_buffer_map (buf, &map, GST_MAP_READWRITE);auto num_samples = map.size / GST_AUDIO_INFO_BPS(&priv->info);auto* float_buffer = reinterpret_cast<float*>(map.data);// parametersauto delay_samples = priv->delay.load() / 1000.0f * priv->info.rate;auto feedback = priv->feedback.load();auto dry = priv->dry.load();auto wet = priv->wet.load();// process audio datafor(auto i = 0; i < num_samples; ++i) {auto in = float_buffer[i];auto d_y = priv->delay_line->get(delay_samples);auto d_x = in + feedback * d_y;priv->delay_line->push(d_x);float_buffer[i] = dry * in + wet * d_y;}/* just push out the incoming buffer without touching it */return gst_pad_push(filter->srcpad, buf);
}

GstBuffer 无法直接获取数据,你需要使用 gst_buffer_map 来访问 GstBuffer 中的数据;map.size 中存放的是数据的字节大小,而音频数据个数需要进一步计算,例如 map.size = 2048,音频格式是 F32LE 的话,那么每个音频数据大小应该是 8,因此音频数据个数是 2048/8 = 512。

接着获取到算法参数的当前值,然后遍历整个 float_buffer 进行音频算法处理。

最后,将处理完的 buffer 送给 src pad 即可。

3.4 Impl 资源的创建与销毁

类似于 C++ 中构造函数和析构函数的概念,我们在 gst_my_filter_init 函数中对实例进行初始化,创建 Impl 指针;而析构函数则需要覆写 finalize 函数,并在其中释放这个指针

static void gst_my_filter_class_init(GstMyFilterClass *klass) {//...gobject_class->finalize = gst_my_filter_finalize;//...
}
static void gst_my_filter_init(GstMyFilter *filter) {filter->impl = new GstMyFilterImpl();//...
}
static void gst_my_filter_finalize(GObject* object) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);delete priv;
}

3.5 测试我们的插件

完成了上述内容,我们的音频插件就完成了,可以运行 gstmyfilter_example 中的代码,只需要修改 data.source 的 uri 属性即可。运行成功的话,你可以听到音频有一个回声。

总结

本文介绍了 GStreamer 插件开发的基本流程,通过开发一款音频特效插件,我们了解了很多 GStreamer 的细节,包括 event function、chain function 的运行机制等等。接下来我们将开发更多插件,以便更加深入了解 GStreamer

参考

  • Writing a Plugin
  • my_plugin - github
  • gstmyfilter.cpp
  • 【音效处理】Delay/Echo 简介
  • gstmyfilter_example

http://www.ppmy.cn/devtools/153104.html

相关文章

09 以太坊技术介绍

以太坊技术架构 架构概述 以太坊属于公链&#xff0c;所有节点都具有相同的功能。 以太坊技术架构自上而下依次为应用层、合约层、通信层、共识层、网络层、数据层、存储层。 应用层 应用层主要对应Dapp应用模块&#xff0c;其中包含多种区块链应用场景典型案例。 合约层 …

element-plus中的table为什么相同的数据并没有合并成一个

我想把所有的第一列的名字相同的内容合并。我发现只有相邻的数据合并了。实际上我想做到的是所有的后端给的数据&#xff0c;不管他的顺序怎样的&#xff0c;只有deviceTypeName 一样的都合并的。 在 element-plus 的 table 中&#xff0c;数据合并行通常是基于相邻行的数据进行…

动静态库的理解

文章目录 动静态库是什么静态库动态库动态库的制作编译时路径运行时路径动静态库的链接方式 动静态库是什么 静态库 概念&#xff1a;静态库是指在编译链接时&#xff0c;将库中的代码直接复制到可执行文件中的一种代码共享方式。其文件扩展名为.a&#xff08;在 Unix/Linux 系…

当面对医疗数据量少而模型复杂度高的情况

当面对医疗数据量少而模型复杂度高的情况时&#xff0c;模型训练不能收敛是一个常见问题。这种情况通常会导致过拟合&#xff08;overfitting&#xff09;&#xff0c;即模型在训练集上表现良好但在未见过的数据&#xff08;验证集或测试集&#xff09;上表现不佳。为了应对这一…

初学stm32 --- CAN

目录 CAN介绍 CAN总线拓扑图 CAN总线特点 CAN应用场景 CAN物理层 CAN收发器芯片介绍 CAN协议层 数据帧介绍 CAN位时序介绍 数据同步过程 硬件同步 再同步 CAN总线仲裁 STM32 CAN控制器介绍 CAN控制器模式 CAN控制器模式 CAN控制器框图 发送处理 接收处理 接收过…

【分布式知识】Spring Cloud Gateway实现跨集群应用访问

SpringCloud Gateway实现跨集群应用访问 1. 设置服务注册中心配置 Eureka Server&#xff08;示例&#xff09;配置服务实例&#xff08;示例&#xff09; 2. 配置 Spring Cloud Gateway引入依赖配置 Gateway 3. 配置路由规则4. 服务实例配置&#xff08;跨集群&#xff09;5. …

详解共享WiFi小程序怎么弄!

在数字化时代&#xff0c;共享WiFi项目​正逐渐成为公共场所的新标配&#xff0c;它不仅为用户提供了便捷的上网方式&#xff0c;还为商家带来了额外的收入来源。那么共享wifi怎么弄&#xff0c;如何搭建并运营一个成功的共享WiFi项目呢&#xff1f; 共享WiFi项目通过在公共场所…

分布式光纤应变监测是一种高精度、分布式的监测技术

一、土木工程领域 桥梁结构健康监测 主跨应变监测&#xff1a;在大跨度桥梁的主跨部分&#xff0c;如悬索桥的主缆、斜拉桥的斜拉索和主梁&#xff0c;分布式光纤应变传感器可以沿着这些关键结构部件进行铺设。通过实时监测应变情况&#xff0c;能够精确捕捉到车辆荷载、风荷…