From 2ab60ecb15ab1a7e30226563a15bbeec05dda54d Mon Sep 17 00:00:00 2001 From: javier Date: Sun, 7 Nov 2010 03:00:56 +0100 Subject: initial import --- Makefile | 43 ++++ cfmradio.c | 263 ++++++++++++++++++++ data/cfmradio.desktop | 9 + data/cfmradio.service | 3 + data/codetables.utf8 | 14 ++ data/icon.png | Bin 0 -> 2916 bytes data/make-table.c | 32 +++ debian/changelog | 5 + debian/compat | 1 + debian/control | 79 ++++++ debian/copyright | 26 ++ debian/dirs | 4 + debian/docs | 0 debian/rules | 70 ++++++ n900-fmrx-enabler.h | 65 +++++ n900-fmrx-enabler.xml | 38 +++ preset_list.c | 149 +++++++++++ preset_list.h | 37 +++ presets.c | 344 ++++++++++++++++++++++++++ presets.h | 45 ++++ radio.c | 666 ++++++++++++++++++++++++++++++++++++++++++++++++++ radio.h | 37 +++ rds.c | 281 +++++++++++++++++++++ rds.h | 8 + tuner.c | 289 ++++++++++++++++++++++ tuner.h | 36 +++ types.c | 22 ++ types.h | 22 ++ 28 files changed, 2588 insertions(+) create mode 100644 Makefile create mode 100644 cfmradio.c create mode 100644 data/cfmradio.desktop create mode 100644 data/cfmradio.service create mode 100644 data/codetables.utf8 create mode 100644 data/icon.png create mode 100644 data/make-table.c create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/dirs create mode 100644 debian/docs create mode 100755 debian/rules create mode 100644 n900-fmrx-enabler.h create mode 100644 n900-fmrx-enabler.xml create mode 100644 preset_list.c create mode 100644 preset_list.h create mode 100644 presets.c create mode 100644 presets.h create mode 100644 radio.c create mode 100644 radio.h create mode 100644 rds.c create mode 100644 rds.h create mode 100644 tuner.c create mode 100644 tuner.h create mode 100644 types.c create mode 100644 types.h 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 +#include + +#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), "%.1f 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("%s", + 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 Binary files /dev/null and b/data/icon.png 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 + +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 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 +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 +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 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 +#include + +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 @@ + + + + + + + + + + + + + + + + + + + + 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 +#include + +#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 +#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 +#include +#include + +#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 +#include + +#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 +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + +#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 +#include + +#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 + +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 +#include +#include +#include + +#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 + +#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 +#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 + +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__ */ + -- cgit v1.2.3