summaryrefslogtreecommitdiff
path: root/gr-audio/lib/alsa/alsa_sink.cc
diff options
context:
space:
mode:
Diffstat (limited to 'gr-audio/lib/alsa/alsa_sink.cc')
-rw-r--r--gr-audio/lib/alsa/alsa_sink.cc542
1 files changed, 542 insertions, 0 deletions
diff --git a/gr-audio/lib/alsa/alsa_sink.cc b/gr-audio/lib/alsa/alsa_sink.cc
new file mode 100644
index 0000000000..4af57105a9
--- /dev/null
+++ b/gr-audio/lib/alsa/alsa_sink.cc
@@ -0,0 +1,542 @@
+/* -*- c++ -*- */
+/*
+ * Copyright 2004-2011 Free Software Foundation, Inc.
+ *
+ * This file is part of GNU Radio
+ *
+ * GNU Radio 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 3, or (at your option)
+ * any later version.
+ *
+ * GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "audio_registry.h"
+#include <alsa_sink.h>
+#include <alsa_impl.h>
+#include <gr_io_signature.h>
+#include <gr_prefs.h>
+#include <stdio.h>
+#include <iostream>
+#include <stdexcept>
+
+namespace gr {
+ namespace audio {
+
+ AUDIO_REGISTER_SINK(REG_PRIO_HIGH, alsa)(int sampling_rate,
+ const std::string &device_name,
+ bool ok_to_block)
+ {
+ return sink::sptr
+ (new alsa_sink(sampling_rate, device_name, ok_to_block));
+ }
+
+ static bool CHATTY_DEBUG = false;
+
+ static snd_pcm_format_t acceptable_formats[] = {
+ // these are in our preferred order...
+ SND_PCM_FORMAT_S32,
+ SND_PCM_FORMAT_S16
+ };
+
+#define NELEMS(x) (sizeof(x)/sizeof(x[0]))
+
+ static std::string
+ default_device_name()
+ {
+ return gr_prefs::singleton()->get_string("audio_alsa", "default_output_device", "hw:0,0");
+ }
+
+ static double
+ default_period_time()
+ {
+ return std::max(0.001,
+ gr_prefs::singleton()->get_double("audio_alsa", "period_time", 0.010));
+ }
+
+ static int
+ default_nperiods()
+ {
+ return std::max(2L,
+ gr_prefs::singleton()->get_long("audio_alsa", "nperiods", 4));
+ }
+
+ // ----------------------------------------------------------------
+
+ alsa_sink::alsa_sink(int sampling_rate,
+ const std::string device_name,
+ bool ok_to_block)
+ : gr_sync_block("audio_alsa_sink",
+ gr_make_io_signature(0, 0, 0),
+ gr_make_io_signature(0, 0, 0)),
+ d_sampling_rate(sampling_rate),
+ d_device_name(device_name.empty() ? default_device_name() : device_name),
+ d_pcm_handle(0),
+ d_hw_params((snd_pcm_hw_params_t*)(new char[snd_pcm_hw_params_sizeof()])),
+ d_sw_params((snd_pcm_sw_params_t*)(new char[snd_pcm_sw_params_sizeof()])),
+ d_nperiods(default_nperiods()),
+ d_period_time_us((unsigned int)(default_period_time() * 1e6)),
+ d_period_size(0),
+ d_buffer_size_bytes(0), d_buffer(0),
+ d_worker(0), d_special_case_mono_to_stereo(false),
+ d_nunderuns(0), d_nsuspends(0), d_ok_to_block(ok_to_block)
+ {
+ CHATTY_DEBUG = gr_prefs::singleton()->get_bool("audio_alsa", "verbose", false);
+
+ int error;
+ int dir;
+
+ // open the device for playback
+ error = snd_pcm_open(&d_pcm_handle, d_device_name.c_str(),
+ SND_PCM_STREAM_PLAYBACK, 0);
+ if(ok_to_block == false)
+ snd_pcm_nonblock(d_pcm_handle, !ok_to_block);
+ if(error < 0){
+ fprintf(stderr, "audio_alsa_sink[%s]: %s\n",
+ d_device_name.c_str(), snd_strerror(error));
+ throw std::runtime_error("audio_alsa_sink");
+ }
+
+ // Fill params with a full configuration space for a PCM.
+ error = snd_pcm_hw_params_any(d_pcm_handle, d_hw_params);
+ if(error < 0)
+ bail("broken configuration for playback", error);
+
+ if(CHATTY_DEBUG)
+ gri_alsa_dump_hw_params(d_pcm_handle, d_hw_params, stdout);
+
+ // now that we know how many channels the h/w can handle, set input signature
+ unsigned int umin_chan, umax_chan;
+ snd_pcm_hw_params_get_channels_min(d_hw_params, &umin_chan);
+ snd_pcm_hw_params_get_channels_max(d_hw_params, &umax_chan);
+ int min_chan = std::min(umin_chan, 1000U);
+ int max_chan = std::min(umax_chan, 1000U);
+
+ // As a special case, if the hw's min_chan is two, we'll accept
+ // a single input and handle the duplication ourselves.
+ if(min_chan == 2) {
+ min_chan = 1;
+ d_special_case_mono_to_stereo = true;
+ }
+ set_input_signature(gr_make_io_signature(min_chan, max_chan,
+ sizeof(float)));
+
+ // fill in portions of the d_hw_params that we know now...
+
+ // Specify the access methods we implement
+ // For now, we only handle RW_INTERLEAVED...
+ snd_pcm_access_mask_t *access_mask;
+ snd_pcm_access_mask_t **access_mask_ptr = &access_mask; // FIXME: workaround for compiler warning
+ snd_pcm_access_mask_alloca(access_mask_ptr);
+ snd_pcm_access_mask_none(access_mask);
+ snd_pcm_access_mask_set(access_mask, SND_PCM_ACCESS_RW_INTERLEAVED);
+ // snd_pcm_access_mask_set(access_mask, SND_PCM_ACCESS_RW_NONINTERLEAVED);
+
+ if((error = snd_pcm_hw_params_set_access_mask(d_pcm_handle,
+ d_hw_params, access_mask)) < 0)
+ bail("failed to set access mask", error);
+
+ // set sample format
+ if(!gri_alsa_pick_acceptable_format(d_pcm_handle, d_hw_params,
+ acceptable_formats,
+ NELEMS(acceptable_formats),
+ &d_format,
+ "audio_alsa_sink",
+ CHATTY_DEBUG))
+ throw std::runtime_error("audio_alsa_sink");
+
+ // sampling rate
+ unsigned int orig_sampling_rate = d_sampling_rate;
+ if((error = snd_pcm_hw_params_set_rate_near(d_pcm_handle, d_hw_params,
+ &d_sampling_rate, 0)) < 0)
+ bail("failed to set rate near", error);
+
+ if(orig_sampling_rate != d_sampling_rate) {
+ fprintf(stderr, "audio_alsa_sink[%s]: unable to support sampling rate %d\n",
+ snd_pcm_name(d_pcm_handle), orig_sampling_rate);
+ fprintf(stderr, " card requested %d instead.\n", d_sampling_rate);
+ }
+
+ /*
+ * ALSA transfers data in units of "periods".
+ * We indirectly determine the underlying buffersize by specifying
+ * the number of periods we want (typically 4) and the length of each
+ * period in units of time (typically 1ms).
+ */
+ unsigned int min_nperiods, max_nperiods;
+ snd_pcm_hw_params_get_periods_min(d_hw_params, &min_nperiods, &dir);
+ snd_pcm_hw_params_get_periods_max(d_hw_params, &max_nperiods, &dir);
+ //fprintf(stderr, "alsa_sink: min_nperiods = %d, max_nperiods = %d\n",
+ // min_nperiods, max_nperiods);
+
+ unsigned int orig_nperiods = d_nperiods;
+ d_nperiods = std::min (std::max (min_nperiods, d_nperiods), max_nperiods);
+
+ // adjust period time so that total buffering remains more-or-less constant
+ d_period_time_us = (d_period_time_us * orig_nperiods) / d_nperiods;
+
+ error = snd_pcm_hw_params_set_periods(d_pcm_handle, d_hw_params,
+ d_nperiods, 0);
+ if(error < 0)
+ bail("set_periods failed", error);
+
+ dir = 0;
+ error = snd_pcm_hw_params_set_period_time_near(d_pcm_handle, d_hw_params,
+ &d_period_time_us, &dir);
+ if(error < 0)
+ bail("set_period_time_near failed", error);
+
+ dir = 0;
+ error = snd_pcm_hw_params_get_period_size(d_hw_params,
+ &d_period_size, &dir);
+ if(error < 0)
+ bail("get_period_size failed", error);
+
+ set_output_multiple(d_period_size);
+ }
+
+ bool
+ alsa_sink::check_topology(int ninputs, int noutputs)
+ {
+ // ninputs is how many channels the user has connected.
+ // Now we can finish up setting up the hw params...
+
+ int nchan = ninputs;
+ int err;
+
+ // Check the state of the stream
+ // Ensure that the pcm is in a state where we can still mess with the hw_params
+ snd_pcm_state_t state;
+ state = snd_pcm_state(d_pcm_handle);
+ if(state == SND_PCM_STATE_RUNNING)
+ return true; // If stream is running, don't change any parameters
+ else if(state == SND_PCM_STATE_XRUN)
+ snd_pcm_prepare(d_pcm_handle); // Prepare stream on underrun, and we can set parameters;
+
+ bool special_case = nchan == 1 && d_special_case_mono_to_stereo;
+ if(special_case)
+ nchan = 2;
+
+ err = snd_pcm_hw_params_set_channels(d_pcm_handle, d_hw_params, nchan);
+
+ if(err < 0) {
+ output_error_msg("set_channels failed", err);
+ return false;
+ }
+
+ // set the parameters into the driver...
+ err = snd_pcm_hw_params(d_pcm_handle, d_hw_params);
+ if(err < 0) {
+ output_error_msg("snd_pcm_hw_params failed", err);
+ return false;
+ }
+
+ // get current s/w params
+ err = snd_pcm_sw_params_current(d_pcm_handle, d_sw_params);
+ if(err < 0)
+ bail("snd_pcm_sw_params_current", err);
+
+ // Tell the PCM device to wait to start until we've filled
+ // it's buffers half way full. This helps avoid audio underruns.
+
+ err = snd_pcm_sw_params_set_start_threshold(d_pcm_handle,
+ d_sw_params,
+ d_nperiods * d_period_size / 2);
+ if(err < 0)
+ bail("snd_pcm_sw_params_set_start_threshold", err);
+
+ // store the s/w params
+ err = snd_pcm_sw_params(d_pcm_handle, d_sw_params);
+ if(err < 0)
+ bail("snd_pcm_sw_params", err);
+
+ d_buffer_size_bytes =
+ d_period_size * nchan * snd_pcm_format_size(d_format, 1);
+
+ d_buffer = new char[d_buffer_size_bytes];
+
+ if(CHATTY_DEBUG)
+ fprintf(stdout, "audio_alsa_sink[%s]: sample resolution = %d bits\n",
+ snd_pcm_name(d_pcm_handle),
+ snd_pcm_hw_params_get_sbits(d_hw_params));
+
+ switch(d_format) {
+ case SND_PCM_FORMAT_S16:
+ if(special_case)
+ d_worker = &alsa_sink::work_s16_1x2;
+ else
+ d_worker = &alsa_sink::work_s16;
+ break;
+
+ case SND_PCM_FORMAT_S32:
+ if(special_case)
+ d_worker = &alsa_sink::work_s32_1x2;
+ else
+ d_worker = &alsa_sink::work_s32;
+ break;
+
+ default:
+ assert(0);
+ }
+ return true;
+ }
+
+ alsa_sink::~alsa_sink()
+ {
+ if(snd_pcm_state(d_pcm_handle) == SND_PCM_STATE_RUNNING)
+ snd_pcm_drop(d_pcm_handle);
+
+ snd_pcm_close(d_pcm_handle);
+ delete [] ((char*)d_hw_params);
+ delete [] ((char*)d_sw_params);
+ delete [] d_buffer;
+ }
+
+ int
+ alsa_sink::work(int noutput_items,
+ gr_vector_const_void_star &input_items,
+ gr_vector_void_star &output_items)
+ {
+ assert((noutput_items % d_period_size) == 0);
+
+ // this is a call through a pointer to a method...
+ return (this->*d_worker)(noutput_items, input_items, output_items);
+ }
+
+ /*
+ * Work function that deals with float to S16 conversion
+ */
+ int
+ alsa_sink::work_s16(int noutput_items,
+ gr_vector_const_void_star &input_items,
+ gr_vector_void_star &output_items)
+ {
+ typedef gr_int16 sample_t; // the type of samples we're creating
+ static const float scale_factor = std::pow(2.0f, 16-1) - 1;
+
+ unsigned int nchan = input_items.size();
+ const float **in = (const float **)&input_items[0];
+ sample_t *buf = (sample_t *)d_buffer;
+ int bi;
+ int n;
+
+ unsigned int sizeof_frame = nchan * sizeof(sample_t);
+ assert(d_buffer_size_bytes == d_period_size * sizeof_frame);
+
+ for(n = 0; n < noutput_items; n += d_period_size) {
+ // process one period of data
+ bi = 0;
+ for(unsigned int i = 0; i < d_period_size; i++) {
+ for (unsigned int chan = 0; chan < nchan; chan++) {
+ buf[bi++] = (sample_t) (in[chan][i] * scale_factor);
+ }
+ }
+
+ // update src pointers
+ for(unsigned int chan = 0; chan < nchan; chan++)
+ in[chan] += d_period_size;
+
+ if(!write_buffer (buf, d_period_size, sizeof_frame))
+ return -1; // No fixing this problem. Say we're done.
+ }
+
+ return n;
+ }
+
+ /*
+ * Work function that deals with float to S32 conversion
+ */
+ int
+ alsa_sink::work_s32(int noutput_items,
+ gr_vector_const_void_star &input_items,
+ gr_vector_void_star &output_items)
+ {
+ typedef gr_int32 sample_t; // the type of samples we're creating
+ static const float scale_factor = std::pow(2.0f, 32-1) - 1;
+
+ unsigned int nchan = input_items.size();
+ const float **in = (const float **)&input_items[0];
+ sample_t *buf = (sample_t *)d_buffer;
+ int bi;
+ int n;
+
+ unsigned int sizeof_frame = nchan * sizeof (sample_t);
+ assert(d_buffer_size_bytes == d_period_size * sizeof_frame);
+
+ for(n = 0; n < noutput_items; n += d_period_size) {
+ // process one period of data
+ bi = 0;
+ for(unsigned int i = 0; i < d_period_size; i++) {
+ for(unsigned int chan = 0; chan < nchan; chan++) {
+ buf[bi++] = (sample_t)(in[chan][i] * scale_factor);
+ }
+ }
+
+ // update src pointers
+ for(unsigned int chan = 0; chan < nchan; chan++)
+ in[chan] += d_period_size;
+
+ if(!write_buffer (buf, d_period_size, sizeof_frame))
+ return -1; // No fixing this problem. Say we're done.
+ }
+
+ return n;
+ }
+
+ /*
+ * Work function that deals with float to S16 conversion and
+ * mono to stereo kludge.
+ */
+ int
+ alsa_sink::work_s16_1x2(int noutput_items,
+ gr_vector_const_void_star &input_items,
+ gr_vector_void_star &output_items)
+ {
+ typedef gr_int16 sample_t; // the type of samples we're creating
+ static const float scale_factor = std::pow(2.0f, 16-1) - 1;
+
+ assert(input_items.size () == 1);
+ static const unsigned int nchan = 2;
+ const float **in = (const float **)&input_items[0];
+ sample_t *buf = (sample_t *)d_buffer;
+ int bi;
+ int n;
+
+ unsigned int sizeof_frame = nchan * sizeof(sample_t);
+ assert(d_buffer_size_bytes == d_period_size * sizeof_frame);
+
+ for(n = 0; n < noutput_items; n += d_period_size) {
+ // process one period of data
+ bi = 0;
+ for(unsigned int i = 0; i < d_period_size; i++) {
+ sample_t t = (sample_t) (in[0][i] * scale_factor);
+ buf[bi++] = t;
+ buf[bi++] = t;
+ }
+
+ // update src pointers
+ in[0] += d_period_size;
+
+ if(!write_buffer (buf, d_period_size, sizeof_frame))
+ return -1; // No fixing this problem. Say we're done.
+ }
+
+ return n;
+ }
+
+ /*
+ * Work function that deals with float to S32 conversion and
+ * mono to stereo kludge.
+ */
+ int
+ alsa_sink::work_s32_1x2(int noutput_items,
+ gr_vector_const_void_star &input_items,
+ gr_vector_void_star &output_items)
+ {
+ typedef gr_int32 sample_t; // the type of samples we're creating
+ static const float scale_factor = std::pow(2.0f, 32-1) - 1;
+
+ assert(input_items.size () == 1);
+ static unsigned int nchan = 2;
+ const float **in = (const float **)&input_items[0];
+ sample_t *buf = (sample_t*)d_buffer;
+ int bi;
+ int n;
+
+ unsigned int sizeof_frame = nchan * sizeof(sample_t);
+ assert(d_buffer_size_bytes == d_period_size * sizeof_frame);
+
+ for(n = 0; n < noutput_items; n += d_period_size) {
+ // process one period of data
+ bi = 0;
+ for(unsigned int i = 0; i < d_period_size; i++) {
+ sample_t t = (sample_t)(in[0][i] * scale_factor);
+ buf[bi++] = t;
+ buf[bi++] = t;
+ }
+
+ // update src pointers
+ in[0] += d_period_size;
+
+ if(!write_buffer (buf, d_period_size, sizeof_frame))
+ return -1; // No fixing this problem. Say we're done.
+ }
+
+ return n;
+ }
+
+ bool
+ alsa_sink::write_buffer(const void *vbuffer,
+ unsigned nframes, unsigned sizeof_frame)
+ {
+ const unsigned char *buffer = (const unsigned char *)vbuffer;
+
+ while(nframes > 0){
+ int r = snd_pcm_writei(d_pcm_handle, buffer, nframes);
+ if(r == -EAGAIN) {
+ if(d_ok_to_block == true)
+ continue; // try again
+ break;
+ }
+
+ else if(r == -EPIPE) { // underrun
+ d_nunderuns++;
+ fputs("aU", stderr);
+ if((r = snd_pcm_prepare (d_pcm_handle)) < 0){
+ output_error_msg("snd_pcm_prepare failed. Can't recover from underrun", r);
+ return false;
+ }
+ continue; // try again
+ }
+
+ else if(r == -ESTRPIPE) { // h/w is suspended (whatever that means)
+ // This is apparently related to power management
+ d_nsuspends++;
+ if((r = snd_pcm_resume (d_pcm_handle)) < 0) {
+ output_error_msg("failed to resume from suspend", r);
+ return false;
+ }
+ continue; // try again
+ }
+
+ else if (r < 0) {
+ output_error_msg("snd_pcm_writei failed", r);
+ return false;
+ }
+
+ nframes -= r;
+ buffer += r * sizeof_frame;
+ }
+
+ return true;
+ }
+
+ void
+ alsa_sink::output_error_msg (const char *msg, int err)
+ {
+ fprintf(stderr, "audio_alsa_sink[%s]: %s: %s\n",
+ snd_pcm_name(d_pcm_handle), msg, snd_strerror(err));
+ }
+
+ void
+ alsa_sink::bail(const char *msg, int err) throw (std::runtime_error)
+ {
+ output_error_msg(msg, err);
+ throw std::runtime_error("audio_alsa_sink");
+ }
+
+ } /* namespace audio */
+} /* namespace gr */