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