Help please on making my audio source plugin play?

Hello All Gstreamer gurus. I’m building a gstreamer plugin which converts TEXT to Morse code. dah’s dit’s and spaces. I spent a far bit of time on trying to develop the plugin… daya on days. Is there anyone able to shed some light on why I’m not able to play.

Here’s the launch command (linux)
gst-launch-1.0 morsesrc text=“CQ CQ DE VK3DG” ! audioconvert ! autoaudiosink

This is all it does.
Setting pipeline to PAUSED …
Pipeline is PREROLLING …
Redistribute latency…
Pipeline is PREROLLED …
Setting pipeline to PLAYING …
Redistribute latency…
New clock: GstPulseSinkClock
0:00:00.0 / 99:99:99.

Here my code:- gst-morsesrc.c

#include <gst/gst.h>
#include <gst/base/gstpushsrc.h>
#include <math.h>
#include <ctype.h>

#define PACKAGE "morsesrc"

#define DEFAULT_RATE 44100
#define DEFAULT_FREQUENCY 880.0
#define DEFAULT_VOLUME 0.5
#define DEFAULT_WPM 20

static unsigned short morse_table[128] = {
    /*00*/ 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
    /*08*/ 0000, 0000, 0412, 0000, 0000, 0412, 0000, 0000,
    /*10*/ 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
    /*18*/ 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
    /*20*/ 0000, 0665, 0622, 0000, 0000, 0000, 0502, 0636,
    /*28*/ 0515, 0000, 0000, 0512, 0663, 0000, 0652, 0511,
    /*30*/ 0537, 0536, 0534, 0530, 0520, 0500, 0501, 0503,
    /*38*/ 0507, 0517, 0607, 0625, 0000, 0521, 0000, 0614,
    /*40*/ 0000, 0202, 0401, 0405, 0301, 0100, 0404, 0303,
    /*48*/ 0400, 0200, 0416, 0305, 0402, 0203, 0201, 0307,
    /*50*/ 0406, 0413, 0302, 0300, 0101, 0304, 0410, 0306,
    /*58*/ 0411, 0415, 0403, 0000, 0000, 0000, 0000, 0000,
    /*60*/ 0000, 0202, 0401, 0405, 0301, 0100, 0404, 0303,
    /*68*/ 0400, 0200, 0416, 0305, 0402, 0203, 0201, 0307,
    /*70*/ 0406, 0413, 0302, 0300, 0101, 0304, 0410, 0306,
    /*78*/ 0411, 0415, 0403, 0000, 0000, 0000, 0000, 0000
};

typedef struct _GstMorseSrc {
    GstPushSrc parent;
    gint rate;
    gdouble frequency;
    gdouble volume;
    gint wpm;
    gchar *text;
    GString *generated_morse;
    guint position;
    guint samples_per_dot;
    guint samples_per_dash;
    guint samples_per_space;
} GstMorseSrc;

typedef struct _GstMorseSrcClass {
    GstPushSrcClass parent_class;
} GstMorseSrcClass;

#define GST_TYPE_MORSE_SRC (gst_morse_src_get_type())
#define GST_MORSE_SRC(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_MORSE_SRC, GstMorseSrc))

G_DEFINE_TYPE(GstMorseSrc, gst_morse_src, GST_TYPE_PUSH_SRC)

enum {
    PROP_0,
    PROP_RATE,
    PROP_FREQUENCY,
    PROP_VOLUME,
    PROP_WPM,
    PROP_TEXT,
    LAST_PROP
};

static GParamSpec *properties[LAST_PROP];

static void gst_morse_src_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) {
    GstMorseSrc *src = GST_MORSE_SRC(object);

    switch (prop_id) {
        case PROP_RATE:
            src->rate = g_value_get_int(value);
            break;
        case PROP_FREQUENCY:
            src->frequency = g_value_get_double(value);
            break;
        case PROP_VOLUME:
            src->volume = g_value_get_double(value);
            break;
        case PROP_WPM:
            src->wpm = g_value_get_int(value);
            break;
        case PROP_TEXT:
            if (src->text) {
                g_free(src->text);
            }
            src->text = g_value_dup_string(value);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
            break;
    }
}

static void gst_morse_src_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) {
    GstMorseSrc *src = GST_MORSE_SRC(object);

    switch (prop_id) {
        case PROP_RATE:
            g_value_set_int(value, src->rate);
            break;
        case PROP_FREQUENCY:
            g_value_set_double(value, src->frequency);
            break;
        case PROP_VOLUME:
            g_value_set_double(value, src->volume);
            break;
        case PROP_WPM:
            g_value_set_int(value, src->wpm);
            break;
        case PROP_TEXT:
            g_value_set_string(value, src->text);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
            break;
    }
}

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS("audio/x-raw, format=(string)S16LE, channels=(int)1, rate=(int)[ 1, MAX ]")
);

static void morse_send_char(GString *text, int ch) {
    int nsyms, bitreg;

    bitreg = morse_table[ch & 0x7f];
    if ((nsyms = (bitreg >> 6) & 07) == 0)
        nsyms = 8;
    bitreg &= 077;
    while (nsyms-- > 0) {
        g_string_append_c(text, ' ');
        if (bitreg & 01) {
            g_string_append_c(text, '-');
        } else {
            g_string_append_c(text, '.');
        }
        bitreg >>= 1;
    }
    g_string_append_c(text, ' '); // space between characters
}

static void morse_send_string(GString *text, const char *str) {
    while (*str) {
        morse_send_char(text, toupper(*str));
        str++;
    }
    g_string_append(text, "   "); // space between words
}

static GstFlowReturn gst_morse_src_fill(GstPushSrc *pushsrc, GstBuffer *buffer) {
    GstMorseSrc *src = GST_MORSE_SRC(pushsrc);

    if (!src->generated_morse || src->position >= src->generated_morse->len) {
        return GST_FLOW_EOS;
    }

    GstMapInfo map;
    gst_buffer_map(buffer, &map, GST_MAP_WRITE);

    gint16 *data = (gint16 *)map.data;
    gsize length = map.size / sizeof(gint16);

    guint samples_per_dot = src->samples_per_dot;
    guint samples_per_dash = src->samples_per_dash;
    guint samples_per_space = src->samples_per_space;

    guint i = 0;
    while (i < length && src->position < src->generated_morse->len) {
        char symbol = src->generated_morse->str[src->position];
        guint num_samples = 0;

        switch (symbol) {
            case '.':
                num_samples = samples_per_dot;
                break;
            case '-':
                num_samples = samples_per_dash;
                break;
            case ' ':
                num_samples = samples_per_space;
                break;
        }

        for (guint j = 0; j < num_samples && i < length; j++) {
            if (symbol == ' ') {
                data[i++] = 0;
            } else {
                data[i++] = (gint16)(src->volume * 32767.0 * sin(2.0 * G_PI * src->frequency * j / src->rate));
            }
        }

        if (num_samples < samples_per_space) {
            for (guint j = num_samples; j < samples_per_space && i < length; j++) {
                data[i++] = 0;
            }
        }

        src->position++;
    }

    gst_buffer_unmap(buffer, &map);

    return GST_FLOW_OK;
}

static gboolean gst_morse_src_start(GstBaseSrc *basesrc) {
    GstMorseSrc *src = GST_MORSE_SRC(basesrc);

    if (src->generated_morse) {
        g_string_free(src->generated_morse, TRUE);
    }

    src->generated_morse = g_string_new(NULL);
    morse_send_string(src->generated_morse, src->text);
    src->position = 0;

    src->samples_per_dot = src->rate * 60 / (src->wpm * 50);
    src->samples_per_dash = src->samples_per_dot * 3;
    src->samples_per_space = src->samples_per_dot;

    return TRUE;
}

static gboolean gst_morse_src_stop(GstBaseSrc *basesrc) {
    GstMorseSrc *src = GST_MORSE_SRC(basesrc);

    if (src->generated_morse) {
        g_string_free(src->generated_morse, TRUE);
        src->generated_morse = NULL;
    }

    return TRUE;
}

static void gst_morse_src_class_init(GstMorseSrcClass *klass) {
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GstBaseSrcClass *basesrc_class = GST_BASE_SRC_CLASS(klass);
    GstPushSrcClass *pushsrc_class = GST_PUSH_SRC_CLASS(klass);

    gobject_class->set_property = gst_morse_src_set_property;
    gobject_class->get_property = gst_morse_src_get_property;

    properties[PROP_RATE] = g_param_spec_int("rate", "Rate", "Sample rate in Hz", 1, G_MAXINT, DEFAULT_RATE, G_PARAM_READWRITE);
    properties[PROP_FREQUENCY] = g_param_spec_double("frequency", "Frequency", "Frequency in Hz", 0.0, G_MAXDOUBLE, DEFAULT_FREQUENCY, G_PARAM_READWRITE);
    properties[PROP_VOLUME] = g_param_spec_double("volume", "Volume", "Volume", 0.0, 1.0, DEFAULT_VOLUME, G_PARAM_READWRITE);
    properties[PROP_WPM] = g_param_spec_int("wpm", "Words per minute", "Words per minute", 1, G_MAXINT, DEFAULT_WPM, G_PARAM_READWRITE);
    properties[PROP_TEXT] = g_param_spec_string("text", "Morse text", "String to convert to Morse code", NULL, G_PARAM_READWRITE);

    g_object_class_install_properties(gobject_class, LAST_PROP, properties);

    gst_element_class_set_static_metadata(GST_ELEMENT_CLASS(klass),
        "Morse Source", "Source/Audio",
        "Generates Morse code audio",
        "Robert Hensel vk3dgtv@gmail.com");

    gst_element_class_add_pad_template(GST_ELEMENT_CLASS(klass),
        gst_static_pad_template_get(&src_template));

    basesrc_class->start = GST_DEBUG_FUNCPTR(gst_morse_src_start);
    basesrc_class->stop = GST_DEBUG_FUNCPTR(gst_morse_src_stop);
    pushsrc_class->fill = GST_DEBUG_FUNCPTR(gst_morse_src_fill);
}

static void gst_morse_src_init(GstMorseSrc *src) {
    src->rate = DEFAULT_RATE;
    src->frequency = DEFAULT_FREQUENCY;
    src->volume = DEFAULT_VOLUME;
    src->wpm = DEFAULT_WPM;
    src->text = NULL;
    src->generated_morse = NULL;
    src->position = 0;
}

static gboolean plugin_init(GstPlugin *plugin) {
    return gst_element_register(plugin, "morsesrc", GST_RANK_NONE, GST_TYPE_MORSE_SRC);
}

GST_PLUGIN_DEFINE(
    GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    morsesrc,
    "Generates Morse code audio",
    plugin_init,
    "1.0",
    "LGPL",
    "GStreamer",
    "http://gstreamer.net/"
)

I haven’t tried to build it or run it, but at first glance there are a couple of things you might want to double-check or try:

  • should make sure that the source base class sends out a GstSegment event in TIME format (not BYTES)
  • The audio buffers your source element produces should probably have timestamps set on them with GST_BUFFER_PTS(buf) = .... You can calculate those based on the number of samples and sample rate of course.
  • I would override the GstPushSrc::create() vfunc instead of the fill function, that way you can allocate a buffer of the right size (number of samples * sample size) and fill it and return it in one go.
  • Are you outputting big endian samples here on purpose? (not that it matters if you have the audioconvert in there, just seems more natural to output native endianness)
  • I’m not seeing where you set/send the output caps, you may need to do that before you output the first buffer
  • you will also want to add a layout field when you do create the output caps (interleaved probably, you may have to specify the field even if you’re sending mono audio)
  • not directly related to your issue at hand, but it’s more idiomatic to have things like output sample rate negotiated via a capsfilter after the source element than via a property (that way the source can automatically negotiate the rate according to downstream requirements)

Thanks for the quick reply tpm… I’ll have to digest your reply and one by one and implement each… I’m created a meson.build script for my plugin which works. Do you offer a paid service to get my code working?

Here’s my script…

project('morsesrc', 'c',
  default_options: ['warning_level=3', 'optimization=0', 'debug=true'])

gst_dep = dependency('gstreamer-1.0')
gstbase_dep = dependency('gstreamer-base-1.0')
glib_dep = dependency('glib-2.0')
gobject_dep = dependency('gobject-2.0')

cc = meson.get_compiler('c')
math_lib = cc.find_library('m', required: true)

src = [
  'src/gstmorsesrc.c',
]

libgstmorsesrc = shared_library('gstmorsesrc', src,
  dependencies: [gst_dep, gstbase_dep, glib_dep, gobject_dep, math_lib],
  install: true
)

install_dir = join_paths(get_option('prefix'), 'lib', 'gstreamer-1.0')

If you could put it into a git repo or so somewhere where people can just clone it and run meson to build it that would probably be the easiest.

There are various consultancies in the GStreamer space that offer support on a commercial basis (I happen to work for one of them), but if you know how to generate the audio samples there shouldn’t really be much missing here and it shouldn’t be too difficult to figure out the missing bits :slightly_smiling_face:

Hi tpm,

I added the code to my github repo https://github.com/TVforME/morsesrc I was hoping to get it working before releasing however, the GStreamer fairy maybe come to visit?

Thanks for you comments… I’ve added the time stamp code and it seems to play however fails with below :-

gst-launch-1.0 morsesrc text=“CQ CQ DE VK3DG” ! audioconvert ! autoaudiosink
Setting pipeline to PAUSED …
Pipeline is PREROLLING …
Redistribute latency…
Pipeline is PREROLLED …
Setting pipeline to PLAYING …
Redistribute latency…
New clock: GstPulseSinkClock

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

(gst-launch-1.0:345224): GStreamer-CRITICAL **: 22:12:54.784: gst_segment_to_running_time: assertion ‘segment->format == format’ failed

[LOTS MORE OF THESE LIKE ABOVE]

Got EOS from element “pipeline0”.
Execution ended after 0:00:00.013413472
Setting pipeline to NULL …
Freeing pipeline …

Adding this in gst_morse_src_init will get rid of those criticals:

    gst_base_src_set_format(GST_BASE_SRC(src), GST_FORMAT_TIME);

Next, you can inspect what your source outputs in terms of events/buffers/timestamps like this:

$ gst-launch-1.0 morsesrc text="CQ CQ DE VK3DG" ! fakesink silent=false -v

which looks mostly fine, only problem is this:

fakesink0.sink: caps = audio/x-raw, format=(string)S16LE, layout=(string)interleaved, channels=(int)1, rate=(int)1

Since you don’t send caps the base class just fixates the existing caps and fixates to sample rate of 1Hz.

Your timestamp calculations are based on 44100 Hz though.

You can work around it for starters with a caps filter, e.g. let’s try writing it into a file first:

$ gst-launch-1.0 morsesrc text="CQ CQ DE VK3DG" ! audio/x-raw,rate=44100 ! wavenc ! filesink location=morse.wav

$ gst-play-1.0 morse.wav

which outputs something, same for:

$ gst-launch-1.0 morsesrc text="CQ CQ DE VK3DG" ! audio/x-raw,rate=44100 ! audioconvert ! audioresample ! autoaudiosink

As for setting the output caps, you can use this for now (but it would be better if the rate was negotiated instead of set via a property, but let’s make it work first):

diff --git a/meson.build b/meson.build
index cf34ba6..8f3d10a 100644
--- a/meson.build
+++ b/meson.build
@@ -3,6 +3,7 @@ project('morsesrc', 'c',
 
 gst_dep = dependency('gstreamer-1.0')
 gstbase_dep = dependency('gstreamer-base-1.0')
+gstaudio_dep = dependency('gstreamer-audio-1.0')
 glib_dep = dependency('glib-2.0')
 gobject_dep = dependency('gobject-2.0')
 
@@ -14,7 +15,7 @@ src = [
 ]
 
 libgstmorsesrc = shared_library('gstmorsesrc', src,
-  dependencies: [gst_dep, gstbase_dep, glib_dep, gobject_dep, math_lib],
+  dependencies: [gstaudio_dep, gstbase_dep, gst_dep, glib_dep, gobject_dep, math_lib],
   install: true
 )
 
diff --git a/src/gstmorsesrc.c b/src/gstmorsesrc.c
index 1560020..2979d3f 100644
--- a/src/gstmorsesrc.c
+++ b/src/gstmorsesrc.c
@@ -31,7 +31,8 @@ Currently Not working.. Looking fo assitance..
 
 
 #include <gst/gst.h>
-#include <gst/base/gstpushsrc.h>
+#include <gst/base/base.h>
+#include <gst/audio/audio.h>
 #include <math.h>
 #include <ctype.h>
 
@@ -271,6 +272,20 @@ static gboolean gst_morse_src_start(GstBaseSrc *basesrc) {
     segment->time = 0;
     gst_base_src_new_segment(basesrc, segment);
 
+    {
+      GstAudioInfo audio_info;
+
+      gst_audio_info_set_format (&audio_info,
+          GST_AUDIO_FORMAT_S16LE,
+          src->rate, // FIXME: should really be negotiated with downstream instead
+          1,
+          NULL);
+
+      GstCaps *caps = gst_audio_info_to_caps (&audio_info);
+      gst_base_src_set_caps (basesrc, caps);
+      gst_caps_unref (caps);
+    }
+
     return TRUE;
 }
 
@@ -323,6 +338,7 @@ static void gst_morse_src_init(GstMorseSrc *src) {
     src->generated_morse = NULL;
     src->position = 0;
     src->timestamp = 0;
+    gst_base_src_set_format(GST_BASE_SRC(src), GST_FORMAT_TIME);
     gst_segment_init(&src->segment, GST_FORMAT_TIME);
 }

However, it looks like your morse signals are not scaled in duration according to the sample rate, so

gst-launch-1.0 morsesrc text="CQ CQ DE VK3DG" rate=24000 ! wavenc ! filesink location=morse.wav

sounds slower than

gst-launch-1.0 morsesrc text="CQ CQ DE VK3DG" rate=48000 ! wavenc ! filesink location=morse.wav

not sure if that’s as intended or not.

Hi again tpm,
Thanks again… I’ll add these an give a try. Your comment to remove the sample rate and allow Gstreamer caps decide is a valid comment. I’m not sure how to do this. In addition, probable need to have a few more audio cap types however, more than likely need to set the audio buffer to handle 16, 24 bits etc 16 bit is fine for my implementation.

I’ll need some time to check in the changes and give it shot.