summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile43
-rw-r--r--cfmradio.c263
-rw-r--r--data/cfmradio.desktop9
-rw-r--r--data/cfmradio.service3
-rw-r--r--data/codetables.utf814
-rw-r--r--data/icon.pngbin0 -> 2916 bytes
-rw-r--r--data/make-table.c32
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control79
-rw-r--r--debian/copyright26
-rw-r--r--debian/dirs4
-rw-r--r--debian/docs0
-rwxr-xr-xdebian/rules70
-rw-r--r--n900-fmrx-enabler.h65
-rw-r--r--n900-fmrx-enabler.xml38
-rw-r--r--preset_list.c149
-rw-r--r--preset_list.h37
-rw-r--r--presets.c344
-rw-r--r--presets.h45
-rw-r--r--radio.c666
-rw-r--r--radio.h37
-rw-r--r--rds.c281
-rw-r--r--rds.h8
-rw-r--r--tuner.c289
-rw-r--r--tuner.h36
-rw-r--r--types.c22
-rw-r--r--types.h22
28 files changed, 2588 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a47812f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+CFLAGS?=-Os -g -Wall
+LDFLAGS?=-Wl,--as-needed
+
+MISC_CFLAGS:=-std=gnu99 -DG_LOG_DOMAIN=\"CFmRadio\"
+PKGCONFIG_PKGS:=libosso hildon-1 alsa libpulse-mainloop-glib dbus-glib-1 gconf-2.0
+PKGCONFIG_CFLAGS:=$(shell pkg-config $(PKGCONFIG_PKGS) --cflags)
+PKGCONFIG_LIBS:=$(shell pkg-config $(PKGCONFIG_PKGS) --libs)
+LAUNCHER_CFLAGS:=$(shell pkg-config maemo-launcher-app --cflags)
+LAUNCHER_LDFLAGS:=$(shell pkg-config maemo-launcher-app --libs)
+
+OBJS=cfmradio.o radio.o types.o tuner.o rds.o presets.o preset_list.o
+
+all: cfmradio.launch
+
+cfmradio.launch: $(OBJS)
+ $(CC) $(LAUNCHER_LDFLAGS) $(LDFLAGS) -o $@ $^ $(PKGCONFIG_LIBS) $(LIBS)
+
+cfmradio: $(OBJS)
+ $(CC) $(LDFLAGS) -o $@ $^ $(PKGCONFIG_LIBS) $(LIBS)
+
+$(OBJS): %.o: %.c
+ $(CC) $(MISC_CFLAGS) $(PKGCONFIG_CFLAGS) $(LAUNCHER_CFLAGS) $(CFLAGS) -o $@ -c $<
+
+radio.c: radio.h types.h n900-fmrx-enabler.h
+
+n900-fmrx-enabler.h: n900-fmrx-enabler.xml
+ dbus-binding-tool --mode=glib-client --output=$@ --prefix=fmrx_enabler $<
+
+install: cfmradio.launch
+ install -m 0755 $(IFLAGS) cfmradio.launch $(DESTDIR)/usr/bin/
+ ln -sf /usr/bin/maemo-invoker $(DESTDIR)/usr/bin/cfmradio
+ install -m 0644 $(IFLAGS) data/icon.png \
+ $(DESTDIR)/usr/share/icons/hicolor/scalable/hildon/cfmradio.png
+ install -m 0644 $(IFLAGS) data/cfmradio.service \
+ $(DESTDIR)/usr/share/dbus-1/services/
+ install -m 0644 $(IFLAGS) data/cfmradio.desktop \
+ $(DESTDIR)/usr/share/applications/hildon/
+
+clean:
+ rm -f cfmradio cfmradio.launch *.o
+
+.PHONY: all clean
+
diff --git a/cfmradio.c b/cfmradio.c
new file mode 100644
index 0000000..a269b53
--- /dev/null
+++ b/cfmradio.c
@@ -0,0 +1,263 @@
+#include <libosso.h>
+#include <hildon/hildon.h>
+
+#include "radio.h"
+#include "presets.h"
+#include "preset_list.h"
+#include "tuner.h"
+#include "types.h"
+
+static osso_context_t *osso_context;
+static HildonProgram *program;
+static HildonWindow *main_window;
+
+static CFmRadio *radio;
+static CFmPresets *presets;
+
+static GtkLabel *freq_label;
+static GtkLabel *ps_label, *rt_label;
+static CFmTuner *tuner;
+//static GtkToolbar *toolbar;
+
+static CFmPresetList *preset_list;
+
+static guint rds_timer;
+
+static void print_freq(gulong freq)
+{
+ static gchar markup[256];
+ float freq_mhz;
+
+ freq_mhz = freq / 1000000.0f;
+
+ g_snprintf(markup, sizeof(markup), "<span font=\"64\">%.1f</span> MHz", freq_mhz);
+ gtk_label_set_markup(freq_label, markup);
+}
+
+static void print_rds()
+{
+ gchar *rds_ps, *rds_rt;
+ gchar *markup;
+
+ g_object_get(G_OBJECT(radio), "rds-ps", &rds_ps, "rds-rt", &rds_rt, NULL);
+
+ markup = g_markup_printf_escaped("<span font=\"31\">%s</span>",
+ g_strstrip(rds_ps));
+ gtk_label_set_markup(ps_label, markup);
+ g_free(markup);
+
+
+ gtk_label_set_text(rt_label, g_strstrip(rds_rt));
+
+ g_free(rds_ps);
+ g_free(rds_rt);
+}
+
+static void range_low_changed_cb(GObject *object, GParamSpec *psec, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(radio), "range-low", &freq, NULL);
+ g_object_set(G_OBJECT(tuner), "range-low", freq, NULL);
+}
+
+static void range_high_changed_cb(GObject *object, GParamSpec *psec, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(radio), "range-high", &freq, NULL);
+ g_object_set(G_OBJECT(tuner), "range-high", freq, NULL);
+}
+
+static void frequency_changed_cb(GObject *object, GParamSpec *psec, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(radio), "frequency", &freq, NULL);
+ g_object_set(G_OBJECT(tuner), "frequency", freq, NULL);
+ print_freq(freq);
+}
+
+static gboolean key_press_cb(GObject *object, GdkEventKey *event, gpointer user_data)
+{
+ gulong freq;
+ switch (event->keyval) {
+ case GDK_Left:
+ g_object_get(G_OBJECT(radio), "frequency", &freq, NULL);
+ freq -= 100000;
+ g_object_set(G_OBJECT(radio), "frequency", freq, NULL);
+ return FALSE;
+ break;
+ case GDK_Right:
+ g_object_get(G_OBJECT(radio), "frequency", &freq, NULL);
+ freq += 100000;
+ g_object_set(G_OBJECT(radio), "frequency", freq, NULL);
+ return FALSE;
+ break;
+ }
+ return TRUE;
+}
+
+static gboolean tuner_changed_cb(GObject *object, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(tuner), "frequency", &freq, NULL);
+ print_freq(freq);
+ return TRUE;
+}
+
+static gboolean tuner_tuned_cb(GObject *object, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(tuner), "frequency", &freq, NULL);
+ g_object_set(G_OBJECT(radio), "frequency", freq, NULL);
+ print_freq(freq);
+ return TRUE;
+}
+
+static gboolean rds_timer_cb(gpointer data)
+{
+ print_rds();
+ return TRUE;
+}
+
+static void presets_clicked(GtkButton *button, gpointer user_data)
+{
+ cfm_preset_list_show_for(preset_list, presets);
+}
+
+static void preset_frequency_cb(GObject *object, GParamSpec *psec, gpointer user_data)
+{
+ gulong freq;
+ g_object_get(G_OBJECT(preset_list), "frequency", &freq, NULL);
+ g_object_set(G_OBJECT(radio), "frequency", freq, NULL);
+ gtk_widget_hide(GTK_WIDGET(preset_list));
+ print_freq(freq);
+}
+
+static void add_preset_clicked(GtkButton *button, gpointer user_data)
+{
+ gchar *rds_ps = NULL;
+ gulong freq;
+
+ g_object_get(G_OBJECT(radio), "rds-ps", &rds_ps, "frequency", &freq, NULL);
+ cfm_presets_set_preset(presets, freq, g_strstrip(rds_ps));
+
+ g_free(rds_ps);
+}
+
+static void build_main_window()
+{
+ GtkBox *box;
+
+ main_window = HILDON_WINDOW(hildon_stackable_window_new());
+ gtk_window_set_title(GTK_WINDOW(main_window), "Radio");
+ hildon_gtk_window_set_portrait_flags(GTK_WINDOW(main_window),
+ HILDON_PORTRAIT_MODE_SUPPORT);
+
+ HildonAppMenu *menu = HILDON_APP_MENU(hildon_app_menu_new());
+
+ GtkWidget *menu_button;
+ menu_button = gtk_button_new_with_label("Presets...");
+ g_signal_connect_after(G_OBJECT(menu_button), "clicked",
+ G_CALLBACK(presets_clicked), NULL);
+ hildon_app_menu_append(menu, GTK_BUTTON(menu_button));
+ menu_button = gtk_button_new_with_label("Add preset");
+ g_signal_connect_after(G_OBJECT(menu_button), "clicked",
+ G_CALLBACK(add_preset_clicked), NULL);
+ hildon_app_menu_append(menu, GTK_BUTTON(menu_button));
+ gtk_widget_show_all(GTK_WIDGET(menu));
+
+ box = GTK_BOX(gtk_vbox_new(FALSE, 0));
+
+ freq_label = GTK_LABEL(gtk_label_new(NULL));
+ ps_label = GTK_LABEL(gtk_label_new(NULL));
+ rt_label = GTK_LABEL(gtk_label_new(NULL));
+ tuner = cfm_tuner_new();
+
+#if 0
+ toolbar = GTK_TOOLBAR(gtk_toolbar_new());
+
+ gtk_toolbar_set_icon_size(toolbar, HILDON_ICON_SIZE_THUMB);
+
+ GtkToolItem* item = gtk_tool_button_new(
+ gtk_image_new_from_icon_name("general_back", HILDON_ICON_SIZE_THUMB),
+ NULL);
+ gtk_toolbar_insert(toolbar, item, -1);
+ item = gtk_tool_item_new();
+ gtk_tool_item_set_expand(item, TRUE);
+ gtk_toolbar_insert(toolbar, item, -1);
+ item = gtk_tool_button_new(gtk_image_new_from_icon_name(
+ "general_add", HILDON_ICON_SIZE_THUMB), NULL);
+ gtk_toolbar_insert(toolbar, item, -1);
+ item = gtk_tool_button_new(gtk_image_new_from_icon_name(
+ "general_mybookmarks_folder", HILDON_ICON_SIZE_THUMB), NULL);
+ gtk_toolbar_insert(toolbar, item, -1);
+ item = gtk_tool_item_new();
+ gtk_tool_item_set_expand(item, TRUE);
+ gtk_toolbar_insert(toolbar, item, -1);
+ item = gtk_tool_button_new(gtk_image_new_from_icon_name(
+ "general_forward", HILDON_ICON_SIZE_THUMB), NULL);
+ gtk_toolbar_insert(toolbar, item, -1);
+#endif
+
+ gtk_box_pack_start(box, GTK_WIDGET(freq_label), TRUE, TRUE, 0);
+ gtk_box_pack_start(box, GTK_WIDGET(ps_label), FALSE, FALSE, 0);
+ gtk_box_pack_start(box, GTK_WIDGET(rt_label), FALSE, FALSE, 0);
+ gtk_box_pack_start(box, GTK_WIDGET(tuner), FALSE, FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(box));
+
+ hildon_window_set_app_menu(main_window, menu);
+
+ //hildon_window_add_toolbar(main_window, toolbar);
+ hildon_program_add_window(program, main_window);
+
+ g_signal_connect(G_OBJECT(main_window), "delete-event",
+ G_CALLBACK(gtk_main_quit), NULL);
+ g_signal_connect(G_OBJECT(main_window), "key-press-event",
+ G_CALLBACK(key_press_cb), NULL);
+ g_signal_connect(G_OBJECT(tuner), "frequency-changed",
+ G_CALLBACK(tuner_changed_cb), NULL);
+ g_signal_connect(G_OBJECT(tuner), "frequency-tuned",
+ G_CALLBACK(tuner_tuned_cb), NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ hildon_gtk_init(&argc, &argv);
+
+ osso_context = osso_initialize("com.javispedro.cfmradio", "0.1", TRUE, NULL);
+ g_warn_if_fail(osso_context != NULL);
+
+ g_set_application_name("FM Radio"); /* This might be important for Pulse */
+ program = hildon_program_get_instance();
+
+ radio = cfm_radio_new();
+ g_signal_connect(G_OBJECT(radio), "notify::range-low",
+ G_CALLBACK(range_low_changed_cb), NULL);
+ g_signal_connect(G_OBJECT(radio), "notify::range-high",
+ G_CALLBACK(range_high_changed_cb), NULL);
+ g_signal_connect(G_OBJECT(radio), "notify::frequency",
+ G_CALLBACK(frequency_changed_cb), NULL);
+
+ presets = cfm_presets_get_default();
+
+ rds_timer = g_timeout_add_seconds(1, rds_timer_cb, NULL);
+
+ build_main_window();
+
+ gtk_widget_show_all(GTK_WIDGET(main_window));
+
+ preset_list = cfm_preset_list_new();
+ g_signal_connect(G_OBJECT(preset_list), "delete-event",
+ G_CALLBACK(gtk_widget_hide_on_delete), NULL);
+ g_signal_connect(G_OBJECT(preset_list), "notify::frequency",
+ G_CALLBACK(preset_frequency_cb), NULL);
+
+ g_object_set(G_OBJECT(radio), "output", CFM_RADIO_OUTPUT_SYSTEM, NULL);
+
+ gtk_main();
+
+ g_object_unref(G_OBJECT(radio));
+ osso_deinitialize(osso_context);
+
+ return 0;
+}
+
diff --git a/data/cfmradio.desktop b/data/cfmradio.desktop
new file mode 100644
index 0000000..3f342e9
--- /dev/null
+++ b/data/cfmradio.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=Radio
+Exec=/usr/bin/cfmradio
+Icon=cfmradio
+X-Osso-Service=com.javispedro.cfmradio
+X-Osso-Type=application/x-executable
diff --git a/data/cfmradio.service b/data/cfmradio.service
new file mode 100644
index 0000000..6c94f2b
--- /dev/null
+++ b/data/cfmradio.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=com.javispedro.cfmradio
+Exec=/usr/bin/cfmradio
diff --git a/data/codetables.utf8 b/data/codetables.utf8
new file mode 100644
index 0000000..0be19e3
--- /dev/null
+++ b/data/codetables.utf8
@@ -0,0 +1,14 @@
+ !"#¤%&'()*+,-./
+0123456789:;<=>?
+@ABCDEFGHIJKLMNO
+PQRSTUVWXYZ[\]
+ abcdefghijklmno
+pqrstuvwxyz{|}
+áàéèíìóòúùÑÇŞß¡IJ
+âäêëîïôöûüñçşğ?ij
+ªα©‰Ǧĕňőπ?£$←↑→↓
+º¹²³±İńűμ¿÷°¼½¾§
+ÁÀÉÈÍÌÓÒÚÙŘČŠŽĐĽ
+ÂÄÊËÎÏÔÖÛÜřčšžďľ
+ÃÅÆŒŷÝÕØþŊŔĆŚŹ?ð
+ãåæœŵýõøþŋŕćśź?
diff --git a/data/icon.png b/data/icon.png
new file mode 100644
index 0000000..7f9cbc1
--- /dev/null
+++ b/data/icon.png
Binary files differ
diff --git a/data/make-table.c b/data/make-table.c
new file mode 100644
index 0000000..9afca7d
--- /dev/null
+++ b/data/make-table.c
@@ -0,0 +1,32 @@
+#include <glib.h>
+
+int main(int argc, char **argv) {
+ GError *error = NULL;
+ gchar *data;
+ if (!g_file_get_contents("codetables.utf8", &data, NULL, &error)) {
+ g_printerr("Failed to open %s: %s\n", error->message);
+ return 1;
+ }
+
+ gint i;
+ guint count = 0;
+
+ for (i = 0; i < 0x20; i++) {
+ g_print("\t%u,\t/* %u */\n", 0, count);
+ count++;
+ }
+
+ gchar * c = data;
+ while (*c) {
+ if (*c != '\n') {
+ gunichar u = g_utf8_get_char(c);
+ g_print("\t%u,\t/* %u */\n", u, count);
+ count++;
+ }
+ c = g_utf8_next_char(c);
+ }
+
+ g_printerr("Total elements = %d\n", count);
+
+ return 0;
+}
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..d3af7d2
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+cfmradio (0.1) unstable; urgency=low
+
+ * Initial Release.
+
+ -- Javier S. Pedro <maemo@javispedro.com> Sat, 30 Oct 2010 22:28:23 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7ed6ff8
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+5
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..21bb97e
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,79 @@
+Source: cfmradio
+Section: user/multimedia
+Priority: extra
+Maintainer: Javier S. Pedro <maemo@javispedro.com>
+Build-Depends: debhelper (>= 5), libosso-dev, libhildon1-dev, libasound2-dev,
+ libpulse-dev, maemo-launcher-dev
+Standards-Version: 3.7.2
+
+Package: cfmradio
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}, n900-fmrx-enabler
+Description: FM radio player
+ Listen to FM radio from your N900
+XB-Description-es_ES: Reproductor de radio FM
+ Escucha radio FM desde tu N900
+XSBC-Maemo-Display-Name: Radio
+XB-Maemo-Icon-26:
+ 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
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..69776ee
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,26 @@
+This is cfmradio, written and maintained by Javier S. Pedro <maemo@javispedro.com>
+on Sat, 30 Oct 2010 22:28:23 +0200.
+
+The original source can always be found at:
+ ftp://ftp.debian.org/dists/unstable/main/source/
+
+Copyright Holder: Javier S. Pedro
+
+License:
+
+ 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 package; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+On Debian systems, the complete text of the GNU General
+Public License can be found in `/usr/share/common-licenses/GPL'.
diff --git a/debian/dirs b/debian/dirs
new file mode 100644
index 0000000..53552b7
--- /dev/null
+++ b/debian/dirs
@@ -0,0 +1,4 @@
+usr/bin
+usr/share/icons/hicolor/scalable/hildon
+usr/share/dbus-1/services
+usr/share/applications/hildon
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/debian/docs
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..ef96488
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,70 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+CFLAGS = -Wall -g
+
+ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
+ CFLAGS += -O0
+else
+ CFLAGS += -Os
+endif
+
+build: build-stamp
+
+build-stamp:
+ dh_testdir
+
+ $(MAKE)
+
+ touch $@
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp configure-stamp
+
+ -$(MAKE) clean
+
+ dh_clean
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ $(MAKE) DESTDIR=$(CURDIR)/debian/cfmradio install
+
+
+# Build architecture-independent files here.
+binary-indep: build install
+# We have nothing to do by default.
+
+# Build architecture-dependent files here.
+binary-arch: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+ dh_installdocs
+ dh_installexamples
+ dh_installman
+ dh_link
+ dh_strip
+ dh_compress
+ dh_fixperms
+ dh_installdeb
+ dh_shlibdeps
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install
diff --git a/n900-fmrx-enabler.h b/n900-fmrx-enabler.h
new file mode 100644
index 0000000..cd8480b
--- /dev/null
+++ b/n900-fmrx-enabler.h
@@ -0,0 +1,65 @@
+/* Generated by dbus-binding-tool; do not edit! */
+
+#include <glib.h>
+#include <dbus/dbus-glib.h>
+
+G_BEGIN_DECLS
+
+#ifndef _DBUS_GLIB_ASYNC_DATA_FREE
+#define _DBUS_GLIB_ASYNC_DATA_FREE
+static
+#ifdef G_HAVE_INLINE
+inline
+#endif
+void
+_dbus_glib_async_data_free (gpointer stuff)
+{
+ g_slice_free (DBusGAsyncData, stuff);
+}
+#endif
+
+#ifndef DBUS_GLIB_CLIENT_WRAPPERS_de_pycage_FMRXEnabler
+#define DBUS_GLIB_CLIENT_WRAPPERS_de_pycage_FMRXEnabler
+
+static
+#ifdef G_HAVE_INLINE
+inline
+#endif
+gboolean
+de_pycage_FMRXEnabler_request (DBusGProxy *proxy, gint* OUT_result, char ** OUT_device, GError **error)
+
+{
+ return dbus_g_proxy_call (proxy, "request", error, G_TYPE_INVALID, G_TYPE_INT, OUT_result, G_TYPE_STRING, OUT_device, G_TYPE_INVALID);
+}
+
+typedef void (*de_pycage_FMRXEnabler_request_reply) (DBusGProxy *proxy, gint OUT_result, char * OUT_device, GError *error, gpointer userdata);
+
+static void
+de_pycage_FMRXEnabler_request_async_callback (DBusGProxy *proxy, DBusGProxyCall *call, void *user_data)
+{
+ DBusGAsyncData *data = (DBusGAsyncData*) user_data;
+ GError *error = NULL;
+ gint OUT_result;
+ char * OUT_device;
+ dbus_g_proxy_end_call (proxy, call, &error, G_TYPE_INT, &OUT_result, G_TYPE_STRING, &OUT_device, G_TYPE_INVALID);
+ (*(de_pycage_FMRXEnabler_request_reply)data->cb) (proxy, OUT_result, OUT_device, error, data->userdata);
+ return;
+}
+
+static
+#ifdef G_HAVE_INLINE
+inline
+#endif
+DBusGProxyCall*
+de_pycage_FMRXEnabler_request_async (DBusGProxy *proxy, de_pycage_FMRXEnabler_request_reply callback, gpointer userdata)
+
+{
+ DBusGAsyncData *stuff;
+ stuff = g_slice_new (DBusGAsyncData);
+ stuff->cb = G_CALLBACK (callback);
+ stuff->userdata = userdata;
+ return dbus_g_proxy_begin_call (proxy, "request", de_pycage_FMRXEnabler_request_async_callback, stuff, _dbus_glib_async_data_free, G_TYPE_INVALID);
+}
+#endif /* defined DBUS_GLIB_CLIENT_WRAPPERS_de_pycage_FMRXEnabler */
+
+G_END_DECLS
diff --git a/n900-fmrx-enabler.xml b/n900-fmrx-enabler.xml
new file mode 100644
index 0000000..3fb6593
--- /dev/null
+++ b/n900-fmrx-enabler.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+D-Bus service bindings
+Copyright (C) 2009 Martin Grimme <martin.grimme@gmail.com>
+
+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
+-->
+
+<node name="/de/pycage/FMRXEnabler">
+
+ <interface name="de.pycage.FMRXEnabler">
+
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="FMRXEnabler"/>
+
+ <method name="request">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol"
+ value="dbus_fmrx_enabler_request"/>
+ <arg type="i" name="result" direction="out"/>
+ <arg type="s" name="device" direction="out"/>
+ </method>
+
+ </interface>
+
+</node>
+
diff --git a/preset_list.c b/preset_list.c
new file mode 100644
index 0000000..0fef49b
--- /dev/null
+++ b/preset_list.c
@@ -0,0 +1,149 @@
+/*
+ * GPL 2
+ */
+
+#include <gtk/gtk.h>
+#include <hildon/hildon.h>
+
+#include "presets.h"
+#include "preset_list.h"
+
+G_DEFINE_TYPE(CFmPresetList, cfm_preset_list, HILDON_TYPE_STACKABLE_WINDOW);
+
+#define CFM_PRESET_LIST_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CFM_TYPE_PRESET_LIST, CFmPresetListPrivate))
+
+#define ABS_RANGE_LOW 60000000
+#define ABS_RANGE_HIGH 140000000
+
+struct _CFmPresetListPrivate {
+ HildonTouchSelector *sel;
+ HildonTouchSelectorColumn *col;
+};
+
+enum {
+ PROP_0,
+ PROP_MODEL,
+ PROP_FREQUENCY,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_0,
+ SIGNAL_LAST
+};
+
+static GParamSpec *properties[PROP_LAST];
+static guint signals[SIGNAL_LAST];
+
+static void cfm_preset_list_selection_changed(HildonTouchSelector *selector,
+ gint column, gpointer user_data)
+{
+ CFmPresetList *self = CFM_PRESET_LIST(user_data);
+
+ g_object_notify(G_OBJECT(self), "frequency");
+}
+
+static void cfm_preset_list_set_property(GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ CFmPresetList *self = CFM_PRESET_LIST(object);
+ CFmPresetListPrivate *priv = self->priv;
+ switch (property_id) {
+ case PROP_MODEL:
+ hildon_touch_selector_set_model(priv->sel, 0, g_value_get_object(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_preset_list_get_property(GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ CFmPresetList *self = CFM_PRESET_LIST(object);
+ CFmPresetListPrivate *priv = self->priv;
+ switch (property_id) {
+ case PROP_MODEL:
+ g_value_set_object(value, hildon_touch_selector_get_model(priv->sel, 0));
+ break;
+ case PROP_FREQUENCY: {
+ GtkTreeIter iter;
+ if (hildon_touch_selector_get_selected(priv->sel, 0, &iter)) {
+ gulong freq;
+ gtk_tree_model_get(hildon_touch_selector_get_model(priv->sel, 0),
+ &iter, 0, &freq, -1);
+ g_value_set_ulong(value, freq);
+ }
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_preset_list_init(CFmPresetList *self)
+{
+ CFmPresetListPrivate *priv;
+
+ self->priv = priv = CFM_PRESET_LIST_GET_PRIVATE(self);
+
+ /* Empty model for now */
+ GtkTreeModel *model = GTK_TREE_MODEL(gtk_list_store_new(2, G_TYPE_FLOAT, G_TYPE_STRING));
+
+ gtk_window_set_title(GTK_WINDOW(self), "Presets");
+ hildon_gtk_window_set_portrait_flags(GTK_WINDOW(self),
+ HILDON_PORTRAIT_MODE_SUPPORT);
+
+ priv->sel = HILDON_TOUCH_SELECTOR(hildon_touch_selector_new());
+ priv->col = hildon_touch_selector_append_text_column(priv->sel, model, FALSE);
+ hildon_touch_selector_column_set_text_column(priv->col, 1);
+
+ gtk_container_add(GTK_CONTAINER(self), GTK_WIDGET(priv->sel));
+
+ g_signal_connect(G_OBJECT(priv->sel), "changed",
+ G_CALLBACK(cfm_preset_list_selection_changed), self);
+
+ g_object_unref(model);
+}
+
+static void cfm_preset_list_class_init(CFmPresetListClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ GParamSpec *param_spec;
+
+ gobject_class->set_property = cfm_preset_list_set_property;
+ gobject_class->get_property = cfm_preset_list_get_property;
+
+ g_type_class_add_private(klass, sizeof(CFmPresetListPrivate));
+
+ param_spec = g_param_spec_object("model",
+ "GtkTreeModel to use",
+ "This is the model where contents will be read from",
+ GTK_TYPE_TREE_MODEL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_MODEL] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_MODEL, 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);
+}
+
+CFmPresetList* cfm_preset_list_new()
+{
+ return g_object_new(CFM_TYPE_PRESET_LIST, NULL);
+}
+
+void cfm_preset_list_show_for(CFmPresetList *self, CFmPresets *presets)
+{
+ g_object_set(self, "model", cfm_presets_get_all(presets), NULL);
+
+ gtk_widget_show_all(GTK_WIDGET(self));
+}
+
diff --git a/preset_list.h b/preset_list.h
new file mode 100644
index 0000000..9930242
--- /dev/null
+++ b/preset_list.h
@@ -0,0 +1,37 @@
+/*
+ * GPL 2
+ */
+
+#ifndef _CFM_PRESET_LIST_H_
+#define _CFM_PRESET_LIST_H_
+
+#include <hildon/hildon.h>
+#include "presets.h"
+
+#define CFM_TYPE_PRESET_LIST (cfm_preset_list_get_type ())
+#define CFM_PRESET_LIST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CFM_TYPE_PRESET_LIST, CFmPresetList))
+#define CFM_IS_PRESET_LIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CFM_TYPE_PRESET_LIST))
+#define CFM_PRESET_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CFM_TYPE_PRESET_LIST, CFmPresetListClass))
+#define CFM_IS_PRESET_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CFM_TYPE_PRESET_LIST))
+#define CFM_PRESET_LIST_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CFM_TYPE_PRESET_LIST, CFmPresetListClass))
+
+typedef struct _CFmPresetList CFmPresetList;
+typedef struct _CFmPresetListPrivate CFmPresetListPrivate;
+typedef struct _CFmPresetListClass CFmPresetListClass;
+
+struct _CFmPresetList
+{
+ HildonStackableWindow parent;
+ CFmPresetListPrivate *priv;
+};
+
+struct _CFmPresetListClass
+{
+ HildonStackableWindowClass parent;
+};
+
+GType cfm_preset_list_get_type (void);
+CFmPresetList* cfm_preset_list_new();
+void cfm_preset_list_show_for(CFmPresetList *self, CFmPresets *presets);
+
+#endif /* _CFM_PRESET_LIST_H_ */
diff --git a/presets.c b/presets.c
new file mode 100644
index 0000000..f35901a
--- /dev/null
+++ b/presets.c
@@ -0,0 +1,344 @@
+/*
+ * GPL 2
+ */
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gconf/gconf-client.h>
+
+#include "presets.h"
+
+G_DEFINE_TYPE(CFmPresets, cfm_presets, G_TYPE_OBJECT);
+
+#define CFM_PRESETS_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CFM_TYPE_PRESETS, CFmPresetsPrivate))
+
+#define GCONF_KEY_BUFFER_LEN 1024
+#define GCONF_PATH "/apps/maemo/cfmradio/presets"
+
+struct _CFmPresetsPrivate {
+ GConfClient *gconf;
+ gchar *name;
+ gchar *gconf_dir;
+ guint gconf_notify;
+ GHashTable *t;
+};
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_0,
+ SIGNAL_LAST
+};
+
+static GParamSpec *properties[PROP_LAST];
+static guint signals[SIGNAL_LAST];
+
+static inline gfloat freq_to_float(gulong f)
+{
+ return f / 1000000.0;
+}
+
+static inline gulong float_to_freq(gfloat f)
+{
+ return f * 1000000.0;
+}
+
+static inline gfloat pointer_to_float(gpointer p)
+{
+ union {
+ gfloat f;
+ gpointer p;
+ } u;
+ u.p = p;
+ return u.f;
+}
+
+static inline gpointer float_to_pointer(gfloat f)
+{
+ union {
+ gfloat f;
+ gpointer p;
+ } u;
+ u.f = f;
+ return u.p;
+}
+
+static inline gpointer freq_to_pointer(gulong f)
+{
+ return float_to_pointer(freq_to_float(f));
+}
+
+static inline gulong pointer_to_freq(gpointer p)
+{
+ return float_to_freq(pointer_to_float(p));
+}
+
+static void func_gconf_entry_free(gpointer data, gpointer user_data)
+{
+ GConfEntry *entry = (GConfEntry*) data;
+ gconf_entry_free(entry);
+}
+
+static void cfm_presets_load(CFmPresets *self)
+{
+ CFmPresetsPrivate *priv = self->priv;
+
+ g_debug("Loading presets from %s\n", priv->gconf_dir);
+
+ g_hash_table_remove_all(priv->t);
+
+ GSList *i, *l = gconf_client_all_entries(priv->gconf, priv->gconf_dir, NULL);
+ for (i = l; i; i = g_slist_next(i)) {
+ GConfEntry *entry = (GConfEntry*) i->data;
+ const gchar *basename = g_basename(gconf_entry_get_key(entry));
+ gfloat freq = g_ascii_strtod(basename, NULL);
+ const gchar *name = gconf_value_get_string(gconf_entry_get_value(entry));
+ g_hash_table_insert(priv->t, float_to_pointer(freq), g_strdup(name));
+ }
+
+ g_slist_foreach(l, func_gconf_entry_free, NULL);
+ g_slist_free(l);
+}
+
+static void cfm_presets_gconf_notify(GConfClient *gconf, guint cnxn_id,
+ GConfEntry *entry, gpointer user_data)
+{
+ CFmPresets *self = CFM_PRESETS(user_data);
+ CFmPresetsPrivate *priv = self->priv;
+
+ if (!entry) {
+ return;
+ }
+
+ const gchar *basename = g_basename(gconf_entry_get_key(entry));
+ gfloat freq = g_ascii_strtod(basename, NULL);
+ GConfValue *value = gconf_entry_get_value(entry);
+
+ if (value) {
+ const gchar *name = gconf_value_get_string(gconf_entry_get_value(entry));
+ g_debug("Preset '%s' changed to '%s'\n", basename, name);
+ g_hash_table_insert(priv->t, float_to_pointer(freq), g_strdup(name));
+ } else {
+ g_debug("Preset '%s' removed\n", basename);
+ g_hash_table_remove(priv->t, float_to_pointer(freq));
+ }
+}
+
+static void cfm_presets_set_property(GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ CFmPresets *self = CFM_PRESETS(object);
+ switch (property_id) {
+ case PROP_NAME:
+ g_free(self->priv->name);
+ self->priv->name = g_value_dup_string(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_presets_get_property(GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ CFmPresets *self = CFM_PRESETS(object);
+ switch (property_id) {
+ case PROP_NAME:
+ g_debug("Prop name is now set\n");
+ g_value_set_string(value, self->priv->name);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_presets_init(CFmPresets *self)
+{
+ CFmPresetsPrivate *priv;
+
+ self->priv = priv = CFM_PRESETS_GET_PRIVATE(self);
+
+ priv->gconf = gconf_client_get_default();
+ priv->t = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
+}
+
+static GObject * cfm_presets_constructor(GType gtype, guint n_properties,
+ GObjectConstructParam *properties)
+{
+ GObject *object = G_OBJECT_CLASS(cfm_presets_parent_class)->constructor(
+ gtype, n_properties, properties);
+ CFmPresets *self = CFM_PRESETS(object);
+ CFmPresetsPrivate *priv = self->priv;
+
+ priv->gconf_dir = g_strdup_printf("%s/%s", GCONF_PATH, priv->name);
+
+ gconf_client_add_dir(priv->gconf, priv->gconf_dir, GCONF_CLIENT_PRELOAD_ONELEVEL,
+ NULL);
+ priv->gconf_notify = gconf_client_notify_add(priv->gconf, priv->gconf_dir,
+ cfm_presets_gconf_notify, self, NULL, NULL);
+
+ cfm_presets_load(self);
+
+ return object;
+}
+
+static void cfm_presets_dispose(GObject *object)
+{
+ CFmPresets *self = CFM_PRESETS(object);
+ CFmPresetsPrivate *priv = self->priv;
+
+ if (priv->gconf && priv->gconf_notify) {
+ gconf_client_notify_remove(priv->gconf, priv->gconf_notify);
+ }
+ if (priv->gconf && priv->gconf_dir) {
+ gconf_client_remove_dir(priv->gconf, priv->gconf_dir, NULL);
+ }
+ if (priv->gconf_dir) {
+ g_free(priv->gconf_dir);
+ priv->gconf_dir = NULL;
+ }
+ if (priv->gconf) {
+ g_object_unref(priv->gconf);
+ priv->gconf = NULL;
+ }
+}
+
+static void cfm_presets_finalize(GObject *object)
+{
+ CFmPresets *self = CFM_PRESETS(object);
+ CFmPresetsPrivate *priv = self->priv;
+
+ g_hash_table_destroy(priv->t);
+ g_free(priv->name);
+}
+
+static void cfm_presets_class_init(CFmPresetsClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ GParamSpec *param_spec;
+
+ g_type_class_add_private (klass, sizeof(CFmPresetsPrivate));
+
+ gobject_class->constructor = cfm_presets_constructor;
+ gobject_class->set_property = cfm_presets_set_property;
+ gobject_class->get_property = cfm_presets_get_property;
+ gobject_class->dispose = cfm_presets_dispose;
+ gobject_class->finalize = cfm_presets_finalize;
+
+ param_spec = g_param_spec_string("name",
+ "Preset set name",
+ "This is the name for this set of presets",
+ "default",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+ properties[PROP_NAME] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_NAME, param_spec);
+}
+
+static gpointer cfm_presets_build_default(gpointer data)
+{
+ return g_object_new(CFM_TYPE_PRESETS, NULL);
+}
+
+CFmPresets* cfm_presets_get_default()
+{
+ static GOnce once = G_ONCE_INIT;
+ g_once(&once, cfm_presets_build_default, NULL);
+
+ return CFM_PRESETS(g_object_ref(G_OBJECT(once.retval)));
+}
+
+CFmPresets* cfm_presets_get_for_name(const char * name)
+{
+ return g_object_new(CFM_TYPE_PRESETS, "name", name, NULL);
+}
+
+void cfm_presets_set_preset(CFmPresets *self, gulong freq, const gchar *name)
+{
+ CFmPresetsPrivate *priv = self->priv;
+ GError *error = NULL;
+ gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
+ gchar *key = g_strdup_printf("%s/%s", priv->gconf_dir,
+ g_ascii_formatd(buf, sizeof(buf), "%.1f", freq_to_float(freq)));
+ if (!gconf_client_set_string(priv->gconf, key, name, &error)) {
+ g_warning("Failed to store preset '%s' ('%s'): %s\n", key, name,
+ error->message);
+ }
+ g_free(key);
+}
+
+void cfm_presets_remove_preset(CFmPresets *self, gulong freq)
+{
+ CFmPresetsPrivate *priv = self->priv;
+ GError *error = NULL;
+ gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
+ gchar *key = g_strdup_printf("%s/%s", priv->gconf_dir,
+ g_ascii_formatd(buf, sizeof(buf), "%.1f", freq_to_float(freq)));
+ if (!gconf_client_unset(priv->gconf, key, &error)) {
+ g_warning("Failed to remove preset '%s': %s\n", key, error->message);
+ }
+ g_free(key);
+}
+
+gboolean cfm_presets_is_preset(CFmPresets *self, gulong freq)
+{
+ CFmPresetsPrivate *priv = self->priv;
+
+ return g_hash_table_lookup(priv->t, freq_to_pointer(freq)) ? TRUE : FALSE;
+}
+
+const gchar * cfm_presets_get_preset(CFmPresets *self, gulong freq)
+{
+ CFmPresetsPrivate *priv = self->priv;
+
+ gpointer found = g_hash_table_lookup(priv->t, freq_to_pointer(freq));
+ if (found) {
+ return (const gchar *) found;
+ } else {
+ return NULL;
+ }
+
+ return NULL;
+}
+
+static void preset_to_list_store(gpointer key, gpointer value, gpointer user_data)
+{
+ GtkListStore *l = GTK_LIST_STORE(user_data);
+ GtkTreeIter iter;
+
+ gulong freq = pointer_to_freq(key);
+ gtk_list_store_insert_with_values(l, &iter, 0, 0, freq, 1, value, -1);
+}
+
+static gint compare_freq(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b,
+ gpointer user_data)
+{
+ gulong freq_a, freq_b;
+ gtk_tree_model_get(model, a, 0, &freq_a, -1);
+ gtk_tree_model_get(model, b, 0, &freq_b, -1);
+
+ if (freq_a > freq_b) return 1;
+ else if (freq_a < freq_b) return -1;
+ else return 0;
+}
+
+GtkListStore* cfm_presets_get_all(CFmPresets *self)
+{
+ CFmPresetsPrivate *priv = self->priv;
+ GtkListStore *l = gtk_list_store_new(2, G_TYPE_ULONG, G_TYPE_STRING);
+ g_hash_table_foreach(priv->t, preset_to_list_store, l);
+
+ gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(l), 0, compare_freq,
+ NULL, NULL);
+ gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(l), 0,
+ GTK_SORT_ASCENDING);
+
+ return l;
+}
+
diff --git a/presets.h b/presets.h
new file mode 100644
index 0000000..88c910d
--- /dev/null
+++ b/presets.h
@@ -0,0 +1,45 @@
+/*
+ * GPL 2
+ */
+
+#ifndef __CFM_PRESETS_H__
+#define __CFM_PRESETS_H__
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#define CFM_TYPE_PRESETS (cfm_presets_get_type ())
+#define CFM_PRESETS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CFM_TYPE_PRESETS, CFmPresets))
+#define CFM_IS_PRESETS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CFM_TYPE_PRESETS))
+#define CFM_PRESETS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CFM_TYPE_PRESETS, CFmPresetsClass))
+#define CFM_IS_PRESETS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CFM_TYPE_PRESETS))
+#define CFM_PRESETS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CFM_TYPE_PRESETS, CFmPresetsClass))
+
+typedef struct _CFmPresets CFmPresets;
+typedef struct _CFmPresetsPrivate CFmPresetsPrivate;
+typedef struct _CFmPresetsClass CFmPresetsClass;
+
+struct _CFmPresets
+{
+ GObject parent_instance;
+ CFmPresetsPrivate *priv;
+};
+
+struct _CFmPresetsClass
+{
+ GObjectClass parent_class;
+};
+
+GType cfm_presets_get_type(void);
+CFmPresets* cfm_presets_get_default();
+CFmPresets* cfm_presets_get_for_name(const gchar *name);
+
+void cfm_presets_set_preset(CFmPresets *self, gulong freq, const gchar *name);
+void cfm_presets_remove_preset(CFmPresets *self, gulong freq);
+gboolean cfm_presets_is_preset(CFmPresets *self, gulong freq);
+const gchar * cfm_presets_get_preset(CFmPresets *self, gulong freq);
+
+GtkListStore* cfm_presets_get_all(CFmPresets *self);
+
+#endif /* __CFM_PRESETS_H__ */
+
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);
+}
+
diff --git a/radio.h b/radio.h
new file mode 100644
index 0000000..b5d0b82
--- /dev/null
+++ b/radio.h
@@ -0,0 +1,37 @@
+/*
+ * GPL 2
+ */
+
+#ifndef __CFM_RADIO_H__
+#define __CFM_RADIO_H__
+
+#include <glib-object.h>
+
+#define CFM_TYPE_RADIO (cfm_radio_get_type ())
+#define CFM_RADIO(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CFM_TYPE_RADIO, CFmRadio))
+#define CFM_IS_RADIO(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CFM_TYPE_RADIO))
+#define CFM_RADIO_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CFM_TYPE_RADIO, CFmRadioClass))
+#define CFM_IS_RADIO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CFM_TYPE_RADIO))
+#define CFM_RADIO_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CFM_TYPE_RADIO, CFmRadioClass))
+
+typedef struct _CFmRadio CFmRadio;
+typedef struct _CFmRadioPrivate CFmRadioPrivate;
+typedef struct _CFmRadioClass CFmRadioClass;
+
+struct _CFmRadio
+{
+ GObject parent_instance;
+ CFmRadioPrivate *priv;
+};
+
+struct _CFmRadioClass
+{
+ GObjectClass parent_class;
+};
+
+GType cfm_radio_get_type (void);
+CFmRadio* cfm_radio_new();
+void cfm_radio_free(CFmRadio* radio);
+
+#endif /* __CFM_RADIO_H__ */
+
diff --git a/rds.c b/rds.c
new file mode 100644
index 0000000..5654050
--- /dev/null
+++ b/rds.c
@@ -0,0 +1,281 @@
+#include <string.h>
+#include <glib.h>
+
+#include "rds.h"
+
+/* This table comes from pyFMRadio source code. */
+static const gunichar rds_table[256] = {
+ 0, /* 0 */
+ 0, /* 1 */
+ 0, /* 2 */
+ 0, /* 3 */
+ 0, /* 4 */
+ 0, /* 5 */
+ 0, /* 6 */
+ 0, /* 7 */
+ 0, /* 8 */
+ 0, /* 9 */
+ 0, /* 10 */
+ 0, /* 11 */
+ 0, /* 12 */
+ 0, /* 13 */
+ 0, /* 14 */
+ 0, /* 15 */
+ 0, /* 16 */
+ 0, /* 17 */
+ 0, /* 18 */
+ 0, /* 19 */
+ 0, /* 20 */
+ 0, /* 21 */
+ 0, /* 22 */
+ 0, /* 23 */
+ 0, /* 24 */
+ 0, /* 25 */
+ 0, /* 26 */
+ 0, /* 27 */
+ 0, /* 28 */
+ 0, /* 29 */
+ 0, /* 30 */
+ 0, /* 31 */
+ 32, /* 32 */
+ 33, /* 33 */
+ 34, /* 34 */
+ 35, /* 35 */
+ 164, /* 36 */
+ 37, /* 37 */
+ 38, /* 38 */
+ 39, /* 39 */
+ 40, /* 40 */
+ 41, /* 41 */
+ 42, /* 42 */
+ 43, /* 43 */
+ 44, /* 44 */
+ 45, /* 45 */
+ 46, /* 46 */
+ 47, /* 47 */
+ 48, /* 48 */
+ 49, /* 49 */
+ 50, /* 50 */
+ 51, /* 51 */
+ 52, /* 52 */
+ 53, /* 53 */
+ 54, /* 54 */
+ 55, /* 55 */
+ 56, /* 56 */
+ 57, /* 57 */
+ 58, /* 58 */
+ 59, /* 59 */
+ 60, /* 60 */
+ 61, /* 61 */
+ 62, /* 62 */
+ 63, /* 63 */
+ 64, /* 64 */
+ 65, /* 65 */
+ 66, /* 66 */
+ 67, /* 67 */
+ 68, /* 68 */
+ 69, /* 69 */
+ 70, /* 70 */
+ 71, /* 71 */
+ 72, /* 72 */
+ 73, /* 73 */
+ 74, /* 74 */
+ 75, /* 75 */
+ 76, /* 76 */
+ 77, /* 77 */
+ 78, /* 78 */
+ 79, /* 79 */
+ 80, /* 80 */
+ 81, /* 81 */
+ 82, /* 82 */
+ 83, /* 83 */
+ 84, /* 84 */
+ 85, /* 85 */
+ 86, /* 86 */
+ 87, /* 87 */
+ 88, /* 88 */
+ 89, /* 89 */
+ 90, /* 90 */
+ 91, /* 91 */
+ 92, /* 92 */
+ 93, /* 93 */
+ 32, /* 94 */
+ 32, /* 95 */
+ 32, /* 96 */
+ 97, /* 97 */
+ 98, /* 98 */
+ 99, /* 99 */
+ 100, /* 100 */
+ 101, /* 101 */
+ 102, /* 102 */
+ 103, /* 103 */
+ 104, /* 104 */
+ 105, /* 105 */
+ 106, /* 106 */
+ 107, /* 107 */
+ 108, /* 108 */
+ 109, /* 109 */
+ 110, /* 110 */
+ 111, /* 111 */
+ 112, /* 112 */
+ 113, /* 113 */
+ 114, /* 114 */
+ 115, /* 115 */
+ 116, /* 116 */
+ 117, /* 117 */
+ 118, /* 118 */
+ 119, /* 119 */
+ 120, /* 120 */
+ 121, /* 121 */
+ 122, /* 122 */
+ 123, /* 123 */
+ 124, /* 124 */
+ 125, /* 125 */
+ 32, /* 126 */
+ 32, /* 127 */
+ 225, /* 128 */
+ 224, /* 129 */
+ 233, /* 130 */
+ 232, /* 131 */
+ 237, /* 132 */
+ 236, /* 133 */
+ 243, /* 134 */
+ 242, /* 135 */
+ 250, /* 136 */
+ 249, /* 137 */
+ 209, /* 138 */
+ 199, /* 139 */
+ 350, /* 140 */
+ 223, /* 141 */
+ 161, /* 142 */
+ 306, /* 143 */
+ 226, /* 144 */
+ 228, /* 145 */
+ 234, /* 146 */
+ 235, /* 147 */
+ 238, /* 148 */
+ 239, /* 149 */
+ 244, /* 150 */
+ 246, /* 151 */
+ 251, /* 152 */
+ 252, /* 153 */
+ 241, /* 154 */
+ 231, /* 155 */
+ 351, /* 156 */
+ 287, /* 157 */
+ 63, /* 158 */
+ 307, /* 159 */
+ 170, /* 160 */
+ 945, /* 161 */
+ 169, /* 162 */
+ 8240, /* 163 */
+ 486, /* 164 */
+ 277, /* 165 */
+ 328, /* 166 */
+ 337, /* 167 */
+ 960, /* 168 */
+ 63, /* 169 */
+ 163, /* 170 */
+ 36, /* 171 */
+ 8592, /* 172 */
+ 8593, /* 173 */
+ 8594, /* 174 */
+ 8595, /* 175 */
+ 186, /* 176 */
+ 185, /* 177 */
+ 178, /* 178 */
+ 179, /* 179 */
+ 177, /* 180 */
+ 304, /* 181 */
+ 324, /* 182 */
+ 369, /* 183 */
+ 956, /* 184 */
+ 191, /* 185 */
+ 247, /* 186 */
+ 176, /* 187 */
+ 188, /* 188 */
+ 189, /* 189 */
+ 190, /* 190 */
+ 167, /* 191 */
+ 193, /* 192 */
+ 192, /* 193 */
+ 201, /* 194 */
+ 200, /* 195 */
+ 205, /* 196 */
+ 204, /* 197 */
+ 211, /* 198 */
+ 210, /* 199 */
+ 218, /* 200 */
+ 217, /* 201 */
+ 344, /* 202 */
+ 268, /* 203 */
+ 352, /* 204 */
+ 381, /* 205 */
+ 272, /* 206 */
+ 317, /* 207 */
+ 194, /* 208 */
+ 196, /* 209 */
+ 202, /* 210 */
+ 203, /* 211 */
+ 206, /* 212 */
+ 207, /* 213 */
+ 212, /* 214 */
+ 214, /* 215 */
+ 219, /* 216 */
+ 220, /* 217 */
+ 345, /* 218 */
+ 269, /* 219 */
+ 353, /* 220 */
+ 382, /* 221 */
+ 271, /* 222 */
+ 318, /* 223 */
+ 195, /* 224 */
+ 197, /* 225 */
+ 198, /* 226 */
+ 338, /* 227 */
+ 375, /* 228 */
+ 221, /* 229 */
+ 213, /* 230 */
+ 216, /* 231 */
+ 254, /* 232 */
+ 330, /* 233 */
+ 340, /* 234 */
+ 262, /* 235 */
+ 346, /* 236 */
+ 377, /* 237 */
+ 63, /* 238 */
+ 240, /* 239 */
+ 227, /* 240 */
+ 229, /* 241 */
+ 230, /* 242 */
+ 339, /* 243 */
+ 373, /* 244 */
+ 253, /* 245 */
+ 245, /* 246 */
+ 248, /* 247 */
+ 254, /* 248 */
+ 331, /* 249 */
+ 341, /* 250 */
+ 263, /* 251 */
+ 347, /* 252 */
+ 378, /* 253 */
+ 63, /* 254 */
+ 32, /* 255 */
+};
+
+gchar * rds_decode(const gchar *s)
+{
+ guint l = strlen(s);
+ GString *b = g_string_sized_new(2 * l);
+
+ guchar *c = (guchar*) s;
+ while (*c) {
+ g_string_append_unichar(b, rds_table[*c]);
+ c++;
+ }
+
+ gchar *r = b->str;
+ g_string_free(b, FALSE);
+ return r;
+}
+
diff --git a/rds.h b/rds.h
new file mode 100644
index 0000000..59bf199
--- /dev/null
+++ b/rds.h
@@ -0,0 +1,8 @@
+#ifndef _CFM_RDS_H_
+#define _CFM_RDS_H_
+
+#include <glib.h>
+
+gchar * rds_decode(const gchar *s);
+
+#endif
diff --git a/tuner.c b/tuner.c
new file mode 100644
index 0000000..04f1f9d
--- /dev/null
+++ b/tuner.c
@@ -0,0 +1,289 @@
+/*
+ * GPL 2
+ */
+
+#include <math.h>
+#include <string.h>
+#include <gtk/gtk.h>
+#include <cairo.h>
+
+#include "tuner.h"
+
+G_DEFINE_TYPE(CFmTuner, cfm_tuner, GTK_TYPE_DRAWING_AREA);
+
+#define CFM_TUNER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CFM_TYPE_TUNER, CFmTunerPrivate))
+
+#define ABS_RANGE_LOW 60000000
+#define ABS_RANGE_HIGH 140000000
+
+#define SCALE_TENTH_MHZ_PIXELS 15.0
+#define LABELS_FONT_SPEC "Nokia Sans 18"
+
+struct _CFmTunerPrivate {
+ gulong range_low, range_high;
+ gulong freq;
+ gboolean dragging;
+ gdouble drag_start_x;
+};
+
+enum {
+ PROP_0,
+ PROP_FREQUENCY,
+ PROP_RANGE_LOW,
+ PROP_RANGE_HIGH,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_0,
+ SIGNAL_FREQUENCY_CHANGED,
+ SIGNAL_FREQUENCY_TUNED,
+ SIGNAL_LAST
+};
+
+static GParamSpec *properties[PROP_LAST];
+static guint signals[SIGNAL_LAST];
+
+static void cfm_tuner_draw(CFmTuner *self, cairo_t *cr)
+{
+ const GtkAllocation *size = &( GTK_WIDGET(self)->allocation );
+ const double w = size->width, h = size->height;
+ const double midx = w / 2;
+ const double cur_f = self->priv->freq / 100000.0; // Tenths of a Mhz
+ const double left_f = cur_f - (midx / SCALE_TENTH_MHZ_PIXELS);
+
+ double left_f_int;
+ double left_f_frac = modf(left_f, &left_f_int);
+
+ static gchar text[12];
+ PangoLayout *layout = pango_cairo_create_layout(cr);
+ PangoFontDescription *desc = pango_font_description_from_string(LABELS_FONT_SPEC);
+ double x = (1.0 - left_f_frac) * SCALE_TENTH_MHZ_PIXELS;
+ gulong f = left_f_int + 1;
+ cairo_set_source_rgb(cr, 1, 1, 1);
+ pango_layout_set_font_description(layout, desc);
+ pango_font_description_free(desc);
+ while (x <= w) {
+ if (f % 10 == 0) {
+ gint tw, th;
+ sprintf(text, "%lu", f / 10);
+ pango_layout_set_text(layout, text, -1);
+ pango_cairo_update_layout(cr, layout);
+ pango_layout_get_size(layout, &tw, &th);
+ cairo_move_to(cr, x - (tw / (PANGO_SCALE * 2)), 0.0);
+ pango_cairo_show_layout(cr, layout);
+ cairo_move_to(cr, x, 0.3 * h);
+ cairo_line_to(cr, x, 0.7 * h);
+ } else {
+ cairo_move_to(cr, x, 0.4 * h);
+ cairo_line_to(cr, x, 0.55 * h);
+ }
+ cairo_stroke(cr);
+ x += SCALE_TENTH_MHZ_PIXELS;
+ f += 1;
+ }
+
+
+ cairo_set_source_rgb(cr, 1, 0, 0);
+ cairo_move_to(cr, w / 2, 0.25 * h);
+ cairo_line_to(cr, w / 2, h);
+ cairo_stroke(cr);
+}
+
+static void cfm_tuner_redraw(CFmTuner *tuner)
+{
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(tuner));
+ GdkRegion *region = gdk_drawable_get_clip_region(GDK_DRAWABLE(window));
+ gdk_window_invalidate_region(window, region, TRUE);
+}
+
+static void cfm_tuner_realize(GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS(cfm_tuner_parent_class)->realize(widget);
+
+ gtk_widget_add_events(widget,
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_MOTION_MASK);
+}
+
+
+static gboolean cfm_tuner_button_press(GtkWidget *widget, GdkEventButton *event)
+{
+ CFmTuner *self = CFM_TUNER(widget);
+ CFmTunerPrivate *priv = self->priv;
+
+ priv->dragging = TRUE;
+ priv->drag_start_x = event->x;
+
+ return FALSE;
+}
+
+static gboolean cfm_tuner_button_release(GtkWidget *widget, GdkEventButton *event)
+{
+ CFmTuner *self = CFM_TUNER(widget);
+ CFmTunerPrivate *priv = self->priv;
+
+ if (priv->dragging) {
+ priv->dragging = FALSE;
+ priv->freq = lrint(priv->freq / 100000.0) * 100000; /* Round it. */
+ cfm_tuner_redraw(self);
+ g_signal_emit(G_OBJECT(self), signals[SIGNAL_FREQUENCY_TUNED], 0, NULL);
+ }
+
+ return FALSE;
+}
+
+static gboolean cfm_tuner_motion_notify(GtkWidget *widget, GdkEventMotion *event)
+{
+ CFmTuner *self = CFM_TUNER(widget);
+ CFmTunerPrivate *priv = self->priv;
+
+ if (priv->dragging) {
+ gdouble moved = (priv->drag_start_x - event->x) / SCALE_TENTH_MHZ_PIXELS;
+ priv->freq += moved * 100000.0;
+ if (priv->freq < priv->range_low) {
+ priv->freq = priv->range_low;
+ } else if (priv->freq > priv->range_high) {
+ priv->freq = priv->range_high;
+ }
+ priv->drag_start_x = event->x;
+ cfm_tuner_redraw(self);
+ g_signal_emit(G_OBJECT(self), signals[SIGNAL_FREQUENCY_CHANGED], 0, NULL);
+ }
+
+ return FALSE;
+}
+
+static void cfm_tuner_size_request(GtkWidget *widget, GtkRequisition *requisition)
+{
+ requisition->width = 120;
+ requisition->height = 120;
+}
+
+static gboolean cfm_tuner_expose(GtkWidget *widget, GdkEventExpose *event)
+{
+ CFmTuner *self = CFM_TUNER(widget);
+ cairo_t *cr = gdk_cairo_create(widget->window);
+ cairo_rectangle(cr, event->area.x, event->area.y, event->area.width,
+ event->area.height);
+ cairo_clip(cr);
+
+ cfm_tuner_draw(self, cr);
+
+ cairo_destroy(cr);
+ return FALSE;
+}
+
+static void cfm_tuner_set_property(GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ CFmTuner *self = CFM_TUNER(object);
+ switch (property_id) {
+ case PROP_FREQUENCY:
+ self->priv->freq = g_value_get_ulong(value);
+ cfm_tuner_redraw(self);
+ break;
+ case PROP_RANGE_LOW:
+ self->priv->range_low = g_value_get_ulong(value);
+ cfm_tuner_redraw(self);
+ break;
+ case PROP_RANGE_HIGH:
+ self->priv->range_high = g_value_get_ulong(value);
+ cfm_tuner_redraw(self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_tuner_get_property(GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ CFmTuner *self = CFM_TUNER(object);
+ switch (property_id) {
+ case PROP_FREQUENCY:
+ g_value_set_ulong(value, self->priv->freq);
+ 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;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void cfm_tuner_init(CFmTuner *self)
+{
+ CFmTunerPrivate *priv;
+
+ self->priv = priv = CFM_TUNER_GET_PRIVATE(self);
+}
+
+static void cfm_tuner_dispose(GObject *object)
+{
+}
+
+static void cfm_tuner_finalize(GObject *object)
+{
+}
+
+static void cfm_tuner_class_init(CFmTunerClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ GParamSpec *param_spec;
+
+ gobject_class->set_property = cfm_tuner_set_property;
+ gobject_class->get_property = cfm_tuner_get_property;
+ gobject_class->dispose = cfm_tuner_dispose;
+ gobject_class->finalize = cfm_tuner_finalize;
+ widget_class->realize = cfm_tuner_realize;
+ widget_class->button_press_event = cfm_tuner_button_press;
+ widget_class->button_release_event = cfm_tuner_button_release;
+ widget_class->motion_notify_event = cfm_tuner_motion_notify;
+ widget_class->size_request = cfm_tuner_size_request;
+ widget_class->expose_event = cfm_tuner_expose;
+
+ g_type_class_add_private (klass, sizeof(CFmTunerPrivate));
+
+ 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_READWRITE | 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_READWRITE | G_PARAM_STATIC_STRINGS);
+ properties[PROP_RANGE_HIGH] = param_spec;
+ g_object_class_install_property(gobject_class, PROP_RANGE_HIGH, param_spec);
+
+ signals[SIGNAL_FREQUENCY_CHANGED] = g_signal_new("frequency-changed",
+ G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ signals[SIGNAL_FREQUENCY_TUNED] = g_signal_new("frequency-tuned",
+ G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+}
+
+CFmTuner* cfm_tuner_new()
+{
+ return g_object_new(CFM_TYPE_TUNER, NULL);
+}
+
diff --git a/tuner.h b/tuner.h
new file mode 100644
index 0000000..c29825d
--- /dev/null
+++ b/tuner.h
@@ -0,0 +1,36 @@
+/*
+ * GPL 2
+ */
+
+#ifndef __CFM_TUNER_H__
+#define __CFM_TUNER_H__
+
+#include <gtk/gtk.h>
+
+#define CFM_TYPE_TUNER (cfm_tuner_get_type ())
+#define CFM_TUNER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CFM_TYPE_TUNER, CFmTuner))
+#define CFM_IS_TUNER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CFM_TYPE_TUNER))
+#define CFM_TUNER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CFM_TYPE_TUNER, CFmTunerClass))
+#define CFM_IS_TUNER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CFM_TYPE_TUNER))
+#define CFM_TUNER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CFM_TYPE_TUNER, CFmTunerClass))
+
+typedef struct _CFmTuner CFmTuner;
+typedef struct _CFmTunerPrivate CFmTunerPrivate;
+typedef struct _CFmTunerClass CFmTunerClass;
+
+struct _CFmTuner
+{
+ GtkDrawingArea parent;
+ CFmTunerPrivate *priv;
+};
+
+struct _CFmTunerClass
+{
+ GtkDrawingAreaClass parent;
+};
+
+GType cfm_tuner_get_type (void);
+CFmTuner* cfm_tuner_new();
+
+#endif /* __CFM_TUNER_H__ */
+
diff --git a/types.c b/types.c
new file mode 100644
index 0000000..0bb63d3
--- /dev/null
+++ b/types.c
@@ -0,0 +1,22 @@
+
+#include <glib-object.h>
+#include "types.h"
+
+GType
+cfm_radio_output_get_type(void)
+{
+ static GType etype = 0;
+
+ if (etype == 0) {
+ static const GEnumValue values[] = {
+ { CFM_RADIO_OUTPUT_MUTE, "CFM_RADIO_OUTPUT_MUTE", "mute" },
+ { CFM_RADIO_OUTPUT_SYSTEM, "CFM_RADIO_OUTPUT_SYSTEM", "system" },
+ { CFM_RADIO_OUTPUT_SPEAKER, "CFM_RADIO_OUTPUT_SPEAKER", "speaker" },
+ { CFM_RADIO_OUTPUT_HEADPHONES, "CFM_RADIO_OUTPUT_HEADPHONES", "headphones" },
+ { 0, NULL, NULL }
+ };
+ etype = g_enum_register_static("CFmRadioOutput", values);
+ }
+ return etype;
+}
+
diff --git a/types.h b/types.h
new file mode 100644
index 0000000..8ecc837
--- /dev/null
+++ b/types.h
@@ -0,0 +1,22 @@
+
+#ifndef __CFMRADIO_TYPES_H__
+#define __CFMRADIO_TYPES_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ CFM_RADIO_OUTPUT_MUTE = 0,
+ CFM_RADIO_OUTPUT_SYSTEM,
+ CFM_RADIO_OUTPUT_SPEAKER,
+ CFM_RADIO_OUTPUT_HEADPHONES
+} CFmRadioOutput;
+
+GType cfm_radio_output_get_type(void) G_GNUC_CONST;
+#define CFM_TYPE_RADIO_OUTPUT (cfm_radio_output_get_type())
+
+G_END_DECLS
+
+#endif /*__CFMRADIO_TYPES_H__ */
+