[PATCH 2/5] qv4l2: new ALSA stream source code

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

 



Code copied from xawtv3

Signed-off-by: Bård Eirik Winther <bwinther@xxxxxxxxx>
---
 utils/qv4l2/alsa_stream.c | 645 ++++++++++++++++++++++++++++++++++++++++++++++
 utils/qv4l2/alsa_stream.h |   5 +
 2 files changed, 650 insertions(+)
 create mode 100644 utils/qv4l2/alsa_stream.c
 create mode 100644 utils/qv4l2/alsa_stream.h

diff --git a/utils/qv4l2/alsa_stream.c b/utils/qv4l2/alsa_stream.c
new file mode 100644
index 0000000..3e33b5e
--- /dev/null
+++ b/utils/qv4l2/alsa_stream.c
@@ -0,0 +1,645 @@
+/*
+ *  ALSA streaming support
+ *
+ *  Originally written by:
+ *      Copyright (c) by Devin Heitmueller <dheitmueller@xxxxxxxxxxxxxx>
+ *	for usage at tvtime
+ *  Derived from the alsa-driver test tool latency.c:
+ *    Copyright (c) by Jaroslav Kysela <perex@xxxxxxxx>
+ *
+ *  Copyright (c) 2011 - Mauro Carvalho Chehab <mchehab@xxxxxxxxxx>
+ *	Ported to xawtv, with bug fixes and improvements
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ *
+ */
+
+#include "config.h"
+
+#ifdef HAVE_ALSA_ASOUNDLIB_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <pthread.h>
+#include <alsa/asoundlib.h>
+#include <sys/time.h>
+#include <math.h>
+#include "alsa_stream.h"
+
+#define ARRAY_SIZE(a) (sizeof(a)/sizeof(*(a)))
+
+/* Private vars to control alsa thread status */
+static int stop_alsa = 0;
+
+/* Error handlers */
+snd_output_t *output = NULL;
+FILE *error_fp;
+int verbose = 0;
+
+struct final_params {
+    int bufsize;
+    int rate;
+    int latency;
+    int channels;
+};
+
+static int setparams_stream(snd_pcm_t *handle,
+			    snd_pcm_hw_params_t *params,
+			    snd_pcm_format_t format,
+			    int *channels,
+			    const char *id)
+{
+    int err;
+
+    err = snd_pcm_hw_params_any(handle, params);
+    if (err < 0) {
+	fprintf(error_fp,
+		"alsa: Broken configuration for %s PCM: no configurations available: %s\n",
+		snd_strerror(err), id);
+	return err;
+    }
+
+    err = snd_pcm_hw_params_set_access(handle, params,
+				       SND_PCM_ACCESS_RW_INTERLEAVED);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Access type not available for %s: %s\n", id,
+		snd_strerror(err));
+	return err;
+    }
+
+    err = snd_pcm_hw_params_set_format(handle, params, format);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Sample format not available for %s: %s\n", id,
+	       snd_strerror(err));
+	return err;
+    }
+
+retry:
+    err = snd_pcm_hw_params_set_channels(handle, params, *channels);
+    if (err < 0) {
+	if (strcmp(id, "capture") == 0 && *channels == 2) {
+	    *channels = 1;
+	    goto retry; /* Retry with mono capture */
+	}
+	fprintf(error_fp, "alsa: Channels count (%i) not available for %s: %s\n",
+		*channels, id, snd_strerror(err));
+	return err;
+    }
+
+    return 0;
+}
+
+static void getparams_periods(snd_pcm_t *handle,
+		      snd_pcm_hw_params_t *params,
+		      unsigned int *usecs,
+		      unsigned int *count,
+		      const char *id)
+{
+    unsigned min = 0, max = 0;
+
+    snd_pcm_hw_params_get_periods_min(params, &min, 0);
+    snd_pcm_hw_params_get_periods_max(params, &max, 0);
+    if (min && max) {
+	if (verbose)
+	    fprintf(error_fp, "alsa: %s periods range between %u and %u. Want: %u\n",
+		    id, min, max, *count);
+	if (*count < min)
+	    *count = min;
+	if (*count > max)
+	    *count = max;
+    }
+
+    min = max = 0;
+    snd_pcm_hw_params_get_period_time_min(params, &min, 0);
+    snd_pcm_hw_params_get_period_time_max(params, &max, 0);
+    if (min && max) {
+	if (verbose)
+	    fprintf(error_fp, "alsa: %s period time range between %u and %u. Want: %u\n",
+		    id, min, max, *usecs);
+	if (*usecs < min)
+	    *usecs = min;
+	if (*usecs > max)
+	    *usecs = max;
+    }
+}
+
+static int setparams_periods(snd_pcm_t *handle,
+		      snd_pcm_hw_params_t *params,
+		      unsigned int *usecs,
+		      unsigned int *count,
+		      const char *id)
+{
+    int err;
+
+    err = snd_pcm_hw_params_set_period_time_near(handle, params, usecs, 0);
+    if (err < 0) {
+	    fprintf(error_fp, "alsa: Unable to set period time %u for %s: %s\n",
+		    *usecs, id, snd_strerror(err));
+	    return err;
+    }
+
+    err = snd_pcm_hw_params_set_periods_near(handle, params, count, 0);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to set %u periods for %s: %s\n",
+		*count, id, snd_strerror(err));
+	return err;
+    }
+
+    if (verbose)
+	fprintf(error_fp, "alsa: %s period set to %u periods of %u time\n",
+		id, *count, *usecs);
+
+    return 0;
+}
+
+static int setparams_set(snd_pcm_t *handle,
+			 snd_pcm_hw_params_t *params,
+			 snd_pcm_sw_params_t *swparams,
+			 snd_pcm_uframes_t start_treshold,
+			 const char *id)
+{
+    int err;
+
+    err = snd_pcm_hw_params(handle, params);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to set hw params for %s: %s\n",
+		id, snd_strerror(err));
+	return err;
+    }
+    err = snd_pcm_sw_params_current(handle, swparams);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to determine current swparams for %s: %s\n",
+		id, snd_strerror(err));
+	return err;
+    }
+    err = snd_pcm_sw_params_set_start_threshold(handle, swparams,
+						start_treshold);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to set start threshold mode for %s: %s\n",
+		id, snd_strerror(err));
+	return err;
+    }
+
+    err = snd_pcm_sw_params_set_avail_min(handle, swparams, 4);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to set avail min for %s: %s\n",
+		id, snd_strerror(err));
+	return err;
+    }
+    err = snd_pcm_sw_params(handle, swparams);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Unable to set sw params for %s: %s\n",
+		id, snd_strerror(err));
+	return err;
+    }
+    return 0;
+}
+
+static int alsa_try_rate(snd_pcm_t *phandle, snd_pcm_t *chandle,
+                        snd_pcm_hw_params_t *p_hwparams,
+                        snd_pcm_hw_params_t *c_hwparams,
+                        int allow_resample, unsigned *ratep, unsigned *ratec)
+{
+    int err;
+
+    err = snd_pcm_hw_params_set_rate_near(chandle, c_hwparams, ratec, 0);
+    if (err)
+        return err;
+
+    *ratep = *ratec;
+    err = snd_pcm_hw_params_set_rate_near(phandle, p_hwparams, ratep, 0);
+    if (err)
+        return err;
+
+    if (*ratep == *ratec)
+        return 0;
+
+    if (verbose)
+        fprintf(error_fp,
+                "alsa_try_rate: capture wanted %u, playback wanted %u%s\n",
+                *ratec, *ratep, allow_resample ? " with resample enabled": "");
+
+    return 1; /* No error, but also no match */
+}
+
+static int setparams(snd_pcm_t *phandle, snd_pcm_t *chandle,
+		     snd_pcm_format_t format,
+		     int latency, int allow_resample,
+		     struct final_params *negotiated)
+{
+    int i;
+    unsigned ratep, ratec = 0;
+    unsigned ratemin = 32000, ratemax = 96000, val;
+    int err, channels = 2;
+    snd_pcm_hw_params_t *p_hwparams, *c_hwparams;
+    snd_pcm_sw_params_t *p_swparams, *c_swparams;
+    snd_pcm_uframes_t c_size, p_psize, c_psize;
+    /* Our latency is 2 periods (in usecs) */
+    unsigned int c_periods = 2, p_periods;
+    unsigned int c_periodtime, p_periodtime;
+    const unsigned int prefered_rates[] = { 44100, 48000, 32000 };
+
+    snd_pcm_hw_params_alloca(&p_hwparams);
+    snd_pcm_hw_params_alloca(&c_hwparams);
+    snd_pcm_sw_params_alloca(&p_swparams);
+    snd_pcm_sw_params_alloca(&c_swparams);
+
+    if (setparams_stream(chandle, c_hwparams, format, &channels, "capture"))
+	return 1;
+
+    if (setparams_stream(phandle, p_hwparams, format, &channels, "playback"))
+	return 1;
+
+    if (allow_resample) {
+	err = snd_pcm_hw_params_set_rate_resample(chandle, c_hwparams, 1);
+	if (err < 0) {
+	    fprintf(error_fp, "alsa: Resample setup failed: %s\n", snd_strerror(err));
+	    return 1;
+	} else if (verbose)
+	   fprintf(error_fp, "alsa: Resample enabled.\n");
+    }
+
+    err = snd_pcm_hw_params_get_rate_min(c_hwparams, &ratemin, 0);
+    if (err >= 0 && verbose)
+	fprintf(error_fp, "alsa: Capture min rate is %d\n", ratemin);
+    err = snd_pcm_hw_params_get_rate_max(c_hwparams, &ratemax, 0);
+    if (err >= 0 && verbose)
+	fprintf(error_fp, "alsa: Capture max rate is %u\n", ratemax);
+
+    err = snd_pcm_hw_params_get_rate_min(p_hwparams, &val, 0);
+    if (err >= 0) {
+	if (verbose)
+	    fprintf(error_fp, "alsa: Playback min rate is %u\n", val);
+	if (val > ratemin)
+		ratemin = val;
+    }
+    err = snd_pcm_hw_params_get_rate_max(p_hwparams, &val, 0);
+    if (err >= 0) {
+	if (verbose)
+	    fprintf(error_fp, "alsa: Playback max rate is %u\n", val);
+	if (val < ratemax)
+		ratemax = val;
+    }
+
+    if (verbose)
+	fprintf(error_fp,
+	        "alsa: Will search a common rate between %u and %u\n",
+		ratemin, ratemax);
+
+    /* First try a set of common rates */
+    err = -1;
+    for (i = 0; i < ARRAY_SIZE(prefered_rates); i++) {
+        if (prefered_rates[i] < ratemin || prefered_rates[i] > ratemax)
+            continue;
+        ratep = ratec = prefered_rates[i];
+        err = alsa_try_rate(phandle, chandle, p_hwparams, c_hwparams,
+                            allow_resample, &ratep, &ratec);
+        if (err == 0)
+            break;
+    }
+
+    if (err != 0) {
+        if (ratemin >= 44100) {
+            for (i = ratemin; i <= ratemax; i += 100) {
+                ratep = ratec = i;
+                err = alsa_try_rate(phandle, chandle, p_hwparams, c_hwparams,
+                                    allow_resample, &ratep, &ratec);
+                if (err == 0)
+                    break;
+            }
+        } else {
+            for (i = ratemax; i >= ratemin; i -= 100) {
+                ratep = ratec = i;
+                err = alsa_try_rate(phandle, chandle, p_hwparams, c_hwparams,
+                                    allow_resample, &ratep, &ratec);
+                if (err == 0)
+                    break;
+            }
+        }
+    }
+
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Failed to set a supported rate: %s\n",
+		snd_strerror(err));
+	return 1;
+    }
+    if (ratep != ratec) {
+	if (verbose || allow_resample)
+	    fprintf(error_fp,
+		    "alsa: Couldn't find a rate that it is supported by both playback and capture\n");
+	return 2;
+    }
+    if (verbose)
+	fprintf(error_fp, "alsa: Using Rate %d\n", ratec);
+
+    /* Negotiate period parameters */
+
+    c_periodtime = latency * 1000 / c_periods;
+    getparams_periods(chandle, c_hwparams, &c_periodtime, &c_periods, "capture");
+    p_periods = c_periods * 2;
+    p_periodtime = c_periodtime;
+    getparams_periods(phandle, p_hwparams, &p_periodtime, &p_periods, "playback");
+    c_periods = p_periods / 2;
+
+    /*
+     * Some playback devices support a very limited periodtime range. If the user needs to
+     * use a higher latency to avoid overrun/underrun, use an alternate algorithm of incresing
+     * the number of periods, to archive the needed latency
+     */
+    if (p_periodtime < c_periodtime) {
+	c_periodtime = p_periodtime;
+	c_periods = round (latency * 1000.0 / c_periodtime + 0.5);
+	getparams_periods(chandle, c_hwparams, &c_periodtime, &c_periods, "capture");
+	p_periods = c_periods * 2;
+	p_periodtime = c_periodtime;
+	getparams_periods(phandle, p_hwparams, &p_periodtime, &p_periods, "playback");
+	c_periods = p_periods / 2;
+    }
+
+    if (setparams_periods(chandle, c_hwparams, &c_periodtime, &c_periods, "capture"))
+	return 1;
+
+    /* Note we use twice as much periods for the playback buffer, since we
+       will get a period size near the requested time and we don't want it to
+       end up smaller then the capture buffer as then we could end up blocking
+       on writing to it. Note we will configure the playback dev to start
+       playing as soon as it has 2 capture periods worth of data, so this
+       won't influence latency */
+    if (setparams_periods(phandle, p_hwparams, &p_periodtime, &p_periods, "playback"))
+	return 1;
+
+    snd_pcm_hw_params_get_period_size(p_hwparams, &p_psize, NULL);
+    snd_pcm_hw_params_get_period_size(c_hwparams, &c_psize, NULL);
+    snd_pcm_hw_params_get_buffer_size(c_hwparams, &c_size);
+
+    latency = c_periods * c_psize;
+    if (setparams_set(phandle, p_hwparams, p_swparams, latency, "playback"))
+	return 1;
+
+    if (setparams_set(chandle, c_hwparams, c_swparams, c_psize, "capture"))
+	return 1;
+
+    if ((err = snd_pcm_prepare(phandle)) < 0) {
+	fprintf(error_fp, "alsa: Prepare error: %s\n", snd_strerror(err));
+	return 1;
+    }
+
+    if (verbose) {
+	fprintf(error_fp, "alsa: Negociated configuration:\n");
+	snd_pcm_dump_setup(phandle, output);
+	snd_pcm_dump_setup(chandle, output);
+	fprintf(error_fp, "alsa: Parameters are %iHz, %s, %i channels\n",
+		ratep, snd_pcm_format_name(format), channels);
+	fprintf(error_fp, "alsa: Set bitrate to %u%s, buffer size is %u\n", ratec,
+		allow_resample ? " with resample enabled at playback": "",
+		(unsigned int)c_size);
+    }
+
+    negotiated->bufsize = c_size;
+    negotiated->rate = ratep;
+    negotiated->channels = channels;
+    negotiated->latency = latency;
+    return 0;
+}
+
+/* Read up to len frames */
+static snd_pcm_sframes_t readbuf(snd_pcm_t *handle, char *buf, long len)
+{
+    snd_pcm_sframes_t r;
+
+    r = snd_pcm_readi(handle, buf, len);
+    if (r < 0 && r != -EAGAIN) {
+	r = snd_pcm_recover(handle, r, 0);
+	if (r < 0)
+	    fprintf(error_fp, "alsa: overrun recover error: %s\n", snd_strerror(r));
+    }
+    return r;
+}
+
+/* Write len frames (note not up to len, but all of len!) */
+static snd_pcm_sframes_t writebuf(snd_pcm_t *handle, char *buf, long len)
+{
+    snd_pcm_sframes_t r;
+
+    while (1) {
+	r = snd_pcm_writei(handle, buf, len);
+	if (r == len)
+	    return 0;
+	if (r < 0) {
+	    r = snd_pcm_recover(handle, r, 0);
+	    if (r < 0) {
+		fprintf(error_fp, "alsa: underrun recover error: %s\n",
+			snd_strerror(r));
+		return r;
+	    }
+	}
+	buf += r * 4;
+	len -= r;
+	snd_pcm_wait(handle, 100);
+    }
+}
+
+static int alsa_stream(const char *pdevice, const char *cdevice, int latency)
+{
+    snd_pcm_t *phandle, *chandle;
+    char *buffer;
+    int err;
+    ssize_t r;
+    struct final_params negotiated;
+    snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
+    char pdevice_new[32];
+
+    err = snd_output_stdio_attach(&output, error_fp, 0);
+    if (err < 0) {
+	fprintf(error_fp, "alsa: Output failed: %s\n", snd_strerror(err));
+	return 0;
+    }
+
+    /* Open the devices */
+    if ((err = snd_pcm_open(&phandle, pdevice, SND_PCM_STREAM_PLAYBACK,
+			    0)) < 0) {
+	fprintf(error_fp, "alsa: Cannot open playback device %s: %s\n",
+		pdevice, snd_strerror(err));
+	return 0;
+    }
+    if ((err = snd_pcm_open(&chandle, cdevice, SND_PCM_STREAM_CAPTURE,
+			    SND_PCM_NONBLOCK)) < 0) {
+	fprintf(error_fp, "alsa: Cannot open capture device %s: %s\n",
+		cdevice, snd_strerror(err));
+	snd_pcm_close(phandle);
+	return 0;
+    }
+
+    err = setparams(phandle, chandle, format, latency, 0, &negotiated);
+
+    /* Try to use plughw instead, as it allows emulating speed */
+    if (err == 2 && strncmp(pdevice, "hw", 2) == 0) {
+
+	snd_pcm_close(phandle);
+
+	sprintf(pdevice_new, "plug%s", pdevice);
+	pdevice = pdevice_new;
+	if (verbose)
+	    fprintf(error_fp, "alsa: Trying %s for playback\n", pdevice);
+	if ((err = snd_pcm_open(&phandle, pdevice, SND_PCM_STREAM_PLAYBACK,
+				0)) < 0) {
+	    fprintf(error_fp, "alsa: Cannot open playback device %s: %s\n",
+		    pdevice, snd_strerror(err));
+	    snd_pcm_close(chandle);
+	    return 0;
+	}
+
+	err = setparams(phandle, chandle, format, latency, 1, &negotiated);
+    }
+
+    if (err != 0) {
+	fprintf(error_fp, "alsa: setparams failed\n");
+	snd_pcm_close(phandle);
+	snd_pcm_close(chandle);
+	return 1;
+    }
+
+    buffer = malloc((negotiated.bufsize * snd_pcm_format_width(format) / 8)
+		    * negotiated.channels);
+    if (buffer == NULL) {
+	fprintf(error_fp, "alsa: Failed allocating buffer for audio\n");
+	snd_pcm_close(phandle);
+	snd_pcm_close(chandle);
+	return 0;
+    }
+
+    if (verbose)
+        fprintf(error_fp,
+	    "alsa: stream started from %s to %s (%i Hz, buffer delay = %.2f ms)\n",
+	    cdevice, pdevice, negotiated.rate,
+	    negotiated.latency * 1000.0 / negotiated.rate);
+
+    while (!stop_alsa) {
+	/* We start with a read and not a wait to auto(re)start the capture */
+	r = readbuf(chandle, buffer, negotiated.bufsize);
+	if (r == 0)   /* Succesfully recovered from an overrun? */
+	    continue; /* Force restart of capture stream */
+	if (r > 0)
+	    writebuf(phandle, buffer, r);
+	/* use poll to wait for next event */
+	while (!stop_alsa && !snd_pcm_wait(chandle, 50))
+	    ;
+    }
+
+    snd_pcm_drop(chandle);
+    snd_pcm_drop(phandle);
+
+    snd_pcm_unlink(chandle);
+    snd_pcm_hw_free(phandle);
+    snd_pcm_hw_free(chandle);
+
+    snd_pcm_close(phandle);
+    snd_pcm_close(chandle);
+
+    return 0;
+}
+
+struct input_params {
+    char *pdevice;
+    char *cdevice;
+    int latency;
+};
+
+static void *alsa_thread_entry(void *whatever)
+{
+    struct input_params *inputs = (struct input_params *) whatever;
+
+    if (verbose)
+	fprintf(error_fp, "alsa: starting copying alsa stream from %s to %s\n",
+		inputs->cdevice, inputs->pdevice);
+    alsa_stream(inputs->pdevice, inputs->cdevice, inputs->latency);
+    if (verbose)
+        fprintf(error_fp, "alsa: stream stopped\n");
+
+    free(inputs->pdevice);
+    free(inputs->cdevice);
+    free(inputs);
+
+    return NULL;
+}
+
+/*************************************************************************
+ Public functions
+ *************************************************************************/
+
+static int alsa_is_running = 0;
+static pthread_t alsa_thread;
+
+int alsa_thread_startup(const char *pdevice, const char *cdevice, int latency,
+			FILE *__error_fp, int __verbose)
+{
+    int ret;
+    struct input_params *inputs;
+
+    if ((strcasecmp(pdevice, "disabled") == 0) ||
+	(strcasecmp(cdevice, "disabled") == 0))
+	return 0;
+
+    if (__error_fp)
+	error_fp = __error_fp;
+    else
+	error_fp = stderr;
+
+    verbose = __verbose;
+
+    if (alsa_is_running) {
+        fprintf(error_fp, "alsa: Already running\n");
+        return EBUSY;
+    }
+
+    inputs = malloc(sizeof(struct input_params));
+    if (inputs == NULL) {
+	fprintf(error_fp, "alsa: failed allocating memory for inputs\n");
+	return ENOMEM;
+    }
+
+    inputs->pdevice = strdup(pdevice);
+    inputs->cdevice = strdup(cdevice);
+    inputs->latency = latency;
+
+    stop_alsa = 0;
+    ret = pthread_create(&alsa_thread, NULL,
+			 &alsa_thread_entry, (void *) inputs);
+    if (ret == 0)
+        alsa_is_running = 1;
+
+    return ret;
+}
+
+void alsa_thread_stop(void)
+{
+    if (!alsa_is_running)
+        return;
+
+    stop_alsa = 1;
+    pthread_join(alsa_thread, NULL);
+    alsa_is_running = 0;
+}
+
+int alsa_thread_is_running(void)
+{
+    return alsa_is_running;
+}
+
+#endif
diff --git a/utils/qv4l2/alsa_stream.h b/utils/qv4l2/alsa_stream.h
new file mode 100644
index 0000000..c68fd6d
--- /dev/null
+++ b/utils/qv4l2/alsa_stream.h
@@ -0,0 +1,5 @@
+int alsa_thread_startup(const char *pdevice, const char *cdevice, int latency,
+			FILE *__error_fp,
+			int __verbose);
+void alsa_thread_stop(void);
+int alsa_thread_is_running(void);
-- 
1.8.3.2

--
To unsubscribe from this list: send the line "unsubscribe linux-media" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Linux Input]     [Video for Linux]     [Gstreamer Embedded]     [Mplayer Users]     [Linux USB Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [Yosemite Backpacking]
  Powered by Linux