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

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、目标
  • 二、准备工作
    • [2.1 生成插件模板代码](#2.1 生成插件模板代码)
    • [2.2 理解 gstmyemptyfilter 代码](#2.2 理解 gstmyemptyfilter 代码)
  • 三、开发一个音频插件
    • [3.1 与 C++ 混编](#3.1 与 C++ 混编)
    • [3.2 设置 Caps 模板](#3.2 设置 Caps 模板)
    • [3.3 插件属性与算法参数](#3.3 插件属性与算法参数)
    • [3.4 创建 Delay Line](#3.4 创建 Delay Line)
    • [3.3 处理音频数据](#3.3 处理音频数据)
    • [3.4 Impl 资源的创建与销毁](#3.4 Impl 资源的创建与销毁)
    • [3.5 测试我们的插件](#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 仓库
shell 复制代码
git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git
  1. 生成模板文件,执行下面命令,会生成 gstmyemptyfilter.c/h 两个文件 ,将 .c 文件后缀修改为 .cpp 文件,以便进行 c++ 编译
shell 复制代码
cd /path/to/gst-template/gst-plugin/src
../tools/make_element MyEmptyFilter
  1. 拷贝模板文件到我们工程下,添加如下 messon.build 即可进行编译。具体代码参考 meson.build
python 复制代码
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 基本信息。如果能正常查询,说明插件正确生成了。

shell 复制代码
./gst-inspect-1.0 myemptyfilter
txt 复制代码
Factory Details:
  Rank                     none (0)
  Long-name                MyEmptyFilter
  Klass                    FIXME:Generic
  Description              FIXME:Generic Template Element
  Author                    <<user@hostname.org>>
  Documentation            https://gstreamer.freedesktop.org/documentation/myemptyfilter/#my_empty_filter-page

Plugin Details:
  Name                     myemptyfilter
  Description              my_empty_filter
  Filename                 /Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/libgstmyemptyfilter.dylib
  Version                  1.25.0.1
  License                  LGPL
  Source module            gstreamer
  Documentation            https://gstreamer.freedesktop.org/documentation/myemptyfilter/
  Binary package           GStreamer git
  Origin URL               Unknown package origin

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstMyEmptyFilter

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      ANY
  
  SRC template: 'src'
    Availability: Always
    Capabilities:
      ANY

Element 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 object
                        flags: readable, writable
                        String. Default: "myemptyfilter0"
  
  parent              : The parent of the object
                        flags: readable, writable
                        Object of type "GstObject"
  
  silent              : Produce verbose output ?
                        flags: readable, writable
                        Boolean. Default: false

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

cpp 复制代码
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 添加一个指针:

c 复制代码
struct _GstMyFilter
{
  GstElement element;

  GstPad *sinkpad, *srcpad;

  void* impl;
};

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

cpp 复制代码
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 之类的,此外还需要处理多声道的情况,对于我们这个简单的教程来说这些情况过于复杂了。

cpp 复制代码
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 个参数

cpp 复制代码
  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 方法,以便控制算法参数的值

cpp 复制代码
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

cpp 复制代码
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

cpp 复制代码
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 buffer
  GstMapInfo 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);

  // parameters
  auto 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 data
  for(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 函数,并在其中释放这个指针

cpp 复制代码
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。

参考

相关推荐
yangshuo128113 分钟前
如何将手机的画面和音频全部传输到电脑显示和使用电脑外放输出
智能手机·音视频
winxp-pic19 小时前
视频行为分析系统,可做安全行为检测,比如周界入侵,打架
安全·音视频
学习嵌入式的小羊~1 天前
RV1126+FFMPEG推流项目(11)编码音视频数据 + FFMPEG时间戳处理
ffmpeg·音视频
刘大猫.1 天前
vue3使用音频audio标签
音视频·audio·preload·加载音频文件·vue3使用audio·vue3使用音频·audio标签
优联前端2 天前
Web 音视频(二)在浏览器中解析视频
前端·javascript·音视频·优联前端·webav
我真不会起名字啊2 天前
“深入浅出”系列之音视频开发:(3)音视频开发的学习路线和必备知识
音视频
是店小二呀2 天前
【2024年CSDN平台总结:新生与成长之路】
数据库·人工智能·程序人生·aigc·音视频
无限大.2 天前
优化使用 Flask 构建视频转 GIF 工具
python·flask·音视频
音视频牛哥2 天前
RTMP|RTSP播放器只解码视频关键帧功能探讨
音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·rtsp player·rtmp player