From 4a0d4a02a64774c61840496c8141b89ed31d1d94 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 24 Aug 2009 18:57:06 +0200 Subject: output/recorder: new output plugin for recording radio streams The recorder plugin writes audio played by MPD to a file. This may be useful for recording radio streams. This implementation is incomplete, because support for tags is missing, and MPD should be able to record each track to a different file. --- Makefile.am | 4 + NEWS | 2 + configure.ac | 37 ++++++- doc/mpdconf.example | 12 ++ doc/user.xml | 67 +++++++++++ src/output/recorder_output_plugin.c | 214 ++++++++++++++++++++++++++++++++++++ src/output_list.c | 4 + 7 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 src/output/recorder_output_plugin.c diff --git a/Makefile.am b/Makefile.am index c3d8b4a18..8e1341f0f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -597,6 +597,10 @@ if HAVE_SHOUT OUTPUT_SRC += src/output/shout_plugin.c endif +if ENABLE_RECORDER_OUTPUT +OUTPUT_SRC += src/output/recorder_output_plugin.c +endif + if ENABLE_HTTPD_OUTPUT OUTPUT_SRC += \ src/icy_server.c \ diff --git a/NEWS b/NEWS index a113dd5b8..5175dfaee 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ ver 0.16 (20??/??/??) - flac: load external cue sheet when no internal one * encoders: - twolame: new encoder plugin based on libtwolame +* output: + - recorder: new output plugin for recording radio streams * mixers: - removed support for legacy mixer configuration - reimplemented software volume as mixer+filter plugin diff --git a/configure.ac b/configure.ac index 67b2f2957..efa638caa 100644 --- a/configure.ac +++ b/configure.ac @@ -708,6 +708,11 @@ fi AM_CONDITIONAL(HAVE_PULSE, test x$enable_pulse = xyes) +AC_ARG_ENABLE(recorder-output, + AS_HELP_STRING([--enable-recorder-output], + [enables the HTTP server output]),, + [enable_recorder_output=auto]) + AC_ARG_ENABLE(httpd-output, AS_HELP_STRING([--enable-httpd-output], [enables the HTTP server output]),, @@ -992,10 +997,14 @@ dnl dnl Encoder API and shout/httpd output plugin dnl -if test x$enable_shout = xyes || test x$enable_httpd_output = xyes; then +if test x$enable_shout = xyes || \ + test x$enable_recorder_output = xyes || \ + test x$enable_httpd_output = xyes; then # at least one output using encoders is explicitly enabled need_encoder=yes -elif test x$enable_shout = xauto || test x$enable_httpd_output = xauto; then +elif test x$enable_shout = xauto || \ + test x$enable_recorder_output = xauto || \ + test x$enable_httpd_output = xauto; then need_encoder=auto else # all outputs using encoders are disabled @@ -1048,6 +1057,17 @@ if test x$enable_shout = xauto; then fi fi +if test x$enable_recorder_output = xauto; then + # handle recorder auto-detection: disable if no encoder is + # available + if test x$enable_encoder = xyes; then + enable_recorder_output=yes + else + AC_MSG_WARN([No encoder plugin -- disabling the recorder output plugin]) + enable_recorder_output=no + fi +fi + if test x$enable_httpd_output = xauto; then # handle HTTPD auto-detection: disable if no encoder is # available @@ -1064,6 +1084,11 @@ if test x$enable_shout = xyes; then AC_DEFINE(HAVE_SHOUT, 1, [Define to enable the shoutcast output]) fi +AM_CONDITIONAL(ENABLE_RECORDER_OUTPUT, test x$enable_recorder_output = xyes) +if test x$enable_recorder_output = xyes; then + AC_DEFINE(ENABLE_RECORDER_OUTPUT, 1, [Define to enable the recorder output]) +fi + AM_CONDITIONAL(ENABLE_HTTPD_OUTPUT, test x$enable_httpd_output = xyes) if test x$enable_httpd_output = xyes; then AC_DEFINE(ENABLE_HTTPD_OUTPUT, 1, [Define to enable the HTTP server output]) @@ -1225,6 +1250,12 @@ else echo " FIFO support ..................disabled" fi +if test x$enable_recorder_output = xyes; then + echo " recorder support ..............enabled" +else + echo " recorder support ..............disabled" +fi + if test x$enable_httpd_output = xyes; then echo " HTTP daemon support ...........enabled" else @@ -1291,6 +1322,7 @@ if test x$enable_ao = xno && test x$enable_oss = xno && test x$enable_shout = xno && + test x$enable_recorder_output = xno && test x$enable_httpd_output = xno && test x$enable_solaris_output = xno && test x$enable_alsa = xno && @@ -1305,6 +1337,7 @@ fi if test x$enable_shout = xyes || + test x$enable_recorder = xyes || test x$enable_httpd_output = xyes; then echo " Streaming Encoder Support:" if test x$enable_lame_encoder = xyes; then diff --git a/doc/mpdconf.example b/doc/mpdconf.example index 1dce635b2..4e3b19a5a 100644 --- a/doc/mpdconf.example +++ b/doc/mpdconf.example @@ -226,6 +226,18 @@ input { # mixer_type "software" # optional #} # +# An example of a recorder output: +# +#audio_output { +# type "recorder" +# name "My recorder" +# encoder "vorbis" # optional, vorbis or lame +# path "/var/lib/mpd/recorder/mpd.ogg" +# quality "5.0" # do not define if bitrate is defined +# bitrate "128" # do not define if quality is defined +# format "44100:16:1" +#} +# # An example of a httpd output (built-in HTTP streaming server): # #audio_output { diff --git a/doc/user.xml b/doc/user.xml index 6e039ebde..853417040 100644 --- a/doc/user.xml +++ b/doc/user.xml @@ -834,6 +834,73 @@ cd mpd-version +
+ <varname>recorder</varname> + + + The recorder plugin writes the audio + played by MPD to a file. This may be useful for recording + radio streams. + + + + You must configure either quality or + bitrate. + + + + + + + Setting + Description + + + + + + path + P + + + Write to this file. + + + + + encoder + NAME + + + Chooses an encoder plugin, + e.g. vorbis. + + + + + quality + Q + + + Configures the encoder quality (for VBR) in the + range -1 .. 10. + + + + + bitrate + BR + + + Sets a constant encoder bit rate, in kilobit per + second. + + + + + +
+
<varname>shout</varname> diff --git a/src/output/recorder_output_plugin.c b/src/output/recorder_output_plugin.c new file mode 100644 index 000000000..413e5d0d1 --- /dev/null +++ b/src/output/recorder_output_plugin.c @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2003-2009 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. + */ + +#include "output_api.h" +#include "encoder_plugin.h" +#include "encoder_list.h" + +#include +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "recorder" + +struct recorder_output { + /** + * The configured encoder plugin. + */ + struct encoder *encoder; + + /** + * The destination file name. + */ + const char *path; + + /** + * The destination file descriptor. + */ + int fd; + + /** + * The buffer for encoder_read(). + */ + char buffer[32768]; +}; + +/** + * The quark used for GError.domain. + */ +static inline GQuark +recorder_output_quark(void) +{ + return g_quark_from_static_string("recorder_output"); +} + +static void * +recorder_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, GError **error_r) +{ + struct recorder_output *recorder = g_new(struct recorder_output, 1); + const char *encoder_name; + const struct encoder_plugin *encoder_plugin; + + /* read configuration */ + + encoder_name = config_get_block_string(param, "encoder", "vorbis"); + encoder_plugin = encoder_plugin_get(encoder_name); + if (encoder_plugin == NULL) { + g_set_error(error_r, recorder_output_quark(), 0, + "No such encoder: %s", encoder_name); + return NULL; + } + + recorder->path = config_get_block_string(param, "path", NULL); + if (recorder->path == NULL) { + g_set_error(error_r, recorder_output_quark(), 0, + "'path' not configured"); + return NULL; + } + + /* initialize encoder */ + + recorder->encoder = encoder_init(encoder_plugin, param, error_r); + if (recorder->encoder == NULL) + return NULL; + + return recorder; +} + +static void +recorder_output_finish(void *data) +{ + struct recorder_output *recorder = data; + + encoder_finish(recorder->encoder); + g_free(recorder); +} + +/** + * Writes pending data from the encoder to the output file. + */ +static bool +recorder_output_encoder_to_file(struct recorder_output *recorder, + GError **error_r) +{ + size_t size = 0, position, nbytes; + + assert(recorder->fd >= 0); + + /* read from the encoder */ + + size = encoder_read(recorder->encoder, recorder->buffer, + sizeof(recorder->buffer)); + if (size == 0) + return true; + + /* write everything into the file */ + + position = 0; + while (true) { + nbytes = write(recorder->fd, recorder->buffer + position, + size - position); + if (nbytes > 0) { + position += (size_t)nbytes; + if (position >= size) + return true; + } else if (nbytes == 0) { + /* shouldn't happen for files */ + g_set_error(error_r, recorder_output_quark(), 0, + "write() returned 0"); + return false; + } else if (errno != EINTR) { + g_set_error(error_r, recorder_output_quark(), 0, + "Failed to write to '%s': %s", + recorder->path, g_strerror(errno)); + return false; + } + } +} + +static bool +recorder_output_open(void *data, struct audio_format *audio_format, + GError **error_r) +{ + struct recorder_output *recorder = data; + bool success; + + /* create the output file */ + + recorder->fd = creat(recorder->path, 0666); + if (recorder->fd < 0) { + g_set_error(error_r, recorder_output_quark(), 0, + "Failed to create '%s': %s", + recorder->path, g_strerror(errno)); + return false; + } + + /* open the encoder */ + + success = encoder_open(recorder->encoder, audio_format, error_r); + if (!success) { + close(recorder->fd); + unlink(recorder->path); + return false; + } + + return true; +} + +static void +recorder_output_close(void *data) +{ + struct recorder_output *recorder = data; + + /* flush the encoder and write the rest to the file */ + + if (encoder_flush(recorder->encoder, NULL)) + recorder_output_encoder_to_file(recorder, NULL); + + /* now really close everything */ + + encoder_close(recorder->encoder); + + close(recorder->fd); +} + +static size_t +recorder_output_play(void *data, const void *chunk, size_t size, + GError **error_r) +{ + struct recorder_output *recorder = data; + + return encoder_write(recorder->encoder, chunk, size, error_r) && + recorder_output_encoder_to_file(recorder, error_r) + ? size : 0; +} + +const struct audio_output_plugin recorder_output_plugin = { + .name = "recorder", + .init = recorder_output_init, + .finish = recorder_output_finish, + .open = recorder_output_open, + .close = recorder_output_close, + .play = recorder_output_play, +}; diff --git a/src/output_list.c b/src/output_list.c index 81de16649..74a9be81c 100644 --- a/src/output_list.c +++ b/src/output_list.c @@ -34,6 +34,7 @@ extern const struct audio_output_plugin pulse_plugin; extern const struct audio_output_plugin mvp_output_plugin; extern const struct audio_output_plugin jackPlugin; extern const struct audio_output_plugin httpd_output_plugin; +extern const struct audio_output_plugin recorder_output_plugin; const struct audio_output_plugin *audio_output_plugins[] = { #ifdef HAVE_SHOUT @@ -72,6 +73,9 @@ const struct audio_output_plugin *audio_output_plugins[] = { #endif #ifdef ENABLE_HTTPD_OUTPUT &httpd_output_plugin, +#endif +#ifdef ENABLE_RECORDER_OUTPUT + &recorder_output_plugin, #endif NULL }; -- cgit v1.2.3