目标

GStreamer建立的pipeline不需要完全关闭。有多种方法可以让数据在任何时候送到pipeline中或者从pipeline中取出。本教程会展示:

如何把外部数据送到pipeline中

如何把数据从pipeline中取出

如何操作这些数据

介绍

有几种方法可以让应用通过pipeline和数据流交互。本教程讲述了最简单的一种,因为使用了专门为这个而创建的element。

专门让应用可以往pipeline里面传入数据的element时appsrc,而appsink就正好相反,让应用可以从pipeline中获得数据。为了避免混淆,我们可以这么来理解,appsrc是一个普通的source element,不过它的数据都是来自外太空,而appsink是一个普通的sink
element,数据从这里出去的就消失不见了。

appsrc和appsink用得非常多,所以他们都自己提供API,你只要连接了gstreamer-app库,那么就可以访问到。在本教程里,我们会使用一种简单地方法通过信号来实现。

appsrc可以有不同的工作模式:在pull模式,在需要时向应用请求数据;在push模式,应用根据自己的节奏把数据推送过来。而且,在push模式,如果已经有了足够的数据,应用可以在push时被阻塞,或者可以经由enough-data和need-data信号来控制。本教程中得例子就采用了这种信号控制的方式,其他没有提及的方法可以在appsrc的文档中查阅。

Buffers

通过pipeline传递的大块数据被称为buffers。因为本例子会制造数据同时也消耗数据,所以我们需要了解GstBuffer。

Source Pads负责制造buffer,这些buffer被sink pad消耗掉。GStreamer在一个个element之间传递这些buffer。

一个buffer只能简单地描述一小片数据,不要认为我们所有的buffer都是一样大小的。而且,buffer有一个时间戳和有效期,这个就描述了什么时候buffer里的数据需要渲染出来。时间戳是个非常复杂和精深的话题,但目前这个简单地解释也足够了。

作为一个例子,一个filesrc会提供“ANY”属性的buffers并且没有时间戳信息。在demux(《GStreamer基础教程03——动态pipeline》)之后,buffers会有一些特定的cap了,比如"video/x-h264",在解码后,每一个buffer都会包含一帧有原始caps的视频帧(比如:video/x-raw-yuv),并且有非常明确地时间戳用来指示这一帧在什么时候显示。

教程

本教程是上一篇教程(《GStreamer基础教程07——多线程和Pad的有效性》)在两个方面的扩展:第一是用appsrc来取代audiotestsrc来生成音频数据;第二是在tee里新加了一个分支,这样流入audio
sink和波形显示的数据同样复制了一份传给appsink。这个appsink就把信息回传给应用,应用就可以通知用户收到了数据或者做其他更复杂的工作。

一个粗糙的波形发生器

[objc] view
plain
 copy

  1. #include <gst/gst.h>
  2. #include <string.h>
  3. #define CHUNK_SIZE 1024   /* Amount of bytes we are sending in each buffer */
  4. #define SAMPLE_RATE 44100 /* Samples per second we are sending */
  5. #define AUDIO_CAPS "audio/x-raw-int,channels=1,rate=%d,signed=(boolean)true,width=16,depth=16,endianness=BYTE_ORDER"
  6. /* Structure to contain all our information, so we can pass it to callbacks */
  7. typedef struct _CustomData {
  8. , *audio_resample, *audio_sink;
  9. , *visual, *video_convert, *video_sink;
  10. GstElement *app_queue, *app_sink;
  11. 4 num_samples;   /* Number of samples generated so far (for timestamp generation) */
  12. gfloat a, b, c, d;     /* For waveform generation */
  13. guint sourceid;        /* To control the GSource */
  14. GMainLoop *main_loop;  /* GLib's Main Loop */
  15. } CustomData;
  16. /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
  17. * The idle handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
  18. * and is removed when appsrc has enough data (enough-data signal).
  19. */
  20. static gboolean push_data (CustomData *data) {
  21. GstBuffer *buffer;
  22. GstFlowReturn ret;
  23. int i;
  24. gint16 *raw;
  25. ; /* Because each sample is 16 bits */
  26. gfloat freq;
  27. /* Create a new empty buffer */
  28. buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
  29. /* Set its timestamp and duration */
  30. 4_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
  31. 4_scale (CHUNK_SIZE, GST_SECOND, SAMPLE_RATE);
  32. /* Generate some psychodelic waveforms */
  33. raw = (gint16 *)GST_BUFFER_DATA (buffer);
  34. data->c += data->d;
  35. 000;
  36. 100 + 11000 * data->d;
  37. ; i < num_samples; i++) {
  38. data->a += data->b;
  39. data->b -= data->a / freq;
  40. 6)(5500 * data->a);
  41. }
  42. data->num_samples += num_samples;
  43. /* Push the buffer into the appsrc */
  44. g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
  45. /* Free the buffer now that we are done with it */
  46. gst_buffer_unref (buffer);
  47. if (ret != GST_FLOW_OK) {
  48. /* We got some error, stop sending data */
  49. return FALSE;
  50. }
  51. return TRUE;
  52. }
  53. /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
  54. * to the mainloop to start pushing data into the appsrc */
  55. static void start_feed (GstElement *source, guint size, CustomData *data) {
  56. ) {
  57. g_print ("Start feeding\n");
  58. data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
  59. }
  60. }
  61. /* This callback triggers when appsrc has enough data and we can stop sending.
  62. * We remove the idle handler from the mainloop */
  63. static void stop_feed (GstElement *source, CustomData *data) {
  64. ) {
  65. g_print ("Stop feeding\n");
  66. g_source_remove (data->sourceid);
  67. ;
  68. }
  69. }
  70. /* The appsink has received a buffer */
  71. static void new_buffer (GstElement *sink, CustomData *data) {
  72. GstBuffer *buffer;
  73. /* Retrieve the buffer */
  74. g_signal_emit_by_name (sink, "pull-buffer", &buffer);
  75. if (buffer) {
  76. /* The only thing we do in this example is print a * to indicate a received buffer */
  77. g_print ("*");
  78. gst_buffer_unref (buffer);
  79. }
  80. }
  81. /* This function is called when an error message is posted on the bus */
  82. static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
  83. GError *err;
  84. gchar *debug_info;
  85. /* Print error details on the screen */
  86. gst_message_parse_error (msg, &err, &debug_info);
  87. g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
  88. g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
  89. g_clear_error (&err);
  90. g_free (debug_info);
  91. g_main_loop_quit (data->main_loop);
  92. }
  93. int main(int argc, charchar *argv[]) {
  94. CustomData data;
  95. GstPadTemplate *tee_src_pad_template;
  96. GstPad *tee_audio_pad, *tee_video_pad, *tee_app_pad;
  97. GstPad *queue_audio_pad, *queue_video_pad, *queue_app_pad;
  98. gchar *audio_caps_text;
  99. GstCaps *audio_caps;
  100. GstBus *bus;
  101. /* Initialize cumstom data structure */
  102. , sizeof (data));
  103. ; /* For waveform generation */
  104. ;
  105. /* Initialize GStreamer */
  106. gst_init (&argc, &argv);
  107. /* Create the elements */
  108. data.app_source = gst_element_factory_make ("appsrc", "audio_source");
  109. data.tee = gst_element_factory_make ("tee", "tee");
  110. data.audio_queue = gst_element_factory_make ("queue", "audio_queue");
  111. data.audio_convert1 = gst_element_factory_make ("audioconvert", "audio_convert1");
  112. data.audio_resample = gst_element_factory_make ("audioresample", "audio_resample");
  113. data.audio_sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
  114. data.video_queue = gst_element_factory_make ("queue", "video_queue");
  115. data.audio_convert2 = gst_element_factory_make ("audioconvert", "audio_convert2");
  116. data.visual = gst_element_factory_make ("wavescope", "visual");
  117. data.video_convert = gst_element_factory_make ("ffmpegcolorspace", "csp");
  118. data.video_sink = gst_element_factory_make ("autovideosink", "video_sink");
  119. data.app_queue = gst_element_factory_make ("queue", "app_queue");
  120. data.app_sink = gst_element_factory_make ("appsink", "app_sink");
  121. /* Create the empty pipeline */
  122. data.pipeline = gst_pipeline_new ("test-pipeline");
  123. if (!data.pipeline || !data.app_source || !data.tee || !data.audio_queue || !data.audio_convert1 ||
  124. !data.audio_resample || !data.audio_sink || !data.video_queue || !data.audio_convert2 || !data.visual ||
  125. !data.video_convert || !data.video_sink || !data.app_queue || !data.app_sink) {
  126. g_printerr ("Not all elements could be created.\n");
  127. ;
  128. }
  129. /* Configure wavescope */
  130. , "style", 0, NULL);
  131. /* Configure appsrc */
  132. audio_caps_text = g_strdup_printf (AUDIO_CAPS, SAMPLE_RATE);
  133. audio_caps = gst_caps_from_string (audio_caps_text);
  134. g_object_set (data.app_source, "caps", audio_caps, NULL);
  135. g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
  136. g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);
  137. /* Configure appsink */
  138. g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
  139. g_signal_connect (data.app_sink, "new-buffer", G_CALLBACK (new_buffer), &data);
  140. gst_caps_unref (audio_caps);
  141. g_free (audio_caps_text);
  142. /* Link all elements that can be automatically linked because they have "Always" pads */
  143. gst_bin_add_many (GST_BIN (data.pipeline), data.app_source, data.tee, data.audio_queue, data.audio_convert1, data.audio_resample,
  144. data.audio_sink, data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, data.app_queue,
  145. data.app_sink, NULL);
  146. if (gst_element_link_many (data.app_source, data.tee, NULL) != TRUE ||
  147. gst_element_link_many (data.audio_queue, data.audio_convert1, data.audio_resample, data.audio_sink, NULL) != TRUE ||
  148. gst_element_link_many (data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, NULL) != TRUE ||
  149. gst_element_link_many (data.app_queue, data.app_sink, NULL) != TRUE) {
  150. g_printerr ("Elements could not be linked.\n");
  151. gst_object_unref (data.pipeline);
  152. ;
  153. }
  154. /* Manually link the Tee, which has "Request" pads */
  155. tee_src_pad_template = gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (data.tee), "src%d");
  156. tee_audio_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  157. g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
  158. queue_audio_pad = gst_element_get_static_pad (data.audio_queue, "sink");
  159. tee_video_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  160. g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
  161. queue_video_pad = gst_element_get_static_pad (data.video_queue, "sink");
  162. tee_app_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  163. g_print ("Obtained request pad %s for app branch.\n", gst_pad_get_name (tee_app_pad));
  164. queue_app_pad = gst_element_get_static_pad (data.app_queue, "sink");
  165. if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
  166. gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK ||
  167. gst_pad_link (tee_app_pad, queue_app_pad) != GST_PAD_LINK_OK) {
  168. g_printerr ("Tee could not be linked\n");
  169. gst_object_unref (data.pipeline);
  170. ;
  171. }
  172. gst_object_unref (queue_audio_pad);
  173. gst_object_unref (queue_video_pad);
  174. gst_object_unref (queue_app_pad);
  175. /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
  176. bus = gst_element_get_bus (data.pipeline);
  177. gst_bus_add_signal_watch (bus);
  178. g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
  179. gst_object_unref (bus);
  180. /* Start playing the pipeline */
  181. gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
  182. /* Create a GLib Main Loop and set it to run */
  183. data.main_loop = g_main_loop_new (NULL, FALSE);
  184. g_main_loop_run (data.main_loop);
  185. /* Release the request pads from the Tee, and unref them */
  186. gst_element_release_request_pad (data.tee, tee_audio_pad);
  187. gst_element_release_request_pad (data.tee, tee_video_pad);
  188. gst_element_release_request_pad (data.tee, tee_app_pad);
  189. gst_object_unref (tee_audio_pad);
  190. gst_object_unref (tee_video_pad);
  191. gst_object_unref (tee_app_pad);
  192. /* Free resources */
  193. gst_element_set_state (data.pipeline, GST_STATE_NULL);
  194. gst_object_unref (data.pipeline);
  195. ;
  196. }

工作流程

创建pipeline段的代码就是上一篇的教程中得例子的扩大版。包括初始或所有的element,连接有Always Pad的element然后手动连接tee element的Request Pad。

下面我们关注一下appsrc和appsink这两个element的配置:

[objc] view
plain
 copy

  1. /* Configure appsrc */
  2. audio_caps_text = g_strdup_printf (AUDIO_CAPS, SAMPLE_RATE);
  3. audio_caps = gst_caps_from_string (audio_caps_text);
  4. g_object_set (data.app_source, "caps", audio_caps, NULL);
  5. g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
  6. g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);

appsrc里面第一个需要关注的属性是caps。它说明了element准备生成的数据的类型,这样GStreamer就可以检查下游的element看看是否支持。这个属性必须是一个GstCaps对象,这个对象可以很方便的由gst_caps_from_string()来生成。

然后我们把need-data和enough-data信号和回调连接起来,这样在appsrc内部的队列里面数据不足或将要满地时候会发送信号,我们就用这些信号来启动/停止我们的信号发生过程。

[objc] view
plain
 copy

  1. /* Configure appsink */
  2. g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
  3. g_signal_connect (data.app_sink, "new-buffer", G_CALLBACK (new_buffer), &data);
  4. gst_caps_unref (audio_caps);
  5. g_free (audio_caps_text);

关于appsink的配置,我们连接了new-buffer的信号,这个信号在每次收到buffer的时候发出。当然,这个信号的发出需要emit-signals这个信号属性被开启(默认是关闭的)。

启动pipeline,等到消息和最后的清理资源都和以前的没什么区别。让我们关注我们刚刚注册的回调吧。

[objc] view
plain
 copy

  1. /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
  2. * to the mainloop to start pushing data into the appsrc */
  3. static void start_feed (GstElement *source, guint size, CustomData *data) {
  4. ) {
  5. g_print ("Start feeding\n");
  6. data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
  7. }
  8. }

这个函数在appsrc内部队列将要空的时候调用,在这里我们做的事情仅仅是用g_idle_add()方法注册一个GLib的idle函数,这个函数会给appsrc输入数据知道内部队列满为止。一个GLib的idle函数是一个GLib在主循环在“idle”时会调用的方法,也就是说,当时没有更高优先级的任务运行。

这只是appsrc多种发出数据方法中的一个。特别需要指出的是,buffer不是必须要在主线程中用GLib方法来传递给appsrc的,你也不是一定要用need-data和enough-data信号来同步appsrc的(据说这样最方便)。

我们记录下g_idle_add()的返回的sourceid,这样后面可以关掉它。

[objc] view
plain
 copy

  1. /* This callback triggers when appsrc has enough data and we can stop sending.
  2. * We remove the idle handler from the mainloop */
  3. static void stop_feed (GstElement *source, CustomData *data) {
  4. ) {
  5. g_print ("Stop feeding\n");
  6. g_source_remove (data->sourceid);
  7. ;
  8. }
  9. }

这个函数当appsrc内部的队列满的时候调用,所以我们需要停止发送数据。这里我们简单地用g_source_remove()来把idle函数移走。

[objc] view
plain
 copy

  1. /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
  2. * The idle handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
  3. * and is removed when appsrc has enough data (enough-data signal).
  4. */
  5. static gboolean push_data (CustomData *data) {
  6. GstBuffer *buffer;
  7. GstFlowReturn ret;
  8. int i;
  9. gint16 *raw;
  10. ; /* Because each sample is 16 bits */
  11. gfloat freq;
  12. /* Create a new empty buffer */
  13. buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
  14. /* Set its timestamp and duration */
  15. 4_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
  16. 4_scale (CHUNK_SIZE, GST_SECOND, SAMPLE_RATE);
  17. /* Generate some psychodelic waveforms */
  18. raw = (gint16 *)GST_BUFFER_DATA (buffer);

这个函数给appsrc发送数据。它被GLib调用的次数和频率我们不加以控制,但我们会在它任务完成时关闭它(appsrc内部队列满)。

这里第一步是用gst_buffer_new_and_alloc()方法和给定的大小创建一个新buffer(例子中是1024字节)。

我们计算我们生成的采样数据的数据量,把数据存在CustomData.num_samples里面,这样我们可以用GstBuffer提供的GST_BUFFER_TIMESTAMP宏来生成buffer的时间戳。

gst_util_uint64_scale是一个工具函数,用来缩放数据,确保不会溢出。

这些给buffer的数据可以用GstBuffer提供的GST_BUFFER_DATA宏来访问。

我们会跳过波形的生成部分,因为这不是本教程要讲述的内容。

[objc] view
plain
 copy

  1. /* Push the buffer into the appsrc */
  2. g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
  3. /* Free the buffer now that we are done with it */
  4. gst_buffer_unref (buffer);

一旦我们的buffer已经准备好,我们把带着这个buffer的push-buffer信号传给appsrc,然后就调用gst_buffer_unref()方法,因为我们不会再用到它了。

[objc] view
plain
 copy

  1. /* The appsink has received a buffer */
  2. static void new_buffer (GstElement *sink, CustomData *data) {
  3. GstBuffer *buffer;
  4. /* Retrieve the buffer */
  5. g_signal_emit_by_name (sink, "pull-buffer", &buffer);
  6. if (buffer) {
  7. /* The only thing we do in this example is print a * to indicate a received buffer */
  8. g_print ("*");
  9. gst_buffer_unref (buffer);
  10. }
  11. }

最后,这个函数在appsink收到buffer时被调用。我们使用了pull-buffer的信号来重新获得buffer,因为是例子,所以仅仅在屏幕上打印一些内容。我们可以用GstBuffer的GST_BUFFER_DATA宏来获得数据指针和用GST_BUFFER_SIZE宏来获得数据大小。请记住,这里的buffer不是一定要和我们在push_data函数里面创建的buffer完全一致的,在传输路径上得任何一个element都可能对buffer进行一些改变。(这个例子中仅仅是在appsrc和appsink中间通过一个tee
element,所以buffer没有变化)。

请不要忘记调用gst_buffer_unref()来释放buffer,就讲这么多吧。

【GStreamer开发】GStreamer基础教程08——pipeline的快捷访问的更多相关文章

  1. 【GStreamer开发】GStreamer播放教程03——pipeline的快捷访问

    目的 <GStreamer08--pipeline的快捷访问>展示了一个应用如何用appsrc和appsink这两个特殊的element在pipeline中手动输入/提取数据.playbi ...

  2. Android程序开发0基础教程(一)

    程序猿学英语就上视觉英语网 Android程序开发0基础教程(一)   平台简单介绍   令人激动的Google手机操作系统平台-Android在2007年11月13日正式公布了,这是一个开放源码的操 ...

  3. GStreamer基础教程08 - 多线程

    摘要 GStreamer框架会自动处理多线程的逻辑,但在某些情况下,我们仍然需要根据实际的情况自己将部分Pipeline在单独的线程中执行,本文将介绍如何处理这种情况. GStreamer多线程 GS ...

  4. iOS开发零基础教程之生成git所需的SSH keys

    在我们github看到了一个不错的第三方库时,可能我们想把他git clone到本地,我们需要复制他的SSH URL,如下图: 复制完地址之后,我们需要打开终端,然后输入命令: git clone + ...

  5. GStreamer基础教程09 - Appsrc及Appsink

    摘要 在我们前面的文章中,我们的Pipline都是使用GStreamer自带的插件去产生/消费数据.在实际的情况中,我们的数据源可能没有相应的gstreamer插件,但我们又需要将数据发送到GStre ...

  6. Chrome扩展开发基础教程(附HelloWorld)

    1 概述 Chrome扩展开发的基础教程,代码基于原生JS+H5,教程内容基于谷歌扩展开发官方文档. 2 环境 Chrome 88.0.4324.96 Chromium 87.0.4280.141 B ...

  7. 【GStreamer开发】GStreamer基础教程03——动态pipeline

    本教程介绍pipeline的一种新的创建方式--在运行中创建,而不是在运行前一次性的创建结束. 介绍 在这篇教程里的pipeline并非在运行前就全部创建结束的.放松一下,这样做没有任何问题.如果我们 ...

  8. 【GStreamer开发】GStreamer基础教程14——常用的element

    目标 本教程给出了一系列开发中常用的element.它们包括大杂烩般的eleemnt(比如playbin2)以及一些调试时很有用的element. 简单来说,下面用gst-launch这个工具给出一个 ...

  9. 【GStreamer开发】GStreamer基础教程13——播放速度

    目标 快进,倒放和慢放是trick模式的共同技巧,它们有一个共同点就是它们都修改了播放的速度.本教程会展示如何来获得这些效果和如何进行逐帧的跳跃.主要内容是: 如何来变换播放的速度,变快或者变慢,前进 ...

随机推荐

  1. Bzoj 4147: [AMPPZ2014]Euclidean Nim(博弈)

    4147: [AMPPZ2014]Euclidean Nim Time Limit: 1 Sec Memory Limit: 256 MB Description Euclid和Pythagoras在 ...

  2. 汇编语言DOSBox软件使用方法

    https://wenku.baidu.com/view/e63b8b46ccbff121dc368305.html

  3. linux 最大TCP连接数限制

    ----------------------------------------------问题--------------------------------------------- 前几日碰到问 ...

  4. 金蝶kis 16.0专业版-破解01

    Kingdee.KIS.MobAppSer>MainViewModel 经过反混淆后,找到导入LIcense文件后的验证函数. 下面仅需进行逆向生成即可,为什么一定要进行生成lic文件方式进行破 ...

  5. Win7如何设置怎样在局域网内共享打印机

    首先进入桌面,点击开始按钮,然后打开控制面板 2 在控制面板设置界面,找到“管理工具”选项 3 接着打开“计算机管理”   选择“本地用户和组”的Guest账户   确保Guest账户被禁用   下面 ...

  6. 14.LAMP服务 Linux Apache Mysql Php和防护机制 xinetd、tcp wapper

    一.安装LAMP服务 Linux Apache Mysql Php       要求操作系统支持 php解析 apache调用php插件解析 phpmyadmin       yum install ...

  7. 响应面分析 | response surface analysis | R代码

    先开题,慢慢补充. 参考: 什么是响应面(RSM)分析 Response-Surface Methods in R, Using rsm In-class Examples with R Code R ...

  8. Qt 中的对象模型(Object Model)

    原标题:Qt 中的对象模型(Object Model)90不太后,余生皆折腾 本节内容主要讲了 Qt 对象模型比标准 C++ 对象模型多了什么内容,并介绍了组成 Qt 对象模型基础的相关的类.最后说明 ...

  9. Faster, more memory efficient and more ordered dictionaries on PyPy

    Faster, more memory efficient and more ordered dictionaries on PyPy https://morepypy.blogspot.com/20 ...

  10. springMVC Controller 参数映射

    springMVC 对参数为null或参数不为null的处理 - 小浩子的博客 - CSDN博客https://blog.csdn.net/change_on/article/details/7664 ...