aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScott Murray <scottm@ghidorah.spiteful.org>2018-05-28 21:16:23 -0400
committerScott Murray <scott.murray@konsulko.com>2018-06-05 21:17:14 -0400
commit9f5ed322b54f12c1055d2a124d687965083d4dee (patch)
tree4d8d115bd3683d3e2ff01379d55e04ff9f9e6579
parent127da1228b47486de500424e4a2ab3f0f5f0ec4a (diff)
Rework output to directly support 4Aflounder_5.99.1flounder/5.99.15.99.1
When building for 4A, switch to new gstreamer-based ALSA output for the RTL-SDR backend, and the Kingfisher backend now uses a gstreamer pipeline for its loopback to either an ALSA or Pulse sink depending on 4A or not. Using gstreamer instead of direct ALSA output has the benefit of transparently handling resampling to the M3ULCB hardware's required 48 KHz sample rate for the RTL-SDR backend. Change-Id: I2bfbf924927bb461cce88b04aba0e626f8d71215 Signed-off-by: Scott Murray <scott.murray@konsulko.com>
-rw-r--r--.gitignore1
-rw-r--r--binding/CMakeLists.txt25
-rw-r--r--binding/radio-binding.c35
-rw-r--r--binding/radio_impl.h2
-rw-r--r--binding/radio_impl_kingfisher.c115
-rw-r--r--binding/radio_impl_rtlsdr.c8
-rw-r--r--binding/radio_output_gstreamer.c218
-rw-r--r--conf.d/cmake/config.cmake6
-rw-r--r--conf.d/wgt/config-4a.xml.in29
9 files changed, 382 insertions, 57 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b25c15b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt
index d279945..c295eb0 100644
--- a/binding/CMakeLists.txt
+++ b/binding/CMakeLists.txt
@@ -26,6 +26,11 @@ PROJECT_TARGET_ADD(radio-binding)
radio-binding.c
radio_impl_kingfisher.c
radio_impl_rtlsdr.c)
+ if(HAVE_4A_FRAMEWORK)
+ add_definitions(-DHAVE_4A_FRAMEWORK)
+ endif()
+
+ PKG_CHECK_MODULES(SOUND REQUIRED gstreamer-1.0)
add_library(${TARGET_NAME} MODULE ${radio_SOURCES})
@@ -38,7 +43,8 @@ PROJECT_TARGET_ADD(radio-binding)
)
# Library dependencies (include updates automatically)
- TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries})
+ TARGET_COMPILE_OPTIONS(${TARGET_NAME} PUBLIC ${SOUND_CFLAGS})
+ TARGET_LINK_LIBRARIES(${TARGET_NAME} ${SOUND_LIBRARIES} ${link_libraries})
# installation directory
INSTALL(TARGETS ${TARGET_NAME}
@@ -51,9 +57,16 @@ PROJECT_TARGET_ADD(rtl_fm_helper)
set(helper_SOURCES
${TARGET_NAME}.c
rtl_fm.c
- convenience/convenience.c
- radio_output.c)
- PKG_CHECK_MODULES(SOUND REQUIRED libpulse-simple)
+ convenience/convenience.c)
+ if(HAVE_4A_FRAMEWORK)
+ set(helper_SOURCES ${helper_SOURCES} radio_output_gstreamer.c)
+ PKG_CHECK_MODULES(helper_SOUND REQUIRED gstreamer-1.0)
+ else()
+ set(helper_SOURCES ${helper_SOURCES} radio_output_pulse.c)
+ PKG_CHECK_MODULES(helper_SOUND REQUIRED libpulse-simple)
+ endif()
+
+ PKG_CHECK_MODULES(helper_RTLSDR REQUIRED librtlsdr)
add_executable(${TARGET_NAME} ${helper_SOURCES})
@@ -62,6 +75,6 @@ PROJECT_TARGET_ADD(rtl_fm_helper)
OUTPUT_NAME ${TARGET_NAME}
)
- PKG_CHECK_MODULES(RTLSDR REQUIRED librtlsdr)
+ TARGET_COMPILE_OPTIONS(${TARGET_NAME} PUBLIC ${helper_SOUND_CFLAGS})
TARGET_LINK_LIBRARIES(${TARGET_NAME}
- ${RTLSDR_LIBRARIES} ${SOUND_LIBRARIES} ${link_libraries} m)
+ ${helper_RTLSDR_LIBRARIES} ${helper_SOUND_LIBRARIES} ${link_libraries} m)
diff --git a/binding/radio-binding.c b/binding/radio-binding.c
index 8efe3bd..4db34db 100644
--- a/binding/radio-binding.c
+++ b/binding/radio-binding.c
@@ -466,22 +466,53 @@ static const struct afb_verb_v2 verbs[]= {
static int init()
{
int rc;
+ char *output = NULL;
+
+#ifdef HAVE_4A_FRAMEWORK
+ json_object *response;
+
+ json_object *jsonData = json_object_new_object();
+ json_object_object_add(jsonData, "audio_role", json_object_new_string("Radio"));
+ json_object_object_add(jsonData, "endpoint_type", json_object_new_string("sink"));
+ rc = afb_service_call_sync("ahl-4a", "stream_open", jsonData, &response);
+ if(!rc) {
+ json_object *valJson = NULL;
+ json_object *val = NULL;
+
+ rc = json_object_object_get_ex(response, "response", &valJson);
+ if(rc) {
+ rc = json_object_object_get_ex(valJson, "device_uri", &val);
+ if(rc) {
+ const char *jres_pcm = json_object_get_string(val);
+ char *p;
+ if((p = strchr(jres_pcm, ':'))) {
+ output = strdup(p + 1);
+ }
+ }
+ }
+ } else {
+ AFB_ERROR("afb_service_call_sync failed\n");
+ return rc;
+ }
+#endif
+ // Initialize event structures
freq_event = afb_daemon_make_event("frequency");
scan_event = afb_daemon_make_event("station_found");
// Look for RTL-SDR USB adapter
radio_impl_ops = &rtlsdr_impl_ops;
- rc = radio_impl_ops->init();
+ rc = radio_impl_ops->init(output);
if(rc != 0) {
// Look for Kingfisher Si4689
radio_impl_ops = &kf_impl_ops;
- rc = radio_impl_ops->init();
+ rc = radio_impl_ops->init(output);
}
if(rc == 0) {
printf("%s found\n", radio_impl_ops->name);
radio_impl_ops->set_frequency_callback(freq_callback, NULL);
}
+ free(output);
return rc;
}
diff --git a/binding/radio_impl.h b/binding/radio_impl.h
index 4b4a2f7..f745f12 100644
--- a/binding/radio_impl.h
+++ b/binding/radio_impl.h
@@ -41,7 +41,7 @@ typedef enum {
typedef struct {
char *name;
- int (*init)(void);
+ int (*init)(const char *output);
uint32_t (*get_frequency)(void);
diff --git a/binding/radio_impl_kingfisher.c b/binding/radio_impl_kingfisher.c
index 0685388..b6c0d11 100644
--- a/binding/radio_impl_kingfisher.c
+++ b/binding/radio_impl_kingfisher.c
@@ -22,6 +22,9 @@
#include <glib.h>
#include <fcntl.h>
#include <sys/stat.h>
+#include <gst/gst.h>
+#define AFB_BINDING_VERSION 2
+#include <afb/afb-binding.h>
#include "radio_impl.h"
@@ -29,6 +32,9 @@
#define SI_INIT "/usr/bin/si_init"
#define SI_CTL "/usr/bin/si_ctl"
+#define GST_SINK_OPT_LEN 128
+#define GST_PIPELINE_LEN 256
+
// Structure to describe FM band plans, all values in Hz.
typedef struct {
char *name;
@@ -47,19 +53,22 @@ static fm_band_plan_t known_fm_band_plans[5] = {
static unsigned int bandplan = 0;
static bool present;
-static bool active;
static uint32_t current_frequency;
static int scan_valid_snr_threshold = 128;
static int scan_valid_rssi_threshold = 128;
static bool scanning = false;
+// GStreamer state
+static GstElement *pipeline;
+static bool running;
+
static void (*freq_callback)(uint32_t, void*);
static void *freq_callback_data;
static uint32_t kf_get_min_frequency(radio_band_t band);
static void kf_scan_stop(void);
-static int kf_init(void)
+static int kf_init(const char *output)
{
GKeyFile* conf_file;
int conf_file_present = 0;
@@ -67,7 +76,8 @@ static int kf_init(void)
char *value_str;
char cmd[128];
int rc;
- char *output_sink;
+ char output_sink_opt[GST_SINK_OPT_LEN];
+ char gst_pipeline_str[GST_PIPELINE_LEN];
if(present)
return 0;
@@ -122,7 +132,7 @@ static int kf_init(void)
"scan_valid_snr_threshold",
&error);
if(!error) {
- fprintf(stderr, "Scan valid SNR level set to %d\n", n);
+ AFB_INFO("Scan valid SNR level set to %d", n);
scan_valid_snr_threshold = n;
}
@@ -132,7 +142,7 @@ static int kf_init(void)
"scan_valid_rssi_threshold",
&error);
if(!error) {
- fprintf(stderr, "Scan valid SNR level set to %d\n", n);
+ AFB_INFO("Scan valid SNR level set to %d", n);
scan_valid_rssi_threshold = n;
}
@@ -141,11 +151,11 @@ static int kf_init(void)
rc = system(SI_INIT);
if(rc != 0) {
- fprintf(stderr, "si_init failed, rc = %d", rc);
+ AFB_ERROR("si_init failed, rc = %d", rc);
return -1;
}
- fprintf(stderr, "Using FM Bandplan [KF]: %s\n", known_fm_band_plans[bandplan].name);
+ AFB_INFO("Using FM Bandplan: %s", known_fm_band_plans[bandplan].name);
current_frequency = kf_get_min_frequency(BAND_FM);
sprintf(cmd,
"%s /dev/i2c-12 0x65 -b fm -p %s -t %d -u %d -c %d",
@@ -156,39 +166,50 @@ static int kf_init(void)
current_frequency / 1000);
rc = system(cmd);
if(rc != 0) {
- fprintf(stderr, "%s failed, rc = %d", SI_CTL, rc);
+ AFB_ERROR("%s failed, rc = %d", SI_CTL, rc);
return -1;
}
- // Handle 4A disabling PA udev module
- if(system("pactl list sources short |grep -q alsa_input.radio") != 0) {
- // Set up radio source
- if(system("pactl load-module module-alsa-source device=hw:radio,0 name=radio") != 0) {
- fprintf(stderr, "radio PA source creation failed!\n");
+ // Initialize GStreamer
+ gst_init(NULL, NULL);
+
+ if(output) {
+ AFB_INFO("Using output device %s", output);
+ rc = snprintf(output_sink_opt, GST_SINK_OPT_LEN, " device=%s", output);
+ if(rc >= GST_SINK_OPT_LEN) {
+ AFB_ERROR("output device string too long");
return -1;
}
+ } else {
+ output_sink_opt[0] = '\0';
}
-
- // Set initial state to muted
- rc = system("pactl set-source-mute alsa_input.radio 1");
- if(rc != 0) {
- fprintf(stderr, "pactl failed, rc = %d", rc);
+ // NOTE: If muting without pausing is desired, it can likely be done
+ // by adding a "volume" element to the pipeline before the sink,
+ // and setting the volume to 0 to mute.
+#ifdef HAVE_4A_FRAMEWORK
+ rc = snprintf(gst_pipeline_str,
+ GST_PIPELINE_LEN,
+ "alsasrc device=hw:radio ! queue ! audioconvert ! audioresample ! alsasink%s",
+ output_sink_opt);
+#else
+ // Use PulseAudio output for compatibility with audiomanager / module_router
+ rc = snprintf(gst_pipeline_str,
+ GST_PIPELINE_LEN,
+ "alsasrc device=hw:radio ! queue ! audioconvert ! audioresample ! pulsesink stream-properties=\"props,media.role=radio\"");
+#endif
+ if(rc >= GST_PIPELINE_LEN) {
+ AFB_ERROR("pipeline string too long");
return -1;
}
-
- // Set up loopback to output sink
- output_sink = getenv("PULSE_SINK");
- if(!output_sink) {
- // On non-4A, loopback to the sink for the on-board Starter Kit M3/H3 audio
- output_sink = "1";
- }
- sprintf(cmd, "pactl load-module module-loopback source=alsa_input.radio sink=%s", output_sink);
- rc = system(cmd);
- if(rc != 0) {
- fprintf(stderr, "pactl failed, rc = %d", rc);
+ pipeline = gst_parse_launch(gst_pipeline_str, NULL);
+ if(!pipeline) {
+ AFB_ERROR("pipeline construction failed!");
return -1;
}
+ // Start pipeline in paused state
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
present = true;
return 0;
}
@@ -281,26 +302,28 @@ static void kf_start(void)
if(!present)
return;
- if(!active) {
- rc = system("pactl set-source-mute alsa_input.radio 0");
- if(rc != 0)
- fprintf(stderr, "pactl set-source-mute failed!\n");
- active = true;
+ if(!running) {
+ // Start pipeline
+ gst_element_set_state(pipeline, GST_STATE_PLAYING);
+ running = true;
}
}
static void kf_stop(void)
{
- int rc;
-
- if(!present)
- return;
-
- if(active) {
- active = false;
- rc = system("pactl set-source-mute alsa_input.radio 1");
- if(rc != 0)
- fprintf(stderr, "pactl set-source-mute failed!\n");
+ GstEvent *event;
+
+ if(present && running) {
+ // Stop pipeline
+ running = false;
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Flush pipeline
+ // This seems required to avoid stutters on starts after a stop
+ event = gst_event_new_flush_start();
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ event = gst_event_new_flush_stop(TRUE);
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
}
}
@@ -324,14 +347,14 @@ static void kf_scan_start(radio_scan_direction_t direction,
sprintf(cmd, "%s /dev/i2c-12 0x65 -l %s", SI_CTL, direction == SCAN_FORWARD ? "up" : "down");
fp = popen(cmd, "r");
if(fp == NULL) {
- fprintf(stderr, "Could not run: %s!\n", cmd);
+ AFB_ERROR("Could not run: %s!", cmd);
return;
}
// Look for "Frequency:" in output
while(fgets(line, 128, fp) != NULL) {
if(strncmp("Frequency:", line, 10) == 0) {
new_frequency = atoi(line + 10);
- //fprintf(stderr, "%s: got new_frequency = %d\n", __FUNCTION__, new_frequency);
+ //AFB_DEBUG("%s: got new_frequency = %d", __FUNCTION__, new_frequency);
break;
}
}
diff --git a/binding/radio_impl_rtlsdr.c b/binding/radio_impl_rtlsdr.c
index 7fd4d69..b8b2454 100644
--- a/binding/radio_impl_rtlsdr.c
+++ b/binding/radio_impl_rtlsdr.c
@@ -125,7 +125,7 @@ static pid_t popen2(char *command, int *in_fd, int *out_fd)
return pid;
}
-static int rtlsdr_init(void)
+static int rtlsdr_init(const char *output)
{
GKeyFile *conf_file;
char *value_str;
@@ -183,6 +183,12 @@ static int rtlsdr_init(void)
return -1;
}
+ if(output) {
+ // Indicate desired output to helper
+ AFB_INFO("Setting RADIO_OUTPUT=%s", output);
+ setenv("RADIO_OUTPUT", output, 1);
+ }
+
// Run helper
if(snprintf(helper_path, PATH_MAX, "%s/bin/%s", rootdir, HELPER_NAME) == PATH_MAX) {
AFB_ERROR("Could not create path to %s", HELPER_NAME);
diff --git a/binding/radio_output_gstreamer.c b/binding/radio_output_gstreamer.c
new file mode 100644
index 0000000..698b81b
--- /dev/null
+++ b/binding/radio_output_gstreamer.c
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2018 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <gst/gst.h>
+
+#include "radio_output.h"
+#include "rtl_fm.h"
+
+// Output buffer
+static unsigned int extra;
+static int16_t extra_buf[1];
+static unsigned char *output_buf;
+
+// GStreamer state
+static GstElement *pipeline, *appsrc;
+static bool running;
+
+int radio_output_open()
+{
+ unsigned int rate = 24000;
+ GstElement *queue, *convert, *sink, *resample;
+ char *p;
+
+ if(pipeline)
+ return 0;
+
+ // Initialize GStreamer
+#ifdef DEBUG
+ unsigned int argc = 2;
+ char **argv = malloc(2 * sizeof(char*));
+ argv[0] = strdup("test");
+ argv[1] = strdup("--gst-debug-level=5");
+ gst_init(&argc, &argv);
+#else
+ gst_init(NULL, NULL);
+#endif
+
+ // Setup pipeline
+ // NOTE: With our use of the simple buffer pushing mode, there seems to
+ // be no need for a mainloop, so currently not instantiating one.
+ pipeline = gst_pipeline_new("pipeline");
+ appsrc = gst_element_factory_make("appsrc", "source");
+ queue = gst_element_factory_make("queue", "queue");
+ convert = gst_element_factory_make("audioconvert", "convert");
+ resample = gst_element_factory_make("audioresample", "resample");
+ sink = gst_element_factory_make("alsasink", "sink");
+ if(!(pipeline && appsrc && queue && convert && resample && sink)) {
+ fprintf(stderr, "pipeline element construction failed!\n");
+ }
+ g_object_set(G_OBJECT(appsrc), "caps",
+ gst_caps_new_simple("audio/x-raw",
+ "format", G_TYPE_STRING, "S16LE",
+ "rate", G_TYPE_INT, rate,
+ "channels", G_TYPE_INT, 2,
+ "layout", G_TYPE_STRING, "interleaved",
+ "channel-mask", G_TYPE_UINT64, 3,
+ NULL), NULL);
+
+ if((p = getenv("RADIO_OUTPUT"))) {
+ fprintf(stderr, "Using output device %s\n", p);
+ g_object_set(sink, "device", p, NULL);
+ }
+ gst_bin_add_many(GST_BIN(pipeline), appsrc, queue, convert, resample, sink, NULL);
+ gst_element_link_many(appsrc, queue, convert, resample, sink, NULL);
+ //gst_bin_add_many(GST_BIN(pipeline), appsrc, convert, resample, sink, NULL);
+ //gst_element_link_many(appsrc, convert, resample, sink, NULL);
+
+ // Set up appsrc
+ // NOTE: Radio seems like it matches the use case the "is-live" property
+ // is for, but setting it seems to require a lot more work with
+ // respect to latency settings to make the pipeline work smoothly.
+ // For now, leave it unset since the result seems to work
+ // reasonably well.
+ g_object_set(G_OBJECT(appsrc),
+ "stream-type", 0,
+ "format", GST_FORMAT_TIME,
+ NULL);
+
+ // Start pipeline in paused state
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Set up output buffer
+ extra = 0;
+ output_buf = malloc(sizeof(unsigned char) * RTL_FM_MAXIMUM_BUF_LENGTH);
+
+ return 0;
+}
+
+int radio_output_start(void)
+{
+ int rc = 0;
+
+ if(!pipeline) {
+ rc = radio_output_open();
+ if(rc)
+ return rc;
+ }
+
+ // Start pipeline
+ running = true;
+ gst_element_set_state(pipeline, GST_STATE_PLAYING);
+
+ return rc;
+}
+
+void radio_output_stop(void)
+{
+ GstEvent *event;
+
+ if(pipeline && running) {
+ // Stop pipeline
+ running = false;
+ gst_element_set_state(pipeline, GST_STATE_PAUSED);
+
+ // Flush pipeline
+ // This seems required to avoid stutters on starts after a stop
+ event = gst_event_new_flush_start();
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ event = gst_event_new_flush_stop(TRUE);
+ gst_element_send_event(GST_ELEMENT(pipeline), event);
+ }
+}
+
+void radio_output_suspend(int state)
+{
+ // Placeholder
+}
+
+void radio_output_close(void)
+{
+ radio_output_stop();
+
+ if(pipeline) {
+ // Tear down pipeline
+ gst_element_set_state(pipeline, GST_STATE_NULL);
+ gst_object_unref(GST_OBJECT(pipeline));
+ pipeline = NULL;
+ running = false;
+ }
+
+ free(output_buf);
+ output_buf = NULL;
+}
+
+int radio_output_write(void *buf, int len)
+{
+ int rc = -EINVAL;
+ size_t n = len;
+ int samples = len / 2;
+ unsigned char *p;
+ GstBuffer *buffer;
+ GstFlowReturn ret;
+
+ if(!(pipeline && buf)) {
+ return rc;
+ }
+
+ // Don't bother pushing samples if output hasn't started
+ if(!running)
+ return 0;
+
+ /*
+ * Handle the rtl_fm code giving us an odd number of samples.
+ * This extra buffer copying approach is not particularly efficient,
+ * but works for now. It looks feasible to hack in something in the
+ * demod and output thread routines in rtl_fm.c to handle it there
+ * if more performance is required.
+ */
+ p = output_buf;
+ if(extra) {
+ memcpy(output_buf, extra_buf, sizeof(int16_t));
+ if((extra + samples) % 2) {
+ // We still have an extra sample, n remains the same, store the extra
+ memcpy(output_buf + sizeof(int16_t), buf, n - 2);
+ memcpy(extra_buf, ((unsigned char*) buf) + n - 2, sizeof(int16_t));
+ } else {
+ // We have an even number of samples, no extra
+ memcpy(output_buf + sizeof(int16_t), buf, n);
+ n += 2;
+ extra = 0;
+ }
+ } else if(samples % 2) {
+ // We have an extra sample, store it, and decrease n
+ n -= 2;
+ memcpy(output_buf + sizeof(int16_t), buf, n);
+ memcpy(extra_buf, ((unsigned char*) buf) + n, sizeof(int16_t));
+ extra = 1;
+ } else {
+ p = buf;
+ }
+
+ // Push buffer into pipeline
+ buffer = gst_buffer_new_allocate(NULL, n, NULL);
+ gst_buffer_fill(buffer, 0, p, n);
+ g_signal_emit_by_name(appsrc, "push-buffer", buffer, &ret);
+ gst_buffer_unref(buffer);
+ rc = n;
+
+ return rc;
+}
diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake
index 4fa7666..86588d4 100644
--- a/conf.d/cmake/config.cmake
+++ b/conf.d/cmake/config.cmake
@@ -91,7 +91,11 @@ set(LD_LIBRARY_PATH ${CMAKE_INSTALL_PREFIX}/lib64 ${CMAKE_INSTALL_PREFIX}/lib)
# Optional location for config.xml.in
# -----------------------------------
-set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in)
+if(HAVE_4A_FRAMEWORK)
+ set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config-4a.xml.in)
+else()
+ set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in)
+endif()
# Mandatory widget Mimetype specification of the main unit
# --------------------------------------------------------------------------
diff --git a/conf.d/wgt/config-4a.xml.in b/conf.d/wgt/config-4a.xml.in
new file mode 100644
index 0000000..42ad037
--- /dev/null
+++ b/conf.d/wgt/config-4a.xml.in
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<widget xmlns="http://www.w3.org/ns/widgets" id="@PROJECT_NAME@" version="@PROJECT_VERSION@">
+ <name>@PROJECT_NAME@</name>
+ <icon src="@PROJECT_ICON@"/>
+ <content src="@WIDGET_ENTRY_POINT@" type="@WIDGET_TYPE@"/>
+ <description>@PROJECT_DESCRIPTION@</description>
+ <author>@PROJECT_AUTHOR@ &lt;@PROJECT_AUTHOR_MAIL@&gt;</author>
+ <license>@PROJECT_LICENSE@</license>
+
+ <feature name="urn:AGL:widget:required-permission">
+ <param name="urn:AGL:permission::public:hidden" value="required" />
+ <param name="urn:AGL:permission::public:no-htdocs" value="required" />
+ <param name="urn:AGL:permission:audio:public:audiostream" value="required" />
+ </feature>
+
+ <feature name="urn:AGL:widget:file-properties">
+ <param name="bin/rtl_fm_helper" value="executable" />
+ </feature>
+
+ <feature name="urn:AGL:widget:provided-api">
+ <param name="radio" value="ws" />
+ </feature>
+
+ <feature name="urn:AGL:widget:required-api">
+ <param name="ahl-4a" value="ws" />
+ <param name="@WIDGET_ENTRY_POINT@" value="local" />
+ </feature>
+
+</widget>