From e65da04f8451c1166a414fdb58acfe01c63e4f94 Mon Sep 17 00:00:00 2001 From: Scott Murray Date: Fri, 18 May 2018 11:48:09 -0400 Subject: Split rtlsdr code into standalone helper To avoid the GPL licensed code in rtl_fm.c making all of the binding GPL, rework things to wrap it with a simple standalone helper executable that is driven via stdin/stdout. While this could potentially be done by running the original unmodified rtl_fm utility itself, it would be impossible to implement scanning with that approach without some compromising of playback latency and quality. The current helper implementation is simple enough that replacing it with an alternate one should be relatively straightforward if that is desired. Change-Id: If83b834da3999f5807d1453524ae72b1c3559c90 Signed-off-by: Scott Murray --- binding/CMakeLists.txt | 28 ++++- binding/radio_impl_rtlsdr.c | 270 +++++++++++++++++++++++++++++++-------- binding/radio_output.c | 294 ------------------------------------------- binding/radio_output_pulse.c | 294 +++++++++++++++++++++++++++++++++++++++++++ binding/rtl_fm_helper.c | 242 +++++++++++++++++++++++++++++++++++ 5 files changed, 776 insertions(+), 352 deletions(-) delete mode 100644 binding/radio_output.c create mode 100644 binding/radio_output_pulse.c create mode 100644 binding/rtl_fm_helper.c (limited to 'binding') diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt index 321022a..d279945 100644 --- a/binding/CMakeLists.txt +++ b/binding/CMakeLists.txt @@ -1,5 +1,6 @@ ########################################################################### # Copyright 2015, 2016, 2017 IoT.bzh +# Copyright (C) 2018 Konsulko Group # # author: Fulup Ar Foll # contrib: Romain Forlot @@ -23,11 +24,8 @@ PROJECT_TARGET_ADD(radio-binding) # Define project Targets set(radio_SOURCES radio-binding.c - radio_output.c radio_impl_kingfisher.c - radio_impl_rtlsdr.c - rtl_fm.c - convenience/convenience.c) + radio_impl_rtlsdr.c) add_library(${TARGET_NAME} MODULE ${radio_SOURCES}) @@ -45,3 +43,25 @@ PROJECT_TARGET_ADD(radio-binding) # installation directory INSTALL(TARGETS ${TARGET_NAME} LIBRARY DESTINATION ${BINDINGS_INSTALL_DIR}) + +# Add helper program target +PROJECT_TARGET_ADD(rtl_fm_helper) + + # Define project targets + set(helper_SOURCES + ${TARGET_NAME}.c + rtl_fm.c + convenience/convenience.c + radio_output.c) + PKG_CHECK_MODULES(SOUND REQUIRED libpulse-simple) + + add_executable(${TARGET_NAME} ${helper_SOURCES}) + + SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES + LABELS "EXECUTABLE" + OUTPUT_NAME ${TARGET_NAME} + ) + + PKG_CHECK_MODULES(RTLSDR REQUIRED librtlsdr) + TARGET_LINK_LIBRARIES(${TARGET_NAME} + ${RTLSDR_LIBRARIES} ${SOUND_LIBRARIES} ${link_libraries} m) diff --git a/binding/radio_impl_rtlsdr.c b/binding/radio_impl_rtlsdr.c index 22d627e..7fd4d69 100644 --- a/binding/radio_impl_rtlsdr.c +++ b/binding/radio_impl_rtlsdr.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Konsulko Group + * Copyright (C) 2017,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. @@ -19,11 +19,18 @@ #include #include #include +#include +#include +#include +#include #include +#define AFB_BINDING_VERSION 2 +#include #include "radio_impl.h" -#include "radio_output.h" -#include "rtl_fm.h" + +#define HELPER_NAME "rtl_fm_helper" +#define HELPER_MAX PATH_MAX + 64 // Structure to describe FM band plans, all values in Hz. typedef struct { @@ -42,27 +49,91 @@ static fm_band_plan_t known_fm_band_plans[5] = { }; static unsigned int bandplan; +static pid_t helper_pid; +static int helper_in; +static int helper_out; +static pthread_mutex_t helper_mutex = PTHREAD_MUTEX_INITIALIZER; static bool present; static bool active; +static bool scanning; static uint32_t current_frequency; +static radio_freq_callback_t freq_callback; +static void *freq_callback_data; static uint32_t rtlsdr_get_min_frequency(radio_band_t band); +static void rtlsdr_set_frequency(uint32_t frequency); static void rtlsdr_scan_stop(void); -static void rtl_output_callback(int16_t *result, int result_len, void *ctx) +/* + * Bi-directional popen implementation + * Based on one of the versions given in: + * + * https://stackoverflow.com/questions/12778672/killing-process-that-has-been-created-with-popen2 + */ +static pid_t popen2(char *command, int *in_fd, int *out_fd) { - if(active) - radio_output_write((char*) result, result_len * 2); + int pin[2], pout[2]; + pid_t pid; + + if(out_fd != NULL) { + if(pipe(pin) != 0) + return -1; + } + if(in_fd != NULL) { + if (pipe(pout) != 0) { + if(out_fd != NULL) { + close(pin[0]); + close(pin[1]); + } + return -1; + } + } + + pid = fork(); + if(pid < 0) { + if(out_fd != NULL) { + close(pin[0]); + close(pin[1]); + } + if(in_fd != NULL) { + close(pout[0]); + close(pout[1]); + } + return pid; + } + if(pid == 0) { + if(out_fd != NULL) { + close(pin[1]); + dup2(pin[0], 0); + } + if(in_fd != NULL) { + close(pout[0]); + dup2(pout[1], 1); + } + execlp(command, command, NULL); + perror("Error:"); + exit(1); + } + if(in_fd != NULL) { + close(pout[1]); + *in_fd = pout[0]; + } + if(out_fd != NULL) { + close(pin[0]); + *out_fd = pin[1]; + } + return pid; } static int rtlsdr_init(void) { - GKeyFile* conf_file; - int conf_file_present = 0; + GKeyFile *conf_file; char *value_str; + char *rootdir; + char *helper_path; if(present) - return -1; + return 0; // Load settings from configuration file if it exists conf_file = g_key_file_new(); @@ -73,7 +144,6 @@ static int rtlsdr_init(void) NULL, G_KEY_FILE_KEEP_COMMENTS, NULL) == TRUE) { - conf_file_present = 1; // Set band plan if it is specified value_str = g_key_file_get_string(conf_file, @@ -92,42 +162,43 @@ static int rtlsdr_init(void) } } } - fprintf(stderr, "Using FM Bandplan: %s\n", known_fm_band_plans[bandplan].name); + // Start off with minimum bandplan frequency current_frequency = rtlsdr_get_min_frequency(BAND_FM); - if(rtl_fm_init(current_frequency, 200000, 48000, rtl_output_callback, NULL) < 0) { - return -1; - } - if(conf_file_present) { - GError *error = NULL; - int n; - - // Allow over-riding scanning parameters just in case a demo - // setup needs to do so to work reliably. - n = g_key_file_get_integer(conf_file, - "radio", - "scan_squelch_level", - &error); - if(!error) { - fprintf(stderr, "Scanning squelch level set to %d\n", n); - rtl_fm_scan_set_squelch_level(n); - } + rootdir = getenv("AFM_APP_INSTALL_DIR"); + if(!rootdir) + return -1; - error = NULL; - n = g_key_file_get_integer(conf_file, - "radio", - "scan_squelch_limit", - &error); - if(!error) { - fprintf(stderr, "Scanning squelch limit set to %d\n", n); - rtl_fm_scan_set_squelch_limit(n); - } + // Run helper to detect adapter + helper_path = malloc(HELPER_MAX); + if(!helper_path) + return -ENOMEM; + if(snprintf(helper_path, HELPER_MAX, "%s/bin/%s --detect", rootdir, HELPER_NAME) == HELPER_MAX) { + AFB_ERROR("Could not create command for \"%s --detect\"", HELPER_NAME); + return -EINVAL; + } + if(system(helper_path) != 0) { + free(helper_path); + return -1; + } - g_key_file_free(conf_file); + // 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); + return -EINVAL; } + helper_pid = popen2(helper_path, &helper_out, &helper_in); + if(helper_pid < 0) { + AFB_ERROR("Could not run %s!", HELPER_NAME); + return -1; + } + AFB_DEBUG("%s started", HELPER_NAME); + free(helper_path); present = true; + rtlsdr_set_frequency(current_frequency); + return 0; } @@ -138,6 +209,12 @@ static uint32_t rtlsdr_get_frequency(void) static void rtlsdr_set_frequency(uint32_t frequency) { + char cmd[64]; + char output[128]; + bool found = false; + int rc; + uint32_t n; + if(!present) return; @@ -145,19 +222,45 @@ static void rtlsdr_set_frequency(uint32_t frequency) frequency > known_fm_band_plans[bandplan].max) return; - rtlsdr_scan_stop(); + if(scanning) + rtlsdr_scan_stop(); + current_frequency = frequency; - rtl_fm_set_freq(frequency); + sprintf(cmd, "F=%u\n", frequency); + pthread_mutex_lock(&helper_mutex); + rc = write(helper_in, cmd, strlen(cmd)); + if(rc < 0) { + pthread_mutex_unlock(&helper_mutex); + return; + } + while(!found) { + rc = read(helper_out, output, 128); + if(rc <= 0) + break; + if(rc == 128) + rc = 127; + output[rc] = '\0'; + if(output[0] == 'F') { + if(sscanf(output, "F=%u\n", &n) == 1) { + if(freq_callback) + freq_callback(n, freq_callback_data); + found = true; + } + } + } + pthread_mutex_unlock(&helper_mutex); } static void rtlsdr_set_frequency_callback(radio_freq_callback_t callback, void *data) { - rtl_fm_set_freq_callback(callback, data); + freq_callback = callback; + freq_callback_data = data; } static radio_band_t rtlsdr_get_band(void) { + // We only support FM return BAND_FM; } @@ -206,10 +309,15 @@ static void rtlsdr_start(void) return; if(!active) { - if(radio_output_start() != 0) + int rc; + char cmd[64]; + + sprintf(cmd, "START\n"); + pthread_mutex_lock(&helper_mutex); + rc = write(helper_in, cmd, strlen(cmd)); + pthread_mutex_unlock(&helper_mutex); + if(rc < 0) return; - - rtl_fm_start(); active = true; } } @@ -220,10 +328,14 @@ static void rtlsdr_stop(void) return; if(active) { - active = false; - radio_output_stop(); - rtl_fm_stop(); + int rc; + char cmd[64]; + active = false; + sprintf(cmd, "STOP\n"); + pthread_mutex_lock(&helper_mutex); + write(helper_in, cmd, strlen(cmd)); + pthread_mutex_unlock(&helper_mutex); } } @@ -231,21 +343,71 @@ static void rtlsdr_scan_start(radio_scan_direction_t direction, radio_scan_callback_t callback, void *data) { - rtl_fm_scan_start(direction == SCAN_FORWARD ? 0 : 1, - callback, - data, - rtlsdr_get_frequency_step(BAND_FM), - rtlsdr_get_min_frequency(BAND_FM), - rtlsdr_get_max_frequency(BAND_FM)); + int rc; + char cmd[64]; + char output[128]; + bool found = false; + uint32_t n; + + if(!active || scanning) + return; + + scanning = true; + sprintf(cmd, "S=%s\n", direction == SCAN_FORWARD ? "UP" : "DOWN"); + pthread_mutex_lock(&helper_mutex); + if(!scanning) { + pthread_mutex_unlock(&helper_mutex); + return; + } + rc = write(helper_in, cmd, strlen(cmd)); + pthread_mutex_unlock(&helper_mutex); + if(rc < 0) + return; + while(!found) { + pthread_mutex_lock(&helper_mutex); + if(!scanning) { + pthread_mutex_unlock(&helper_mutex); + break; + } + rc = read(helper_out, output, 128); + pthread_mutex_unlock(&helper_mutex); + if(rc <= 0) + break; + if(rc == 128) + rc = 127; + output[rc] = '\0'; + if(output[0] == 'F') { + if(sscanf(output, "F=%u\n", &n) == 1) { + current_frequency = n; + if(freq_callback) + freq_callback(n, freq_callback_data); + } + } + if(output[0] == 'S') { + if(sscanf(output, "S=%u\n", &n) == 1) { + if(callback) + callback(n, data); + found = true; + scanning = false; + } + } + } } static void rtlsdr_scan_stop(void) { - rtl_fm_scan_stop(); + char cmd[64]; + + sprintf(cmd, "S=STOP\n"); + pthread_mutex_lock(&helper_mutex); + scanning = false; + write(helper_in, cmd, strlen(cmd)); + pthread_mutex_unlock(&helper_mutex); } static radio_stereo_mode_t rtlsdr_get_stereo_mode(void) { + // We only support stereo return STEREO; } diff --git a/binding/radio_output.c b/binding/radio_output.c deleted file mode 100644 index a49687b..0000000 --- a/binding/radio_output.c +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright (C) 2017 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 -#include -#include -#include -#include - -#include "radio_output.h" -#include "rtl_fm.h" - -static pa_threaded_mainloop *mainloop; -static pa_context *context; -static pa_stream *stream; - -static unsigned int extra; -static int16_t extra_buf[1]; -static unsigned char *output_buf; - -static void pa_context_state_cb(pa_context *c, void *data) { - pa_operation *o; - - assert(c); - switch (pa_context_get_state(c)) { - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - case PA_CONTEXT_READY: - break; - case PA_CONTEXT_TERMINATED: - pa_threaded_mainloop_stop(mainloop); - break; - case PA_CONTEXT_FAILED: - default: - fprintf(stderr, "PA connection failed: %s\n", - pa_strerror(pa_context_errno(c))); - pa_threaded_mainloop_stop(mainloop); - break; - } - pa_threaded_mainloop_signal(mainloop, 0); -} - -int radio_output_open(void) -{ - pa_context *c; - pa_mainloop_api *mapi; - char *client; - - if(context) - return 0; - - if (!(mainloop = pa_threaded_mainloop_new())) { - fprintf(stderr, "pa_mainloop_new() failed.\n"); - return -1; - } - - pa_threaded_mainloop_set_name(mainloop, "pa_mainloop"); - mapi = pa_threaded_mainloop_get_api(mainloop); - - client = pa_xstrdup("radio"); - if (!(c = pa_context_new(mapi, client))) { - fprintf(stderr, "pa_context_new() failed.\n"); - goto exit; - } - - pa_context_set_state_callback(c, pa_context_state_cb, NULL); - if (pa_context_connect(c, NULL, 0, NULL) < 0) { - fprintf(stderr, "pa_context_connect(): %s", pa_strerror(pa_context_errno(c))); - goto exit; - } - - if (pa_threaded_mainloop_start(mainloop) < 0) { - fprintf(stderr, "pa_mainloop_run() failed.\n"); - goto exit; - } - - context = c; - - extra = 0; - output_buf = malloc(sizeof(unsigned char) * RTL_FM_MAXIMUM_BUF_LENGTH); - - return 0; - -exit: - if (c) - pa_context_unref(c); - - if (mainloop) - pa_threaded_mainloop_free(mainloop); - - pa_xfree(client); - return -1; -} - -int radio_output_start(void) -{ - int error = 0; - pa_sample_spec *spec; - - if(stream) - return 0; - - if(!context) { - error = radio_output_open(); - if(error != 0) - return error; - } - - while(pa_context_get_state(context) != PA_CONTEXT_READY) - pa_threaded_mainloop_wait(mainloop); - - spec = (pa_sample_spec*) calloc(1, sizeof(pa_sample_spec)); - spec->format = PA_SAMPLE_S16LE; - spec->rate = 24000; - spec->channels = 2; - if (!pa_sample_spec_valid(spec)) { - fprintf(stderr, "%s\n", - pa_strerror(pa_context_errno(context))); - return -1; - } - - pa_threaded_mainloop_lock(mainloop); - pa_proplist *props = pa_proplist_new(); - pa_proplist_sets(props, PA_PROP_MEDIA_ROLE, "radio"); - stream = pa_stream_new_with_proplist(context, "radio-output", spec, 0, props); - if(!stream) { - fprintf(stderr, "Error creating stream %s\n", - pa_strerror(pa_context_errno(context))); - pa_proplist_free(props); - free(spec); - pa_threaded_mainloop_unlock(mainloop); - return -1; - } - pa_proplist_free(props); - free(spec); - - if(pa_stream_connect_playback(stream, - NULL, - NULL, - (pa_stream_flags_t) 0, - NULL, - NULL) < 0) { - fprintf(stderr, "Error connecting to PulseAudio : %s\n", - pa_strerror(pa_context_errno(context))); - pa_stream_unref(stream); - stream = NULL; - pa_threaded_mainloop_unlock(mainloop); - return -1; - } - - pa_threaded_mainloop_unlock(mainloop); - - while(pa_stream_get_state(stream) != PA_STREAM_READY) - pa_threaded_mainloop_wait(mainloop); - - return error; -} - -void radio_output_stop(void) -{ - if(stream) { - pa_threaded_mainloop_lock(mainloop); - - pa_stream_set_state_callback(stream, 0, 0); - pa_stream_set_write_callback(stream, 0, 0); - pa_stream_set_underflow_callback(stream, 0, 0); - pa_stream_set_overflow_callback(stream, 0, 0); - pa_stream_set_latency_update_callback(stream, 0, 0); - - pa_operation *o = pa_stream_flush(stream, NULL, NULL); - if(o) - pa_operation_unref(o); - - pa_stream_disconnect(stream); - pa_stream_unref(stream); - stream = NULL; - - pa_threaded_mainloop_unlock(mainloop); - } -} - -void radio_output_suspend(int state) -{ - if(stream) { - pa_stream_cork(stream, state, NULL, NULL); - } -} - -void radio_output_close(void) -{ - radio_output_stop(); - - if(context) { - pa_context_disconnect(context); - pa_context_unref(context); - context = NULL; - } - - if(mainloop) { - pa_threaded_mainloop_stop(mainloop); - pa_threaded_mainloop_free(mainloop); - mainloop = NULL; - } - - free(output_buf); - output_buf = NULL; -} - -int radio_output_write(void *buf, int len) -{ - int rc = -EINVAL; - int error; - size_t n = len; - size_t avail; - int samples = len / 2; - void *p; - - if(!stream) { - return -1; - } - - if(!buf) { - fprintf(stderr, "Error: buf == null!\n"); - return rc; - } - - pa_threaded_mainloop_lock(mainloop); - - avail = pa_stream_writable_size(stream); - if(avail < n) { - /* - * NOTE: Definitely room for improvement here,but for now just - * check for the no space case that happens when the - * stream is corked. - */ - if(!avail) { - rc = 0; - goto exit; - } - } - - /* - * Handle the rtl_fm code giving us an odd number of samples, which - * PA does not like. 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; - } - - if ((rc = pa_stream_write(stream, p, n, NULL, 0, PA_SEEK_RELATIVE)) < 0) { - fprintf(stderr, "Error writing %d bytes to PulseAudio : %s\n", - n, pa_strerror(pa_context_errno(context))); - } -exit: - pa_threaded_mainloop_unlock(mainloop); - - return rc; -} diff --git a/binding/radio_output_pulse.c b/binding/radio_output_pulse.c new file mode 100644 index 0000000..a49687b --- /dev/null +++ b/binding/radio_output_pulse.c @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2017 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 +#include +#include +#include +#include + +#include "radio_output.h" +#include "rtl_fm.h" + +static pa_threaded_mainloop *mainloop; +static pa_context *context; +static pa_stream *stream; + +static unsigned int extra; +static int16_t extra_buf[1]; +static unsigned char *output_buf; + +static void pa_context_state_cb(pa_context *c, void *data) { + pa_operation *o; + + assert(c); + switch (pa_context_get_state(c)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + case PA_CONTEXT_READY: + break; + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_stop(mainloop); + break; + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "PA connection failed: %s\n", + pa_strerror(pa_context_errno(c))); + pa_threaded_mainloop_stop(mainloop); + break; + } + pa_threaded_mainloop_signal(mainloop, 0); +} + +int radio_output_open(void) +{ + pa_context *c; + pa_mainloop_api *mapi; + char *client; + + if(context) + return 0; + + if (!(mainloop = pa_threaded_mainloop_new())) { + fprintf(stderr, "pa_mainloop_new() failed.\n"); + return -1; + } + + pa_threaded_mainloop_set_name(mainloop, "pa_mainloop"); + mapi = pa_threaded_mainloop_get_api(mainloop); + + client = pa_xstrdup("radio"); + if (!(c = pa_context_new(mapi, client))) { + fprintf(stderr, "pa_context_new() failed.\n"); + goto exit; + } + + pa_context_set_state_callback(c, pa_context_state_cb, NULL); + if (pa_context_connect(c, NULL, 0, NULL) < 0) { + fprintf(stderr, "pa_context_connect(): %s", pa_strerror(pa_context_errno(c))); + goto exit; + } + + if (pa_threaded_mainloop_start(mainloop) < 0) { + fprintf(stderr, "pa_mainloop_run() failed.\n"); + goto exit; + } + + context = c; + + extra = 0; + output_buf = malloc(sizeof(unsigned char) * RTL_FM_MAXIMUM_BUF_LENGTH); + + return 0; + +exit: + if (c) + pa_context_unref(c); + + if (mainloop) + pa_threaded_mainloop_free(mainloop); + + pa_xfree(client); + return -1; +} + +int radio_output_start(void) +{ + int error = 0; + pa_sample_spec *spec; + + if(stream) + return 0; + + if(!context) { + error = radio_output_open(); + if(error != 0) + return error; + } + + while(pa_context_get_state(context) != PA_CONTEXT_READY) + pa_threaded_mainloop_wait(mainloop); + + spec = (pa_sample_spec*) calloc(1, sizeof(pa_sample_spec)); + spec->format = PA_SAMPLE_S16LE; + spec->rate = 24000; + spec->channels = 2; + if (!pa_sample_spec_valid(spec)) { + fprintf(stderr, "%s\n", + pa_strerror(pa_context_errno(context))); + return -1; + } + + pa_threaded_mainloop_lock(mainloop); + pa_proplist *props = pa_proplist_new(); + pa_proplist_sets(props, PA_PROP_MEDIA_ROLE, "radio"); + stream = pa_stream_new_with_proplist(context, "radio-output", spec, 0, props); + if(!stream) { + fprintf(stderr, "Error creating stream %s\n", + pa_strerror(pa_context_errno(context))); + pa_proplist_free(props); + free(spec); + pa_threaded_mainloop_unlock(mainloop); + return -1; + } + pa_proplist_free(props); + free(spec); + + if(pa_stream_connect_playback(stream, + NULL, + NULL, + (pa_stream_flags_t) 0, + NULL, + NULL) < 0) { + fprintf(stderr, "Error connecting to PulseAudio : %s\n", + pa_strerror(pa_context_errno(context))); + pa_stream_unref(stream); + stream = NULL; + pa_threaded_mainloop_unlock(mainloop); + return -1; + } + + pa_threaded_mainloop_unlock(mainloop); + + while(pa_stream_get_state(stream) != PA_STREAM_READY) + pa_threaded_mainloop_wait(mainloop); + + return error; +} + +void radio_output_stop(void) +{ + if(stream) { + pa_threaded_mainloop_lock(mainloop); + + pa_stream_set_state_callback(stream, 0, 0); + pa_stream_set_write_callback(stream, 0, 0); + pa_stream_set_underflow_callback(stream, 0, 0); + pa_stream_set_overflow_callback(stream, 0, 0); + pa_stream_set_latency_update_callback(stream, 0, 0); + + pa_operation *o = pa_stream_flush(stream, NULL, NULL); + if(o) + pa_operation_unref(o); + + pa_stream_disconnect(stream); + pa_stream_unref(stream); + stream = NULL; + + pa_threaded_mainloop_unlock(mainloop); + } +} + +void radio_output_suspend(int state) +{ + if(stream) { + pa_stream_cork(stream, state, NULL, NULL); + } +} + +void radio_output_close(void) +{ + radio_output_stop(); + + if(context) { + pa_context_disconnect(context); + pa_context_unref(context); + context = NULL; + } + + if(mainloop) { + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + mainloop = NULL; + } + + free(output_buf); + output_buf = NULL; +} + +int radio_output_write(void *buf, int len) +{ + int rc = -EINVAL; + int error; + size_t n = len; + size_t avail; + int samples = len / 2; + void *p; + + if(!stream) { + return -1; + } + + if(!buf) { + fprintf(stderr, "Error: buf == null!\n"); + return rc; + } + + pa_threaded_mainloop_lock(mainloop); + + avail = pa_stream_writable_size(stream); + if(avail < n) { + /* + * NOTE: Definitely room for improvement here,but for now just + * check for the no space case that happens when the + * stream is corked. + */ + if(!avail) { + rc = 0; + goto exit; + } + } + + /* + * Handle the rtl_fm code giving us an odd number of samples, which + * PA does not like. 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; + } + + if ((rc = pa_stream_write(stream, p, n, NULL, 0, PA_SEEK_RELATIVE)) < 0) { + fprintf(stderr, "Error writing %d bytes to PulseAudio : %s\n", + n, pa_strerror(pa_context_errno(context))); + } +exit: + pa_threaded_mainloop_unlock(mainloop); + + return rc; +} diff --git a/binding/rtl_fm_helper.c b/binding/rtl_fm_helper.c new file mode 100644 index 0000000..9405ae2 --- /dev/null +++ b/binding/rtl_fm_helper.c @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2018 Konsulko Group + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include +#include +#include +#include + +#include "rtl_fm.h" +#include "radio_output.h" + +#ifdef DEBUG +#define LOG_FILE "/tmp/helper.log" +FILE *_log; +#define LOG(...) \ + do { \ + if(!_log) _log = fopen(LOG_FILE, "w"); \ + fprintf(_log, __VA_ARGS__); \ + fflush(_log); \ + } while(0) +#else +#define LOG(...) do { } while(0) +#endif + +// Structure to describe FM band plans, all values in Hz. +typedef struct { + const char *name; + uint32_t min; + uint32_t max; + uint32_t step; +} fm_band_plan_t; + +static fm_band_plan_t known_fm_band_plans[5] = { + { .name = "US", .min = 87900000, .max = 107900000, .step = 200000 }, + { .name = "JP", .min = 76000000, .max = 95000000, .step = 100000 }, + { .name = "EU", .min = 87500000, .max = 108000000, .step = 50000 }, + { .name = "ITU-1", .min = 87500000, .max = 108000000, .step = 50000 }, + { .name = "ITU-2", .min = 87900000, .max = 107900000, .step = 50000 } +}; + +static unsigned int bandplan; +static char line[64]; + +static void rtl_output_callback(int16_t *result, int result_len, void *ctx) +{ + radio_output_write((char*) result, result_len * 2); +} + +static void rtl_freq_callback(uint32_t freq, void *ctx) +{ + // Note, need to flush output to ensure parent sees it + printf("F=%u\n", freq); + fflush(stdout); + LOG("F=%u\n", freq); +} + +static void rtl_scan_callback(uint32_t freq, void *ctx) +{ + // Note, need to flush output to ensure parent sees it + printf("S=%u\n", freq); + fflush(stdout); + LOG("S=%u\n", freq); +} + +static void read_config(void) +{ + GKeyFile* conf_file; + int conf_file_present = 0; + char *value_str; + + // Load settings from configuration file if it exists + conf_file = g_key_file_new(); + if(conf_file && + g_key_file_load_from_dirs(conf_file, + "AGL.conf", + (const gchar**) g_get_system_config_dirs(), + NULL, + G_KEY_FILE_KEEP_COMMENTS, + NULL) == TRUE) { + conf_file_present = 1; + + // Set band plan if it is specified + value_str = g_key_file_get_string(conf_file, + "radio", + "fmbandplan", + NULL); + if(value_str) { + unsigned int i; + for(i = 0; + i < sizeof(known_fm_band_plans) / sizeof(fm_band_plan_t); + i++) { + if(!strcasecmp(value_str, known_fm_band_plans[i].name)) { + bandplan = i; + break; + } + } + } + } + fprintf(stderr, "Using FM Bandplan: %s\n", known_fm_band_plans[bandplan].name); + + if(conf_file_present) { + GError *error = NULL; + int n; + + // Allow over-riding scanning parameters just in case a demo + // setup needs to do so to work reliably. + n = g_key_file_get_integer(conf_file, + "radio", + "scan_squelch_level", + &error); + if(!error) { + fprintf(stderr, "Scanning squelch level set to %d\n", n); + rtl_fm_scan_set_squelch_level(n); + } + + error = NULL; + n = g_key_file_get_integer(conf_file, + "radio", + "scan_squelch_limit", + &error); + if(!error) { + fprintf(stderr, "Scanning squelch limit set to %d\n", n); + rtl_fm_scan_set_squelch_limit(n); + } + + g_key_file_free(conf_file); + } +} + +int main(int argc, char *argv[]) +{ + int rc; + bool detect = false; + bool done = false; + bool started = false; + uint32_t frequency; + + LOG("started\n"); + + read_config(); + frequency = known_fm_band_plans[bandplan].min; + + if(argc == 2 && strcmp(argv[1], "--detect") == 0) { + detect = true; + } + + rc = rtl_fm_init(frequency, 200000, 48000, rtl_output_callback, NULL); + if(rc < 0) { + fprintf(stderr, "No RTL USB adapter?\n"); + exit(1); + } + if(detect) { + rtl_fm_cleanup(); + exit(0); + } + + rtl_fm_set_freq_callback(rtl_freq_callback, NULL); + + while(!done) { + LOG("Reading command\n"); + fgets(line, sizeof(line), stdin); + if(line[0] == '\0' || line[0] == '\n') + continue; + if(strcmp(line, "START\n") == 0) { + LOG("START received\n"); + if(!started) { + LOG("Starting\n"); + radio_output_start(); + rtl_fm_start(); + started = true; + } + } else if(strcmp(line, "STOP\n") == 0) { + LOG("STOP received\n"); + if(started) { + LOG("Stopping\n"); + radio_output_stop(); + rtl_fm_stop(); + started = false; + } + } else if(strncmp(line, "F=", 2) == 0) { + uint32_t n; + if(sscanf(line, "F=%u\n", &n) == 1) { + LOG("F=%d received\n", n); + rtl_fm_scan_stop(); + rtl_fm_set_freq(n); + } + } else if(strcmp(line, "S=UP\n") == 0) { + LOG("S=UP received\n"); + if(!started) + continue; + LOG("Calling rtl_fm_scan_start\n"); + rtl_fm_scan_start(0, + rtl_scan_callback, + NULL, + known_fm_band_plans[bandplan].step, + known_fm_band_plans[bandplan].min, + known_fm_band_plans[bandplan].max); + } else if(strcmp(line, "S=DOWN\n") == 0) { + LOG("S=DOWN received\n"); + if(!started) + continue; + LOG("Calling rtl_fm_scan_start\n"); + rtl_fm_scan_start(1, + rtl_scan_callback, + NULL, + known_fm_band_plans[bandplan].step, + known_fm_band_plans[bandplan].min, + known_fm_band_plans[bandplan].max); + } else if(strcmp(line, "S=STOP\n") == 0) { + LOG("S=STOP received\n"); + if(started) { + LOG("Calling rtl_fm_scan_stop\n"); + rtl_fm_scan_stop(); + } + } else if(line[0] == 'q' || line[0] == 'Q') + break; + } + if(started) { + radio_output_stop(); + rtl_fm_stop(); + } + rtl_fm_cleanup(); + LOG("done"); + return 0; +} -- cgit 1.2.3-korg