Alexander Gromnitsky's Blog

Embedding .oga files into an executable to play them via GStreamer

Latest update:

We begin with a digression.

Gtk4 has an abstract interface MediaStream for media playback. Why not use it? I found its realisation (GtkMediaFile) strangly quirky: after receiving notify::ended signal & calling g_object_unref() on GtkMediaStream object, it still retains a connection to the pipewire-pulse daemon.

You can easily test this behavior with tests/testsounds.c from the gtk repo. Compile the example:

cc -o testsounds testsounds.c `pkg-config --cflags --libs gtk4`

& run it while keeping pavucontrol open on the 'Playback' tab. If you press any of the buttons in the testsounds app 5 times, you will notice 5 new entries in pavucontrol, even after the playback has finished:

These entries only disappear when you kill the Gtk4 app!

If all you need is to play a short sound to attract a user's attention, you can use GStreamer directly without additional wrappers.

Why embeed?

There are no 'standard' sounds in gtk4. There is humble gdk_display_beep(), but it relies on a PC speaker under X11 that some users disable, for beeps can be too annoying when typing in a terminal emulator.

Therefore, it's preferable to play real sound files, like the collection of .oga files (with Vorbis streams inside) from

$ rpm -qf /usr/share/sounds/freedesktop/stereo
sound-theme-freedesktop-0.8-19.fc38.noarch

which is fine & dandy if a user has at least the default freedesktop.org sound theme installed, but a Linux desktop is not Windows, there is no guarantee for anything.

We could bring an .oga file with our executable, or, if the sound file is small, embed it directly into the compiled app.

How to embed

The C23 revision provides #embed preprocessor directive for

  char bell[] = {
#embed "bell.oga"
  };

party tricks, but the current GCC 13 doesn't support it, & even if it did, the users of various slowpoke distros (like the one starting with 'D') woudn't be able to enjoy such a modern compiler for the next N years anyway.

This leaves us with base64 strings, as if we were embeding something in an HTML page to decode it with JavaScript. E.g., if we have a function

void play_ogg(char *base64);

in file.c, then our Makefile can contain

file.o: CFLAGS += -DSOUND_BELL=\"`base64 -w0 bell.oga`\"
file.o: bell.oga

that adds SOUND_BELL macro to the CFLAGS env var only for 'file.o' target. GNU Make calls this stunt 'target-specific variable values' & I recomend it wholeheartedly.

GStreamer

In file.c, the simplest function to play an .oga file may look like

#include <gst/gst.h>

void play_ogg(char *base64) {
  size_t len = strlen(base64);
  char ps[len+50];
  snprintf(ps, sizeof(ps), "playbin uri=data:audio/ogg;base64,%s", base64);
  GstElement *pipeline = gst_parse_launch(ps, NULL);
  gst_element_set_state(pipeline, GST_STATE_PLAYING);
  GstBus *bus = gst_element_get_bus(pipeline);
  GstMessage *msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
  if (msg != NULL) gst_message_unref(msg);
  gst_object_unref(bus);
  gst_element_set_state(pipeline, GST_STATE_NULL);
  gst_object_unref(pipeline);
}

You call it as

play_ogg(SOUND_BELL);

and the preprocessor replaces it with play_ogg("T2dnUwACAA…") during a compilation. Don't forget to initialise GStreamer somewhere in the beginning of main():

gst_init(NULL, NULL);
GtkApplication *app = gtk_application_new(…);

Tags: ойті
Authors: ag