summaryrefslogtreecommitdiff
path: root/radio.c
diff options
context:
space:
mode:
Diffstat (limited to 'radio.c')
-rw-r--r--radio.c666
1 files changed, 666 insertions, 0 deletions
diff --git a/radio.c b/radio.c
new file mode 100644
index 0000000..8139894
--- /dev/null
+++ b/radio.c
@@ -0,0 +1,666 @@
+/*
+ * GPL 2
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <alsa/asoundlib.h>
+#include <linux/videodev2.h>
+#include <dbus/dbus-glib.h>
+#include <pulse/glib-mainloop.h>
+#include <pulse/error.h>
+#include <pulse/context.h>
+#include <pulse/stream.h>
+#include <pulse/xmalloc.h>
+
+#include "radio.h"
+#include "types.h"
+#include "n900-fmrx-enabler.h"
+#include "rds.h"
+
+G_DEFINE_TYPE(CFmRadio, cfm_radio, G_TYPE_OBJECT);
+
+#define CFM_RADIO_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CFM_TYPE_RADIO, CFmRadioPrivate))
+
+#define ABS_RANGE_LOW 60000000
+#define ABS_RANGE_HIGH 140000000
+#define MIN_BUFFER_SIZE (4096*4)
+
+#define FMRX_SERVICE_NAME "de.pycage.FMRXEnabler"
+#define FMRX_OBJECT_PATH "/de/pycage/FMRXEnabler"
+#define FMRX_INTERFACE "de.pycage.FMRXEnabler"
+#define FMRX_KEEPALIVE_INTERVAL 20
+
+#define SYSFS_NODE_PATH "/sys/class/i2c-adapter/i2c-3/3-0022"
+
+#define MIXER_NAME "hw:0"
+
+static void cfm_radio_turn_on(CFmRadio *self);
+static void cfm_radio_turn_off(CFmRadio *self);
+
+struct _CFmRadioPrivate {
+ int fd;
+
+ CFmRadioOutput output;
+
+ gboolean precise_tuner;
+ gulong range_low, range_high;
+
+ DBusGProxy *enabler;
+ guint enabler_timer;
+
+ pa_glib_mainloop *pa_loop;
+ pa_context *pa_ctx;
+ pa_stream *so, *si;
+
+ snd_hctl_t *mixer;
+};
+
+enum {
+ PROP_0,
+ PROP_OUTPUT,
+ PROP_FREQUENCY,
+ PROP_RANGE_LOW,
+ PROP_RANGE_HIGH,
+ PROP_SIGNAL,
+ PROP_RDS_PI,
+ PROP_RDS_PS,
+ PROP_RDS_RT,
+ PROP_LAST
+};
+
+static GParamSpec *properties[PROP_LAST];
+
+static void cfm_radio_tuner_power(CFmRadio *self, gboolean enable)
+{
+ CFmRadioPrivate *priv = self->priv;
+ struct v4l2_control vctrl;
+ int res;
+
+ if (priv->fd == -1) return;
+
+ vctrl.id = V4L2_CID_AUDIO_MUTE;
+ vctrl.value = enable ? 0 : 1;
+ res = ioctl(priv->fd, VIDIOC_S_CTRL, &vctrl);
+ if (res < 0) {
+ perror("VIDIOC_S_CTRL");
+ }
+}
+
+static void cfm_radio_mixer_set_enum_value(CFmRadio *self, const char * name, const char * value)
+{
+ CFmRadioPrivate *priv = self->priv;
+ int err;
+
+ snd_ctl_elem_id_t *id;
+ snd_ctl_elem_id_alloca(&id);
+ snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
+ snd_ctl_elem_id_set_name(id, name);
+
+ snd_hctl_elem_t *elem = snd_hctl_find_elem(priv->mixer, id);
+ g_return_if_fail(elem != NULL);
+
+ snd_ctl_elem_info_t *info;
+ snd_ctl_elem_info_alloca(&info);
+ snd_ctl_elem_info_set_id(info, id);
+ err = snd_hctl_elem_info(elem, info);
+ g_return_if_fail(err == 0);
+
+ long value_idx = -1;
+ int i, items = snd_ctl_elem_info_get_items(info);
+ for (i = 0; i < items; i++) {
+ snd_ctl_elem_info_set_item(info, i);
+ err = snd_hctl_elem_info(elem, info);
+ g_return_if_fail(err == 0);
+ if (strcmp(snd_ctl_elem_info_get_item_name(info), value) == 0) {
+ value_idx = i;
+ break;
+ }
+ }
+ g_return_if_fail(value_idx >= 0);
+
+ snd_ctl_elem_value_t *control;
+ snd_ctl_elem_value_alloca(&control);
+ snd_ctl_elem_value_set_id(control, id);
+
+ items = snd_ctl_elem_info_get_count(info);
+ for (i = 0; i < items; i++) {
+ snd_ctl_elem_value_set_enumerated(control, i, value_idx);
+ }
+
+ err = snd_hctl_elem_write(elem, control);
+ g_return_if_fail(err == 0);
+}
+
+static void cfm_radio_mixer_set_bool_value(CFmRadio *self, const char * name, gboolean value)
+{
+ CFmRadioPrivate *priv = self->priv;
+ int err;
+
+ snd_ctl_elem_id_t *id;
+ snd_ctl_elem_id_alloca(&id);
+ snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
+ snd_ctl_elem_id_set_name(id, name);
+
+ snd_hctl_elem_t *elem = snd_hctl_find_elem(priv->mixer, id);
+ g_return_if_fail(elem != NULL);
+
+ snd_ctl_elem_info_t *info;
+ snd_ctl_elem_info_alloca(&info);
+ snd_ctl_elem_info_set_id(info, id);
+ err = snd_hctl_elem_info(elem, info);
+ g_return_if_fail(err == 0);
+
+ snd_ctl_elem_value_t *control;
+ snd_ctl_elem_value_alloca(&control);
+ snd_ctl_elem_value_set_id(control, id);
+
+ int i, items = snd_ctl_elem_info_get_count(info);
+ for (i = 0; i < items; i++) {
+ snd_ctl_elem_value_set_boolean(control, i, value);
+ }
+
+ err = snd_hctl_elem_write(elem, control);
+ g_return_if_fail(err == 0);
+}
+
+static void cfm_radio_mixer_enable(CFmRadio *self, gboolean enable)
+{
+ if (enable) {
+ cfm_radio_mixer_set_enum_value(self, "Input Select", "ADC");
+ cfm_radio_mixer_set_bool_value(self, "PGA Capture Switch", TRUE);
+ cfm_radio_mixer_set_bool_value(self, "Left PGA Mixer Line2L Switch", TRUE);
+ cfm_radio_mixer_set_bool_value(self, "Right PGA Mixer Line2R Switch", TRUE);
+ } else {
+ cfm_radio_mixer_set_enum_value(self, "Input Select", "Digital Mic");
+ cfm_radio_mixer_set_bool_value(self, "PGA Capture Switch", FALSE);
+ cfm_radio_mixer_set_bool_value(self, "Left PGA Mixer Line2L Switch", FALSE);
+ cfm_radio_mixer_set_bool_value(self, "Right PGA Mixer Line2R Switch", FALSE);
+ }
+}
+
+static void cfm_radio_init_tuner(CFmRadio *self, const gchar * device)
+{
+ CFmRadioPrivate *priv = self->priv;
+ struct v4l2_tuner tuner = { 0 };
+ int res;
+
+ priv->fd = open(device, O_RDONLY);
+ if (priv->fd == -1) {
+ perror("Open radio tuner");
+ }
+
+ tuner.index = 0;
+ res = ioctl(priv->fd, VIDIOC_G_TUNER, &tuner);
+ if (res < 0) {
+ perror("VIDIOC_G_TUNER");
+ return;
+ }
+
+ if (tuner.type != V4L2_TUNER_RADIO) {
+ g_warning("Not a radio tuner\n");
+ return;
+ }
+
+ priv->precise_tuner = (tuner.capability & V4L2_TUNER_CAP_LOW) ?
+ TRUE : FALSE;
+
+ if (priv->precise_tuner) {
+ priv->range_low = tuner.rangelow * 62.5f;
+ priv->range_high = tuner.rangehigh * 62.5f;
+ } else {
+ priv->range_low = tuner.rangelow * 62500;
+ priv->range_high = tuner.rangehigh * 62500;
+ }
+
+ g_debug("Tuner detected (from %lu to %lu)\n", priv->range_low, priv->range_high);
+
+ cfm_radio_tuner_power(self, TRUE);
+
+ g_debug("Tuner powered!\n");
+
+ g_object_notify(G_OBJECT(self), "range-low");
+ g_object_notify(G_OBJECT(self), "range-high");
+ g_object_notify(G_OBJECT(self), "frequency");
+}
+
+static void cfm_radio_fmrx_request_cb(DBusGProxy *proxy, gint result, char * device, GError *error, gpointer userdata)
+{
+ CFmRadio *self = CFM_RADIO(userdata);
+ CFmRadioPrivate *priv = self->priv;
+
+ if (error) {
+ g_warning("D-Bus error while contacting fmrx enabler: %s\n",
+ error->message);
+ g_error_free(error);
+ } else if (result != 0) {
+ g_warning("Ungranted acess to fmrx device (%d)\n", result);
+ } else if (priv->fd != -1) {
+ g_debug("Renewed access to device\n");
+ } else {
+ g_debug("Granted access to device: %s\n", device);
+ cfm_radio_init_tuner(self, device);
+ /* TODO: Possibly initialize more stuff. */
+ }
+
+ if (device) {
+ g_free(device);
+ }
+}
+
+static void cfm_radio_fmrx_request(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ g_return_if_fail(priv->enabler);
+
+ de_pycage_FMRXEnabler_request_async(priv->enabler,
+ cfm_radio_fmrx_request_cb, self);
+}
+
+static gboolean cfm_radio_fmrx_keepalive(gpointer data)
+{
+ CFmRadio *self = CFM_RADIO(data);
+ CFmRadioPrivate *priv = self->priv;
+
+ if (!priv->enabler) {
+ priv->enabler_timer = 0;
+ return FALSE;
+ }
+
+ cfm_radio_fmrx_request(self);
+
+ return TRUE;
+}
+
+static void cfm_radio_ctx_state_change(pa_context *c, void *userdata)
+{
+ CFmRadio *self = CFM_RADIO(userdata);
+ CFmRadioPrivate *priv = self->priv;
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_READY:
+ if (priv->output != CFM_RADIO_OUTPUT_MUTE && (!priv->so || !priv->si)) {
+ cfm_radio_turn_on(self);
+ }
+ break;
+ case PA_CONTEXT_FAILED:
+ cfm_radio_turn_off(self);
+ g_warning("Pulseaudio connection failed\n");
+ break;
+ default:
+ break;
+ }
+}
+
+static void cfm_radio_si_request(pa_stream *p, size_t nbytes, void *userdata)
+{
+ CFmRadio *self = CFM_RADIO(userdata);
+ CFmRadioPrivate *priv = self->priv;
+
+ if (nbytes < MIN_BUFFER_SIZE)
+ return;
+
+ void * in;
+ size_t in_nbytes;
+ int res = pa_stream_peek(priv->si, (const void **) &in, &in_nbytes);
+ if (res != 0) {
+ g_warning("Failed to read from input stream: %s\n", pa_strerror(res));
+ return;
+ }
+ g_warn_if_fail(in);
+
+ res = pa_stream_write(priv->so, in, in_nbytes, NULL, 0, PA_SEEK_RELATIVE);
+ if (res != 0) {
+ g_warning("Failed to write to output stream: %s\n", pa_strerror(res));
+ return;
+ }
+
+ pa_stream_drop(priv->si);
+}
+
+static void cfm_radio_turn_on(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ /* A good default for the N900. */
+ pa_sample_spec spec = {
+ .format = PA_SAMPLE_S16LE,
+ .rate = 48000,
+ .channels = 2
+ };
+ int res;
+ g_warn_if_fail(priv->pa_ctx);
+
+ priv->si = pa_stream_new(priv->pa_ctx, "FMRadio input", &spec, NULL);
+ priv->so = pa_stream_new(priv->pa_ctx, "FMRadio output", &spec, NULL);
+
+ pa_stream_set_read_callback(priv->si, cfm_radio_si_request, self);
+
+ res = pa_stream_connect_playback(priv->so, NULL, NULL, 0, NULL, NULL);
+ if (res != 0) {
+ g_warning("Failed to connect output stream: %s\n", pa_strerror(res));
+ }
+ res = pa_stream_connect_record(priv->si, NULL, NULL, 0);
+ if (res != 0) {
+ g_warning("Failed to connect input stream: %s\n", pa_strerror(res));
+ }
+
+ cfm_radio_mixer_enable(self, TRUE);
+
+ g_debug("Turned on\n");
+}
+
+static void cfm_radio_turn_off(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ if (priv->so) {
+ pa_stream_disconnect(priv->so);
+ pa_stream_unref(priv->so);
+ priv->so = NULL;
+ }
+ if (priv->si) {
+ pa_stream_disconnect(priv->si);
+ pa_stream_unref(priv->si);
+ priv->si = NULL;
+ }
+ cfm_radio_mixer_enable(self, FALSE);
+ g_debug("Turned off\n");
+}
+
+static void cfm_radio_init(CFmRadio *self)
+{
+ CFmRadioPrivate *priv;
+ GError *error = NULL;
+ int res;
+
+ self->priv = priv = CFM_RADIO_GET_PRIVATE(self);
+ priv->fd = -1;
+
+ DBusGConnection *conn = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
+ if (!conn) {
+ g_warning("Failed to get system bus: %s\n", error->message);
+ g_error_free(error);
+ }
+
+ priv->enabler = dbus_g_proxy_new_for_name_owner(conn, FMRX_SERVICE_NAME,
+ FMRX_OBJECT_PATH, FMRX_INTERFACE, &error);
+ if (!priv->enabler) {
+ g_warning("Failed to connect to fmrx enabler service: %s\n",
+ error->message);
+ g_error_free(error);
+ }
+
+ cfm_radio_fmrx_request(self);
+
+ priv->pa_loop = pa_glib_mainloop_new(NULL);
+ priv->pa_ctx = pa_context_new(pa_glib_mainloop_get_api(priv->pa_loop),
+ "FMRadio"); /* Note that the name is very important on Maemo. */
+ pa_context_set_state_callback(priv->pa_ctx, cfm_radio_ctx_state_change, self);
+ res = pa_context_connect(priv->pa_ctx, NULL, 0, NULL);
+ g_warn_if_fail(res == 0);
+
+ priv->enabler_timer = g_timeout_add_seconds(FMRX_KEEPALIVE_INTERVAL,
+ cfm_radio_fmrx_keepalive, self);
+
+ res = snd_hctl_open(&priv->mixer, MIXER_NAME, 0);
+ if (res < 0) {
+ g_warning("Failed to open ALSA mixer res=%d\n", res);
+ }
+ res = snd_hctl_load(priv->mixer);
+ if (res < 0) {
+ g_warning("Failed to load ALSA hmixer elements res=%d\n", res);
+ }
+}
+
+static void cfm_radio_set_output(CFmRadio *self, CFmRadioOutput mode)
+{
+ CFmRadioPrivate *priv = self->priv;
+ //CFmRadioOutput old_output = priv->output;
+ priv->output = mode;
+ if (mode == CFM_RADIO_OUTPUT_MUTE) {
+ cfm_radio_turn_off(self);
+ } else if (pa_context_get_state(priv->pa_ctx) == PA_CONTEXT_READY) {
+ cfm_radio_turn_on(self);
+ }
+}
+
+static CFmRadioOutput cfm_radio_get_output(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ return priv->output;
+}
+
+static void cfm_radio_set_frequency(CFmRadio *self, gulong freq)
+{
+ CFmRadioPrivate *priv = self->priv;
+ struct v4l2_frequency t_freq = { 0 };
+ g_return_if_fail(priv->fd != -1);
+ t_freq.tuner = 0;
+ t_freq.type = V4L2_TUNER_RADIO;
+ t_freq.frequency = priv->precise_tuner ? freq / 62.5f : freq / 62500;
+ int res = ioctl(priv->fd, VIDIOC_S_FREQUENCY, &t_freq);
+ g_warn_if_fail(res == 0);
+}
+
+static gulong cfm_radio_get_frequency(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ struct v4l2_frequency t_freq = { 0 };
+ g_return_val_if_fail(priv->fd != -1, 0);
+ t_freq.tuner = 0;
+ int res = ioctl(priv->fd, VIDIOC_G_FREQUENCY, &t_freq);
+ g_return_val_if_fail(res == 0, 0);
+ return priv->precise_tuner ? t_freq.frequency * 62.5f : t_freq.frequency * 62500;
+}
+
+static guint cfm_radio_get_signal(CFmRadio *self)
+{
+ CFmRadioPrivate *priv = self->priv;
+ struct v4l2_tuner tuner = { 0 };
+ g_return_val_if_fail(priv->fd != -1, 0);
+ tuner.index = 0;
+ int res = ioctl(priv->fd, VIDIOC_G_TUNER, &tuner);
+ g_return_val_if_fail(res == 0, 0);
+ return tuner.signal;
+}
+
+static gchar* cfm_radio_get_sysfs_key(CFmRadio *self, const gchar *key)
+{
+ GError *error = NULL;
+ gchar *file = g_strdup_printf("%s/%s", SYSFS_NODE_PATH, key);
+ gchar *r = NULL;
+
+ if (!g_file_get_contents(file, &r, NULL, &error)) {
+ g_warning("Unable to read sysfs key %s: %s\n", key, error->message);
+ }
+
+ g_free(file);
+
+ return r;
+}
+
+static gchar* cfm_radio_get_rds(CFmRadio *self, const gchar *key)
+{
+ gchar *v = cfm_radio_get_sysfs_key(self, key);
+ if (v) {
+ gchar * r = rds_decode(v);
+ g_free(v);
+ return r;
+ } else {
+ return NULL;
+ }
+}
+
+static void cfm_radio_set_property(GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ CFmRadio *self = CFM_RADIO(object);
+ switch (property_id) {
+ case PROP_OUTPUT:
+ cfm_radio_set_output(self, g_value_get_enum(value));
+ break;
+ case PROP_FREQUENCY:
+ cfm_radio_set_frequency(self, g_value_get_ulong(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_radio_get_property(GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ CFmRadio *self = CFM_RADIO(object);
+ switch (property_id) {
+ case PROP_OUTPUT:
+ g_value_set_enum(value, cfm_radio_get_output(self));
+ break;
+ case PROP_FREQUENCY:
+ g_value_set_ulong(value, cfm_radio_get_frequency(self));
+ break;
+ case PROP_RANGE_LOW:
+ g_value_set_ulong(value, self->priv->range_low);
+ break;
+ case PROP_RANGE_HIGH:
+ g_value_set_ulong(value, self->priv->range_high);
+ break;
+ case PROP_SIGNAL:
+ g_value_set_uint(value, cfm_radio_get_signal(self));
+ break;
+ case PROP_RDS_PI:
+ g_value_take_string(value, cfm_radio_get_rds(self, "rds_pi"));
+ break;
+ case PROP_RDS_PS:
+ g_value_take_string(value, cfm_radio_get_rds(self, "rds_ps"));
+ break;
+ case PROP_RDS_RT:
+ g_value_take_string(value, cfm_radio_get_rds(self, "rds_rt"));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_radio_dispose(GObject *object)
+{
+ CFmRadio *self = CFM_RADIO(object);
+ CFmRadioPrivate *priv = self->priv;
+ if (priv->enabler_timer) {
+ g_source_remove(priv->enabler_timer);
+ priv->enabler_timer = 0;
+ }
+ cfm_radio_tuner_power(self, FALSE);
+ cfm_radio_turn_off(self);
+ if (priv->enabler) {
+ g_object_unref(priv->enabler);
+ priv->enabler = NULL;
+ }
+ if (priv->pa_ctx) {
+ pa_context_disconnect(priv->pa_ctx);
+ pa_context_unref(priv->pa_ctx);
+ priv->pa_ctx = NULL;
+ }
+ if (priv->pa_loop) {
+ pa_glib_mainloop_free(priv->pa_loop);
+ priv->pa_loop = NULL;
+ }
+}
+
+static void cfm_radio_finalize(GObject *object)
+{
+ CFmRadio *self = CFM_RADIO(object);
+ CFmRadioPrivate *priv = self->priv;
+ if (priv->mixer) {
+ snd_hctl_close(priv->mixer);
+ priv->mixer = NULL;
+ }
+ if (priv->fd != -1) {
+ close(priv->fd);
+ priv->fd = -1;
+ }
+}
+
+static void cfm_radio_class_init(CFmRadioClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GParamSpec *param_spec;
+
+ gobject_class->set_property = cfm_radio_set_property;
+ gobject_class->get_property = cfm_radio_get_property;
+ gobject_class->dispose = cfm_radio_dispose;
+ gobject_class->finalize = cfm_radio_finalize;
+
+ g_type_class_add_private (klass, sizeof(CFmRadioPrivate));
+
+ param_spec = g_param_spec_enum("output", "Output device",
+ "Audio output device",
+ CFM_TYPE_RADIO_OUTPUT,
+ CFM_RADIO_OUTPUT_MUTE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_OUTPUT] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_OUTPUT, param_spec);
+
+ param_spec = g_param_spec_ulong("frequency",
+ "Frequency to tune (Hz)",
+ "This is the frequency to tune, in Hz",
+ ABS_RANGE_LOW, ABS_RANGE_HIGH, ABS_RANGE_LOW,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_FREQUENCY] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_FREQUENCY, param_spec);
+
+ param_spec = g_param_spec_ulong("range-low",
+ "Min frequency range (Hz)",
+ "This is the lowest frequency that can be tuned, in Hz",
+ ABS_RANGE_LOW, ABS_RANGE_HIGH, ABS_RANGE_LOW,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_property(gobject_class, PROP_RANGE_LOW, param_spec);
+ properties[PROP_RANGE_LOW] = param_spec;
+ param_spec = g_param_spec_ulong("range-high",
+ "Max frequency range (Hz)",
+ "This is the highest frequency that can be tuned, in Hz",
+ ABS_RANGE_LOW, ABS_RANGE_HIGH, ABS_RANGE_HIGH,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_RANGE_HIGH] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_RANGE_HIGH, param_spec);
+ param_spec = g_param_spec_uint("signal",
+ "Signal strength",
+ "The signal strength if known, ranging from 0 (worse) to 65536 (best)",
+ 0, 65536, 0,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_SIGNAL] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_SIGNAL, param_spec);
+ param_spec = g_param_spec_string("rds-pi",
+ "RDS Program Identification",
+ "Current station's code",
+ "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_RDS_PI] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_RDS_PI, param_spec);
+ param_spec = g_param_spec_string("rds-ps",
+ "RDS Program Server",
+ "Current station's name",
+ "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_RDS_PS] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_RDS_PS, param_spec);
+ param_spec = g_param_spec_string("rds-rt",
+ "RDS Radio Text",
+ "Current station's radio text",
+ "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_RDS_RT] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_RDS_RT, param_spec);
+}
+
+CFmRadio* cfm_radio_new()
+{
+ return g_object_new(CFM_TYPE_RADIO, NULL);
+}
+