From 35f85ddd860a819dbdebed1124f572719b28ef0b Mon Sep 17 00:00:00 2001 From: Steven O'Brien Date: Sun, 15 Dec 2013 16:52:21 +0000 Subject: add draft ALSA input plugin I've created an elementary input plugin that plays sound from the soundcard, so you can use MPD to listen to anything connected to the line-in jack, or to Video4Linux FM radio cards that send audio through the soundcard. There has been a small number of posts here in the past requesting line-in input, so here is a first, simplistic stab at it. The patch adds a new sheme, alsa://, which causes mpd to play data read directly from a souncdard. It defaults to hw:0,0, but you can pass any ALSA device name in the URI. So, using mpc for example: mpc add alsa:// mpc play will play from device hw:0,0. To use a diffferent device: mpc add alsa://hw:1,0 --- Makefile.am | 8 ++ NEWS | 2 + src/InputRegistry.cxx | 7 ++ src/input/AlsaInputPlugin.cxx | 206 ++++++++++++++++++++++++++++++++++++++++++ src/input/AlsaInputPlugin.hxx | 28 ++++++ src/ls.cxx | 3 + 6 files changed, 254 insertions(+) create mode 100644 src/input/AlsaInputPlugin.cxx create mode 100644 src/input/AlsaInputPlugin.hxx diff --git a/Makefile.am b/Makefile.am index d5d9c0294..2b610a22f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -802,6 +802,14 @@ INPUT_LIBS = \ $(DESPOTIFY_LIBS) \ $(MMS_LIBS) +if HAVE_ALSA +libinput_a_SOURCES += \ + src/input/AlsaInputPlugin.cxx \ + src/input/AlsaInputPlugin.hxx +INPUT_LIBS += $(ALSA_LIBS) +endif + + if ENABLE_CURL libinput_a_SOURCES += \ src/input/CurlInputPlugin.cxx src/input/CurlInputPlugin.hxx \ diff --git a/NEWS b/NEWS index 133e283fb..f83f72697 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,8 @@ ver 0.19 (not yet released) * protocol - new commands "addtagid", "cleartagid" +* input + - alsa: new input plugin * new resampler option using libsoxr ver 0.18.6 (not yet released) diff --git a/src/InputRegistry.cxx b/src/InputRegistry.cxx index aa6c06ed1..ef4acb237 100644 --- a/src/InputRegistry.cxx +++ b/src/InputRegistry.cxx @@ -22,6 +22,10 @@ #include "util/Macros.hxx" #include "input/FileInputPlugin.hxx" +#ifdef HAVE_ALSA +#include "input/AlsaInputPlugin.hxx" +#endif + #ifdef ENABLE_ARCHIVE #include "input/ArchiveInputPlugin.hxx" #endif @@ -48,6 +52,9 @@ const InputPlugin *const input_plugins[] = { &input_plugin_file, +#ifdef HAVE_ALSA + &input_plugin_alsa, +#endif #ifdef ENABLE_ARCHIVE &input_plugin_archive, #endif diff --git a/src/input/AlsaInputPlugin.cxx b/src/input/AlsaInputPlugin.cxx new file mode 100644 index 000000000..f014434c1 --- /dev/null +++ b/src/input/AlsaInputPlugin.cxx @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2003-2013 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* + * ALSA code based on an example by Paul Davis released under GPL here: + * http://equalarea.com/paul/alsa-audio.html + * and one by Matthias Nagorni, also GPL, here: + * http://alsamodular.sourceforge.net/alsa_programming_howto.html + */ + +#include "config.h" +#include "AlsaInputPlugin.hxx" +#include "InputPlugin.hxx" +#include "InputStream.hxx" +#include "util/Domain.hxx" +#include "util/Error.hxx" +#include "util/StringUtil.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain alsa_input_domain("alsa"); + +static constexpr const char *default_device = "hw:0,0"; + +// this value chosen to balance between limiting latency and avoiding stutter +static constexpr int max_frames_to_buffer = 64; + +// the following defaults are because the PcmDecoderPlugin forces CD format +static constexpr snd_pcm_format_t default_format = SND_PCM_FORMAT_S16; +static constexpr int default_channels = 2; // stereo +static constexpr unsigned int default_rate = 44100; // cd quality + +struct AlsaInputStream { + InputStream base; + snd_pcm_t *capture_handle; + size_t frame_size; + size_t max_bytes_to_read; + + AlsaInputStream(const char *uri, Mutex &mutex, Cond &cond, + snd_pcm_t *handle) + :base(input_plugin_alsa, uri, mutex, cond), + capture_handle(handle) { + frame_size = snd_pcm_format_width(default_format) / 8 * default_channels; + max_bytes_to_read = max_frames_to_buffer * frame_size; + base.mime = strdup("audio/x-mpd-cdda-pcm"); + base.seekable = false; + base.size = -1; + base.ready = true; + } + + ~AlsaInputStream() { + snd_pcm_close(capture_handle); + } +}; + +static InputStream * +alsa_input_open(const char *uri, Mutex &mutex, Cond &cond, Error &error) +{ + int err; + + // check uri is appropriate for alsa input + if (!StringStartsWith(uri, "alsa://")) + return nullptr; + + const char *device = uri + 7; + if (device[0] == '\0') + device = default_device; + + snd_pcm_t *capture_handle; + if ((err = snd_pcm_open(&capture_handle, device, + SND_PCM_STREAM_CAPTURE, 0)) < 0) { + error.Format(alsa_input_domain, "Failed to open device: %s (%s)", device, snd_strerror(err)); + return nullptr; + } + + snd_pcm_hw_params_t *hw_params; + if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) { + error.Format(alsa_input_domain, "Cannot allocate hardware parameter structure (%s)", snd_strerror(err)); + snd_pcm_close(capture_handle); + return nullptr; + } + + if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) { + error.Format(alsa_input_domain, "Cannot initialize hardware parameter structure (%s)", snd_strerror(err)); + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + return nullptr; + } + + if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + error.Format(alsa_input_domain, "Cannot set access type (%s)", snd_strerror (err)); + snd_pcm_hw_params_free (hw_params); + snd_pcm_close(capture_handle); + return nullptr; + } + + if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, default_format)) < 0) { + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + error.Format(alsa_input_domain, "Cannot set sample format (%s)", snd_strerror (err)); + return nullptr; + } + + if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, default_channels)) < 0) { + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + error.Format(alsa_input_domain, "Cannot set channels (%s)", snd_strerror (err)); + return nullptr; + } + + if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, default_rate, 0)) < 0) { + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + error.Format(alsa_input_domain, "Cannot set sample rate (%s)", snd_strerror (err)); + return nullptr; + } + + if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) { + error.Format(alsa_input_domain, "Cannot set parameters (%s)", snd_strerror (err)); + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + return nullptr; + } + + snd_pcm_hw_params_free (hw_params); + + // clear any data already in the PCM buffer + if ((err = snd_pcm_drop(capture_handle)) < 0) { + error.Format(alsa_input_domain, "Cannot clear PCM buffer (%s)", snd_strerror (err)); + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(capture_handle); + return nullptr; + } + + AlsaInputStream *ais = new AlsaInputStream(uri, mutex, cond, capture_handle); + return &ais->base; +} + +static void +alsa_input_close(InputStream *is) +{ + AlsaInputStream *ais = (AlsaInputStream*) is; + delete ais; +} + +static size_t +alsa_input_read(InputStream *is, void *ptr, size_t size, + gcc_unused Error &error) +{ + AlsaInputStream *ais = (AlsaInputStream*) is; + int num_frames = max_frames_to_buffer; + if (size < ais->max_bytes_to_read) + // calculate number of whole frames that will fit in size bytes + num_frames = size / ais->frame_size; + + int ret; + while ((ret = snd_pcm_readi(ais->capture_handle, ptr, + num_frames)) < 0) { + snd_pcm_prepare(ais->capture_handle); + LogDebug(alsa_input_domain, "Buffer Overrun"); + } + + size_t nbytes = ret == max_frames_to_buffer + ? ais->max_bytes_to_read + : ret * ais->frame_size; + is->offset += nbytes; + return nbytes; +} + +static bool +alsa_input_eof(gcc_unused InputStream *is) +{ + return false; +}; + +const struct InputPlugin input_plugin_alsa = { + .name = "alsa", + .init = nullptr, + .finish = nullptr, + .open = alsa_input_open, + .close = alsa_input_close, + .check = nullptr, + .update = nullptr, + .tag = nullptr, + .available = nullptr, + .read = alsa_input_read, + .eof = alsa_input_eof, + .seek = nullptr +}; diff --git a/src/input/AlsaInputPlugin.hxx b/src/input/AlsaInputPlugin.hxx new file mode 100644 index 000000000..ac9519588 --- /dev/null +++ b/src/input/AlsaInputPlugin.hxx @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2003-2013 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_ALSA_INPUT_PLUGIN_HXX +#define MPD_ALSA_INPUT_PLUGIN_HXX + +#include "InputPlugin.hxx" + +extern const struct InputPlugin input_plugin_alsa; + + +#endif diff --git a/src/ls.cxx b/src/ls.cxx index 117c8875b..8039cf1d8 100644 --- a/src/ls.cxx +++ b/src/ls.cxx @@ -54,6 +54,9 @@ static const char *remoteUrlPrefixes[] = { #endif #ifdef ENABLE_DESPOTIFY "spt://", +#endif +#ifdef HAVE_ALSA + "alsa://", #endif NULL }; -- cgit v1.2.3