[PATCH 4/4] alsa: Add module to talk to the Android audio hal to set up voice calls

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



---
 configure.ac                                |   17 +
 src/Makefile.am                             |   12 +
 src/daemon/default.pa.in                    |    4 +
 src/modules/alsa/module-android-audio-hal.c |  508 +++++++++++++++++++++++++++
 4 files changed, 541 insertions(+)
 create mode 100644 src/modules/alsa/module-android-audio-hal.c

diff --git a/configure.ac b/configure.ac
index 15f2f99..105c1d2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -781,6 +781,21 @@ AM_CONDITIONAL([HAVE_ALSA], [test "x$HAVE_ALSA" = x1])
 AS_IF([test "x$HAVE_ALSA" = "x1"], AC_DEFINE([HAVE_ALSA], 1, [Have ALSA?]))
 AS_IF([test "x$HAVE_ALSA_UCM" = "x1"], AC_DEFINE([HAVE_ALSA_UCM], 1, [Have ALSA UCM?]))
 
+#### Android audio HAL support (optional) ####
+
+AC_ARG_ENABLE([android],
+    AS_HELP_STRING([--disable-android],[Disable optional Android audio HAL support]))
+
+AS_IF([test "x$enable_android" != "xno"],
+    [AC_CHECK_HEADERS([android/hardware/audio.h], HAVE_ANDROID=1, HAVE_ANDROID=0)],
+    HAVE_ANDROID=0)
+
+AS_IF([test "x$enable_android" = "xyes" && test "x$HAVE_ANDROID" = "x0"],
+    [AC_MSG_ERROR([*** Android audio HAL support not found])])
+
+AM_CONDITIONAL([HAVE_ANDROID], [test "x$HAVE_ANDROID" = "x1"])
+AS_IF([test "x$HAVE_ANDROID" = "x1"], AC_DEFINE([HAVE_ANDROID], 1, [Have Android audio HAL?]))
+
 #### EsounD support (optional) ####
 
 AC_ARG_ENABLE([esound],
@@ -1375,6 +1390,7 @@ AS_IF([test "x$HAVE_X11" = "x1"], ENABLE_X11=yes, ENABLE_X11=no)
 AS_IF([test "x$HAVE_OSS_OUTPUT" = "x1"], ENABLE_OSS_OUTPUT=yes, ENABLE_OSS_OUTPUT=no)
 AS_IF([test "x$HAVE_OSS_WRAPPER" = "x1"], ENABLE_OSS_WRAPPER=yes, ENABLE_OSS_WRAPPER=no)
 AS_IF([test "x$HAVE_ALSA" = "x1"], ENABLE_ALSA=yes, ENABLE_ALSA=no)
+AS_IF([test "x$HAVE_ANDROID" = "x1"], ENABLE_ANDROID=yes, ENABLE_ANDROID=no)
 AS_IF([test "x$HAVE_COREAUDIO" = "x1"], ENABLE_COREAUDIO=yes, ENABLE_COREAUDIO=no)
 AS_IF([test "x$HAVE_SOLARIS" = "x1"], ENABLE_SOLARIS=yes, ENABLE_SOLARIS=no)
 AS_IF([test "x$HAVE_WAVEOUT" = "x1"], ENABLE_WAVEOUT=yes, ENABLE_WAVEOUT=no)
@@ -1428,6 +1444,7 @@ echo "
     Enable OSS Wrapper:            ${ENABLE_OSS_WRAPPER}
     Enable EsounD:                 ${ENABLE_ESOUND}
     Enable Alsa:                   ${ENABLE_ALSA}
+    Enable Android audio HAL:      ${ENABLE_ANDROID}
     Enable CoreAudio:              ${ENABLE_COREAUDIO}
     Enable Solaris:                ${ENABLE_SOLARIS}
     Enable WaveOut:                ${ENABLE_WAVEOUT}
diff --git a/src/Makefile.am b/src/Makefile.am
index 7caa1db..34a22a5 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1187,6 +1187,10 @@ modlibexec_LTLIBRARIES += \
 		module-alsa-source.la \
 		module-alsa-card.la
 
+if HAVE_ANDROID
+modlibexec_LTLIBRARIES += module-android-audio-hal.la
+endif
+
 dist_alsaprofilesets_DATA = \
 		modules/alsa/mixer/profile-sets/default.conf \
 		modules/alsa/mixer/profile-sets/extra-hdmi.conf \
@@ -1394,6 +1398,7 @@ SYMDEF_FILES = \
 		module-alsa-sink-symdef.h \
 		module-alsa-source-symdef.h \
 		module-alsa-card-symdef.h \
+		module-android-audio-hal-symdef.h \
 		module-coreaudio-detect-symdef.h \
 		module-coreaudio-device-symdef.h \
 		module-solaris-symdef.h \
@@ -1750,6 +1755,13 @@ libalsa_util_la_LIBADD += $(DBUS_LIBS)
 libalsa_util_la_CFLAGS += $(DBUS_CFLAGS)
 endif
 
+if HAVE_ANDROID
+module_android_audio_hal_la_SOURCES = modules/alsa/module-android-audio-hal.c
+module_android_audio_hal_la_LDFLAGS = $(MODULE_LDFLAGS)
+module_android_audio_hal_la_LIBADD = $(MODULE_LIBADD) $(ASOUNDLIB_LIBS) libalsa-util.la -lhardware
+module_android_audio_hal_la_CFLAGS = $(AM_CFLAGS) $(ASOUNDLIB_CFLAGS)
+endif
+
 module_alsa_sink_la_SOURCES = modules/alsa/module-alsa-sink.c
 module_alsa_sink_la_LDFLAGS = $(MODULE_LDFLAGS)
 module_alsa_sink_la_LIBADD = $(MODULE_LIBADD) $(ASOUNDLIB_LIBS) libalsa-util.la
diff --git a/src/daemon/default.pa.in b/src/daemon/default.pa.in
index f50d929..1b17689 100755
--- a/src/daemon/default.pa.in
+++ b/src/daemon/default.pa.in
@@ -78,6 +78,10 @@ load-module module-udev-detect
 load-module module-detect
 .endif
 
+.ifexists module-android-audio-hal at PA_SOEXT@
+load-module module-android-audio-hal
+.endif
+
 ### Automatically connect sink and source if JACK server is present
 .ifexists module-jackdbus-detect at PA_SOEXT@
 .nofail
diff --git a/src/modules/alsa/module-android-audio-hal.c b/src/modules/alsa/module-android-audio-hal.c
new file mode 100644
index 0000000..c5e66c3
--- /dev/null
+++ b/src/modules/alsa/module-android-audio-hal.c
@@ -0,0 +1,508 @@
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2013 David Henningsson, Canonical Ltd.
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio 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 Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+/*
+  Okay, so this module might need some explanation.
+
+  First, this module is currently used to set up and tear down voice calls only.
+  This is because Android handles this by calling into proprietary blobs, which
+  e g talks to the modem/baseband to set up routes correctly. For normal HiFi
+  playback, we use UCM only.
+
+  In Ubuntu Phone this is used together with libhybris to link into android code
+  from Ubuntu/Linux code.
+
+  The UCM file needs to set up a source and sink in order to have devices. We
+  need devices in order to be able to select headset/speaker/earpiece correctly.
+  This sink/source also controls call volume and mic mute.
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "module-android-audio-hal-symdef.h"
+
+PA_MODULE_AUTHOR("David Henningsson");
+PA_MODULE_DESCRIPTION("Android Audio HAL (Voice call helper)");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(TRUE);
+PA_MODULE_USAGE("");
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include <pulsecore/log.h>
+#include <pulsecore/card.h>
+#include <pulsecore/once.h>
+#include <pulsecore/core-util.h>
+
+#include <android/hardware/hardware.h>
+#include <android/hardware/audio.h>
+
+#include "alsa-mixer.h"
+
+const hw_module_t *primary_audio_module = NULL;
+
+typedef struct pa_android_audio_hal {
+    struct audio_hw_device *dev;
+    struct audio_stream_out *ostream;
+    struct audio_stream_in *istream;
+    int odevice; /* AUDIO_DEVICE_OUT_xxx */
+    int idevice; /* AUDIO_DEVICE_IN_xxx */
+} pa_android_audio_hal;
+
+static pa_android_audio_hal* pa_android_audio_hal_new() {
+
+    int r;
+    struct audio_hw_device *device;
+    struct pa_android_audio_hal *hal;
+
+    if (!primary_audio_module) {
+        r = hw_get_module_by_class("audio", "primary", &primary_audio_module);
+        if (r < 0 || primary_audio_module == NULL) {
+            pa_log("hw_get_module_by_class failed with error %d", r);
+            return NULL;
+        }
+        pa_log_debug("Opened Android module: '%s' - '%s', author: '%s'",
+                     primary_audio_module->id, primary_audio_module->name,
+                     primary_audio_module->author);
+    }
+
+    r = audio_hw_device_open(primary_audio_module, &device);
+    if (r || device == NULL) {
+        pa_log("audio_hw_device_open failed with error %d", r);
+        return NULL;
+    }
+    pa_log_debug("Opened audio hw device");
+
+    r = device->init_check(device);
+    if (r) {
+        pa_log("init_check failed with error %d", r);
+        audio_hw_device_close(device);
+        return NULL;
+    }
+    pa_log_debug("init_check succeeded");
+
+    hal = pa_xnew0(struct pa_android_audio_hal, 1);
+    hal->dev = device;
+    return hal;
+}
+
+static void print_err(int err, const char *func) {
+
+    if (err)
+        pa_log("%s returned error %d", func, err);
+    else
+        pa_log_debug("%s succeeded", func);
+}
+
+static void set_stream_device(struct audio_stream *stream, int device) {
+
+    char *s;
+    int err;
+
+    if (!stream->set_parameters) {
+        pa_log_warn("no set_parameters callback");
+        return;
+    }
+
+    s = pa_sprintf_malloc("routing=%d;", device);
+    pa_log_debug("Calling set_parameters with '%s'", s);
+
+    err = stream->set_parameters(stream, s);
+    print_err(err < 0 ? err : 0, "set_parameters");
+
+    pa_xfree(s);
+}
+
+static void start_voice_call(pa_android_audio_hal *hal) {
+
+    pa_assert(hal);
+    pa_assert(hal->dev);
+
+    if (!hal->dev->set_mode)
+        pa_log_warn("no set_mode callback");
+    else
+        print_err(hal->dev->set_mode(hal->dev, AUDIO_MODE_IN_CALL), "set_mode");
+
+    if (!hal->ostream) {
+        if (!hal->dev->open_output_stream)
+            pa_log_warn("no open_output_stream callback");
+        else {
+            struct audio_config config = { .sample_rate = 8000,
+                .channel_mask = AUDIO_CHANNEL_OUT_MONO, .format = AUDIO_FORMAT_PCM_16_BIT };
+            print_err(hal->dev->open_output_stream(hal->dev, 1, hal->odevice,
+                AUDIO_OUTPUT_FLAG_PRIMARY,
+                &config, &hal->ostream), "open_output_stream");
+        }
+    }
+
+    if (hal->ostream)
+        set_stream_device(&hal->ostream->common, hal->odevice);
+
+    if (!hal->istream) {
+        if (!hal->dev->open_input_stream)
+            pa_log_warn("no open_input_stream callback");
+        else {
+            struct audio_config config = { .sample_rate = 8000,
+                .channel_mask = AUDIO_CHANNEL_IN_MONO, .format = AUDIO_FORMAT_PCM_16_BIT };
+            print_err(hal->dev->open_input_stream(hal->dev, 2, hal->idevice,
+                  &config, &hal->istream), "open_input_stream");
+        }
+    }
+
+    if (hal->istream)
+        set_stream_device(&hal->istream->common, hal->idevice);
+}
+
+static void set_voice_call_volume(pa_android_audio_hal *hal, double v) {
+
+    if (!hal->dev->set_voice_volume)
+        pa_log_warn("no set_voice_volume callback");
+    else
+        print_err(hal->dev->set_voice_volume(hal->dev, v), "set_voice_volume");
+}
+
+static void set_voice_mic_mute(pa_android_audio_hal *hal, bool mute) {
+
+    if (!hal->dev->set_mic_mute)
+        pa_log_warn("no set_mic_mute callback");
+    else
+        print_err(hal->dev->set_mic_mute(hal->dev, mute), "set_mic_mute");
+}
+
+static void stop_voice_call(pa_android_audio_hal *hal) {
+
+    pa_assert(hal);
+    pa_assert(hal->dev);
+
+    if (hal->ostream) {
+        if (!hal->dev->close_output_stream)
+            pa_log_warn("no close_output_stream callback");
+        else {
+            pa_log_debug("Closing output device");
+            hal->dev->close_output_stream(hal->dev, hal->ostream);
+            hal->ostream = NULL;
+        }
+    }
+
+    if (hal->istream) {
+        if (!hal->dev->close_input_stream)
+            pa_log_warn("no close_input_stream callback");
+        else {
+            pa_log_debug("Closing input device");
+            hal->dev->close_input_stream(hal->dev, hal->istream);
+            hal->istream = NULL;
+        }
+    }
+
+    if (hal->dev->set_parameters) {
+        pa_log_debug("Setting mode to normal through set_parameters (Nexus 4 workaround)");
+        print_err(hal->dev->set_parameters(hal->dev, "CALL_KEY=0;"), "device set_parameters");
+    }
+
+    if (!hal->dev->set_mode)
+        pa_log_warn("no set_mode callback");
+    else
+        print_err(hal->dev->set_mode(hal->dev, AUDIO_MODE_NORMAL), "set_mode");
+}
+
+static void update_devices(pa_android_audio_hal *hal) {
+
+    if (hal->istream)
+        set_stream_device(&hal->istream->common, hal->idevice);
+    if (hal->ostream)
+        set_stream_device(&hal->ostream->common, hal->odevice);
+}
+
+static void pa_android_audio_hal_free(pa_android_audio_hal *hal) {
+
+    if (hal->dev) {
+        stop_voice_call(hal);
+        audio_hw_device_close(hal->dev);
+    }
+
+    pa_xfree(hal);
+}
+
+static bool card_has_voice_call(pa_card *c) {
+
+    pa_card_profile *p;
+    void *state;
+
+    PA_HASHMAP_FOREACH(p, c->profiles, state)
+        if (pa_streq(p->name, SND_USE_CASE_VERB_VOICECALL))
+            return true;
+
+    return false;
+}
+
+static bool profile_in_voice_call(pa_card_profile *p) {
+
+    return p && pa_streq(p->name, SND_USE_CASE_VERB_VOICECALL);
+}
+
+struct userdata {
+    pa_hook_slot
+        *card_profile_before_slot,
+        *card_profile_after_slot,
+        *sink_port_slot,
+        *source_port_slot;
+    pa_android_audio_hal *hal;
+    int savedidev, savedodev;
+};
+
+static struct userdata* userdata_instance = NULL; /* We need this for the volume callback */
+
+static bool get_devices_for_card(struct userdata *u, pa_card *card) {
+
+    pa_sink *sink;
+    pa_source *source;
+    uint32_t state;
+    int idev = 0, odev = 0;
+    bool hasports = false;
+
+    PA_IDXSET_FOREACH(sink, card->sinks, state) {
+        const char *n;
+        if (!sink->active_port)
+            continue;
+        n = sink->active_port->name;
+        hasports = true;
+
+        pa_log_debug("Current output port: %s", n);
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_EARPIECE, true))
+            odev |= AUDIO_DEVICE_OUT_EARPIECE;
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_SPEAKER, true))
+            odev |= AUDIO_DEVICE_OUT_SPEAKER;
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_HEADSET, true))
+            odev |= AUDIO_DEVICE_OUT_WIRED_HEADSET;
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_HEADPHONES, true))
+            odev |= AUDIO_DEVICE_OUT_WIRED_HEADPHONE;
+    }
+
+    PA_IDXSET_FOREACH(source, card->sources, state) {
+        const char *n;
+        if (!source->active_port)
+            continue;
+        n = source->active_port->name;
+        hasports = true;
+
+        pa_log_debug("Current input port: %s", n);
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_HANDSET, false))
+            idev |= (odev & AUDIO_DEVICE_OUT_SPEAKER) ? AUDIO_DEVICE_IN_BACK_MIC : AUDIO_DEVICE_IN_BUILTIN_MIC;
+        if (pa_alsa_ucm_port_contains(n, SND_USE_CASE_DEV_HEADSET, false))
+            idev |= AUDIO_DEVICE_IN_WIRED_HEADSET;
+    }
+
+    if (hasports) {
+        pa_log_debug("Card %s - active android devices: 0x%x (output) and 0x%x (input)",
+                     card->name, odev, idev);
+        u->savedidev = idev;
+        u->savedodev = odev;
+    }
+    if (u->hal) {
+        u->hal->idevice = u->savedidev;
+        u->hal->odevice = u->savedodev;
+    }
+
+    return hasports;
+}
+
+/* We can "hack" into the volume setting code, because PulseAudio does not (yet)
+   support hardware volumes for UCM, so set_volume callback is always NULL. */
+
+static void sink_set_volume_cb(pa_sink *s) {
+
+    struct userdata *u = userdata_instance;
+    pa_volume_t v = pa_cvolume_avg(&s->real_volume);
+    double d = (double) v / (double) PA_VOLUME_NORM; /* Should this be pa_sw_volume_to_linear(v)? */
+    pa_log_debug("Setting voice volume for sink '%s' to %f", s->name, d);
+    if (u->hal)
+        set_voice_call_volume(u->hal, d);
+}
+
+static void source_set_mute_cb(pa_source *s) {
+
+    struct userdata *u = userdata_instance;
+    pa_log_debug("Setting voice mic mute for source '%s' to %d", s->name, (int) s->muted);
+    if (u->hal)
+       set_voice_mic_mute(u->hal, s->muted);
+}
+
+static void teardown_voice_call(struct userdata *u, pa_card *card) {
+
+    /* remove volume hook */
+    uint32_t state;
+    pa_sink *sink;
+    pa_source *source;
+
+    PA_IDXSET_FOREACH(sink, card->sinks, state) {
+        if (sink->set_volume == sink_set_volume_cb)
+            sink->set_volume = NULL;
+    }
+
+    PA_IDXSET_FOREACH(source, card->sources, state) {
+        if (source->set_mute == source_set_mute_cb)
+            source->set_mute = NULL;
+    }
+
+    stop_voice_call(u->hal);
+}
+
+static bool setup_voice_call(struct userdata *u, pa_card *card) {
+
+    uint32_t state;
+    pa_sink *sink;
+    pa_source *source;
+
+    if (!u->hal) {
+        u->hal = pa_android_audio_hal_new();
+        if (!u->hal)
+            return false;
+    }
+
+    get_devices_for_card(u, card);
+    start_voice_call(u->hal);
+
+    /* setup volume hook */
+    PA_IDXSET_FOREACH(sink, card->sinks, state) {
+        if (!sink->set_volume)
+            sink->set_volume = sink_set_volume_cb;
+        sink_set_volume_cb(sink);
+    }
+
+    PA_IDXSET_FOREACH(source, card->sources, state) {
+        if (!source->set_mute)
+            source->set_mute = source_set_mute_cb;
+        source_set_mute_cb(source);
+    }
+
+    return true;
+}
+
+
+static pa_hook_result_t sink_port_hook_callback(pa_core *c, pa_sink *sink, struct userdata* u) {
+
+    if (!sink->card)
+        return PA_HOOK_OK;
+    if (!card_has_voice_call(sink->card) || !profile_in_voice_call(sink->card->active_profile))
+        return PA_HOOK_OK;
+
+    get_devices_for_card(u, sink->card);
+    if (u->hal)
+        update_devices(u->hal);
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t source_port_hook_callback(pa_core *c, pa_source *source, struct userdata* u) {
+
+    if (!source->card)
+        return PA_HOOK_OK;
+    if (!card_has_voice_call(source->card) || !profile_in_voice_call(source->card->active_profile))
+        return PA_HOOK_OK;
+
+    get_devices_for_card(u, source->card);
+    if (u->hal)
+        update_devices(u->hal);
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t card_profile_before_hook_callback(pa_core *c, pa_card_profile *profile, struct userdata *u) {
+
+    if (!card_has_voice_call(profile->card))
+        return PA_HOOK_OK;
+
+    if (u->hal && !profile_in_voice_call(profile)) {
+        teardown_voice_call(u, profile->card);
+    }
+
+    get_devices_for_card(u, profile->card); /* Save for later usage */
+    return PA_HOOK_OK;
+}
+
+
+static pa_hook_result_t card_profile_after_hook_callback(pa_core *c, pa_card *card, struct userdata *u) {
+
+    if (!card_has_voice_call(card))
+        return PA_HOOK_OK;
+
+    if (profile_in_voice_call(card->active_profile))
+        setup_voice_call(u, card);
+
+    return PA_HOOK_OK;
+}
+
+int pa__init(pa_module*m) {
+
+    pa_card *card;
+    uint32_t state;
+
+    struct userdata *u = pa_xnew0(struct userdata, 1);
+
+    pa_assert(userdata_instance == NULL);
+    userdata_instance = u;
+
+    u->card_profile_before_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_CHANGING],
+        PA_HOOK_LATE+30, (pa_hook_cb_t) card_profile_before_hook_callback, u);
+    u->card_profile_after_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_CHANGED],
+        PA_HOOK_LATE+30, (pa_hook_cb_t) card_profile_after_hook_callback, u);
+    u->sink_port_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PORT_CHANGED],
+        PA_HOOK_LATE+30, (pa_hook_cb_t) sink_port_hook_callback, u);
+    u->source_port_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PORT_CHANGED],
+        PA_HOOK_LATE+30, (pa_hook_cb_t) source_port_hook_callback, u);
+
+    PA_IDXSET_FOREACH(card, m->core->cards, state) {
+        if (!card_has_voice_call(card))
+            continue;
+
+        get_devices_for_card(u, card); /* Save for later usage */
+    }
+
+    return 0;
+}
+
+void pa__done(pa_module*m) {
+
+    struct userdata *u;
+
+    pa_assert(m);
+
+    if (!(u = m->userdata))
+        return;
+
+    if (u->card_profile_before_slot)
+        pa_hook_slot_free(u->card_profile_before_slot);
+    if (u->card_profile_after_slot)
+        pa_hook_slot_free(u->card_profile_after_slot);
+    if (u->sink_port_slot)
+        pa_hook_slot_free(u->sink_port_slot);
+    if (u->source_port_slot)
+        pa_hook_slot_free(u->source_port_slot);
+
+    if (u->hal)
+        pa_android_audio_hal_free(u->hal);
+
+    if (u == userdata_instance)
+        userdata_instance = NULL;
+
+    pa_xfree(u);
+}
-- 
1.7.9.5



[Index of Archives]     [Linux Audio Users]     [AMD Graphics]     [Linux USB Devel]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux