diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | binding/CMakeLists.txt | 25 | ||||
-rw-r--r-- | binding/radio-binding.c | 35 | ||||
-rw-r--r-- | binding/radio_impl.h | 2 | ||||
-rw-r--r-- | binding/radio_impl_kingfisher.c | 115 | ||||
-rw-r--r-- | binding/radio_impl_rtlsdr.c | 8 | ||||
-rw-r--r-- | binding/radio_output_gstreamer.c | 218 | ||||
-rw-r--r-- | conf.d/cmake/config.cmake | 6 | ||||
-rw-r--r-- | conf.d/wgt/config-4a.xml.in | 29 |
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 4a61011..a652ff1 100644 --- a/binding/radio-binding.c +++ b/binding/radio-binding.c @@ -467,22 +467,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 ac7ec7f..3612424 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: %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-11 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-11 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@ <@PROJECT_AUTHOR_MAIL@></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> |