• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Audio latency measurement plugin
2  * Copyright (C) 2018 Centricular Ltd.
3  *   Author: Nirbheek Chauhan <nirbheek@centricular.com>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this library; if not, write to the
17  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20 
21 /**
22  * SECTION:element-audiolatency
23  * @title: audiolatency
24  *
25  * Measures the audio latency between the source pad and the sink pad by
26  * outputting period ticks on the source pad and measuring how long they take to
27  * arrive on the sink pad.
28  *
29  * The ticks have a period of 1 second, so this element can only measure
30  * latencies smaller than that.
31  *
32  * ## Example pipeline
33  * |[
34  * gst-launch-1.0 -v autoaudiosrc ! audiolatency print-latency=true ! autoaudiosink
35  * ]| Continuously print the latency of the audio output and the audio capture
36  *
37  * In this case, you must ensure that the audio output is captured by the audio
38  * source. The simplest way is to use a standard 3.5mm male-to-male audio cable
39  * to connect line-out to line-in, or speaker-out to mic-in, etc.
40  *
41  * Capturing speaker output with a microphone should also work, as long as the
42  * ambient noise level is low enough. You may have to adjust the microphone gain
43  * to ensure that the volume is loud enough to be detected by the element, and
44  * at the same time that it's not so loud that it picks up ambient noise.
45  *
46  * For programmatic use, instead of using 'print-stats', you should read the
47  * 'last-latency' and 'average-latency' properties at most once a second, or
48  * parse the "latency" element message, which contains the "last-latency" and
49  * "average-latency" fields in the GstStructure.
50  *
51  * The average latency is a running average of the last 5 measurements.
52  */
53 
54 #ifdef HAVE_CONFIG_H
55 #include "config.h"
56 #endif
57 
58 #include "gstaudiolatency.h"
59 
60 #define AUDIOLATENCY_CAPS "audio/x-raw, " \
61     "format = (string) F32LE, " \
62     "layout = (string) interleaved, " \
63     "rate = (int) [ 1, MAX ], " \
64     "channels = (int) [ 1, MAX ]"
65 
66 GST_DEBUG_CATEGORY_STATIC (gst_audiolatency_debug);
67 #define GST_CAT_DEFAULT gst_audiolatency_debug
68 
69 static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
70     GST_PAD_SRC,
71     GST_PAD_ALWAYS,
72     GST_STATIC_CAPS (AUDIOLATENCY_CAPS)
73     );
74 
75 static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
76     GST_PAD_SINK,
77     GST_PAD_ALWAYS,
78     GST_STATIC_CAPS (AUDIOLATENCY_CAPS)
79     );
80 
81 #define gst_audiolatency_parent_class parent_class
82 G_DEFINE_TYPE_WITH_CODE (GstAudioLatency, gst_audiolatency, GST_TYPE_BIN,
83     GST_DEBUG_CATEGORY_INIT (gst_audiolatency_debug, "audiolatency", 0,
84         "audiolatency"););
85 GST_ELEMENT_REGISTER_DEFINE (audiolatency, "audiolatency", GST_RANK_PRIMARY,
86     GST_TYPE_AUDIOLATENCY);
87 
88 #define DEFAULT_PRINT_LATENCY   FALSE
89 #define DEFAULT_SAMPLES_PER_BUFFER 240
90 
91 enum
92 {
93   PROP_0,
94   PROP_PRINT_LATENCY,
95   PROP_LAST_LATENCY,
96   PROP_AVERAGE_LATENCY,
97   PROP_SAMPLES_PER_BUFFER,
98 };
99 
100 static gint64 gst_audiolatency_get_latency (GstAudioLatency * self);
101 static gint64 gst_audiolatency_get_average_latency (GstAudioLatency * self);
102 static GstFlowReturn gst_audiolatency_sink_chain (GstPad * pad,
103     GstObject * parent, GstBuffer * buffer);
104 static gboolean gst_audiolatency_sink_event (GstPad * pad,
105     GstObject * parent, GstEvent * event);
106 static GstPadProbeReturn gst_audiolatency_src_probe (GstPad * pad,
107     GstPadProbeInfo * info, gpointer user_data);
108 
109 static void
gst_audiolatency_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)110 gst_audiolatency_get_property (GObject * object,
111     guint prop_id, GValue * value, GParamSpec * pspec)
112 {
113   GstAudioLatency *self = GST_AUDIOLATENCY (object);
114 
115   switch (prop_id) {
116     case PROP_PRINT_LATENCY:
117       g_value_set_boolean (value, self->print_latency);
118       break;
119     case PROP_LAST_LATENCY:
120       g_value_set_int64 (value, gst_audiolatency_get_latency (self));
121       break;
122     case PROP_AVERAGE_LATENCY:
123       g_value_set_int64 (value, gst_audiolatency_get_average_latency (self));
124       break;
125     case PROP_SAMPLES_PER_BUFFER:
126       g_value_set_int (value, self->samples_per_buffer);
127       break;
128     default:
129       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
130       break;
131   }
132 }
133 
134 static void
gst_audiolatency_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)135 gst_audiolatency_set_property (GObject * object,
136     guint prop_id, const GValue * value, GParamSpec * pspec)
137 {
138   GstAudioLatency *self = GST_AUDIOLATENCY (object);
139 
140   switch (prop_id) {
141     case PROP_PRINT_LATENCY:
142       self->print_latency = g_value_get_boolean (value);
143       break;
144     case PROP_SAMPLES_PER_BUFFER:
145       self->samples_per_buffer = g_value_get_int (value);
146       g_object_set (self->audiosrc,
147           "samplesperbuffer", self->samples_per_buffer, NULL);
148       break;
149     default:
150       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
151       break;
152   }
153 }
154 
155 static void
gst_audiolatency_class_init(GstAudioLatencyClass * klass)156 gst_audiolatency_class_init (GstAudioLatencyClass * klass)
157 {
158   GObjectClass *gobject_class = (GObjectClass *) klass;
159   GstElementClass *gstelement_class = (GstElementClass *) klass;
160 
161   gobject_class->get_property = gst_audiolatency_get_property;
162   gobject_class->set_property = gst_audiolatency_set_property;
163 
164   g_object_class_install_property (gobject_class, PROP_PRINT_LATENCY,
165       g_param_spec_boolean ("print-latency", "Print latencies",
166           "Print the measured latencies on stdout",
167           DEFAULT_PRINT_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
168 
169   g_object_class_install_property (gobject_class, PROP_LAST_LATENCY,
170       g_param_spec_int64 ("last-latency", "Last measured latency",
171           "The last latency that was measured, in microseconds", 0,
172           G_USEC_PER_SEC, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
173 
174   g_object_class_install_property (gobject_class, PROP_AVERAGE_LATENCY,
175       g_param_spec_int64 ("average-latency", "Running average latency",
176           "The running average latency, in microseconds", 0,
177           G_USEC_PER_SEC, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
178 
179   /**
180    * GstAudioLatency:samplesperbuffer:
181    *
182    * The number of audio samples in each outgoing buffer.
183    * See also #GstAudioTestSrc:samplesperbuffer
184    *
185    * Since: 1.20
186    */
187   g_object_class_install_property (gobject_class, PROP_SAMPLES_PER_BUFFER,
188       g_param_spec_int ("samplesperbuffer", "Samples per buffer",
189           "Number of samples in each outgoing buffer",
190           1, G_MAXINT, DEFAULT_SAMPLES_PER_BUFFER,
191           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
192 
193   gst_element_class_add_static_pad_template (gstelement_class, &src_template);
194   gst_element_class_add_static_pad_template (gstelement_class, &sink_template);
195 
196   gst_element_class_set_static_metadata (gstelement_class, "AudioLatency",
197       "Audio/Util",
198       "Measures the audio latency between the source and the sink",
199       "Nirbheek Chauhan <nirbheek@centricular.com>");
200 }
201 
202 static void
gst_audiolatency_init(GstAudioLatency * self)203 gst_audiolatency_init (GstAudioLatency * self)
204 {
205   GstPad *srcpad;
206   GstPadTemplate *templ;
207 
208   self->send_pts = 0;
209   self->recv_pts = 0;
210   self->print_latency = DEFAULT_PRINT_LATENCY;
211   self->samples_per_buffer = DEFAULT_SAMPLES_PER_BUFFER;
212 
213   /* Setup sinkpad */
214   self->sinkpad = gst_pad_new_from_static_template (&sink_template, "sink");
215   gst_pad_set_chain_function (self->sinkpad,
216       GST_DEBUG_FUNCPTR (gst_audiolatency_sink_chain));
217   gst_pad_set_event_function (self->sinkpad,
218       GST_DEBUG_FUNCPTR (gst_audiolatency_sink_event));
219 
220   gst_element_add_pad (GST_ELEMENT (self), self->sinkpad);
221 
222   /* Setup srcpad */
223   self->audiosrc = gst_element_factory_make ("audiotestsrc", NULL);
224   g_object_set (self->audiosrc, "wave", 8, "samplesperbuffer",
225       DEFAULT_SAMPLES_PER_BUFFER, "is-live", TRUE, NULL);
226   gst_bin_add (GST_BIN (self), self->audiosrc);
227 
228   templ = gst_static_pad_template_get (&src_template);
229   srcpad = gst_element_get_static_pad (self->audiosrc, "src");
230   gst_pad_add_probe (srcpad, GST_PAD_PROBE_TYPE_BUFFER,
231       (GstPadProbeCallback) gst_audiolatency_src_probe, self, NULL);
232 
233   self->srcpad = gst_ghost_pad_new_from_template ("src", srcpad, templ);
234   gst_element_add_pad (GST_ELEMENT (self), self->srcpad);
235   gst_object_unref (srcpad);
236   gst_object_unref (templ);
237 
238   GST_LOG_OBJECT (self, "Initialized audiolatency");
239 }
240 
241 static gint64
gst_audiolatency_get_latency(GstAudioLatency * self)242 gst_audiolatency_get_latency (GstAudioLatency * self)
243 {
244   gint64 last_latency;
245   gint last_latency_idx;
246 
247   GST_OBJECT_LOCK (self);
248   /* Decrement index, with wrap-around */
249   last_latency_idx = self->next_latency_idx - 1;
250   if (last_latency_idx < 0)
251     last_latency_idx = GST_AUDIOLATENCY_NUM_LATENCIES - 1;
252 
253   last_latency = self->latencies[last_latency_idx];
254   GST_OBJECT_UNLOCK (self);
255 
256   return last_latency;
257 }
258 
259 static gint64
gst_audiolatency_get_average_latency_unlocked(GstAudioLatency * self)260 gst_audiolatency_get_average_latency_unlocked (GstAudioLatency * self)
261 {
262   int ii, n = 0;
263   gint64 average = 0;
264 
265   for (ii = 0; ii < GST_AUDIOLATENCY_NUM_LATENCIES; ii++) {
266     if (G_LIKELY (self->latencies[ii] > 0))
267       n += 1;
268     average += self->latencies[ii];
269   }
270 
271   return average / MAX (n, 1);
272 }
273 
274 static gint64
gst_audiolatency_get_average_latency(GstAudioLatency * self)275 gst_audiolatency_get_average_latency (GstAudioLatency * self)
276 {
277   gint64 average;
278 
279   GST_OBJECT_LOCK (self);
280   average = gst_audiolatency_get_average_latency_unlocked (self);
281   GST_OBJECT_UNLOCK (self);
282 
283   return average;
284 }
285 
286 static void
gst_audiolatency_set_latency(GstAudioLatency * self,gint64 latency)287 gst_audiolatency_set_latency (GstAudioLatency * self, gint64 latency)
288 {
289   gint64 avg_latency;
290 
291   GST_OBJECT_LOCK (self);
292   self->latencies[self->next_latency_idx] = latency;
293 
294   /* Increment index, with wrap-around */
295   self->next_latency_idx += 1;
296   if (self->next_latency_idx > GST_AUDIOLATENCY_NUM_LATENCIES - 1)
297     self->next_latency_idx = 0;
298 
299   avg_latency = gst_audiolatency_get_average_latency_unlocked (self);
300 
301   if (self->print_latency)
302     g_print ("last latency: %" G_GINT64_FORMAT "ms, running average: %"
303         G_GINT64_FORMAT "ms\n", latency / 1000, avg_latency / 1000);
304   GST_OBJECT_UNLOCK (self);
305 
306   /* Post an element message about it */
307   gst_element_post_message (GST_ELEMENT (self),
308       gst_message_new_element (GST_OBJECT (self),
309           gst_structure_new ("latency", "last-latency", G_TYPE_INT64, latency,
310               "average-latency", G_TYPE_INT64, avg_latency, NULL)));
311 }
312 
313 static gint64
buffer_has_wave(GstBuffer * buffer,GstPad * pad)314 buffer_has_wave (GstBuffer * buffer, GstPad * pad)
315 {
316   const GstStructure *s;
317   GstCaps *caps;
318   GstMapInfo minfo;
319   guint64 duration;
320   gint64 offset;
321   gint ii, channels, fsize, rate;
322   gfloat *fdata;
323   gboolean ret;
324   GstMemory *memory = NULL;
325 
326   switch (gst_buffer_n_memory (buffer)) {
327     case 0:
328       GST_WARNING_OBJECT (pad, "buffer %" GST_PTR_FORMAT "has no memory?",
329           buffer);
330       return -1;
331     case 1:
332       memory = gst_buffer_peek_memory (buffer, 0);
333       ret = gst_memory_map (memory, &minfo, GST_MAP_READ);
334       break;
335     default:
336       ret = gst_buffer_map (buffer, &minfo, GST_MAP_READ);
337   }
338 
339   if (!ret) {
340     GST_WARNING_OBJECT (pad, "failed to map buffer %" GST_PTR_FORMAT, buffer);
341     return -1;
342   }
343 
344   caps = gst_pad_get_current_caps (pad);
345   s = gst_caps_get_structure (caps, 0);
346   /* channels and rate are required in caps, so will always be present */
347   gst_structure_get_int (s, "channels", &channels);
348   gst_structure_get_int (s, "rate", &rate);
349   gst_caps_unref (caps);
350 
351   fdata = (gfloat *) minfo.data;
352   fsize = minfo.size / sizeof (gfloat);
353 
354   offset = -1;
355   if (GST_BUFFER_DURATION_IS_VALID (buffer)) {
356     duration = GST_BUFFER_DURATION (buffer);
357   } else {
358     /* Cannot do a rounding-accurate duration calculation here because in the
359      * case when the duration is invalid, the pts might also be invalid */
360     duration = gst_util_uint64_scale_int_round (GST_SECOND, fsize / channels,
361         rate);
362     GST_LOG_OBJECT (pad, "buffer duration is invalid, calculated likely "
363         "duration as %" G_GINT64_FORMAT "us", duration / GST_USECOND);
364   }
365 
366   /* Read only one channel */
367   for (ii = 1; ii < fsize; ii += channels) {
368     if (ABS (fdata[ii]) > 0.7) {
369       /* The waveform probably starts somewhere inside the buffer,
370        * so get the offset in nanoseconds from the buffer pts */
371       offset = gst_util_uint64_scale_int_round (duration, ii, fsize);
372       break;
373     }
374   }
375 
376   if (memory)
377     gst_memory_unmap (memory, &minfo);
378   else
379     gst_buffer_unmap (buffer, &minfo);
380 
381   /* Return offset in microseconds */
382   return (offset > 0) ? offset / 1000 : -1;
383 }
384 
385 static GstPadProbeReturn
gst_audiolatency_src_probe(GstPad * pad,GstPadProbeInfo * info,gpointer user_data)386 gst_audiolatency_src_probe (GstPad * pad, GstPadProbeInfo * info,
387     gpointer user_data)
388 {
389   GstAudioLatency *self = user_data;
390   GstBuffer *buffer;
391   gint64 pts, offset;
392 
393   if (!(info->type & GST_PAD_PROBE_TYPE_BUFFER))
394     goto out;
395 
396   if (GST_STATE (self) != GST_STATE_PLAYING)
397     goto out;
398 
399   GST_TRACE ("audiotestsrc pushed out a buffer");
400 
401   pts = g_get_monotonic_time ();
402   /* Ticks are once a second, so once we send something, we can skip
403    * checking ~1sec of buffers till the next one. */
404   if (self->send_pts > 0 && pts - self->send_pts <= 950 * 1000)
405     goto out;
406 
407   /* Check if buffer contains a waveform */
408   buffer = gst_pad_probe_info_get_buffer (info);
409   offset = buffer_has_wave (buffer, pad);
410   if (offset < 0)
411     goto out;
412 
413   pts -= offset;
414   {
415     gint64 after = 0;
416     if (self->send_pts > 0)
417       after = (pts - self->send_pts) / 1000;
418     GST_INFO ("send pts: %" G_GINT64_FORMAT "us (after %" G_GINT64_FORMAT
419         "ms, offset %" G_GINT64_FORMAT "ms)", pts, after, offset / 1000);
420   }
421 
422   self->send_pts = pts + offset;
423 
424 out:
425   return GST_PAD_PROBE_OK;
426 }
427 
428 static GstFlowReturn
gst_audiolatency_sink_chain(GstPad * pad,GstObject * parent,GstBuffer * buffer)429 gst_audiolatency_sink_chain (GstPad * pad, GstObject * parent,
430     GstBuffer * buffer)
431 {
432   GstAudioLatency *self = GST_AUDIOLATENCY (parent);
433   gint64 latency, offset, pts;
434 
435   /* Ignore buffers till something gets sent out by us. Fixes a bug where we'd
436    * start out by printing one garbage latency value on Windows. */
437   if (self->send_pts == 0)
438     goto out;
439 
440   GST_TRACE_OBJECT (pad, "Got buffer %p", buffer);
441 
442   pts = g_get_monotonic_time ();
443   /* Ticks are once a second, so once we receive something, we can skip
444    * checking ~1sec of buffers till the next one. This way we also don't count
445    * the same tick twice for latency measurement. */
446   if (self->recv_pts > 0 && pts - self->recv_pts <= 950 * 1000)
447     goto out;
448 
449   offset = buffer_has_wave (buffer, pad);
450   if (offset < 0)
451     goto out;
452 
453   self->recv_pts = pts + offset;
454   latency = (self->recv_pts - self->send_pts);
455   gst_audiolatency_set_latency (self, latency);
456 
457   GST_INFO ("recv pts: %" G_GINT64_FORMAT "us, latency: %" G_GINT64_FORMAT
458       "ms, offset: %" G_GINT64_FORMAT "ms", self->recv_pts, latency / 1000,
459       offset / 1000);
460 
461 out:
462   gst_buffer_unref (buffer);
463   return GST_FLOW_OK;
464 }
465 
466 static gboolean
gst_audiolatency_sink_event(GstPad * pad,GstObject * parent,GstEvent * event)467 gst_audiolatency_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
468 {
469   switch (GST_EVENT_TYPE (event)) {
470       /* Drop below events. audiotestsrc will push its own event */
471     case GST_EVENT_STREAM_START:
472     case GST_EVENT_CAPS:
473     case GST_EVENT_SEGMENT:
474       gst_event_unref (event);
475       return TRUE;
476     default:
477       break;
478   }
479 
480   return gst_pad_event_default (pad, parent, event);
481 }
482 
483 /* Element registration */
484 static gboolean
plugin_init(GstPlugin * plugin)485 plugin_init (GstPlugin * plugin)
486 {
487   return GST_ELEMENT_REGISTER (audiolatency, plugin);
488 }
489 
490 GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
491     GST_VERSION_MINOR,
492     audiolatency,
493     "A plugin to measure audio latency",
494     plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
495