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

news/2025/1/24 17:59:30/

系列文章目录

  • 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/news/1565840.html

相关文章

FPGA实现任意角度视频旋转(二)视频90度/270度无裁剪旋转

本文主要介绍如何基于FPGA实现视频的90度/270度无裁剪旋转&#xff0c;关于视频180度实时旋转&#xff0c;请见本专栏前面的文章&#xff0c;旋转效果示意图如下&#xff1a; 为了实时对比旋转效果&#xff0c;采用分屏显示进行处理&#xff0c;左边代表旋转前的视频在屏幕中…

3.2 Go 返回值详解

在 Go 语言中&#xff0c;函数调用完成后会产生一个返回值&#xff0c;该值的类型和数量取决于函数定义。返回值在函数调用结束时通过 return 语句返回&#xff0c;具体规则如下&#xff1a; 一. 返回值的基本规则 1.返回值类型&#xff1a; 返回值必须有类型&#xff0c;类…

云计算中的微服务架构是什么

云计算中的微服务架构是什么 从巨石到微服务&#xff1a;一场架构革命 还记得早期软件开发吗&#xff1f;一个庞大的单体应用&#xff0c;就像一个臃肿的巨人&#xff0c;笨重且脆弱。微服务就是这个巨人的解体与重生。 想象一下&#xff0c;你正在搭建一个电商平台。过去&a…

CentOS7使用源码安装PHP8教程整理

CentOS7使用源码安装PHP8教程整理 下载安装包解压下载的php tar源码包安装所需的一些依赖扩展库安装前的配置修改配置文件1、进入php8的安装包 配置环境变量开机自启启动服务创建软连接常见问题1、checking for icu-uc > 50.1 icu-io icu-i18n... no2、configure: error: Pa…

DBSyncer开源数据同步中间件

一、简介 DBSyncer(英[dbsɪŋkɜː(r)]&#xff0c;美[dbsɪŋkɜː(r) 简称dbs)是一款开源的数据同步中间件&#xff0c;提供MySQL、Oracle、SqlServer、PostgreSQL、Elasticsearch(ES)、Kafka、File、SQL等同步场景。支持上传插件自定义同步转换业务&#xff0c;提供监控全量…

【MySQL】 常见数据类型

MySQL常见数据类型 1.整数类型2.浮点数类型3.定点数类型4.bit类型5.字符串类型 5.1char和varchar类型5.2日期类型和时间类型5.3enum和set类型 1.整数类型 整数类型默认都是有符号整数 类型名称 字节数 类型说明 tinyint 1 带符号的范围-128127&#xff0c;无符号范围…

第三章 C 开头的术语

文章目录 第三章 C 开头的术语以 C 开头的术语CLASSPATH计算属性 (calculated property)调用方法 (call method)Callin 接口 (callin interface)回调方法 (callback method)标准形式 (canonical form)级联点语法 (cascading dot syntax)字符集 (character set)字符流 (characte…

MarsCode青训营打卡Day10(2025年1月23日)|稀土掘金-147.寻找独一无二的糖葫芦串、119.游戏队友搜索

资源引用&#xff1a; 147.寻找独一无二的糖葫芦串 119.游戏队友搜索 今日小记&#xff1a; 回乡聚会陪家人&#xff0c;休息一天~ 稀土掘金-147.寻找独一无二的糖葫芦串&#xff08;147.寻找独一无二的糖葫芦串&#xff09; 题目分析&#xff1a; 给定n个长度为m的字符串表…