diff options
Diffstat (limited to 'src')
209 files changed, 14309 insertions, 5121 deletions
diff --git a/src/archive/zip_plugin.c b/src/archive/zip_plugin.c index dbd2534fa..ad3b403b4 100644 --- a/src/archive/zip_plugin.c +++ b/src/archive/zip_plugin.c @@ -157,7 +157,7 @@ zip_is_eof(struct input_stream *is) static bool zip_is_seek(G_GNUC_UNUSED struct input_stream *is, - G_GNUC_UNUSED off_t offset, G_GNUC_UNUSED int whence) + G_GNUC_UNUSED goffset offset, G_GNUC_UNUSED int whence) { zip_context *context = (zip_context *) is->data; zzip_off_t ofs = zzip_seek(context->file, offset, whence); diff --git a/src/audio.c b/src/audio.c index d48558e46..a0259cb37 100644 --- a/src/audio.c +++ b/src/audio.c @@ -35,9 +35,8 @@ static struct audio_format configured_audio_format; void getOutputAudioFormat(const struct audio_format *inAudioFormat, struct audio_format *outAudioFormat) { - *outAudioFormat = audio_format_defined(&configured_audio_format) - ? configured_audio_format - : *inAudioFormat; + *outAudioFormat = *inAudioFormat; + audio_format_mask_apply(outAudioFormat, &configured_audio_format); } void initAudioConfig(void) @@ -46,17 +45,12 @@ void initAudioConfig(void) GError *error = NULL; bool ret; - if (NULL == param || NULL == param->value) + if (param == NULL) return; ret = audio_format_parse(&configured_audio_format, param->value, - &error); + true, &error); if (!ret) g_error("error parsing \"%s\" at line %i: %s", CONF_AUDIO_OUTPUT_FORMAT, param->line, error->message); } - -void finishAudioConfig(void) -{ - audio_format_clear(&configured_audio_format); -} diff --git a/src/audio.h b/src/audio.h index 4d80ee0e6..4b80cfd27 100644 --- a/src/audio.h +++ b/src/audio.h @@ -30,6 +30,4 @@ void getOutputAudioFormat(const struct audio_format *inFormat, /* make sure initPlayerData is called before this function!! */ void initAudioConfig(void); -void finishAudioConfig(void); - #endif diff --git a/src/audio_format.h b/src/audio_format.h index 64087d070..a4f5ba2e0 100644 --- a/src/audio_format.h +++ b/src/audio_format.h @@ -23,25 +23,95 @@ #include <stdint.h> #include <stdbool.h> +/** + * This structure describes the format of a raw PCM stream. + */ struct audio_format { + /** + * The sample rate in Hz. A better name for this attribute is + * "frame rate", because technically, you have two samples per + * frame in stereo sound. + */ uint32_t sample_rate; + + /** + * The number of significant bits per sample. Samples are + * currently always signed. Supported values are 8, 16, 24, + * 32. 24 bit samples are packed in 32 bit integers. + */ uint8_t bits; + + /** + * The number of channels. Only mono (1) and stereo (2) are + * fully supported currently. + */ uint8_t channels; + + /** + * If zero, then samples are stored in host byte order. If + * nonzero, then samples are stored in the reverse host byte + * order. + */ + uint8_t reverse_endian; }; +/** + * Clears the #audio_format object, i.e. sets all attributes to an + * undefined (invalid) value. + */ static inline void audio_format_clear(struct audio_format *af) { af->sample_rate = 0; af->bits = 0; af->channels = 0; + af->reverse_endian = 0; +} + +/** + * Initializes an #audio_format object, i.e. sets all + * attributes to valid values. + */ +static inline void audio_format_init(struct audio_format *af, + uint32_t sample_rate, + uint8_t bits, uint8_t channels) +{ + af->sample_rate = sample_rate; + af->bits = bits; + af->channels = channels; + af->reverse_endian = 0; } +/** + * Checks whether the specified #audio_format object has a defined + * value. + */ static inline bool audio_format_defined(const struct audio_format *af) { return af->sample_rate != 0; } /** + * Checks whether the specified #audio_format object is full, i.e. all + * attributes are defined. This is more complete than + * audio_format_defined(), but slower. + */ +static inline bool +audio_format_fully_defined(const struct audio_format *af) +{ + return af->sample_rate != 0 && af->bits != 0 && af->channels != 0; +} + +/** + * Checks whether the specified #audio_format object has at least one + * defined value. + */ +static inline bool +audio_format_mask_defined(const struct audio_format *af) +{ + return af->sample_rate != 0 || af->bits != 0 || af->channels != 0; +} + +/** * Checks whether the sample rate is valid. * * @param sample_rate the sample rate in Hz @@ -83,12 +153,39 @@ static inline bool audio_format_valid(const struct audio_format *af) audio_valid_channel_count(af->channels); } +/** + * Returns false if the format mask is not valid for playback with + * MPD. This function performs some basic validity checks. + */ +static inline bool audio_format_mask_valid(const struct audio_format *af) +{ + return (af->sample_rate == 0 || + audio_valid_sample_rate(af->sample_rate)) && + (af->bits == 0 || audio_valid_sample_format(af->bits)) && + (af->channels == 0 || audio_valid_channel_count(af->channels)); +} + static inline bool audio_format_equals(const struct audio_format *a, const struct audio_format *b) { return a->sample_rate == b->sample_rate && a->bits == b->bits && - a->channels == b->channels; + a->channels == b->channels && + a->reverse_endian == b->reverse_endian; +} + +static inline void +audio_format_mask_apply(struct audio_format *af, + const struct audio_format *mask) +{ + if (mask->sample_rate != 0) + af->sample_rate = mask->sample_rate; + + if (mask->bits != 0) + af->bits = mask->bits; + + if (mask->channels != 0) + af->channels = mask->channels; } /** @@ -104,20 +201,22 @@ static inline unsigned audio_format_sample_size(const struct audio_format *af) return 4; } +/** + * Returns the size of each full frame in bytes. + */ static inline unsigned audio_format_frame_size(const struct audio_format *af) { return audio_format_sample_size(af) * af->channels; } +/** + * Returns the floating point factor which converts a time span to a + * storage size in bytes. + */ static inline double audio_format_time_to_size(const struct audio_format *af) { return af->sample_rate * audio_format_frame_size(af); } -static inline double audioFormatSizeToTime(const struct audio_format *af) -{ - return 1.0 / audio_format_time_to_size(af); -} - #endif diff --git a/src/audio_parser.c b/src/audio_parser.c index 906b0f819..7c0d45ddc 100644 --- a/src/audio_parser.c +++ b/src/audio_parser.c @@ -36,64 +36,136 @@ audio_parser_quark(void) return g_quark_from_static_string("audio_parser"); } -bool -audio_format_parse(struct audio_format *dest, const char *src, GError **error) +static bool +parse_sample_rate(const char *src, bool mask, uint32_t *sample_rate_r, + const char **endptr_r, GError **error_r) { - char *endptr; unsigned long value; + char *endptr; - audio_format_clear(dest); - - /* parse sample rate */ + if (mask && *src == '*') { + *sample_rate_r = 0; + *endptr_r = src + 1; + return true; + } value = strtoul(src, &endptr, 10); if (endptr == src) { - g_set_error(error, audio_parser_quark(), 0, - "Sample rate missing"); - return false; - } else if (*endptr != ':') { - g_set_error(error, audio_parser_quark(), 0, - "Sample format missing"); + g_set_error(error_r, audio_parser_quark(), 0, + "Failed to parse the sample rate"); return false; } else if (!audio_valid_sample_rate(value)) { - g_set_error(error, audio_parser_quark(), 0, + g_set_error(error_r, audio_parser_quark(), 0, "Invalid sample rate: %lu", value); return false; } - dest->sample_rate = value; + *sample_rate_r = value; + *endptr_r = endptr; + return true; +} - /* parse sample format */ +static bool +parse_sample_format(const char *src, bool mask, uint8_t *bits_r, + const char **endptr_r, GError **error_r) +{ + unsigned long value; + char *endptr; + + if (mask && *src == '*') { + *bits_r = 0; + *endptr_r = src + 1; + return true; + } - src = endptr + 1; value = strtoul(src, &endptr, 10); if (endptr == src) { - g_set_error(error, audio_parser_quark(), 0, - "Sample format missing"); - return false; - } else if (*endptr != ':') { - g_set_error(error, audio_parser_quark(), 0, - "Channel count missing"); + g_set_error(error_r, audio_parser_quark(), 0, + "Failed to parse the sample format"); return false; } else if (!audio_valid_sample_format(value)) { - g_set_error(error, audio_parser_quark(), 0, + g_set_error(error_r, audio_parser_quark(), 0, "Invalid sample format: %lu", value); return false; } - dest->bits = value; + *bits_r = value; + *endptr_r = endptr; + return true; +} - /* parse channel count */ +static bool +parse_channel_count(const char *src, bool mask, uint8_t *channels_r, + const char **endptr_r, GError **error_r) +{ + unsigned long value; + char *endptr; + + if (mask && *src == '*') { + *channels_r = 0; + *endptr_r = src + 1; + return true; + } - src = endptr + 1; value = strtoul(src, &endptr, 10); - if (*endptr != 0 || !audio_valid_channel_count(value)) { - g_set_error(error, audio_parser_quark(), 0, - "Invalid channel count: %s", src); + if (endptr == src) { + g_set_error(error_r, audio_parser_quark(), 0, + "Failed to parse the channel count"); + return false; + } else if (!audio_valid_channel_count(value)) { + g_set_error(error_r, audio_parser_quark(), 0, + "Invalid channel count: %lu", value); + return false; + } + + *channels_r = value; + *endptr_r = endptr; + return true; +} + +bool +audio_format_parse(struct audio_format *dest, const char *src, + bool mask, GError **error_r) +{ + uint32_t rate; + uint8_t bits, channels; + + audio_format_clear(dest); + + /* parse sample rate */ + + if (!parse_sample_rate(src, mask, &rate, &src, error_r)) + return false; + + if (*src++ != ':') { + g_set_error(error_r, audio_parser_quark(), 0, + "Sample format missing"); + return false; + } + + /* parse sample format */ + + if (!parse_sample_format(src, mask, &bits, &src, error_r)) + return false; + + if (*src++ != ':') { + g_set_error(error_r, audio_parser_quark(), 0, + "Channel count missing"); + return false; + } + + /* parse channel count */ + + if (!parse_channel_count(src, mask, &channels, &src, error_r)) + return false; + + if (*src != 0) { + g_set_error(error_r, audio_parser_quark(), 0, + "Extra data after channel count: %s", src); return false; } - dest->channels = value; + audio_format_init(dest, rate, bits, channels); return true; } diff --git a/src/audio_parser.h b/src/audio_parser.h index 30a927456..d50c17489 100644 --- a/src/audio_parser.h +++ b/src/audio_parser.h @@ -37,11 +37,13 @@ struct audio_format; * * @param dest the destination #audio_format struct * @param src the input string - * @param error location to store the error occuring, or NULL to + * @param mask if true, then "*" is allowed for any number of items + * @param error_r location to store the error occuring, or NULL to * ignore errors * @return true on success */ bool -audio_format_parse(struct audio_format *dest, const char *src, GError **error); +audio_format_parse(struct audio_format *dest, const char *src, + bool mask, GError **error_r); #endif diff --git a/src/buffer2array.c b/src/buffer2array.c deleted file mode 100644 index b6029d754..000000000 --- a/src/buffer2array.c +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 "buffer2array.h" - -#include <glib.h> - -#include <string.h> - -int buffer2array(char *buffer, char *array[], const int max) -{ - int i = 0; - char *c = buffer; - - while (*c != '\0' && i < max) { - if (*c == '\"') { - array[i++] = ++c; - while (*c != '\0') { - if (*c == '\"') { - *(c++) = '\0'; - break; - } - else if (*(c++) == '\\' && *c != '\0') { - memmove(c - 1, c, strlen(c) + 1); - } - } - } else { - c = g_strchug(c); - if (*c == '\0') - return i; - - array[i++] = c++; - - while (!g_ascii_isspace(*c) && *c != '\0') - ++c; - } - if (*c == '\0') - return i; - *(c++) = '\0'; - - c = g_strchug(c); - } - return i; -} - -#ifdef UNIT_TEST - -#include <stdio.h> -#include <string.h> -#include <assert.h> - -int main() -{ - char *a[4] = { NULL }; - char *b; - int max; - - b = strdup("lsinfo \"/some/dir/name \\\"test\\\"\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("/some/dir/name \"test\"", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"/some/dir/name \\\"test\\\" something else\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("/some/dir/name \"test\" something else", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"/some/dir\\\\name\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("/some/dir\\name", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"/some/dir name\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("/some/dir name", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"\\\"/some/dir\\\"\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("\"/some/dir\"", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"\\\"/some/dir\\\" x\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("\"/some/dir\" x", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"single quote\\'d from php magicquotes\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("single quote\'d from php magicquotes", a[1]) ); - assert( !a[2] ); - - b = strdup("lsinfo \"double quote\\\"d from php magicquotes\""); - assert(b); - max = buffer2array(b, a, 4); - assert( !strcmp("lsinfo", a[0]) ); - assert( !strcmp("double quote\"d from php magicquotes", a[1]) ); - assert( !a[2] ); - - return 0; -} - -#endif diff --git a/src/client.c b/src/client.c index 6a256998f..827f1d752 100644 --- a/src/client.c +++ b/src/client.c @@ -17,110 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "client.h" -#include "fifo_buffer.h" -#include "command.h" -#include "conf.h" -#include "listen.h" -#include "socket_util.h" -#include "permission.h" -#include "event_pipe.h" -#include "idle.h" -#include "main.h" -#include "config.h" - -#include <glib.h> -#include <assert.h> -#include <unistd.h> -#include <string.h> -#include <stdlib.h> -#include <stdio.h> -#include <errno.h> - -#ifdef WIN32 -#include <ws2tcpip.h> -#include <winsock.h> -#else -#include <sys/socket.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#endif - -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "client" -#define LOG_LEVEL_SECURE G_LOG_LEVEL_INFO - -static const char GREETING[] = "OK MPD " PROTOCOL_VERSION "\n"; - -#define CLIENT_LIST_MODE_BEGIN "command_list_begin" -#define CLIENT_LIST_OK_MODE_BEGIN "command_list_ok_begin" -#define CLIENT_LIST_MODE_END "command_list_end" -#define CLIENT_TIMEOUT_DEFAULT (60) -#define CLIENT_MAX_CONNECTIONS_DEFAULT (10) -#define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*1024) -#define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT (8192*1024) - -/* set this to zero to indicate we have no possible clients */ -static unsigned int client_max_connections; /*CLIENT_MAX_CONNECTIONS_DEFAULT; */ -static int client_timeout; -static size_t client_max_command_list_size; -static size_t client_max_output_buffer_size; - -struct deferred_buffer { - size_t size; - char data[sizeof(long)]; -}; - -struct client { - GIOChannel *channel; - guint source_id; - - /** the buffer for reading lines from the #channel */ - struct fifo_buffer *input; - - unsigned permission; - - /** the uid of the client process, or -1 if unknown */ - int uid; - - /** - * How long since the last activity from this client? - */ - GTimer *last_activity; - - GSList *cmd_list; /* for when in list mode */ - int cmd_list_OK; /* print OK after each command execution */ - size_t cmd_list_size; /* mem cmd_list consumes */ - GQueue *deferred_send; /* for output if client is slow */ - size_t deferred_bytes; /* mem deferred_send consumes */ - unsigned int num; /* client number */ - - char send_buf[4096]; - size_t send_buf_used; /* bytes used this instance */ - - /** is this client waiting for an "idle" response? */ - bool idle_waiting; - - /** idle flags pending on this client, to be sent as soon as - the client enters "idle" */ - unsigned idle_flags; - - /** idle flags that the client wants to receive */ - unsigned idle_subscriptions; -}; - -static GList *clients; -static unsigned num_clients; -static guint expire_source_id; - -static void client_write_deferred(struct client *client); - -static void client_write_output(struct client *client); - -static void client_manager_expire(void); - -static gboolean -client_in_event(GIOChannel *source, GIOCondition condition, gpointer data); +#include "client_internal.h" bool client_is_expired(const struct client *client) { @@ -141,782 +38,3 @@ void client_set_permission(struct client *client, unsigned permission) { client->permission = permission; } - -/** - * An idle event which calls client_manager_expire(). - */ -static gboolean -client_manager_expire_event(G_GNUC_UNUSED gpointer data) -{ - expire_source_id = 0; - client_manager_expire(); - return false; -} - -static inline void client_set_expired(struct client *client) -{ - if (expire_source_id == 0 && !client_is_expired(client)) - /* delayed deletion */ - expire_source_id = g_idle_add(client_manager_expire_event, - NULL); - - if (client->source_id != 0) { - g_source_remove(client->source_id); - client->source_id = 0; - } - - if (client->channel != NULL) { - g_io_channel_unref(client->channel); - client->channel = NULL; - } -} - -static void client_init(struct client *client, int fd) -{ - static unsigned int next_client_num; - - assert(fd >= 0); - - client->cmd_list_size = 0; - client->cmd_list_OK = -1; - -#ifndef G_OS_WIN32 - client->channel = g_io_channel_unix_new(fd); -#else - client->channel = g_io_channel_win32_new_socket(fd); -#endif - /* GLib is responsible for closing the file descriptor */ - g_io_channel_set_close_on_unref(client->channel, true); - /* NULL encoding means the stream is binary safe; the MPD - protocol is UTF-8 only, but we are doing this call anyway - to prevent GLib from messing around with the stream */ - g_io_channel_set_encoding(client->channel, NULL, NULL); - /* we prefer to do buffering */ - g_io_channel_set_buffered(client->channel, false); - - client->source_id = g_io_add_watch(client->channel, - G_IO_IN|G_IO_ERR|G_IO_HUP, - client_in_event, client); - - client->input = fifo_buffer_new(4096); - - client->cmd_list = NULL; - client->deferred_send = g_queue_new(); - client->deferred_bytes = 0; - client->num = next_client_num++; - client->send_buf_used = 0; - - client->permission = getDefaultPermissions(); - - (void)write(fd, GREETING, sizeof(GREETING) - 1); -} - -static void free_cmd_list(GSList *list) -{ - for (GSList *tmp = list; tmp != NULL; tmp = g_slist_next(tmp)) - g_free(tmp->data); - - g_slist_free(list); -} - -static void new_cmd_list_ptr(struct client *client, char *s) -{ - client->cmd_list = g_slist_prepend(client->cmd_list, g_strdup(s)); -} - -static void -deferred_buffer_free(gpointer data, G_GNUC_UNUSED gpointer user_data) -{ - struct deferred_buffer *buffer = data; - g_free(buffer); -} - -static void client_close(struct client *client) -{ - assert(num_clients > 0); - assert(clients != NULL); - - clients = g_list_remove(clients, client); - --num_clients; - - client_set_expired(client); - - g_timer_destroy(client->last_activity); - - if (client->cmd_list) { - free_cmd_list(client->cmd_list); - client->cmd_list = NULL; - } - - g_queue_foreach(client->deferred_send, deferred_buffer_free, NULL); - g_queue_free(client->deferred_send); - - fifo_buffer_free(client->input); - - g_log(G_LOG_DOMAIN, LOG_LEVEL_SECURE, - "[%u] closed", client->num); - g_free(client); -} - -void client_new(int fd, const struct sockaddr *sa, size_t sa_length, int uid) -{ - struct client *client; - char *remote; - - if (num_clients >= client_max_connections) { - g_warning("Max Connections Reached!"); - close(fd); - return; - } - - client = g_new0(struct client, 1); - clients = g_list_prepend(clients, client); - ++num_clients; - - client_init(client, fd); - client->uid = uid; - - client->last_activity = g_timer_new(); - - remote = sockaddr_to_string(sa, sa_length, NULL); - g_log(G_LOG_DOMAIN, LOG_LEVEL_SECURE, - "[%u] opened from %s", client->num, remote); - g_free(remote); -} - -static int client_process_line(struct client *client, char *line) -{ - int ret = 1; - - if (strcmp(line, "noidle") == 0) { - if (client->idle_waiting) { - /* send empty idle response and leave idle mode */ - client->idle_waiting = false; - command_success(client); - client_write_output(client); - } - - /* do nothing if the client wasn't idling: the client - has already received the full idle response from - client_idle_notify(), which he can now evaluate */ - - return 0; - } else if (client->idle_waiting) { - /* during idle mode, clients must not send anything - except "noidle" */ - g_warning("[%u] command \"%s\" during idle", - client->num, line); - return COMMAND_RETURN_CLOSE; - } - - if (client->cmd_list_OK >= 0) { - if (strcmp(line, CLIENT_LIST_MODE_END) == 0) { - g_debug("[%u] process command list", - client->num); - - /* for scalability reasons, we have prepended - each new command; now we have to reverse it - to restore the correct order */ - client->cmd_list = g_slist_reverse(client->cmd_list); - - ret = command_process_list(client, - client->cmd_list_OK, - client->cmd_list); - g_debug("[%u] process command " - "list returned %i", client->num, ret); - - if (ret == COMMAND_RETURN_CLOSE || - client_is_expired(client)) - return COMMAND_RETURN_CLOSE; - - if (ret == 0) - command_success(client); - - client_write_output(client); - free_cmd_list(client->cmd_list); - client->cmd_list = NULL; - client->cmd_list_OK = -1; - } else { - size_t len = strlen(line) + 1; - client->cmd_list_size += len; - if (client->cmd_list_size > - client_max_command_list_size) { - g_warning("[%u] command list size (%lu) " - "is larger than the max (%lu)", - client->num, - (unsigned long)client->cmd_list_size, - (unsigned long)client_max_command_list_size); - return COMMAND_RETURN_CLOSE; - } else - new_cmd_list_ptr(client, line); - } - } else { - if (strcmp(line, CLIENT_LIST_MODE_BEGIN) == 0) { - client->cmd_list_OK = 0; - ret = 1; - } else if (strcmp(line, CLIENT_LIST_OK_MODE_BEGIN) == 0) { - client->cmd_list_OK = 1; - ret = 1; - } else { - g_debug("[%u] process command \"%s\"", - client->num, line); - ret = command_process(client, line); - g_debug("[%u] command returned %i", - client->num, ret); - - if (ret == COMMAND_RETURN_CLOSE || - client_is_expired(client)) - return COMMAND_RETURN_CLOSE; - - if (ret == 0) - command_success(client); - - client_write_output(client); - } - } - - return ret; -} - -static char * -client_read_line(struct client *client) -{ - const char *p, *newline; - size_t length; - char *line; - - p = fifo_buffer_read(client->input, &length); - if (p == NULL) - return NULL; - - newline = memchr(p, '\n', length); - if (newline == NULL) - return NULL; - - line = g_strndup(p, newline - p); - fifo_buffer_consume(client->input, newline - p + 1); - - return g_strchomp(line); -} - -static int client_input_received(struct client *client, size_t bytesRead) -{ - char *line; - int ret; - - fifo_buffer_append(client->input, bytesRead); - - /* process all lines */ - - while ((line = client_read_line(client)) != NULL) { - ret = client_process_line(client, line); - g_free(line); - - if (ret == COMMAND_RETURN_KILL || - ret == COMMAND_RETURN_CLOSE) - return ret; - if (client_is_expired(client)) - return COMMAND_RETURN_CLOSE; - } - - return 0; -} - -static int client_read(struct client *client) -{ - char *p; - size_t max_length; - GError *error = NULL; - GIOStatus status; - gsize bytes_read; - - assert(client != NULL); - assert(client->channel != NULL); - - p = fifo_buffer_write(client->input, &max_length); - if (p == NULL) { - g_warning("[%u] buffer overflow", client->num); - return COMMAND_RETURN_CLOSE; - } - - status = g_io_channel_read_chars(client->channel, p, max_length, - &bytes_read, &error); - switch (status) { - case G_IO_STATUS_NORMAL: - return client_input_received(client, bytes_read); - - case G_IO_STATUS_AGAIN: - /* try again later, after select() */ - return 0; - - case G_IO_STATUS_EOF: - /* peer disconnected */ - return COMMAND_RETURN_CLOSE; - - case G_IO_STATUS_ERROR: - /* I/O error */ - g_warning("failed to read from client %d: %s", - client->num, error->message); - g_error_free(error); - return COMMAND_RETURN_CLOSE; - } - - /* unreachable */ - return COMMAND_RETURN_CLOSE; -} - -static gboolean -client_out_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition, - gpointer data); - -static gboolean -client_in_event(G_GNUC_UNUSED GIOChannel *source, - GIOCondition condition, - gpointer data) -{ - struct client *client = data; - int ret; - - assert(!client_is_expired(client)); - - if (condition != G_IO_IN) { - client_set_expired(client); - return false; - } - - g_timer_start(client->last_activity); - - ret = client_read(client); - switch (ret) { - case COMMAND_RETURN_KILL: - client_close(client); - g_main_loop_quit(main_loop); - return false; - - case COMMAND_RETURN_CLOSE: - client_close(client); - return false; - } - - if (client_is_expired(client)) { - client_close(client); - return false; - } - - if (!g_queue_is_empty(client->deferred_send)) { - /* deferred buffers exist: schedule write */ - client->source_id = g_io_add_watch(client->channel, - G_IO_OUT|G_IO_ERR|G_IO_HUP, - client_out_event, client); - return false; - } - - /* read more */ - return true; -} - -static gboolean -client_out_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition, - gpointer data) -{ - struct client *client = data; - - assert(!client_is_expired(client)); - - if (condition != G_IO_OUT) { - client_set_expired(client); - return false; - } - - client_write_deferred(client); - - if (client_is_expired(client)) { - client_close(client); - return false; - } - - g_timer_start(client->last_activity); - - if (g_queue_is_empty(client->deferred_send)) { - /* done sending deferred buffers exist: schedule - read */ - client->source_id = g_io_add_watch(client->channel, - G_IO_IN|G_IO_ERR|G_IO_HUP, - client_in_event, client); - return false; - } - - /* write more */ - return true; -} - -void client_manager_init(void) -{ - client_timeout = config_get_positive(CONF_CONN_TIMEOUT, - CLIENT_TIMEOUT_DEFAULT); - client_max_connections = - config_get_positive(CONF_MAX_CONN, - CLIENT_MAX_CONNECTIONS_DEFAULT); - client_max_command_list_size = - config_get_positive(CONF_MAX_COMMAND_LIST_SIZE, - CLIENT_MAX_COMMAND_LIST_DEFAULT / 1024) - * 1024; - - client_max_output_buffer_size = - config_get_positive(CONF_MAX_OUTPUT_BUFFER_SIZE, - CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT / 1024) - * 1024; -} - -static void client_close_all(void) -{ - while (clients != NULL) { - struct client *client = clients->data; - - client_close(client); - } - - assert(num_clients == 0); -} - -void client_manager_deinit(void) -{ - client_close_all(); - - client_max_connections = 0; - - if (expire_source_id != 0) - g_source_remove(expire_source_id); -} - -static void -client_check_expired_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) -{ - struct client *client = data; - - if (client_is_expired(client)) { - g_debug("[%u] expired", client->num); - client_close(client); - } else if (!client->idle_waiting && /* idle clients - never expire */ - (int)g_timer_elapsed(client->last_activity, NULL) > - client_timeout) { - g_debug("[%u] timeout", client->num); - client_close(client); - } -} - -static void -client_manager_expire(void) -{ - g_list_foreach(clients, client_check_expired_callback, NULL); -} - -static size_t -client_write_deferred_buffer(struct client *client, - const struct deferred_buffer *buffer) -{ - GError *error = NULL; - GIOStatus status; - gsize bytes_written; - - assert(client != NULL); - assert(client->channel != NULL); - assert(buffer != NULL); - - status = g_io_channel_write_chars - (client->channel, buffer->data, buffer->size, - &bytes_written, &error); - switch (status) { - case G_IO_STATUS_NORMAL: - return bytes_written; - - case G_IO_STATUS_AGAIN: - return 0; - - case G_IO_STATUS_EOF: - /* client has disconnected */ - - client_set_expired(client); - return 0; - - case G_IO_STATUS_ERROR: - /* I/O error */ - - client_set_expired(client); - g_warning("failed to flush buffer for %i: %s", - client->num, error->message); - g_error_free(error); - return 0; - } - - /* unreachable */ - return 0; -} - -static void client_write_deferred(struct client *client) -{ - size_t ret; - - while (!g_queue_is_empty(client->deferred_send)) { - struct deferred_buffer *buf = - g_queue_peek_head(client->deferred_send); - - assert(buf->size > 0); - assert(buf->size <= client->deferred_bytes); - - ret = client_write_deferred_buffer(client, buf); - if (ret == 0) - break; - - if (ret < buf->size) { - assert(client->deferred_bytes >= (size_t)ret); - client->deferred_bytes -= ret; - buf->size -= ret; - memmove(buf->data, buf->data + ret, buf->size); - break; - } else { - size_t decr = sizeof(*buf) - - sizeof(buf->data) + buf->size; - - assert(client->deferred_bytes >= decr); - client->deferred_bytes -= decr; - g_free(buf); - g_queue_pop_head(client->deferred_send); - } - - g_timer_start(client->last_activity); - } - - if (g_queue_is_empty(client->deferred_send)) { - g_debug("[%u] buffer empty %lu", client->num, - (unsigned long)client->deferred_bytes); - assert(client->deferred_bytes == 0); - } -} - -static void client_defer_output(struct client *client, - const void *data, size_t length) -{ - size_t alloc; - struct deferred_buffer *buf; - - assert(length > 0); - - alloc = sizeof(*buf) - sizeof(buf->data) + length; - client->deferred_bytes += alloc; - if (client->deferred_bytes > client_max_output_buffer_size) { - g_warning("[%u] output buffer size (%lu) is " - "larger than the max (%lu)", - client->num, - (unsigned long)client->deferred_bytes, - (unsigned long)client_max_output_buffer_size); - /* cause client to close */ - client_set_expired(client); - return; - } - - buf = g_malloc(alloc); - buf->size = length; - memcpy(buf->data, data, length); - - g_queue_push_tail(client->deferred_send, buf); -} - -static void client_write_direct(struct client *client, - const char *data, size_t length) -{ - GError *error = NULL; - GIOStatus status; - gsize bytes_written; - - assert(client != NULL); - assert(client->channel != NULL); - assert(data != NULL); - assert(length > 0); - assert(g_queue_is_empty(client->deferred_send)); - - status = g_io_channel_write_chars(client->channel, data, length, - &bytes_written, &error); - switch (status) { - case G_IO_STATUS_NORMAL: - case G_IO_STATUS_AGAIN: - break; - - case G_IO_STATUS_EOF: - /* client has disconnected */ - - client_set_expired(client); - return; - - case G_IO_STATUS_ERROR: - /* I/O error */ - - client_set_expired(client); - g_warning("failed to write to %i: %s", - client->num, error->message); - g_error_free(error); - return; - } - - if (bytes_written < length) - client_defer_output(client, data + bytes_written, - length - bytes_written); - - if (!g_queue_is_empty(client->deferred_send)) - g_debug("[%u] buffer created", client->num); -} - -static void client_write_output(struct client *client) -{ - if (client_is_expired(client) || !client->send_buf_used) - return; - - if (!g_queue_is_empty(client->deferred_send)) { - client_defer_output(client, client->send_buf, - client->send_buf_used); - - if (client_is_expired(client)) - return; - - /* try to flush the deferred buffers now; the current - server command may take too long to finish, and - meanwhile try to feed output to the client, - otherwise it will time out. One reason why - deferring is slow might be that currently each - client_write() allocates a new deferred buffer. - This should be optimized after MPD 0.14. */ - client_write_deferred(client); - } else - client_write_direct(client, client->send_buf, - client->send_buf_used); - - client->send_buf_used = 0; -} - -/** - * Write a block of data to the client. - */ -static void client_write(struct client *client, const char *buffer, size_t buflen) -{ - /* if the client is going to be closed, do nothing */ - if (client_is_expired(client)) - return; - - while (buflen > 0 && !client_is_expired(client)) { - size_t copylen; - - assert(client->send_buf_used < sizeof(client->send_buf)); - - copylen = sizeof(client->send_buf) - client->send_buf_used; - if (copylen > buflen) - copylen = buflen; - - memcpy(client->send_buf + client->send_buf_used, buffer, - copylen); - buflen -= copylen; - client->send_buf_used += copylen; - buffer += copylen; - if (client->send_buf_used >= sizeof(client->send_buf)) - client_write_output(client); - } -} - -void client_puts(struct client *client, const char *s) -{ - client_write(client, s, strlen(s)); -} - -void client_vprintf(struct client *client, const char *fmt, va_list args) -{ - va_list tmp; - int length; - char *buffer; - - va_copy(tmp, args); - length = vsnprintf(NULL, 0, fmt, tmp); - va_end(tmp); - - if (length <= 0) - /* wtf.. */ - return; - - buffer = g_malloc(length + 1); - vsnprintf(buffer, length + 1, fmt, args); - client_write(client, buffer, length); - g_free(buffer); -} - -G_GNUC_PRINTF(2, 3) void client_printf(struct client *client, const char *fmt, ...) -{ - va_list args; - - va_start(args, fmt); - client_vprintf(client, fmt, args); - va_end(args); -} - -/** - * Send "idle" response to this client. - */ -static void -client_idle_notify(struct client *client) -{ - unsigned flags, i; - const char *const* idle_names; - - assert(client->idle_waiting); - assert(client->idle_flags != 0); - - flags = client->idle_flags; - client->idle_flags = 0; - client->idle_waiting = false; - - idle_names = idle_get_names(); - for (i = 0; idle_names[i]; ++i) { - if (flags & (1 << i) & client->idle_subscriptions) - client_printf(client, "changed: %s\n", - idle_names[i]); - } - - client_puts(client, "OK\n"); - g_timer_start(client->last_activity); -} - -static void -client_idle_callback(gpointer data, gpointer user_data) -{ - struct client *client = data; - unsigned flags = GPOINTER_TO_UINT(user_data); - - if (client_is_expired(client)) - return; - - client->idle_flags |= flags; - if (client->idle_waiting - && (client->idle_flags & client->idle_subscriptions)) { - client_idle_notify(client); - client_write_output(client); - } -} - -void client_manager_idle_add(unsigned flags) -{ - assert(flags != 0); - - g_list_foreach(clients, client_idle_callback, GUINT_TO_POINTER(flags)); -} - -bool client_idle_wait(struct client *client, unsigned flags) -{ - assert(!client->idle_waiting); - - client->idle_waiting = true; - client->idle_subscriptions = flags; - - if (client->idle_flags & client->idle_subscriptions) { - client_idle_notify(client); - return true; - } else - return false; -} diff --git a/src/client_event.c b/src/client_event.c new file mode 100644 index 000000000..93279b283 --- /dev/null +++ b/src/client_event.c @@ -0,0 +1,107 @@ +/* + * 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 "client_internal.h" +#include "main.h" + +#include <assert.h> + +static gboolean +client_out_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition, + gpointer data) +{ + struct client *client = data; + + assert(!client_is_expired(client)); + + if (condition != G_IO_OUT) { + client_set_expired(client); + return false; + } + + client_write_deferred(client); + + if (client_is_expired(client)) { + client_close(client); + return false; + } + + g_timer_start(client->last_activity); + + if (g_queue_is_empty(client->deferred_send)) { + /* done sending deferred buffers exist: schedule + read */ + client->source_id = g_io_add_watch(client->channel, + G_IO_IN|G_IO_ERR|G_IO_HUP, + client_in_event, client); + return false; + } + + /* write more */ + return true; +} + +gboolean +client_in_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition, + gpointer data) +{ + struct client *client = data; + enum command_return ret; + + assert(!client_is_expired(client)); + + if (condition != G_IO_IN) { + client_set_expired(client); + return false; + } + + g_timer_start(client->last_activity); + + ret = client_read(client); + switch (ret) { + case COMMAND_RETURN_OK: + case COMMAND_RETURN_ERROR: + break; + + case COMMAND_RETURN_KILL: + client_close(client); + g_main_loop_quit(main_loop); + return false; + + case COMMAND_RETURN_CLOSE: + client_close(client); + return false; + } + + if (client_is_expired(client)) { + client_close(client); + return false; + } + + if (!g_queue_is_empty(client->deferred_send)) { + /* deferred buffers exist: schedule write */ + client->source_id = g_io_add_watch(client->channel, + G_IO_OUT|G_IO_ERR|G_IO_HUP, + client_out_event, client); + return false; + } + + /* read more */ + return true; +} diff --git a/src/client_expire.c b/src/client_expire.c new file mode 100644 index 000000000..372af1774 --- /dev/null +++ b/src/client_expire.c @@ -0,0 +1,89 @@ +/* + * 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 "client_internal.h" + +static guint expire_source_id; + +void +client_set_expired(struct client *client) +{ + if (!client_is_expired(client)) + client_schedule_expire(); + + if (client->source_id != 0) { + g_source_remove(client->source_id); + client->source_id = 0; + } + + if (client->channel != NULL) { + g_io_channel_unref(client->channel); + client->channel = NULL; + } +} + +static void +client_check_expired_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct client *client = data; + + if (client_is_expired(client)) { + g_debug("[%u] expired", client->num); + client_close(client); + } else if (!client->idle_waiting && /* idle clients + never expire */ + (int)g_timer_elapsed(client->last_activity, NULL) > + client_timeout) { + g_debug("[%u] timeout", client->num); + client_close(client); + } +} + +static void +client_manager_expire(void) +{ + client_list_foreach(client_check_expired_callback, NULL); +} + +/** + * An idle event which calls client_manager_expire(). + */ +static gboolean +client_manager_expire_event(G_GNUC_UNUSED gpointer data) +{ + expire_source_id = 0; + client_manager_expire(); + return false; +} + +void +client_schedule_expire(void) +{ + if (expire_source_id == 0) + /* delayed deletion */ + expire_source_id = g_idle_add(client_manager_expire_event, + NULL); +} + +void +client_deinit_expire(void) +{ + if (expire_source_id != 0) + g_source_remove(expire_source_id); +} diff --git a/src/client_global.c b/src/client_global.c new file mode 100644 index 000000000..d99e00b82 --- /dev/null +++ b/src/client_global.c @@ -0,0 +1,72 @@ +/* + * 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 "client_internal.h" +#include "conf.h" + +#include <assert.h> + +#define CLIENT_TIMEOUT_DEFAULT (60) +#define CLIENT_MAX_CONNECTIONS_DEFAULT (10) +#define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*1024) +#define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT (8192*1024) + +/* set this to zero to indicate we have no possible clients */ +unsigned int client_max_connections; +int client_timeout; +size_t client_max_command_list_size; +size_t client_max_output_buffer_size; + +void client_manager_init(void) +{ + client_timeout = config_get_positive(CONF_CONN_TIMEOUT, + CLIENT_TIMEOUT_DEFAULT); + client_max_connections = + config_get_positive(CONF_MAX_CONN, + CLIENT_MAX_CONNECTIONS_DEFAULT); + client_max_command_list_size = + config_get_positive(CONF_MAX_COMMAND_LIST_SIZE, + CLIENT_MAX_COMMAND_LIST_DEFAULT / 1024) + * 1024; + + client_max_output_buffer_size = + config_get_positive(CONF_MAX_OUTPUT_BUFFER_SIZE, + CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT / 1024) + * 1024; +} + +static void client_close_all(void) +{ + while (!client_list_is_empty()) { + struct client *client = client_list_get_first(); + + client_close(client); + } + + assert(client_list_is_empty()); +} + +void client_manager_deinit(void) +{ + client_close_all(); + + client_max_connections = 0; + + client_deinit_expire(); +} diff --git a/src/client_idle.c b/src/client_idle.c new file mode 100644 index 000000000..c6d0bc006 --- /dev/null +++ b/src/client_idle.c @@ -0,0 +1,88 @@ +/* + * 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 "client_internal.h" +#include "idle.h" + +#include <assert.h> + +/** + * Send "idle" response to this client. + */ +static void +client_idle_notify(struct client *client) +{ + unsigned flags, i; + const char *const* idle_names; + + assert(client->idle_waiting); + assert(client->idle_flags != 0); + + flags = client->idle_flags; + client->idle_flags = 0; + client->idle_waiting = false; + + idle_names = idle_get_names(); + for (i = 0; idle_names[i]; ++i) { + if (flags & (1 << i) & client->idle_subscriptions) + client_printf(client, "changed: %s\n", + idle_names[i]); + } + + client_puts(client, "OK\n"); + g_timer_start(client->last_activity); +} + +static void +client_idle_callback(gpointer data, gpointer user_data) +{ + struct client *client = data; + unsigned flags = GPOINTER_TO_UINT(user_data); + + if (client_is_expired(client)) + return; + + client->idle_flags |= flags; + if (client->idle_waiting + && (client->idle_flags & client->idle_subscriptions)) { + client_idle_notify(client); + client_write_output(client); + } +} + +void client_manager_idle_add(unsigned flags) +{ + assert(flags != 0); + + client_list_foreach(client_idle_callback, GUINT_TO_POINTER(flags)); +} + +bool client_idle_wait(struct client *client, unsigned flags) +{ + assert(!client->idle_waiting); + + client->idle_waiting = true; + client->idle_subscriptions = flags; + + if (client->idle_flags & client->idle_subscriptions) { + client_idle_notify(client); + return true; + } else + return false; +} diff --git a/src/client_internal.h b/src/client_internal.h new file mode 100644 index 000000000..91e360fe0 --- /dev/null +++ b/src/client_internal.h @@ -0,0 +1,145 @@ +/* + * 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. + */ + +#ifndef MPD_CLIENT_INTERNAL_H +#define MPD_CLIENT_INTERNAL_H + +#include "client.h" +#include "command.h" + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "client" + +struct deferred_buffer { + size_t size; + char data[sizeof(long)]; +}; + +struct client { + GIOChannel *channel; + guint source_id; + + /** the buffer for reading lines from the #channel */ + struct fifo_buffer *input; + + unsigned permission; + + /** the uid of the client process, or -1 if unknown */ + int uid; + + /** + * How long since the last activity from this client? + */ + GTimer *last_activity; + + GSList *cmd_list; /* for when in list mode */ + int cmd_list_OK; /* print OK after each command execution */ + size_t cmd_list_size; /* mem cmd_list consumes */ + GQueue *deferred_send; /* for output if client is slow */ + size_t deferred_bytes; /* mem deferred_send consumes */ + unsigned int num; /* client number */ + + char send_buf[4096]; + size_t send_buf_used; /* bytes used this instance */ + + /** is this client waiting for an "idle" response? */ + bool idle_waiting; + + /** idle flags pending on this client, to be sent as soon as + the client enters "idle" */ + unsigned idle_flags; + + /** idle flags that the client wants to receive */ + unsigned idle_subscriptions; +}; + +extern unsigned int client_max_connections; +extern int client_timeout; +extern size_t client_max_command_list_size; +extern size_t client_max_output_buffer_size; + +bool +client_list_is_empty(void); + +bool +client_list_is_full(void); + +struct client * +client_list_get_first(void); + +void +client_list_add(struct client *client); + +void +client_list_foreach(GFunc func, gpointer user_data); + +void +client_list_remove(struct client *client); + +void +client_close(struct client *client); + +static inline void +new_cmd_list_ptr(struct client *client, const char *s) +{ + client->cmd_list = g_slist_prepend(client->cmd_list, g_strdup(s)); +} + +static inline void +free_cmd_list(GSList *list) +{ + for (GSList *tmp = list; tmp != NULL; tmp = g_slist_next(tmp)) + g_free(tmp->data); + + g_slist_free(list); +} + +void +client_set_expired(struct client *client); + +/** + * Schedule an "expired" check for all clients: permanently delete + * clients which have been set "expired" with client_set_expired(). + */ +void +client_schedule_expire(void); + +/** + * Removes a scheduled "expired" check. + */ +void +client_deinit_expire(void); + +enum command_return +client_read(struct client *client); + +enum command_return +client_process_line(struct client *client, char *line); + +void +client_write_deferred(struct client *client); + +void +client_write_output(struct client *client); + +gboolean +client_in_event(GIOChannel *source, GIOCondition condition, + gpointer data); + +#endif diff --git a/src/client_list.c b/src/client_list.c new file mode 100644 index 000000000..80eb7a29d --- /dev/null +++ b/src/client_list.c @@ -0,0 +1,68 @@ +/* + * 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 "client_internal.h" + +#include <assert.h> + +static GList *clients; +static unsigned num_clients; + +bool +client_list_is_empty(void) +{ + return num_clients == 0; +} + +bool +client_list_is_full(void) +{ + return num_clients >= client_max_connections; +} + +struct client * +client_list_get_first(void) +{ + assert(clients != NULL); + + return clients->data; +} + +void +client_list_add(struct client *client) +{ + clients = g_list_prepend(clients, client); + ++num_clients; +} + +void +client_list_foreach(GFunc func, gpointer user_data) +{ + g_list_foreach(clients, func, user_data); +} + +void +client_list_remove(struct client *client) +{ + assert(num_clients > 0); + assert(clients != NULL); + + clients = g_list_remove(clients, client); + --num_clients; +} diff --git a/src/client_new.c b/src/client_new.c new file mode 100644 index 000000000..bc7ee2140 --- /dev/null +++ b/src/client_new.c @@ -0,0 +1,123 @@ +/* + * 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 "client_internal.h" +#include "fifo_buffer.h" +#include "socket_util.h" +#include "permission.h" +#include "config.h" + +#include <assert.h> +#include <unistd.h> + +#define LOG_LEVEL_SECURE G_LOG_LEVEL_INFO + +static const char GREETING[] = "OK MPD " PROTOCOL_VERSION "\n"; + +void client_new(int fd, const struct sockaddr *sa, size_t sa_length, int uid) +{ + static unsigned int next_client_num; + struct client *client; + char *remote; + + assert(fd >= 0); + + if (client_list_is_full()) { + g_warning("Max Connections Reached!"); + close(fd); + return; + } + + client = g_new0(struct client, 1); + +#ifndef G_OS_WIN32 + client->channel = g_io_channel_unix_new(fd); +#else + client->channel = g_io_channel_win32_new_socket(fd); +#endif + /* GLib is responsible for closing the file descriptor */ + g_io_channel_set_close_on_unref(client->channel, true); + /* NULL encoding means the stream is binary safe; the MPD + protocol is UTF-8 only, but we are doing this call anyway + to prevent GLib from messing around with the stream */ + g_io_channel_set_encoding(client->channel, NULL, NULL); + /* we prefer to do buffering */ + g_io_channel_set_buffered(client->channel, false); + + client->source_id = g_io_add_watch(client->channel, + G_IO_IN|G_IO_ERR|G_IO_HUP, + client_in_event, client); + + client->input = fifo_buffer_new(4096); + + client->permission = getDefaultPermissions(); + client->uid = uid; + + client->last_activity = g_timer_new(); + + client->cmd_list = NULL; + client->cmd_list_OK = -1; + client->cmd_list_size = 0; + + client->deferred_send = g_queue_new(); + client->deferred_bytes = 0; + client->num = next_client_num++; + + client->send_buf_used = 0; + + (void)write(fd, GREETING, sizeof(GREETING) - 1); + + client_list_add(client); + + remote = sockaddr_to_string(sa, sa_length, NULL); + g_log(G_LOG_DOMAIN, LOG_LEVEL_SECURE, + "[%u] opened from %s", client->num, remote); + g_free(remote); +} + +static void +deferred_buffer_free(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct deferred_buffer *buffer = data; + g_free(buffer); +} + +void +client_close(struct client *client) +{ + client_list_remove(client); + + client_set_expired(client); + + g_timer_destroy(client->last_activity); + + if (client->cmd_list) { + free_cmd_list(client->cmd_list); + client->cmd_list = NULL; + } + + g_queue_foreach(client->deferred_send, deferred_buffer_free, NULL); + g_queue_free(client->deferred_send); + + fifo_buffer_free(client->input); + + g_log(G_LOG_DOMAIN, LOG_LEVEL_SECURE, + "[%u] closed", client->num); + g_free(client); +} diff --git a/src/client_process.c b/src/client_process.c new file mode 100644 index 000000000..2f69cc6a8 --- /dev/null +++ b/src/client_process.c @@ -0,0 +1,145 @@ +/* + * 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 "client_internal.h" + +#include <string.h> + +#define CLIENT_LIST_MODE_BEGIN "command_list_begin" +#define CLIENT_LIST_OK_MODE_BEGIN "command_list_ok_begin" +#define CLIENT_LIST_MODE_END "command_list_end" + +static enum command_return +client_process_command_list(struct client *client, bool list_ok, GSList *list) +{ + enum command_return ret = COMMAND_RETURN_OK; + unsigned num = 0; + + for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) { + char *cmd = cur->data; + + g_debug("command_process_list: process command \"%s\"", + cmd); + ret = command_process(client, num++, cmd); + g_debug("command_process_list: command returned %i", ret); + if (ret != COMMAND_RETURN_OK || client_is_expired(client)) + break; + else if (list_ok) + client_puts(client, "list_OK\n"); + } + + return ret; +} + +enum command_return +client_process_line(struct client *client, char *line) +{ + enum command_return ret; + + if (strcmp(line, "noidle") == 0) { + if (client->idle_waiting) { + /* send empty idle response and leave idle mode */ + client->idle_waiting = false; + command_success(client); + client_write_output(client); + } + + /* do nothing if the client wasn't idling: the client + has already received the full idle response from + client_idle_notify(), which he can now evaluate */ + + return COMMAND_RETURN_OK; + } else if (client->idle_waiting) { + /* during idle mode, clients must not send anything + except "noidle" */ + g_warning("[%u] command \"%s\" during idle", + client->num, line); + return COMMAND_RETURN_CLOSE; + } + + if (client->cmd_list_OK >= 0) { + if (strcmp(line, CLIENT_LIST_MODE_END) == 0) { + g_debug("[%u] process command list", + client->num); + + /* for scalability reasons, we have prepended + each new command; now we have to reverse it + to restore the correct order */ + client->cmd_list = g_slist_reverse(client->cmd_list); + + ret = client_process_command_list(client, + client->cmd_list_OK, + client->cmd_list); + g_debug("[%u] process command " + "list returned %i", client->num, ret); + + if (ret == COMMAND_RETURN_CLOSE || + client_is_expired(client)) + return COMMAND_RETURN_CLOSE; + + if (ret == COMMAND_RETURN_OK) + command_success(client); + + client_write_output(client); + free_cmd_list(client->cmd_list); + client->cmd_list = NULL; + client->cmd_list_OK = -1; + } else { + size_t len = strlen(line) + 1; + client->cmd_list_size += len; + if (client->cmd_list_size > + client_max_command_list_size) { + g_warning("[%u] command list size (%lu) " + "is larger than the max (%lu)", + client->num, + (unsigned long)client->cmd_list_size, + (unsigned long)client_max_command_list_size); + return COMMAND_RETURN_CLOSE; + } + + new_cmd_list_ptr(client, line); + ret = COMMAND_RETURN_OK; + } + } else { + if (strcmp(line, CLIENT_LIST_MODE_BEGIN) == 0) { + client->cmd_list_OK = 0; + ret = COMMAND_RETURN_OK; + } else if (strcmp(line, CLIENT_LIST_OK_MODE_BEGIN) == 0) { + client->cmd_list_OK = 1; + ret = COMMAND_RETURN_OK; + } else { + g_debug("[%u] process command \"%s\"", + client->num, line); + ret = command_process(client, 0, line); + g_debug("[%u] command returned %i", + client->num, ret); + + if (ret == COMMAND_RETURN_CLOSE || + client_is_expired(client)) + return COMMAND_RETURN_CLOSE; + + if (ret == COMMAND_RETURN_OK) + command_success(client); + + client_write_output(client); + } + } + + return ret; +} diff --git a/src/client_read.c b/src/client_read.c new file mode 100644 index 000000000..1d64035e3 --- /dev/null +++ b/src/client_read.c @@ -0,0 +1,112 @@ +/* + * 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 "client_internal.h" +#include "fifo_buffer.h" + +#include <assert.h> +#include <string.h> + +static char * +client_read_line(struct client *client) +{ + const char *p, *newline; + size_t length; + char *line; + + p = fifo_buffer_read(client->input, &length); + if (p == NULL) + return NULL; + + newline = memchr(p, '\n', length); + if (newline == NULL) + return NULL; + + line = g_strndup(p, newline - p); + fifo_buffer_consume(client->input, newline - p + 1); + + return g_strchomp(line); +} + +static enum command_return +client_input_received(struct client *client, size_t bytesRead) +{ + char *line; + + fifo_buffer_append(client->input, bytesRead); + + /* process all lines */ + + while ((line = client_read_line(client)) != NULL) { + enum command_return ret = client_process_line(client, line); + g_free(line); + + if (ret == COMMAND_RETURN_KILL || + ret == COMMAND_RETURN_CLOSE) + return ret; + if (client_is_expired(client)) + return COMMAND_RETURN_CLOSE; + } + + return COMMAND_RETURN_OK; +} + +enum command_return +client_read(struct client *client) +{ + char *p; + size_t max_length; + GError *error = NULL; + GIOStatus status; + gsize bytes_read; + + assert(client != NULL); + assert(client->channel != NULL); + + p = fifo_buffer_write(client->input, &max_length); + if (p == NULL) { + g_warning("[%u] buffer overflow", client->num); + return COMMAND_RETURN_CLOSE; + } + + status = g_io_channel_read_chars(client->channel, p, max_length, + &bytes_read, &error); + switch (status) { + case G_IO_STATUS_NORMAL: + return client_input_received(client, bytes_read); + + case G_IO_STATUS_AGAIN: + /* try again later, after select() */ + return COMMAND_RETURN_OK; + + case G_IO_STATUS_EOF: + /* peer disconnected */ + return COMMAND_RETURN_CLOSE; + + case G_IO_STATUS_ERROR: + /* I/O error */ + g_warning("failed to read from client %d: %s", + client->num, error->message); + g_error_free(error); + return COMMAND_RETURN_CLOSE; + } + + /* unreachable */ + return COMMAND_RETURN_CLOSE; +} diff --git a/src/client_write.c b/src/client_write.c new file mode 100644 index 000000000..686c7d96a --- /dev/null +++ b/src/client_write.c @@ -0,0 +1,270 @@ +/* + * 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 "client_internal.h" + +#include <assert.h> +#include <string.h> +#include <stdio.h> + +static size_t +client_write_deferred_buffer(struct client *client, + const struct deferred_buffer *buffer) +{ + GError *error = NULL; + GIOStatus status; + gsize bytes_written; + + assert(client != NULL); + assert(client->channel != NULL); + assert(buffer != NULL); + + status = g_io_channel_write_chars + (client->channel, buffer->data, buffer->size, + &bytes_written, &error); + switch (status) { + case G_IO_STATUS_NORMAL: + return bytes_written; + + case G_IO_STATUS_AGAIN: + return 0; + + case G_IO_STATUS_EOF: + /* client has disconnected */ + + client_set_expired(client); + return 0; + + case G_IO_STATUS_ERROR: + /* I/O error */ + + client_set_expired(client); + g_warning("failed to flush buffer for %i: %s", + client->num, error->message); + g_error_free(error); + return 0; + } + + /* unreachable */ + return 0; +} + +void +client_write_deferred(struct client *client) +{ + size_t ret; + + while (!g_queue_is_empty(client->deferred_send)) { + struct deferred_buffer *buf = + g_queue_peek_head(client->deferred_send); + + assert(buf->size > 0); + assert(buf->size <= client->deferred_bytes); + + ret = client_write_deferred_buffer(client, buf); + if (ret == 0) + break; + + if (ret < buf->size) { + assert(client->deferred_bytes >= (size_t)ret); + client->deferred_bytes -= ret; + buf->size -= ret; + memmove(buf->data, buf->data + ret, buf->size); + break; + } else { + size_t decr = sizeof(*buf) - + sizeof(buf->data) + buf->size; + + assert(client->deferred_bytes >= decr); + client->deferred_bytes -= decr; + g_free(buf); + g_queue_pop_head(client->deferred_send); + } + + g_timer_start(client->last_activity); + } + + if (g_queue_is_empty(client->deferred_send)) { + g_debug("[%u] buffer empty %lu", client->num, + (unsigned long)client->deferred_bytes); + assert(client->deferred_bytes == 0); + } +} + +static void client_defer_output(struct client *client, + const void *data, size_t length) +{ + size_t alloc; + struct deferred_buffer *buf; + + assert(length > 0); + + alloc = sizeof(*buf) - sizeof(buf->data) + length; + client->deferred_bytes += alloc; + if (client->deferred_bytes > client_max_output_buffer_size) { + g_warning("[%u] output buffer size (%lu) is " + "larger than the max (%lu)", + client->num, + (unsigned long)client->deferred_bytes, + (unsigned long)client_max_output_buffer_size); + /* cause client to close */ + client_set_expired(client); + return; + } + + buf = g_malloc(alloc); + buf->size = length; + memcpy(buf->data, data, length); + + g_queue_push_tail(client->deferred_send, buf); +} + +static void client_write_direct(struct client *client, + const char *data, size_t length) +{ + GError *error = NULL; + GIOStatus status; + gsize bytes_written; + + assert(client != NULL); + assert(client->channel != NULL); + assert(data != NULL); + assert(length > 0); + assert(g_queue_is_empty(client->deferred_send)); + + status = g_io_channel_write_chars(client->channel, data, length, + &bytes_written, &error); + switch (status) { + case G_IO_STATUS_NORMAL: + case G_IO_STATUS_AGAIN: + break; + + case G_IO_STATUS_EOF: + /* client has disconnected */ + + client_set_expired(client); + return; + + case G_IO_STATUS_ERROR: + /* I/O error */ + + client_set_expired(client); + g_warning("failed to write to %i: %s", + client->num, error->message); + g_error_free(error); + return; + } + + if (bytes_written < length) + client_defer_output(client, data + bytes_written, + length - bytes_written); + + if (!g_queue_is_empty(client->deferred_send)) + g_debug("[%u] buffer created", client->num); +} + +void +client_write_output(struct client *client) +{ + if (client_is_expired(client) || !client->send_buf_used) + return; + + if (!g_queue_is_empty(client->deferred_send)) { + client_defer_output(client, client->send_buf, + client->send_buf_used); + + if (client_is_expired(client)) + return; + + /* try to flush the deferred buffers now; the current + server command may take too long to finish, and + meanwhile try to feed output to the client, + otherwise it will time out. One reason why + deferring is slow might be that currently each + client_write() allocates a new deferred buffer. + This should be optimized after MPD 0.14. */ + client_write_deferred(client); + } else + client_write_direct(client, client->send_buf, + client->send_buf_used); + + client->send_buf_used = 0; +} + +/** + * Write a block of data to the client. + */ +static void client_write(struct client *client, const char *buffer, size_t buflen) +{ + /* if the client is going to be closed, do nothing */ + if (client_is_expired(client)) + return; + + while (buflen > 0 && !client_is_expired(client)) { + size_t copylen; + + assert(client->send_buf_used < sizeof(client->send_buf)); + + copylen = sizeof(client->send_buf) - client->send_buf_used; + if (copylen > buflen) + copylen = buflen; + + memcpy(client->send_buf + client->send_buf_used, buffer, + copylen); + buflen -= copylen; + client->send_buf_used += copylen; + buffer += copylen; + if (client->send_buf_used >= sizeof(client->send_buf)) + client_write_output(client); + } +} + +void client_puts(struct client *client, const char *s) +{ + client_write(client, s, strlen(s)); +} + +void client_vprintf(struct client *client, const char *fmt, va_list args) +{ + va_list tmp; + int length; + char *buffer; + + va_copy(tmp, args); + length = vsnprintf(NULL, 0, fmt, tmp); + va_end(tmp); + + if (length <= 0) + /* wtf.. */ + return; + + buffer = g_malloc(length + 1); + vsnprintf(buffer, length + 1, fmt, args); + client_write(client, buffer, length); + g_free(buffer); +} + +G_GNUC_PRINTF(2, 3) void client_printf(struct client *client, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + client_vprintf(client, fmt, args); + va_end(args); +} diff --git a/src/cmdline.c b/src/cmdline.c index e0274ef36..5dd6b26d6 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -35,10 +35,15 @@ #include <stdio.h> #include <stdlib.h> -#define SYSTEM_CONFIG_FILE_LOCATION "/etc/mpd.conf" #define USER_CONFIG_FILE_LOCATION1 ".mpdconf" #define USER_CONFIG_FILE_LOCATION2 ".mpd/mpd.conf" +static GQuark +cmdline_quark(void) +{ + return g_quark_from_static_string("cmdline"); +} + G_GNUC_NORETURN static void version(void) { @@ -72,31 +77,29 @@ static void version(void) exit(EXIT_SUCCESS); } -#if GLIB_CHECK_VERSION(2,12,0) static const char *summary = "Music Player Daemon - a daemon for playing music."; -#endif -void parseOptions(int argc, char **argv, Options *options) +bool +parse_cmdline(int argc, char **argv, struct options *options, + GError **error_r) { GError *error = NULL; GOptionContext *context; bool ret; static gboolean option_version, - option_create_db, option_no_create_db, option_no_daemon, + option_no_daemon, option_no_config; const GOptionEntry entries[] = { - { "create-db", 0, 0, G_OPTION_ARG_NONE, &option_create_db, - "force (re)creation of database", NULL }, { "kill", 0, 0, G_OPTION_ARG_NONE, &options->kill, "kill the currently running mpd session", NULL }, { "no-config", 0, 0, G_OPTION_ARG_NONE, &option_no_config, "don't read from config", NULL }, - { "no-create-db", 0, 0, G_OPTION_ARG_NONE, &option_no_create_db, - "don't create database, even if it doesn't exist", NULL }, { "no-daemon", 0, 0, G_OPTION_ARG_NONE, &option_no_daemon, "don't detach from console", NULL }, - { "stdout", 0, 0, G_OPTION_ARG_NONE, &options->stdOutput, + { "stdout", 0, 0, G_OPTION_ARG_NONE, &options->log_stderr, + NULL, NULL }, + { "stderr", 0, 0, G_OPTION_ARG_NONE, &options->log_stderr, "print messages to stderr", NULL }, { "verbose", 'v', 0, G_OPTION_ARG_NONE, &options->verbose, "verbose logging", NULL }, @@ -107,16 +110,13 @@ void parseOptions(int argc, char **argv, Options *options) options->kill = false; options->daemon = true; - options->stdOutput = false; + options->log_stderr = false; options->verbose = false; - options->createDB = 0; context = g_option_context_new("[path/to/mpd.conf]"); g_option_context_add_main_entries(context, entries, NULL); -#if GLIB_CHECK_VERSION(2,12,0) g_option_context_set_summary(context, summary); -#endif ret = g_option_context_parse(context, &argc, &argv, &error); g_option_context_free(context); @@ -133,18 +133,11 @@ void parseOptions(int argc, char **argv, Options *options) parser can use it already */ log_early_init(options->verbose); - if (option_create_db && option_no_create_db) - g_error("Cannot use both --create-db and --no-create-db\n"); - - if (option_no_create_db) - options->createDB = -1; - else if (option_create_db) - options->createDB = 1; - options->daemon = !option_no_daemon; if (option_no_config) { g_debug("Ignoring config, using daemon defaults\n"); + return true; } else if (argc <= 1) { /* default configuration file path */ char *path1; @@ -155,17 +148,23 @@ void parseOptions(int argc, char **argv, Options *options) path2 = g_build_filename(g_get_home_dir(), USER_CONFIG_FILE_LOCATION2, NULL); if (g_file_test(path1, G_FILE_TEST_IS_REGULAR)) - config_read_file(path1); + ret = config_read_file(path1, error_r); else if (g_file_test(path2, G_FILE_TEST_IS_REGULAR)) - config_read_file(path2); + ret = config_read_file(path2, error_r); else if (g_file_test(SYSTEM_CONFIG_FILE_LOCATION, G_FILE_TEST_IS_REGULAR)) - config_read_file(SYSTEM_CONFIG_FILE_LOCATION); + ret = config_read_file(SYSTEM_CONFIG_FILE_LOCATION, + error_r); g_free(path1); g_free(path2); + + return ret; } else if (argc == 2) { /* specified configuration file */ - config_read_file(argv[1]); - } else - g_error("too many arguments"); + return config_read_file(argv[1], error_r); + } else { + g_set_error(error_r, cmdline_quark(), 0, + "too many arguments"); + return false; + } } diff --git a/src/cmdline.h b/src/cmdline.h index 673701055..945868b8c 100644 --- a/src/cmdline.h +++ b/src/cmdline.h @@ -22,14 +22,17 @@ #include <glib.h> -typedef struct _Options { +#include <stdbool.h> + +struct options { gboolean kill; gboolean daemon; - gboolean stdOutput; + gboolean log_stderr; gboolean verbose; - int createDB; -} Options; +}; -void parseOptions(int argc, char **argv, Options *options); +bool +parse_cmdline(int argc, char **argv, struct options *options, + GError **error_r); #endif diff --git a/src/command.c b/src/command.c index d30b63594..38673f008 100644 --- a/src/command.c +++ b/src/command.c @@ -22,6 +22,7 @@ #include "playlist.h" #include "playlist_print.h" #include "playlist_save.h" +#include "playlist_queue.h" #include "queue_print.h" #include "ls.h" #include "uri.h" @@ -32,7 +33,7 @@ #include "volume.h" #include "stats.h" #include "permission.h" -#include "buffer2array.h" +#include "tokenizer.h" #include "stored_playlist.h" #include "ack.h" #include "output_command.h" @@ -43,6 +44,7 @@ #include "client.h" #include "tag_print.h" #include "path.h" +#include "replay_gain.h" #include "idle.h" #include "config.h" @@ -58,7 +60,6 @@ #include <stdlib.h> #include <errno.h> -#define COMMAND_STATUS_VOLUME "volume" #define COMMAND_STATUS_STATE "state" #define COMMAND_STATUS_REPEAT "repeat" #define COMMAND_STATUS_SINGLE "single" @@ -166,8 +167,8 @@ check_int(struct client *client, int *value_r, return false; } -#if LONG_MAX > INT_MAX - if (value < INT_MIN || value > INT_MAX) { +#if G_MAXLONG > G_MAXINT + if (value < G_MININT || value > G_MAXINT) { command_error(client, ACK_ERROR_ARG, "Number too large: %s", s); return false; @@ -198,7 +199,7 @@ check_range(struct client *client, unsigned *value_r1, unsigned *value_r2, /* compatibility with older MPD versions: specifying "-1" makes MPD display the whole list */ *value_r1 = 0; - *value_r2 = UINT_MAX; + *value_r2 = G_MAXUINT; return true; } @@ -208,8 +209,8 @@ check_range(struct client *client, unsigned *value_r1, unsigned *value_r2, return false; } -#if LONG_MAX > UINT_MAX - if (value > UINT_MAX) { +#if G_MAXLONG > G_MAXUINT + if (value > G_MAXUINT) { command_error(client, ACK_ERROR_ARG, "Number too large: %s", s); return false; @@ -220,7 +221,7 @@ check_range(struct client *client, unsigned *value_r1, unsigned *value_r2, if (*test == ':') { value = strtol(++test, &test2, 10); - if (*test2 != '\0' || test == test2) { + if (*test2 != '\0') { va_list args; va_start(args, fmt); command_error_v(client, ACK_ERROR_ARG, fmt, args); @@ -228,14 +229,17 @@ check_range(struct client *client, unsigned *value_r1, unsigned *value_r2, return false; } + if (test == test2) + value = G_MAXUINT; + if (value < 0) { command_error(client, ACK_ERROR_ARG, "Number is negative: %s", s); return false; } -#if LONG_MAX > UINT_MAX - if (value > UINT_MAX) { +#if G_MAXLONG > G_MAXUINT + if (value > G_MAXUINT) { command_error(client, ACK_ERROR_ARG, "Number too large: %s", s); return false; @@ -262,7 +266,7 @@ check_unsigned(struct client *client, unsigned *value_r, const char *s) return false; } - if (value > UINT_MAX) { + if (value > G_MAXUINT) { command_error(client, ACK_ERROR_ARG, "Number too large: %s", s); return false; @@ -400,7 +404,7 @@ handle_play(struct client *client, int argc, char *argv[]) if (argc == 2 && !check_int(client, &song, argv[1], need_positive)) return COMMAND_RETURN_ERROR; - result = playPlaylist(&g_playlist, song); + result = playlist_play(&g_playlist, song); return print_playlist_result(client, result); } @@ -413,7 +417,7 @@ handle_playid(struct client *client, int argc, char *argv[]) if (argc == 2 && !check_int(client, &id, argv[1], need_positive)) return COMMAND_RETURN_ERROR; - result = playPlaylistById(&g_playlist, id); + result = playlist_play_id(&g_playlist, id); return print_playlist_result(client, result); } @@ -421,7 +425,7 @@ static enum command_return handle_stop(G_GNUC_UNUSED struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { - stopPlaylist(&g_playlist); + playlist_stop(&g_playlist); return COMMAND_RETURN_OK; } @@ -441,11 +445,11 @@ handle_pause(struct client *client, bool pause_flag; if (!check_bool(client, &pause_flag, argv[1])) return COMMAND_RETURN_ERROR; - playerSetPause(pause_flag); - return COMMAND_RETURN_OK; - } - playerPause(); + pc_set_pause(pause_flag); + } else + pc_pause(); + return COMMAND_RETURN_OK; } @@ -454,10 +458,14 @@ handle_status(struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { const char *state = NULL; + struct player_status player_status; int updateJobId; + char *error; int song; - switch (getPlayerState()) { + pc_get_status(&player_status); + + switch (player_status.state) { case PLAYER_STATE_STOP: state = "stop"; break; @@ -470,7 +478,7 @@ handle_status(struct client *client, } client_printf(client, - COMMAND_STATUS_VOLUME ": %i\n" + "volume: %i\n" COMMAND_STATUS_REPEAT ": %i\n" COMMAND_STATUS_RANDOM ": %i\n" COMMAND_STATUS_SINGLE ": %i\n" @@ -480,32 +488,36 @@ handle_status(struct client *client, COMMAND_STATUS_CROSSFADE ": %i\n" COMMAND_STATUS_STATE ": %s\n", volume_level_get(), - getPlaylistRepeatStatus(&g_playlist), - getPlaylistRandomStatus(&g_playlist), - getPlaylistSingleStatus(&g_playlist), - getPlaylistConsumeStatus(&g_playlist), - getPlaylistVersion(&g_playlist), - getPlaylistLength(&g_playlist), - (int)(getPlayerCrossFade() + 0.5), + playlist_get_repeat(&g_playlist), + playlist_get_random(&g_playlist), + playlist_get_single(&g_playlist), + playlist_get_consume(&g_playlist), + playlist_get_version(&g_playlist), + playlist_get_length(&g_playlist), + (int)(pc_get_cross_fade() + 0.5), state); - song = getPlaylistCurrentSong(&g_playlist); + song = playlist_get_current_song(&g_playlist); if (song >= 0) { client_printf(client, COMMAND_STATUS_SONG ": %i\n" COMMAND_STATUS_SONGID ": %u\n", - song, getPlaylistSongId(&g_playlist, song)); + song, playlist_get_song_id(&g_playlist, song)); } - if (getPlayerState() != PLAYER_STATE_STOP) { - const struct audio_format *af = player_get_audio_format(); + if (player_status.state != PLAYER_STATE_STOP) { client_printf(client, COMMAND_STATUS_TIME ": %i:%i\n" - COMMAND_STATUS_BITRATE ": %li\n" + "elapsed: %1.3f\n" + COMMAND_STATUS_BITRATE ": %u\n" COMMAND_STATUS_AUDIO ": %u:%u:%u\n", - getPlayerElapsedTime(), getPlayerTotalTime(), - getPlayerBitRate(), - af->sample_rate, af->bits, af->channels); + (int)(player_status.elapsed_time + 0.5), + (int)(player_status.total_time + 0.5), + player_status.elapsed_time, + player_status.bit_rate, + player_status.audio_format.sample_rate, + player_status.audio_format.bits, + player_status.audio_format.channels); } if ((updateJobId = isUpdatingDB())) { @@ -514,18 +526,20 @@ handle_status(struct client *client, updateJobId); } - if (getPlayerError() != PLAYER_ERROR_NOERROR) { + error = pc_get_error_message(); + if (error != NULL) { client_printf(client, COMMAND_STATUS_ERROR ": %s\n", - getPlayerErrorStr()); + error); + g_free(error); } - song = getPlaylistNextSong(&g_playlist); + song = playlist_get_next_song(&g_playlist); if (song >= 0) { client_printf(client, COMMAND_STATUS_NEXTSONG ": %i\n" COMMAND_STATUS_NEXTSONGID ": %u\n", - song, getPlaylistSongId(&g_playlist, song)); + song, playlist_get_song_id(&g_playlist, song)); } return COMMAND_RETURN_OK; @@ -569,7 +583,7 @@ handle_add(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - result = addToPlaylist(&g_playlist, uri, NULL); + result = playlist_append_uri(&g_playlist, uri, NULL); return print_playlist_result(client, result); } @@ -605,7 +619,7 @@ handle_addid(struct client *client, int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - result = addToPlaylist(&g_playlist, uri, &added_id); + result = playlist_append_uri(&g_playlist, uri, &added_id); } if (result != PLAYLIST_RESULT_SUCCESS) @@ -615,11 +629,11 @@ handle_addid(struct client *client, int argc, char *argv[]) int to; if (!check_int(client, &to, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = moveSongInPlaylistById(&g_playlist, added_id, to); + result = playlist_move_id(&g_playlist, added_id, to); if (result != PLAYLIST_RESULT_SUCCESS) { enum command_return ret = print_playlist_result(client, result); - deleteFromPlaylistById(&g_playlist, added_id); + playlist_delete_id(&g_playlist, added_id); return ret; } } @@ -631,13 +645,13 @@ handle_addid(struct client *client, int argc, char *argv[]) static enum command_return handle_delete(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) { - int song; + unsigned start, end; enum playlist_result result; - if (!check_int(client, &song, argv[1], need_positive)) + if (!check_range(client, &start, &end, argv[1], need_range)) return COMMAND_RETURN_ERROR; - result = deleteFromPlaylist(&g_playlist, song); + result = playlist_delete_range(&g_playlist, start, end); return print_playlist_result(client, result); } @@ -650,7 +664,7 @@ handle_deleteid(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) if (!check_int(client, &id, argv[1], need_positive)) return COMMAND_RETURN_ERROR; - result = deleteFromPlaylistById(&g_playlist, id); + result = playlist_delete_id(&g_playlist, id); return print_playlist_result(client, result); } @@ -671,7 +685,7 @@ handle_shuffle(G_GNUC_UNUSED struct client *client, argv[1], need_range)) return COMMAND_RETURN_ERROR; - shufflePlaylist(&g_playlist, start, end); + playlist_shuffle(&g_playlist, start, end); return COMMAND_RETURN_OK; } @@ -679,7 +693,7 @@ static enum command_return handle_clear(G_GNUC_UNUSED struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { - clearPlaylist(&g_playlist); + playlist_clear(&g_playlist); return COMMAND_RETURN_OK; } @@ -698,6 +712,10 @@ handle_load(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) { enum playlist_result result; + result = playlist_open_into_queue(argv[1], &g_playlist); + if (result != PLAYLIST_RESULT_NO_SUCH_LIST) + return result; + result = playlist_load_spl(&g_playlist, argv[1]); return print_playlist_result(client, result); } @@ -808,7 +826,7 @@ handle_plchangesposid(struct client *client, G_GNUC_UNUSED int argc, char *argv[ static enum command_return handle_playlistinfo(struct client *client, int argc, char *argv[]) { - unsigned start = 0, end = UINT_MAX; + unsigned start = 0, end = G_MAXUINT; bool ret; if (argc == 2 && !check_range(client, &start, &end, @@ -837,7 +855,7 @@ handle_playlistid(struct client *client, int argc, char *argv[]) return print_playlist_result(client, PLAYLIST_RESULT_NO_SUCH_SONG); } else { - playlist_print_info(client, &g_playlist, 0, UINT_MAX); + playlist_print_info(client, &g_playlist, 0, G_MAXUINT); } return COMMAND_RETURN_OK; @@ -869,6 +887,30 @@ handle_find(struct client *client, int argc, char *argv[]) } static enum command_return +handle_findadd(struct client *client, int argc, char *argv[]) +{ + int ret; + struct locate_item_list *list = + locate_item_list_parse(argv + 1, argc - 1); + if (list == NULL || list->length == 0) { + if (list != NULL) + locate_item_list_free(list); + + command_error(client, ACK_ERROR_ARG, "incorrect arguments"); + return COMMAND_RETURN_ERROR; + } + + ret = findAddIn(client, NULL, list); + if (ret == -1) + command_error(client, ACK_ERROR_NO_EXIST, + "directory or file not found"); + + locate_item_list_free(list); + + return ret; +} + +static enum command_return handle_search(struct client *client, int argc, char *argv[]) { int ret; @@ -993,14 +1035,35 @@ handle_playlistmove(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) static enum command_return handle_update(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) { - char *path = NULL; + const char *path = NULL; + unsigned ret; + + assert(argc <= 2); + if (argc == 2) + path = argv[1]; + + ret = update_enqueue(path, false); + if (ret > 0) { + client_printf(client, "updating_db: %i\n", ret); + return COMMAND_RETURN_OK; + } else { + command_error(client, ACK_ERROR_UPDATE_ALREADY, + "already updating"); + return COMMAND_RETURN_ERROR; + } +} + +static enum command_return +handle_rescan(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) +{ + const char *path = NULL; unsigned ret; assert(argc <= 2); if (argc == 2) - path = g_strdup(argv[1]); + path = argv[1]; - ret = directory_update_init(path); + ret = update_enqueue(path, true); if (ret > 0) { client_printf(client, "updating_db: %i\n", ret); return COMMAND_RETURN_OK; @@ -1020,7 +1083,7 @@ handle_next(G_GNUC_UNUSED struct client *client, int single = g_playlist.queue.single; g_playlist.queue.single = false; - nextSongInPlaylist(&g_playlist); + playlist_next(&g_playlist); g_playlist.queue.single = single; return COMMAND_RETURN_OK; @@ -1030,7 +1093,7 @@ static enum command_return handle_previous(G_GNUC_UNUSED struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { - previousSongInPlaylist(&g_playlist); + playlist_previous(&g_playlist); return COMMAND_RETURN_OK; } @@ -1052,25 +1115,6 @@ handle_listall(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) } static enum command_return -handle_volume(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) -{ - int change; - bool success; - - if (!check_int(client, &change, argv[1], need_integer)) - return COMMAND_RETURN_ERROR; - - success = volume_level_change(change, true); - if (!success) { - command_error(client, ACK_ERROR_SYSTEM, - "problems setting volume"); - return COMMAND_RETURN_ERROR; - } - - return COMMAND_RETURN_OK; -} - -static enum command_return handle_setvol(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) { int level; @@ -1079,7 +1123,12 @@ handle_setvol(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) if (!check_int(client, &level, argv[1], need_integer)) return COMMAND_RETURN_ERROR; - success = volume_level_change(level, 0); + if (level < 0 || level > 100) { + command_error(client, ACK_ERROR_ARG, "Invalid volume value"); + return COMMAND_RETURN_ERROR; + } + + success = volume_level_change(level); if (!success) { command_error(client, ACK_ERROR_SYSTEM, "problems setting volume"); @@ -1103,7 +1152,7 @@ handle_repeat(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - setPlaylistRepeatStatus(&g_playlist, status); + playlist_set_repeat(&g_playlist, status); return COMMAND_RETURN_OK; } @@ -1121,7 +1170,7 @@ handle_single(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - setPlaylistSingleStatus(&g_playlist, status); + playlist_set_single(&g_playlist, status); return COMMAND_RETURN_OK; } @@ -1139,7 +1188,7 @@ handle_consume(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - setPlaylistConsumeStatus(&g_playlist, status); + playlist_set_consume(&g_playlist, status); return COMMAND_RETURN_OK; } @@ -1157,7 +1206,7 @@ handle_random(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; } - setPlaylistRandomStatus(&g_playlist, status); + playlist_set_random(&g_playlist, status); return COMMAND_RETURN_OK; } @@ -1172,7 +1221,7 @@ static enum command_return handle_clearerror(G_GNUC_UNUSED struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { - clearPlayerError(); + pc_clear_error(); return COMMAND_RETURN_OK; } @@ -1196,17 +1245,17 @@ handle_list(struct client *client, int argc, char *argv[]) /* for compatibility with < 0.12.0 */ if (argc == 3) { - if (tagType != TAG_ITEM_ALBUM) { + if (tagType != TAG_ALBUM) { command_error(client, ACK_ERROR_ARG, "should be \"%s\" for 3 arguments", - tag_item_names[TAG_ITEM_ALBUM]); + tag_item_names[TAG_ALBUM]); return COMMAND_RETURN_ERROR; } locate_item_list_parse(argv + 1, argc - 1); conditionals = locate_item_list_new(1); - conditionals->items[0].tag = TAG_ITEM_ARTIST; + conditionals->items[0].tag = TAG_ARTIST; conditionals->items[0].needle = g_strdup(argv[2]); } else { conditionals = @@ -1241,7 +1290,7 @@ handle_move(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; if (!check_int(client, &to, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = moveSongRangeInPlaylist(&g_playlist, start, end, to); + result = playlist_move_range(&g_playlist, start, end, to); return print_playlist_result(client, result); } @@ -1255,7 +1304,7 @@ handle_moveid(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; if (!check_int(client, &to, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = moveSongInPlaylistById(&g_playlist, id, to); + result = playlist_move_id(&g_playlist, id, to); return print_playlist_result(client, result); } @@ -1269,7 +1318,7 @@ handle_swap(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; if (!check_int(client, &song2, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = swapSongsInPlaylist(&g_playlist, song1, song2); + result = playlist_swap_songs(&g_playlist, song1, song2); return print_playlist_result(client, result); } @@ -1283,7 +1332,7 @@ handle_swapid(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) return COMMAND_RETURN_ERROR; if (!check_int(client, &id2, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = swapSongsInPlaylistById(&g_playlist, id1, id2); + result = playlist_swap_songs_id(&g_playlist, id1, id2); return print_playlist_result(client, result); } @@ -1298,7 +1347,7 @@ handle_seek(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) if (!check_int(client, &seek_time, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = seekSongInPlaylist(&g_playlist, song, seek_time); + result = playlist_seek_song(&g_playlist, song, seek_time); return print_playlist_result(client, result); } @@ -1313,7 +1362,7 @@ handle_seekid(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) if (!check_int(client, &seek_time, argv[2], check_integer, argv[2])) return COMMAND_RETURN_ERROR; - result = seekSongInPlaylistById(&g_playlist, id, seek_time); + result = playlist_seek_song_id(&g_playlist, id, seek_time); return print_playlist_result(client, result); } @@ -1363,7 +1412,7 @@ handle_crossfade(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) if (!check_unsigned(client, &xfade_time, argv[1])) return COMMAND_RETURN_ERROR; - setPlayerCrossFade(xfade_time); + pc_set_cross_fade(xfade_time); return COMMAND_RETURN_OK; } @@ -1477,6 +1526,28 @@ handle_listplaylists(struct client *client, } static enum command_return +handle_replay_gain_mode(struct client *client, + G_GNUC_UNUSED int argc, char *argv[]) +{ + if (!replay_gain_set_mode_string(argv[1])) { + command_error(client, ACK_ERROR_ARG, + "Unrecognized replay gain mode"); + return COMMAND_RETURN_ERROR; + } + + return COMMAND_RETURN_OK; +} + +static enum command_return +handle_replay_gain_status(struct client *client, + G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) +{ + client_printf(client, "replay_gain_mode: %s\n", + replay_gain_get_mode_string()); + return COMMAND_RETURN_OK; +} + +static enum command_return handle_idle(struct client *client, G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) { @@ -1519,7 +1590,7 @@ sticker_song_find_print_cb(struct song *song, const char *value, { struct sticker_song_find_data *data = user_data; - song_print_url(data->client, song); + song_print_uri(data->client, song); sticker_print_value(data->client, data->name, value); } @@ -1684,6 +1755,7 @@ static const struct command commands[] = { { "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput }, { "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput }, { "find", PERMISSION_READ, 2, -1, handle_find }, + { "findadd", PERMISSION_READ, 2, -1, handle_findadd}, { "idle", PERMISSION_READ, 0, -1, handle_idle }, { "kill", PERMISSION_ADMIN, -1, -1, handle_kill }, { "list", PERMISSION_READ, 1, -1, handle_list }, @@ -1719,6 +1791,11 @@ static const struct command commands[] = { { "random", PERMISSION_CONTROL, 1, 1, handle_random }, { "rename", PERMISSION_CONTROL, 2, 2, handle_rename }, { "repeat", PERMISSION_CONTROL, 1, 1, handle_repeat }, + { "replay_gain_mode", PERMISSION_CONTROL, 1, 1, + handle_replay_gain_mode }, + { "replay_gain_status", PERMISSION_READ, 0, 0, + handle_replay_gain_status }, + { "rescan", PERMISSION_ADMIN, 0, 1, handle_rescan }, { "rm", PERMISSION_CONTROL, 1, 1, handle_rm }, { "save", PERMISSION_CONTROL, 1, 1, handle_save }, { "search", PERMISSION_READ, 2, -1, handle_search }, @@ -1738,7 +1815,6 @@ static const struct command commands[] = { { "tagtypes", PERMISSION_READ, 0, 0, handle_tagtypes }, { "update", PERMISSION_ADMIN, 0, 1, handle_update }, { "urlhandlers", PERMISSION_READ, 0, 0, handle_urlhandlers }, - { "volume", PERMISSION_CONTROL, 1, 1, handle_volume }, }; static const unsigned num_commands = sizeof(commands) / sizeof(commands[0]); @@ -1892,48 +1968,71 @@ command_checked_lookup(struct client *client, unsigned permission, } enum command_return -command_process(struct client *client, char *commandString) +command_process(struct client *client, unsigned num, char *line) { + GError *error = NULL; int argc; char *argv[COMMAND_ARGV_MAX] = { NULL }; const struct command *cmd; enum command_return ret = COMMAND_RETURN_ERROR; - if (!(argc = buffer2array(commandString, argv, COMMAND_ARGV_MAX))) - return COMMAND_RETURN_OK; + command_list_num = num; - cmd = command_checked_lookup(client, client_get_permission(client), - argc, argv); - if (cmd) - ret = cmd->handler(client, argc, argv); + /* get the command name (first word on the line) */ - current_command = NULL; + argv[0] = tokenizer_next_word(&line, &error); + if (argv[0] == NULL) { + current_command = ""; + if (*line == 0) + command_error(client, ACK_ERROR_UNKNOWN, + "No command given"); + else { + command_error(client, ACK_ERROR_UNKNOWN, + "%s", error->message); + g_error_free(error); + } + current_command = NULL; - return ret; -} + return COMMAND_RETURN_ERROR; + } -enum command_return -command_process_list(struct client *client, - bool list_ok, GSList *list) -{ - enum command_return ret = COMMAND_RETURN_OK; + argc = 1; - command_list_num = 0; + /* now parse the arguments (quoted or unquoted) */ + + while (argc < (int)G_N_ELEMENTS(argv) && + (argv[argc] = + tokenizer_next_param(&line, &error)) != NULL) + ++argc; + + /* some error checks; we have to set current_command because + command_error() expects it to be set */ + + current_command = argv[0]; + + if (argc >= (int)G_N_ELEMENTS(argv)) { + command_error(client, ACK_ERROR_ARG, "Too many arguments"); + current_command = NULL; + return COMMAND_RETURN_ERROR; + } - for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) { - char *cmd = cur->data; - - g_debug("command_process_list: process command \"%s\"", - cmd); - ret = command_process(client, cmd); - g_debug("command_process_list: command returned %i", ret); - if (ret != COMMAND_RETURN_OK || client_is_expired(client)) - break; - else if (list_ok) - client_puts(client, "list_OK\n"); - command_list_num++; + if (*line != 0) { + command_error(client, ACK_ERROR_ARG, + "%s", error->message); + current_command = NULL; + g_error_free(error); + return COMMAND_RETURN_ERROR; } + /* look up and invoke the command handler */ + + cmd = command_checked_lookup(client, client_get_permission(client), + argc, argv); + if (cmd) + ret = cmd->handler(client, argc, argv); + + current_command = NULL; command_list_num = 0; + return ret; } diff --git a/src/command.h b/src/command.h index a7c408ed7..614a414b6 100644 --- a/src/command.h +++ b/src/command.h @@ -39,11 +39,7 @@ void command_init(void); void command_finish(void); enum command_return -command_process_list(struct client *client, - bool list_ok, GSList *list); - -enum command_return -command_process(struct client *client, char *commandString); +command_process(struct client *client, unsigned num, char *line); void command_success(struct client *client); diff --git a/src/conf.c b/src/conf.c index cce7dbf27..803ad8503 100644 --- a/src/conf.c +++ b/src/conf.c @@ -19,7 +19,7 @@ #include "conf.h" #include "utils.h" -#include "buffer2array.h" +#include "tokenizer.h" #include "path.h" #include <glib.h> @@ -36,37 +36,92 @@ #define MAX_STRING_SIZE MPD_PATH_MAX+80 #define CONF_COMMENT '#' -#define CONF_BLOCK_BEGIN "{" -#define CONF_BLOCK_END "}" - -#define CONF_REPEATABLE_MASK 0x01 -#define CONF_BLOCK_MASK 0x02 -#define CONF_LINE_TOKEN_MAX 3 struct config_entry { - const char *name; - unsigned char mask; + const char *const name; + const bool repeatable; + const bool block; GSList *params; }; -static GSList *config_entries; +static struct config_entry config_entries[] = { + { .name = CONF_MUSIC_DIR, false, false }, + { .name = CONF_PLAYLIST_DIR, false, false }, + { .name = CONF_FOLLOW_INSIDE_SYMLINKS, false, false }, + { .name = CONF_FOLLOW_OUTSIDE_SYMLINKS, false, false }, + { .name = CONF_DB_FILE, false, false }, + { .name = CONF_STICKER_FILE, false, false }, + { .name = CONF_LOG_FILE, false, false }, + { .name = CONF_PID_FILE, false, false }, + { .name = CONF_STATE_FILE, false, false }, + { .name = CONF_USER, false, false }, + { .name = CONF_GROUP, false, false }, + { .name = CONF_BIND_TO_ADDRESS, true, false }, + { .name = CONF_PORT, false, false }, + { .name = CONF_LOG_LEVEL, false, false }, + { .name = CONF_ZEROCONF_NAME, false, false }, + { .name = CONF_ZEROCONF_ENABLED, false, false }, + { .name = CONF_PASSWORD, true, false }, + { .name = CONF_DEFAULT_PERMS, false, false }, + { .name = CONF_AUDIO_OUTPUT, true, true }, + { .name = CONF_AUDIO_OUTPUT_FORMAT, false, false }, + { .name = CONF_MIXER_TYPE, false, false }, + { .name = CONF_REPLAYGAIN, false, false }, + { .name = CONF_REPLAYGAIN_PREAMP, false, false }, + { .name = CONF_REPLAYGAIN_MISSING_PREAMP, false, false }, + { .name = CONF_VOLUME_NORMALIZATION, false, false }, + { .name = CONF_SAMPLERATE_CONVERTER, false, false }, + { .name = CONF_AUDIO_BUFFER_SIZE, false, false }, + { .name = CONF_BUFFER_BEFORE_PLAY, false, false }, + { .name = CONF_HTTP_PROXY_HOST, false, false }, + { .name = CONF_HTTP_PROXY_PORT, false, false }, + { .name = CONF_HTTP_PROXY_USER, false, false }, + { .name = CONF_HTTP_PROXY_PASSWORD, false, false }, + { .name = CONF_CONN_TIMEOUT, false, false }, + { .name = CONF_MAX_CONN, false, false }, + { .name = CONF_MAX_PLAYLIST_LENGTH, false, false }, + { .name = CONF_MAX_COMMAND_LIST_SIZE, false, false }, + { .name = CONF_MAX_OUTPUT_BUFFER_SIZE, false, false }, + { .name = CONF_FS_CHARSET, false, false }, + { .name = CONF_ID3V1_ENCODING, false, false }, + { .name = CONF_METADATA_TO_USE, false, false }, + { .name = CONF_SAVE_ABSOLUTE_PATHS, false, false }, + { .name = CONF_DECODER, true, true }, + { .name = CONF_INPUT, true, true }, + { .name = CONF_GAPLESS_MP3_PLAYBACK, false, false }, + { .name = CONF_PLAYLIST_PLUGIN, true, true }, + { .name = CONF_AUTO_UPDATE, false, false }, + { .name = "filter", true, true }, +}; + +static bool +string_array_contains(const char *const* array, const char *value) +{ + for (const char *const* x = array; *x; x++) + if (g_ascii_strcasecmp(*x, value) == 0) + return true; + + return false; +} -static int get_bool(const char *value) +static bool +get_bool(const char *value, bool *value_r) { - const char **x; static const char *t[] = { "yes", "true", "1", NULL }; static const char *f[] = { "no", "false", "0", NULL }; - for (x = t; *x; x++) { - if (!g_ascii_strcasecmp(*x, value)) - return 1; + if (string_array_contains(t, value)) { + *value_r = true; + return true; } - for (x = f; *x; x++) { - if (!g_ascii_strcasecmp(*x, value)) - return 0; + + if (string_array_contains(f, value)) { + *value_r = false; + return true; } - return CONF_BOOL_INVALID; + + return false; } struct config_param * @@ -83,15 +138,14 @@ config_new_param(const char *value, int line) ret->num_block_params = 0; ret->block_params = NULL; + ret->used = false; return ret; } static void -config_param_free(gpointer data, G_GNUC_UNUSED gpointer user_data) +config_param_free(struct config_param *param) { - struct config_param *param = data; - g_free(param->value); for (unsigned i = 0; i < param->num_block_params; i++) { @@ -105,42 +159,19 @@ config_param_free(gpointer data, G_GNUC_UNUSED gpointer user_data) g_free(param); } -static struct config_entry * -newConfigEntry(const char *name, int repeatable, int block) -{ - struct config_entry *ret = g_new(struct config_entry, 1); - - ret->name = name; - ret->mask = 0; - ret->params = NULL; - - if (repeatable) - ret->mask |= CONF_REPEATABLE_MASK; - if (block) - ret->mask |= CONF_BLOCK_MASK; - - return ret; -} - static void -config_entry_free(gpointer data, G_GNUC_UNUSED gpointer user_data) +config_param_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) { - struct config_entry *entry = data; - - g_slist_foreach(entry->params, config_param_free, NULL); - g_slist_free(entry->params); + struct config_param *param = data; - g_free(entry); + config_param_free(param); } static struct config_entry * config_entry_get(const char *name) { - GSList *list; - - for (list = config_entries; list != NULL; - list = g_slist_next(list)) { - struct config_entry *entry = list->data; + for (unsigned i = 0; i < G_N_ELEMENTS(config_entries); ++i) { + struct config_entry *entry = &config_entries[i]; if (strcmp(entry->name, name) == 0) return entry; } @@ -148,82 +179,65 @@ config_entry_get(const char *name) return NULL; } -static void registerConfigParam(const char *name, int repeatable, int block) +void config_global_finish(void) { - struct config_entry *entry; + for (unsigned i = 0; i < G_N_ELEMENTS(config_entries); ++i) { + struct config_entry *entry = &config_entries[i]; - entry = config_entry_get(name); - if (entry != NULL) - g_error("config parameter \"%s\" already registered\n", name); + g_slist_foreach(entry->params, + config_param_free_callback, NULL); + g_slist_free(entry->params); + } +} - entry = newConfigEntry(name, repeatable, block); - config_entries = g_slist_prepend(config_entries, entry); +void config_global_init(void) +{ } -void config_global_finish(void) +static void +config_param_check(gpointer data, G_GNUC_UNUSED gpointer user_data) { - g_slist_foreach(config_entries, config_entry_free, NULL); - g_slist_free(config_entries); + struct config_param *param = data; + + if (!param->used) + /* this whole config_param was not queried at all - + the feature might be disabled at compile time? + Silently ignore it here. */ + return; + + for (unsigned i = 0; i < param->num_block_params; i++) { + struct block_param *bp = ¶m->block_params[i]; + + if (!bp->used) + g_warning("option '%s' on line %i was not recognized", + bp->name, bp->line); + } } -void config_global_init(void) +void config_global_check(void) { - config_entries = NULL; - - /* registerConfigParam(name, repeatable, block); */ - registerConfigParam(CONF_MUSIC_DIR, 0, 0); - registerConfigParam(CONF_PLAYLIST_DIR, 0, 0); - registerConfigParam(CONF_FOLLOW_INSIDE_SYMLINKS, 0, 0); - registerConfigParam(CONF_FOLLOW_OUTSIDE_SYMLINKS, 0, 0); - registerConfigParam(CONF_DB_FILE, 0, 0); - registerConfigParam(CONF_STICKER_FILE, false, false); - registerConfigParam(CONF_LOG_FILE, 0, 0); - registerConfigParam(CONF_ERROR_FILE, 0, 0); - registerConfigParam(CONF_PID_FILE, 0, 0); - registerConfigParam(CONF_STATE_FILE, 0, 0); - registerConfigParam(CONF_USER, 0, 0); - registerConfigParam(CONF_BIND_TO_ADDRESS, 1, 0); - registerConfigParam(CONF_PORT, 0, 0); - registerConfigParam(CONF_LOG_LEVEL, 0, 0); - registerConfigParam(CONF_ZEROCONF_NAME, 0, 0); - registerConfigParam(CONF_ZEROCONF_ENABLED, 0, 0); - registerConfigParam(CONF_PASSWORD, 1, 0); - registerConfigParam(CONF_DEFAULT_PERMS, 0, 0); - registerConfigParam(CONF_AUDIO_OUTPUT, 1, 1); - registerConfigParam(CONF_AUDIO_OUTPUT_FORMAT, 0, 0); - registerConfigParam(CONF_MIXER_TYPE, 0, 0); - registerConfigParam(CONF_MIXER_DEVICE, 0, 0); - registerConfigParam(CONF_MIXER_CONTROL, 0, 0); - registerConfigParam(CONF_REPLAYGAIN, 0, 0); - registerConfigParam(CONF_REPLAYGAIN_PREAMP, 0, 0); - registerConfigParam(CONF_VOLUME_NORMALIZATION, 0, 0); - registerConfigParam(CONF_SAMPLERATE_CONVERTER, 0, 0); - registerConfigParam(CONF_AUDIO_BUFFER_SIZE, 0, 0); - registerConfigParam(CONF_BUFFER_BEFORE_PLAY, 0, 0); - registerConfigParam(CONF_HTTP_PROXY_HOST, 0, 0); - registerConfigParam(CONF_HTTP_PROXY_PORT, 0, 0); - registerConfigParam(CONF_HTTP_PROXY_USER, 0, 0); - registerConfigParam(CONF_HTTP_PROXY_PASSWORD, 0, 0); - registerConfigParam(CONF_CONN_TIMEOUT, 0, 0); - registerConfigParam(CONF_MAX_CONN, 0, 0); - registerConfigParam(CONF_MAX_PLAYLIST_LENGTH, 0, 0); - registerConfigParam(CONF_MAX_COMMAND_LIST_SIZE, 0, 0); - registerConfigParam(CONF_MAX_OUTPUT_BUFFER_SIZE, 0, 0); - registerConfigParam(CONF_FS_CHARSET, 0, 0); - registerConfigParam(CONF_ID3V1_ENCODING, 0, 0); - registerConfigParam(CONF_METADATA_TO_USE, 0, 0); - registerConfigParam(CONF_SAVE_ABSOLUTE_PATHS, 0, 0); - registerConfigParam(CONF_DECODER, true, true); - registerConfigParam(CONF_INPUT, true, true); - registerConfigParam(CONF_GAPLESS_MP3_PLAYBACK, 0, 0); + for (unsigned i = 0; i < G_N_ELEMENTS(config_entries); ++i) { + struct config_entry *entry = &config_entries[i]; + + g_slist_foreach(entry->params, config_param_check, NULL); + } } -void +bool config_add_block_param(struct config_param * param, const char *name, - const char *value, int line) + const char *value, int line, GError **error_r) { struct block_param *bp; + bp = config_get_block_param(param, name); + if (bp != NULL) { + g_set_error(error_r, config_quark(), 0, + "\"%s\" first defined on line %i, and " + "redefined on line %i\n", name, + bp->line, line); + return false; + } + param->num_block_params++; param->block_params = g_realloc(param->block_params, @@ -235,67 +249,97 @@ config_add_block_param(struct config_param * param, const char *name, bp->name = g_strdup(name); bp->value = g_strdup(value); bp->line = line; + bp->used = false; + + return true; } static struct config_param * -config_read_block(FILE *fp, int *count, char *string) +config_read_block(FILE *fp, int *count, char *string, GError **error_r) { struct config_param *ret = config_new_param(NULL, *count); - - int i; - int numberOfArgs; - int argsMinusComment; - - while (fgets(string, MAX_STRING_SIZE, fp)) { - char *array[CONF_LINE_TOKEN_MAX] = { NULL }; + GError *error = NULL; + bool success; + + while (true) { + char *line; + const char *name, *value; + + line = fgets(string, MAX_STRING_SIZE, fp); + if (line == NULL) { + config_param_free(ret); + g_set_error(error_r, config_quark(), 0, + "Expected '}' before end-of-file"); + return NULL; + } (*count)++; + line = g_strchug(line); + if (*line == 0 || *line == CONF_COMMENT) + continue; - numberOfArgs = buffer2array(string, array, CONF_LINE_TOKEN_MAX); + if (*line == '}') { + /* end of this block; return from the function + (and from this "while" loop) */ + + line = g_strchug(line + 1); + if (*line != 0 && *line != CONF_COMMENT) { + config_param_free(ret); + g_set_error(error_r, config_quark(), 0, + "line %i: Unknown tokens after '}'", + *count); + return false; + } - for (i = 0; i < numberOfArgs; i++) { - if (array[i][0] == CONF_COMMENT) - break; + return ret; } - argsMinusComment = i; + /* parse name and value */ - if (0 == argsMinusComment) { - continue; + name = tokenizer_next_word(&line, &error); + if (name == NULL) { + assert(*line != 0); + config_param_free(ret); + g_propagate_prefixed_error(error_r, error, + "line %i: ", *count); + return NULL; } - if (1 == argsMinusComment && - 0 == strcmp(array[0], CONF_BLOCK_END)) { - break; + value = tokenizer_next_string(&line, &error); + if (value == NULL) { + config_param_free(ret); + if (*line == 0) + g_set_error(error_r, config_quark(), 0, + "line %i: Value missing", *count); + else + g_propagate_prefixed_error(error_r, error, + "line %i: ", + *count); + return NULL; } - if (2 != argsMinusComment) { - g_error("improperly formatted config file at line %i:" - " %s\n", *count, string); + if (*line != 0 && *line != CONF_COMMENT) { + config_param_free(ret); + g_set_error(error_r, config_quark(), 0, + "line %i: Unknown tokens after value", + *count); + return NULL; } - if (0 == strcmp(array[0], CONF_BLOCK_BEGIN) || - 0 == strcmp(array[1], CONF_BLOCK_BEGIN) || - 0 == strcmp(array[0], CONF_BLOCK_END) || - 0 == strcmp(array[1], CONF_BLOCK_END)) { - g_error("improperly formatted config file at line %i: %s " - "in block beginning at line %i\n", - *count, string, ret->line); + success = config_add_block_param(ret, name, value, *count, + error_r); + if (!success) { + config_param_free(ret); + return false; } - - config_add_block_param(ret, array[0], array[1], *count); } - - return ret; } -void config_read_file(const char *file) +bool +config_read_file(const char *file, GError **error_r) { FILE *fp; char string[MAX_STRING_SIZE + 1]; - int i; - int numberOfArgs; - int argsMinusComment; int count = 0; struct config_entry *entry; struct config_param *param; @@ -303,67 +347,110 @@ void config_read_file(const char *file) g_debug("loading file %s", file); if (!(fp = fopen(file, "r"))) { - g_error("problems opening file %s for reading: %s\n", - file, strerror(errno)); + g_set_error(error_r, config_quark(), errno, + "Failed to open %s: %s", + file, strerror(errno)); + return false; } while (fgets(string, MAX_STRING_SIZE, fp)) { - char *array[CONF_LINE_TOKEN_MAX] = { NULL }; + char *line; + const char *name, *value; + GError *error = NULL; count++; - numberOfArgs = buffer2array(string, array, CONF_LINE_TOKEN_MAX); + line = g_strchug(string); + if (*line == 0 || *line == CONF_COMMENT) + continue; + + /* the first token in each line is the name, followed + by either the value or '{' */ - for (i = 0; i < numberOfArgs; i++) { - if (array[i][0] == CONF_COMMENT) - break; + name = tokenizer_next_word(&line, &error); + if (name == NULL) { + assert(*line != 0); + g_propagate_prefixed_error(error_r, error, + "line %i: ", count); + return false; } - argsMinusComment = i; + /* get the definition of that option, and check the + "repeatable" flag */ - if (0 == argsMinusComment) { - continue; + entry = config_entry_get(name); + if (entry == NULL) { + g_set_error(error_r, config_quark(), 0, + "unrecognized parameter in config file at " + "line %i: %s\n", count, name); + return false; } - if (2 != argsMinusComment) { - g_error("improperly formatted config file at line %i:" - " %s\n", count, string); + if (entry->params != NULL && !entry->repeatable) { + param = entry->params->data; + g_set_error(error_r, config_quark(), 0, + "config parameter \"%s\" is first defined " + "on line %i and redefined on line %i\n", + name, param->line, count); + return false; } - entry = config_entry_get(array[0]); - if (entry == NULL) - g_error("unrecognized parameter in config file at " - "line %i: %s\n", count, string); + /* now parse the block or the value */ - if (!(entry->mask & CONF_REPEATABLE_MASK) && - entry->params != NULL) { - param = entry->params->data; - g_error("config parameter \"%s\" is first defined on " - "line %i and redefined on line %i\n", - array[0], param->line, count); - } + if (entry->block) { + /* it's a block, call config_read_block() */ + + if (*line != '{') { + g_set_error(error_r, config_quark(), 0, + "line %i: '{' expected", count); + return false; + } - if (entry->mask & CONF_BLOCK_MASK) { - if (0 != strcmp(array[1], CONF_BLOCK_BEGIN)) { - g_error("improperly formatted config file at " - "line %i: %s\n", count, string); + line = g_strchug(line + 1); + if (*line != 0 && *line != CONF_COMMENT) { + g_set_error(error_r, config_quark(), 0, + "line %i: Unknown tokens after '{'", + count); + return false; } - param = config_read_block(fp, &count, string); - } else - param = config_new_param(array[1], count); + + param = config_read_block(fp, &count, string, error_r); + if (param == NULL) + return false; + } else { + /* a string value */ + + value = tokenizer_next_string(&line, &error); + if (value == NULL) { + if (*line == 0) + g_set_error(error_r, config_quark(), 0, + "line %i: Value missing", + count); + else { + g_set_error(error_r, config_quark(), 0, + "line %i: %s", count, + error->message); + g_error_free(error); + } + + return false; + } + + if (*line != 0 && *line != CONF_COMMENT) { + g_set_error(error_r, config_quark(), 0, + "line %i: Unknown tokens after value", + count); + return false; + } + + param = config_new_param(value, count); + } entry->params = g_slist_append(entry->params, param); } fclose(fp); -} -void -config_add_param(const char *name, struct config_param *param) -{ - struct config_entry *entry = config_entry_get(name); - assert(entry != NULL); - - entry->params = g_slist_append(entry->params, param); + return true; } struct config_param * @@ -391,7 +478,7 @@ config_get_next_param(const char *name, const struct config_param * last) return NULL; param = node->data; - + param->used = true; return param; } @@ -447,43 +534,35 @@ config_get_positive(const char *name, unsigned default_value) struct block_param * config_get_block_param(const struct config_param * param, const char *name) { - struct block_param *ret = NULL; - if (param == NULL) return NULL; for (unsigned i = 0; i < param->num_block_params; i++) { if (0 == strcmp(name, param->block_params[i].name)) { - if (ret) { - g_warning("\"%s\" first defined on line %i, and " - "redefined on line %i\n", name, - ret->line, param->block_params[i].line); - } - ret = param->block_params + i; + struct block_param *bp = ¶m->block_params[i]; + bp->used = true; + return bp; } } - return ret; + return NULL; } bool config_get_bool(const char *name, bool default_value) { const struct config_param *param = config_get_param(name); - int value; + bool success, value; if (param == NULL) return default_value; - value = get_bool(param->value); - if (value == CONF_BOOL_INVALID) + success = get_bool(param->value, &value); + if (!success) g_error("%s is not a boolean value (yes, true, 1) or " "(no, false, 0) on line %i\n", name, param->line); - if (value == CONF_BOOL_UNSET) - return default_value; - - return !!value; + return value; } const char * @@ -524,19 +603,16 @@ config_get_block_bool(const struct config_param *param, const char *name, bool default_value) { struct block_param *bp = config_get_block_param(param, name); - int value; + bool success, value; if (bp == NULL) return default_value; - value = get_bool(bp->value); - if (value == CONF_BOOL_INVALID) + success = get_bool(bp->value, &value); + if (!success) g_error("%s is not a boolean value (yes, true, 1) or " "(no, false, 0) on line %i\n", name, bp->line); - if (value == CONF_BOOL_UNSET) - return default_value; - - return !!value; + return value; } diff --git a/src/conf.h b/src/conf.h index c5e49960e..7dd12afda 100644 --- a/src/conf.h +++ b/src/conf.h @@ -30,10 +30,10 @@ #define CONF_DB_FILE "db_file" #define CONF_STICKER_FILE "sticker_file" #define CONF_LOG_FILE "log_file" -#define CONF_ERROR_FILE "error_file" #define CONF_PID_FILE "pid_file" #define CONF_STATE_FILE "state_file" #define CONF_USER "user" +#define CONF_GROUP "group" #define CONF_BIND_TO_ADDRESS "bind_to_address" #define CONF_PORT "port" #define CONF_LOG_LEVEL "log_level" @@ -44,10 +44,9 @@ #define CONF_AUDIO_OUTPUT "audio_output" #define CONF_AUDIO_OUTPUT_FORMAT "audio_output_format" #define CONF_MIXER_TYPE "mixer_type" -#define CONF_MIXER_DEVICE "mixer_device" -#define CONF_MIXER_CONTROL "mixer_control" #define CONF_REPLAYGAIN "replaygain" #define CONF_REPLAYGAIN_PREAMP "replaygain_preamp" +#define CONF_REPLAYGAIN_MISSING_PREAMP "replaygain_missing_preamp" #define CONF_VOLUME_NORMALIZATION "volume_normalization" #define CONF_SAMPLERATE_CONVERTER "samplerate_converter" #define CONF_AUDIO_BUFFER_SIZE "audio_buffer_size" @@ -68,9 +67,8 @@ #define CONF_DECODER "decoder" #define CONF_INPUT "input" #define CONF_GAPLESS_MP3_PLAYBACK "gapless_mp3_playback" - -#define CONF_BOOL_UNSET -1 -#define CONF_BOOL_INVALID -2 +#define CONF_PLAYLIST_PLUGIN "playlist_plugin" +#define CONF_AUTO_UPDATE "auto_update" #define DEFAULT_PLAYLIST_MAX_LENGTH (1024*16) #define DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS false @@ -79,6 +77,12 @@ struct block_param { char *name; char *value; int line; + + /** + * This flag is false when nobody has queried the value of + * this option yet. + */ + bool used; }; struct config_param { @@ -87,31 +91,57 @@ struct config_param { struct block_param *block_params; unsigned num_block_params; + + /** + * This flag is false when nobody has queried the value of + * this option yet. + */ + bool used; }; +/** + * A GQuark for GError instances, resulting from malformed + * configuration. + */ +static inline GQuark +config_quark(void) +{ + return g_quark_from_static_string("config"); +} + void config_global_init(void); void config_global_finish(void); -void config_read_file(const char *file); - /** - * Adds a new configuration parameter. The name must be registered - * with registerConfigParam(). + * Call this function after all configuration has been evaluated. It + * checks for unused parameters, and logs warnings. */ -void -config_add_param(const char *name, struct config_param *param); +void config_global_check(void); + +bool +config_read_file(const char *file, GError **error_r); /* don't free the returned value set _last_ to NULL to get first entry */ +G_GNUC_PURE struct config_param * config_get_next_param(const char *name, const struct config_param *last); +G_GNUC_PURE static inline struct config_param * config_get_param(const char *name) { return config_get_next_param(name, NULL); } +/* Note on G_GNUC_PURE: Some of the functions declared pure are not + really pure in strict sense. They have side effect such that they + validate parameter's value and signal an error if it's invalid. + However, if the argument was already validated or we don't care + about the argument at all, this may be ignored so in the end, we + should be fine with calling those functions pure. */ + +G_GNUC_PURE const char * config_get_string(const char *name, const char *default_value); @@ -120,17 +150,27 @@ config_get_string(const char *name, const char *default_value); * absolute path. If there is a tilde prefix, it is expanded. Aborts * MPD if the path is not a valid absolute path. */ +/* We lie here really. This function is not pure as it has side + effects -- it parse the value and creates new string freeing + previous one. However, because this works the very same way each + time (ie. from the outside it appears as if function had no side + effects) we should be in the clear declaring it pure. */ +G_GNUC_PURE const char * config_get_path(const char *name); +G_GNUC_PURE unsigned config_get_positive(const char *name, unsigned default_value); +G_GNUC_PURE struct block_param * config_get_block_param(const struct config_param *param, const char *name); +G_GNUC_PURE bool config_get_bool(const char *name, bool default_value); +G_GNUC_PURE const char * config_get_block_string(const struct config_param *param, const char *name, const char *default_value); @@ -142,10 +182,12 @@ config_dup_block_string(const struct config_param *param, const char *name, return g_strdup(config_get_block_string(param, name, default_value)); } +G_GNUC_PURE unsigned config_get_block_unsigned(const struct config_param *param, const char *name, unsigned default_value); +G_GNUC_PURE bool config_get_block_bool(const struct config_param *param, const char *name, bool default_value); @@ -153,8 +195,8 @@ config_get_block_bool(const struct config_param *param, const char *name, struct config_param * config_new_param(const char *value, int line); -void -config_add_block_param(struct config_param *param, const char *name, - const char *value, int line); +bool +config_add_block_param(struct config_param * param, const char *name, + const char *value, int line, GError **error_r); #endif diff --git a/src/cue/cue_tag.c b/src/cue/cue_tag.c index ce8202a81..e45b26a7e 100644 --- a/src/cue/cue_tag.c +++ b/src/cue/cue_tag.c @@ -13,64 +13,64 @@ cue_tag_cd(struct Cdtext* cdtext, struct Rem* rem) tag_begin_add(tag); - { /* TAG_ITEM_ALBUM_ARTIST */ + { /* TAG_ALBUM_ARTIST */ if ((tmp = cdtext_get(PTI_PERFORMER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ALBUM_ARTIST, tmp); + tag_add_item(tag, TAG_ALBUM_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_SONGWRITER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ALBUM_ARTIST, tmp); + tag_add_item(tag, TAG_ALBUM_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_COMPOSER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ALBUM_ARTIST, tmp); + tag_add_item(tag, TAG_ALBUM_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_ARRANGER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ALBUM_ARTIST, tmp); - /* TAG_ITEM_ALBUM_ARTIST */ } + tag_add_item(tag, TAG_ALBUM_ARTIST, tmp); + /* TAG_ALBUM_ARTIST */ } - { /* TAG_ITEM_ARTIST */ + { /* TAG_ARTIST */ if ((tmp = cdtext_get(PTI_PERFORMER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_SONGWRITER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_COMPOSER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_ARRANGER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); - /* TAG_ITEM_ARTIST */ } + tag_add_item(tag, TAG_ARTIST, tmp); + /* TAG_ARTIST */ } - /* TAG_ITEM_PERFORMER */ + /* TAG_PERFORMER */ if ((tmp = cdtext_get(PTI_PERFORMER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_PERFORMER, tmp); + tag_add_item(tag, TAG_PERFORMER, tmp); - /* TAG_ITEM_COMPOSER */ + /* TAG_COMPOSER */ if ((tmp = cdtext_get(PTI_COMPOSER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_COMPOSER, tmp); + tag_add_item(tag, TAG_COMPOSER, tmp); - /* TAG_ITEM_ALBUM */ + /* TAG_ALBUM */ if ((tmp = cdtext_get(PTI_TITLE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ALBUM, tmp); + tag_add_item(tag, TAG_ALBUM, tmp); - /* TAG_ITEM_GENRE */ + /* TAG_GENRE */ if ((tmp = cdtext_get(PTI_GENRE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_GENRE, tmp); + tag_add_item(tag, TAG_GENRE, tmp); - /* TAG_ITEM_DATE */ + /* TAG_DATE */ if ((tmp = rem_get(REM_DATE, rem)) != NULL) - tag_add_item(tag, TAG_ITEM_DATE, tmp); + tag_add_item(tag, TAG_DATE, tmp); - /* TAG_ITEM_COMMENT */ + /* TAG_COMMENT */ if ((tmp = cdtext_get(PTI_MESSAGE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_COMMENT, tmp); + tag_add_item(tag, TAG_COMMENT, tmp); - /* TAG_ITEM_DISC */ + /* TAG_DISC */ if ((tmp = cdtext_get(PTI_DISC_ID, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_DISC, tmp); + tag_add_item(tag, TAG_DISC, tmp); /* stream name, usually empty - * tag_add_item(tag, TAG_ITEM_NAME,); + * tag_add_item(tag, TAG_NAME,); */ /* REM MUSICBRAINZ entry? @@ -109,47 +109,47 @@ cue_tag_track(struct Cdtext* cdtext, struct Rem* rem) tag_begin_add(tag); - { /* TAG_ITEM_ARTIST */ + { /* TAG_ARTIST */ if ((tmp = cdtext_get(PTI_PERFORMER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_SONGWRITER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_COMPOSER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); + tag_add_item(tag, TAG_ARTIST, tmp); else if ((tmp = cdtext_get(PTI_ARRANGER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, tmp); - /* TAG_ITEM_ARTIST */ } + tag_add_item(tag, TAG_ARTIST, tmp); + /* TAG_ARTIST */ } - /* TAG_ITEM_TITLE */ + /* TAG_TITLE */ if ((tmp = cdtext_get(PTI_TITLE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_TITLE, tmp); + tag_add_item(tag, TAG_TITLE, tmp); - /* TAG_ITEM_GENRE */ + /* TAG_GENRE */ if ((tmp = cdtext_get(PTI_GENRE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_GENRE, tmp); + tag_add_item(tag, TAG_GENRE, tmp); - /* TAG_ITEM_DATE */ + /* TAG_DATE */ if ((tmp = rem_get(REM_DATE, rem)) != NULL) - tag_add_item(tag, TAG_ITEM_DATE, tmp); + tag_add_item(tag, TAG_DATE, tmp); - /* TAG_ITEM_COMPOSER */ + /* TAG_COMPOSER */ if ((tmp = cdtext_get(PTI_COMPOSER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_COMPOSER, tmp); + tag_add_item(tag, TAG_COMPOSER, tmp); - /* TAG_ITEM_PERFORMER */ + /* TAG_PERFORMER */ if ((tmp = cdtext_get(PTI_PERFORMER, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_PERFORMER, tmp); + tag_add_item(tag, TAG_PERFORMER, tmp); - /* TAG_ITEM_COMMENT */ + /* TAG_COMMENT */ if ((tmp = cdtext_get(PTI_MESSAGE, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_COMMENT, tmp); + tag_add_item(tag, TAG_COMMENT, tmp); - /* TAG_ITEM_DISC */ + /* TAG_DISC */ if ((tmp = cdtext_get(PTI_DISC_ID, cdtext)) != NULL) - tag_add_item(tag, TAG_ITEM_DISC, tmp); + tag_add_item(tag, TAG_DISC, tmp); tag_end_add(tag); diff --git a/src/daemon.c b/src/daemon.c index 33b2953a9..192fa8d37 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "daemon.h" #include <glib.h> @@ -45,20 +46,21 @@ static char *user_name; /** the Unix user id which MPD runs as */ -static uid_t user_uid; +static uid_t user_uid = (uid_t)-1; /** the Unix group id which MPD runs as */ -static gid_t user_gid; +static gid_t user_gid = (pid_t)-1; /** the absolute path of the pidfile */ static char *pidfile; -#endif +/* whether "group" conf. option was given */ +static bool had_group = false; + void daemonize_kill(void) { -#ifndef WIN32 FILE *fp; int pid, ret; @@ -82,41 +84,34 @@ daemonize_kill(void) pid, g_strerror(errno)); exit(EXIT_SUCCESS); -#else - g_error("--kill is not available on WIN32"); -#endif } void daemonize_close_stdin(void) { - int fd = open("/dev/null", O_RDONLY); - - if (fd < 0) - close(STDIN_FILENO); - else if (fd != STDIN_FILENO) { - dup2(fd, STDIN_FILENO); - close(fd); - } + close(STDIN_FILENO); + open("/dev/null", O_RDONLY); } void daemonize_set_user(void) { -#ifndef WIN32 if (user_name == NULL) return; - /* get uid */ - if (setgid(user_gid) == -1) { - g_error("cannot setgid for user \"%s\": %s", - user_name, g_strerror(errno)); + /* set gid */ + if (user_gid != (gid_t)-1 && user_gid != getgid()) { + if (setgid(user_gid) == -1) { + g_error("cannot setgid to %d: %s", + (int)user_gid, g_strerror(errno)); + } } + #ifdef _BSD_SOURCE /* init suplementary groups * (must be done before we change our uid) */ - if (initgroups(user_name, user_gid) == -1) { + if (!had_group && initgroups(user_name, user_gid) == -1) { g_warning("cannot init supplementary groups " "of user \"%s\": %s", user_name, g_strerror(errno)); @@ -124,32 +119,38 @@ daemonize_set_user(void) #endif /* set uid */ - if (setuid(user_uid) == -1) { + if (user_uid != (uid_t)-1 && user_uid != getuid() && + setuid(user_uid) == -1) { g_error("cannot change to uid of user \"%s\": %s", user_name, g_strerror(errno)); } -#endif } -#ifndef G_OS_WIN32 static void daemonize_detach(void) { - pid_t pid; - /* flush all file handles before duplicating the buffers */ fflush(NULL); +#ifdef HAVE_DAEMON + + if (daemon(0, 1)) + g_error("daemon() failed: %s", g_strerror(errno)); + +#elif defined(HAVE_FORK) + /* detach from parent process */ - pid = fork(); - if (pid < 0) + switch (fork()) { + case -1: g_error("fork() failed: %s", g_strerror(errno)); - - if (pid > 0) + case 0: + break; + default: /* exit the parent process */ _exit(EXIT_SUCCESS); + } /* release the current working directory */ @@ -160,14 +161,16 @@ daemonize_detach(void) setsid(); +#else + g_error("no support for daemonizing"); +#endif + g_debug("daemonized!"); } -#endif void daemonize(bool detach) { -#ifndef WIN32 FILE *fp = NULL; if (pidfile != NULL) { @@ -189,47 +192,45 @@ daemonize(bool detach) fprintf(fp, "%lu\n", (unsigned long)getpid()); fclose(fp); } -#else - /* no daemonization on WIN32 */ - (void)detach; -#endif } void -daemonize_init(const char *user, const char *_pidfile) +daemonize_init(const char *user, const char *group, const char *_pidfile) { -#ifndef WIN32 - if (user != NULL && strcmp(user, g_get_user_name()) != 0) { - struct passwd *pwd; - - user_name = g_strdup(user); - - pwd = getpwnam(user_name); - if (pwd == NULL) - g_error("no such user \"%s\"", user_name); + if (user) { + struct passwd *pwd = getpwnam(user); + if (!pwd) + g_error("no such user \"%s\"", user); user_uid = pwd->pw_uid; user_gid = pwd->pw_gid; + user_name = g_strdup(user); + /* this is needed by libs such as arts */ g_setenv("HOME", pwd->pw_dir, true); } + if (group) { + struct group *grp = grp = getgrnam(group); + if (!grp) + g_error("no such group \"%s\"", group); + user_gid = grp->gr_gid; + had_group = true; + } + + pidfile = g_strdup(_pidfile); -#else - (void)user; - (void)_pidfile; -#endif } void daemonize_finish(void) { -#ifndef WIN32 if (pidfile != NULL) unlink(pidfile); g_free(user_name); g_free(pidfile); -#endif } + +#endif diff --git a/src/daemon.h b/src/daemon.h index 5b3f9a7dc..a29945607 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -22,32 +22,67 @@ #include <stdbool.h> +#ifndef WIN32 void -daemonize_init(const char *user, const char *pidfile); +daemonize_init(const char *user, const char *group, const char *pidfile); +#else +static inline void +daemonize_init(const char *user, const char *group, const char *pidfile) +{ (void)user; (void)group; (void)pidfile; } +#endif +#ifndef WIN32 void daemonize_finish(void); +#else +static inline void +daemonize_finish(void) +{ /* nop */ } +#endif /** * Kill the MPD which is currently running, pid determined from the * pid file. */ +#ifndef WIN32 void daemonize_kill(void); +#else +static inline void +daemonize_kill(void) +{ g_error("--kill is not available on WIN32"); } +#endif /** * Close stdin (fd 0) and re-open it as /dev/null. */ +#ifndef WIN32 void daemonize_close_stdin(void); +#else +static inline void +daemonize_close_stdin(void) {} +#endif /** * Change to the configured Unix user. */ +#ifndef WIN32 void daemonize_set_user(void); +#else +static inline void +daemonize_set_user(void) +{ /* nop */ } +#endif +#ifndef WIN32 void daemonize(bool detach); +#else +static inline void +daemonize(bool detach) +{ (void)detach; } +#endif #endif diff --git a/src/dbUtils.c b/src/dbUtils.c index f89148c1b..9978daa43 100644 --- a/src/dbUtils.c +++ b/src/dbUtils.c @@ -59,7 +59,7 @@ static int printSongInDirectory(struct song *song, G_GNUC_UNUSED void *data) { struct client *client = data; - song_print_url(client, song); + song_print_uri(client, song); return 0; } @@ -168,7 +168,7 @@ int printAllIn(struct client *client, const char *name) static int directoryAddSongToPlaylist(struct song *song, G_GNUC_UNUSED void *data) { - return addSongToPlaylist(&g_playlist, song, NULL); + return playlist_append_song(&g_playlist, song, NULL); } struct add_data { @@ -200,6 +200,28 @@ int addAllInToStoredPlaylist(const char *name, const char *utf8file) } static int +findAddInDirectory(struct song *song, void *_data) +{ + struct search_data *data = _data; + + if (locate_song_match(song, data->criteria)) + return directoryAddSongToPlaylist(song, data); + + return 0; +} + +int findAddIn(struct client *client, const char *name, + const struct locate_item_list *criteria) +{ + struct search_data data; + + data.client = client; + data.criteria = criteria; + + return db_walk(name, findAddInDirectory, NULL, &data); +} + +static int directoryPrintSongInfo(struct song *song, void *data) { struct client *client = data; @@ -236,7 +258,7 @@ visitTag(struct client *client, struct strset *set, struct tag *tag = song->tag; if (tagType == LOCATE_TAG_FILE_TYPE) { - song_print_url(client, song); + song_print_uri(client, song); return; } diff --git a/src/dbUtils.h b/src/dbUtils.h index 1382c243e..914b6fa84 100644 --- a/src/dbUtils.h +++ b/src/dbUtils.h @@ -40,6 +40,10 @@ findSongsIn(struct client *client, const char *name, const struct locate_item_list *criteria); int +findAddIn(struct client *client, const char *name, + const struct locate_item_list *criteria); + +int searchStatsForSongsIn(struct client *client, const char *name, const struct locate_item_list *criteria); diff --git a/src/decoder/_flac_common.c b/src/decoder/_flac_common.c index e096750f3..d8802a6a3 100644 --- a/src/decoder/_flac_common.c +++ b/src/decoder/_flac_common.c @@ -170,11 +170,11 @@ flac_parse_comment(struct tag *tag, const char *char_tnum, assert(tag != NULL); if (flac_copy_comment(tag, entry, VORBIS_COMMENT_TRACK_KEY, - TAG_ITEM_TRACK, char_tnum) || + TAG_TRACK, char_tnum) || flac_copy_comment(tag, entry, VORBIS_COMMENT_DISC_KEY, - TAG_ITEM_DISC, char_tnum) || + TAG_DISC, char_tnum) || flac_copy_comment(tag, entry, "album artist", - TAG_ITEM_ALBUM_ARTIST, char_tnum)) + TAG_ALBUM_ARTIST, char_tnum)) return; for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) @@ -201,9 +201,8 @@ void flac_metadata_common_cb(const FLAC__StreamMetadata * block, switch (block->type) { case FLAC__METADATA_TYPE_STREAMINFO: - data->audio_format.bits = (int8_t)si->bits_per_sample; - data->audio_format.sample_rate = si->sample_rate; - data->audio_format.channels = (int8_t)si->channels; + audio_format_init(&data->audio_format, si->sample_rate, + si->bits_per_sample, si->channels); data->total_time = ((float)si->total_samples) / (si->sample_rate); break; case FLAC__METADATA_TYPE_VORBIS_COMMENT: diff --git a/src/decoder/audiofile_plugin.c b/src/decoder/audiofile_plugin.c index f66d90dc1..b4959f6c2 100644 --- a/src/decoder/audiofile_plugin.c +++ b/src/decoder/audiofile_plugin.c @@ -136,11 +136,9 @@ audiofile_stream_decode(struct decoder *decoder, struct input_stream *is) afSetVirtualSampleFormat(af_fp, AF_DEFAULT_TRACK, AF_SAMPFMT_TWOSCOMP, bits); afGetVirtualSampleFormat(af_fp, AF_DEFAULT_TRACK, &fs, &bits); - audio_format.bits = (uint8_t)bits; - audio_format.sample_rate = - (unsigned int)afGetRate(af_fp, AF_DEFAULT_TRACK); - audio_format.channels = - (uint8_t)afGetVirtualChannels(af_fp, AF_DEFAULT_TRACK); + + audio_format_init(&audio_format, afGetRate(af_fp, AF_DEFAULT_TRACK), + bits, afGetVirtualChannels(af_fp, AF_DEFAULT_TRACK)); if (!audio_format_valid(&audio_format)) { g_warning("Invalid audio format: %u:%u:%u\n", diff --git a/src/decoder/faad_plugin.c b/src/decoder/faad_plugin.c index 7b2806a4c..516f741c7 100644 --- a/src/decoder/faad_plugin.c +++ b/src/decoder/faad_plugin.c @@ -266,11 +266,7 @@ faad_decoder_init(faacDecHandle decoder, struct decoder_buffer *buffer, decoder_buffer_consume(buffer, nbytes); - *audio_format = (struct audio_format){ - .bits = 16, - .channels = channels, - .sample_rate = sample_rate, - }; + audio_format_init(audio_format, sample_rate, 16, channels); return true; } diff --git a/src/decoder/ffmpeg_plugin.c b/src/decoder/ffmpeg_plugin.c index 2f7ed6d1f..63f0f7ece 100644 --- a/src/decoder/ffmpeg_plugin.c +++ b/src/decoder/ffmpeg_plugin.c @@ -267,6 +267,7 @@ ffmpeg_decode_internal(struct ffmpeg_context *ctx) struct audio_format audio_format; enum decoder_command cmd; int total_time; + uint8_t bits; total_time = 0; @@ -275,13 +276,13 @@ ffmpeg_decode_internal(struct ffmpeg_context *ctx) } #if LIBAVCODEC_VERSION_INT >= ((51<<16)+(41<<8)+0) - audio_format.bits = (uint8_t) av_get_bits_per_sample_format(codec_context->sample_fmt); + bits = (uint8_t) av_get_bits_per_sample_format(codec_context->sample_fmt); #else /* XXX fixme 16-bit for older ffmpeg (13 Aug 2007) */ - audio_format.bits = (uint8_t) 16; + bits = (uint8_t) 16; #endif - audio_format.sample_rate = (unsigned int)codec_context->sample_rate; - audio_format.channels = codec_context->channels; + audio_format_init(&audio_format, codec_context->sample_rate, bits, + codec_context->channels); if (!audio_format_valid(&audio_format)) { g_warning("Invalid audio format: %u:%u:%u\n", @@ -342,8 +343,9 @@ static bool ffmpeg_copy_metadata(struct tag *tag, AVMetadata *m, enum tag_type type, const char *name) { - AVMetadataTag *mt = av_metadata_get(m, name, NULL, 0); - if (mt != NULL) + AVMetadataTag *mt = NULL; + + while ((mt = av_metadata_get(m, name, mt, 0)) != NULL) tag_add_item(tag, type, mt->value); return mt != NULL; } @@ -361,35 +363,35 @@ static bool ffmpeg_tag_internal(struct ffmpeg_context *ctx) #if LIBAVFORMAT_VERSION_INT >= ((52<<16)+(31<<8)+0) av_metadata_conv(f, NULL, f->iformat->metadata_conv); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_TITLE, "title"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_ARTIST, "author"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_ALBUM, "album"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_COMMENT, "comment"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_GENRE, "genre"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_TRACK, "track"); - ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_DATE, "year"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_TITLE, "title"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_ARTIST, "author"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_ALBUM, "album"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_COMMENT, "comment"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_GENRE, "genre"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_TRACK, "track"); + ffmpeg_copy_metadata(tag, f->metadata, TAG_DATE, "year"); #else if (f->author[0]) - tag_add_item(tag, TAG_ITEM_ARTIST, f->author); + tag_add_item(tag, TAG_ARTIST, f->author); if (f->title[0]) - tag_add_item(tag, TAG_ITEM_TITLE, f->title); + tag_add_item(tag, TAG_TITLE, f->title); if (f->album[0]) - tag_add_item(tag, TAG_ITEM_ALBUM, f->album); + tag_add_item(tag, TAG_ALBUM, f->album); if (f->track > 0) { char buffer[16]; snprintf(buffer, sizeof(buffer), "%d", f->track); - tag_add_item(tag, TAG_ITEM_TRACK, buffer); + tag_add_item(tag, TAG_TRACK, buffer); } if (f->comment[0]) - tag_add_item(tag, TAG_ITEM_COMMENT, f->comment); + tag_add_item(tag, TAG_COMMENT, f->comment); if (f->genre[0]) - tag_add_item(tag, TAG_ITEM_GENRE, f->genre); + tag_add_item(tag, TAG_GENRE, f->genre); if (f->year > 0) { char buffer[16]; snprintf(buffer, sizeof(buffer), "%d", f->year); - tag_add_item(tag, TAG_ITEM_DATE, buffer); + tag_add_item(tag, TAG_DATE, buffer); } #endif diff --git a/src/decoder/flac_plugin.c b/src/decoder/flac_plugin.c index 1d5d48d09..9692ba49f 100644 --- a/src/decoder/flac_plugin.c +++ b/src/decoder/flac_plugin.c @@ -300,6 +300,8 @@ flac_cue_tag_load(const char *file) FLAC__uint64 track_time = 0; #ifdef HAVE_CUE /* libcue */ FLAC__StreamMetadata* vc; + char* cs_filename; + FILE* cs_file; #endif /* libcue */ FLAC__StreamMetadata* si = FLAC__metadata_object_new(FLAC__METADATA_TYPE_STREAMINFO); FLAC__StreamMetadata* cs; @@ -329,16 +331,25 @@ flac_cue_tag_load(const char *file) FLAC__metadata_object_delete(vc); } + + if (tag == NULL) { + cs_filename = g_strconcat(file, ".cue", NULL); + + cs_file = fopen(cs_filename, "rt"); + g_free(cs_filename); + + if (cs_file != NULL) { + tag = cue_tag_file(cs_file, tnum); + fclose(cs_file); + } + } #endif /* libcue */ if (tag == NULL) tag = flac_tag_load(file, char_tnum); - if (char_tnum != NULL) - { - tag_add_item( tag, - TAG_ITEM_TRACK, - char_tnum); + if (char_tnum != NULL) { + tag_add_item(tag, TAG_TRACK, char_tnum); g_free(char_tnum); } diff --git a/src/decoder/mad_plugin.c b/src/decoder/mad_plugin.c index 1ef7183fa..27ddf655d 100644 --- a/src/decoder/mad_plugin.c +++ b/src/decoder/mad_plugin.c @@ -779,10 +779,10 @@ mp3_frame_duration(const struct mad_frame *frame) MAD_UNITS_MILLISECONDS) / 1000.0; } -static off_t +static goffset mp3_this_frame_offset(const struct mp3_data *data) { - off_t offset = data->input_stream->offset; + goffset offset = data->input_stream->offset; if (data->stream.this_frame != NULL) offset -= data->stream.bufend - data->stream.this_frame; @@ -792,7 +792,7 @@ mp3_this_frame_offset(const struct mp3_data *data) return offset; } -static off_t +static goffset mp3_rest_including_this_frame(const struct mp3_data *data) { return data->input_stream->size - mp3_this_frame_offset(data); @@ -804,7 +804,7 @@ mp3_rest_including_this_frame(const struct mp3_data *data) static void mp3_filesize_to_song_length(struct mp3_data *data) { - off_t rest = mp3_rest_including_this_frame(data); + goffset rest = mp3_rest_including_this_frame(data); if (rest > 0) { float frame_duration = mp3_frame_duration(&data->frame); @@ -1170,13 +1170,6 @@ mp3_read(struct mp3_data *data, struct replay_gain_info **replay_gain_info_r) return ret != DECODE_BREAK; } -static void mp3_audio_format(struct mp3_data *data, struct audio_format *af) -{ - af->bits = 24; - af->sample_rate = (data->frame).header.samplerate; - af->channels = MAD_NCHANNELS(&(data->frame).header); -} - static void mp3_decode(struct decoder *decoder, struct input_stream *input_stream) { @@ -1192,7 +1185,8 @@ mp3_decode(struct decoder *decoder, struct input_stream *input_stream) return; } - mp3_audio_format(&data, &audio_format); + audio_format_init(&audio_format, data.frame.header.samplerate, 24, + MAD_NCHANNELS(&data.frame.header)); decoder_initialized(decoder, &audio_format, data.input_stream->seekable, data.total_time); diff --git a/src/decoder/mikmod_plugin.c b/src/decoder/mikmod_plugin.c index 065c34319..850232f34 100644 --- a/src/decoder/mikmod_plugin.c +++ b/src/decoder/mikmod_plugin.c @@ -175,9 +175,7 @@ mod_decode(struct decoder *decoder, const char *path) return; } - audio_format.bits = 16; - audio_format.sample_rate = 44100; - audio_format.channels = 2; + audio_format_init(&audio_format, 44100, 16, 2); secPerByte = 1.0 / ((audio_format.bits * audio_format.channels / 8.0) * @@ -222,7 +220,7 @@ static struct tag *modTagDup(const char *file) title = g_strdup(Player_LoadTitle(path2)); g_free(path2); if (title) - tag_add_item(ret, TAG_ITEM_TITLE, title); + tag_add_item(ret, TAG_TITLE, title); return ret; } diff --git a/src/decoder/modplug_plugin.c b/src/decoder/modplug_plugin.c index f636f2fa6..301c80674 100644 --- a/src/decoder/modplug_plugin.c +++ b/src/decoder/modplug_plugin.c @@ -121,9 +121,7 @@ mod_decode(struct decoder *decoder, struct input_stream *is) return; } - audio_format.bits = 16; - audio_format.sample_rate = 44100; - audio_format.channels = 2; + audio_format_init(&audio_format, 44100, 16, 2); sec_perbyte = 1.0 / ((audio_format.bits * audio_format.channels / 8.0) * @@ -186,11 +184,11 @@ static struct tag *mod_tagdup(const char *file) return NULL; } ret = tag_new(); - ret->time = 0; + ret->time = ModPlug_GetLength(f) / 1000; title = g_strdup(ModPlug_GetName(f)); if (title) - tag_add_item(ret, TAG_ITEM_TITLE, title); + tag_add_item(ret, TAG_TITLE, title); g_free(title); ModPlug_Unload(f); diff --git a/src/decoder/mp4ff_plugin.c b/src/decoder/mp4ff_plugin.c index cf9382904..745d1e619 100644 --- a/src/decoder/mp4ff_plugin.c +++ b/src/decoder/mp4ff_plugin.c @@ -131,11 +131,7 @@ mp4_faad_new(mp4ff_t *mp4fh, int *track_r, struct audio_format *audio_format) } *track_r = track; - *audio_format = (struct audio_format){ - .bits = 16, - .channels = channels, - .sample_rate = sample_rate, - }; + audio_format_init(audio_format, sample_rate, 16, channels); if (!audio_format_valid(audio_format)) { g_warning("Invalid audio format: %u:%u:%u\n", @@ -395,22 +391,22 @@ mp4_tag_dup(const char *file) mp4ff_meta_get_by_index(mp4fh, i, &item, &value); if (0 == g_ascii_strcasecmp("artist", item)) { - tag_add_item(ret, TAG_ITEM_ARTIST, value); + tag_add_item(ret, TAG_ARTIST, value); } else if (0 == g_ascii_strcasecmp("title", item)) { - tag_add_item(ret, TAG_ITEM_TITLE, value); + tag_add_item(ret, TAG_TITLE, value); } else if (0 == g_ascii_strcasecmp("album", item)) { - tag_add_item(ret, TAG_ITEM_ALBUM, value); + tag_add_item(ret, TAG_ALBUM, value); } else if (0 == g_ascii_strcasecmp("track", item)) { - tag_add_item(ret, TAG_ITEM_TRACK, value); + tag_add_item(ret, TAG_TRACK, value); } else if (0 == g_ascii_strcasecmp("disc", item)) { /* Is that the correct id? */ - tag_add_item(ret, TAG_ITEM_DISC, value); + tag_add_item(ret, TAG_DISC, value); } else if (0 == g_ascii_strcasecmp("genre", item)) { - tag_add_item(ret, TAG_ITEM_GENRE, value); + tag_add_item(ret, TAG_GENRE, value); } else if (0 == g_ascii_strcasecmp("date", item)) { - tag_add_item(ret, TAG_ITEM_DATE, value); + tag_add_item(ret, TAG_DATE, value); } else if (0 == g_ascii_strcasecmp("writer", item)) { - tag_add_item(ret, TAG_ITEM_COMPOSER, value); + tag_add_item(ret, TAG_COMPOSER, value); } free(item); diff --git a/src/decoder/mpcdec_plugin.c b/src/decoder/mpcdec_plugin.c index 26349f93a..a684da104 100644 --- a/src/decoder/mpcdec_plugin.c +++ b/src/decoder/mpcdec_plugin.c @@ -193,9 +193,7 @@ mpcdec_decode(struct decoder *mpd_decoder, struct input_stream *is) mpc_demux_get_info(demux, &info); #endif - audio_format.bits = 24; - audio_format.channels = info.channels; - audio_format.sample_rate = info.sample_freq; + audio_format_init(&audio_format, info.sample_freq, 24, info.channels); if (!audio_format_valid(&audio_format)) { #ifndef MPC_IS_OLD_API diff --git a/src/decoder/mpg123_decoder_plugin.c b/src/decoder/mpg123_decoder_plugin.c new file mode 100644 index 000000000..20d9c4a54 --- /dev/null +++ b/src/decoder/mpg123_decoder_plugin.c @@ -0,0 +1,211 @@ +/* + * 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 "config.h" /* must be first for large file support */ +#include "decoder_api.h" + +#include <glib.h> + +#include <mpg123.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "mpg123" + +static bool +mpd_mpg123_init(G_GNUC_UNUSED const struct config_param *param) +{ + mpg123_init(); + + return true; +} + +static void +mpd_mpg123_finish(void) +{ + mpg123_exit(); +} + +/** + * Opens a file with an existing #mpg123_handle. + * + * @param handle a handle which was created before; on error, this + * function will not free it + * @param audio_format this parameter is filled after successful + * return + * @return true on success + */ +static bool +mpd_mpg123_open(mpg123_handle *handle, const char *path_fs, + struct audio_format *audio_format) +{ + char *path_dup; + int error; + int channels, encoding; + long rate; + + /* mpg123_open() wants a writable string :-( */ + path_dup = g_strdup(path_fs); + + error = mpg123_open(handle, path_dup); + g_free(path_dup); + if (error != MPG123_OK) { + g_warning("libmpg123 failed to open %s: %s", + path_fs, mpg123_plain_strerror(error)); + return false; + } + + /* obtain the audio format */ + + error = mpg123_getformat(handle, &rate, &channels, &encoding); + if (error != MPG123_OK) { + g_warning("mpg123_getformat() failed: %s", + mpg123_plain_strerror(error)); + return false; + } + + if (encoding != MPG123_ENC_SIGNED_16) { + /* other formats not yet implemented */ + g_warning("expected MPG123_ENC_SIGNED_16, got %d", encoding); + return false; + } + + audio_format_init(audio_format, rate, 16, channels); + if (!audio_format_valid(audio_format)) { + g_warning("invalid audio format"); + return false; + } + + return true; +} + +static void +mpd_mpg123_file_decode(struct decoder *decoder, const char *path_fs) +{ + struct audio_format audio_format; + mpg123_handle *handle; + int error; + off_t num_samples, position; + enum decoder_command cmd; + + /* open the file */ + + handle = mpg123_new(NULL, &error); + if (handle == NULL) { + g_warning("mpg123_new() failed: %s", + mpg123_plain_strerror(error)); + return; + } + + if (!mpd_mpg123_open(handle, path_fs, &audio_format)) { + mpg123_delete(handle); + return; + } + + num_samples = mpg123_length(handle); + + /* tell MPD core we're ready */ + + decoder_initialized(decoder, &audio_format, false, + (float)num_samples / + (float)audio_format.sample_rate); + + /* the decoder main loop */ + + do { + unsigned char buffer[8192]; + size_t nbytes; + + position = mpg123_tell(handle); + + /* decode */ + + error = mpg123_read(handle, buffer, sizeof(buffer), &nbytes); + if (error != MPG123_OK) { + if (error != MPG123_DONE) + g_warning("mpg123_read() failed: %s", + mpg123_plain_strerror(error)); + break; + } + + /* send to MPD */ + + cmd = decoder_data(decoder, NULL, buffer, nbytes, + (float)position / + (float)audio_format.sample_rate, + 0, NULL); + + /* seeking not yet implemented */ + } while (cmd == DECODE_COMMAND_NONE); + + /* cleanup */ + + mpg123_delete(handle); +} + +static struct tag * +mpd_mpg123_tag_dup(const char *path_fs) +{ + struct audio_format audio_format; + mpg123_handle *handle; + int error; + off_t num_samples; + struct tag *tag; + + handle = mpg123_new(NULL, &error); + if (handle == NULL) { + g_warning("mpg123_new() failed: %s", + mpg123_plain_strerror(error)); + return NULL; + } + + if (!mpd_mpg123_open(handle, path_fs, &audio_format)) { + mpg123_delete(handle); + return NULL; + } + + num_samples = mpg123_length(handle); + if (num_samples <= 0) { + mpg123_delete(handle); + return NULL; + } + + tag = tag_new(); + + tag->time = num_samples / audio_format.sample_rate; + + /* ID3 tag support not yet implemented */ + + mpg123_delete(handle); + return tag; +} + +static const char *const mpg123_suffixes[] = { + "mp3", + NULL +}; + +const struct decoder_plugin mpg123_decoder_plugin = { + .name = "mpg123", + .init = mpd_mpg123_init, + .finish = mpd_mpg123_finish, + .file_decode = mpd_mpg123_file_decode, + /* streaming not yet implemented */ + .tag_dup = mpd_mpg123_tag_dup, + .suffixes = mpg123_suffixes, +}; diff --git a/src/decoder/sidplay_plugin.cxx b/src/decoder/sidplay_plugin.cxx index c62e6b4b6..079b178fb 100644 --- a/src/decoder/sidplay_plugin.cxx +++ b/src/decoder/sidplay_plugin.cxx @@ -21,14 +21,180 @@ extern "C" { #include "../decoder_api.h" } +#include <errno.h> +#include <stdlib.h> #include <glib.h> #include <sidplay/sidplay2.h> #include <sidplay/builders/resid.h> +#include <sidplay/utils/SidTuneMod.h> #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "sidplay" +#define SUBTUNE_PREFIX "tune_" + +static GPatternSpec *path_with_subtune; +static const char *songlength_file; +static GKeyFile *songlength_database; + +static bool all_files_are_containers; +static unsigned default_songlength; + +static bool filter_setting; + +static GKeyFile * +sidplay_load_songlength_db(const char *path) +{ + GError *error = NULL; + gchar *data; + gsize size; + + if (!g_file_get_contents(path, &data, &size, &error)) { + g_warning("unable to read songlengths file %s: %s", + path, error->message); + g_error_free(error); + return NULL; + } + + /* replace any ; comment characters with # */ + for (gsize i = 0; i < size; i++) + if (data[i] == ';') + data[i] = '#'; + + GKeyFile *db = g_key_file_new(); + bool success = g_key_file_load_from_data(db, data, size, + G_KEY_FILE_NONE, &error); + g_free(data); + if (!success) { + g_warning("unable to parse songlengths file %s: %s", + path, error->message); + g_error_free(error); + g_key_file_free(db); + return NULL; + } + + g_key_file_set_list_separator(db, ' '); + return db; +} + +static bool +sidplay_init(const struct config_param *param) +{ + /* read the songlengths database file */ + songlength_file=config_get_block_string(param, + "songlength_database", NULL); + if (songlength_file != NULL) + songlength_database = sidplay_load_songlength_db(songlength_file); + + default_songlength=config_get_block_unsigned(param, + "default_songlength", 0); + + all_files_are_containers=config_get_block_bool(param, + "all_files_are_containers", true); + + path_with_subtune=g_pattern_spec_new( + "*/" SUBTUNE_PREFIX "???.sid"); + + filter_setting=config_get_block_bool(param, "filter", true); + + return true; +} + +void +sidplay_finish() +{ + g_pattern_spec_free(path_with_subtune); + + if(songlength_database) + g_key_file_free(songlength_database); +} + +/** + * returns the file path stripped of any /tune_xxx.sid subtune + * suffix + */ +static char * +get_container_name(const char *path_fs) +{ + char *path_container=g_strdup(path_fs); + + if(!g_pattern_match(path_with_subtune, + strlen(path_container), path_container, NULL)) + return path_container; + + char *ptr=g_strrstr(path_container, "/" SUBTUNE_PREFIX); + if(ptr) *ptr='\0'; + + return path_container; +} + +/** + * returns tune number from file.sid/tune_xxx.sid style path or 1 if + * no subtune is appended + */ +static int +get_song_num(const char *path_fs) +{ + if(g_pattern_match(path_with_subtune, + strlen(path_fs), path_fs, NULL)) { + char *sub=g_strrstr(path_fs, "/" SUBTUNE_PREFIX); + if(!sub) return 1; + + sub+=strlen("/" SUBTUNE_PREFIX); + int song_num=strtol(sub, NULL, 10); + + if (errno == EINVAL) + return 1; + else + return song_num; + } else + return 1; +} + +/* get the song length in seconds */ +static int +get_song_length(const char *path_fs) +{ + if (songlength_database == NULL) + return -1; + + gchar *sid_file=get_container_name(path_fs); + SidTuneMod tune(sid_file); + g_free(sid_file); + if(!tune) { + g_warning("failed to load file for calculating md5 sum"); + return -1; + } + char md5sum[SIDTUNE_MD5_LENGTH+1]; + tune.createMD5(md5sum); + + int song_num=get_song_num(path_fs); + + gsize num_items; + gchar **values=g_key_file_get_string_list(songlength_database, + "Database", md5sum, &num_items, NULL); + if(!values || song_num>num_items) { + g_strfreev(values); + return -1; + } + + int minutes=strtol(values[song_num-1], NULL, 10); + if(errno==EINVAL) minutes=0; + + int seconds; + char *ptr=strchr(values[song_num-1], ':'); + if(ptr) { + seconds=strtol(ptr+1, NULL, 10); + if(errno==EINVAL) seconds=0; + } else + seconds=0; + + g_strfreev(values); + + return (minutes*60)+seconds; +} + static void sidplay_file_decode(struct decoder *decoder, const char *path_fs) { @@ -36,13 +202,19 @@ sidplay_file_decode(struct decoder *decoder, const char *path_fs) /* load the tune */ - SidTune tune(path_fs, NULL, true); + char *path_container=get_container_name(path_fs); + SidTune tune(path_container, NULL, true); + g_free(path_container); if (!tune) { g_warning("failed to load file"); return; } - tune.selectSong(1); + int song_num=get_song_num(path_fs); + tune.selectSong(song_num); + + int song_len=get_song_length(path_fs); + if(song_len==-1) song_len=default_songlength; /* initialize the player */ @@ -67,7 +239,7 @@ sidplay_file_decode(struct decoder *decoder, const char *path_fs) return; } - builder.filter(false); + builder.filter(filter_setting); if (!builder) { g_warning("ReSIDBuilder.filter() failed"); return; @@ -103,14 +275,14 @@ sidplay_file_decode(struct decoder *decoder, const char *path_fs) /* initialize the MPD decoder */ struct audio_format audio_format; - audio_format.sample_rate = 48000; - audio_format.bits = 16; - audio_format.channels = 2; + audio_format_init(&audio_format, 48000, 16, 2); - decoder_initialized(decoder, &audio_format, false, -1); + decoder_initialized(decoder, &audio_format, true, (float)song_len); /* .. and play */ + float data_time=0; + int timebase=player.timebase(); enum decoder_command cmd; do { char buffer[4096]; @@ -121,29 +293,105 @@ sidplay_file_decode(struct decoder *decoder, const char *path_fs) break; cmd = decoder_data(decoder, NULL, buffer, nbytes, - 0, 0, NULL); - } while (cmd == DECODE_COMMAND_NONE); + data_time, 0, NULL); + + data_time=player.time()/timebase; + + if(cmd==DECODE_COMMAND_SEEK) { + int target_time=decoder_seek_where(decoder); + + /* can't rewind so return to zero and seek forward */ + if(target_time<data_time) { + player.stop(); + data_time=0; + } + + /* ignore data until target time is reached */ + while(data_time<target_time) { + nbytes=player.play(buffer, sizeof(buffer)); + if(nbytes==0) + break; + data_time=player.time()/timebase; + } + + decoder_command_finished(decoder); + } + + if(song_len && data_time>=(float)song_len) + break; + + } while (cmd != DECODE_COMMAND_STOP); } static struct tag * sidplay_tag_dup(const char *path_fs) { - SidTune tune(path_fs, NULL, true); + int song_num=get_song_num(path_fs); + char *path_container=get_container_name(path_fs); + + SidTune tune(path_container, NULL, true); + g_free(path_container); if (!tune) return NULL; const SidTuneInfo &info = tune.getInfo(); struct tag *tag = tag_new(); + /* title */ + const char *title; if (info.numberOfInfoStrings > 0 && info.infoString[0] != NULL) - tag_add_item(tag, TAG_ITEM_TITLE, info.infoString[0]); - + title=info.infoString[0]; + else + title=""; + + if(info.songs>1) { + char *tag_title=g_strdup_printf("%s (%d/%d)", + title, song_num, info.songs); + tag_add_item(tag, TAG_TITLE, tag_title); + g_free(tag_title); + } else + tag_add_item(tag, TAG_TITLE, title); + + /* artist */ if (info.numberOfInfoStrings > 1 && info.infoString[1] != NULL) - tag_add_item(tag, TAG_ITEM_ARTIST, info.infoString[1]); + tag_add_item(tag, TAG_ARTIST, info.infoString[1]); + + /* track */ + char *track=g_strdup_printf("%d", song_num); + tag_add_item(tag, TAG_TRACK, track); + g_free(track); + + /* time */ + int song_len=get_song_length(path_fs); + if(song_len!=-1) tag->time=song_len; return tag; } +static char * +sidplay_container_scan(const char *path_fs, const unsigned int tnum) +{ + SidTune tune(path_fs, NULL, true); + if (!tune) + return NULL; + + const SidTuneInfo &info=tune.getInfo(); + + /* Don't treat sids containing a single tune + as containers */ + if(!all_files_are_containers && info.songs<2) + return NULL; + + /* Construct container/tune path names, eg. + Delta.sid/tune_001.sid */ + if(tnum<=info.songs) { + char *subtune= g_strdup_printf( + SUBTUNE_PREFIX "%03u.sid", tnum); + return subtune; + } else + return NULL; +} + static const char *const sidplay_suffixes[] = { "sid", NULL @@ -152,12 +400,12 @@ static const char *const sidplay_suffixes[] = { extern const struct decoder_plugin sidplay_decoder_plugin; const struct decoder_plugin sidplay_decoder_plugin = { "sidplay", - NULL, /* init() */ - NULL, /* finish() */ + sidplay_init, + sidplay_finish, NULL, /* stream_decode() */ sidplay_file_decode, sidplay_tag_dup, - NULL, /* container_scan */ + sidplay_container_scan, sidplay_suffixes, NULL, /* mime_types */ }; diff --git a/src/decoder/sndfile_decoder_plugin.c b/src/decoder/sndfile_decoder_plugin.c new file mode 100644 index 000000000..59993ef89 --- /dev/null +++ b/src/decoder/sndfile_decoder_plugin.c @@ -0,0 +1,244 @@ +/* + * 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 "decoder_api.h" + +#include <sndfile.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "sndfile" + +static sf_count_t +sndfile_vio_get_filelen(void *user_data) +{ + const struct input_stream *is = user_data; + + return is->size; +} + +static sf_count_t +sndfile_vio_seek(sf_count_t offset, int whence, void *user_data) +{ + struct input_stream *is = user_data; + bool success; + + success = input_stream_seek(is, offset, whence); + if (!success) + return -1; + + return is->offset; +} + +static sf_count_t +sndfile_vio_read(void *ptr, sf_count_t count, void *user_data) +{ + struct input_stream *is = user_data; + size_t nbytes; + + nbytes = input_stream_read(is, ptr, count); + if (nbytes == 0 && is->error != 0) + return -1; + + return nbytes; +} + +static sf_count_t +sndfile_vio_write(G_GNUC_UNUSED const void *ptr, + G_GNUC_UNUSED sf_count_t count, + G_GNUC_UNUSED void *user_data) +{ + /* no writing! */ + return -1; +} + +static sf_count_t +sndfile_vio_tell(void *user_data) +{ + const struct input_stream *is = user_data; + + return is->offset; +} + +/** + * This SF_VIRTUAL_IO implementation wraps MPD's #input_stream to a + * libsndfile stream. + */ +static SF_VIRTUAL_IO vio = { + .get_filelen = sndfile_vio_get_filelen, + .seek = sndfile_vio_seek, + .read = sndfile_vio_read, + .write = sndfile_vio_write, + .tell = sndfile_vio_tell, +}; + +/** + * Converts a frame number to a timestamp (in seconds). + */ +static float +frame_to_time(sf_count_t frame, const struct audio_format *audio_format) +{ + return (float)frame / (float)audio_format->sample_rate; +} + +/** + * Converts a timestamp (in seconds) to a frame number. + */ +static sf_count_t +time_to_frame(float t, const struct audio_format *audio_format) +{ + return (sf_count_t)(t * audio_format->sample_rate); +} + +static void +sndfile_stream_decode(struct decoder *decoder, struct input_stream *is) +{ + SNDFILE *sf; + SF_INFO info; + struct audio_format audio_format; + size_t frame_size; + sf_count_t read_frames, num_frames, position = 0; + int buffer[4096]; + enum decoder_command cmd; + + info.format = 0; + + sf = sf_open_virtual(&vio, SFM_READ, &info, is); + if (sf == NULL) { + g_warning("sf_open_virtual() failed"); + return; + } + + /* for now, always read 32 bit samples. Later, we could lower + MPD's CPU usage by reading 16 bit samples with + sf_readf_short() on low-quality source files. */ + audio_format_init(&audio_format, info.samplerate, 32, info.channels); + + if (!audio_format_valid(&audio_format)) { + g_warning("invalid audio format"); + return; + } + + decoder_initialized(decoder, &audio_format, info.seekable, + frame_to_time(info.frames, &audio_format)); + + frame_size = audio_format_frame_size(&audio_format); + read_frames = sizeof(buffer) / frame_size; + + do { + num_frames = sf_readf_int(sf, buffer, read_frames); + if (num_frames <= 0) + break; + + cmd = decoder_data(decoder, is, + buffer, num_frames * frame_size, + frame_to_time(position, &audio_format), + 0, NULL); + if (cmd == DECODE_COMMAND_SEEK) { + sf_count_t c = + time_to_frame(decoder_seek_where(decoder), + &audio_format); + c = sf_seek(sf, c, SEEK_SET); + if (c < 0) + decoder_seek_error(decoder); + else + decoder_command_finished(decoder); + cmd = DECODE_COMMAND_NONE; + } + } while (cmd == DECODE_COMMAND_NONE); + + sf_close(sf); +} + +static struct tag * +sndfile_tag_dup(const char *path_fs) +{ + SNDFILE *sf; + SF_INFO info; + struct tag *tag; + const char *p; + + info.format = 0; + + sf = sf_open(path_fs, SFM_READ, &info); + if (sf == NULL) + return NULL; + + if (!audio_valid_sample_rate(info.samplerate)) { + sf_close(sf); + g_warning("Invalid sample rate in %s\n", path_fs); + return NULL; + } + + tag = tag_new(); + tag->time = info.frames / info.samplerate; + + p = sf_get_string(sf, SF_STR_TITLE); + if (p != NULL) + tag_add_item(tag, TAG_TITLE, p); + + p = sf_get_string(sf, SF_STR_ARTIST); + if (p != NULL) + tag_add_item(tag, TAG_ARTIST, p); + + p = sf_get_string(sf, SF_STR_DATE); + if (p != NULL) + tag_add_item(tag, TAG_DATE, p); + + sf_close(sf); + + return tag; +} + +static const char *const sndfile_suffixes[] = { + "wav", "aiff", "aif", /* Microsoft / SGI / Apple */ + "au", "snd", /* Sun / DEC / NeXT */ + "paf", /* Paris Audio File */ + "iff", "svx", /* Commodore Amiga IFF / SVX */ + "sf", /* IRCAM */ + "voc", /* Creative */ + "w64", /* Soundforge */ + "pvf", /* Portable Voice Format */ + "xi", /* Fasttracker */ + "htk", /* HMM Tool Kit */ + "caf", /* Apple */ + "sd2", /* Sound Designer II */ + + /* libsndfile also supports FLAC and Ogg Vorbis, but only by + linking with libFLAC and libvorbis - we can do better, we + have native plugins for these libraries */ + + NULL +}; + +static const char *const sndfile_mime_types[] = { + "audio/x-wav", + "audio/x-aiff", + + /* what are the MIME types of the other supported formats? */ + + NULL +}; + +const struct decoder_plugin sndfile_decoder_plugin = { + .name = "sndfile", + .stream_decode = sndfile_stream_decode, + .tag_dup = sndfile_tag_dup, + .suffixes = sndfile_suffixes, + .mime_types = sndfile_mime_types, +}; diff --git a/src/decoder/vorbis_plugin.c b/src/decoder/vorbis_plugin.c index d4f81e91f..2ac9c3bcb 100644..100755 --- a/src/decoder/vorbis_plugin.c +++ b/src/decoder/vorbis_plugin.c @@ -24,6 +24,7 @@ #include "uri.h" #ifndef HAVE_TREMOR +#define OV_EXCLUDE_STATIC_CALLBACKS #include <vorbis/vorbisfile.h> #else #include <tremor/ivorbisfile.h> @@ -176,11 +177,11 @@ vorbis_parse_comment(struct tag *tag, const char *comment) assert(tag != NULL); if (vorbis_copy_comment(tag, comment, VORBIS_COMMENT_TRACK_KEY, - TAG_ITEM_TRACK) || + TAG_TRACK) || vorbis_copy_comment(tag, comment, VORBIS_COMMENT_DISC_KEY, - TAG_ITEM_DISC) || + TAG_DISC) || vorbis_copy_comment(tag, comment, "album artist", - TAG_ITEM_ALBUM_ARTIST)) + TAG_ALBUM_ARTIST)) return; for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) @@ -324,8 +325,7 @@ vorbis_stream_decode(struct decoder *decoder, vorbis_info *vi = ov_info(&vf, -1); struct replay_gain_info *new_rgi; - audio_format.channels = vi->channels; - audio_format.sample_rate = vi->rate; + audio_format_init(&audio_format, vi->rate, 16, vi->channels); if (!audio_format_valid(&audio_format)) { g_warning("Invalid audio format: %u:%u:%u\n", @@ -378,7 +378,7 @@ vorbis_tag_dup(const char *file) FILE *fp; OggVorbis_File vf; - fp = fopen(file, "r"); + fp = fopen(file, "rb"); if (!fp) { return NULL; } diff --git a/src/decoder/wavpack_plugin.c b/src/decoder/wavpack_plugin.c index 821536fb5..3e7c0cef7 100644 --- a/src/decoder/wavpack_plugin.c +++ b/src/decoder/wavpack_plugin.c @@ -41,17 +41,17 @@ static struct { const char *name; enum tag_type type; } tagtypes[] = { - { "artist", TAG_ITEM_ARTIST }, - { "album", TAG_ITEM_ALBUM }, - { "title", TAG_ITEM_TITLE }, - { "track", TAG_ITEM_TRACK }, - { "name", TAG_ITEM_NAME }, - { "genre", TAG_ITEM_GENRE }, - { "date", TAG_ITEM_DATE }, - { "composer", TAG_ITEM_COMPOSER }, - { "performer", TAG_ITEM_PERFORMER }, - { "comment", TAG_ITEM_COMMENT }, - { "disc", TAG_ITEM_DISC }, + { "artist", TAG_ARTIST }, + { "album", TAG_ALBUM }, + { "title", TAG_TITLE }, + { "track", TAG_TRACK }, + { "name", TAG_NAME }, + { "genre", TAG_GENRE }, + { "date", TAG_DATE }, + { "composer", TAG_COMPOSER }, + { "performer", TAG_PERFORMER }, + { "comment", TAG_COMMENT }, + { "disc", TAG_DISC }, }; /** A pointer type for format converter function. */ @@ -145,9 +145,9 @@ wavpack_decode(struct decoder *decoder, WavpackContext *wpc, bool can_seek, int bytes_per_sample, output_sample_size; int position; - audio_format.sample_rate = WavpackGetSampleRate(wpc); - audio_format.channels = WavpackGetReducedChannels(wpc); - audio_format.bits = WavpackGetBitsPerSample(wpc); + audio_format_init(&audio_format, WavpackGetSampleRate(wpc), + WavpackGetBitsPerSample(wpc), + WavpackGetReducedChannels(wpc)); /* round bitwidth to 8-bit units */ audio_format.bits = (audio_format.bits + 7) & (~7); diff --git a/src/decoder_api.c b/src/decoder_api.c index 2ece3bb98..070151a67 100644 --- a/src/decoder_api.c +++ b/src/decoder_api.c @@ -57,8 +57,11 @@ void decoder_initialized(G_GNUC_UNUSED struct decoder * decoder, dc.seekable = seekable; dc.total_time = total_time; + decoder_lock(); dc.state = DECODE_STATE_DECODE; - notify_signal(&pc.notify); + decoder_unlock(); + + player_lock_signal(); g_debug("audio_format=%u:%u:%u, seekable=%s", dc.in_audio_format.sample_rate, dc.in_audio_format.bits, @@ -88,6 +91,8 @@ enum decoder_command decoder_get_command(G_GNUC_UNUSED struct decoder * decoder) void decoder_command_finished(G_GNUC_UNUSED struct decoder * decoder) { + decoder_lock(); + assert(dc.command != DECODE_COMMAND_NONE); assert(dc.command != DECODE_COMMAND_SEEK || dc.seek_error || decoder->seeking); @@ -105,7 +110,9 @@ void decoder_command_finished(G_GNUC_UNUSED struct decoder * decoder) } dc.command = DECODE_COMMAND_NONE; - notify_signal(&pc.notify); + decoder_unlock(); + + player_lock_signal(); } double decoder_seek_where(G_GNUC_UNUSED struct decoder * decoder) @@ -177,7 +184,7 @@ do_send_tag(struct decoder *decoder, struct input_stream *is, /* there is a partial chunk - flush it, we want the tag in a new chunk */ decoder_flush_chunk(decoder); - notify_signal(&pc.notify); + player_lock_signal(); } assert(decoder->chunk == NULL); @@ -225,21 +232,24 @@ decoder_data(struct decoder *decoder, struct replay_gain_info *replay_gain_info) { const char *data = _data; + GError *error = NULL; + enum decoder_command cmd; assert(dc.state == DECODE_STATE_DECODE); assert(dc.pipe != NULL); assert(length % audio_format_frame_size(&dc.in_audio_format) == 0); - if (dc.command == DECODE_COMMAND_STOP || - dc.command == DECODE_COMMAND_SEEK || + decoder_lock(); + cmd = dc.command; + decoder_unlock(); + + if (cmd == DECODE_COMMAND_STOP || cmd == DECODE_COMMAND_SEEK || length == 0) - return dc.command; + return cmd; /* send stream tags */ if (update_stream_tag(decoder, is)) { - enum decoder_command cmd; - if (decoder->decoder_tag != NULL) { /* merge with tag from decoder plugin */ struct tag *tag; @@ -259,14 +269,15 @@ decoder_data(struct decoder *decoder, if (!audio_format_equals(&dc.in_audio_format, &dc.out_audio_format)) { data = pcm_convert(&decoder->conv_state, &dc.in_audio_format, data, length, - &dc.out_audio_format, &length); - - /* under certain circumstances, pcm_convert() may - return an empty buffer - this condition should be - investigated further, but for now, do this check as - a workaround: */ - if (data == NULL) - return DECODE_COMMAND_NONE; + &dc.out_audio_format, &length, + &error); + if (data == NULL) { + /* the PCM conversion has failed - stop + playback, since we have no better way to + bail out */ + g_warning("%s", error->message); + return DECODE_COMMAND_STOP; + } } while (length > 0) { @@ -286,7 +297,7 @@ decoder_data(struct decoder *decoder, if (dest == NULL) { /* the chunk is full, flush it */ decoder_flush_chunk(decoder); - notify_signal(&pc.notify); + player_lock_signal(); continue; } @@ -301,8 +312,7 @@ decoder_data(struct decoder *decoder, /* apply replay gain or normalization */ - if (replay_gain_info != NULL && - replay_gain_mode != REPLAY_GAIN_OFF) + if (replay_gain_mode != REPLAY_GAIN_OFF) replay_gain_apply(replay_gain_info, dest, nbytes, &dc.out_audio_format); else if (normalizationEnabled) @@ -314,7 +324,7 @@ decoder_data(struct decoder *decoder, if (full) { /* the chunk is full, flush it */ decoder_flush_chunk(decoder); - notify_signal(&pc.notify); + player_lock_signal(); } data += nbytes; diff --git a/src/decoder_api.h b/src/decoder_api.h index 37090d8d0..2ecd98ce7 100644 --- a/src/decoder_api.h +++ b/src/decoder_api.h @@ -17,15 +17,16 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#ifndef MPD_DECODER_API_H -#define MPD_DECODER_API_H - -/* +/*! \file + * \brief The MPD Decoder API + * * This is the public API which is used by decoder plugins to * communicate with the mpd core. - * */ +#ifndef MPD_DECODER_API_H +#define MPD_DECODER_API_H + #include "decoder_command.h" #include "decoder_plugin.h" #include "input_stream.h" @@ -36,56 +37,97 @@ #include <stdbool.h> - /** * Notify the player thread that it has finished initialization and * that it has read the song's meta data. + * + * @param decoder the decoder object + * @param audio_format the audio format which is going to be sent to + * decoder_data() + * @param seekable true if the song is seekable + * @param total_time the total number of seconds in this song; -1 if unknown */ -void decoder_initialized(struct decoder * decoder, - const struct audio_format *audio_format, - bool seekable, float total_time); +void +decoder_initialized(struct decoder *decoder, + const struct audio_format *audio_format, + bool seekable, float total_time); /** * Returns the URI of the current song in UTF-8 encoding. * - * The return value is allocated on the heap, and must be freed by the - * caller. + * @param decoder the decoder object + * @return an allocated string which must be freed with g_free() */ -char *decoder_get_uri(struct decoder *decoder); +char * +decoder_get_uri(struct decoder *decoder); -enum decoder_command decoder_get_command(struct decoder * decoder); +/** + * Determines the pending decoder command. + * + * @param decoder the decoder object + * @return the current command, or DECODE_COMMAND_NONE if there is no + * command pending + */ +enum decoder_command +decoder_get_command(struct decoder *decoder); /** * Called by the decoder when it has performed the requested command * (dc->command). This function resets dc->command and wakes up the * player thread. + * + * @param decoder the decoder object */ -void decoder_command_finished(struct decoder * decoder); +void +decoder_command_finished(struct decoder *decoder); -double decoder_seek_where(struct decoder * decoder); +/** + * Call this when you have received the DECODE_COMMAND_SEEK command. + * + * @param decoder the decoder object + * @return the destination position for the week + */ +double +decoder_seek_where(struct decoder *decoder); -void decoder_seek_error(struct decoder * decoder); +/** + * Call this right before decoder_command_finished() when seeking has + * failed. + * + * @param decoder the decoder object + */ +void +decoder_seek_error(struct decoder *decoder); /** - * Blocking read from the input stream. Returns the number of bytes - * read, or 0 if one of the following occurs: end of file; error; - * command (like SEEK or STOP). + * Blocking read from the input stream. + * + * @param decoder the decoder object + * @param is the input stream to read from + * @param buffer the destination buffer + * @param length the maximum number of bytes to read + * @return the number of bytes read, or 0 if one of the following + * occurs: end of file; error; command (like SEEK or STOP). */ -size_t decoder_read(struct decoder *decoder, - struct input_stream *inStream, - void *buffer, size_t length); +size_t +decoder_read(struct decoder *decoder, struct input_stream *is, + void *buffer, size_t length); /** * This function is called by the decoder plugin when it has * successfully decoded block of input data. * - * We send inStream for buffering the inputStream while waiting to - * send the next chunk + * @param decoder the decoder object + * @param is an input stream which is buffering while we are waiting + * for the player + * @param data the source buffer + * @param length the number of bytes in the buffer + * @return the current command, or DECODE_COMMAND_NONE if there is no + * command pending */ enum decoder_command -decoder_data(struct decoder *decoder, - struct input_stream *inStream, - const void *data, size_t datalen, +decoder_data(struct decoder *decoder, struct input_stream *is, + const void *data, size_t length, float data_time, uint16_t bitRate, struct replay_gain_info *replay_gain_info); @@ -93,8 +135,12 @@ decoder_data(struct decoder *decoder, * This function is called by the decoder plugin when it has * successfully decoded a tag. * + * @param decoder the decoder object * @param is an input stream which is buffering while we are waiting * for the player + * @param tag the tag to send + * @return the current command, or DECODE_COMMAND_NONE if there is no + * command pending */ enum decoder_command decoder_tag(struct decoder *decoder, struct input_stream *is, diff --git a/src/decoder_control.c b/src/decoder_control.c index 44bb63e15..d7f5808fc 100644 --- a/src/decoder_control.c +++ b/src/decoder_control.c @@ -18,6 +18,7 @@ */ #include "decoder_control.h" +#include "player_control.h" #include <assert.h> @@ -25,46 +26,69 @@ struct decoder_control dc; void dc_init(void) { - notify_init(&dc.notify); + dc.mutex = g_mutex_new(); + dc.cond = g_cond_new(); + dc.state = DECODE_STATE_STOP; dc.command = DECODE_COMMAND_NONE; } void dc_deinit(void) { - notify_deinit(&dc.notify); + g_cond_free(dc.cond); + g_mutex_free(dc.mutex); } -void -dc_command_wait(struct notify *notify) +static void +dc_command_wait_locked(void) { while (dc.command != DECODE_COMMAND_NONE) { - notify_signal(&dc.notify); - notify_wait(notify); + decoder_signal(); + player_wait_decoder(); } } +void +dc_command_wait(void) +{ + decoder_lock(); + dc_command_wait_locked(); + decoder_unlock(); +} + static void -dc_command(struct notify *notify, enum decoder_command cmd) +dc_command_locked(enum decoder_command cmd) { dc.command = cmd; - dc_command_wait(notify); + dc_command_wait_locked(); +} + +static void +dc_command(enum decoder_command cmd) +{ + decoder_lock(); + dc_command_locked(cmd); + decoder_unlock(); } static void dc_command_async(enum decoder_command cmd) { + decoder_lock(); + dc.command = cmd; - notify_signal(&dc.notify); + decoder_signal(); + + decoder_unlock(); } void -dc_start(struct notify *notify, struct song *song) +dc_start(struct song *song) { assert(dc.pipe != NULL); assert(song != NULL); dc.next_song = song; - dc_command(notify, DECODE_COMMAND_START); + dc_command(DECODE_COMMAND_START); } void @@ -78,21 +102,25 @@ dc_start_async(struct song *song) } void -dc_stop(struct notify *notify) +dc_stop(void) { + decoder_lock(); + if (dc.command != DECODE_COMMAND_NONE) /* Attempt to cancel the current command. If it's too late and the decoder thread is already executing the old command, we'll call STOP again in this function (see below). */ - dc_command(notify, DECODE_COMMAND_STOP); + dc_command_locked(DECODE_COMMAND_STOP); if (dc.state != DECODE_STATE_STOP && dc.state != DECODE_STATE_ERROR) - dc_command(notify, DECODE_COMMAND_STOP); + dc_command_locked(DECODE_COMMAND_STOP); + + decoder_unlock(); } bool -dc_seek(struct notify *notify, double where) +dc_seek(double where) { assert(dc.state != DECODE_STATE_START); assert(where >= 0.0); @@ -103,7 +131,7 @@ dc_seek(struct notify *notify, double where) dc.seek_where = where; dc.seek_error = false; - dc_command(notify, DECODE_COMMAND_SEEK); + dc_command(DECODE_COMMAND_SEEK); if (dc.seek_error) return false; diff --git a/src/decoder_control.h b/src/decoder_control.h index 6a04a1617..cab6c69c7 100644 --- a/src/decoder_control.h +++ b/src/decoder_control.h @@ -22,7 +22,8 @@ #include "decoder_command.h" #include "audio_format.h" -#include "notify.h" + +#include <glib.h> #include <assert.h> @@ -45,14 +46,25 @@ struct decoder_control { thread isn't running */ GThread *thread; - struct notify notify; + /** + * This lock protects #state and #command. + */ + GMutex *mutex; + + /** + * Trigger this object after you have modified #command. This + * is also used by the decoder thread to notify the caller + * when it has finished a command. + */ + GCond *cond; + + enum decoder_state state; + enum decoder_command command; - volatile enum decoder_state state; - volatile enum decoder_command command; bool quit; bool seek_error; bool seekable; - volatile double seek_where; + double seek_where; /** the format of the song file */ struct audio_format in_audio_format; @@ -77,6 +89,46 @@ void dc_init(void); void dc_deinit(void); +/** + * Locks the #decoder_control object. + */ +static inline void +decoder_lock(void) +{ + g_mutex_lock(dc.mutex); +} + +/** + * Unlocks the #decoder_control object. + */ +static inline void +decoder_unlock(void) +{ + g_mutex_unlock(dc.mutex); +} + +/** + * Waits for a signal on the #decoder_control object. This function + * is only valid in the decoder thread. The object must be locked + * prior to calling this function. + */ +static inline void +decoder_wait(void) +{ + g_cond_wait(dc.cond, dc.mutex); +} + +/** + * Signals the #decoder_control object. This function is only valid + * in the player thread. The object should be locked prior to calling + * this function. + */ +static inline void +decoder_signal(void) +{ + g_cond_signal(dc.cond); +} + static inline bool decoder_is_idle(void) { return (dc.state == DECODE_STATE_STOP || @@ -97,6 +149,39 @@ static inline bool decoder_has_failed(void) return dc.state == DECODE_STATE_ERROR; } +static inline bool decoder_lock_is_idle(void) +{ + bool ret; + + decoder_lock(); + ret = decoder_is_idle(); + decoder_unlock(); + + return ret; +} + +static inline bool decoder_lock_is_starting(void) +{ + bool ret; + + decoder_lock(); + ret = decoder_is_starting(); + decoder_unlock(); + + return ret; +} + +static inline bool decoder_lock_has_failed(void) +{ + bool ret; + + decoder_lock(); + ret = decoder_has_failed(); + decoder_unlock(); + + return ret; +} + static inline struct song * decoder_current_song(void) { @@ -115,19 +200,19 @@ decoder_current_song(void) } void -dc_command_wait(struct notify *notify); +dc_command_wait(void); void -dc_start(struct notify *notify, struct song *song); +dc_start(struct song *song); void dc_start_async(struct song *song); void -dc_stop(struct notify *notify); +dc_stop(void); bool -dc_seek(struct notify *notify, double where); +dc_seek(double where); void dc_quit(void); diff --git a/src/decoder_internal.c b/src/decoder_internal.c index 4a56fa5f3..db90333b4 100644 --- a/src/decoder_internal.c +++ b/src/decoder_internal.c @@ -28,6 +28,24 @@ #include <assert.h> /** + * This is a wrapper for input_stream_buffer(). It assumes that the + * decoder is currently locked, and temporarily unlocks it while + * calling input_stream_buffer(). We shouldn't hold the lock during a + * potentially blocking operation. + */ +static int +decoder_input_buffer(struct input_stream *is) +{ + int ret; + + decoder_unlock(); + ret = input_stream_buffer(is) > 0; + decoder_lock(); + + return ret; +} + +/** * All chunks are full of decoded data; wait for the player to free * one. */ @@ -38,9 +56,9 @@ need_chunks(struct input_stream *is, bool do_wait) dc.command == DECODE_COMMAND_SEEK) return dc.command; - if ((is == NULL || input_stream_buffer(is) <= 0) && do_wait) { - notify_wait(&dc.notify); - notify_signal(&pc.notify); + if ((is == NULL || decoder_input_buffer(is) <= 0) && do_wait) { + decoder_wait(); + player_signal(); return dc.command; } @@ -63,7 +81,9 @@ decoder_get_chunk(struct decoder *decoder, struct input_stream *is) if (decoder->chunk != NULL) return decoder->chunk; + decoder_lock(); cmd = need_chunks(is, true); + decoder_unlock(); } while (cmd == DECODE_COMMAND_NONE); return NULL; diff --git a/src/decoder_list.c b/src/decoder_list.c index a42585e34..d30611e1b 100644 --- a/src/decoder_list.c +++ b/src/decoder_list.c @@ -28,9 +28,11 @@ #include <string.h> extern const struct decoder_plugin mad_decoder_plugin; +extern const struct decoder_plugin mpg123_decoder_plugin; extern const struct decoder_plugin vorbis_decoder_plugin; extern const struct decoder_plugin flac_decoder_plugin; extern const struct decoder_plugin oggflac_decoder_plugin; +extern const struct decoder_plugin sndfile_decoder_plugin; extern const struct decoder_plugin audiofile_decoder_plugin; extern const struct decoder_plugin mp4ff_decoder_plugin; extern const struct decoder_plugin faad_decoder_plugin; @@ -47,6 +49,9 @@ static const struct decoder_plugin *const decoder_plugins[] = { #ifdef HAVE_MAD &mad_decoder_plugin, #endif +#ifdef HAVE_MPG123 + &mpg123_decoder_plugin, +#endif #ifdef ENABLE_VORBIS_DECODER &vorbis_decoder_plugin, #endif @@ -56,6 +61,9 @@ static const struct decoder_plugin *const decoder_plugins[] = { #ifdef HAVE_FLAC &flac_decoder_plugin, #endif +#ifdef ENABLE_SNDFILE + &sndfile_decoder_plugin, +#endif #ifdef HAVE_AUDIOFILE &audiofile_decoder_plugin, #endif diff --git a/src/decoder_thread.c b/src/decoder_thread.c index d6ff058ec..712e4d20c 100644 --- a/src/decoder_thread.c +++ b/src/decoder_thread.c @@ -49,11 +49,15 @@ decoder_stream_decode(const struct decoder_plugin *plugin, assert(input_stream->ready); assert(dc.state == DECODE_STATE_START); + decoder_unlock(); + /* rewind the stream, so each plugin gets a fresh start */ input_stream_seek(input_stream, 0, SEEK_SET); decoder_plugin_stream_decode(plugin, decoder, input_stream); + decoder_lock(); + assert(dc.state == DECODE_STATE_START || dc.state == DECODE_STATE_DECODE); @@ -70,11 +74,15 @@ decoder_file_decode(const struct decoder_plugin *plugin, assert(decoder->stream_tag == NULL); assert(decoder->decoder_tag == NULL); assert(path != NULL); - assert(path[0] == '/'); + assert(g_path_is_absolute(path)); assert(dc.state == DECODE_STATE_START); + decoder_unlock(); + decoder_plugin_file_decode(plugin, decoder, path); + decoder_lock(); + assert(dc.state == DECODE_STATE_START || dc.state == DECODE_STATE_DECODE); @@ -103,28 +111,38 @@ static void decoder_run_song(const struct song *song, const char *uri) dc.state = DECODE_STATE_START; dc.command = DECODE_COMMAND_NONE; - notify_signal(&pc.notify); + + player_signal(); /* wait for the input stream to become ready; its metadata will be available then */ while (!input_stream.ready) { if (dc.command == DECODE_COMMAND_STOP) { + decoder_unlock(); input_stream_close(&input_stream); + decoder_lock(); dc.state = DECODE_STATE_STOP; return; } + decoder_unlock(); ret = input_stream_buffer(&input_stream); if (ret < 0) { input_stream_close(&input_stream); + decoder_lock(); dc.state = DECODE_STATE_ERROR; return; } + + decoder_lock(); } if (dc.command == DECODE_COMMAND_STOP) { + decoder_unlock(); input_stream_close(&input_stream); + decoder_lock(); + dc.state = DECODE_STATE_STOP; return; } @@ -144,7 +162,7 @@ static void decoder_run_song(const struct song *song, const char *uri) if (ret) break; - plugin = NULL; + assert(dc.state == DECODE_STATE_START); } /* if that fails, try suffix matching the URL: */ @@ -160,7 +178,6 @@ static void decoder_run_song(const struct song *song, const char *uri) break; assert(dc.state == DECODE_STATE_START); - plugin = NULL; } } /* fallback to mp3: */ @@ -179,7 +196,10 @@ static void decoder_run_song(const struct song *song, const char *uri) const char *s = uri_get_suffix(uri); while ((plugin = decoder_plugin_from_suffix(s, next++))) { if (plugin->file_decode != NULL) { + decoder_unlock(); input_stream_close(&input_stream); + decoder_lock(); + close_instream = false; ret = decoder_file_decode(plugin, &decoder, uri); @@ -191,7 +211,13 @@ static void decoder_run_song(const struct song *song, const char *uri) been closed before decoder_file_decode() - reopen it */ - if (input_stream_open(&input_stream, uri)) + bool success; + + decoder_unlock(); + success = input_stream_open(&input_stream, uri); + decoder_lock(); + + if (success) close_instream = true; else continue; @@ -205,6 +231,8 @@ static void decoder_run_song(const struct song *song, const char *uri) } } + decoder_unlock(); + pcm_convert_deinit(&decoder.conv_state); /* flush the last chunk */ @@ -223,6 +251,8 @@ static void decoder_run_song(const struct song *song, const char *uri) if (decoder.decoder_tag != NULL) tag_free(decoder.decoder_tag); + decoder_lock(); + dc.state = ret ? DECODE_STATE_STOP : DECODE_STATE_ERROR; } @@ -249,6 +279,8 @@ static void decoder_run(void) static gpointer decoder_task(G_GNUC_UNUSED gpointer arg) { + decoder_lock(); + do { assert(dc.state == DECODE_STATE_STOP || dc.state == DECODE_STATE_ERROR); @@ -259,20 +291,24 @@ static gpointer decoder_task(G_GNUC_UNUSED gpointer arg) decoder_run(); dc.command = DECODE_COMMAND_NONE; - notify_signal(&pc.notify); + + player_signal(); break; case DECODE_COMMAND_STOP: dc.command = DECODE_COMMAND_NONE; - notify_signal(&pc.notify); + + player_signal(); break; case DECODE_COMMAND_NONE: - notify_wait(&dc.notify); + decoder_wait(); break; } } while (dc.command != DECODE_COMMAND_NONE || !dc.quit); + decoder_unlock(); + return NULL; } diff --git a/src/directory_save.c b/src/directory_save.c index 132508447..cb76b225f 100644 --- a/src/directory_save.c +++ b/src/directory_save.c @@ -138,7 +138,10 @@ directory_load(FILE *fp, struct directory *directory, GError **error) if (!success) return false; } else if (g_str_has_prefix(buffer, SONG_BEGIN)) { - readSongInfoIntoList(fp, &directory->songs, directory); + success = songvec_load(fp, &directory->songs, + directory, error); + if (!success) + return false; } else { g_set_error(error, directory_quark(), 0, "Malformed line: %s", buffer); diff --git a/src/encoder/null_encoder.c b/src/encoder/null_encoder.c new file mode 100644 index 000000000..218a0cb5f --- /dev/null +++ b/src/encoder/null_encoder.c @@ -0,0 +1,124 @@ +/* + * 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 "encoder_api.h" +#include "encoder_plugin.h" +#include "audio_format.h" + +#include <assert.h> +#include <string.h> + +#define MAX_BUFFER 32768 + +struct null_encoder { + struct encoder encoder; + + struct audio_format audio_format; + + unsigned char buffer[MAX_BUFFER]; + size_t buffer_length; +}; + +extern const struct encoder_plugin null_encoder_plugin; + +static inline GQuark +null_encoder_quark(void) +{ + return g_quark_from_static_string("null_encoder"); +} + +static struct encoder * +null_encoder_init(G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error) +{ + struct null_encoder *encoder; + + encoder = g_new(struct null_encoder, 1); + encoder_struct_init(&encoder->encoder, &null_encoder_plugin); + + return &encoder->encoder; +} + +static void +null_encoder_finish(struct encoder *_encoder) +{ + struct null_encoder *encoder = (struct null_encoder *)_encoder; + + g_free(encoder); +} + +static bool +null_encoder_open(struct encoder *_encoder, struct audio_format *audio_format, + G_GNUC_UNUSED GError **error) +{ + struct null_encoder *encoder = (struct null_encoder *)_encoder; + + encoder->audio_format = *audio_format; + encoder->buffer_length = 0; + + return true; +} + +static void +null_encoder_close(G_GNUC_UNUSED struct encoder *_encoder) +{ +} + +static bool +null_encoder_write(struct encoder *_encoder, + const void *data, size_t length, + G_GNUC_UNUSED GError **error) +{ + struct null_encoder *encoder = (struct null_encoder *)_encoder; + + assert(length + encoder->buffer_length < MAX_BUFFER); + + memcpy(encoder->buffer+encoder->buffer_length, + data, length); + + encoder->buffer_length += length; + return true; +} + +static size_t +null_encoder_read(struct encoder *_encoder, void *dest, size_t length) +{ + struct null_encoder *encoder = (struct null_encoder *)_encoder; + + if (length > encoder->buffer_length) + length = encoder->buffer_length; + + memcpy(dest, encoder->buffer, length); + + encoder->buffer_length -= length; + memmove(encoder->buffer, encoder->buffer + length, + encoder->buffer_length); + + return length; +} + +const struct encoder_plugin null_encoder_plugin = { + .name = "null", + .init = null_encoder_init, + .finish = null_encoder_finish, + .open = null_encoder_open, + .close = null_encoder_close, + .write = null_encoder_write, + .read = null_encoder_read, +}; diff --git a/src/encoder/twolame_encoder.c b/src/encoder/twolame_encoder.c new file mode 100644 index 000000000..5a8a82d81 --- /dev/null +++ b/src/encoder/twolame_encoder.c @@ -0,0 +1,299 @@ +/* + * 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 "encoder_api.h" +#include "encoder_plugin.h" +#include "audio_format.h" + +#include <twolame.h> +#include <assert.h> +#include <string.h> + +struct twolame_encoder { + struct encoder encoder; + + struct audio_format audio_format; + float quality; + int bitrate; + + twolame_options *options; + + unsigned char buffer[32768]; + size_t buffer_length; + + /** + * Call libtwolame's flush function when the buffer is empty? + */ + bool flush; +}; + +extern const struct encoder_plugin twolame_encoder_plugin; + +static inline GQuark +twolame_encoder_quark(void) +{ + return g_quark_from_static_string("twolame_encoder"); +} + +static bool +twolame_encoder_configure(struct twolame_encoder *encoder, + const struct config_param *param, GError **error) +{ + const char *value; + char *endptr; + + value = config_get_block_string(param, "quality", NULL); + if (value != NULL) { + /* a quality was configured (VBR) */ + + encoder->quality = g_ascii_strtod(value, &endptr); + + if (*endptr != '\0' || encoder->quality < -1.0 || + encoder->quality > 10.0) { + g_set_error(error, twolame_encoder_quark(), 0, + "quality \"%s\" is not a number in the " + "range -1 to 10, line %i", + value, param->line); + return false; + } + + if (config_get_block_string(param, "bitrate", NULL) != NULL) { + g_set_error(error, twolame_encoder_quark(), 0, + "quality and bitrate are " + "both defined (line %i)", + param->line); + return false; + } + } else { + /* a bit rate was configured */ + + value = config_get_block_string(param, "bitrate", NULL); + if (value == NULL) { + g_set_error(error, twolame_encoder_quark(), 0, + "neither bitrate nor quality defined " + "at line %i", + param->line); + return false; + } + + encoder->quality = -2.0; + encoder->bitrate = g_ascii_strtoll(value, &endptr, 10); + + if (*endptr != '\0' || encoder->bitrate <= 0) { + g_set_error(error, twolame_encoder_quark(), 0, + "bitrate at line %i should be a positive integer", + param->line); + return false; + } + } + + return true; +} + +static struct encoder * +twolame_encoder_init(const struct config_param *param, GError **error) +{ + struct twolame_encoder *encoder; + + g_debug("libtwolame version %s", get_twolame_version()); + + encoder = g_new(struct twolame_encoder, 1); + encoder_struct_init(&encoder->encoder, &twolame_encoder_plugin); + + /* load configuration from "param" */ + if (!twolame_encoder_configure(encoder, param, error)) { + /* configuration has failed, roll back and return error */ + g_free(encoder); + return NULL; + } + + return &encoder->encoder; +} + +static void +twolame_encoder_finish(struct encoder *_encoder) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + + /* the real libtwolame cleanup was already performed by + twolame_encoder_close(), so no real work here */ + g_free(encoder); +} + +static bool +twolame_encoder_setup(struct twolame_encoder *encoder, GError **error) +{ + if (encoder->quality >= -1.0) { + /* a quality was configured (VBR) */ + + if (0 != twolame_set_VBR(encoder->options, true)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error setting twolame VBR mode"); + return false; + } + if (0 != twolame_set_VBR_q(encoder->options, encoder->quality)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error setting twolame VBR quality"); + return false; + } + } else { + /* a bit rate was configured */ + + if (0 != twolame_set_brate(encoder->options, encoder->bitrate)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error setting twolame bitrate"); + return false; + } + } + + if (0 != twolame_set_num_channels(encoder->options, + encoder->audio_format.channels)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error setting twolame num channels"); + return false; + } + + if (0 != twolame_set_in_samplerate(encoder->options, + encoder->audio_format.sample_rate)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error setting twolame sample rate"); + return false; + } + + if (0 > twolame_init_params(encoder->options)) { + g_set_error(error, twolame_encoder_quark(), 0, + "error initializing twolame params"); + return false; + } + + return true; +} + +static bool +twolame_encoder_open(struct encoder *_encoder, struct audio_format *audio_format, + GError **error) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + + audio_format->bits = 16; + audio_format->channels = 2; + + encoder->audio_format = *audio_format; + + encoder->options = twolame_init(); + if (encoder->options == NULL) { + g_set_error(error, twolame_encoder_quark(), 0, + "twolame_init() failed"); + return false; + } + + if (!twolame_encoder_setup(encoder, error)) { + twolame_close(&encoder->options); + return false; + } + + encoder->buffer_length = 0; + encoder->flush = false; + + return true; +} + +static void +twolame_encoder_close(struct encoder *_encoder) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + + twolame_close(&encoder->options); +} + +static bool +twolame_encoder_flush(struct encoder *_encoder, G_GNUC_UNUSED GError **error) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + + encoder->flush = true; + return true; +} + +static bool +twolame_encoder_write(struct encoder *_encoder, + const void *data, size_t length, + G_GNUC_UNUSED GError **error) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + unsigned num_frames; + const int16_t *src = (const int16_t*)data; + int bytes_out; + + assert(encoder->buffer_length == 0); + + num_frames = + length / audio_format_frame_size(&encoder->audio_format); + + bytes_out = twolame_encode_buffer_interleaved(encoder->options, + src, num_frames, + encoder->buffer, + sizeof(encoder->buffer)); + if (bytes_out < 0) { + g_set_error(error, twolame_encoder_quark(), 0, + "twolame encoder failed"); + return false; + } + + encoder->buffer_length = (size_t)bytes_out; + return true; +} + +static size_t +twolame_encoder_read(struct encoder *_encoder, void *dest, size_t length) +{ + struct twolame_encoder *encoder = (struct twolame_encoder *)_encoder; + + if (encoder->buffer_length == 0 && encoder->flush) { + int ret = twolame_encode_flush(encoder->options, + encoder->buffer, + sizeof(encoder->buffer)); + if (ret > 0) + encoder->buffer_length = (size_t)ret; + + encoder->flush = false; + } + + if (length > encoder->buffer_length) + length = encoder->buffer_length; + + memcpy(dest, encoder->buffer, length); + + encoder->buffer_length -= length; + memmove(encoder->buffer, encoder->buffer + length, + encoder->buffer_length); + + return length; +} + +const struct encoder_plugin twolame_encoder_plugin = { + .name = "twolame", + .init = twolame_encoder_init, + .finish = twolame_encoder_finish, + .open = twolame_encoder_open, + .close = twolame_encoder_close, + .flush = twolame_encoder_flush, + .write = twolame_encoder_write, + .read = twolame_encoder_read, +}; diff --git a/src/encoder_list.c b/src/encoder_list.c index d563b6bc8..ad10e4e28 100644 --- a/src/encoder_list.c +++ b/src/encoder_list.c @@ -23,16 +23,22 @@ #include <string.h> +extern const struct encoder_plugin null_encoder_plugin; extern const struct encoder_plugin vorbis_encoder_plugin; extern const struct encoder_plugin lame_encoder_plugin; +extern const struct encoder_plugin twolame_encoder_plugin; static const struct encoder_plugin *encoder_plugins[] = { + &null_encoder_plugin, #ifdef ENABLE_VORBIS_ENCODER &vorbis_encoder_plugin, #endif #ifdef ENABLE_LAME_ENCODER &lame_encoder_plugin, #endif +#ifdef ENABLE_TWOLAME_ENCODER + &twolame_encoder_plugin, +#endif NULL }; diff --git a/src/event_pipe.h b/src/event_pipe.h index ecb7ec9e8..4614ef25c 100644 --- a/src/event_pipe.h +++ b/src/event_pipe.h @@ -32,7 +32,7 @@ enum pipe_event { /** an idle event was emitted */ PIPE_EVENT_IDLE, - /** must call syncPlayerAndPlaylist() */ + /** must call playlist_sync() */ PIPE_EVENT_PLAYLIST, /** the current song's tag has changed */ @@ -41,6 +41,9 @@ enum pipe_event { /** SIGHUP received: reload configuration, roll log file */ PIPE_EVENT_RELOAD, + /** a hardware mixer plugin has detected a change */ + PIPE_EVENT_MIXER, + PIPE_EVENT_MAX }; diff --git a/src/exclude.c b/src/exclude.c new file mode 100644 index 000000000..4a1fb21f6 --- /dev/null +++ b/src/exclude.c @@ -0,0 +1,94 @@ +/* + * 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. + */ + +/* + * The .mpdignore backend code. + * + */ + +#include "exclude.h" +#include "path.h" + +#include <assert.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <fnmatch.h> + +GSList * +exclude_list_load(const char *path_fs) +{ + FILE *file; + char line[1024]; + GSList *list = NULL; + + assert(path_fs != NULL); + + file = fopen(path_fs, "r"); + if (file == NULL) { + if (errno != ENOENT) { + char *path_utf8 = fs_charset_to_utf8(path_fs); + g_debug("Failed to open %s: %s", + path_utf8, g_strerror(errno)); + g_free(path_utf8); + } + + return NULL; + } + + while (fgets(line, sizeof(line), file) != NULL) { + char *p = strchr(line, '#'); + if (p != NULL) + *p = 0; + + p = g_strstrip(line); + if (*p != 0) + list = g_slist_prepend(list, g_strdup(p)); + } + + fclose(file); + + return list; +} + +void +exclude_list_free(GSList *list) +{ + while (list != NULL) { + g_free(list->data); + list = g_slist_remove(list, list->data); + } +} + +bool +exclude_list_check(GSList *list, const char *name_fs) +{ + assert(name_fs != NULL); + + /* XXX include full path name in check */ + + for (; list != NULL; list = list->next) { + const char *pattern = list->data; + + if (fnmatch(pattern, name_fs, FNM_PATHNAME|FNM_PERIOD) == 0) + return true; + } + + return false; +} diff --git a/src/exclude.h b/src/exclude.h new file mode 100644 index 000000000..637feb846 --- /dev/null +++ b/src/exclude.h @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/* + * The .mpdignore backend code. + * + */ + +#ifndef MPD_EXCLUDE_H +#define MPD_EXCLUDE_H + +#include <glib.h> + +#include <stdbool.h> + +/** + * Loads and parses a .mpdignore file. + */ +GSList * +exclude_list_load(const char *path_fs); + +/** + * Frees a list returned by exclude_list_load(). + */ +void +exclude_list_free(GSList *list); + +/** + * Checks whether one of the patterns in the .mpdignore file matches + * the specified file name. + */ +bool +exclude_list_check(GSList *list, const char *name_fs); + +#endif diff --git a/src/filter/chain_filter_plugin.c b/src/filter/chain_filter_plugin.c new file mode 100644 index 000000000..ec8bef5c0 --- /dev/null +++ b/src/filter/chain_filter_plugin.c @@ -0,0 +1,177 @@ +/* + * 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 "filter/chain_filter_plugin.h" +#include "filter_plugin.h" +#include "filter_internal.h" +#include "filter_registry.h" + +#include <assert.h> + +struct filter_chain { + /** the base class */ + struct filter base; + + GSList *children; +}; + +static struct filter * +chain_filter_init(G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct filter_chain *chain = g_new(struct filter_chain, 1); + + filter_init(&chain->base, &chain_filter_plugin); + chain->children = NULL; + + return &chain->base; +} + +static void +chain_free_child(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct filter *filter = data; + + filter_free(filter); +} + +static void +chain_filter_finish(struct filter *_filter) +{ + struct filter_chain *chain = (struct filter_chain *)_filter; + + g_slist_foreach(chain->children, chain_free_child, NULL); + g_slist_free(chain->children); + + g_free(chain); +} + +/** + * Close all filters in the chain until #until is reached. #until + * itself is not closed. + */ +static void +chain_close_until(struct filter_chain *chain, const struct filter *until) +{ + GSList *i = chain->children; + struct filter *filter; + + while (true) { + /* this assertion fails if #until does not exist + (anymore) */ + assert(i != NULL); + + if (i->data == until) + /* don't close this filter */ + break; + + /* close this filter */ + filter = i->data; + filter_close(filter); + + i = g_slist_next(i); + } +} + +static const struct audio_format * +chain_filter_open(struct filter *_filter, + const struct audio_format *audio_format, + GError **error_r) +{ + struct filter_chain *chain = (struct filter_chain *)_filter; + + for (GSList *i = chain->children; i != NULL; i = g_slist_next(i)) { + struct filter *filter = i->data; + + audio_format = filter_open(filter, audio_format, error_r); + if (audio_format == NULL) { + /* rollback, close all children */ + chain_close_until(chain, filter); + return NULL; + } + } + + /* return the output format of the last filter */ + return audio_format; +} + +static void +chain_close_child(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct filter *filter = data; + + filter_close(filter); +} + +static void +chain_filter_close(struct filter *_filter) +{ + struct filter_chain *chain = (struct filter_chain *)_filter; + + g_slist_foreach(chain->children, chain_close_child, NULL); +} + +static const void * +chain_filter_filter(struct filter *_filter, + const void *src, size_t src_size, + size_t *dest_size_r, GError **error_r) +{ + struct filter_chain *chain = (struct filter_chain *)_filter; + + for (GSList *i = chain->children; i != NULL; i = g_slist_next(i)) { + struct filter *filter = i->data; + + /* feed the output of the previous filter as input + into the current one */ + src = filter_filter(filter, src, src_size, &src_size, error_r); + if (src == NULL) + chain_close_until(chain, filter); + } + + /* return the output of the last filter */ + *dest_size_r = src_size; + return src; +} + +const struct filter_plugin chain_filter_plugin = { + .name = "chain", + .init = chain_filter_init, + .finish = chain_filter_finish, + .open = chain_filter_open, + .close = chain_filter_close, + .filter = chain_filter_filter, +}; + +struct filter * +filter_chain_new(void) +{ + struct filter *filter = filter_new(&chain_filter_plugin, NULL, NULL); + /* chain_filter_init() never fails */ + assert(filter != NULL); + + return filter; +} + +void +filter_chain_append(struct filter *_chain, struct filter *filter) +{ + struct filter_chain *chain = (struct filter_chain *)_chain; + + chain->children = g_slist_append(chain->children, filter); +} diff --git a/src/filter/chain_filter_plugin.h b/src/filter/chain_filter_plugin.h new file mode 100644 index 000000000..f8462b22d --- /dev/null +++ b/src/filter/chain_filter_plugin.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +/** \file + * + * A filter chain is a container for several filters. They are + * chained together, i.e. called in a row, one filter passing its + * output to the next one. + */ + +#ifndef MPD_FILTER_CHAIN_H +#define MPD_FILTER_CHAIN_H + +struct filter; + +/** + * Creates a new filter chain. + */ +struct filter * +filter_chain_new(void); + +/** + * Appends a new filter at the end of the filter chain. You must call + * this function before the first filter_open() call. + * + * @param chain the filter chain created with filter_chain_new() + * @param filter the filter to be appended to #chain + */ +void +filter_chain_append(struct filter *chain, struct filter *filter); + +#endif diff --git a/src/filter/convert_filter_plugin.c b/src/filter/convert_filter_plugin.c new file mode 100644 index 000000000..d197dbdb9 --- /dev/null +++ b/src/filter/convert_filter_plugin.c @@ -0,0 +1,147 @@ +/* + * 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 "filter/convert_filter_plugin.h" +#include "filter_plugin.h" +#include "filter_internal.h" +#include "filter_registry.h" +#include "conf.h" +#include "pcm_convert.h" +#include "audio_format.h" +#include "poison.h" + +#include <assert.h> +#include <string.h> + +struct convert_filter { + struct filter base; + + /** + * The current convert, from 0 to #PCM_CONVERT_1. + */ + unsigned convert; + + /** + * The input audio format; PCM data is passed to the filter() + * method in this format. + */ + struct audio_format in_audio_format; + + /** + * The output audio format; the consumer of this plugin + * expects PCM data in this format. This defaults to + * #in_audio_format, and can be set with convert_filter_set(). + */ + struct audio_format out_audio_format; + + struct pcm_convert_state state; +}; + +static struct filter * +convert_filter_init(G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct convert_filter *filter = g_new(struct convert_filter, 1); + + filter_init(&filter->base, &convert_filter_plugin); + return &filter->base; +} + +static void +convert_filter_finish(struct filter *filter) +{ + g_free(filter); +} + +static const struct audio_format * +convert_filter_open(struct filter *_filter, + const struct audio_format *audio_format, + G_GNUC_UNUSED GError **error_r) +{ + struct convert_filter *filter = (struct convert_filter *)_filter; + + assert(audio_format_valid(audio_format)); + + filter->in_audio_format = filter->out_audio_format = *audio_format; + pcm_convert_init(&filter->state); + + return audio_format; +} + +static void +convert_filter_close(struct filter *_filter) +{ + struct convert_filter *filter = (struct convert_filter *)_filter; + + pcm_convert_deinit(&filter->state); + + poison_undefined(&filter->in_audio_format, + sizeof(filter->in_audio_format)); + poison_undefined(&filter->out_audio_format, + sizeof(filter->out_audio_format)); +} + +static const void * +convert_filter_filter(struct filter *_filter, const void *src, size_t src_size, + size_t *dest_size_r, GError **error_r) +{ + struct convert_filter *filter = (struct convert_filter *)_filter; + const void *dest; + + if (audio_format_equals(&filter->in_audio_format, + &filter->out_audio_format)) { + /* optimized special case: no-op */ + *dest_size_r = src_size; + return src; + } + + dest = pcm_convert(&filter->state, &filter->in_audio_format, + src, src_size, + &filter->out_audio_format, dest_size_r, + error_r); + if (dest == NULL) + return NULL; + + return dest; +} + +const struct filter_plugin convert_filter_plugin = { + .name = "convert", + .init = convert_filter_init, + .finish = convert_filter_finish, + .open = convert_filter_open, + .close = convert_filter_close, + .filter = convert_filter_filter, +}; + +void +convert_filter_set(struct filter *_filter, + const struct audio_format *out_audio_format) +{ + struct convert_filter *filter = (struct convert_filter *)_filter; + + assert(filter != NULL); + assert(audio_format_valid(&filter->in_audio_format)); + assert(audio_format_valid(&filter->out_audio_format)); + assert(out_audio_format != NULL); + assert(audio_format_valid(out_audio_format)); + assert(filter->in_audio_format.reverse_endian == 0); + + filter->out_audio_format = *out_audio_format; +} diff --git a/src/filter/convert_filter_plugin.h b/src/filter/convert_filter_plugin.h new file mode 100644 index 000000000..8d370b0cb --- /dev/null +++ b/src/filter/convert_filter_plugin.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#ifndef CONVERT_FILTER_PLUGIN_H +#define CONVERT_FILTER_PLUGIN_H + +struct filter; +struct audio_format; + +/** + * Sets the output audio format for the specified filter. You must + * call this after the filter has been opened. Since this audio + * format switch is a violation of the filter API, this filter must be + * the last in a chain. + */ +void +convert_filter_set(struct filter *filter, + const struct audio_format *out_audio_format); + +#endif diff --git a/src/filter/null_filter_plugin.c b/src/filter/null_filter_plugin.c new file mode 100644 index 000000000..689388558 --- /dev/null +++ b/src/filter/null_filter_plugin.c @@ -0,0 +1,93 @@ +/* + * 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. + */ + +/** \file + * + * This filter plugin does nothing. That is not quite useful, except + * for testing the filter core, or as a template for new filter + * plugins. + */ + +#include "filter_plugin.h" +#include "filter_internal.h" +#include "filter_registry.h" + +#include <assert.h> + +struct null_filter { + struct filter filter; +}; + +static struct filter * +null_filter_init(G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct null_filter *filter = g_new(struct null_filter, 1); + + filter_init(&filter->filter, &null_filter_plugin); + return &filter->filter; +} + +static void +null_filter_finish(struct filter *_filter) +{ + struct null_filter *filter = (struct null_filter *)_filter; + + g_free(filter); +} + +static const struct audio_format * +null_filter_open(struct filter *_filter, + const struct audio_format *audio_format, + G_GNUC_UNUSED GError **error_r) +{ + struct null_filter *filter = (struct null_filter *)_filter; + (void)filter; + + return audio_format; +} + +static void +null_filter_close(struct filter *_filter) +{ + struct null_filter *filter = (struct null_filter *)_filter; + (void)filter; +} + +static const void * +null_filter_filter(struct filter *_filter, + const void *src, size_t src_size, + size_t *dest_size_r, G_GNUC_UNUSED GError **error_r) +{ + struct null_filter *filter = (struct null_filter *)_filter; + (void)filter; + + /* return the unmodified source buffer */ + *dest_size_r = src_size; + return src; +} + +const struct filter_plugin null_filter_plugin = { + .name = "null", + .init = null_filter_init, + .finish = null_filter_finish, + .open = null_filter_open, + .close = null_filter_close, + .filter = null_filter_filter, +}; diff --git a/src/filter/volume_filter_plugin.c b/src/filter/volume_filter_plugin.c new file mode 100644 index 000000000..298ca2f36 --- /dev/null +++ b/src/filter/volume_filter_plugin.c @@ -0,0 +1,174 @@ +/* + * 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 "filter/volume_filter_plugin.h" +#include "filter_plugin.h" +#include "filter_internal.h" +#include "filter_registry.h" +#include "conf.h" +#include "pcm_buffer.h" +#include "pcm_volume.h" +#include "audio_format.h" +#include "player_control.h" + +#include <assert.h> +#include <string.h> + +struct volume_filter { + struct filter filter; + + /** + * The current volume, from 0 to #PCM_VOLUME_1. + */ + unsigned volume; + + struct audio_format audio_format; + + struct pcm_buffer buffer; +}; + +static inline GQuark +volume_quark(void) +{ + return g_quark_from_static_string("pcm_volume"); +} + +static struct filter * +volume_filter_init(G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct volume_filter *filter = g_new(struct volume_filter, 1); + + filter_init(&filter->filter, &volume_filter_plugin); + filter->volume = PCM_VOLUME_1; + + return &filter->filter; +} + +static void +volume_filter_finish(struct filter *filter) +{ + g_free(filter); +} + +static const struct audio_format * +volume_filter_open(struct filter *_filter, + const struct audio_format *audio_format, + GError **error_r) +{ + struct volume_filter *filter = (struct volume_filter *)_filter; + + if (audio_format->bits != 8 && audio_format->bits != 16 && + audio_format->bits != 24) { + g_set_error(error_r, volume_quark(), 0, + "Unsupported audio format"); + return false; + } + + if (audio_format->reverse_endian) { + g_set_error(error_r, volume_quark(), 0, + "Software volume for reverse endian " + "samples is not implemented"); + return false; + } + + filter->audio_format = *audio_format; + pcm_buffer_init(&filter->buffer); + + return &filter->audio_format; +} + +static void +volume_filter_close(struct filter *_filter) +{ + struct volume_filter *filter = (struct volume_filter *)_filter; + + pcm_buffer_deinit(&filter->buffer); +} + +static const void * +volume_filter_filter(struct filter *_filter, const void *src, size_t src_size, + size_t *dest_size_r, GError **error_r) +{ + struct volume_filter *filter = (struct volume_filter *)_filter; + bool success; + void *dest; + + if (filter->volume >= PCM_VOLUME_1) { + /* optimized special case: 100% volume = no-op */ + *dest_size_r = src_size; + return src; + } + + dest = pcm_buffer_get(&filter->buffer, src_size); + *dest_size_r = src_size; + + if (filter->volume <= 0) { + /* optimized special case: 0% volume = memset(0) */ + /* XXX is this valid for all sample formats? What + about floating point? */ + memset(dest, 0, src_size); + return dest; + } + + memcpy(dest, src, src_size); + + success = pcm_volume(dest, src_size, &filter->audio_format, + filter->volume); + if (!success) { + g_set_error(error_r, volume_quark(), 0, + "pcm_volume() has failed"); + return NULL; + } + + return dest; +} + +const struct filter_plugin volume_filter_plugin = { + .name = "volume", + .init = volume_filter_init, + .finish = volume_filter_finish, + .open = volume_filter_open, + .close = volume_filter_close, + .filter = volume_filter_filter, +}; + +unsigned +volume_filter_get(const struct filter *_filter) +{ + const struct volume_filter *filter = + (const struct volume_filter *)_filter; + + assert(filter->filter.plugin == &volume_filter_plugin); + assert(filter->volume <= PCM_VOLUME_1); + + return filter->volume; +} + +void +volume_filter_set(struct filter *_filter, unsigned volume) +{ + struct volume_filter *filter = (struct volume_filter *)_filter; + + assert(filter->filter.plugin == &volume_filter_plugin); + assert(volume <= PCM_VOLUME_1); + + filter->volume = volume; +} + diff --git a/src/filter/volume_filter_plugin.h b/src/filter/volume_filter_plugin.h new file mode 100644 index 000000000..c064741a2 --- /dev/null +++ b/src/filter/volume_filter_plugin.h @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#ifndef VOLUME_FILTER_PLUGIN_H +#define VOLUME_FILTER_PLUGIN_H + +struct filter; + +unsigned +volume_filter_get(const struct filter *filter); + +void +volume_filter_set(struct filter *filter, unsigned volume); + +#endif diff --git a/src/buffer2array.h b/src/filter_internal.h index bed23a29f..b086e31b1 100644 --- a/src/buffer2array.h +++ b/src/filter_internal.h @@ -17,15 +17,22 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#ifndef MPD_BUFFER_2_ARRAY_H -#define MPD_BUFFER_2_ARRAY_H - -/* tokenizes up to max elements in buffer (a null-terminated string) and - * stores the result in array (which must be capable of holding up to - * max elements). Tokenization is based on C string quoting rules. - * The arguments buffer and array are modified. - * Returns the number of elements tokenized. +/** \file + * + * Internal stuff for the filter core and filter plugins. */ -int buffer2array(char *buffer, char *array[], const int max); + +#ifndef MPD_FILTER_INTERNAL_H +#define MPD_FILTER_INTERNAL_H + +struct filter { + const struct filter_plugin *plugin; +}; + +static inline void +filter_init(struct filter *filter, const struct filter_plugin *plugin) +{ + filter->plugin = plugin; +} #endif diff --git a/src/filter_plugin.c b/src/filter_plugin.c new file mode 100644 index 000000000..e5c1d5cd8 --- /dev/null +++ b/src/filter_plugin.c @@ -0,0 +1,106 @@ +/* + * 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 "filter_plugin.h" +#include "filter_internal.h" +#include "filter_registry.h" +#include "conf.h" + +#ifndef NDEBUG +#include "audio_format.h" +#endif + +#include <assert.h> + +struct filter * +filter_new(const struct filter_plugin *plugin, + const struct config_param *param, GError **error_r) +{ + assert(plugin != NULL); + assert(error_r == NULL || *error_r == NULL); + + return plugin->init(param, error_r); +} + +struct filter * +filter_configured_new(const struct config_param *param, GError **error_r) +{ + const char *plugin_name; + const struct filter_plugin *plugin; + + assert(param != NULL); + assert(error_r == NULL || *error_r == NULL); + + plugin_name = config_get_block_string(param, "plugin", NULL); + if (plugin_name == NULL) + g_set_error(error_r, config_quark(), 0, + "No filter plugin specified"); + + plugin = filter_plugin_by_name(plugin_name); + if (plugin == NULL) + g_set_error(error_r, config_quark(), 0, + "No such filter plugin: %s", plugin_name); + + return filter_new(plugin, param, error_r); +} + +void +filter_free(struct filter *filter) +{ + assert(filter != NULL); + + filter->plugin->finish(filter); +} + +const struct audio_format * +filter_open(struct filter *filter, const struct audio_format *audio_format, + GError **error_r) +{ + assert(filter != NULL); + assert(audio_format != NULL); + assert(audio_format_valid(audio_format)); + assert(error_r == NULL || *error_r == NULL); + + audio_format = filter->plugin->open(filter, audio_format, error_r); + assert(audio_format == NULL || audio_format_valid(audio_format)); + + return audio_format; +} + +void +filter_close(struct filter *filter) +{ + assert(filter != NULL); + + filter->plugin->close(filter); +} + +const void * +filter_filter(struct filter *filter, const void *src, size_t src_size, + size_t *dest_size_r, + GError **error_r) +{ + assert(filter != NULL); + assert(src != NULL); + assert(src_size > 0); + assert(dest_size_r != NULL); + assert(error_r == NULL || *error_r == NULL); + + return filter->plugin->filter(filter, src, src_size, dest_size_r, error_r); +} diff --git a/src/filter_plugin.h b/src/filter_plugin.h new file mode 100644 index 000000000..0043246d1 --- /dev/null +++ b/src/filter_plugin.h @@ -0,0 +1,145 @@ +/* + * 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. + */ + +/** \file + * + * This header declares the filter_plugin class. It describes a + * plugin API for objects which filter raw PCM data. + */ + +#ifndef MPD_FILTER_PLUGIN_H +#define MPD_FILTER_PLUGIN_H + +#include <glib.h> + +#include <stdbool.h> +#include <stddef.h> + +struct config_param; +struct filter; + +struct filter_plugin { + const char *name; + + /** + * Allocates and configures a filter. + */ + struct filter *(*init)(const struct config_param *param, + GError **error_r); + + /** + * Free instance data. + */ + void (*finish)(struct filter *filter); + + /** + * Opens a filter. + */ + const struct audio_format * + (*open)(struct filter *filter, + const struct audio_format *audio_format, + GError **error_r); + + /** + * Closes a filter. + */ + void (*close)(struct filter *filter); + + /** + * Filters a block of PCM data. + */ + const void *(*filter)(struct filter *filter, + const void *src, size_t src_size, + size_t *dest_buffer_r, + GError **error_r); +}; + +/** + * Creates a new instance of the specified filter plugin. + * + * @param plugin the filter plugin + * @param param optional configuration section + * @param error location to store the error occuring, or NULL to + * ignore errors. + * @return a new filter object, or NULL on error + */ +struct filter * +filter_new(const struct filter_plugin *plugin, + const struct config_param *param, GError **error_r); + +/** + * Creates a new filter, loads configuration and the plugin name from + * the specified configuration section. + * + * @param param the configuration section + * @param error location to store the error occuring, or NULL to + * ignore errors. + * @return a new filter object, or NULL on error + */ +struct filter * +filter_configured_new(const struct config_param *param, GError **error_r); + +/** + * Deletes a filter. It must be closed prior to calling this + * function, see filter_close(). + * + * @param filter the filter object + */ +void +filter_free(struct filter *filter); + +/** + * Opens the filter, preparing it for filter_filter(). + * + * @param filter the filter object + * @param audio_format the audio format of incoming and outgoing data + * @param error location to store the error occuring, or NULL to + * ignore errors. + * @return true on success, false on error + */ +const struct audio_format * +filter_open(struct filter *filter, const struct audio_format *audio_format, + GError **error_r); + +/** + * Closes the filter. After that, you may call filter_open() again. + * + * @param filter the filter object + */ +void +filter_close(struct filter *filter); + +/** + * Filters a block of PCM data. + * + * @param filter the filter object + * @param src the input buffer + * @param src_size the size of #src_buffer in bytes + * @param dest_size_r the size of the returned buffer + * @param error location to store the error occuring, or NULL to + * ignore errors. + * @return the destination buffer on success (will be invalidated by + * filter_close() or filter_filter()), NULL on error + */ +const void * +filter_filter(struct filter *filter, const void *src, size_t src_size, + size_t *dest_size_r, + GError **error_r); + +#endif diff --git a/src/filter_registry.c b/src/filter_registry.c new file mode 100644 index 000000000..c8887aabf --- /dev/null +++ b/src/filter_registry.c @@ -0,0 +1,41 @@ +/* + * 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 "filter_registry.h" +#include "filter_plugin.h" + +#include <stddef.h> +#include <string.h> + +const struct filter_plugin *const filter_plugins[] = { + &null_filter_plugin, + &chain_filter_plugin, + &volume_filter_plugin, + NULL, +}; + +const struct filter_plugin * +filter_plugin_by_name(const char *name) +{ + for (unsigned i = 0; filter_plugins[i] != NULL; ++i) + if (strcmp(filter_plugins[i]->name, name) == 0) + return filter_plugins[i]; + + return NULL; +} diff --git a/src/filter_registry.h b/src/filter_registry.h new file mode 100644 index 000000000..7eb7f7038 --- /dev/null +++ b/src/filter_registry.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** \file + * + * This library manages all filter plugins which are enabled at + * compile time. + */ + +#ifndef MPD_FILTER_REGISTRY_H +#define MPD_FILTER_REGISTRY_H + +extern const struct filter_plugin null_filter_plugin; +extern const struct filter_plugin chain_filter_plugin; +extern const struct filter_plugin convert_filter_plugin; +extern const struct filter_plugin volume_filter_plugin; + +const struct filter_plugin * +filter_plugin_by_name(const char *name); + +#endif diff --git a/src/icy_metadata.c b/src/icy_metadata.c index 69aa89092..4089e9c6f 100644 --- a/src/icy_metadata.c +++ b/src/icy_metadata.c @@ -95,7 +95,7 @@ icy_parse_tag_item(struct tag *tag, const char *item) if (p[0] != NULL && p[1] != NULL) { if (strcmp(p[0], "StreamTitle") == 0) - icy_add_item(tag, TAG_ITEM_TITLE, p[1]); + icy_add_item(tag, TAG_TITLE, p[1]); else g_debug("unknown icy-tag: '%s'", p[0]); } diff --git a/src/idle.c b/src/idle.c index 11b57376d..c0bb7a908 100644 --- a/src/idle.c +++ b/src/idle.c @@ -40,6 +40,7 @@ static const char *const idle_names[] = { "output", "options", "sticker", + "update", NULL }; diff --git a/src/idle.h b/src/idle.h index a69acabb0..c8ed57f74 100644 --- a/src/idle.h +++ b/src/idle.h @@ -50,6 +50,9 @@ enum { /** a sticker has been modified. */ IDLE_STICKER = 0x80, + + /** a database update has started or finished. */ + IDLE_UPDATE = 0x100, }; /** diff --git a/src/inotify_queue.c b/src/inotify_queue.c new file mode 100644 index 000000000..4941f4905 --- /dev/null +++ b/src/inotify_queue.c @@ -0,0 +1,134 @@ +/* + * 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 "inotify_queue.h" +#include "update.h" + +#include <glib.h> + +#include <string.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +enum { + /** + * Wait this long after the last change before calling + * update_enqueue(). This increases the probability that + * updates can be bundled. + */ + INOTIFY_UPDATE_DELAY_S = 5, +}; + +static GSList *inotify_queue; +static guint queue_source_id; + +void +mpd_inotify_queue_init(void) +{ +} + +static void +free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + g_free(data); +} + +void +mpd_inotify_queue_finish(void) +{ + if (queue_source_id != 0) + g_source_remove(queue_source_id); + + g_slist_foreach(inotify_queue, free_callback, NULL); + g_slist_free(inotify_queue); +} + +static gboolean +mpd_inotify_run_update(G_GNUC_UNUSED gpointer data) +{ + unsigned id; + + while (inotify_queue != NULL) { + char *uri_utf8 = inotify_queue->data; + + id = update_enqueue(uri_utf8, false); + if (id == 0) + /* retry later */ + return true; + + g_debug("updating '%s' job=%u", uri_utf8, id); + + g_free(uri_utf8); + inotify_queue = g_slist_delete_link(inotify_queue, + inotify_queue); + } + + /* done, remove the timer event by returning false */ + queue_source_id = 0; + return false; +} + +static bool +path_in(const char *path, const char *possible_parent) +{ + size_t length = strlen(possible_parent); + + return path[0] == 0 || + (memcmp(possible_parent, path, length) == 0 && + (path[length] == 0 || path[length] == '/')); +} + +void +mpd_inotify_enqueue(char *uri_utf8) +{ + GSList *old_queue = inotify_queue; + + if (queue_source_id != 0) + g_source_remove(queue_source_id); + queue_source_id = g_timeout_add_seconds(INOTIFY_UPDATE_DELAY_S, + mpd_inotify_run_update, NULL); + + inotify_queue = NULL; + while (old_queue != NULL) { + char *current_uri = old_queue->data; + + if (path_in(uri_utf8, current_uri)) { + /* already enqueued */ + g_free(uri_utf8); + inotify_queue = g_slist_concat(inotify_queue, + old_queue); + return; + } + + old_queue = g_slist_delete_link(old_queue, old_queue); + + if (path_in(current_uri, uri_utf8)) + /* existing path is a sub-path of the new + path; we can dequeue the existing path and + update the new path instead */ + g_free(current_uri); + else + /* move the existing path to the new queue */ + inotify_queue = g_slist_prepend(inotify_queue, + current_uri); + } + + inotify_queue = g_slist_prepend(inotify_queue, uri_utf8); +} diff --git a/src/inotify_queue.h b/src/inotify_queue.h new file mode 100644 index 000000000..6e12e9bda --- /dev/null +++ b/src/inotify_queue.h @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#ifndef MPD_INOTIFY_QUEUE_H +#define MPD_INOTIFY_QUEUE_H + +void +mpd_inotify_queue_init(void); + +void +mpd_inotify_queue_finish(void); + +void +mpd_inotify_enqueue(char *uri_utf8); + +#endif diff --git a/src/inotify_source.c b/src/inotify_source.c new file mode 100644 index 000000000..d5feec3e0 --- /dev/null +++ b/src/inotify_source.c @@ -0,0 +1,163 @@ +/* + * 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 "inotify_source.h" +#include "fifo_buffer.h" + +#include <sys/inotify.h> +#include <unistd.h> +#include <errno.h> +#include <stdbool.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +struct mpd_inotify_source { + int fd; + + GIOChannel *channel; + + /** + * The channel's source id in the GLib main loop. + */ + guint id; + + struct fifo_buffer *buffer; + + mpd_inotify_callback_t callback; + void *callback_ctx; +}; + +/** + * A GQuark for GError instances. + */ +static inline GQuark +mpd_inotify_quark(void) +{ + return g_quark_from_static_string("inotify"); +} + +static gboolean +mpd_inotify_in_event(G_GNUC_UNUSED GIOChannel *_source, + G_GNUC_UNUSED GIOCondition condition, + gpointer data) +{ + struct mpd_inotify_source *source = data; + void *dest; + size_t length; + ssize_t nbytes; + const struct inotify_event *event; + + dest = fifo_buffer_write(source->buffer, &length); + if (dest == NULL) + g_error("buffer full"); + + nbytes = read(source->fd, dest, length); + if (nbytes < 0) + g_error("failed to read from inotify: %s", g_strerror(errno)); + if (nbytes == 0) + g_error("end of file from inotify"); + + fifo_buffer_append(source->buffer, nbytes); + + while (true) { + const char *name; + + event = fifo_buffer_read(source->buffer, &length); + if (event == NULL || length < sizeof(*event) || + length < sizeof(*event) + event->len) + break; + + if (event->len > 0 && event->name[event->len - 1] == 0) + name = event->name; + else + name = NULL; + + source->callback(event->wd, event->mask, name, + source->callback_ctx); + fifo_buffer_consume(source->buffer, + sizeof(*event) + event->len); + } + + return true; +} + +struct mpd_inotify_source * +mpd_inotify_source_new(mpd_inotify_callback_t callback, void *callback_ctx, + GError **error_r) +{ + struct mpd_inotify_source *source = + g_new(struct mpd_inotify_source, 1); + + source->fd = inotify_init(); + if (source->fd < 0) { + g_set_error(error_r, mpd_inotify_quark(), errno, + "inotify_init() has failed: %s", + g_strerror(errno)); + g_free(source); + return NULL; + } + + source->buffer = fifo_buffer_new(4096); + + source->channel = g_io_channel_unix_new(source->fd); + source->id = g_io_add_watch(source->channel, G_IO_IN, + mpd_inotify_in_event, source); + + source->callback = callback; + source->callback_ctx = callback_ctx; + + return source; +} + +void +mpd_inotify_source_free(struct mpd_inotify_source *source) +{ + g_source_remove(source->id); + g_io_channel_unref(source->channel); + fifo_buffer_free(source->buffer); + close(source->fd); + g_free(source); +} + +int +mpd_inotify_source_add(struct mpd_inotify_source *source, + const char *path_fs, unsigned mask, + GError **error_r) +{ + int wd = inotify_add_watch(source->fd, path_fs, mask); + if (wd < 0) + g_set_error(error_r, mpd_inotify_quark(), errno, + "inotify_add_watch() has failed: %s", + g_strerror(errno)); + + return wd; +} + +void +mpd_inotify_source_rm(struct mpd_inotify_source *source, unsigned wd) +{ + int ret = inotify_rm_watch(source->fd, wd); + if (ret < 0 && errno != EINVAL) + g_warning("inotify_rm_watch() has failed: %s", + g_strerror(errno)); + + /* EINVAL may happen here when the file has been deleted; the + kernel seems to auto-unregister deleted files */ +} diff --git a/src/inotify_source.h b/src/inotify_source.h new file mode 100644 index 000000000..eb609ccfc --- /dev/null +++ b/src/inotify_source.h @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#ifndef MPD_INOTIFY_SOURCE_H +#define MPD_INOTIFY_SOURCE_H + +#include <glib.h> + +typedef void (*mpd_inotify_callback_t)(int wd, unsigned mask, + const char *name, void *ctx); + +struct mpd_inotify_source; + +/** + * Creates a new inotify source and registers it in the GLib main + * loop. + * + * @param a callback invoked for events received from the kernel + */ +struct mpd_inotify_source * +mpd_inotify_source_new(mpd_inotify_callback_t callback, void *callback_ctx, + GError **error_r); + +void +mpd_inotify_source_free(struct mpd_inotify_source *source); + +/** + * Adds a path to the notify list. + * + * @return a watch descriptor or -1 on error + */ +int +mpd_inotify_source_add(struct mpd_inotify_source *source, + const char *path_fs, unsigned mask, + GError **error_r); + +/** + * Removes a path from the notify list. + * + * @param wd the watch descriptor returned by mpd_inotify_source_add() + */ +void +mpd_inotify_source_rm(struct mpd_inotify_source *source, unsigned wd); + +#endif diff --git a/src/inotify_update.c b/src/inotify_update.c new file mode 100644 index 000000000..996afb0aa --- /dev/null +++ b/src/inotify_update.c @@ -0,0 +1,349 @@ +/* + * 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 "inotify_update.h" +#include "inotify_source.h" +#include "inotify_queue.h" +#include "database.h" +#include "mapper.h" +#include "path.h" + +#include <assert.h> +#include <sys/inotify.h> +#include <sys/stat.h> +#include <stdbool.h> +#include <string.h> +#include <dirent.h> +#include <errno.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +enum { + IN_MASK = IN_ATTRIB|IN_CLOSE_WRITE|IN_CREATE|IN_DELETE|IN_DELETE_SELF + |IN_MOVE|IN_MOVE_SELF +#ifdef IN_ONLYDIR + |IN_ONLYDIR +#endif +}; + +struct watch_directory { + struct watch_directory *parent; + + char *name; + + int descriptor; + + GList *children; +}; + +static struct mpd_inotify_source *inotify_source; + +static struct watch_directory inotify_root; +static GTree *inotify_directories; + +static gint +compare(gconstpointer a, gconstpointer b) +{ + if (a < b) + return -1; + else if (a > b) + return 1; + else + return 0; +} + +static void +tree_add_watch_directory(struct watch_directory *directory) +{ + g_tree_insert(inotify_directories, + GINT_TO_POINTER(directory->descriptor), directory); +} + +static void +tree_remove_watch_directory(struct watch_directory *directory) +{ + bool found = g_tree_remove(inotify_directories, + GINT_TO_POINTER(directory->descriptor)); + assert(found); +} + +static struct watch_directory * +tree_find_watch_directory(int wd) +{ + return g_tree_lookup(inotify_directories, GINT_TO_POINTER(wd)); +} + +static void +remove_watch_directory(struct watch_directory *directory) +{ + assert(directory != NULL); + assert(directory->parent != NULL); + assert(directory->parent->children != NULL); + + tree_remove_watch_directory(directory); + + while (directory->children != NULL) + remove_watch_directory(directory->children->data); + + directory->parent->children = + g_list_remove(directory->parent->children, directory); + + mpd_inotify_source_rm(inotify_source, directory->descriptor); + g_free(directory->name); + g_slice_free(struct watch_directory, directory); +} + +static char * +watch_directory_get_uri_fs(const struct watch_directory *directory) +{ + char *parent_uri, *uri; + + if (directory->parent == NULL) + return NULL; + + parent_uri = watch_directory_get_uri_fs(directory->parent); + if (parent_uri == NULL) + return g_strdup(directory->name); + + uri = g_strconcat(parent_uri, "/", directory->name, NULL); + g_free(parent_uri); + + return uri; +} + +/* we don't look at "." / ".." nor files with newlines in their name */ +static bool skip_path(const char *path) +{ + return (path[0] == '.' && path[1] == 0) || + (path[0] == '.' && path[1] == '.' && path[2] == 0) || + strchr(path, '\n') != NULL; +} + +static void +recursive_watch_subdirectories(struct watch_directory *directory, + const char *path_fs) +{ + GError *error = NULL; + DIR *dir; + struct dirent *ent; + + assert(directory != NULL); + assert(path_fs != NULL); + + dir = opendir(path_fs); + if (dir == NULL) { + g_warning("Failed to open directory %s: %s", + path_fs, g_strerror(errno)); + return; + } + + while ((ent = readdir(dir))) { + char *child_path_fs; + struct stat st; + int ret; + struct watch_directory *child; + + if (skip_path(ent->d_name)) + continue; + + child_path_fs = g_strconcat(path_fs, "/", ent->d_name, NULL); + /* XXX what about symlinks? */ + ret = lstat(child_path_fs, &st); + if (ret < 0) { + g_warning("Failed to stat %s: %s", + child_path_fs, g_strerror(errno)); + g_free(child_path_fs); + continue; + } + + if (!S_ISDIR(st.st_mode)) { + g_free(child_path_fs); + continue; + } + + ret = mpd_inotify_source_add(inotify_source, child_path_fs, + IN_MASK, &error); + if (ret < 0) { + g_warning("Failed to register %s: %s", + child_path_fs, error->message); + g_error_free(error); + error = NULL; + g_free(child_path_fs); + continue; + } + + child = tree_find_watch_directory(ret); + if (child != NULL) { + /* already being watched */ + g_free(child_path_fs); + continue; + } + + child = g_slice_new(struct watch_directory); + child->parent = directory; + child->name = g_strdup(ent->d_name); + child->descriptor = ret; + child->children = NULL; + + directory->children = g_list_prepend(directory->children, + child); + + tree_add_watch_directory(child); + + recursive_watch_subdirectories(child, child_path_fs); + g_free(child_path_fs); + } + + closedir(dir); +} + +static void +mpd_inotify_callback(int wd, unsigned mask, + G_GNUC_UNUSED const char *name, G_GNUC_UNUSED void *ctx) +{ + struct watch_directory *directory; + char *uri_fs; + + /*g_debug("wd=%d mask=0x%x name='%s'", wd, mask, name);*/ + + directory = tree_find_watch_directory(wd); + if (directory == NULL) + return; + + uri_fs = watch_directory_get_uri_fs(directory); + + if ((mask & (IN_DELETE_SELF|IN_MOVE_SELF)) != 0) { + g_free(uri_fs); + remove_watch_directory(directory); + return; + } + + if ((mask & (IN_ATTRIB|IN_CREATE|IN_MOVE)) != 0 && + (mask & IN_ISDIR) != 0) { + /* a sub directory was changed: register those in + inotify */ + char *root = map_directory_fs(db_get_root()); + char *path_fs; + + if (uri_fs != NULL) { + path_fs = g_strconcat(root, "/", uri_fs, NULL); + g_free(root); + } else + path_fs = root; + + recursive_watch_subdirectories(directory, path_fs); + g_free(path_fs); + } + + if ((mask & (IN_CLOSE_WRITE|IN_MOVE|IN_DELETE)) != 0) { + /* a file was changed, or a direectory was + moved/deleted: queue a database update */ + char *uri_utf8 = uri_fs != NULL + ? fs_charset_to_utf8(uri_fs) + : g_strdup(""); + + if (uri_utf8 != NULL) + /* this function will take care of freeing + uri_utf8 */ + mpd_inotify_enqueue(uri_utf8); + } + + g_free(uri_fs); +} + +void +mpd_inotify_init(void) +{ + struct directory *root; + char *path; + GError *error = NULL; + + g_debug("initializing inotify"); + + root = db_get_root(); + if (root == NULL) { + g_debug("no music directory configured"); + return; + } + + path = map_directory_fs(root); + if (path == NULL) { + g_warning("mapper has failed"); + return; + } + + inotify_source = mpd_inotify_source_new(mpd_inotify_callback, NULL, + &error); + if (inotify_source == NULL) { + g_warning("%s", error->message); + g_error_free(error); + g_free(path); + return; + } + + inotify_root.name = path; + inotify_root.descriptor = mpd_inotify_source_add(inotify_source, path, + IN_MASK, &error); + if (inotify_root.descriptor < 0) { + g_warning("%s", error->message); + g_error_free(error); + mpd_inotify_source_free(inotify_source); + inotify_source = NULL; + g_free(path); + return; + } + + inotify_directories = g_tree_new(compare); + tree_add_watch_directory(&inotify_root); + + recursive_watch_subdirectories(&inotify_root, path); + + mpd_inotify_queue_init(); + + g_debug("watching music directory"); +} + +static gboolean +free_watch_directory(G_GNUC_UNUSED gpointer key, gpointer value, + G_GNUC_UNUSED gpointer data) +{ + struct watch_directory *directory = value; + + g_free(directory->name); + g_list_free(directory->children); + + if (directory != &inotify_root) + g_slice_free(struct watch_directory, directory); + + return false; +} + +void +mpd_inotify_finish(void) +{ + if (inotify_source == NULL) + return; + + mpd_inotify_queue_finish(); + mpd_inotify_source_free(inotify_source); + + g_tree_foreach(inotify_directories, free_watch_directory, NULL); + g_tree_destroy(inotify_directories); +} diff --git a/src/inotify_update.h b/src/inotify_update.h new file mode 100644 index 000000000..45466afae --- /dev/null +++ b/src/inotify_update.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef MPD_INOTIFY_UPDATE_H +#define MPD_INOTIFY_UPDATE_H + +#include "config.h" + +#ifdef HAVE_INOTIFY_INIT + +void +mpd_inotify_init(void); + +void +mpd_inotify_finish(void); + +#else /* !HAVE_INOTIFY_INIT */ + +static inline void +mpd_inotify_init(void) +{ +} + +static inline void +mpd_inotify_finish(void) +{ +} + +#endif /* !HAVE_INOTIFY_INIT */ + +#endif diff --git a/src/input/curl_input_plugin.c b/src/input/curl_input_plugin.c index 95d269ce5..c3928a09b 100644 --- a/src/input/curl_input_plugin.c +++ b/src/input/curl_input_plugin.c @@ -42,7 +42,7 @@ #define G_LOG_DOMAIN "input_curl" /** rewinding is possible after up to 64 kB */ -static const off_t max_rewind_size = 64 * 1024; +static const goffset max_rewind_size = 64 * 1024; /** * Buffers created by input_curl_writefunction(). @@ -150,11 +150,6 @@ buffer_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) g_free(data); } -/* g_queue_clear() was introduced in GLib 2.14 */ -#if !GLIB_CHECK_VERSION(2,14,0) -#define g_queue_clear(q) do { g_queue_free(q); q = g_queue_new(); } while (0) -#endif - /** * Frees the current "libcurl easy" handle, and everything associated * with it. @@ -407,8 +402,8 @@ copy_icy_tag(struct input_curl *c) if (c->tag != NULL) tag_free(c->tag); - if (c->meta_name != NULL && !tag_has_type(tag, TAG_ITEM_NAME)) - tag_add_item(tag, TAG_ITEM_NAME, c->meta_name); + if (c->meta_name != NULL && !tag_has_type(tag, TAG_NAME)) + tag_add_item(tag, TAG_NAME, c->meta_name); c->tag = tag; } @@ -425,7 +420,7 @@ input_curl_read(struct input_stream *is, void *ptr, size_t size) #ifndef NDEBUG if (c->rewind != NULL && (!g_queue_is_empty(c->rewind) || is->offset == 0)) { - off_t offset = 0; + goffset offset = 0; struct buffer *buffer; for (GList *list = g_queue_peek_head_link(c->rewind); @@ -474,11 +469,11 @@ input_curl_read(struct input_stream *is, void *ptr, size_t size) if (icy_defined(&c->icy_metadata)) copy_icy_tag(c); - is->offset += (off_t)nbytes; + is->offset += (goffset)nbytes; #ifndef NDEBUG if (rewind_buffers != NULL) { - off_t offset = 0; + goffset offset = 0; struct buffer *buffer; for (GList *list = g_queue_peek_head_link(c->rewind); @@ -614,7 +609,7 @@ input_curl_headerfunction(void *ptr, size_t size, size_t nmemb, void *stream) tag_free(c->tag); c->tag = tag_new(); - tag_add_item(c->tag, TAG_ITEM_NAME, c->meta_name); + tag_add_item(c->tag, TAG_NAME, c->meta_name); } else if (g_ascii_strcasecmp(name, "icy-metaint") == 0) { char buffer[64]; size_t icy_metaint; @@ -772,7 +767,7 @@ input_curl_can_rewind(struct input_stream *is) /* rewind is possible if this is the very first buffer of the resource */ buffer = (struct buffer*)g_queue_peek_head(c->buffers); - return (off_t)buffer->consumed == is->offset; + return (goffset)buffer->consumed == is->offset; } static void @@ -780,7 +775,7 @@ input_curl_rewind(struct input_stream *is) { struct input_curl *c = is->data; #ifndef NDEBUG - off_t offset = 0; + goffset offset = 0; #endif assert(c->rewind != NULL); @@ -818,7 +813,7 @@ input_curl_rewind(struct input_stream *is) } static bool -input_curl_seek(struct input_stream *is, off_t offset, int whence) +input_curl_seek(struct input_stream *is, goffset offset, int whence) { struct input_curl *c = is->data; bool ret; @@ -884,7 +879,7 @@ input_curl_seek(struct input_stream *is, off_t offset, int whence) buffer = (struct buffer *)g_queue_pop_head(c->buffers); length = buffer->size - buffer->consumed; - if (offset - is->offset < (off_t)length) + if (offset - is->offset < (goffset)length) length = offset - is->offset; buffer = consume_buffer(buffer, length, rewind_buffers); diff --git a/src/input/file_input_plugin.c b/src/input/file_input_plugin.c index 64a4030ab..07c1e4ed6 100644 --- a/src/input/file_input_plugin.c +++ b/src/input/file_input_plugin.c @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" /* must be first for large file support */ #include "input/file_input_plugin.h" #include "input_plugin.h" @@ -38,7 +39,7 @@ input_file_open(struct input_stream *is, const char *filename) char* pathname = g_strdup(filename); - if (filename[0] != '/') + if (!g_path_is_absolute(filename)) { g_free(pathname); return false; @@ -92,11 +93,11 @@ input_file_open(struct input_stream *is, const char *filename) } static bool -input_file_seek(struct input_stream *is, off_t offset, int whence) +input_file_seek(struct input_stream *is, goffset offset, int whence) { int fd = GPOINTER_TO_INT(is->data); - offset = lseek(fd, offset, whence); + offset = (goffset)lseek(fd, (off_t)offset, whence); if (offset < 0) { is->error = errno; return false; diff --git a/src/input/lastfm_input_plugin.c b/src/input/lastfm_input_plugin.c deleted file mode 100644 index 0039f7069..000000000 --- a/src/input/lastfm_input_plugin.c +++ /dev/null @@ -1,229 +0,0 @@ -/* - * 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 "input/lastfm_input_plugin.h" -#include "input/curl_input_plugin.h" -#include "input_plugin.h" -#include "conf.h" - -#include <string.h> - -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "input_lastfm" - -static const char *lastfm_user, *lastfm_password; - -static bool -lastfm_input_init(const struct config_param *param) -{ - lastfm_user = config_get_block_string(param, "user", NULL); - lastfm_password = config_get_block_string(param, "password", NULL); - - return lastfm_user != NULL && lastfm_password != NULL; -} - -static char * -lastfm_get(const char *url) -{ - struct input_stream input_stream; - bool success; - int ret; - char buffer[4096]; - size_t length = 0, nbytes; - - success = input_stream_open(&input_stream, url); - if (!success) - return NULL; - - while (!input_stream.ready) { - ret = input_stream_buffer(&input_stream); - if (ret < 0) { - input_stream_close(&input_stream); - return NULL; - } - } - - do { - nbytes = input_stream_read(&input_stream, buffer + length, - sizeof(buffer) - length); - if (nbytes == 0) { - if (input_stream_eof(&input_stream)) - break; - - /* I/O error */ - input_stream_close(&input_stream); - return NULL; - } - - length += nbytes; - } while (length < sizeof(buffer)); - - input_stream_close(&input_stream); - return g_strndup(buffer, length); -} - -static char * -lastfm_find(const char *response, const char *name) -{ - size_t name_length = strlen(name); - - while (true) { - const char *eol = strchr(response, '\n'); - if (eol == NULL) - return NULL; - - if (strncmp(response, name, name_length) == 0 && - response[name_length] == '=') { - response += name_length + 1; - return g_strndup(response, eol - response); - } - - response = eol + 1; - } -} - -static bool -lastfm_input_open(struct input_stream *is, const char *url) -{ - char *md5, *p, *q, *response, *session, *stream_url; - bool success; - - if (strncmp(url, "lastfm://", 9) != 0) - return false; - - /* handshake */ - -#if GLIB_CHECK_VERSION(2,16,0) - q = g_uri_escape_string(lastfm_user, NULL, false); -#else - q = g_strdup(lastfm_username); -#endif - -#if GLIB_CHECK_VERSION(2,16,0) - if (strlen(lastfm_password) != 32) - md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, - lastfm_password, - strlen(lastfm_password)); - else -#endif - md5 = g_strdup(lastfm_password); - - p = g_strconcat("http://ws.audioscrobbler.com/radio/handshake.php?" - "version=1.1.1&platform=linux&" - "username=", q, "&" - "passwordmd5=", md5, "&debug=0&partner=", NULL); - g_free(q); - g_free(md5); - - response = lastfm_get(p); - g_free(p); - if (response == NULL) - return false; - - /* extract session id from response */ - - session = lastfm_find(response, "session"); - stream_url = lastfm_find(response, "stream_url"); - g_free(response); - if (session == NULL || stream_url == NULL) { - g_free(session); - g_free(stream_url); - return false; - } - -#if GLIB_CHECK_VERSION(2,16,0) - q = g_uri_escape_string(session, NULL, false); - g_free(session); - session = q; -#endif - - /* "adjust" last.fm radio */ - - if (strlen(url) > 9) { - char *escaped_url; - -#if GLIB_CHECK_VERSION(2,16,0) - escaped_url = g_uri_escape_string(url, NULL, false); -#else - escaped_url = g_strdup(url); -#endif - - p = g_strconcat("http://ws.audioscrobbler.com/radio/adjust.php?" - "session=", session, "&url=", escaped_url, "&debug=0", - NULL); - g_free(escaped_url); - - response = lastfm_get(p); - g_free(response); - g_free(p); - - if (response == NULL) { - g_free(session); - g_free(stream_url); - return false; - } - } - - /* load the last.fm playlist */ - - p = g_strconcat("http://ws.audioscrobbler.com/radio/xspf.php?" - "sk=", session, "&discovery=0&desktop=1.5.1.31879", - NULL); - g_free(session); - - response = lastfm_get(p); - g_free(p); - - if (response == NULL) { - g_free(stream_url); - return false; - } - - p = strstr(response, "<location>"); - if (p == NULL) { - g_free(response); - g_free(stream_url); - return false; - } - - p += 10; - q = strchr(p, '<'); - - if (q == NULL) { - g_free(response); - g_free(stream_url); - return false; - } - - g_free(stream_url); - stream_url = g_strndup(p, q - p); - g_free(response); - - /* now really open the last.fm radio stream */ - - success = input_stream_open(is, stream_url); - g_free(stream_url); - return success; -} - -const struct input_plugin lastfm_input_plugin = { - .name = "lastfm", - .init = lastfm_input_init, - .open = lastfm_input_open, -}; diff --git a/src/input/mms_input_plugin.c b/src/input/mms_input_plugin.c index 2a3c53776..335571bef 100644 --- a/src/input/mms_input_plugin.c +++ b/src/input/mms_input_plugin.c @@ -110,7 +110,7 @@ input_mms_buffer(G_GNUC_UNUSED struct input_stream *is) static bool input_mms_seek(G_GNUC_UNUSED struct input_stream *is, - G_GNUC_UNUSED off_t offset, G_GNUC_UNUSED int whence) + G_GNUC_UNUSED goffset offset, G_GNUC_UNUSED int whence) { return false; } diff --git a/src/input_plugin.h b/src/input_plugin.h index 8fe852bc6..f38174d42 100644 --- a/src/input_plugin.h +++ b/src/input_plugin.h @@ -53,7 +53,7 @@ struct input_plugin { int (*buffer)(struct input_stream *is); size_t (*read)(struct input_stream *is, void *ptr, size_t size); bool (*eof)(struct input_stream *is); - bool (*seek)(struct input_stream *is, off_t offset, int whence); + bool (*seek)(struct input_stream *is, goffset offset, int whence); }; #endif diff --git a/src/input_stream.c b/src/input_stream.c index 69dc644a2..c6da87996 100644 --- a/src/input_stream.c +++ b/src/input_stream.c @@ -27,12 +27,10 @@ #include "input/archive_input_plugin.h" #endif -#ifdef HAVE_CURL +#ifdef ENABLE_CURL #include "input/curl_input_plugin.h" #endif -#include "input/lastfm_input_plugin.h" - #ifdef ENABLE_MMS #include "input/mms_input_plugin.h" #endif @@ -46,12 +44,9 @@ static const struct input_plugin *const input_plugins[] = { #ifdef ENABLE_ARCHIVE &input_plugin_archive, #endif -#ifdef HAVE_CURL +#ifdef ENABLE_CURL &input_plugin_curl, #endif -#ifdef ENABLE_LASTFM - &lastfm_input_plugin, -#endif #ifdef ENABLE_MMS &input_plugin_mms, #endif @@ -139,7 +134,7 @@ input_stream_open(struct input_stream *is, const char *url) } bool -input_stream_seek(struct input_stream *is, off_t offset, int whence) +input_stream_seek(struct input_stream *is, goffset offset, int whence) { if (is->plugin->seek == NULL) return false; diff --git a/src/input_stream.h b/src/input_stream.h index 35b0d44fd..9e4a526ef 100644 --- a/src/input_stream.h +++ b/src/input_stream.h @@ -20,12 +20,12 @@ #ifndef MPD_INPUT_STREAM_H #define MPD_INPUT_STREAM_H +#include <glib.h> + #include <stddef.h> #include <stdbool.h> #include <sys/types.h> -struct input_stream; - struct input_stream { /** * the plugin which implements this input stream @@ -56,12 +56,12 @@ struct input_stream { /** * the size of the resource, or -1 if unknown */ - off_t size; + goffset size; /** * the current offset within the stream */ - off_t offset; + goffset offset; /** * the MIME content type of the resource, or NULL if unknown @@ -106,7 +106,7 @@ input_stream_close(struct input_stream *is); * @param whence the base of the seek, one of SEEK_SET, SEEK_CUR, SEEK_END */ bool -input_stream_seek(struct input_stream *is, off_t offset, int whence); +input_stream_seek(struct input_stream *is, goffset offset, int whence); /** * Returns true if the stream has reached end-of-file. diff --git a/src/listen.c b/src/listen.c index 98108d9da..4728e7c7a 100644 --- a/src/listen.c +++ b/src/listen.c @@ -347,7 +347,8 @@ listen_add_config_param(unsigned int port, } } -void listen_global_init(void) +bool +listen_global_init(GError **error_r) { int port = config_get_positive(CONF_PORT, DEFAULT_PORT); const struct config_param *param = @@ -361,10 +362,12 @@ void listen_global_init(void) do { success = listen_add_config_param(port, param, &error); - if (!success) - g_error("Failed to listen on %s (line %i): %s", - param->value, param->line, - error->message); + if (!success) { + g_propagate_prefixed_error(error_r, error, + "Failed to listen on %s (line %i): ", + param->value, param->line); + return false; + } param = config_get_next_param(CONF_BIND_TO_ADDRESS, param); @@ -374,12 +377,16 @@ void listen_global_init(void) configured port on all interfaces */ success = listen_add_port(port, &error); - if (!success) - g_error("Failed to listen on *:%d: %s", - port, error->message); + if (!success) { + g_propagate_prefixed_error(error_r, error, + "Failed to listen on *:%d: ", + port); + return false; + } } listen_port = port; + return true; } void listen_global_finish(void) diff --git a/src/listen.h b/src/listen.h index 63253fc53..9f55edb88 100644 --- a/src/listen.h +++ b/src/listen.h @@ -20,9 +20,14 @@ #ifndef MPD_LISTEN_H #define MPD_LISTEN_H +#include <glib.h> + +#include <stdbool.h> + extern int listen_port; -void listen_global_init(void); +bool +listen_global_init(GError **error_r); void listen_global_finish(void); @@ -32,12 +32,9 @@ * connected by IPC socket. */ static const char *remoteUrlPrefixes[] = { -#ifdef HAVE_CURL +#ifdef ENABLE_CURL "http://", #endif -#ifdef ENABLE_LASTFM - "lastfm://", -#endif #ifdef ENABLE_MMS "mms://", "mmsh://", diff --git a/src/main.c b/src/main.c index 5035a4836..18647eb80 100644 --- a/src/main.c +++ b/src/main.c @@ -45,6 +45,7 @@ #include "replay_gain.h" #include "decoder_list.h" #include "input_stream.h" +#include "playlist_list.h" #include "state_file.h" #include "tag.h" #include "dbUtils.h" @@ -56,6 +57,10 @@ #include "songvec.h" #include "tag_pool.h" +#ifdef ENABLE_INOTIFY +#include "inotify_update.h" +#endif + #ifdef ENABLE_SQLITE #include "sticker.h" #endif @@ -88,7 +93,32 @@ enum { GThread *main_task; GMainLoop *main_loop; -struct notify main_notify; +GCond *main_cond; + +static void +glue_daemonize_init(const struct options *options) +{ + daemonize_init(config_get_string(CONF_USER, NULL), + config_get_string(CONF_GROUP, NULL), + config_get_path(CONF_PID_FILE)); + + if (options->kill) + daemonize_kill(); +} + +static void +glue_mapper_init(void) +{ + const char *music_dir, *playlist_dir; + + music_dir = config_get_path(CONF_MUSIC_DIR); + if (music_dir == NULL) + music_dir = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC); + + playlist_dir = config_get_path(CONF_PLAYLIST_DIR); + + mapper_init(music_dir, playlist_dir); +} /** * Returns the database. If this function returns false, this has not @@ -96,7 +126,7 @@ struct notify main_notify; * process has been daemonized. */ static bool -openDB(const Options *options) +glue_db_init_and_load(void) { const char *path = config_get_path(CONF_DB_FILE); bool ret; @@ -115,19 +145,11 @@ openDB(const Options *options) db_init(path); - if (options->createDB > 0) - /* don't attempt to load the old database */ - return false; - ret = db_load(&error); if (!ret) { g_warning("Failed to load database: %s", error->message); g_error_free(error); - if (options->createDB < 0) - g_error("can't open db file and using " - "\"--no-create-db\" command line option"); - if (!db_check()) exit(EXIT_FAILURE); @@ -141,11 +163,34 @@ openDB(const Options *options) } /** + * Configure and initialize the sticker subsystem. + */ +static void +glue_sticker_init(void) +{ +#ifdef ENABLE_SQLITE + bool success; + GError *error = NULL; + + success = sticker_global_init(config_get_path(CONF_STICKER_FILE), + &error); + if (!success) + g_error("%s", error->message); +#endif +} + +static void +glue_state_file_init(void) +{ + state_file_init(config_get_path(CONF_STATE_FILE)); +} + +/** * Windows-only initialization of the Winsock2 library. */ -#ifdef WIN32 static void winsock_init(void) { +#ifdef WIN32 WSADATA sockinfo; int retval; @@ -161,9 +206,8 @@ static void winsock_init(void) g_error("We use Winsock2 but your version is either too new or " "old; please install Winsock 2.x\n"); } - -} #endif +} /** * Initialize the decoder and player core, including the music pipe. @@ -228,9 +272,11 @@ idle_event_emitted(void) int main(int argc, char *argv[]) { - Options options; + struct options options; clock_t start; bool create_db; + GError *error = NULL; + bool success; daemonize_close_stdin(); @@ -239,45 +285,51 @@ int main(int argc, char *argv[]) setlocale(LC_CTYPE,""); #endif + g_set_application_name("Music Player Daemon"); + /* enable GLib's thread safety code */ g_thread_init(NULL); -#ifdef WIN32 winsock_init(); -#endif idle_init(); dirvec_init(); songvec_init(); tag_pool_init(); config_global_init(); - parseOptions(argc, argv, &options); - - daemonize_init(config_get_string(CONF_USER, NULL), - config_get_path(CONF_PID_FILE)); + success = parse_cmdline(argc, argv, &options, &error); + if (!success) { + g_warning("%s", error->message); + g_error_free(error); + return EXIT_FAILURE; + } - if (options.kill) - daemonize_kill(); + glue_daemonize_init(&options); stats_global_init(); tag_lib_init(); - log_init(options.verbose, options.stdOutput); + log_init(options.verbose, options.log_stderr); - listen_global_init(); + success = listen_global_init(&error); + if (!success) { + g_warning("%s", error->message); + g_error_free(error); + return EXIT_FAILURE; + } daemonize_set_user(); main_task = g_thread_self(); main_loop = g_main_loop_new(NULL, FALSE); - notify_init(&main_notify); + main_cond = g_cond_new(); event_pipe_init(); event_pipe_register(PIPE_EVENT_IDLE, idle_event_emitted); path_global_init(); - mapper_init(); + glue_mapper_init(); initPermissions(); - initPlaylist(); + playlist_global_init(); spl_global_init(); #ifdef ENABLE_ARCHIVE archive_plugin_init_all(); @@ -285,11 +337,9 @@ int main(int argc, char *argv[]) decoder_plugin_init_all(); update_global_init(); - create_db = !openDB(&options); + create_db = !glue_db_init_and_load(); -#ifdef ENABLE_SQLITE - sticker_global_init(config_get_path(CONF_STICKER_FILE)); -#endif + glue_sticker_init(); command_init(); initialize_decoder_and_player(); @@ -300,10 +350,11 @@ int main(int argc, char *argv[]) replay_gain_global_init(); initNormalization(); input_stream_global_init(); + playlist_list_global_init(); daemonize(options.daemon); - setup_log_output(options.stdOutput); + setup_log_output(options.log_stderr); initSigHandlers(); @@ -312,15 +363,29 @@ int main(int argc, char *argv[]) player_create(); if (create_db) { - /* the database failed to load, or MPD was started - with --create-db: recreate a new database */ - unsigned job = directory_update_init(NULL); + /* the database failed to load: recreate the + database */ + unsigned job = update_enqueue(NULL, true); if (job == 0) g_error("directory update failed"); } + glue_state_file_init(); - state_file_init(config_get_path(CONF_STATE_FILE)); + success = config_get_bool(CONF_AUTO_UPDATE, false); +#ifdef ENABLE_INOTIFY + if (success && mapper_has_music_directory()) + mpd_inotify_init(); +#else + if (success) + g_warning("inotify: auto_update was disabled. enable during compilation phase"); +#endif + + config_global_check(); + + /* enable all audio outputs (if not already done by + playlist_state_restore() */ + pc_update_audio(); /* run the main loop */ @@ -330,12 +395,16 @@ int main(int argc, char *argv[]) g_main_loop_unref(main_loop); +#ifdef ENABLE_INOTIFY + mpd_inotify_finish(); +#endif + state_file_finish(); - playerKill(); + pc_kill(); finishZeroconf(); client_manager_deinit(); listen_global_finish(); - finishPlaylist(); + playlist_global_finish(); start = clock(); db_finish(); @@ -346,13 +415,13 @@ int main(int argc, char *argv[]) sticker_global_finish(); #endif - notify_deinit(&main_notify); + g_cond_free(main_cond); event_pipe_deinit(); + playlist_list_global_finish(); input_stream_global_finish(); finishNormalization(); audio_output_all_finish(); - finishAudioConfig(); volume_finish(); mapper_finish(); path_global_finish(); diff --git a/src/main.h b/src/main.h index 8ed02bf5d..de673829d 100644 --- a/src/main.h +++ b/src/main.h @@ -26,6 +26,6 @@ extern GThread *main_task; extern GMainLoop *main_loop; -extern struct notify main_notify; +extern GCond *main_cond; #endif diff --git a/src/mapper.c b/src/mapper.c index aac7c0c48..a48e12a35 100644 --- a/src/mapper.c +++ b/src/mapper.c @@ -25,16 +25,11 @@ #include "directory.h" #include "song.h" #include "path.h" -#include "conf.h" #include <glib.h> #include <assert.h> -#include <sys/types.h> -#include <sys/stat.h> -#include <unistd.h> #include <string.h> -#include <errno.h> static char *music_dir; static size_t music_dir_length; @@ -58,17 +53,10 @@ strdup_chop_slash(const char *path_fs) static void mapper_set_music_dir(const char *path) { - int ret; - struct stat st; - music_dir = strdup_chop_slash(path); music_dir_length = strlen(music_dir); - ret = stat(music_dir, &st); - if (ret < 0) - g_warning("failed to stat music directory \"%s\": %s", - music_dir, g_strerror(errno)); - else if (!S_ISDIR(st.st_mode)) + if (!g_file_test(music_dir, G_FILE_TEST_IS_DIR)) g_warning("music directory is not a directory: \"%s\"", music_dir); } @@ -76,38 +64,20 @@ mapper_set_music_dir(const char *path) static void mapper_set_playlist_dir(const char *path) { - int ret; - struct stat st; - playlist_dir = g_strdup(path); - ret = stat(playlist_dir, &st); - if (ret < 0) - g_warning("failed to stat playlist directory \"%s\": %s", - playlist_dir, g_strerror(errno)); - else if (!S_ISDIR(st.st_mode)) + if (!g_file_test(playlist_dir, G_FILE_TEST_IS_DIR)) g_warning("playlist directory is not a directory: \"%s\"", playlist_dir); } -void mapper_init(void) +void mapper_init(const char *_music_dir, const char *_playlist_dir) { - const char *path; - - path = config_get_path(CONF_MUSIC_DIR); - if (path != NULL) - mapper_set_music_dir(path); -#if GLIB_CHECK_VERSION(2,14,0) - else { - path = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC); - if (path != NULL) - mapper_set_music_dir(path); - } -#endif + if (_music_dir != NULL) + mapper_set_music_dir(_music_dir); - path = config_get_path(CONF_PLAYLIST_DIR); - if (path != NULL) - mapper_set_playlist_dir(path); + if (_playlist_dir != NULL) + mapper_set_playlist_dir(_playlist_dir); } void mapper_finish(void) @@ -189,9 +159,9 @@ map_song_fs(const struct song *song) assert(song_is_file(song)); if (song_in_database(song)) - return map_directory_child_fs(song->parent, song->url); + return map_directory_child_fs(song->parent, song->uri); else - return utf8_to_fs_charset(song->url); + return utf8_to_fs_charset(song->uri); } char * @@ -199,10 +169,10 @@ map_fs_to_utf8(const char *path_fs) { if (music_dir != NULL && strncmp(path_fs, music_dir, music_dir_length) == 0 && - path_fs[music_dir_length] == '/') + G_IS_DIR_SEPARATOR(path_fs[music_dir_length])) /* remove musicDir prefix */ path_fs += music_dir_length + 1; - else if (path_fs[0] == '/') + else if (G_IS_DIR_SEPARATOR(path_fs[0])) /* not within musicDir */ return NULL; diff --git a/src/mapper.h b/src/mapper.h index 2667f1eee..d63243ff7 100644 --- a/src/mapper.h +++ b/src/mapper.h @@ -31,7 +31,7 @@ struct directory; struct song; -void mapper_init(void); +void mapper_init(const char *_music_dir, const char *_playlist_dir); void mapper_finish(void); diff --git a/src/mixer/alsa_mixer.c b/src/mixer/alsa_mixer_plugin.c index 52e553cc5..baf12030c 100644 --- a/src/mixer/alsa_mixer.c +++ b/src/mixer/alsa_mixer_plugin.c @@ -17,8 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" -#include "../mixer_api.h" +#include "mixer_api.h" +#include "output_api.h" #include <glib.h> #include <alsa/asoundlib.h> @@ -42,12 +42,22 @@ struct alsa_mixer { int volume_set; }; +/** + * The quark used for GError.domain. + */ +static inline GQuark +alsa_mixer_quark(void) +{ + return g_quark_from_static_string("alsa_mixer"); +} + static struct mixer * -alsa_mixer_init(const struct config_param *param) +alsa_mixer_init(G_GNUC_UNUSED void *ao, const struct config_param *param, + G_GNUC_UNUSED GError **error_r) { struct alsa_mixer *am = g_new(struct alsa_mixer, 1); - mixer_init(&am->base, &alsa_mixer); + mixer_init(&am->base, &alsa_mixer_plugin); am->device = config_get_block_string(param, "mixer_device", VOLUME_MIXER_ALSA_DEFAULT); @@ -81,7 +91,7 @@ alsa_mixer_close(struct mixer *data) } static bool -alsa_mixer_open(struct mixer *data) +alsa_mixer_open(struct mixer *data, GError **error_r) { struct alsa_mixer *am = (struct alsa_mixer *)data; int err; @@ -91,29 +101,33 @@ alsa_mixer_open(struct mixer *data) err = snd_mixer_open(&am->handle, 0); if (err < 0) { - g_warning("problems opening alsa mixer: %s\n", snd_strerror(err)); + g_set_error(error_r, alsa_mixer_quark(), err, + "snd_mixer_open() failed: %s", snd_strerror(err)); return false; } if ((err = snd_mixer_attach(am->handle, am->device)) < 0) { - g_warning("problems attaching alsa mixer: %s\n", - snd_strerror(err)); alsa_mixer_close(data); + g_set_error(error_r, alsa_mixer_quark(), err, + "failed to attach to %s: %s", + am->device, snd_strerror(err)); return false; } if ((err = snd_mixer_selem_register(am->handle, NULL, NULL)) < 0) { - g_warning("problems snd_mixer_selem_register'ing: %s\n", - snd_strerror(err)); alsa_mixer_close(data); + g_set_error(error_r, alsa_mixer_quark(), err, + "snd_mixer_selem_register() failed: %s", + snd_strerror(err)); return false; } if ((err = snd_mixer_load(am->handle)) < 0) { - g_warning("problems snd_mixer_selem_register'ing: %s\n", - snd_strerror(err)); alsa_mixer_close(data); + g_set_error(error_r, alsa_mixer_quark(), err, + "snd_mixer_load() failed: %s\n", + snd_strerror(err)); return false; } @@ -138,14 +152,14 @@ alsa_mixer_open(struct mixer *data) return true; } - g_warning("can't find alsa mixer control \"%s\"\n", am->control); - alsa_mixer_close(data); + g_set_error(error_r, alsa_mixer_quark(), 0, + "no such mixer control: %s", am->control); return false; } static int -alsa_mixer_get_volume(struct mixer *mixer) +alsa_mixer_get_volume(struct mixer *mixer, GError **error_r) { struct alsa_mixer *am = (struct alsa_mixer *)mixer; int err; @@ -156,8 +170,9 @@ alsa_mixer_get_volume(struct mixer *mixer) err = snd_mixer_handle_events(am->handle); if (err < 0) { - g_warning("problems getting alsa volume: %s (snd_mixer_%s)\n", - snd_strerror(err), "handle_events"); + g_set_error(error_r, alsa_mixer_quark(), err, + "snd_mixer_handle_events() failed: %s", + snd_strerror(err)); return false; } @@ -165,8 +180,9 @@ alsa_mixer_get_volume(struct mixer *mixer) SND_MIXER_SCHN_FRONT_LEFT, &level); if (err < 0) { - g_warning("problems getting alsa volume: %s (snd_mixer_%s)\n", - snd_strerror(err), "selem_get_playback_volume"); + g_set_error(error_r, alsa_mixer_quark(), err, + "failed to read ALSA volume: %s", + snd_strerror(err)); return false; } @@ -183,7 +199,7 @@ alsa_mixer_get_volume(struct mixer *mixer) } static bool -alsa_mixer_set_volume(struct mixer *mixer, unsigned volume) +alsa_mixer_set_volume(struct mixer *mixer, unsigned volume, GError **error_r) { struct alsa_mixer *am = (struct alsa_mixer *)mixer; float vol; @@ -203,15 +219,16 @@ alsa_mixer_set_volume(struct mixer *mixer, unsigned volume) err = snd_mixer_selem_set_playback_volume_all(am->elem, level); if (err < 0) { - g_warning("problems setting alsa volume: %s\n", - snd_strerror(err)); + g_set_error(error_r, alsa_mixer_quark(), err, + "failed to set ALSA volume: %s", + snd_strerror(err)); return false; } return true; } -const struct mixer_plugin alsa_mixer = { +const struct mixer_plugin alsa_mixer_plugin = { .init = alsa_mixer_init, .finish = alsa_mixer_finish, .open = alsa_mixer_open, diff --git a/src/mixer/oss_mixer.c b/src/mixer/oss_mixer_plugin.c index f2db01ff4..4e169bbc4 100644 --- a/src/mixer/oss_mixer.c +++ b/src/mixer/oss_mixer_plugin.c @@ -17,8 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" -#include "../mixer_api.h" +#include "mixer_api.h" +#include "output_api.h" #include <glib.h> @@ -49,6 +49,15 @@ struct oss_mixer { int volume_control; }; +/** + * The quark used for GError.domain. + */ +static inline GQuark +oss_mixer_quark(void) +{ + return g_quark_from_static_string("oss_mixer"); +} + static int oss_find_mixer(const char *name) { @@ -65,11 +74,12 @@ oss_find_mixer(const char *name) } static struct mixer * -oss_mixer_init(const struct config_param *param) +oss_mixer_init(G_GNUC_UNUSED void *ao, const struct config_param *param, + GError **error_r) { struct oss_mixer *om = g_new(struct oss_mixer, 1); - mixer_init(&om->base, &oss_mixer); + mixer_init(&om->base, &oss_mixer_plugin); om->device = config_get_block_string(param, "mixer_device", VOLUME_MIXER_OSS_DEFAULT); @@ -78,9 +88,9 @@ oss_mixer_init(const struct config_param *param) if (om->control != NULL) { om->volume_control = oss_find_mixer(om->control); if (om->volume_control < 0) { - g_warning("mixer control \"%s\" not found", - om->control); g_free(om); + g_set_error(error_r, oss_mixer_quark(), 0, + "no such mixer control: %s", om->control); return NULL; } } else @@ -108,13 +118,15 @@ oss_mixer_close(struct mixer *data) } static bool -oss_mixer_open(struct mixer *data) +oss_mixer_open(struct mixer *data, GError **error_r) { struct oss_mixer *om = (struct oss_mixer *) data; om->device_fd = open(om->device, O_RDONLY); if (om->device_fd < 0) { - g_warning("Unable to open oss mixer \"%s\"\n", om->device); + g_set_error(error_r, oss_mixer_quark(), errno, + "failed to open %s: %s", + om->device, g_strerror(errno)); return false; } @@ -122,14 +134,17 @@ oss_mixer_open(struct mixer *data) int devmask = 0; if (ioctl(om->device_fd, SOUND_MIXER_READ_DEVMASK, &devmask) < 0) { - g_warning("errors getting read_devmask for oss mixer\n"); + g_set_error(error_r, oss_mixer_quark(), errno, + "READ_DEVMASK failed: %s", + g_strerror(errno)); oss_mixer_close(data); return false; } if (((1 << om->volume_control) & devmask) == 0) { - g_warning("mixer control \"%s\" not usable\n", - om->control); + g_set_error(error_r, oss_mixer_quark(), 0, + "mixer control \"%s\" not usable", + om->control); oss_mixer_close(data); return false; } @@ -138,7 +153,7 @@ oss_mixer_open(struct mixer *data) } static int -oss_mixer_get_volume(struct mixer *mixer) +oss_mixer_get_volume(struct mixer *mixer, GError **error_r) { struct oss_mixer *om = (struct oss_mixer *)mixer; int left, right, level; @@ -148,7 +163,9 @@ oss_mixer_get_volume(struct mixer *mixer) ret = ioctl(om->device_fd, MIXER_READ(om->volume_control), &level); if (ret < 0) { - g_warning("unable to read oss volume\n"); + g_set_error(error_r, oss_mixer_quark(), errno, + "failed to read OSS volume: %s", + g_strerror(errno)); return false; } @@ -164,7 +181,7 @@ oss_mixer_get_volume(struct mixer *mixer) } static bool -oss_mixer_set_volume(struct mixer *mixer, unsigned volume) +oss_mixer_set_volume(struct mixer *mixer, unsigned volume, GError **error_r) { struct oss_mixer *om = (struct oss_mixer *)mixer; int level; @@ -177,14 +194,16 @@ oss_mixer_set_volume(struct mixer *mixer, unsigned volume) ret = ioctl(om->device_fd, MIXER_WRITE(om->volume_control), &level); if (ret < 0) { - g_warning("unable to set oss volume\n"); + g_set_error(error_r, oss_mixer_quark(), errno, + "failed to set OSS volume: %s", + g_strerror(errno)); return false; } return true; } -const struct mixer_plugin oss_mixer = { +const struct mixer_plugin oss_mixer_plugin = { .init = oss_mixer_init, .finish = oss_mixer_finish, .open = oss_mixer_open, diff --git a/src/mixer/pulse_mixer.c b/src/mixer/pulse_mixer.c deleted file mode 100644 index 5d9ce0475..000000000 --- a/src/mixer/pulse_mixer.c +++ /dev/null @@ -1,382 +0,0 @@ -/* - * 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 "mixer_api.h" -#include "conf.h" - -#include <glib.h> -#include <pulse/volume.h> -#include <pulse/pulseaudio.h> - -#include <string.h> - -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "pulse_mixer" - -struct pulse_mixer { - struct mixer base; - - const char *server; - const char *sink; - const char *output_name; - - uint32_t index; - bool online; - - struct pa_context *context; - struct pa_threaded_mainloop *mainloop; - struct pa_cvolume volume; - -}; - -/** - * \brief waits for a pulseaudio operation to finish, frees it and - * unlocks the mainloop - * \param operation the operation to wait for - * \return true if operation has finished normally (DONE state), - * false otherwise - */ -static bool -pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop, - struct pa_operation *operation) -{ - pa_operation_state_t state; - - assert(mainloop != NULL); - assert(operation != NULL); - - state = pa_operation_get_state(operation); - while (state == PA_OPERATION_RUNNING) { - pa_threaded_mainloop_wait(mainloop); - state = pa_operation_get_state(operation); - } - - pa_operation_unref(operation); - - return state == PA_OPERATION_DONE; -} - -static void -sink_input_cb(G_GNUC_UNUSED pa_context *context, const pa_sink_input_info *i, - int eol, void *userdata) -{ - - struct pulse_mixer *pm = userdata; - - if (eol) { - g_debug("eol error sink_input_cb"); - return; - } - - if (i == NULL) { - g_debug("Sink input callback failure"); - return; - } - - g_debug("sink input cb %s, index %d ",i->name,i->index); - - if (strcmp(i->name,pm->output_name) == 0) { - pm->index = i->index; - pm->online = true; - pm->volume = i->volume; - } else - g_debug("bad name"); -} - -static void -sink_input_vol(G_GNUC_UNUSED pa_context *context, const pa_sink_input_info *i, - int eol, void *userdata) -{ - - struct pulse_mixer *pm = userdata; - - if (eol) { - g_debug("eol error sink_input_vol"); - return; - } - - if (i == NULL) { - g_debug("Sink input callback failure"); - return; - } - - g_debug("sink input vol %s, index %d ", i->name, i->index); - - pm->volume = i->volume; - - pa_threaded_mainloop_signal(pm->mainloop, 0); -} - -static void -subscribe_cb(pa_context *c, pa_subscription_event_type_t t, - uint32_t idx, void *userdata) -{ - - struct pulse_mixer *pm = userdata; - - g_debug("subscribe call back"); - - switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { - case PA_SUBSCRIPTION_EVENT_SINK_INPUT: - if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == - PA_SUBSCRIPTION_EVENT_REMOVE && - pm->index == idx) - pm->online = false; - else { - pa_operation *o; - - o = pa_context_get_sink_input_info(c, idx, - sink_input_cb, pm); - if (o == NULL) { - g_debug("pa_context_get_sink_input_info() failed"); - return; - } - - pa_operation_unref(o); - } - - break; - } -} - -static void -context_state_cb(pa_context *context, void *userdata) -{ - struct pulse_mixer *pm = userdata; - - switch (pa_context_get_state(context)) { - case PA_CONTEXT_READY: { - pa_operation *o; - - pa_context_set_subscribe_callback(context, subscribe_cb, pm); - - o = pa_context_subscribe(context, - (pa_subscription_mask_t)PA_SUBSCRIPTION_MASK_SINK_INPUT, - NULL, NULL); - if (o == NULL) { - g_debug("pa_context_subscribe() failed"); - return; - } - - pa_operation_unref(o); - - o = pa_context_get_sink_input_info_list(context, - sink_input_cb, pm); - if (o == NULL) { - g_debug("pa_context_get_sink_input_info_list() failed"); - return; - } - - pa_operation_unref(o); - - pa_threaded_mainloop_signal(pm->mainloop, 0); - break; - } - - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - pa_threaded_mainloop_signal(pm->mainloop, 0); - break; - } -} - - -static struct mixer * -pulse_mixer_init(const struct config_param *param) -{ - struct pulse_mixer *pm = g_new(struct pulse_mixer,1); - mixer_init(&pm->base, &pulse_mixer); - - pm->online = false; - - pm->server = config_get_block_string(param, "server", NULL); - pm->sink = config_get_block_string(param, "sink", NULL); - pm->output_name = config_get_block_string(param, "name", NULL); - - return &pm->base; -} - -static void -pulse_mixer_finish(struct mixer *data) -{ - struct pulse_mixer *pm = (struct pulse_mixer *) data; - - g_free(pm); -} - -static bool -pulse_mixer_setup(struct pulse_mixer *pm) -{ - pa_context_set_state_callback(pm->context, context_state_cb, pm); - - if (pa_context_connect(pm->context, pm->server, - (pa_context_flags_t)0, NULL) < 0) { - g_debug("context server fail"); - return false; - } - - pa_threaded_mainloop_lock(pm->mainloop); - - if (pa_threaded_mainloop_start(pm->mainloop) < 0) { - pa_threaded_mainloop_unlock(pm->mainloop); - g_debug("error start mainloop"); - return false; - } - - pa_threaded_mainloop_wait(pm->mainloop); - - if (pa_context_get_state(pm->context) != PA_CONTEXT_READY) { - pa_threaded_mainloop_unlock(pm->mainloop); - g_debug("error context not ready"); - return false; - } - - pa_threaded_mainloop_unlock(pm->mainloop); - - return true; -} - -static bool -pulse_mixer_open(struct mixer *data) -{ - struct pulse_mixer *pm = (struct pulse_mixer *) data; - - g_debug("pulse mixer open"); - - pm->index = 0; - pm->online = false; - - pm->mainloop = pa_threaded_mainloop_new(); - if (pm->mainloop == NULL) { - g_debug("failed mainloop"); - return false; - } - - pm->context = pa_context_new(pa_threaded_mainloop_get_api(pm->mainloop), - "Mixer mpd"); - if (pm->context == NULL) { - pa_threaded_mainloop_stop(pm->mainloop); - pa_threaded_mainloop_free(pm->mainloop); - g_debug("failed context"); - return false; - } - - if (!pulse_mixer_setup(pm)) { - pa_threaded_mainloop_stop(pm->mainloop); - pa_context_disconnect(pm->context); - pa_context_unref(pm->context); - pa_threaded_mainloop_free(pm->mainloop); - return false; - } - - return true; -} - -static void -pulse_mixer_close(struct mixer *data) -{ - struct pulse_mixer *pm = (struct pulse_mixer *) data; - - pa_threaded_mainloop_stop(pm->mainloop); - pa_context_disconnect(pm->context); - pa_context_unref(pm->context); - pa_threaded_mainloop_free(pm->mainloop); - - pm->online = false; -} - -static int -pulse_mixer_get_volume(struct mixer *mixer) -{ - struct pulse_mixer *pm = (struct pulse_mixer *) mixer; - int ret; - pa_operation *o; - - pa_threaded_mainloop_lock(pm->mainloop); - - if (!pm->online) { - pa_threaded_mainloop_unlock(pm->mainloop); - return false; - } - - o = pa_context_get_sink_input_info(pm->context, pm->index, - sink_input_vol, pm); - if (o == NULL) { - pa_threaded_mainloop_unlock(pm->mainloop); - g_debug("pa_context_get_sink_input_info() failed"); - return false; - } - - if (!pulse_wait_for_operation(pm->mainloop, o)) { - pa_threaded_mainloop_unlock(pm->mainloop); - return false; - } - - ret = pm->online - ? (int)((100*(pa_cvolume_avg(&pm->volume)+1))/PA_VOLUME_NORM) - : -1; - - pa_threaded_mainloop_unlock(pm->mainloop); - - return ret; -} - -static bool -pulse_mixer_set_volume(struct mixer *mixer, unsigned volume) -{ - struct pulse_mixer *pm = (struct pulse_mixer *) mixer; - struct pa_cvolume cvolume; - pa_operation *o; - - pa_threaded_mainloop_lock(pm->mainloop); - - if (!pm->online) { - pa_threaded_mainloop_unlock(pm->mainloop); - return false; - } - - pa_cvolume_set(&cvolume, pm->volume.channels, - (pa_volume_t)volume * PA_VOLUME_NORM / 100 + 0.5); - - o = pa_context_set_sink_input_volume(pm->context, pm->index, - &cvolume, NULL, NULL); - pa_threaded_mainloop_unlock(pm->mainloop); - if (o == NULL) { - g_debug("pa_context_set_sink_input_volume() failed"); - return false; - } - - pa_operation_unref(o); - - return true; -} - -const struct mixer_plugin pulse_mixer = { - .init = pulse_mixer_init, - .finish = pulse_mixer_finish, - .open = pulse_mixer_open, - .close = pulse_mixer_close, - .get_volume = pulse_mixer_get_volume, - .set_volume = pulse_mixer_set_volume, -}; diff --git a/src/mixer/pulse_mixer_plugin.c b/src/mixer/pulse_mixer_plugin.c new file mode 100644 index 000000000..53f4436ea --- /dev/null +++ b/src/mixer/pulse_mixer_plugin.c @@ -0,0 +1,233 @@ +/* + * 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 "pulse_mixer_plugin.h" +#include "mixer_api.h" +#include "output/pulse_output_plugin.h" +#include "conf.h" +#include "event_pipe.h" + +#include <glib.h> + +#include <pulse/thread-mainloop.h> +#include <pulse/context.h> +#include <pulse/introspect.h> +#include <pulse/stream.h> +#include <pulse/subscribe.h> +#include <pulse/error.h> + +#include <assert.h> +#include <string.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "pulse_mixer" + +struct pulse_mixer { + struct mixer base; + + struct pulse_output *output; + + bool online; + struct pa_cvolume volume; + +}; + +/** + * The quark used for GError.domain. + */ +static inline GQuark +pulse_mixer_quark(void) +{ + return g_quark_from_static_string("pulse_mixer"); +} + +static void +pulse_mixer_offline(struct pulse_mixer *pm) +{ + if (!pm->online) + return; + + pm->online = false; + + event_pipe_emit(PIPE_EVENT_MIXER); +} + +/** + * Callback invoked by pulse_mixer_update(). Receives the new mixer + * value. + */ +static void +pulse_mixer_volume_cb(G_GNUC_UNUSED pa_context *context, const pa_sink_input_info *i, + int eol, void *userdata) +{ + struct pulse_mixer *pm = userdata; + + if (eol) + return; + + if (i == NULL) { + pulse_mixer_offline(pm); + return; + } + + pm->online = true; + pm->volume = i->volume; + + event_pipe_emit(PIPE_EVENT_MIXER); +} + +static void +pulse_mixer_update(struct pulse_mixer *pm, + struct pa_context *context, struct pa_stream *stream) +{ + pa_operation *o; + + assert(context != NULL); + assert(stream != NULL); + assert(pa_stream_get_state(stream) == PA_STREAM_READY); + + o = pa_context_get_sink_input_info(context, + pa_stream_get_index(stream), + pulse_mixer_volume_cb, pm); + if (o == NULL) { + g_warning("pa_context_get_sink_input_info() failed: %s", + pa_strerror(pa_context_errno(context))); + pulse_mixer_offline(pm); + return; + } + + pa_operation_unref(o); +} + +void +pulse_mixer_on_connect(G_GNUC_UNUSED struct pulse_mixer *pm, + struct pa_context *context) +{ + pa_operation *o; + + assert(context != NULL); + + o = pa_context_subscribe(context, + (pa_subscription_mask_t)PA_SUBSCRIPTION_MASK_SINK_INPUT, + NULL, NULL); + if (o == NULL) { + g_warning("pa_context_subscribe() failed: %s", + pa_strerror(pa_context_errno(context))); + return; + } + + pa_operation_unref(o); +} + +void +pulse_mixer_on_disconnect(struct pulse_mixer *pm) +{ + pulse_mixer_offline(pm); +} + +void +pulse_mixer_on_change(struct pulse_mixer *pm, + struct pa_context *context, struct pa_stream *stream) +{ + pulse_mixer_update(pm, context, stream); +} + +static struct mixer * +pulse_mixer_init(void *ao, G_GNUC_UNUSED const struct config_param *param, + GError **error_r) +{ + struct pulse_mixer *pm; + struct pulse_output *po = ao; + + if (ao == NULL) { + g_set_error(error_r, pulse_mixer_quark(), 0, + "The pulse mixer cannot work without the audio output"); + return false; + } + + pm = g_new(struct pulse_mixer,1); + mixer_init(&pm->base, &pulse_mixer_plugin); + + pm->online = false; + pm->output = po; + + pulse_output_set_mixer(po, pm); + + return &pm->base; +} + +static void +pulse_mixer_finish(struct mixer *data) +{ + struct pulse_mixer *pm = (struct pulse_mixer *) data; + + pulse_output_clear_mixer(pm->output, pm); + + /* free resources */ + + g_free(pm); +} + +static int +pulse_mixer_get_volume(struct mixer *mixer, G_GNUC_UNUSED GError **error_r) +{ + struct pulse_mixer *pm = (struct pulse_mixer *) mixer; + int ret; + + pa_threaded_mainloop_lock(pm->output->mainloop); + + ret = pm->online + ? (int)((100*(pa_cvolume_avg(&pm->volume)+1))/PA_VOLUME_NORM) + : -1; + + pa_threaded_mainloop_unlock(pm->output->mainloop); + + return ret; +} + +static bool +pulse_mixer_set_volume(struct mixer *mixer, unsigned volume, GError **error_r) +{ + struct pulse_mixer *pm = (struct pulse_mixer *) mixer; + struct pa_cvolume cvolume; + bool success; + + pa_threaded_mainloop_lock(pm->output->mainloop); + if (!pm->online) { + pa_threaded_mainloop_unlock(pm->output->mainloop); + g_set_error(error_r, pulse_mixer_quark(), 0, "disconnected"); + return false; + } + + pa_cvolume_set(&cvolume, pm->volume.channels, + (pa_volume_t)volume * PA_VOLUME_NORM / 100 + 0.5); + success = pulse_output_set_volume(pm->output, &cvolume, error_r); + if (success) + pm->volume = cvolume; + pa_threaded_mainloop_unlock(pm->output->mainloop); + + return success; +} + +const struct mixer_plugin pulse_mixer_plugin = { + .init = pulse_mixer_init, + .finish = pulse_mixer_finish, + .get_volume = pulse_mixer_get_volume, + .set_volume = pulse_mixer_set_volume, +}; diff --git a/src/mixer/pulse_mixer_plugin.h b/src/mixer/pulse_mixer_plugin.h new file mode 100644 index 000000000..2318b94be --- /dev/null +++ b/src/mixer/pulse_mixer_plugin.h @@ -0,0 +1,39 @@ +/* + * 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. + */ + +#ifndef MPD_PULSE_MIXER_PLUGIN_H +#define MPD_PULSE_MIXER_PLUGIN_H + +#include <pulse/def.h> + +struct pulse_mixer; +struct pa_context; +struct pa_stream; + +void +pulse_mixer_on_connect(struct pulse_mixer *pm, struct pa_context *context); + +void +pulse_mixer_on_disconnect(struct pulse_mixer *pm); + +void +pulse_mixer_on_change(struct pulse_mixer *pm, + struct pa_context *context, struct pa_stream *stream); + +#endif diff --git a/src/mixer/software_mixer_plugin.c b/src/mixer/software_mixer_plugin.c new file mode 100644 index 000000000..062b9b3ef --- /dev/null +++ b/src/mixer/software_mixer_plugin.c @@ -0,0 +1,108 @@ +/* + * 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 "software_mixer_plugin.h" +#include "mixer_api.h" +#include "filter_plugin.h" +#include "filter_registry.h" +#include "filter/volume_filter_plugin.h" +#include "pcm_volume.h" + +#include <assert.h> +#include <math.h> + +struct software_mixer { + /** the base mixer class */ + struct mixer base; + + struct filter *filter; + + unsigned volume; +}; + +static struct mixer * +software_mixer_init(G_GNUC_UNUSED void *ao, + G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct software_mixer *sm = g_new(struct software_mixer, 1); + + mixer_init(&sm->base, &software_mixer_plugin); + + sm->filter = filter_new(&volume_filter_plugin, NULL, NULL); + assert(sm->filter != NULL); + + sm->volume = 100; + + return &sm->base; +} + +static void +software_mixer_finish(struct mixer *data) +{ + struct software_mixer *sm = (struct software_mixer *)data; + + g_free(sm); +} + +static int +software_mixer_get_volume(struct mixer *mixer, G_GNUC_UNUSED GError **error_r) +{ + struct software_mixer *sm = (struct software_mixer *)mixer; + + return sm->volume; +} + +static bool +software_mixer_set_volume(struct mixer *mixer, unsigned volume, + G_GNUC_UNUSED GError **error_r) +{ + struct software_mixer *sm = (struct software_mixer *)mixer; + + assert(volume <= 100); + + sm->volume = volume; + + if (volume >= 100) + volume = PCM_VOLUME_1; + else if (volume > 0) + volume = pcm_float_to_volume((exp(volume / 25.0) - 1) / + (54.5981500331F - 1)); + + volume_filter_set(sm->filter, volume); + return true; +} + +const struct mixer_plugin software_mixer_plugin = { + .init = software_mixer_init, + .finish = software_mixer_finish, + .get_volume = software_mixer_get_volume, + .set_volume = software_mixer_set_volume, + .global = true, +}; + +struct filter * +software_mixer_get_filter(struct mixer *mixer) +{ + struct software_mixer *sm = (struct software_mixer *)mixer; + + assert(sm->base.plugin == &software_mixer_plugin); + + return sm->filter; +} diff --git a/src/mixer/software_mixer_plugin.h b/src/mixer/software_mixer_plugin.h new file mode 100644 index 000000000..a59f7edeb --- /dev/null +++ b/src/mixer/software_mixer_plugin.h @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#ifndef SOFTWARE_MIXER_PLUGIN_H +#define SOFTWARE_MIXER_PLUGIN_H + +struct mixer; +struct filter; + +/** + * Returns the (volume) filter associated with this mixer. All users + * of this mixer plugin should install this filter. + */ +struct filter * +software_mixer_get_filter(struct mixer *mixer); + +#endif diff --git a/src/mixer_all.c b/src/mixer_all.c index 252cb61ab..b9c1afdad 100644 --- a/src/mixer_all.c +++ b/src/mixer_all.c @@ -22,6 +22,9 @@ #include "output_all.h" #include "output_plugin.h" #include "output_internal.h" +#include "pcm_volume.h" +#include "mixer_api.h" +#include "mixer_list.h" #include <glib.h> @@ -35,6 +38,8 @@ output_mixer_get_volume(unsigned i) { struct audio_output *output; struct mixer *mixer; + int volume; + GError *error = NULL; assert(i < audio_output_count()); @@ -46,7 +51,14 @@ output_mixer_get_volume(unsigned i) if (mixer == NULL) return -1; - return mixer_get_volume(mixer); + volume = mixer_get_volume(mixer, &error); + if (volume < 0 && error != NULL) { + g_warning("Failed to read mixer for '%s': %s", + output->name, error->message); + g_error_free(error); + } + + return volume; } int @@ -70,12 +82,15 @@ mixer_all_get_volume(void) } static bool -output_mixer_set_volume(unsigned i, int volume, bool relative) +output_mixer_set_volume(unsigned i, unsigned volume) { struct audio_output *output; struct mixer *mixer; + bool success; + GError *error = NULL; assert(i < audio_output_count()); + assert(volume <= 100); output = audio_output_get(i); if (!output->enabled) @@ -85,31 +100,81 @@ output_mixer_set_volume(unsigned i, int volume, bool relative) if (mixer == NULL) return false; - if (relative) { - int prev = mixer_get_volume(mixer); - if (prev < 0) - return false; - - volume += prev; + success = mixer_set_volume(mixer, volume, &error); + if (!success && error != NULL) { + g_warning("Failed to set mixer for '%s': %s", + output->name, error->message); + g_error_free(error); } - if (volume > 100) - volume = 100; - else if (volume < 0) - volume = 0; - - return mixer_set_volume(mixer, volume); + return success; } bool -mixer_all_set_volume(int volume, bool relative) +mixer_all_set_volume(unsigned volume) { bool success = false; unsigned count = audio_output_count(); + assert(volume <= 100); + for (unsigned i = 0; i < count; i++) - success = output_mixer_set_volume(i, volume, relative) + success = output_mixer_set_volume(i, volume) || success; return success; } + +static int +output_mixer_get_software_volume(unsigned i) +{ + struct audio_output *output; + struct mixer *mixer; + + assert(i < audio_output_count()); + + output = audio_output_get(i); + if (!output->enabled) + return -1; + + mixer = output->mixer; + if (mixer == NULL || mixer->plugin != &software_mixer_plugin) + return -1; + + return mixer_get_volume(mixer, NULL); +} + +int +mixer_all_get_software_volume(void) +{ + unsigned count = audio_output_count(), ok = 0; + int volume, total = 0; + + for (unsigned i = 0; i < count; i++) { + volume = output_mixer_get_software_volume(i); + if (volume >= 0) { + total += volume; + ++ok; + } + } + + if (ok == 0) + return -1; + + return total / ok; +} + +void +mixer_all_set_software_volume(unsigned volume) +{ + unsigned count = audio_output_count(); + + assert(volume <= PCM_VOLUME_1); + + for (unsigned i = 0; i < count; i++) { + struct audio_output *output = audio_output_get(i); + if (output->mixer != NULL && + output->mixer->plugin == &software_mixer_plugin) + mixer_set_volume(output->mixer, volume, NULL); + } +} diff --git a/src/mixer_all.h b/src/mixer_all.h index 66c4988de..ebe8fed68 100644 --- a/src/mixer_all.h +++ b/src/mixer_all.h @@ -37,11 +37,26 @@ mixer_all_get_volume(void); /** * Sets the volume on all available mixers. * - * @param volume the volume (range 0..100 or -100..100 if #relative) - * @param relative if true, then the #volume is added to the current value + * @param volume the volume (range 0..100) * @return true on success, false on failure */ bool -mixer_all_set_volume(int volume, bool relative); +mixer_all_set_volume(unsigned volume); + +/** + * Similar to mixer_all_get_volume(), but gets the volume only for + * software mixers. See #software_mixer_plugin. This function fails + * if no software mixer is configured. + */ +int +mixer_all_get_software_volume(void); + +/** + * Similar to mixer_all_set_volume(), but sets the volume only for + * software mixers. See #software_mixer_plugin. This function cannot + * fail, because the underlying software mixers cannot fail either. + */ +void +mixer_all_set_software_volume(unsigned volume); #endif diff --git a/src/mixer_control.c b/src/mixer_control.c index a17885935..3369f9ae8 100644 --- a/src/mixer_control.c +++ b/src/mixer_control.c @@ -20,35 +20,22 @@ #include "mixer_control.h" #include "mixer_api.h" -#include <glib.h> - #include <assert.h> #include <stddef.h> #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "mixer" -static bool mixers_enabled = true; - -void -mixer_disable_all(void) -{ - g_debug("mixer api is disabled"); - mixers_enabled = false; -} - struct mixer * -mixer_new(const struct mixer_plugin *plugin, const struct config_param *param) +mixer_new(const struct mixer_plugin *plugin, void *ao, + const struct config_param *param, + GError **error_r) { struct mixer *mixer; - //mixers are disabled (by using software volume) - if (!mixers_enabled) { - return NULL; - } assert(plugin != NULL); - mixer = plugin->init(param); + mixer = plugin->init(ao, param, error_r); assert(mixer == NULL || mixer->plugin == plugin); @@ -68,7 +55,7 @@ mixer_free(struct mixer *mixer) } bool -mixer_open(struct mixer *mixer) +mixer_open(struct mixer *mixer, GError **error_r) { bool success; @@ -79,8 +66,10 @@ mixer_open(struct mixer *mixer) if (mixer->open) success = true; + else if (mixer->plugin->open == NULL) + success = mixer->open = true; else - success = mixer->open = mixer->plugin->open(mixer); + success = mixer->open = mixer->plugin->open(mixer, error_r); mixer->failed = !success; @@ -96,7 +85,9 @@ mixer_close_internal(struct mixer *mixer) assert(mixer->plugin != NULL); assert(mixer->open); - mixer->plugin->close(mixer); + if (mixer->plugin->close != NULL) + mixer->plugin->close(mixer); + mixer->open = false; } @@ -136,21 +127,26 @@ mixer_failed(struct mixer *mixer) } int -mixer_get_volume(struct mixer *mixer) +mixer_get_volume(struct mixer *mixer, GError **error_r) { int volume; assert(mixer != NULL); - if (mixer->plugin->global && !mixer->failed && !mixer_open(mixer)) + if (mixer->plugin->global && !mixer->failed && + !mixer_open(mixer, error_r)) return -1; g_mutex_lock(mixer->mutex); if (mixer->open) { - volume = mixer->plugin->get_volume(mixer); - if (volume < 0) + GError *error = NULL; + + volume = mixer->plugin->get_volume(mixer, &error); + if (volume < 0 && error != NULL) { + g_propagate_error(error_r, error); mixer_failed(mixer); + } } else volume = -1; @@ -160,22 +156,21 @@ mixer_get_volume(struct mixer *mixer) } bool -mixer_set_volume(struct mixer *mixer, unsigned volume) +mixer_set_volume(struct mixer *mixer, unsigned volume, GError **error_r) { bool success; assert(mixer != NULL); assert(volume <= 100); - if (mixer->plugin->global && !mixer->failed && !mixer_open(mixer)) + if (mixer->plugin->global && !mixer->failed && + !mixer_open(mixer, error_r)) return false; g_mutex_lock(mixer->mutex); if (mixer->open) { - success = mixer->plugin->set_volume(mixer, volume); - if (!success) - mixer_failed(mixer); + success = mixer->plugin->set_volume(mixer, volume, error_r); } else success = false; diff --git a/src/mixer_control.h b/src/mixer_control.h index 0f73e8f75..a550e0864 100644 --- a/src/mixer_control.h +++ b/src/mixer_control.h @@ -25,23 +25,24 @@ #ifndef MPD_MIXER_CONTROL_H #define MPD_MIXER_CONTROL_H +#include <glib.h> + #include <stdbool.h> struct mixer; struct mixer_plugin; struct config_param; -void -mixer_disable_all(void); - struct mixer * -mixer_new(const struct mixer_plugin *plugin, const struct config_param *param); +mixer_new(const struct mixer_plugin *plugin, void *ao, + const struct config_param *param, + GError **error_r); void mixer_free(struct mixer *mixer); bool -mixer_open(struct mixer *mixer); +mixer_open(struct mixer *mixer, GError **error_r); void mixer_close(struct mixer *mixer); @@ -54,9 +55,9 @@ void mixer_auto_close(struct mixer *mixer); int -mixer_get_volume(struct mixer *mixer); +mixer_get_volume(struct mixer *mixer, GError **error_r); bool -mixer_set_volume(struct mixer *mixer, unsigned volume); +mixer_set_volume(struct mixer *mixer, unsigned volume, GError **error_r); #endif diff --git a/src/mixer_list.h b/src/mixer_list.h index 7db4a00d8..2d9c773f6 100644 --- a/src/mixer_list.h +++ b/src/mixer_list.h @@ -25,8 +25,9 @@ #ifndef MPD_MIXER_LIST_H #define MPD_MIXER_LIST_H -extern const struct mixer_plugin alsa_mixer; -extern const struct mixer_plugin oss_mixer; -extern const struct mixer_plugin pulse_mixer; +extern const struct mixer_plugin software_mixer_plugin; +extern const struct mixer_plugin alsa_mixer_plugin; +extern const struct mixer_plugin oss_mixer_plugin; +extern const struct mixer_plugin pulse_mixer_plugin; #endif diff --git a/src/mixer_plugin.h b/src/mixer_plugin.h index 2b9b440e5..b6e67e8ff 100644 --- a/src/mixer_plugin.h +++ b/src/mixer_plugin.h @@ -27,6 +27,8 @@ #ifndef MPD_MIXER_PLUGIN_H #define MPD_MIXER_PLUGIN_H +#include <glib.h> + #include <stdbool.h> struct config_param; @@ -35,8 +37,16 @@ struct mixer; struct mixer_plugin { /** * Alocates and configures a mixer device. + * + * @param ao the pointer returned by audio_output_plugin.init + * @param param the configuration section, or NULL if there is + * no configuration + * @param error_r location to store the error occuring, or + * NULL to ignore errors + * @return a mixer object, or NULL on error */ - struct mixer *(*init)(const struct config_param *param); + struct mixer *(*init)(void *ao, const struct config_param *param, + GError **error_r); /** * Finish and free mixer data @@ -45,8 +55,12 @@ struct mixer_plugin { /** * Open mixer device + * + * @param error_r location to store the error occuring, or + * NULL to ignore errors + * @return true on success, false on error */ - bool (*open)(struct mixer *data); + bool (*open)(struct mixer *data, GError **error_r); /** * Close mixer device @@ -56,18 +70,23 @@ struct mixer_plugin { /** * Reads the current volume. * - * @return the current volume (0..100 including) or -1 on - * error + * @param error_r location to store the error occuring, or + * NULL to ignore errors + * @return the current volume (0..100 including) or -1 if + * unavailable or on error (error_r set, mixer will be closed) */ - int (*get_volume)(struct mixer *mixer); + int (*get_volume)(struct mixer *mixer, GError **error_r); /** * Sets the volume. * + * @param error_r location to store the error occuring, or + * NULL to ignore errors * @param volume the new volume (0..100 including) - * @return true on success + * @return true on success, false on error */ - bool (*set_volume)(struct mixer *mixer, unsigned volume); + bool (*set_volume)(struct mixer *mixer, unsigned volume, + GError **error_r); /** * If true, then the mixer is automatically opened, even if diff --git a/src/mixer_type.c b/src/mixer_type.c new file mode 100644 index 000000000..6cf007856 --- /dev/null +++ b/src/mixer_type.c @@ -0,0 +1,38 @@ +/* + * 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 "mixer_type.h" + +#include <assert.h> +#include <string.h> + +enum mixer_type +mixer_type_parse(const char *input) +{ + assert(input != NULL); + + if (strcmp(input, "none") == 0 || strcmp(input, "disabled") == 0) + return MIXER_TYPE_NONE; + else if (strcmp(input, "hardware") == 0) + return MIXER_TYPE_HARDWARE; + else if (strcmp(input, "software") == 0) + return MIXER_TYPE_SOFTWARE; + else + return MIXER_TYPE_UNKNOWN; +} diff --git a/src/mixer_type.h b/src/mixer_type.h new file mode 100644 index 000000000..ec3cbf9cc --- /dev/null +++ b/src/mixer_type.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef MPD_MIXER_TYPE_H +#define MPD_MIXER_TYPE_H + +enum mixer_type { + /** parser error */ + MIXER_TYPE_UNKNOWN, + + /** mixer disabled */ + MIXER_TYPE_NONE, + + /** software mixer with pcm_volume() */ + MIXER_TYPE_SOFTWARE, + + /** hardware mixer (output's plugin) */ + MIXER_TYPE_HARDWARE, +}; + +/** + * Parses a "mixer_type" setting from the configuration file. + * + * @param input the configured string value; must not be NULL + * @return a #mixer_type value; MIXER_TYPE_UNKNOWN means #input could + * not be parsed + */ +enum mixer_type +mixer_type_parse(const char *input); + +#endif diff --git a/src/output/alsa_plugin.c b/src/output/alsa_plugin.c index 818c83ca2..870115998 100644 --- a/src/output/alsa_plugin.c +++ b/src/output/alsa_plugin.c @@ -183,6 +183,19 @@ get_bitformat(const struct audio_format *af) return SND_PCM_FORMAT_UNKNOWN; } +static snd_pcm_format_t +byteswap_bitformat(snd_pcm_format_t fmt) +{ + switch(fmt) { + case SND_PCM_FORMAT_S16_LE: return SND_PCM_FORMAT_S16_BE; + case SND_PCM_FORMAT_S24_LE: return SND_PCM_FORMAT_S24_BE; + case SND_PCM_FORMAT_S32_LE: return SND_PCM_FORMAT_S32_BE; + case SND_PCM_FORMAT_S16_BE: return SND_PCM_FORMAT_S16_LE; + case SND_PCM_FORMAT_S24_BE: return SND_PCM_FORMAT_S24_LE; + case SND_PCM_FORMAT_S32_BE: return SND_PCM_FORMAT_S32_LE; + default: return SND_PCM_FORMAT_UNKNOWN; + } +} /** * Set up the snd_pcm_t object which was opened by the caller. Set up * the configured settings and the audio format. @@ -208,7 +221,6 @@ alsa_setup(struct alsa_data *ad, struct audio_format *audio_format, configure_hw: /* configure HW params */ snd_pcm_hw_params_alloca(&hwparams); - cmd = "snd_pcm_hw_params_any"; err = snd_pcm_hw_params_any(ad->pcm, hwparams); if (err < 0) @@ -236,13 +248,38 @@ configure_hw: } err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, bitformat); + if (err == -EINVAL && + byteswap_bitformat(bitformat) != SND_PCM_FORMAT_UNKNOWN) { + err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, + byteswap_bitformat(bitformat)); + if (err == 0) { + g_debug("ALSA device \"%s\": converting %u bit to reverse-endian\n", + alsa_device(ad), audio_format->bits); + audio_format->reverse_endian = 1; + } + } if (err == -EINVAL && (audio_format->bits == 24 || audio_format->bits == 16)) { /* fall back to 32 bit, let pcm_convert.c do the conversion */ err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, SND_PCM_FORMAT_S32); - if (err == 0) + if (err == 0) { + g_debug("ALSA device \"%s\": converting %u bit to 32 bit\n", + alsa_device(ad), audio_format->bits); + audio_format->bits = 32; + } + } + if (err == -EINVAL && (audio_format->bits == 24 || + audio_format->bits == 16)) { + /* fall back to 32 bit, let pcm_convert.c do the conversion */ + err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, + byteswap_bitformat(SND_PCM_FORMAT_S32)); + if (err == 0) { + g_debug("ALSA device \"%s\": converting %u bit to 32 bit backward-endian\n", + alsa_device(ad), audio_format->bits); audio_format->bits = 32; + audio_format->reverse_endian = 1; + } } if (err == -EINVAL && audio_format->bits != 16) { @@ -255,6 +292,17 @@ configure_hw: audio_format->bits = 16; } } + if (err == -EINVAL && audio_format->bits != 16) { + /* fall back to 16 bit, let pcm_convert.c do the conversion */ + err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, + byteswap_bitformat(SND_PCM_FORMAT_S16)); + if (err == 0) { + g_debug("ALSA device \"%s\": converting %u bit to 16 bit backward-endian\n", + alsa_device(ad), audio_format->bits); + audio_format->bits = 16; + audio_format->reverse_endian = 1; + } + } if (err < 0) { g_set_error(error, alsa_output_quark(), err, @@ -448,11 +496,19 @@ alsa_recover(struct alsa_data *ad, int err) } static void +alsa_drain(void *data) +{ + struct alsa_data *ad = data; + + snd_pcm_drain(ad->pcm); +} + +static void alsa_cancel(void *data) { struct alsa_data *ad = data; - alsa_recover(ad, snd_pcm_drop(ad->pcm)); + snd_pcm_drop(ad->pcm); } static void @@ -460,9 +516,6 @@ alsa_close(void *data) { struct alsa_data *ad = data; - if (snd_pcm_state(ad->pcm) == SND_PCM_STATE_RUNNING) - snd_pcm_drain(ad->pcm); - snd_pcm_close(ad->pcm); } @@ -494,7 +547,9 @@ const struct audio_output_plugin alsaPlugin = { .finish = alsa_finish, .open = alsa_open, .play = alsa_play, + .drain = alsa_drain, .cancel = alsa_cancel, .close = alsa_close, - .mixer_plugin = &alsa_mixer, + + .mixer_plugin = &alsa_mixer_plugin, }; diff --git a/src/output/fifo_plugin.c b/src/output/fifo_output_plugin.c index 76bbe8cfa..d3145748f 100644 --- a/src/output/fifo_plugin.c +++ b/src/output/fifo_output_plugin.c @@ -17,9 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" -#include "../utils.h" -#include "../timer.h" +#include "output_api.h" +#include "utils.h" +#include "timer.h" #include <glib.h> diff --git a/src/output/httpd_client.c b/src/output/httpd_client.c index 52a398e3b..8157ebb44 100644 --- a/src/output/httpd_client.c +++ b/src/output/httpd_client.c @@ -482,11 +482,6 @@ httpd_client_queue_size(const struct httpd_client *client) return size; } -/* g_queue_clear() was introduced in GLib 2.14 */ -#if !GLIB_CHECK_VERSION(2,14,0) -#define g_queue_clear(q) do { g_queue_free(q); q = g_queue_new(); } while (0) -#endif - void httpd_client_cancel(struct httpd_client *client) { diff --git a/src/output/httpd_internal.h b/src/output/httpd_internal.h index 2257e27a2..83e9498c6 100644 --- a/src/output/httpd_internal.h +++ b/src/output/httpd_internal.h @@ -97,6 +97,12 @@ struct httpd_output { * function. */ char buffer[32768]; + + /** + * The maximum and current number of clients connected + * at the same time. + */ + guint clients_max, clients_cnt; }; /** diff --git a/src/output/httpd_output_plugin.c b/src/output/httpd_output_plugin.c index 9fdf46456..675297cd3 100644 --- a/src/output/httpd_output_plugin.c +++ b/src/output/httpd_output_plugin.c @@ -76,6 +76,8 @@ httpd_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, else httpd->content_type = "application/octet-stream"; + httpd->clients_max = config_get_block_unsigned(param,"max_clients", 0); + /* initialize listen address */ sin = (struct sockaddr_in *)&httpd->address; @@ -124,6 +126,7 @@ httpd_client_add(struct httpd_output *httpd, int fd) httpd->encoder->plugin->tag == NULL); httpd->clients = g_list_prepend(httpd->clients, client); + httpd->clients_cnt++; /* pass metadata to client */ if (httpd->metadata) @@ -146,10 +149,16 @@ httpd_listen_in_event(G_GNUC_UNUSED GIOChannel *source, connected */ fd = accept(httpd->fd, (struct sockaddr*)&sa, &sa_length); - if (fd >= 0) - httpd_client_add(httpd, fd); - else if (fd < 0 && errno != EINTR) + if (fd >= 0) { + /* can we allow additional client */ + if (!httpd->clients_max || + httpd->clients_cnt < httpd->clients_max) + httpd_client_add(httpd, fd); + else + close(fd); + } else if (fd < 0 && errno != EINTR) { g_warning("accept() failed: %s", g_strerror(errno)); + } g_mutex_unlock(httpd->mutex); @@ -237,6 +246,7 @@ httpd_output_open(void *data, struct audio_format *audio_format, /* initialize other attributes */ httpd->clients = NULL; + httpd->clients_cnt = 0; httpd->timer = timer_new(audio_format); g_mutex_unlock(httpd->mutex); @@ -281,6 +291,7 @@ httpd_output_remove_client(struct httpd_output *httpd, assert(client != NULL); httpd->clients = g_list_remove(httpd->clients, client); + httpd->clients_cnt--; } void @@ -433,9 +444,8 @@ httpd_output_tag(void *data, const struct tag *tag) page_unref (httpd->metadata); httpd->metadata = - icy_server_metadata_page(tag, TAG_ITEM_ALBUM, - TAG_ITEM_ARTIST, - TAG_ITEM_TITLE, + icy_server_metadata_page(tag, TAG_ALBUM, + TAG_ARTIST, TAG_TITLE, TAG_NUM_OF_ITEM_TYPES); if (httpd->metadata != NULL) { g_mutex_lock(httpd->mutex); diff --git a/src/output/jack_plugin.c b/src/output/jack_output_plugin.c index 5dc1eca24..fba921e04 100644 --- a/src/output/jack_plugin.c +++ b/src/output/jack_output_plugin.c @@ -17,7 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" +#include "output_api.h" #include "config.h" #include <assert.h> @@ -47,7 +47,7 @@ struct jack_data { /* configuration */ char *output_ports[2]; - int ringbuffer_size; + size_t ringbuffer_size; /* the current audio format */ struct audio_format audio_format; @@ -58,6 +58,12 @@ struct jack_data { jack_ringbuffer_t *ringbuffer[2]; bool shutdown; + + /** + * While this flag is set, the "process" callback generates + * silence. + */ + bool pause; }; /** @@ -69,43 +75,6 @@ jack_output_quark(void) return g_quark_from_static_string("jack_output"); } -static void -mpd_jack_client_free(struct jack_data *jd) -{ - assert(jd != NULL); - - if (jd->client != NULL) { - jack_deactivate(jd->client); - jack_client_close(jd->client); - jd->client = NULL; - } - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { - if (jd->ringbuffer[i] != NULL) { - jack_ringbuffer_free(jd->ringbuffer[i]); - jd->ringbuffer[i] = NULL; - } - } -} - -static void -mpd_jack_free(struct jack_data *jd) -{ - assert(jd != NULL); - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->output_ports); ++i) - g_free(jd->output_ports[i]); - - g_free(jd); -} - -static void -mpd_jack_finish(void *data) -{ - struct jack_data *jd = data; - mpd_jack_free(jd); -} - static int mpd_jack_process(jack_nframes_t nframes, void *arg) { @@ -116,6 +85,19 @@ mpd_jack_process(jack_nframes_t nframes, void *arg) if (nframes <= 0) return 0; + if (jd->pause) { + /* generate silence while MPD is paused */ + + for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { + out = jack_port_get_buffer(jd->ports[i], nframes); + + for (jack_nframes_t f = 0; f < nframes; ++f) + out[f] = 0.0; + } + + return 0; + } + for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { available = jack_ringbuffer_read_space(jd->ringbuffer[i]); assert(available % sample_size == 0); @@ -166,9 +148,65 @@ mpd_jack_info(const char *msg) } #endif +/** + * Disconnect the JACK client. + */ +static void +mpd_jack_disconnect(struct jack_data *jd) +{ + assert(jd != NULL); + assert(jd->client != NULL); + + jack_deactivate(jd->client); + jack_client_close(jd->client); + jd->client = NULL; +} + +/** + * Connect the JACK client and performs some basic setup + * (e.g. register callbacks). + */ +static bool +mpd_jack_connect(struct jack_data *jd, GError **error_r) +{ + assert(jd != NULL); + + jd->shutdown = false; + + if ((jd->client = jack_client_new(jd->name)) == NULL) { + g_set_error(error_r, jack_output_quark(), 0, + "Failed to connect to JACK server"); + return false; + } + + jack_set_process_callback(jd->client, mpd_jack_process, jd); + jack_on_shutdown(jd->client, mpd_jack_shutdown, jd); + + for (unsigned i = 0; i < G_N_ELEMENTS(jd->ports); ++i) { + jd->ports[i] = jack_port_register(jd->client, port_names[i], + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (jd->ports[i] == NULL) { + g_set_error(error_r, jack_output_quark(), 0, + "Cannot register output port \"%s\"", + port_names[i]); + mpd_jack_disconnect(jd); + return false; + } + } + + return true; +} + +static bool +mpd_jack_test_default_device(void) +{ + return true; +} + static void * mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format, - const struct config_param *param, GError **error) + const struct config_param *param, GError **error_r) { struct jack_data *jd; const char *value; @@ -183,7 +221,7 @@ mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format, char **ports = g_strsplit(value, ",", 0); if (ports[0] == NULL || ports[1] == NULL || ports[2] != NULL) { - g_set_error(error, jack_output_quark(), 0, + g_set_error(error_r, jack_output_quark(), 0, "two port names expected in line %d", param->line); return NULL; @@ -210,47 +248,84 @@ mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format, return jd; } -static bool -mpd_jack_test_default_device(void) +static void +mpd_jack_finish(void *data) { - return true; + struct jack_data *jd = data; + + for (unsigned i = 0; i < G_N_ELEMENTS(jd->output_ports); ++i) + g_free(jd->output_ports[i]); + + g_free(jd); } static bool -mpd_jack_connect(struct jack_data *jd, GError **error) +mpd_jack_enable(void *data, GError **error_r) { - const char *output_ports[2], **jports; + struct jack_data *jd = (struct jack_data *)data; for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) - jd->ringbuffer[i] = - jack_ringbuffer_create(jd->ringbuffer_size); + jd->ringbuffer[i] = NULL; - jd->shutdown = false; + return mpd_jack_connect(jd, error_r); +} - if ((jd->client = jack_client_new(jd->name)) == NULL) { - g_set_error(error, jack_output_quark(), 0, - "Failed to connect to JACK server"); - return false; - } +static void +mpd_jack_disable(void *data) +{ + struct jack_data *jd = (struct jack_data *)data; - jack_set_process_callback(jd->client, mpd_jack_process, jd); - jack_on_shutdown(jd->client, mpd_jack_shutdown, jd); + if (jd->client != NULL) + mpd_jack_disconnect(jd); - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ports); ++i) { - jd->ports[i] = jack_port_register(jd->client, port_names[i], - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (jd->ports[i] == NULL) { - g_set_error(error, jack_output_quark(), 0, - "Cannot register output port \"%s\"", - port_names[i]); - return false; + for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { + if (jd->ringbuffer[i] != NULL) { + jack_ringbuffer_free(jd->ringbuffer[i]); + jd->ringbuffer[i] = NULL; } } +} + +/** + * Stops the playback on the JACK connection. + */ +static void +mpd_jack_stop(struct jack_data *jd) +{ + assert(jd != NULL); + + if (jd->client == NULL) + return; + + if (jd->shutdown) + /* the connection has failed; close it */ + mpd_jack_disconnect(jd); + else + /* the connection is alive: just stop playback */ + jack_deactivate(jd->client); +} + +static bool +mpd_jack_start(struct jack_data *jd, GError **error_r) +{ + const char *output_ports[2], **jports; + + if (jd->client == NULL && !mpd_jack_connect(jd, error_r)) + return false; + + /* allocate the ring buffers on the first open(); these + persist until MPD exits. It's too unsafe to delete them + because we can never know when mpd_jack_process() gets + called */ + for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) + if (jd->ringbuffer[i] == NULL) + jd->ringbuffer[i] = + jack_ringbuffer_create(jd->ringbuffer_size); if ( jack_activate(jd->client) ) { - g_set_error(error, jack_output_quark(), 0, + g_set_error(error_r, jack_output_quark(), 0, "cannot activate client"); + mpd_jack_stop(jd); return false; } @@ -260,8 +335,9 @@ mpd_jack_connect(struct jack_data *jd, GError **error) jports = jack_get_ports(jd->client, NULL, NULL, JackPortIsPhysical | JackPortIsInput); if (jports == NULL) { - g_set_error(error, jack_output_quark(), 0, + g_set_error(error_r, jack_output_quark(), 0, "no ports found"); + mpd_jack_stop(jd); return false; } @@ -284,13 +360,14 @@ mpd_jack_connect(struct jack_data *jd, GError **error) ret = jack_connect(jd->client, jack_port_name(jd->ports[i]), output_ports[i]); if (ret != 0) { - g_set_error(error, jack_output_quark(), 0, + g_set_error(error_r, jack_output_quark(), 0, "Not a valid JACK port: %s", output_ports[i]); if (jports != NULL) free(jports); + mpd_jack_stop(jd); return false; } } @@ -302,16 +379,16 @@ mpd_jack_connect(struct jack_data *jd, GError **error) } static bool -mpd_jack_open(void *data, struct audio_format *audio_format, GError **error) +mpd_jack_open(void *data, struct audio_format *audio_format, GError **error_r) { struct jack_data *jd = data; assert(jd != NULL); - if (!mpd_jack_connect(jd, error)) { - mpd_jack_client_free(jd); + jd->pause = false; + + if (!mpd_jack_start(jd, error_r)) return false; - } set_audioformat(jd, audio_format); jd->audio_format = *audio_format; @@ -324,12 +401,7 @@ mpd_jack_close(G_GNUC_UNUSED void *data) { struct jack_data *jd = data; - mpd_jack_client_free(jd); -} - -static void -mpd_jack_cancel (G_GNUC_UNUSED void *data) -{ + mpd_jack_stop(jd); } static inline jack_default_audio_sample_t @@ -399,18 +471,20 @@ mpd_jack_write_samples(struct jack_data *jd, const void *src, } static size_t -mpd_jack_play(void *data, const void *chunk, size_t size, GError **error) +mpd_jack_play(void *data, const void *chunk, size_t size, GError **error_r) { struct jack_data *jd = data; const size_t frame_size = audio_format_frame_size(&jd->audio_format); size_t space = 0, space1; + jd->pause = false; + assert(size % frame_size == 0); size /= frame_size; while (true) { if (jd->shutdown) { - g_set_error(error, jack_output_quark(), 0, + g_set_error(error_r, jack_output_quark(), 0, "Refusing to play, because " "there is no client thread"); return 0; @@ -438,13 +512,32 @@ mpd_jack_play(void *data, const void *chunk, size_t size, GError **error) return size * frame_size; } -const struct audio_output_plugin jackPlugin = { +static bool +mpd_jack_pause(void *data) +{ + struct jack_data *jd = data; + + if (jd->shutdown) + return false; + + jd->pause = true; + + /* due to a MPD API limitation, we have to sleep a little bit + here, to avoid hogging the CPU */ + g_usleep(50000); + + return true; +} + +const struct audio_output_plugin jack_output_plugin = { .name = "jack", .test_default_device = mpd_jack_test_default_device, .init = mpd_jack_init, .finish = mpd_jack_finish, + .enable = mpd_jack_enable, + .disable = mpd_jack_disable, .open = mpd_jack_open, .play = mpd_jack_play, - .cancel = mpd_jack_cancel, + .pause = mpd_jack_pause, .close = mpd_jack_close, }; diff --git a/src/output/openal_plugin.c b/src/output/openal_plugin.c new file mode 100644 index 000000000..92ee82ef3 --- /dev/null +++ b/src/output/openal_plugin.c @@ -0,0 +1,273 @@ +/* + * 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 "../timer.h" +#include "config.h" + +#include <glib.h> + +#ifndef HAVE_OSX +#include <AL/al.h> +#include <AL/alc.h> +#else +#include <OpenAL/al.h> +#include <OpenAL/alc.h> +#endif + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "openal" + +/* should be enough for buffer size = 2048 */ +#define NUM_BUFFERS 16 + +struct openal_data { + const char *device_name; + ALCdevice *device; + ALCcontext *context; + Timer *timer; + ALuint buffers[NUM_BUFFERS]; + int filled; + ALuint source; + ALenum format; + ALuint frequency; +}; + +static inline GQuark +openal_output_quark(void) +{ + return g_quark_from_static_string("openal_output"); +} + +static ALenum +openal_audio_format(struct audio_format *audio_format) +{ + /* Only 8 and 16 bit samples are supported */ + if (audio_format->bits != 16 && audio_format->bits != 8) + audio_format->bits = 16; + + switch (audio_format->bits) + { + case 16: + if (audio_format->channels == 2) + return AL_FORMAT_STEREO16; + if (audio_format->channels == 1) + return AL_FORMAT_MONO16; + break; + + case 8: + if (audio_format->channels == 2) + return AL_FORMAT_STEREO8; + if (audio_format->channels == 1) + return AL_FORMAT_MONO8; + break; + } + + return 0; +} + +static bool +openal_setup_context(struct openal_data *od, + GError **error) +{ + od->device = alcOpenDevice(od->device_name); + + if (od->device == NULL) { + g_set_error(error, openal_output_quark(), 0, + "Error opening OpenAL device \"%s\"\n", + od->device_name); + return false; + } + + od->context = alcCreateContext(od->device, NULL); + + if (od->context == NULL) { + g_set_error(error, openal_output_quark(), 0, + "Error creating context for \"%s\"\n", + od->device_name); + alcCloseDevice(od->device); + return false; + } + + return true; +} + +static void +openal_unqueue_buffers(struct openal_data *od) +{ + ALint num; + ALuint buffer; + + alGetSourcei(od->source, AL_BUFFERS_QUEUED, &num); + + while (num--) { + alSourceUnqueueBuffers(od->source, 1, &buffer); + } +} + +static void * +openal_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, + G_GNUC_UNUSED GError **error) +{ + const char *device_name = config_get_block_string(param, "device", NULL); + struct openal_data *od; + + if (device_name == NULL) { + device_name = alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER); + } + + od = g_new(struct openal_data, 1); + od->device_name = device_name; + + return od; +} + +static void +openal_finish(void *data) +{ + struct openal_data *od = data; + + g_free(od); +} + +static bool +openal_open(void *data, struct audio_format *audio_format, + GError **error) +{ + struct openal_data *od = data; + + od->format = openal_audio_format(audio_format); + + if (!od->format) { + g_set_error(error, openal_output_quark(), 0, + "Unsupported audio format (%i channels, %i bps)", + audio_format->channels, + audio_format->bits); + return false; + } + + if (!openal_setup_context(od, error)) { + return false; + } + + alcMakeContextCurrent(od->context); + alGenBuffers(NUM_BUFFERS, od->buffers); + + if (alGetError() != AL_NO_ERROR) { + g_set_error(error, openal_output_quark(), 0, + "Failed to generate buffers"); + return false; + } + + alGenSources(1, &od->source); + + if (alGetError() != AL_NO_ERROR) { + g_set_error(error, openal_output_quark(), 0, + "Failed to generate source"); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + return false; + } + + od->filled = 0; + od->timer = timer_new(audio_format); + od->frequency = audio_format->sample_rate; + + return true; +} + +static void +openal_close(void *data) +{ + struct openal_data *od = data; + + timer_free(od->timer); + alcMakeContextCurrent(od->context); + alDeleteSources(1, &od->source); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + alcDestroyContext(od->context); + alcCloseDevice(od->device); +} + +static size_t +openal_play(void *data, const void *chunk, size_t size, + G_GNUC_UNUSED GError **error) +{ + struct openal_data *od = data; + ALuint buffer; + ALint num, state; + + if (alcGetCurrentContext() != od->context) { + alcMakeContextCurrent(od->context); + } + + alGetSourcei(od->source, AL_BUFFERS_PROCESSED, &num); + + if (od->filled < NUM_BUFFERS) { + /* fill all buffers */ + buffer = od->buffers[od->filled]; + od->filled++; + } else { + /* wait for processed buffer */ + while (num < 1) { + if (!od->timer->started) { + timer_start(od->timer); + } else { + timer_sync(od->timer); + } + + timer_add(od->timer, size); + + alGetSourcei(od->source, AL_BUFFERS_PROCESSED, &num); + } + + alSourceUnqueueBuffers(od->source, 1, &buffer); + } + + alBufferData(buffer, od->format, chunk, size, od->frequency); + alSourceQueueBuffers(od->source, 1, &buffer); + alGetSourcei(od->source, AL_SOURCE_STATE, &state); + + if (state != AL_PLAYING) { + alSourcePlay(od->source); + } + + return size; +} + +static void +openal_cancel(void *data) +{ + struct openal_data *od = data; + + od->filled = 0; + alcMakeContextCurrent(od->context); + alSourceStop(od->source); + openal_unqueue_buffers(od); +} + +const struct audio_output_plugin openal_output_plugin = { + .name = "openal", + .init = openal_init, + .finish = openal_finish, + .open = openal_open, + .close = openal_close, + .play = openal_play, + .cancel = openal_cancel, +}; diff --git a/src/output/oss_plugin.c b/src/output/oss_plugin.c index a66bc0598..258fbfa9d 100644 --- a/src/output/oss_plugin.c +++ b/src/output/oss_plugin.c @@ -601,5 +601,6 @@ const struct audio_output_plugin oss_output_plugin = { .close = oss_output_close, .play = oss_output_play, .cancel = oss_output_cancel, - .mixer_plugin = &oss_mixer, + + .mixer_plugin = &oss_mixer_plugin, }; diff --git a/src/output/pulse_output_plugin.c b/src/output/pulse_output_plugin.c new file mode 100644 index 000000000..13e1b6624 --- /dev/null +++ b/src/output/pulse_output_plugin.c @@ -0,0 +1,823 @@ +/* + * 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 "pulse_output_plugin.h" +#include "output_api.h" +#include "mixer_list.h" +#include "mixer/pulse_mixer_plugin.h" + +#include <glib.h> + +#include <pulse/thread-mainloop.h> +#include <pulse/context.h> +#include <pulse/stream.h> +#include <pulse/introspect.h> +#include <pulse/subscribe.h> +#include <pulse/error.h> + +#include <assert.h> + +#define MPD_PULSE_NAME "Music Player Daemon" + +/** + * The quark used for GError.domain. + */ +static inline GQuark +pulse_output_quark(void) +{ + return g_quark_from_static_string("pulse_output"); +} + +void +pulse_output_set_mixer(struct pulse_output *po, struct pulse_mixer *pm) +{ + assert(po != NULL); + assert(po->mixer == NULL); + assert(pm != NULL); + + po->mixer = pm; + + if (po->mainloop == NULL) + return; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context != NULL && + pa_context_get_state(po->context) == PA_CONTEXT_READY) { + pulse_mixer_on_connect(pm, po->context); + + if (po->stream != NULL && + pa_stream_get_state(po->stream) == PA_STREAM_READY) + pulse_mixer_on_change(pm, po->context, po->stream); + } + + pa_threaded_mainloop_unlock(po->mainloop); +} + +void +pulse_output_clear_mixer(struct pulse_output *po, struct pulse_mixer *pm) +{ + assert(po != NULL); + assert(pm != NULL); + assert(po->mixer == pm); + + po->mixer = NULL; +} + +bool +pulse_output_set_volume(struct pulse_output *po, + const struct pa_cvolume *volume, GError **error_r) +{ + pa_operation *o; + + if (po->context == NULL || po->stream == NULL || + pa_stream_get_state(po->stream) != PA_STREAM_READY) { + g_set_error(error_r, pulse_output_quark(), 0, "disconnected"); + return false; + } + + o = pa_context_set_sink_input_volume(po->context, + pa_stream_get_index(po->stream), + volume, NULL, NULL); + if (o == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "failed to set PulseAudio volume: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + pa_operation_unref(o); + return true; +} + +/** + * \brief waits for a pulseaudio operation to finish, frees it and + * unlocks the mainloop + * \param operation the operation to wait for + * \return true if operation has finished normally (DONE state), + * false otherwise + */ +static bool +pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop, + struct pa_operation *operation) +{ + pa_operation_state_t state; + + assert(mainloop != NULL); + assert(operation != NULL); + + state = pa_operation_get_state(operation); + while (state == PA_OPERATION_RUNNING) { + pa_threaded_mainloop_wait(mainloop); + state = pa_operation_get_state(operation); + } + + pa_operation_unref(operation); + + return state == PA_OPERATION_DONE; +} + +/** + * Callback function for stream operation. It just sends a signal to + * the caller thread, to wake pulse_wait_for_operation() up. + */ +static void +pulse_output_stream_success_cb(G_GNUC_UNUSED pa_stream *s, + G_GNUC_UNUSED int success, void *userdata) +{ + struct pulse_output *po = userdata; + + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static void +pulse_output_context_state_cb(struct pa_context *context, void *userdata) +{ + struct pulse_output *po = userdata; + + switch (pa_context_get_state(context)) { + case PA_CONTEXT_READY: + if (po->mixer != NULL) + pulse_mixer_on_connect(po->mixer, context); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + if (po->mixer != NULL) + pulse_mixer_on_disconnect(po->mixer); + + /* the caller thread might be waiting for these + states */ + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } +} + +static void +pulse_output_subscribe_cb(pa_context *context, + pa_subscription_event_type_t t, + uint32_t idx, void *userdata) +{ + struct pulse_output *po = userdata; + pa_subscription_event_type_t facility + = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + pa_subscription_event_type_t type + = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + + if (po->mixer != NULL && + facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT && + po->stream != NULL && + pa_stream_get_state(po->stream) == PA_STREAM_READY && + idx == pa_stream_get_index(po->stream) && + (type == PA_SUBSCRIPTION_EVENT_NEW || + type == PA_SUBSCRIPTION_EVENT_CHANGE)) + pulse_mixer_on_change(po->mixer, context, po->stream); +} + +/** + * Attempt to connect asynchronously to the PulseAudio server. + * + * @return true on success, false on error + */ +static bool +pulse_output_connect(struct pulse_output *po, GError **error_r) +{ + int error; + + error = pa_context_connect(po->context, po->server, + (pa_context_flags_t)0, NULL); + if (error < 0) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_context_connect() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + return true; +} + +/** + * Create, set up and connect a context. + * + * @return true on success, false on error + */ +static bool +pulse_output_setup_context(struct pulse_output *po, GError **error_r) +{ + po->context = pa_context_new(pa_threaded_mainloop_get_api(po->mainloop), + MPD_PULSE_NAME); + if (po->context == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_context_new() has failed"); + return false; + } + + pa_context_set_state_callback(po->context, + pulse_output_context_state_cb, po); + pa_context_set_subscribe_callback(po->context, + pulse_output_subscribe_cb, po); + + if (!pulse_output_connect(po, error_r)) { + pa_context_unref(po->context); + return false; + } + + return true; +} + +/** + * Frees and clears the context. + */ +static void +pulse_output_delete_context(struct pulse_output *po) +{ + pa_context_disconnect(po->context); + pa_context_unref(po->context); + po->context = NULL; +} + +static void * +pulse_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct pulse_output *po; + + g_setenv("PULSE_PROP_media.role", "music", true); + + po = g_new(struct pulse_output, 1); + po->name = config_get_block_string(param, "name", "mpd_pulse"); + po->server = config_get_block_string(param, "server", NULL); + po->sink = config_get_block_string(param, "sink", NULL); + + po->mixer = NULL; + po->mainloop = NULL; + po->context = NULL; + po->stream = NULL; + + return po; +} + +static void +pulse_output_finish(void *data) +{ + struct pulse_output *po = data; + + g_free(po); +} + +static bool +pulse_output_enable(void *data, GError **error_r) +{ + struct pulse_output *po = data; + + assert(po->mainloop == NULL); + assert(po->context == NULL); + + /* create the libpulse mainloop and start the thread */ + + po->mainloop = pa_threaded_mainloop_new(); + if (po->mainloop == NULL) { + g_free(po); + + g_set_error(error_r, pulse_output_quark(), 0, + "pa_threaded_mainloop_new() has failed"); + return false; + } + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_threaded_mainloop_start(po->mainloop) < 0) { + pa_threaded_mainloop_unlock(po->mainloop); + pa_threaded_mainloop_free(po->mainloop); + g_free(po); + + g_set_error(error_r, pulse_output_quark(), 0, + "pa_threaded_mainloop_start() has failed"); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + /* create the libpulse context and connect it */ + + pa_threaded_mainloop_lock(po->mainloop); + + if (!pulse_output_setup_context(po, error_r)) { + pa_threaded_mainloop_unlock(po->mainloop); + pa_threaded_mainloop_stop(po->mainloop); + pa_threaded_mainloop_free(po->mainloop); + g_free(po); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static void +pulse_output_disable(void *data) +{ + struct pulse_output *po = data; + + pa_threaded_mainloop_stop(po->mainloop); + if (po->context != NULL) + pulse_output_delete_context(po); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = NULL; +} + +/** + * Check if the context is (already) connected, and waits if not. If + * the context has been disconnected, retry to connect. + * + * @return true on success, false on error + */ +static bool +pulse_output_wait_connection(struct pulse_output *po, GError **error_r) +{ + pa_context_state_t state; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context == NULL && !pulse_output_setup_context(po, error_r)) + return false; + + while (true) { + state = pa_context_get_state(po->context); + switch (state) { + case PA_CONTEXT_READY: + /* nothing to do */ + pa_threaded_mainloop_unlock(po->mainloop); + return true; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + /* failure */ + g_set_error(error_r, pulse_output_quark(), 0, + "failed to connect: %s", + pa_strerror(pa_context_errno(po->context))); + pulse_output_delete_context(po); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + /* wait some more */ + pa_threaded_mainloop_wait(po->mainloop); + break; + } + } +} + +static void +pulse_output_stream_state_cb(pa_stream *stream, void *userdata) +{ + struct pulse_output *po = userdata; + + switch (pa_stream_get_state(stream)) { + case PA_STREAM_READY: + if (po->mixer != NULL) + pulse_mixer_on_change(po->mixer, po->context, stream); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + if (po->mixer != NULL) + pulse_mixer_on_disconnect(po->mixer); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + break; + } +} + +static void +pulse_output_stream_write_cb(G_GNUC_UNUSED pa_stream *stream, size_t nbytes, + void *userdata) +{ + struct pulse_output *po = userdata; + + po->writable = nbytes; + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static bool +pulse_output_open(void *data, struct audio_format *audio_format, + GError **error_r) +{ + struct pulse_output *po = data; + pa_sample_spec ss; + int error; + + if (po->context != NULL) { + switch (pa_context_get_state(po->context)) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + /* the connection was closed meanwhile; delete + it, and pulse_output_wait_connection() will + reopen it */ + pulse_output_delete_context(po); + break; + + case PA_CONTEXT_READY: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } + } + + if (!pulse_output_wait_connection(po, error_r)) + return false; + + /* MPD doesn't support the other pulseaudio sample formats, so + we just force MPD to send us everything as 16 bit */ + audio_format->bits = 16; + + ss.format = PA_SAMPLE_S16NE; + ss.rate = audio_format->sample_rate; + ss.channels = audio_format->channels; + + pa_threaded_mainloop_lock(po->mainloop); + + /* create a stream .. */ + + po->stream = pa_stream_new(po->context, po->name, &ss, NULL); + if (po->stream == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_new() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + pa_stream_set_state_callback(po->stream, + pulse_output_stream_state_cb, po); + pa_stream_set_write_callback(po->stream, + pulse_output_stream_write_cb, po); + + /* .. and connect it (asynchronously) */ + + error = pa_stream_connect_playback(po->stream, po->sink, + NULL, 0, NULL, NULL); + if (error < 0) { + pa_stream_unref(po->stream); + po->stream = NULL; + + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_connect_playback() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + +#if !PA_CHECK_VERSION(0,9,11) + po->pause = false; +#endif + + return true; +} + +static void +pulse_output_close(void *data) +{ + struct pulse_output *po = data; + pa_operation *o; + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_stream_get_state(po->stream) == PA_STREAM_READY) { + o = pa_stream_drain(po->stream, + pulse_output_stream_success_cb, po); + if (o == NULL) { + g_warning("pa_stream_drain() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + } else + pulse_wait_for_operation(po->mainloop, o); + } + + pa_stream_disconnect(po->stream); + pa_stream_unref(po->stream); + po->stream = NULL; + + if (po->context != NULL && + pa_context_get_state(po->context) != PA_CONTEXT_READY) + pulse_output_delete_context(po); + + pa_threaded_mainloop_unlock(po->mainloop); +} + +/** + * Check if the stream is (already) connected, and waits for a signal + * if not. The mainloop must be locked before calling this function. + * + * @return the current stream state + */ +static pa_stream_state_t +pulse_output_check_stream(struct pulse_output *po) +{ + pa_stream_state_t state = pa_stream_get_state(po->stream); + + switch (state) { + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + case PA_STREAM_UNCONNECTED: + break; + + case PA_STREAM_CREATING: + pa_threaded_mainloop_wait(po->mainloop); + state = pa_stream_get_state(po->stream); + break; + } + + return state; +} + +/** + * Check if the stream is (already) connected, and waits if not. The + * mainloop must be locked before calling this function. + * + * @return true on success, false on error + */ +static bool +pulse_output_wait_stream(struct pulse_output *po, GError **error_r) +{ + pa_stream_state_t state = pa_stream_get_state(po->stream); + + switch (state) { + case PA_STREAM_READY: + return true; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + case PA_STREAM_UNCONNECTED: + g_set_error(error_r, pulse_output_quark(), 0, + "disconnected"); + return false; + + case PA_STREAM_CREATING: + break; + } + + do { + state = pulse_output_check_stream(po); + } while (state == PA_STREAM_CREATING); + + if (state != PA_STREAM_READY) { + g_set_error(error_r, pulse_output_quark(), 0, + "failed to connect the stream: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + return true; +} + +/** + * Determines whether the stream is paused. On libpulse older than + * 0.9.11, it uses a custom pause flag. + */ +static bool +pulse_output_stream_is_paused(struct pulse_output *po) +{ + assert(po->stream != NULL); + +#if !defined(PA_CHECK_VERSION) || !PA_CHECK_VERSION(0,9,11) + return po->pause; +#else + return pa_stream_is_corked(po->stream); +#endif +} + +/** + * Sets cork mode on the stream. + */ +static bool +pulse_output_stream_pause(struct pulse_output *po, bool pause, + GError **error_r) +{ + pa_operation *o; + + assert(po->stream != NULL); + + o = pa_stream_cork(po->stream, pause, + pulse_output_stream_success_cb, po); + if (o == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_cork() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + if (!pulse_wait_for_operation(po->mainloop, o)) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_cork() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + +#if !PA_CHECK_VERSION(0,9,11) + po->pause = pause; +#endif + return true; +} + +static size_t +pulse_output_play(void *data, const void *chunk, size_t size, GError **error_r) +{ + struct pulse_output *po = data; + int error; + + assert(po->stream != NULL); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already) connected */ + + if (!pulse_output_wait_stream(po, error_r)) { + pa_threaded_mainloop_unlock(po->mainloop); + return 0; + } + + assert(po->context != NULL); + + /* unpause if previously paused */ + + if (pulse_output_stream_is_paused(po) && + !pulse_output_stream_pause(po, false, error_r)) + return 0; + + /* wait until the server allows us to write */ + + while (po->writable == 0) { + pa_threaded_mainloop_wait(po->mainloop); + + if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { + pa_threaded_mainloop_unlock(po->mainloop); + g_set_error(error_r, pulse_output_quark(), 0, + "disconnected"); + return false; + } + } + + /* now write */ + + if (size > po->writable) + /* don't send more than possible */ + size = po->writable; + + po->writable -= size; + + error = pa_stream_write(po->stream, chunk, size, NULL, + 0, PA_SEEK_RELATIVE); + pa_threaded_mainloop_unlock(po->mainloop); + if (error < 0) { + g_set_error(error_r, pulse_output_quark(), error, + "%s", pa_strerror(error)); + return 0; + } + + return size; +} + +static void +pulse_output_cancel(void *data) +{ + struct pulse_output *po = data; + pa_operation *o; + + assert(po->stream != NULL); + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { + /* no need to flush when the stream isn't connected + yet */ + pa_threaded_mainloop_unlock(po->mainloop); + return; + } + + assert(po->context != NULL); + + o = pa_stream_flush(po->stream, pulse_output_stream_success_cb, po); + if (o == NULL) { + g_warning("pa_stream_flush() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return; + } + + pulse_wait_for_operation(po->mainloop, o); + pa_threaded_mainloop_unlock(po->mainloop); +} + +static bool +pulse_output_pause(void *data) +{ + struct pulse_output *po = data; + GError *error = NULL; + + assert(po->stream != NULL); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already/still) connected */ + + if (!pulse_output_wait_stream(po, &error)) { + pa_threaded_mainloop_unlock(po->mainloop); + g_warning("%s", error->message); + g_error_free(error); + return false; + } + + assert(po->context != NULL); + + /* cork the stream */ + + if (pulse_output_stream_is_paused(po)) { + /* already paused; due to a MPD API limitation, we + have to sleep a little bit here, to avoid hogging + the CPU */ + + g_usleep(50000); + } else if (!pulse_output_stream_pause(po, true, &error)) { + pa_threaded_mainloop_unlock(po->mainloop); + g_warning("%s", error->message); + g_error_free(error); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static bool +pulse_output_test_default_device(void) +{ + struct pulse_output *po; + bool success; + + po = pulse_output_init(NULL, NULL, NULL); + if (po == NULL) + return false; + + success = pulse_output_wait_connection(po, NULL); + pulse_output_finish(po); + + return success; +} + +const struct audio_output_plugin pulse_output_plugin = { + .name = "pulse", + + .test_default_device = pulse_output_test_default_device, + .init = pulse_output_init, + .finish = pulse_output_finish, + .enable = pulse_output_enable, + .disable = pulse_output_disable, + .open = pulse_output_open, + .play = pulse_output_play, + .cancel = pulse_output_cancel, + .pause = pulse_output_pause, + .close = pulse_output_close, + + .mixer_plugin = &pulse_mixer_plugin, +}; diff --git a/src/output/pulse_output_plugin.h b/src/output/pulse_output_plugin.h new file mode 100644 index 000000000..e6b37443f --- /dev/null +++ b/src/output/pulse_output_plugin.h @@ -0,0 +1,72 @@ +/* + * 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. + */ + +#ifndef MPD_PULSE_OUTPUT_PLUGIN_H +#define MPD_PULSE_OUTPUT_PLUGIN_H + +#include <stdbool.h> +#include <stddef.h> + +#include <glib.h> + +#include <pulse/version.h> + +#if !defined(PA_CHECK_VERSION) +/** + * This macro was implemented in libpulse 0.9.16. + */ +#define PA_CHECK_VERSION(a,b,c) false +#endif + +struct pa_operation; +struct pa_cvolume; + +struct pulse_output { + const char *name; + const char *server; + const char *sink; + + struct pulse_mixer *mixer; + + struct pa_threaded_mainloop *mainloop; + struct pa_context *context; + struct pa_stream *stream; + + size_t writable; + +#if !PA_CHECK_VERSION(0,9,11) + /** + * We need this variable because pa_stream_is_corked() wasn't + * added before 0.9.11. + */ + bool pause; +#endif +}; + +void +pulse_output_set_mixer(struct pulse_output *po, struct pulse_mixer *pm); + +void +pulse_output_clear_mixer(struct pulse_output *po, struct pulse_mixer *pm); + +bool +pulse_output_set_volume(struct pulse_output *po, + const struct pa_cvolume *volume, GError **error_r); + +#endif diff --git a/src/output/pulse_plugin.c b/src/output/pulse_plugin.c deleted file mode 100644 index ffc7abc8b..000000000 --- a/src/output/pulse_plugin.c +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 "mixer_list.h" - -#include <glib.h> -#include <pulse/simple.h> -#include <pulse/error.h> - -#define MPD_PULSE_NAME "mpd" - -struct pulse_data { - const char *name; - const char *server; - const char *sink; - - pa_simple *s; -}; - -/** - * The quark used for GError.domain. - */ -static inline GQuark -pulse_output_quark(void) -{ - return g_quark_from_static_string("pulse_output"); -} - -static struct pulse_data *pulse_new_data(void) -{ - struct pulse_data *ret; - - ret = g_new(struct pulse_data, 1); - - ret->server = NULL; - ret->sink = NULL; - - return ret; -} - -static void pulse_free_data(struct pulse_data *pd) -{ - g_free(pd); -} - -static void * -pulse_init(G_GNUC_UNUSED const struct audio_format *audio_format, - const struct config_param *param, G_GNUC_UNUSED GError **error) -{ - struct pulse_data *pd; - - pd = pulse_new_data(); - pd->name = config_get_block_string(param, "name", "mpd_pulse"); - pd->server = config_get_block_string(param, "server", NULL); - pd->sink = config_get_block_string(param, "sink", NULL); - - return pd; -} - -static void pulse_finish(void *data) -{ - struct pulse_data *pd = data; - - pulse_free_data(pd); -} - -static bool pulse_test_default_device(void) -{ - pa_simple *s; - pa_sample_spec ss; - int error; - - ss.format = PA_SAMPLE_S16NE; - ss.rate = 44100; - ss.channels = 2; - - s = pa_simple_new(NULL, MPD_PULSE_NAME, PA_STREAM_PLAYBACK, NULL, - MPD_PULSE_NAME, &ss, NULL, NULL, &error); - if (!s) { - g_message("Cannot connect to default PulseAudio server: %s\n", - pa_strerror(error)); - return false; - } - - pa_simple_free(s); - - return true; -} - -static bool -pulse_open(void *data, struct audio_format *audio_format, GError **error_r) -{ - struct pulse_data *pd = data; - pa_sample_spec ss; - int error; - - /* MPD doesn't support the other pulseaudio sample formats, so - we just force MPD to send us everything as 16 bit */ - audio_format->bits = 16; - - ss.format = PA_SAMPLE_S16NE; - ss.rate = audio_format->sample_rate; - ss.channels = audio_format->channels; - - pd->s = pa_simple_new(pd->server, MPD_PULSE_NAME, PA_STREAM_PLAYBACK, - pd->sink, pd->name, - &ss, NULL, NULL, - &error); - if (!pd->s) { - g_set_error(error_r, pulse_output_quark(), error, - "Cannot connect to PulseAudio server: %s", - pa_strerror(error)); - return false; - } - - return true; -} - -static void pulse_cancel(void *data) -{ - struct pulse_data *pd = data; - int error; - - if (pa_simple_flush(pd->s, &error) < 0) - g_warning("Flush failed in PulseAudio output \"%s\": %s\n", - pd->name, pa_strerror(error)); -} - -static void pulse_close(void *data) -{ - struct pulse_data *pd = data; - - pa_simple_drain(pd->s, NULL); - pa_simple_free(pd->s); -} - -static size_t -pulse_play(void *data, const void *chunk, size_t size, GError **error_r) -{ - struct pulse_data *pd = data; - int error; - - if (pa_simple_write(pd->s, chunk, size, &error) < 0) { - g_set_error(error_r, pulse_output_quark(), error, - "%s", pa_strerror(error)); - return 0; - } - - return size; -} - -const struct audio_output_plugin pulse_plugin = { - .name = "pulse", - .test_default_device = pulse_test_default_device, - .init = pulse_init, - .finish = pulse_finish, - .open = pulse_open, - .play = pulse_play, - .cancel = pulse_cancel, - .close = pulse_close, - .mixer_plugin = &pulse_mixer, -}; 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 <assert.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> + +#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/shout_plugin.c b/src/output/shout_plugin.c index 4412d26ff..da90efd2d 100644 --- a/src/output/shout_plugin.c +++ b/src/output/shout_plugin.c @@ -126,6 +126,13 @@ my_shout_init_driver(const struct audio_format *audio_format, struct block_param *block_param; int public; + if (audio_format == NULL || + !audio_format_fully_defined(audio_format)) { + g_set_error(error, shout_output_quark(), 0, + "Need full audio format specification"); + return NULL; + } + sd = new_shout_data(); if (shout_init_count == 0) @@ -191,8 +198,6 @@ my_shout_init_driver(const struct audio_format *audio_format, } } - check_block_param("format"); - encoding = config_get_block_string(param, "encoding", "ogg"); encoder_plugin = shout_encoder_plugin_get(encoding); if (encoder_plugin == NULL) { @@ -471,10 +476,10 @@ shout_tag_to_metadata(const struct tag *tag, char *dest, size_t size) for (unsigned i = 0; i < tag->num_items; i++) { switch (tag->items[i]->type) { - case TAG_ITEM_ARTIST: + case TAG_ARTIST: strncpy(artist, tag->items[i]->value, size); break; - case TAG_ITEM_TITLE: + case TAG_TITLE: strncpy(title, tag->items[i]->value, size); break; diff --git a/src/output_all.c b/src/output_all.c index 4b5ba3a6f..bdf0385bb 100644 --- a/src/output_all.c +++ b/src/output_all.c @@ -52,6 +52,11 @@ static struct music_buffer *g_music_buffer; */ static struct music_pipe *g_mp; +/** + * The "elapsed_time" stamp of the most recently finished chunk. + */ +static float audio_output_all_elapsed_time = -1.0; + unsigned int audio_output_count(void) { return num_audio_outputs; @@ -148,6 +153,25 @@ audio_output_all_finish(void) notify_deinit(&audio_output_client_notify); } +void +audio_output_all_enable_disable(void) +{ + for (unsigned i = 0; i < num_audio_outputs; i++) { + struct audio_output *ao = &audio_outputs[i]; + bool enabled; + + g_mutex_lock(ao->mutex); + enabled = ao->really_enabled; + g_mutex_unlock(ao->mutex); + + if (ao->enabled != enabled) { + if (ao->enabled) + audio_output_enable(ao); + else + audio_output_disable(ao); + } + } +} /** * Determine if all (active) outputs have finished the current @@ -156,10 +180,18 @@ audio_output_all_finish(void) static bool audio_output_all_finished(void) { - for (unsigned i = 0; i < num_audio_outputs; ++i) - if (audio_output_is_open(&audio_outputs[i]) && - !audio_output_command_is_finished(&audio_outputs[i])) + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = &audio_outputs[i]; + bool not_finished; + + g_mutex_lock(ao->mutex); + not_finished = audio_output_is_open(ao) && + !audio_output_command_is_finished(ao); + g_mutex_unlock(ao->mutex); + + if (not_finished) return false; + } return true; } @@ -237,8 +269,7 @@ audio_output_all_play(struct music_chunk *chunk) music_pipe_push(g_mp, chunk); for (i = 0; i < num_audio_outputs; ++i) - if (audio_output_is_open(&audio_outputs[i])) - audio_output_play(&audio_outputs[i]); + audio_output_play(&audio_outputs[i]); return true; } @@ -385,6 +416,8 @@ audio_output_all_check(void) this chunk */ return music_pipe_size(g_mp); + audio_output_all_elapsed_time = chunk->times; + is_tail = chunk->next == NULL; if (is_tail) /* this is the tail of the pipe - clear the @@ -412,10 +445,15 @@ audio_output_all_check(void) bool audio_output_all_wait(unsigned threshold) { - if (audio_output_all_check() < threshold) + player_lock(); + + if (audio_output_all_check() < threshold) { + player_unlock(); return true; + } - notify_wait(&pc.notify); + player_wait(); + player_unlock(); return audio_output_all_check() < threshold; } @@ -428,8 +466,7 @@ audio_output_all_pause(void) audio_output_all_update(); for (i = 0; i < num_audio_outputs; ++i) - if (audio_output_is_open(&audio_outputs[i])) - audio_output_pause(&audio_outputs[i]); + audio_output_pause(&audio_outputs[i]); audio_output_wait_all(); } @@ -441,10 +478,8 @@ audio_output_all_cancel(void) /* send the cancel() command to all audio outputs */ - for (i = 0; i < num_audio_outputs; ++i) { - if (audio_output_is_open(&audio_outputs[i])) - audio_output_cancel(&audio_outputs[i]); - } + for (i = 0; i < num_audio_outputs; ++i) + audio_output_cancel(&audio_outputs[i]); audio_output_wait_all(); @@ -452,6 +487,8 @@ audio_output_all_cancel(void) if (g_mp != NULL) music_pipe_clear(g_mp, g_music_buffer); + + audio_output_all_elapsed_time = -1.0; } void @@ -473,4 +510,12 @@ audio_output_all_close(void) g_music_buffer = NULL; audio_format_clear(&input_audio_format); + + audio_output_all_elapsed_time = -1.0; +} + +float +audio_output_all_get_elapsed_time(void) +{ + return audio_output_all_elapsed_time; } diff --git a/src/output_all.h b/src/output_all.h index 2a09514b2..91b6f3f5d 100644 --- a/src/output_all.h +++ b/src/output_all.h @@ -66,6 +66,13 @@ struct audio_output * audio_output_find(const char *name); /** + * Checks the "enabled" flag of all audio outputs, and if one has + * changed, commit the change. + */ +void +audio_output_all_enable_disable(void); + +/** * Opens all audio outputs which are not disabled. * * @param audio_format the preferred audio format, or NULL to reuse @@ -128,4 +135,12 @@ audio_output_all_pause(void); void audio_output_all_cancel(void); +/** + * Returns the "elapsed_time" stamp of the most recently finished + * chunk. A negative value is returned when no chunk has been + * finished yet. + */ +float +audio_output_all_get_elapsed_time(void); + #endif diff --git a/src/output_command.c b/src/output_command.c index 5da176dde..b47890043 100644 --- a/src/output_command.c +++ b/src/output_command.c @@ -29,8 +29,11 @@ #include "output_internal.h" #include "output_plugin.h" #include "mixer_control.h" +#include "player_control.h" #include "idle.h" +extern unsigned audio_output_state_version; + bool audio_output_enable_index(unsigned idx) { @@ -40,10 +43,16 @@ audio_output_enable_index(unsigned idx) return false; ao = audio_output_get(idx); + if (ao->enabled) + return true; ao->enabled = true; idle_add(IDLE_OUTPUT); + pc_update_audio(); + + ++audio_output_state_version; + return true; } @@ -57,6 +66,8 @@ audio_output_disable_index(unsigned idx) return false; ao = audio_output_get(idx); + if (!ao->enabled) + return true; ao->enabled = false; idle_add(IDLE_OUTPUT); @@ -67,5 +78,9 @@ audio_output_disable_index(unsigned idx) idle_add(IDLE_MIXER); } + pc_update_audio(); + + ++audio_output_state_version; + return true; } diff --git a/src/output_control.c b/src/output_control.c index 16c0dbb75..973baa463 100644 --- a/src/output_control.c +++ b/src/output_control.c @@ -23,6 +23,8 @@ #include "output_thread.h" #include "mixer_control.h" #include "mixer_plugin.h" +#include "filter_plugin.h" +#include "notify.h" #include <assert.h> #include <stdlib.h> @@ -38,8 +40,10 @@ struct notify audio_output_client_notify; static void ao_command_wait(struct audio_output *ao) { while (ao->command != AO_COMMAND_NONE) { - notify_signal(&ao->notify); + g_cond_signal(ao->cond); + g_mutex_unlock(ao->mutex); notify_wait(&audio_output_client_notify); + g_mutex_lock(ao->mutex); } } @@ -55,7 +59,46 @@ static void ao_command_async(struct audio_output *ao, { assert(ao->command == AO_COMMAND_NONE); ao->command = cmd; - notify_signal(&ao->notify); + g_cond_signal(ao->cond); +} + +void +audio_output_enable(struct audio_output *ao) +{ + if (ao->thread == NULL) { + if (ao->plugin->enable == NULL) { + /* don't bother to start the thread now if the + device doesn't even have a enable() method; + just assign the variable and we're done */ + ao->really_enabled = true; + return; + } + + audio_output_thread_start(ao); + } + + g_mutex_lock(ao->mutex); + ao_command(ao, AO_COMMAND_ENABLE); + g_mutex_unlock(ao->mutex); +} + +void +audio_output_disable(struct audio_output *ao) +{ + if (ao->thread == NULL) { + if (ao->plugin->disable == NULL) + ao->really_enabled = false; + else + /* if there's no thread yet, the device cannot + be enabled */ + assert(!ao->really_enabled); + + return; + } + + g_mutex_lock(ao->mutex); + ao_command(ao, AO_COMMAND_DISABLE); + g_mutex_unlock(ao->mutex); } static bool @@ -93,33 +136,47 @@ audio_output_open(struct audio_output *ao, ao->in_audio_format = *audio_format; ao->chunk = NULL; - if (!ao->config_audio_format) { - if (ao->open) - audio_output_close(ao); - - /* no audio format is configured: copy in->out, let - the output's open() method determine the effective - out_audio_format */ - ao->out_audio_format = ao->in_audio_format; - } - ao->pipe = mp; if (ao->thread == NULL) audio_output_thread_start(ao); + ao_command(ao, ao->open ? AO_COMMAND_REOPEN : AO_COMMAND_OPEN); open = ao->open; - if (!open) { - ao_command(ao, AO_COMMAND_OPEN); - open = ao->open; - } - if (open && ao->mixer != NULL) - mixer_open(ao->mixer); + if (open && ao->mixer != NULL) { + GError *error = NULL; + + if (!mixer_open(ao->mixer, &error)) { + g_warning("Failed to open mixer for '%s': %s", + ao->name, error->message); + g_error_free(error); + } + } return open; } +/** + * Same as audio_output_close(), but expects the lock to be held by + * the caller. + */ +static void +audio_output_close_locked(struct audio_output *ao) +{ + if (ao->mixer != NULL) + mixer_auto_close(ao->mixer); + + assert(!ao->open || ao->fail_timer == NULL); + + if (ao->open) + ao_command(ao, AO_COMMAND_CLOSE); + else if (ao->fail_timer != NULL) { + g_timer_destroy(ao->fail_timer); + ao->fail_timer = NULL; + } +} + bool audio_output_update(struct audio_output *ao, const struct audio_format *audio_format, @@ -127,23 +184,31 @@ audio_output_update(struct audio_output *ao, { assert(mp != NULL); - if (ao->enabled) { + g_mutex_lock(ao->mutex); + + if (ao->enabled && ao->really_enabled) { if (ao->fail_timer == NULL || - g_timer_elapsed(ao->fail_timer, NULL) > REOPEN_AFTER) - return audio_output_open(ao, audio_format, mp); + g_timer_elapsed(ao->fail_timer, NULL) > REOPEN_AFTER) { + bool success = audio_output_open(ao, audio_format, mp); + g_mutex_unlock(ao->mutex); + return success; + } } else if (audio_output_is_open(ao)) - audio_output_close(ao); + audio_output_close_locked(ao); + g_mutex_unlock(ao->mutex); return false; } void audio_output_play(struct audio_output *ao) { - if (!ao->open) - return; + g_mutex_lock(ao->mutex); + + if (audio_output_is_open(ao)) + g_cond_signal(ao->cond); - notify_signal(&ao->notify); + g_mutex_unlock(ao->mutex); } void audio_output_pause(struct audio_output *ao) @@ -154,27 +219,37 @@ void audio_output_pause(struct audio_output *ao) mixer_auto_close()) */ mixer_auto_close(ao->mixer); - ao_command_async(ao, AO_COMMAND_PAUSE); + g_mutex_lock(ao->mutex); + if (audio_output_is_open(ao)) + ao_command_async(ao, AO_COMMAND_PAUSE); + g_mutex_unlock(ao->mutex); } void audio_output_cancel(struct audio_output *ao) { - ao_command_async(ao, AO_COMMAND_CANCEL); + g_mutex_lock(ao->mutex); + if (audio_output_is_open(ao)) + ao_command_async(ao, AO_COMMAND_CANCEL); + g_mutex_unlock(ao->mutex); } void audio_output_close(struct audio_output *ao) { - assert(!ao->open || ao->fail_timer == NULL); - if (ao->mixer != NULL) mixer_auto_close(ao->mixer); + g_mutex_lock(ao->mutex); + + assert(!ao->open || ao->fail_timer == NULL); + if (ao->open) ao_command(ao, AO_COMMAND_CLOSE); else if (ao->fail_timer != NULL) { g_timer_destroy(ao->fail_timer); ao->fail_timer = NULL; } + + g_mutex_unlock(ao->mutex); } void audio_output_finish(struct audio_output *ao) @@ -184,7 +259,9 @@ void audio_output_finish(struct audio_output *ao) assert(ao->fail_timer == NULL); if (ao->thread != NULL) { + g_mutex_lock(ao->mutex); ao_command(ao, AO_COMMAND_KILL); + g_mutex_unlock(ao->mutex); g_thread_join(ao->thread); } @@ -193,6 +270,8 @@ void audio_output_finish(struct audio_output *ao) ao_plugin_finish(ao->plugin, ao->data); - notify_deinit(&ao->notify); + g_cond_free(ao->cond); g_mutex_free(ao->mutex); + + filter_free(ao->filter); } diff --git a/src/output_control.h b/src/output_control.h index ce3abe3f6..b2e48fa8d 100644 --- a/src/output_control.h +++ b/src/output_control.h @@ -38,7 +38,19 @@ audio_output_quark(void) bool audio_output_init(struct audio_output *ao, const struct config_param *param, - GError **error); + GError **error_r); + +/** + * Enables the device. + */ +void +audio_output_enable(struct audio_output *ao); + +/** + * Disables the device. + */ +void +audio_output_disable(struct audio_output *ao); /** * Opens or closes the device, depending on the "enabled" flag. diff --git a/src/output_init.c b/src/output_init.c index 927424324..a7272bfc2 100644 --- a/src/output_init.c +++ b/src/output_init.c @@ -23,9 +23,17 @@ #include "output_list.h" #include "audio_parser.h" #include "mixer_control.h" +#include "mixer_type.h" +#include "mixer_list.h" +#include "mixer/software_mixer_plugin.h" +#include "filter_plugin.h" +#include "filter_registry.h" +#include "filter/chain_filter_plugin.h" #include <glib.h> +#include <assert.h> + #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "output" @@ -56,46 +64,109 @@ audio_output_detect(GError **error) return NULL; } +/** + * Determines the mixer type which should be used for the specified + * configuration block. + * + * This handles the deprecated options mixer_type (global) and + * mixer_enabled, if the mixer_type setting is not configured. + */ +static enum mixer_type +audio_output_mixer_type(const struct config_param *param) +{ + /* read the local "mixer_type" setting */ + const char *p = config_get_block_string(param, "mixer_type", NULL); + if (p != NULL) + return mixer_type_parse(p); + + /* try the local "mixer_enabled" setting next (deprecated) */ + if (!config_get_block_bool(param, "mixer_enabled", true)) + return MIXER_TYPE_NONE; + + /* fall back to the global "mixer_type" setting (also + deprecated) */ + return mixer_type_parse(config_get_string("mixer_type", "hardware")); +} + +static struct mixer * +audio_output_load_mixer(void *ao, const struct config_param *param, + const struct mixer_plugin *plugin, + struct filter *filter_chain, + GError **error_r) +{ + struct mixer *mixer; + + switch (audio_output_mixer_type(param)) { + case MIXER_TYPE_NONE: + case MIXER_TYPE_UNKNOWN: + return NULL; + + case MIXER_TYPE_HARDWARE: + if (plugin == NULL) + return NULL; + + return mixer_new(plugin, ao, param, error_r); + + case MIXER_TYPE_SOFTWARE: + mixer = mixer_new(&software_mixer_plugin, NULL, NULL, NULL); + assert(mixer != NULL); + + filter_chain_append(filter_chain, + software_mixer_get_filter(mixer)); + return mixer; + } + + assert(false); + return NULL; +} + bool audio_output_init(struct audio_output *ao, const struct config_param *param, - GError **error) + GError **error_r) { - const char *format; const struct audio_output_plugin *plugin = NULL; + GError *error = NULL; if (param) { - const char *type = NULL; + const char *p; - type = config_get_block_string(param, AUDIO_OUTPUT_TYPE, NULL); - if (type == NULL) { - g_set_error(error, audio_output_quark(), 0, + p = config_get_block_string(param, AUDIO_OUTPUT_TYPE, NULL); + if (p == NULL) { + g_set_error(error_r, audio_output_quark(), 0, "Missing \"type\" configuration"); return false; } - plugin = audio_output_plugin_get(type); + plugin = audio_output_plugin_get(p); if (plugin == NULL) { - g_set_error(error, audio_output_quark(), 0, - "No such audio output plugin: %s", - type); + g_set_error(error_r, audio_output_quark(), 0, + "No such audio output plugin: %s", p); return false; } ao->name = config_get_block_string(param, AUDIO_OUTPUT_NAME, NULL); if (ao->name == NULL) { - g_set_error(error, audio_output_quark(), 0, + g_set_error(error_r, audio_output_quark(), 0, "Missing \"name\" configuration"); return false; } - format = config_get_block_string(param, AUDIO_OUTPUT_FORMAT, + p = config_get_block_string(param, AUDIO_OUTPUT_FORMAT, NULL); + if (p != NULL) { + bool success = + audio_format_parse(&ao->config_audio_format, + p, true, error_r); + if (!success) + return false; + } else + audio_format_clear(&ao->config_audio_format); } else { g_warning("No \"%s\" defined in config file\n", CONF_AUDIO_OUTPUT); - plugin = audio_output_detect(error); + plugin = audio_output_detect(error_r); if (plugin == NULL) return false; @@ -103,44 +174,50 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, plugin->name); ao->name = "default detected output"; - format = NULL; + + audio_format_clear(&ao->config_audio_format); } ao->plugin = plugin; ao->enabled = config_get_block_bool(param, "enabled", true); + ao->really_enabled = false; ao->open = false; ao->pause = false; ao->fail_timer = NULL; - pcm_convert_init(&ao->convert_state); + /* set up the filter chain */ - ao->config_audio_format = format != NULL; - if (ao->config_audio_format) { - bool ret; - - ret = audio_format_parse(&ao->out_audio_format, format, - error); - if (!ret) - return false; - } + ao->filter = filter_chain_new(); + assert(ao->filter != NULL); ao->thread = NULL; - notify_init(&ao->notify); ao->command = AO_COMMAND_NONE; ao->mutex = g_mutex_new(); + ao->cond = g_cond_new(); ao->data = ao_plugin_init(plugin, - ao->config_audio_format - ? &ao->out_audio_format : NULL, - param, error); + &ao->config_audio_format, + param, error_r); if (ao->data == NULL) return false; - if (plugin->mixer_plugin != NULL && - config_get_block_bool(param, "mixer_enabled", true)) - ao->mixer = mixer_new(plugin->mixer_plugin, param); - else - ao->mixer = NULL; + ao->mixer = audio_output_load_mixer(ao->data, param, + plugin->mixer_plugin, + ao->filter, &error); + if (ao->mixer == NULL && error != NULL) { + g_warning("Failed to initialize hardware mixer for '%s': %s", + ao->name, error->message); + g_error_free(error); + } + + /* the "convert" filter must be the last one in the chain */ + + ao->convert_filter = filter_new(&convert_filter_plugin, NULL, NULL); + assert(ao->convert_filter != NULL); + + filter_chain_append(ao->filter, ao->convert_filter); + + /* done */ return true; } diff --git a/src/output_internal.h b/src/output_internal.h index 72596c1c3..6b81bbc78 100644 --- a/src/output_internal.h +++ b/src/output_internal.h @@ -21,14 +21,23 @@ #define MPD_OUTPUT_INTERNAL_H #include "audio_format.h" -#include "pcm_convert.h" -#include "notify.h" + +#include <glib.h> #include <time.h> enum audio_output_command { AO_COMMAND_NONE = 0, + AO_COMMAND_ENABLE, + AO_COMMAND_DISABLE, AO_COMMAND_OPEN, + + /** + * This command is invoked when the input audio format + * changes. + */ + AO_COMMAND_REOPEN, + AO_COMMAND_CLOSE, AO_COMMAND_PAUSE, AO_COMMAND_CANCEL, @@ -60,15 +69,15 @@ struct audio_output { struct mixer *mixer; /** - * This flag is true, when the audio_format of this device is - * configured in mpd.conf. + * Has the user enabled this device? */ - bool config_audio_format; + bool enabled; /** - * Has the user enabled this device? + * Is this device actually enabled, i.e. the "enable" method + * has succeeded? */ - bool enabled; + bool really_enabled; /** * Is the device (already) open and functional? @@ -94,6 +103,11 @@ struct audio_output { GTimer *fail_timer; /** + * The configured audio format. + */ + struct audio_format config_audio_format; + + /** * The audio_format in which audio data is received from the * player thread (which in turn receives it from the decoder). */ @@ -107,18 +121,25 @@ struct audio_output { */ struct audio_format out_audio_format; - struct pcm_convert_state convert_state; + /** + * The filter object of this audio output. This is an + * instance of chain_filter_plugin. + */ + struct filter *filter; /** - * The thread handle, or NULL if the output thread isn't - * running. + * The convert_filter_plugin instance of this audio output. + * It is the last item in the filter chain, and is responsible + * for converting the input data into the appropriate format + * for this audio output. */ - GThread *thread; + struct filter *convert_filter; /** - * Notify object for the thread. + * The thread handle, or NULL if the output thread isn't + * running. */ - struct notify notify; + GThread *thread; /** * The next command to be performed by the output thread. @@ -136,6 +157,12 @@ struct audio_output { GMutex *mutex; /** + * This condition object wakes up the output thread after + * #command has been set. + */ + GCond *cond; + + /** * The #music_chunk which is currently being played. All * chunks before this one may be returned to the * #music_buffer, because they are not going to be used by diff --git a/src/output_list.c b/src/output_list.c index 81de16649..fcb796751 100644 --- a/src/output_list.c +++ b/src/output_list.c @@ -28,12 +28,14 @@ extern const struct audio_output_plugin pipe_output_plugin; extern const struct audio_output_plugin alsaPlugin; extern const struct audio_output_plugin ao_output_plugin; extern const struct audio_output_plugin oss_output_plugin; +extern const struct audio_output_plugin openal_output_plugin; extern const struct audio_output_plugin osxPlugin; extern const struct audio_output_plugin solaris_output_plugin; -extern const struct audio_output_plugin pulse_plugin; +extern const struct audio_output_plugin pulse_output_plugin; extern const struct audio_output_plugin mvp_output_plugin; -extern const struct audio_output_plugin jackPlugin; +extern const struct audio_output_plugin jack_output_plugin; 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 @@ -55,6 +57,9 @@ const struct audio_output_plugin *audio_output_plugins[] = { #ifdef HAVE_OSS &oss_output_plugin, #endif +#ifdef HAVE_OPENAL + &openal_output_plugin, +#endif #ifdef HAVE_OSX &osxPlugin, #endif @@ -62,17 +67,20 @@ const struct audio_output_plugin *audio_output_plugins[] = { &solaris_output_plugin, #endif #ifdef HAVE_PULSE - &pulse_plugin, + &pulse_output_plugin, #endif #ifdef HAVE_MVP &mvp_output_plugin, #endif #ifdef HAVE_JACK - &jackPlugin, + &jack_output_plugin, #endif #ifdef ENABLE_HTTPD_OUTPUT &httpd_output_plugin, #endif +#ifdef ENABLE_RECORDER_OUTPUT + &recorder_output_plugin, +#endif NULL }; diff --git a/src/output_plugin.h b/src/output_plugin.h index 13dba0d0b..ee7f7c73d 100644 --- a/src/output_plugin.h +++ b/src/output_plugin.h @@ -67,6 +67,24 @@ struct audio_output_plugin { void (*finish)(void *data); /** + * Enable the device. This may allocate resources, preparing + * for the device to be opened. Enabling a device cannot + * fail: if an error occurs during that, it should be reported + * by the open() method. + * + * @param error_r location to store the error occuring, or + * NULL to ignore errors + * @return true on success, false on error + */ + bool (*enable)(void *data, GError **error_r); + + /** + * Disables the device. It is closed before this method is + * called. + */ + void (*disable)(void *data); + + /** * Really open the device. * * @param audio_format the audio format in which data is going @@ -99,6 +117,11 @@ struct audio_output_plugin { GError **error); /** + * Wait until the device has finished playing. + */ + void (*drain)(void *data); + + /** * Try to cancel data which may still be in the device's * buffers. */ @@ -150,6 +173,22 @@ ao_plugin_finish(const struct audio_output_plugin *plugin, void *data) } static inline bool +ao_plugin_enable(const struct audio_output_plugin *plugin, void *data, + GError **error_r) +{ + return plugin->enable != NULL + ? plugin->enable(data, error_r) + : true; +} + +static inline void +ao_plugin_disable(const struct audio_output_plugin *plugin, void *data) +{ + if (plugin->disable != NULL) + plugin->disable(data); +} + +static inline bool ao_plugin_open(const struct audio_output_plugin *plugin, void *data, struct audio_format *audio_format, GError **error) @@ -180,6 +219,13 @@ ao_plugin_play(const struct audio_output_plugin *plugin, } static inline void +ao_plugin_drain(const struct audio_output_plugin *plugin, void *data) +{ + if (plugin->drain != NULL) + plugin->drain(data); +} + +static inline void ao_plugin_cancel(const struct audio_output_plugin *plugin, void *data) { if (plugin->cancel != NULL) diff --git a/src/output_state.c b/src/output_state.c index c7e6c8579..be135f6d0 100644 --- a/src/output_state.c +++ b/src/output_state.c @@ -34,8 +34,10 @@ #define AUDIO_DEVICE_STATE "audio_device_state:" +unsigned audio_output_state_version; + void -saveAudioDevicesState(FILE *fp) +audio_output_state_save(FILE *fp) { unsigned n = audio_output_count(); @@ -49,35 +51,40 @@ saveAudioDevicesState(FILE *fp) } } -void -readAudioDevicesState(FILE *fp) +bool +audio_output_state_read(const char *line) { - char buffer[1024]; + long value; + char *endptr; + const char *name; + struct audio_output *ao; - while (fgets(buffer, sizeof(buffer), fp)) { - char *c, *name; - struct audio_output *ao; + if (!g_str_has_prefix(line, AUDIO_DEVICE_STATE)) + return false; - g_strchomp(buffer); + line += sizeof(AUDIO_DEVICE_STATE) - 1; - if (!g_str_has_prefix(buffer, AUDIO_DEVICE_STATE)) - continue; + value = strtol(line, &endptr, 10); + if (*endptr != ':' || (value != 0 && value != 1)) + return false; - c = strchr(buffer, ':'); - if (!c || !(++c)) - goto errline; + if (value != 0) + /* state is "enabled": no-op */ + return true; - name = strchr(c, ':'); - if (!name || !(++name)) - goto errline; + name = endptr + 1; + ao = audio_output_find(name); + if (ao == NULL) { + g_debug("Ignoring device state for '%s'", name); + return true; + } - ao = audio_output_find(name); - if (ao != NULL && atoi(c) == 0) - ao->enabled = false; + ao->enabled = false; + return true; +} - continue; -errline: - /* nonfatal */ - g_warning("invalid line in state_file: %s\n", buffer); - } +unsigned +audio_output_state_get_version(void) +{ + return audio_output_state_version; } diff --git a/src/output_state.h b/src/output_state.h index 8592574ab..3b865f5fe 100644 --- a/src/output_state.h +++ b/src/output_state.h @@ -25,12 +25,21 @@ #ifndef OUTPUT_STATE_H #define OUTPUT_STATE_H +#include <stdbool.h> #include <stdio.h> -void -readAudioDevicesState(FILE *fp); +bool +audio_output_state_read(const char *line); void -saveAudioDevicesState(FILE *fp); +audio_output_state_save(FILE *fp); + +/** + * Generates a version number for the current state of the audio + * outputs. This is used by timer_save_state_file() to determine + * whether the state has changed and the state file should be saved. + */ +unsigned +audio_output_state_get_version(void); #endif diff --git a/src/output_thread.c b/src/output_thread.c index 770b377e8..da1caef3f 100644 --- a/src/output_thread.c +++ b/src/output_thread.c @@ -23,6 +23,8 @@ #include "chunk.h" #include "pipe.h" #include "player_control.h" +#include "filter_plugin.h" +#include "filter/convert_filter_plugin.h" #include <glib.h> @@ -37,27 +39,212 @@ static void ao_command_finished(struct audio_output *ao) { assert(ao->command != AO_COMMAND_NONE); ao->command = AO_COMMAND_NONE; + + g_mutex_unlock(ao->mutex); notify_signal(&audio_output_client_notify); + g_mutex_lock(ao->mutex); +} + +static bool +ao_enable(struct audio_output *ao) +{ + GError *error = NULL; + bool success; + + if (ao->really_enabled) + return true; + + g_mutex_unlock(ao->mutex); + success = ao_plugin_enable(ao->plugin, ao->data, &error); + g_mutex_lock(ao->mutex); + if (!success) { + g_warning("Failed to enable \"%s\" [%s]: %s\n", + ao->name, ao->plugin->name, error->message); + g_error_free(error); + return false; + } + + ao->really_enabled = true; + return true; +} + +static void +ao_close(struct audio_output *ao, bool drain); + +static void +ao_disable(struct audio_output *ao) +{ + if (ao->open) + ao_close(ao, false); + + if (ao->really_enabled) { + ao->really_enabled = false; + + g_mutex_unlock(ao->mutex); + ao_plugin_disable(ao->plugin, ao->data); + g_mutex_lock(ao->mutex); + } +} + +static void +ao_open(struct audio_output *ao) +{ + bool success; + GError *error = NULL; + const struct audio_format *filter_audio_format; + + assert(!ao->open); + assert(ao->fail_timer == NULL); + assert(ao->pipe != NULL); + assert(ao->chunk == NULL); + + /* enable the device (just in case the last enable has failed) */ + + if (!ao_enable(ao)) + /* still no luck */ + return; + + /* open the filter */ + + filter_audio_format = filter_open(ao->filter, &ao->in_audio_format, + &error); + if (filter_audio_format == NULL) { + g_warning("Failed to open filter for \"%s\" [%s]: %s", + ao->name, ao->plugin->name, error->message); + g_error_free(error); + + ao->fail_timer = g_timer_new(); + return; + } + + ao->out_audio_format = *filter_audio_format; + audio_format_mask_apply(&ao->out_audio_format, + &ao->config_audio_format); + + g_mutex_unlock(ao->mutex); + success = ao_plugin_open(ao->plugin, ao->data, + &ao->out_audio_format, + &error); + g_mutex_lock(ao->mutex); + + assert(!ao->open); + + if (!success) { + g_warning("Failed to open \"%s\" [%s]: %s", + ao->name, ao->plugin->name, error->message); + g_error_free(error); + + filter_close(ao->filter); + ao->fail_timer = g_timer_new(); + return; + } + + convert_filter_set(ao->convert_filter, &ao->out_audio_format); + + ao->open = true; + + g_debug("opened plugin=%s name=\"%s\" " + "audio_format=%u:%u:%u:%u", + ao->plugin->name, ao->name, + ao->out_audio_format.sample_rate, + ao->out_audio_format.bits, + ao->out_audio_format.channels, + ao->out_audio_format.reverse_endian); + + if (!audio_format_equals(&ao->in_audio_format, + &ao->out_audio_format)) + g_debug("converting from %u:%u:%u:%u", + ao->in_audio_format.sample_rate, + ao->in_audio_format.bits, + ao->in_audio_format.channels, + ao->in_audio_format.reverse_endian); } static void -ao_close(struct audio_output *ao) +ao_close(struct audio_output *ao, bool drain) { assert(ao->open); ao->pipe = NULL; - g_mutex_lock(ao->mutex); ao->chunk = NULL; ao->open = false; + g_mutex_unlock(ao->mutex); + if (drain) + ao_plugin_drain(ao->plugin, ao->data); + else + ao_plugin_cancel(ao->plugin, ao->data); + ao_plugin_close(ao->plugin, ao->data); - pcm_convert_deinit(&ao->convert_state); + filter_close(ao->filter); + + g_mutex_lock(ao->mutex); g_debug("closed plugin=%s name=\"%s\"", ao->plugin->name, ao->name); } +static void +ao_reopen_filter(struct audio_output *ao) +{ + const struct audio_format *filter_audio_format; + GError *error = NULL; + + filter_close(ao->filter); + filter_audio_format = filter_open(ao->filter, &ao->in_audio_format, + &error); + if (filter_audio_format == NULL) { + g_warning("Failed to open filter for \"%s\" [%s]: %s", + ao->name, ao->plugin->name, error->message); + g_error_free(error); + + /* this is a little code duplication fro ao_close(), + but we cannot call this function because we must + not call filter_close(ao->filter) again */ + + ao->pipe = NULL; + + ao->chunk = NULL; + ao->open = false; + ao->fail_timer = g_timer_new(); + + g_mutex_unlock(ao->mutex); + ao_plugin_close(ao->plugin, ao->data); + g_mutex_lock(ao->mutex); + + return; + } + + convert_filter_set(ao->convert_filter, &ao->out_audio_format); +} + +static void +ao_reopen(struct audio_output *ao) +{ + if (!audio_format_fully_defined(&ao->config_audio_format)) { + if (ao->open) { + const struct music_pipe *mp = ao->pipe; + ao_close(ao, true); + ao->pipe = mp; + } + + /* no audio format is configured: copy in->out, let + the output's open() method determine the effective + out_audio_format */ + ao->out_audio_format = ao->in_audio_format; + audio_format_mask_apply(&ao->out_audio_format, + &ao->config_audio_format); + } + + if (ao->open) + /* the audio format has changed, and all filters have + to be reconfigured */ + ao_reopen_filter(ao); + else + ao_open(ao); +} + static bool ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk) { @@ -65,43 +252,49 @@ ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk) size_t size = chunk->length; GError *error = NULL; + assert(ao != NULL); + assert(ao->filter != NULL); assert(!music_chunk_is_empty(chunk)); assert(music_chunk_check_format(chunk, &ao->in_audio_format)); assert(size % audio_format_frame_size(&ao->in_audio_format) == 0); - if (chunk->tag != NULL) + if (chunk->tag != NULL) { + g_mutex_unlock(ao->mutex); ao_plugin_send_tag(ao->plugin, ao->data, chunk->tag); + g_mutex_lock(ao->mutex); + } if (size == 0) return true; - if (!audio_format_equals(&ao->in_audio_format, - &ao->out_audio_format)) { - data = pcm_convert(&ao->convert_state, - &ao->in_audio_format, data, size, - &ao->out_audio_format, &size); - - /* under certain circumstances, pcm_convert() may - return an empty buffer - this condition should be - investigated further, but for now, do this check as - a workaround: */ - if (data == NULL) - return true; + data = filter_filter(ao->filter, data, size, &size, &error); + if (data == NULL) { + g_warning("\"%s\" [%s] failed to filter: %s", + ao->name, ao->plugin->name, error->message); + g_error_free(error); + + ao_close(ao, false); + + /* don't automatically reopen this device for 10 + seconds */ + ao->fail_timer = g_timer_new(); + return false; } while (size > 0 && ao->command == AO_COMMAND_NONE) { size_t nbytes; + g_mutex_unlock(ao->mutex); nbytes = ao_plugin_play(ao->plugin, ao->data, data, size, &error); + g_mutex_lock(ao->mutex); if (nbytes == 0) { /* play()==0 means failure */ g_warning("\"%s\" [%s] failed to play: %s", ao->name, ao->plugin->name, error->message); g_error_free(error); - ao_plugin_cancel(ao->plugin, ao->data); - ao_close(ao); + ao_close(ao, false); /* don't automatically reopen this device for 10 seconds */ @@ -119,32 +312,41 @@ ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk) return true; } -static void ao_play(struct audio_output *ao) +/** + * Plays all remaining chunks, until the tail of the pipe has been + * reached (and no more chunks are queued), or until a command is + * received. + * + * @return true if at least one chunk has been available, false if the + * tail of the pipe was already reached + */ +static bool +ao_play(struct audio_output *ao) { bool success; const struct music_chunk *chunk; assert(ao->pipe != NULL); - g_mutex_lock(ao->mutex); chunk = ao->chunk; if (chunk != NULL) /* continue the previous play() call */ chunk = chunk->next; - else + else { chunk = music_pipe_peek(ao->pipe); + if (chunk == NULL) + /* no chunk available */ + return false; + } + ao->chunk_finished = false; while (chunk != NULL && ao->command == AO_COMMAND_NONE) { assert(!ao->chunk_finished); ao->chunk = chunk; - g_mutex_unlock(ao->mutex); success = ao_play_chunk(ao, chunk); - - g_mutex_lock(ao->mutex); - if (!success) { assert(ao->chunk == NULL); break; @@ -155,23 +357,32 @@ static void ao_play(struct audio_output *ao) } ao->chunk_finished = true; + g_mutex_unlock(ao->mutex); + player_lock_signal(); + g_mutex_lock(ao->mutex); - notify_signal(&pc.notify); + return true; } static void ao_pause(struct audio_output *ao) { bool ret; + g_mutex_unlock(ao->mutex); ao_plugin_cancel(ao->plugin, ao->data); + g_mutex_lock(ao->mutex); + ao->pause = true; ao_command_finished(ao); do { + g_mutex_unlock(ao->mutex); ret = ao_plugin_pause(ao->plugin, ao->data); + g_mutex_lock(ao->mutex); + if (!ret) { - ao_close(ao); + ao_close(ao, false); break; } } while (ao->command == AO_COMMAND_NONE); @@ -182,56 +393,31 @@ static void ao_pause(struct audio_output *ao) static gpointer audio_output_task(gpointer arg) { struct audio_output *ao = arg; - bool ret; - GError *error; + + g_mutex_lock(ao->mutex); while (1) { switch (ao->command) { case AO_COMMAND_NONE: break; - case AO_COMMAND_OPEN: - assert(!ao->open); - assert(ao->fail_timer == NULL); - assert(ao->pipe != NULL); - assert(ao->chunk == NULL); + case AO_COMMAND_ENABLE: + ao_enable(ao); + ao_command_finished(ao); + break; - error = NULL; - ret = ao_plugin_open(ao->plugin, ao->data, - &ao->out_audio_format, - &error); - - assert(!ao->open); - if (ret) { - pcm_convert_init(&ao->convert_state); - - g_mutex_lock(ao->mutex); - ao->open = true; - g_mutex_unlock(ao->mutex); - - g_debug("opened plugin=%s name=\"%s\" " - "audio_format=%u:%u:%u", - ao->plugin->name, - ao->name, - ao->out_audio_format.sample_rate, - ao->out_audio_format.bits, - ao->out_audio_format.channels); - - if (!audio_format_equals(&ao->in_audio_format, - &ao->out_audio_format)) - g_debug("converting from %u:%u:%u", - ao->in_audio_format.sample_rate, - ao->in_audio_format.bits, - ao->in_audio_format.channels); - } else { - g_warning("Failed to open \"%s\" [%s]: %s", - ao->name, ao->plugin->name, - error->message); - g_error_free(error); - - ao->fail_timer = g_timer_new(); - } + case AO_COMMAND_DISABLE: + ao_disable(ao); + ao_command_finished(ao); + break; + + case AO_COMMAND_OPEN: + ao_open(ao); + ao_command_finished(ao); + break; + case AO_COMMAND_REOPEN: + ao_reopen(ao); ao_command_finished(ao); break; @@ -239,11 +425,7 @@ static gpointer audio_output_task(gpointer arg) assert(ao->open); assert(ao->pipe != NULL); - ao->pipe = NULL; - ao->chunk = NULL; - - ao_plugin_cancel(ao->plugin, ao->data); - ao_close(ao); + ao_close(ao, false); ao_command_finished(ao); break; @@ -273,19 +455,23 @@ static gpointer audio_output_task(gpointer arg) /* the player thread will now clear our music pipe - wait for a notify, to give it some time */ - notify_wait(&ao->notify); + g_cond_wait(ao->cond, ao->mutex); continue; case AO_COMMAND_KILL: ao->chunk = NULL; ao_command_finished(ao); + g_mutex_unlock(ao->mutex); return NULL; } - if (ao->open) - ao_play(ao); + if (ao->open && ao_play(ao)) + /* don't wait for an event if there are more + chunks in the pipe */ + continue; - notify_wait(&ao->notify); + if (ao->command == AO_COMMAND_NONE) + g_cond_wait(ao->cond, ao->mutex); } } diff --git a/src/pcm_byteswap.c b/src/pcm_byteswap.c new file mode 100644 index 000000000..1c1caeeca --- /dev/null +++ b/src/pcm_byteswap.c @@ -0,0 +1,69 @@ +/* + * 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 "pcm_byteswap.h" +#include "pcm_buffer.h" + +#include <glib.h> + +#include <assert.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "pcm" + +static inline uint16_t swab16(uint16_t x) +{ + return (x << 8) | (x >> 8); +} + +const int16_t *pcm_byteswap_16(struct pcm_buffer *buffer, + const int16_t *src, size_t len) +{ + unsigned i; + int16_t *buf = pcm_buffer_get(buffer, len); + + assert(buf != NULL); + + for (i = 0; i < len / 2; i++) + buf[i] = swab16(src[i]); + + return buf; +} + +static inline uint32_t swab32(uint32_t x) +{ + return (x << 24) | + ((x & 0xff00) << 8) | + ((x & 0xff0000) >> 8) | + (x >> 24); +} + +const int32_t *pcm_byteswap_32(struct pcm_buffer *buffer, + const int32_t *src, size_t len) +{ + unsigned i; + int32_t *buf = pcm_buffer_get(buffer, len); + + assert(buf != NULL); + + for (i = 0; i < len / 4; i++) + buf[i] = swab32(src[i]); + + return buf; +} diff --git a/src/pcm_byteswap.h b/src/pcm_byteswap.h new file mode 100644 index 000000000..e1196d9b2 --- /dev/null +++ b/src/pcm_byteswap.h @@ -0,0 +1,50 @@ +/* + * 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. + */ + +#ifndef MPD_PCM_BYTESWAP_H +#define MPD_PCM_BYTESWAP_H + +#include <stdint.h> +#include <stddef.h> + +struct pcm_buffer; + +/** + * Changes the endianness of 16 bit PCM data. + * + * @param buffer the destination pcm_buffer object + * @param src the source PCM buffer + * @param src_size the number of bytes in #src + * @return the destination buffer + */ +const int16_t *pcm_byteswap_16(struct pcm_buffer *buffer, + const int16_t *src, size_t len); + +/** + * Changes the endianness of 32-bit (or 24-bit) PCM data. + * + * @param buffer the destination pcm_buffer object + * @param src the source PCM buffer + * @param src_size the number of bytes in #src + * @return the destination buffer + */ +const int32_t *pcm_byteswap_32(struct pcm_buffer *buffer, + const int32_t *src, size_t len); + +#endif diff --git a/src/pcm_channels.c b/src/pcm_channels.c index 969ddff32..38445f958 100644 --- a/src/pcm_channels.c +++ b/src/pcm_channels.c @@ -20,13 +20,8 @@ #include "pcm_channels.h" #include "pcm_buffer.h" -#include <glib.h> - #include <assert.h> -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "pcm" - static void pcm_convert_channels_16_1_to_2(int16_t *dest, const int16_t *src, unsigned num_frames) @@ -75,8 +70,8 @@ pcm_convert_channels_16_n_to_2(int16_t *dest, const int16_t * pcm_convert_channels_16(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int16_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int16_t *src, size_t src_size, size_t *dest_size_r) { unsigned num_frames = src_size / src_channels / sizeof(*src); @@ -92,11 +87,8 @@ pcm_convert_channels_16(struct pcm_buffer *buffer, else if (dest_channels == 2) pcm_convert_channels_16_n_to_2(dest, src_channels, src, num_frames); - else { - g_warning("conversion %u->%u channels is not supported", - src_channels, dest_channels); + else return NULL; - } return dest; } @@ -149,8 +141,8 @@ pcm_convert_channels_24_n_to_2(int32_t *dest, const int32_t * pcm_convert_channels_24(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int32_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int32_t *src, size_t src_size, size_t *dest_size_r) { unsigned num_frames = src_size / src_channels / sizeof(*src); @@ -166,11 +158,8 @@ pcm_convert_channels_24(struct pcm_buffer *buffer, else if (dest_channels == 2) pcm_convert_channels_24_n_to_2(dest, src_channels, src, num_frames); - else { - g_warning("conversion %u->%u channels is not supported", - src_channels, dest_channels); + else return NULL; - } return dest; } @@ -218,8 +207,8 @@ pcm_convert_channels_32_n_to_2(int32_t *dest, const int32_t * pcm_convert_channels_32(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int32_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int32_t *src, size_t src_size, size_t *dest_size_r) { unsigned num_frames = src_size / src_channels / sizeof(*src); @@ -235,11 +224,8 @@ pcm_convert_channels_32(struct pcm_buffer *buffer, else if (dest_channels == 2) pcm_convert_channels_32_n_to_2(dest, src_channels, src, num_frames); - else { - g_warning("conversion %u->%u channels is not supported", - src_channels, dest_channels); + else return NULL; - } return dest; } diff --git a/src/pcm_channels.h b/src/pcm_channels.h index accf4b07b..13b7ffbf3 100644 --- a/src/pcm_channels.h +++ b/src/pcm_channels.h @@ -38,8 +38,8 @@ struct pcm_buffer; */ const int16_t * pcm_convert_channels_16(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int16_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int16_t *src, size_t src_size, size_t *dest_size_r); /** @@ -56,8 +56,8 @@ pcm_convert_channels_16(struct pcm_buffer *buffer, */ const int32_t * pcm_convert_channels_24(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int32_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int32_t *src, size_t src_size, size_t *dest_size_r); /** @@ -73,8 +73,8 @@ pcm_convert_channels_24(struct pcm_buffer *buffer, */ const int32_t * pcm_convert_channels_32(struct pcm_buffer *buffer, - int8_t dest_channels, - int8_t src_channels, const int32_t *src, + uint8_t dest_channels, + uint8_t src_channels, const int32_t *src, size_t src_size, size_t *dest_size_r); #endif diff --git a/src/pcm_convert.c b/src/pcm_convert.c index ebb4adff5..4879dc0ab 100644 --- a/src/pcm_convert.c +++ b/src/pcm_convert.c @@ -20,6 +20,7 @@ #include "pcm_convert.h" #include "pcm_channels.h" #include "pcm_format.h" +#include "pcm_byteswap.h" #include "audio_format.h" #include <assert.h> @@ -39,6 +40,7 @@ void pcm_convert_init(struct pcm_convert_state *state) pcm_buffer_init(&state->format_buffer); pcm_buffer_init(&state->channels_buffer); + pcm_buffer_init(&state->byteswap_buffer); } void pcm_convert_deinit(struct pcm_convert_state *state) @@ -47,14 +49,15 @@ void pcm_convert_deinit(struct pcm_convert_state *state) pcm_buffer_deinit(&state->format_buffer); pcm_buffer_deinit(&state->channels_buffer); + pcm_buffer_deinit(&state->byteswap_buffer); } static const int16_t * pcm_convert_16(struct pcm_convert_state *state, const struct audio_format *src_format, const void *src_buffer, size_t src_size, - const struct audio_format *dest_format, - size_t *dest_size_r) + const struct audio_format *dest_format, size_t *dest_size_r, + GError **error_r) { const int16_t *buf; size_t len; @@ -64,24 +67,42 @@ pcm_convert_16(struct pcm_convert_state *state, buf = pcm_convert_to_16(&state->format_buffer, &state->dither, src_format->bits, src_buffer, src_size, &len); - if (!buf) - g_error("pcm_convert_to_16() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to 16 bit is not implemented", + src_format->bits); + return NULL; + } if (src_format->channels != dest_format->channels) { buf = pcm_convert_channels_16(&state->channels_buffer, dest_format->channels, src_format->channels, buf, len, &len); - if (!buf) - g_error("pcm_convert_channels_16() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to %u channels " + "is not implemented", + src_format->channels, + dest_format->channels); + return NULL; + } } - if (src_format->sample_rate != dest_format->sample_rate) + if (src_format->sample_rate != dest_format->sample_rate) { buf = pcm_resample_16(&state->resample, dest_format->channels, src_format->sample_rate, buf, len, - dest_format->sample_rate, - &len); + dest_format->sample_rate, &len, + error_r); + if (buf == NULL) + return NULL; + } + + if (dest_format->reverse_endian) { + buf = pcm_byteswap_16(&state->byteswap_buffer, buf, len); + assert(buf != NULL); + } *dest_size_r = len; return buf; @@ -91,8 +112,8 @@ static const int32_t * pcm_convert_24(struct pcm_convert_state *state, const struct audio_format *src_format, const void *src_buffer, size_t src_size, - const struct audio_format *dest_format, - size_t *dest_size_r) + const struct audio_format *dest_format, size_t *dest_size_r, + GError **error_r) { const int32_t *buf; size_t len; @@ -101,24 +122,42 @@ pcm_convert_24(struct pcm_convert_state *state, buf = pcm_convert_to_24(&state->format_buffer, src_format->bits, src_buffer, src_size, &len); - if (!buf) - g_error("pcm_convert_to_24() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to 24 bit is not implemented", + src_format->bits); + return NULL; + } if (src_format->channels != dest_format->channels) { buf = pcm_convert_channels_24(&state->channels_buffer, dest_format->channels, src_format->channels, buf, len, &len); - if (!buf) - g_error("pcm_convert_channels_24() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to %u channels " + "is not implemented", + src_format->channels, + dest_format->channels); + return NULL; + } } - if (src_format->sample_rate != dest_format->sample_rate) + if (src_format->sample_rate != dest_format->sample_rate) { buf = pcm_resample_24(&state->resample, dest_format->channels, src_format->sample_rate, buf, len, - dest_format->sample_rate, - &len); + dest_format->sample_rate, &len, + error_r); + if (buf == NULL) + return NULL; + } + + if (dest_format->reverse_endian) { + buf = pcm_byteswap_32(&state->byteswap_buffer, buf, len); + assert(buf != NULL); + } *dest_size_r = len; return buf; @@ -128,8 +167,8 @@ static const int32_t * pcm_convert_32(struct pcm_convert_state *state, const struct audio_format *src_format, const void *src_buffer, size_t src_size, - const struct audio_format *dest_format, - size_t *dest_size_r) + const struct audio_format *dest_format, size_t *dest_size_r, + GError **error_r) { const int32_t *buf; size_t len; @@ -138,24 +177,42 @@ pcm_convert_32(struct pcm_convert_state *state, buf = pcm_convert_to_32(&state->format_buffer, src_format->bits, src_buffer, src_size, &len); - if (!buf) - g_error("pcm_convert_to_32() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to 24 bit is not implemented", + src_format->bits); + return NULL; + } if (src_format->channels != dest_format->channels) { buf = pcm_convert_channels_32(&state->channels_buffer, dest_format->channels, src_format->channels, buf, len, &len); - if (!buf) - g_error("pcm_convert_channels_32() failed"); + if (buf == NULL) { + g_set_error(error_r, pcm_convert_quark(), 0, + "Conversion from %u to %u channels " + "is not implemented", + src_format->channels, + dest_format->channels); + return NULL; + } } - if (src_format->sample_rate != dest_format->sample_rate) + if (src_format->sample_rate != dest_format->sample_rate) { buf = pcm_resample_32(&state->resample, dest_format->channels, src_format->sample_rate, buf, len, - dest_format->sample_rate, - &len); + dest_format->sample_rate, &len, + error_r); + if (buf == NULL) + return buf; + } + + if (dest_format->reverse_endian) { + buf = pcm_byteswap_32(&state->byteswap_buffer, buf, len); + assert(buf != NULL); + } *dest_size_r = len; return buf; @@ -166,26 +223,32 @@ pcm_convert(struct pcm_convert_state *state, const struct audio_format *src_format, const void *src, size_t src_size, const struct audio_format *dest_format, - size_t *dest_size_r) + size_t *dest_size_r, + GError **error_r) { switch (dest_format->bits) { case 16: return pcm_convert_16(state, src_format, src, src_size, - dest_format, dest_size_r); + dest_format, dest_size_r, + error_r); case 24: return pcm_convert_24(state, src_format, src, src_size, - dest_format, dest_size_r); + dest_format, dest_size_r, + error_r); case 32: return pcm_convert_32(state, src_format, src, src_size, - dest_format, dest_size_r); + dest_format, dest_size_r, + error_r); default: - g_error("cannot convert to %u bit\n", dest_format->bits); + g_set_error(error_r, pcm_convert_quark(), 0, + "PCM conversion to %u bit is not implemented", + dest_format->bits); return NULL; } } diff --git a/src/pcm_convert.h b/src/pcm_convert.h index be08ad8a8..7ef0782df 100644 --- a/src/pcm_convert.h +++ b/src/pcm_convert.h @@ -41,8 +41,17 @@ struct pcm_convert_state { /** the buffer for converting the channel count */ struct pcm_buffer channels_buffer; + + /** the buffer for swapping the byte order */ + struct pcm_buffer byteswap_buffer; }; +static inline GQuark +pcm_convert_quark(void) +{ + return g_quark_from_static_string("pcm_convert"); +} + /** * Initializes a pcm_convert_state object. */ @@ -63,13 +72,16 @@ void pcm_convert_deinit(struct pcm_convert_state *state); * @param src_size the size of #src in bytes * @param dest_format the requested destination audio format * @param dest_size_r returns the number of bytes of the destination buffer - * @return the destination buffer + * @param error_r location to store the error occuring, or NULL to + * ignore errors + * @return the destination buffer, or NULL on error */ const void * pcm_convert(struct pcm_convert_state *state, const struct audio_format *src_format, const void *src, size_t src_size, const struct audio_format *dest_format, - size_t *dest_size_r); + size_t *dest_size_r, + GError **error_r); #endif diff --git a/src/pcm_format.c b/src/pcm_format.c index 0e686e17c..64e5167b5 100644 --- a/src/pcm_format.c +++ b/src/pcm_format.c @@ -21,8 +21,6 @@ #include "pcm_dither.h" #include "pcm_buffer.h" -#include <glib.h> - static void pcm_convert_8_to_16(int16_t *out, const int8_t *in, unsigned num_samples) @@ -93,7 +91,6 @@ pcm_convert_to_16(struct pcm_buffer *buffer, struct pcm_dither *dither, return dest; } - g_warning("only 8 or 16 bits are supported for conversion!\n"); return NULL; } @@ -168,7 +165,6 @@ pcm_convert_to_24(struct pcm_buffer *buffer, return dest; } - g_warning("only 8 or 24 bits are supported for conversion!\n"); return NULL; } @@ -243,6 +239,5 @@ pcm_convert_to_32(struct pcm_buffer *buffer, return src; } - g_warning("only 8 or 32 bits are supported for conversion!\n"); return NULL; } diff --git a/src/pcm_resample.c b/src/pcm_resample.c index d1360d02a..f09c65a32 100644 --- a/src/pcm_resample.c +++ b/src/pcm_resample.c @@ -62,16 +62,18 @@ void pcm_resample_deinit(struct pcm_resample_state *state) const int16_t * pcm_resample_16(struct pcm_resample_state *state, uint8_t channels, - unsigned src_rate, - const int16_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r) + unsigned src_rate, const int16_t *src_buffer, size_t src_size, + unsigned dest_rate, size_t *dest_size_r, + GError **error_r) { #ifdef HAVE_LIBSAMPLERATE if (pcm_resample_lsr_enabled()) return pcm_resample_lsr_16(state, channels, src_rate, src_buffer, src_size, - dest_rate, dest_size_r); + dest_rate, dest_size_r, + error_r); +#else + (void)error_r; #endif return pcm_resample_fallback_16(state, channels, @@ -82,16 +84,18 @@ pcm_resample_16(struct pcm_resample_state *state, const int32_t * pcm_resample_32(struct pcm_resample_state *state, uint8_t channels, - unsigned src_rate, - const int32_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r) + unsigned src_rate, const int32_t *src_buffer, size_t src_size, + unsigned dest_rate, size_t *dest_size_r, + GError **error_r) { #ifdef HAVE_LIBSAMPLERATE if (pcm_resample_lsr_enabled()) return pcm_resample_lsr_32(state, channels, src_rate, src_buffer, src_size, - dest_rate, dest_size_r); + dest_rate, dest_size_r, + error_r); +#else + (void)error_r; #endif return pcm_resample_fallback_32(state, channels, diff --git a/src/pcm_resample.h b/src/pcm_resample.h index 44720f7b2..9d03bbfbf 100644 --- a/src/pcm_resample.h +++ b/src/pcm_resample.h @@ -48,7 +48,7 @@ struct pcm_resample_state { uint8_t channels; } prev; - bool error; + int error; #endif struct pcm_buffer buffer; @@ -82,8 +82,8 @@ pcm_resample_16(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int16_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r); + unsigned dest_rate, size_t *dest_size_r, + GError **error_r); /** * Resamples 32 bit PCM data. @@ -102,8 +102,8 @@ pcm_resample_32(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int32_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r); + unsigned dest_rate, size_t *dest_size_r, + GError **error_r); /** * Resamples 24 bit PCM data. @@ -122,14 +122,14 @@ pcm_resample_24(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int32_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r) + unsigned dest_rate, size_t *dest_size_r, + GError **error_r) { /* reuse the 32 bit code - the resampler code doesn't care if the upper 8 bits are actually used */ return pcm_resample_32(state, channels, src_rate, src_buffer, src_size, - dest_rate, dest_size_r); + dest_rate, dest_size_r, error_r); } #endif diff --git a/src/pcm_resample_fallback.c b/src/pcm_resample_fallback.c index 36af51ad0..9a403ddc7 100644 --- a/src/pcm_resample_fallback.c +++ b/src/pcm_resample_fallback.c @@ -20,7 +20,6 @@ #include "pcm_resample_internal.h" #include <assert.h> -#include <glib.h> void pcm_resample_fallback_deinit(struct pcm_resample_state *state) @@ -74,8 +73,7 @@ const int32_t * pcm_resample_fallback_32(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, - const int32_t *src_buffer, - G_GNUC_UNUSED size_t src_size, + const int32_t *src_buffer, size_t src_size, unsigned dest_rate, size_t *dest_size_r) { diff --git a/src/pcm_resample_internal.h b/src/pcm_resample_internal.h index a10ba08cd..74363a590 100644 --- a/src/pcm_resample_internal.h +++ b/src/pcm_resample_internal.h @@ -40,8 +40,8 @@ pcm_resample_lsr_16(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int16_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r); + unsigned dest_rate, size_t *dest_size_r, + GError **error_r); const int32_t * pcm_resample_lsr_32(struct pcm_resample_state *state, @@ -49,8 +49,8 @@ pcm_resample_lsr_32(struct pcm_resample_state *state, unsigned src_rate, const int32_t *src_buffer, G_GNUC_UNUSED size_t src_size, - unsigned dest_rate, - size_t *dest_size_r); + unsigned dest_rate, size_t *dest_size_r, + GError **error_r); #endif diff --git a/src/pcm_resample_libsamplerate.c b/src/pcm_resample_libsamplerate.c index 6d019e892..66a1c3193 100644 --- a/src/pcm_resample_libsamplerate.c +++ b/src/pcm_resample_libsamplerate.c @@ -30,6 +30,12 @@ #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "pcm" +static inline GQuark +libsamplerate_quark(void) +{ + return g_quark_from_static_string("libsamplerate"); +} + void pcm_resample_lsr_deinit(struct pcm_resample_state *state) { @@ -77,9 +83,10 @@ out: return convalgo; } -static void +static bool pcm_resample_set(struct pcm_resample_state *state, - uint8_t channels, unsigned src_rate, unsigned dest_rate) + uint8_t channels, unsigned src_rate, unsigned dest_rate, + GError **error_r) { static int convalgo = -1; int error; @@ -92,9 +99,9 @@ pcm_resample_set(struct pcm_resample_state *state, if (channels == state->prev.channels && src_rate == state->prev.src_rate && dest_rate == state->prev.dest_rate) - return; + return true; - state->error = false; + state->error = 0; state->prev.channels = channels; state->prev.src_rate = src_rate; state->prev.dest_rate = dest_rate; @@ -104,16 +111,18 @@ pcm_resample_set(struct pcm_resample_state *state, state->state = src_new(convalgo, channels, &error); if (!state->state) { - g_warning("cannot create new libsamplerate state: %s", - src_strerror(error)); - state->error = true; - return; + g_set_error(error_r, libsamplerate_quark(), state->error, + "libsamplerate initialization has failed: %s", + src_strerror(error)); + return false; } data->src_ratio = (double)dest_rate / (double)src_rate; g_debug("setting samplerate conversion ratio to %.2lf", data->src_ratio); src_set_ratio(state->state, data->src_ratio); + + return true; } const int16_t * @@ -121,9 +130,10 @@ pcm_resample_lsr_16(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int16_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r) + unsigned dest_rate, size_t *dest_size_r, + GError **error_r) { + bool success; SRC_DATA *data = &state->data; size_t data_in_size; size_t data_out_size; @@ -132,11 +142,18 @@ pcm_resample_lsr_16(struct pcm_resample_state *state, assert((src_size % (sizeof(*src_buffer) * channels)) == 0); - pcm_resample_set(state, channels, src_rate, dest_rate); + success = pcm_resample_set(state, channels, src_rate, dest_rate, + error_r); + if (!success) + return NULL; /* there was an error previously, and nothing has changed */ - if (state->error) + if (state->error) { + g_set_error(error_r, libsamplerate_quark(), state->error, + "libsamplerate has failed: %s", + src_strerror(state->error)); return NULL; + } data->input_frames = src_size / sizeof(*src_buffer) / channels; data_in_size = data->input_frames * sizeof(float) * channels; @@ -151,9 +168,10 @@ pcm_resample_lsr_16(struct pcm_resample_state *state, error = src_process(state->state, data); if (error) { - g_warning("error processing samples with libsamplerate: %s", - src_strerror(error)); - state->error = true; + g_set_error(error_r, libsamplerate_quark(), error, + "libsamplerate has failed: %s", + src_strerror(error)); + state->error = error; return NULL; } @@ -191,9 +209,10 @@ pcm_resample_lsr_32(struct pcm_resample_state *state, uint8_t channels, unsigned src_rate, const int32_t *src_buffer, size_t src_size, - unsigned dest_rate, - size_t *dest_size_r) + unsigned dest_rate, size_t *dest_size_r, + GError **error_r) { + bool success; SRC_DATA *data = &state->data; size_t data_in_size; size_t data_out_size; @@ -202,11 +221,18 @@ pcm_resample_lsr_32(struct pcm_resample_state *state, assert((src_size % (sizeof(*src_buffer) * channels)) == 0); - pcm_resample_set(state, channels, src_rate, dest_rate); + success = pcm_resample_set(state, channels, src_rate, dest_rate, + error_r); + if (!success) + return NULL; /* there was an error previously, and nothing has changed */ - if (state->error) + if (state->error) { + g_set_error(error_r, libsamplerate_quark(), state->error, + "libsamplerate has failed: %s", + src_strerror(state->error)); return NULL; + } data->input_frames = src_size / sizeof(*src_buffer) / channels; data_in_size = data->input_frames * sizeof(float) * channels; @@ -221,9 +247,10 @@ pcm_resample_lsr_32(struct pcm_resample_state *state, error = src_process(state->state, data); if (error) { - g_warning("error processing samples with libsamplerate: %s", - src_strerror(error)); - state->error = true; + g_set_error(error_r, libsamplerate_quark(), error, + "libsamplerate has failed: %s", + src_strerror(error)); + state->error = error; return NULL; } diff --git a/src/permission.c b/src/permission.c index 7df4e27fc..a65941771 100644 --- a/src/permission.c +++ b/src/permission.c @@ -111,7 +111,7 @@ void initPermissions(void) permission_default = parsePermissions(param->value); } -int getPermissionFromPassword(char *password, unsigned *permission) +int getPermissionFromPassword(char const* password, unsigned* permission) { bool found; gpointer key, value; diff --git a/src/permission.h b/src/permission.h index bad26aa3c..2a5d999ce 100644 --- a/src/permission.h +++ b/src/permission.h @@ -27,7 +27,7 @@ #define PERMISSION_ADMIN 8 -int getPermissionFromPassword(char *password, unsigned *permission); +int getPermissionFromPassword(char const* password, unsigned* permission); void finishPermissions(void); diff --git a/src/player_control.c b/src/player_control.c index ac4b006dd..aa018ceda 100644 --- a/src/player_control.c +++ b/src/player_control.c @@ -18,6 +18,7 @@ */ #include "player_control.h" +#include "decoder_control.h" #include "path.h" #include "log.h" #include "tag.h" @@ -35,17 +36,28 @@ void pc_init(unsigned buffer_chunks, unsigned int buffered_before_play) { pc.buffer_chunks = buffer_chunks; pc.buffered_before_play = buffered_before_play; - notify_init(&pc.notify); + + pc.mutex = g_mutex_new(); + pc.cond = g_cond_new(); + pc.command = PLAYER_COMMAND_NONE; pc.error = PLAYER_ERROR_NOERROR; pc.state = PLAYER_STATE_STOP; pc.cross_fade_seconds = 0; - pc.software_volume = PCM_VOLUME_1; } void pc_deinit(void) { - notify_deinit(&pc.notify); + g_cond_free(pc.cond); + g_mutex_free(pc.mutex); +} + +void +player_wait_decoder(void) +{ + /* during this function, the decoder lock is held, because + we're waiting for the decoder thread */ + g_cond_wait(pc.cond, dc.mutex); } void @@ -57,27 +69,45 @@ pc_song_deleted(const struct song *song) } } -static void player_command(enum player_command cmd) +static void +player_command_wait_locked(void) +{ + while (pc.command != PLAYER_COMMAND_NONE) { + player_signal(); + g_cond_wait(main_cond, pc.mutex); + } +} + +static void +player_command_locked(enum player_command cmd) { assert(pc.command == PLAYER_COMMAND_NONE); pc.command = cmd; - while (pc.command != PLAYER_COMMAND_NONE) { - notify_signal(&pc.notify); - notify_wait(&main_notify); - } + player_command_wait_locked(); +} + +static void +player_command(enum player_command cmd) +{ + player_lock(); + player_command_locked(cmd); + player_unlock(); } void -playerPlay(struct song *song) +pc_play(struct song *song) { assert(song != NULL); if (pc.state != PLAYER_STATE_STOP) player_command(PLAYER_COMMAND_STOP); - pc.next_song = song; - player_command(PLAYER_COMMAND_PLAY); + assert(pc.next_song == NULL); + + pc_enqueue_song(song); + + assert(pc.next_song == NULL); idle_add(IDLE_PLAYER); } @@ -85,16 +115,26 @@ playerPlay(struct song *song) void pc_cancel(void) { player_command(PLAYER_COMMAND_CANCEL); + assert(pc.next_song == NULL); } -void playerWait(void) +void +pc_stop(void) { player_command(PLAYER_COMMAND_CLOSE_AUDIO); + assert(pc.next_song == NULL); idle_add(IDLE_PLAYER); } -void playerKill(void) +void +pc_update_audio(void) +{ + player_command(PLAYER_COMMAND_UPDATE_AUDIO); +} + +void +pc_kill(void) { assert(pc.thread != NULL); @@ -105,7 +145,8 @@ void playerKill(void) idle_add(IDLE_PLAYER); } -void playerPause(void) +void +pc_pause(void) { if (pc.state != PLAYER_STATE_STOP) { player_command(PLAYER_COMMAND_PAUSE); @@ -113,7 +154,8 @@ void playerPause(void) } } -void playerSetPause(int pause_flag) +void +pc_set_pause(bool pause_flag) { switch (pc.state) { case PLAYER_STATE_STOP: @@ -121,41 +163,46 @@ void playerSetPause(int pause_flag) case PLAYER_STATE_PLAY: if (pause_flag) - playerPause(); + pc_pause(); break; + case PLAYER_STATE_PAUSE: if (!pause_flag) - playerPause(); + pc_pause(); break; } } -int getPlayerElapsedTime(void) +void +pc_get_status(struct player_status *status) { - return (int)(pc.elapsed_time + 0.5); -} + player_command(PLAYER_COMMAND_REFRESH); -unsigned long getPlayerBitRate(void) -{ - return pc.bit_rate; -} + status->state = pc.state; -int getPlayerTotalTime(void) -{ - return (int)(pc.total_time + 0.5); + if (pc.state != PLAYER_STATE_STOP) { + status->bit_rate = pc.bit_rate; + status->audio_format = pc.audio_format; + status->total_time = pc.total_time; + status->elapsed_time = pc.elapsed_time; + } } -enum player_state getPlayerState(void) +enum player_state +pc_get_state(void) { return pc.state; } -void clearPlayerError(void) +void +pc_clear_error(void) { - pc.error = 0; + pc.error = PLAYER_ERROR_NOERROR; + pc.errored_song = NULL; } -enum player_error getPlayerError(void) +enum player_error +pc_get_error(void) { return pc.error; } @@ -166,52 +213,47 @@ pc_errored_song_uri(void) return song_get_uri(pc.errored_song); } -char *getPlayerErrorStr(void) +char * +pc_get_error_message(void) { - /* static OK here, only one user in main task */ - static char error[MPD_PATH_MAX + 64]; /* still too much */ - static const size_t errorlen = sizeof(error); + char *error; char *uri; - *error = '\0'; /* likely */ - switch (pc.error) { case PLAYER_ERROR_NOERROR: - break; + return NULL; case PLAYER_ERROR_FILENOTFOUND: uri = pc_errored_song_uri(); - snprintf(error, errorlen, - "file \"%s\" does not exist or is inaccessible", uri); + error = g_strdup_printf("file \"%s\" does not exist or is inaccessible", uri); g_free(uri); - break; + return error; case PLAYER_ERROR_FILE: uri = pc_errored_song_uri(); - snprintf(error, errorlen, "problems decoding \"%s\"", uri); + error = g_strdup_printf("problems decoding \"%s\"", uri); g_free(uri); - break; + return error; case PLAYER_ERROR_AUDIO: - strcpy(error, "problems opening audio device"); - break; + return g_strdup("problems opening audio device"); case PLAYER_ERROR_SYSTEM: - strcpy(error, "system error occured"); - break; + return g_strdup("system error occured"); case PLAYER_ERROR_UNKTYPE: uri = pc_errored_song_uri(); - snprintf(error, errorlen, - "file type of \"%s\" is unknown", uri); + error = g_strdup_printf("file type of \"%s\" is unknown", uri); g_free(uri); - break; + return error; } - return *error ? error : NULL; + + assert(false); + return NULL; } void -queueSong(struct song *song) +pc_enqueue_song(struct song *song) { assert(song != NULL); assert(pc.next_song == NULL); @@ -239,31 +281,24 @@ pc_seek(struct song *song, float seek_time) return true; } -float getPlayerCrossFade(void) +float +pc_get_cross_fade(void) { return pc.cross_fade_seconds; } -void setPlayerCrossFade(float crossFadeInSeconds) +void +pc_set_cross_fade(float cross_fade_seconds) { - if (crossFadeInSeconds < 0) - crossFadeInSeconds = 0; - pc.cross_fade_seconds = crossFadeInSeconds; + if (cross_fade_seconds < 0) + cross_fade_seconds = 0; + pc.cross_fade_seconds = cross_fade_seconds; idle_add(IDLE_OPTIONS); } -void setPlayerSoftwareVolume(int volume) -{ - if (volume > PCM_VOLUME_1) - volume = PCM_VOLUME_1; - else if (volume < 0) - volume = 0; - - pc.software_volume = volume; -} - -double getPlayerTotalPlayTime(void) +double +pc_get_total_play_time(void) { return pc.total_play_time; } diff --git a/src/player_control.h b/src/player_control.h index b1f7481cd..449e98aa5 100644 --- a/src/player_control.h +++ b/src/player_control.h @@ -35,11 +35,16 @@ enum player_command { PLAYER_COMMAND_NONE = 0, PLAYER_COMMAND_EXIT, PLAYER_COMMAND_STOP, - PLAYER_COMMAND_PLAY, PLAYER_COMMAND_PAUSE, PLAYER_COMMAND_SEEK, PLAYER_COMMAND_CLOSE_AUDIO, + /** + * At least one audio_output.enabled flag has been modified; + * commit those changes to the output threads. + */ + PLAYER_COMMAND_UPDATE_AUDIO, + /** player_control.next_song has been updated */ PLAYER_COMMAND_QUEUE, @@ -49,6 +54,12 @@ enum player_command { * stop */ PLAYER_COMMAND_CANCEL, + + /** + * Refresh status information in the #player_control struct, + * e.g. elapsed_time. + */ + PLAYER_COMMAND_REFRESH, }; enum player_error { @@ -60,6 +71,14 @@ enum player_error { PLAYER_ERROR_FILENOTFOUND, }; +struct player_status { + enum player_state state; + uint16_t bit_rate; + struct audio_format audio_format; + float total_time; + float elapsed_time; +}; + struct player_control { unsigned buffer_chunks; @@ -69,10 +88,19 @@ struct player_control { thread isn't running */ GThread *thread; - struct notify notify; - volatile enum player_command command; - volatile enum player_state state; - volatile enum player_error error; + /** + * This lock protects #command, #state, #error. + */ + GMutex *mutex; + + /** + * Trigger this object after you have modified #command. + */ + GCond *cond; + + enum player_command command; + enum player_state state; + enum player_error error; uint16_t bit_rate; struct audio_format audio_format; float total_time; @@ -81,7 +109,6 @@ struct player_control { struct song *errored_song; volatile double seek_where; float cross_fade_seconds; - uint16_t software_volume; double total_play_time; }; @@ -92,6 +119,67 @@ void pc_init(unsigned buffer_chunks, unsigned buffered_before_play); void pc_deinit(void); /** + * Locks the #player_control object. + */ +static inline void +player_lock(void) +{ + g_mutex_lock(pc.mutex); +} + +/** + * Unlocks the #player_control object. + */ +static inline void +player_unlock(void) +{ + g_mutex_unlock(pc.mutex); +} + +/** + * Waits for a signal on the #player_control object. This function is + * only valid in the player thread. The object must be locked prior + * to calling this function. + */ +static inline void +player_wait(void) +{ + g_cond_wait(pc.cond, pc.mutex); +} + +/** + * Waits for a signal on the #player_control object. This function is + * only valid in the player thread. The #decoder_control object must + * be locked prior to calling this function. + * + * Note the small difference to the player_wait() function! + */ +void +player_wait_decoder(void); + +/** + * Signals the #player_control object. The object should be locked + * prior to calling this function. + */ +static inline void +player_signal(void) +{ + g_cond_signal(pc.cond); +} + +/** + * Signals the #player_control object. The object is temporarily + * locked by this function. + */ +static inline void +player_lock_signal(void) +{ + player_lock(); + player_signal(); + player_unlock(); +} + +/** * Call this function when the specified song pointer is about to be * invalidated. This makes sure that player_control.errored_song does * not point to an invalid pointer. @@ -100,37 +188,50 @@ void pc_song_deleted(const struct song *song); void -playerPlay(struct song *song); +pc_play(struct song *song); /** * see PLAYER_COMMAND_CANCEL */ void pc_cancel(void); -void playerSetPause(int pause_flag); - -void playerPause(void); +void +pc_set_pause(bool pause_flag); -void playerKill(void); +void +pc_pause(void); -int getPlayerTotalTime(void); +void +pc_kill(void); -int getPlayerElapsedTime(void); +void +pc_get_status(struct player_status *status); -unsigned long getPlayerBitRate(void); +enum player_state +pc_get_state(void); -enum player_state getPlayerState(void); +void +pc_clear_error(void); -void clearPlayerError(void); +/** + * Returns the human-readable message describing the last error during + * playback, NULL if no error occurred. The caller has to free the + * returned string. + */ +char * +pc_get_error_message(void); -char *getPlayerErrorStr(void); +enum player_error +pc_get_error(void); -enum player_error getPlayerError(void); +void +pc_stop(void); -void playerWait(void); +void +pc_update_audio(void); void -queueSong(struct song *song); +pc_enqueue_song(struct song *song); /** * Makes the player thread seek the specified song to a position. @@ -141,20 +242,13 @@ queueSong(struct song *song); bool pc_seek(struct song *song, float seek_time); -void setPlayerCrossFade(float crossFadeInSeconds); - -float getPlayerCrossFade(void); - -void setPlayerSoftwareVolume(int volume); - -double getPlayerTotalPlayTime(void); +void +pc_set_cross_fade(float cross_fade_seconds); -static inline const struct audio_format * -player_get_audio_format(void) -{ - return &pc.audio_format; -} +float +pc_get_cross_fade(void); -void playerInit(void); +double +pc_get_total_play_time(void); #endif diff --git a/src/player_thread.c b/src/player_thread.c index 7fc55d3d1..3e822e280 100644 --- a/src/player_thread.c +++ b/src/player_thread.c @@ -95,29 +95,41 @@ struct player { struct audio_format play_audio_format; /** - * Coefficient for converting a PCM buffer size into a time - * span. + * The time stamp of the chunk most recently sent to the + * output thread. This attribute is only used if + * audio_output_all_get_elapsed_time() didn't return a usable + * value; the output thread can estimate the elapsed time more + * precisly. */ - double size_to_time; + float elapsed_time; }; static struct music_buffer *player_buffer; -static void player_command_finished(void) +static void player_command_finished_locked(void) { assert(pc.command != PLAYER_COMMAND_NONE); pc.command = PLAYER_COMMAND_NONE; - notify_signal(&main_notify); + g_cond_signal(main_cond); +} + +static void player_command_finished(void) +{ + player_lock(); + player_command_finished_locked(); + player_unlock(); } /** * Stop the decoder and clears (and frees) its music pipe. + * + * Player lock is not held. */ static void player_dc_stop(struct player *player) { - dc_stop(&pc.notify); + dc_stop(); if (dc.pipe != NULL) { /* clear and free the decoder pipe */ @@ -135,17 +147,23 @@ player_dc_stop(struct player *player) * After the decoder has been started asynchronously, wait for the * "START" command to finish. The decoder may not be initialized yet, * i.e. there is no audio_format information yet. + * + * The player lock is not held. */ static bool player_wait_for_decoder(struct player *player) { - dc_command_wait(&pc.notify); + dc_command_wait(); - if (decoder_has_failed()) { - assert(dc.next_song == NULL || dc.next_song->url != NULL); + if (decoder_lock_has_failed()) { + assert(dc.next_song == NULL || dc.next_song->uri != NULL); + + player_lock(); pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_FILE; pc.next_song = NULL; + player_unlock(); + player->queued = false; return false; } @@ -157,8 +175,8 @@ player_wait_for_decoder(struct player *player) player->song = pc.next_song; pc.next_song = NULL; - pc.elapsed_time = 0; player->queued = false; + player->elapsed_time = 0.0; /* set the "starting" flag, which will be cleared by player_check_decoder_startup() */ @@ -174,15 +192,21 @@ player_wait_for_decoder(struct player *player) * The decoder has acknowledged the "START" command (see * player_wait_for_decoder()). This function checks if the decoder * initialization has completed yet. + * + * The player lock is not held. */ static bool player_check_decoder_startup(struct player *player) { assert(player->decoder_starting); + decoder_lock(); + if (decoder_has_failed()) { /* the decoder failed */ - assert(dc.next_song == NULL || dc.next_song->url != NULL); + assert(dc.next_song == NULL || dc.next_song->uri != NULL); + + decoder_unlock(); pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_FILE; @@ -191,6 +215,8 @@ player_check_decoder_startup(struct player *player) } else if (!decoder_is_starting()) { /* the decoder is ready and ok */ + decoder_unlock(); + if (audio_format_defined(&player->play_audio_format) && !audio_output_all_wait(1)) /* the output devices havn't finished playing @@ -200,8 +226,6 @@ player_check_decoder_startup(struct player *player) pc.total_time = dc.total_time; pc.audio_format = dc.in_audio_format; player->play_audio_format = dc.out_audio_format; - player->size_to_time = - audioFormatSizeToTime(&dc.out_audio_format); player->decoder_starting = false; if (!player->paused && @@ -212,7 +236,7 @@ player_check_decoder_startup(struct player *player) "while playing \"%s\"", uri); g_free(uri); - assert(dc.next_song == NULL || dc.next_song->url != NULL); + assert(dc.next_song == NULL || dc.next_song->uri != NULL); pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_AUDIO; @@ -227,7 +251,8 @@ player_check_decoder_startup(struct player *player) } else { /* the decoder is not yet ready; wait some more */ - notify_wait(&pc.notify); + player_wait_decoder(); + decoder_unlock(); return true; } @@ -237,6 +262,8 @@ player_check_decoder_startup(struct player *player) * Sends a chunk of silence to the audio outputs. This is called when * there is not enough decoded data in the pipe yet, to prevent * underruns in the hardware buffers. + * + * The player lock is not held. */ static bool player_send_silence(struct player *player) @@ -273,6 +300,8 @@ player_send_silence(struct player *player) /** * This is the handler for the #PLAYER_COMMAND_SEEK command. + * + * The player lock is not held. */ static bool player_seek_decoder(struct player *player) { @@ -324,14 +353,15 @@ static bool player_seek_decoder(struct player *player) if (where < 0.0) where = 0.0; - ret = dc_seek(&pc.notify, where); + ret = dc_seek(where); if (!ret) { /* decoder failure */ player_command_finished(); return false; } - pc.elapsed_time = where; + player->elapsed_time = where; + player_command_finished(); player->xfade = XFADE_UNKNOWN; @@ -344,53 +374,73 @@ static bool player_seek_decoder(struct player *player) return true; } +/** + * Player lock must be held before calling. + */ static void player_process_command(struct player *player) { switch (pc.command) { case PLAYER_COMMAND_NONE: - case PLAYER_COMMAND_PLAY: case PLAYER_COMMAND_STOP: case PLAYER_COMMAND_EXIT: case PLAYER_COMMAND_CLOSE_AUDIO: break; + case PLAYER_COMMAND_UPDATE_AUDIO: + player_unlock(); + audio_output_all_enable_disable(); + player_lock(); + player_command_finished_locked(); + break; + case PLAYER_COMMAND_QUEUE: assert(pc.next_song != NULL); assert(!player->queued); assert(dc.pipe == NULL || dc.pipe == player->pipe); player->queued = true; - player_command_finished(); + player_command_finished_locked(); break; case PLAYER_COMMAND_PAUSE: + player_unlock(); + player->paused = !player->paused; if (player->paused) { audio_output_all_pause(); + player_lock(); + pc.state = PLAYER_STATE_PAUSE; } else if (!audio_format_defined(&player->play_audio_format)) { /* the decoder hasn't provided an audio format yet - don't open the audio device yet */ + player_lock(); pc.state = PLAYER_STATE_PLAY; } else if (audio_output_all_open(&player->play_audio_format, player_buffer)) { /* unpaused, continue playing */ + player_lock(); + pc.state = PLAYER_STATE_PLAY; } else { /* the audio device has failed - rollback to pause mode */ - assert(dc.next_song == NULL || dc.next_song->url != NULL); + assert(dc.next_song == NULL || dc.next_song->uri != NULL); pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_AUDIO; player->paused = true; + + player_lock(); } - player_command_finished(); + player_command_finished_locked(); break; case PLAYER_COMMAND_SEEK: + player_unlock(); player_seek_decoder(player); + player_lock(); break; case PLAYER_COMMAND_CANCEL: @@ -402,80 +452,91 @@ static void player_process_command(struct player *player) return; } - if (dc.pipe != NULL && dc.pipe != player->pipe) + if (dc.pipe != NULL && dc.pipe != player->pipe) { /* the decoder is already decoding the song - stop it and reset the position */ + player_unlock(); player_dc_stop(player); + player_lock(); + } pc.next_song = NULL; player->queued = false; - player_command_finished(); + player_command_finished_locked(); + break; + + case PLAYER_COMMAND_REFRESH: + if (audio_format_defined(&player->play_audio_format) && + !player->paused) { + player_unlock(); + audio_output_all_check(); + player_lock(); + } + + pc.elapsed_time = audio_output_all_get_elapsed_time(); + if (pc.elapsed_time < 0.0) + pc.elapsed_time = player->elapsed_time; + + player_command_finished_locked(); break; } } +static void +update_song_tag(struct song *song, const struct tag *new_tag) +{ + struct tag *old_tag; + + if (song_is_file(song)) + /* don't update tags of local files, only remote + streams may change tags dynamically */ + return; + + old_tag = song->tag; + song->tag = tag_dup(new_tag); + + if (old_tag != NULL) + tag_free(old_tag); + + /* the main thread will update the playlist version when he + receives this event */ + event_pipe_emit(PIPE_EVENT_TAG); + + /* notify all clients that the tag of the current song has + changed */ + idle_add(IDLE_PLAYER); +} + /** * Plays a #music_chunk object (after applying software volume). If * it contains a (stream) tag, copy it to the current song, so MPD's * playlist reflects the new stream tag. + * + * Player lock is not held. */ static bool play_chunk(struct song *song, struct music_chunk *chunk, - const struct audio_format *format, double sizeToTime) + const struct audio_format *format) { - bool success; - assert(music_chunk_check_format(chunk, format)); - if (chunk->tag != NULL) { - if (!song_is_file(song)) { - /* always update the tag of remote streams */ - struct tag *old_tag = song->tag; - - song->tag = tag_dup(chunk->tag); - - if (old_tag != NULL) - tag_free(old_tag); - - /* the main thread will update the playlist - version when he receives this event */ - event_pipe_emit(PIPE_EVENT_TAG); - - /* notify all clients that the tag of the - current song has changed */ - idle_add(IDLE_PLAYER); - } - } + if (chunk->tag != NULL) + update_song_tag(song, chunk->tag); if (chunk->length == 0) { music_buffer_return(player_buffer, chunk); return true; } - pc.elapsed_time = chunk->times; pc.bit_rate = chunk->bit_rate; - /* apply software volume */ - - success = pcm_volume(chunk->data, chunk->length, - format, pc.software_volume); - if (!success) { - g_warning("pcm_volume() failed on %u:%u:%u", - format->sample_rate, format->bits, format->channels); - pc.errored_song = dc.current_song; - pc.error = PLAYER_ERROR_AUDIO; - return false; - } - /* send the chunk to the audio outputs */ - if (!audio_output_all_play(chunk)) { - pc.errored_song = dc.current_song; - pc.error = PLAYER_ERROR_AUDIO; + if (!audio_output_all_play(chunk)) return false; - } - pc.total_play_time += sizeToTime * chunk->length; + pc.total_play_time += (double)chunk->length / + audio_format_time_to_size(format); return true; } @@ -525,14 +586,20 @@ play_next_chunk(struct player *player) music_buffer_return(player_buffer, other_chunk); } else { /* there are not enough decoded chunks yet */ + + decoder_lock(); + if (decoder_is_idle()) { /* the decoder isn't running, abort cross fading */ + decoder_unlock(); + player->xfade = XFADE_DISABLED; } else { /* wait for the decoder */ - notify_signal(&dc.notify); - notify_wait(&pc.notify); + decoder_signal(); + player_wait_decoder(); + decoder_unlock(); return true; } @@ -546,27 +613,34 @@ play_next_chunk(struct player *player) /* play the current chunk */ - success = play_chunk(player->song, chunk, &player->play_audio_format, - player->size_to_time); + success = play_chunk(player->song, chunk, &player->play_audio_format); if (!success) { music_buffer_return(player_buffer, chunk); + player_lock(); + + pc.error = PLAYER_ERROR_AUDIO; + /* pause: the user may resume playback as soon as an audio output becomes available */ pc.state = PLAYER_STATE_PAUSE; player->paused = true; + player_unlock(); + return false; } /* this formula should prevent that the decoder gets woken up with each chunk; it is more efficient to make it decode a larger block at a time */ + decoder_lock(); if (!decoder_is_idle() && music_pipe_size(dc.pipe) <= (pc.buffered_before_play + music_buffer_size(player_buffer) * 3) / 4) - notify_signal(&dc.notify); + decoder_signal(); + decoder_unlock(); return true; } @@ -576,13 +650,21 @@ play_next_chunk(struct player *player) * has consumed all chunks of the current song, and we should start * sending chunks from the next one. * + * The player lock is not held. + * * @return true on success, false on error (playback will be stopped) */ static bool player_song_border(struct player *player) { + char *uri; + player->xfade = XFADE_UNKNOWN; + uri = song_get_uri(player->song); + g_message("played \"%s\"", uri); + g_free(uri); + music_pipe_free(player->pipe); player->pipe = dc.pipe; @@ -608,42 +690,48 @@ static void do_play(void) .xfade = XFADE_UNKNOWN, .cross_fading = false, .cross_fade_chunks = 0, - .size_to_time = 0.0, + .elapsed_time = 0.0, }; + player_unlock(); + player.pipe = music_pipe_new(); dc.buffer = player_buffer; dc.pipe = player.pipe; - dc_start(&pc.notify, pc.next_song); + dc_start(pc.next_song); if (!player_wait_for_decoder(&player)) { player_dc_stop(&player); player_command_finished(); music_pipe_free(player.pipe); event_pipe_emit(PIPE_EVENT_PLAYLIST); + player_lock(); return; } - pc.elapsed_time = 0; + player_lock(); pc.state = PLAYER_STATE_PLAY; - player_command_finished(); + player_command_finished_locked(); while (true) { player_process_command(&player); if (pc.command == PLAYER_COMMAND_STOP || pc.command == PLAYER_COMMAND_EXIT || pc.command == PLAYER_COMMAND_CLOSE_AUDIO) { + player_unlock(); audio_output_all_cancel(); break; } + player_unlock(); + if (player.buffering) { /* buffering at the start of the song - wait until the buffer is large enough, to prevent stuttering on slow machines */ if (music_pipe_size(player.pipe) < pc.buffered_before_play && - !decoder_is_idle()) { + !decoder_lock_is_idle()) { /* not enough decoded buffer space yet */ if (!player.paused && @@ -652,7 +740,11 @@ static void do_play(void) !player_send_silence(&player)) break; - notify_wait(&pc.notify); + decoder_lock(); + /* XXX race condition: check decoder again */ + player_wait_decoder(); + decoder_unlock(); + player_lock(); continue; } else { /* buffering is complete */ @@ -667,6 +759,8 @@ static void do_play(void) success = player_check_decoder_startup(&player); if (!success) break; + + player_lock(); continue; } @@ -678,7 +772,7 @@ static void do_play(void) */ #endif - if (decoder_is_idle() && player.queued) { + if (decoder_lock_is_idle() && player.queued) { /* the decoder has finished the current song; make it decode the next song */ assert(pc.next_song != NULL); @@ -691,7 +785,7 @@ static void do_play(void) if (dc.pipe != NULL && dc.pipe != player.pipe && player.xfade == XFADE_UNKNOWN && - !decoder_is_starting()) { + !decoder_lock_is_starting()) { /* enable cross fading in this song? if yes, calculate how many chunks will be required for it */ @@ -710,9 +804,11 @@ static void do_play(void) player.xfade = XFADE_DISABLED; } - if (player.paused) - notify_wait(&pc.notify); - else if (music_pipe_size(player.pipe) > 0) { + if (player.paused) { + player_lock(); + player_wait(); + continue; + } else if (music_pipe_size(player.pipe) > 0) { /* at least one music chunk is ready - send it to the audio output */ @@ -729,7 +825,7 @@ static void do_play(void) if (!player_song_border(&player)) break; - } else if (decoder_is_idle()) { + } else if (decoder_lock_is_idle()) { /* check the size of the pipe again, because the decoder thread may have added something since we last checked */ @@ -742,20 +838,25 @@ static void do_play(void) if (!player_send_silence(&player)) break; } - } - if (player.queued) { - assert(pc.next_song != NULL); - pc.next_song = NULL; + player_lock(); } player_dc_stop(&player); + assert(!player.queued || pc.next_song != NULL); + pc.next_song = NULL; + music_pipe_clear(player.pipe, player_buffer); music_pipe_free(player.pipe); + player_lock(); pc.state = PLAYER_STATE_STOP; + player_unlock(); + event_pipe_emit(PIPE_EVENT_PLAYLIST); + + player_lock(); } static gpointer player_task(G_GNUC_UNUSED gpointer arg) @@ -764,9 +865,10 @@ static gpointer player_task(G_GNUC_UNUSED gpointer arg) player_buffer = music_buffer_new(pc.buffer_chunks); + player_lock(); + while (1) { switch (pc.command) { - case PLAYER_COMMAND_PLAY: case PLAYER_COMMAND_QUEUE: assert(pc.next_song != NULL); @@ -774,18 +876,25 @@ static gpointer player_task(G_GNUC_UNUSED gpointer arg) break; case PLAYER_COMMAND_STOP: + player_unlock(); audio_output_all_cancel(); + player_lock(); + /* fall through */ case PLAYER_COMMAND_SEEK: case PLAYER_COMMAND_PAUSE: pc.next_song = NULL; - player_command_finished(); + player_command_finished_locked(); break; case PLAYER_COMMAND_CLOSE_AUDIO: + player_unlock(); + audio_output_all_close(); - player_command_finished(); + + player_lock(); + player_command_finished_locked(); #ifndef NDEBUG /* in the DEBUG build, check for leaked @@ -797,25 +906,38 @@ static gpointer player_task(G_GNUC_UNUSED gpointer arg) break; + case PLAYER_COMMAND_UPDATE_AUDIO: + player_unlock(); + audio_output_all_enable_disable(); + player_lock(); + player_command_finished_locked(); + break; + case PLAYER_COMMAND_EXIT: + player_unlock(); + dc_quit(); audio_output_all_close(); music_buffer_free(player_buffer); + player_command_finished(); - g_thread_exit(NULL); - break; + return NULL; case PLAYER_COMMAND_CANCEL: pc.next_song = NULL; - player_command_finished(); + player_command_finished_locked(); + break; + + case PLAYER_COMMAND_REFRESH: + /* no-op when not playing */ + player_command_finished_locked(); break; case PLAYER_COMMAND_NONE: - notify_wait(&pc.notify); + player_wait(); break; } } - return NULL; } void player_create(void) diff --git a/src/playlist.c b/src/playlist.c index 35c09329a..6ca9d014b 100644 --- a/src/playlist.c +++ b/src/playlist.c @@ -34,7 +34,8 @@ #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "playlist" -void playlistVersionChange(struct playlist *playlist) +void +playlist_increment_version_all(struct playlist *playlist) { queue_modify_all(&playlist->queue); idle_add(IDLE_PLAYLIST); @@ -61,16 +62,12 @@ playlist_init(struct playlist *playlist) playlist->queued = -1; playlist->current = -1; - - playlist->prev_elapsed = g_timer_new(); } void playlist_finish(struct playlist *playlist) { queue_finish(&playlist->queue); - - g_timer_destroy(playlist->prev_elapsed); } /** @@ -91,14 +88,15 @@ playlist_queue_song_order(struct playlist *playlist, unsigned order) g_debug("queue song %i:\"%s\"", playlist->queued, uri); g_free(uri); - queueSong(song); + pc_enqueue_song(song); } /** * Check if the player thread has already started playing the "queued" * song. */ -static void syncPlaylistWithQueue(struct playlist *playlist) +static void +playlist_sync_with_queue(struct playlist *playlist) { if (pc.next_song == NULL && playlist->queued != -1) { /* queued song has started: copy queued to current, @@ -109,7 +107,7 @@ static void syncPlaylistWithQueue(struct playlist *playlist) playlist->queued = -1; if(playlist->queue.consume) - deleteFromPlaylist(playlist, queue_order_to_position(&playlist->queue, current)); + playlist_delete(playlist, queue_order_to_position(&playlist->queue, current)); idle_add(IDLE_PLAYER); } @@ -178,7 +176,7 @@ playlist_update_queued_song(struct playlist *playlist, const struct song *prev) } void -playPlaylistOrderNumber(struct playlist *playlist, int orderNum) +playlist_play_order(struct playlist *playlist, int orderNum) { struct song *song; char *uri; @@ -192,34 +190,35 @@ playPlaylistOrderNumber(struct playlist *playlist, int orderNum) g_debug("play %i:\"%s\"", orderNum, uri); g_free(uri); - playerPlay(song); + pc_play(song); playlist->current = orderNum; } static void -playPlaylistIfPlayerStopped(struct playlist *playlist); +playlist_resume_playback(struct playlist *playlist); /** * This is the "PLAYLIST" event handler. It is invoked by the player * thread whenever it requests a new queued song, or when it exits. */ -void syncPlayerAndPlaylist(struct playlist *playlist) +void +playlist_sync(struct playlist *playlist) { if (!playlist->playing) /* this event has reached us out of sync: we aren't playing anymore; ignore the event */ return; - if (getPlayerState() == PLAYER_STATE_STOP) + if (pc_get_state() == PLAYER_STATE_STOP) /* the player thread has stopped: check if playback should be restarted with the next song. That can happen if the playlist isn't filling the queue fast enough */ - playPlaylistIfPlayerStopped(playlist); + playlist_resume_playback(playlist); else { /* check if the player thread has already started playing the queued song */ - syncPlaylistWithQueue(playlist); + playlist_sync_with_queue(playlist); /* make sure the queued song is always set (if possible) */ @@ -233,14 +232,14 @@ void syncPlayerAndPlaylist(struct playlist *playlist) * decide whether to re-start playback */ static void -playPlaylistIfPlayerStopped(struct playlist *playlist) +playlist_resume_playback(struct playlist *playlist) { enum player_error error; assert(playlist->playing); - assert(getPlayerState() == PLAYER_STATE_STOP); + assert(pc_get_state() == PLAYER_STATE_STOP); - error = getPlayerError(); + error = pc_get_error(); if (error == PLAYER_ERROR_NOERROR) playlist->error_count = 0; else @@ -251,37 +250,38 @@ playPlaylistIfPlayerStopped(struct playlist *playlist) playlist->error_count >= queue_length(&playlist->queue)) /* too many errors, or critical error: stop playback */ - stopPlaylist(playlist); + playlist_stop(playlist); else /* continue playback at the next song */ - nextSongInPlaylist(playlist); + playlist_next(playlist); } bool -getPlaylistRepeatStatus(const struct playlist *playlist) +playlist_get_repeat(const struct playlist *playlist) { return playlist->queue.repeat; } bool -getPlaylistRandomStatus(const struct playlist *playlist) +playlist_get_random(const struct playlist *playlist) { return playlist->queue.random; } bool -getPlaylistSingleStatus(const struct playlist *playlist) +playlist_get_single(const struct playlist *playlist) { return playlist->queue.single; } bool -getPlaylistConsumeStatus(const struct playlist *playlist) +playlist_get_consume(const struct playlist *playlist) { return playlist->queue.consume; } -void setPlaylistRepeatStatus(struct playlist *playlist, bool status) +void +playlist_set_repeat(struct playlist *playlist, bool status) { if (status == playlist->queue.repeat) return; @@ -296,7 +296,8 @@ void setPlaylistRepeatStatus(struct playlist *playlist, bool status) idle_add(IDLE_OPTIONS); } -static void orderPlaylist(struct playlist *playlist) +static void +playlist_order(struct playlist *playlist) { if (playlist->current >= 0) /* update playlist.current, order==position now */ @@ -306,7 +307,8 @@ static void orderPlaylist(struct playlist *playlist) queue_restore_order(&playlist->queue); } -void setPlaylistSingleStatus(struct playlist *playlist, bool status) +void +playlist_set_single(struct playlist *playlist, bool status) { if (status == playlist->queue.single) return; @@ -321,7 +323,8 @@ void setPlaylistSingleStatus(struct playlist *playlist, bool status) idle_add(IDLE_OPTIONS); } -void setPlaylistConsumeStatus(struct playlist *playlist, bool status) +void +playlist_set_consume(struct playlist *playlist, bool status) { if (status == playlist->queue.consume) return; @@ -330,7 +333,8 @@ void setPlaylistConsumeStatus(struct playlist *playlist, bool status) idle_add(IDLE_OPTIONS); } -void setPlaylistRandomStatus(struct playlist *playlist, bool status) +void +playlist_set_random(struct playlist *playlist, bool status) { const struct song *queued; @@ -365,14 +369,15 @@ void setPlaylistRandomStatus(struct playlist *playlist, bool status) } else playlist->current = -1; } else - orderPlaylist(playlist); + playlist_order(playlist); playlist_update_queued_song(playlist, queued); idle_add(IDLE_OPTIONS); } -int getPlaylistCurrentSong(const struct playlist *playlist) +int +playlist_get_current_song(const struct playlist *playlist) { if (playlist->current >= 0) return queue_order_to_position(&playlist->queue, @@ -381,7 +386,8 @@ int getPlaylistCurrentSong(const struct playlist *playlist) return -1; } -int getPlaylistNextSong(const struct playlist *playlist) +int +playlist_get_next_song(const struct playlist *playlist) { if (playlist->current >= 0) { @@ -404,19 +410,19 @@ int getPlaylistNextSong(const struct playlist *playlist) } unsigned long -getPlaylistVersion(const struct playlist *playlist) +playlist_get_version(const struct playlist *playlist) { return playlist->queue.version; } int -getPlaylistLength(const struct playlist *playlist) +playlist_get_length(const struct playlist *playlist) { return queue_length(&playlist->queue); } unsigned -getPlaylistSongId(const struct playlist *playlist, unsigned song) +playlist_get_song_id(const struct playlist *playlist, unsigned song) { return queue_position_to_id(&playlist->queue, song); } diff --git a/src/playlist.h b/src/playlist.h index 57b2450fa..f8f5bd942 100644 --- a/src/playlist.h +++ b/src/playlist.h @@ -23,7 +23,6 @@ #include "queue.h" #include <stdbool.h> -#include <stdio.h> #define PLAYLIST_COMMENT '#' @@ -82,21 +81,16 @@ struct playlist { * This variable is only valid if #playing is true. */ int queued; - - /** - * This timer tracks the time elapsed since the last "prev" - * command. If that is less than one second ago, "prev" jumps - * to the previous song instead of rewinding the current song. - */ - GTimer *prev_elapsed; }; /** the global playlist object */ extern struct playlist g_playlist; -void initPlaylist(void); +void +playlist_global_init(void); -void finishPlaylist(void); +void +playlist_global_finish(void); void playlist_init(struct playlist *playlist); @@ -116,11 +110,8 @@ playlist_get_queue(const struct playlist *playlist) return &playlist->queue; } -void readPlaylistState(FILE *); - -void savePlaylistState(FILE *); - -void clearPlaylist(struct playlist *playlist); +void +playlist_clear(struct playlist *playlist); #ifndef WIN32 /** @@ -133,90 +124,111 @@ playlist_append_file(struct playlist *playlist, const char *path, int uid, #endif enum playlist_result -addToPlaylist(struct playlist *playlist, const char *file, unsigned *added_id); +playlist_append_uri(struct playlist *playlist, const char *file, + unsigned *added_id); enum playlist_result -addSongToPlaylist(struct playlist *playlist, +playlist_append_song(struct playlist *playlist, struct song *song, unsigned *added_id); enum playlist_result -deleteFromPlaylist(struct playlist *playlist, unsigned song); +playlist_delete(struct playlist *playlist, unsigned song); + +/** + * Deletes a range of songs from the playlist. + * + * @param start the position of the first song to delete + * @param end the position after the last song to delete + */ +enum playlist_result +playlist_delete_range(struct playlist *playlist, unsigned start, unsigned end); enum playlist_result -deleteFromPlaylistById(struct playlist *playlist, unsigned song); +playlist_delete_id(struct playlist *playlist, unsigned song); -void stopPlaylist(struct playlist *playlist); +void +playlist_stop(struct playlist *playlist); enum playlist_result -playPlaylist(struct playlist *playlist, int song); +playlist_play(struct playlist *playlist, int song); enum playlist_result -playPlaylistById(struct playlist *playlist, int song); +playlist_play_id(struct playlist *playlist, int song); -void nextSongInPlaylist(struct playlist *playlist); +void +playlist_next(struct playlist *playlist); -void syncPlayerAndPlaylist(struct playlist *playlist); +void +playlist_sync(struct playlist *playlist); -void previousSongInPlaylist(struct playlist *playlist); +void +playlist_previous(struct playlist *playlist); -void shufflePlaylist(struct playlist *playlist, unsigned start, unsigned end); +void +playlist_shuffle(struct playlist *playlist, unsigned start, unsigned end); void -deleteASongFromPlaylist(struct playlist *playlist, const struct song *song); +playlist_delete_song(struct playlist *playlist, const struct song *song); enum playlist_result -moveSongRangeInPlaylist(struct playlist *playlist, unsigned start, unsigned end, int to); +playlist_move_range(struct playlist *playlist, unsigned start, unsigned end, int to); enum playlist_result -moveSongInPlaylistById(struct playlist *playlist, unsigned id, int to); +playlist_move_id(struct playlist *playlist, unsigned id, int to); enum playlist_result -swapSongsInPlaylist(struct playlist *playlist, unsigned song1, unsigned song2); +playlist_swap_songs(struct playlist *playlist, unsigned song1, unsigned song2); enum playlist_result -swapSongsInPlaylistById(struct playlist *playlist, unsigned id1, unsigned id2); +playlist_swap_songs_id(struct playlist *playlist, unsigned id1, unsigned id2); bool -getPlaylistRepeatStatus(const struct playlist *playlist); +playlist_get_repeat(const struct playlist *playlist); -void setPlaylistRepeatStatus(struct playlist *playlist, bool status); +void +playlist_set_repeat(struct playlist *playlist, bool status); bool -getPlaylistRandomStatus(const struct playlist *playlist); +playlist_get_random(const struct playlist *playlist); -void setPlaylistRandomStatus(struct playlist *playlist, bool status); +void +playlist_set_random(struct playlist *playlist, bool status); bool -getPlaylistSingleStatus(const struct playlist *playlist); +playlist_get_single(const struct playlist *playlist); -void setPlaylistSingleStatus(struct playlist *playlist, bool status); +void +playlist_set_single(struct playlist *playlist, bool status); bool -getPlaylistConsumeStatus(const struct playlist *playlist); +playlist_get_consume(const struct playlist *playlist); -void setPlaylistConsumeStatus(struct playlist *playlist, bool status); +void +playlist_set_consume(struct playlist *playlist, bool status); -int getPlaylistCurrentSong(const struct playlist *playlist); +int +playlist_get_current_song(const struct playlist *playlist); -int getPlaylistNextSong(const struct playlist *playlist); +int +playlist_get_next_song(const struct playlist *playlist); unsigned -getPlaylistSongId(const struct playlist *playlist, unsigned song); +playlist_get_song_id(const struct playlist *playlist, unsigned song); -int getPlaylistLength(const struct playlist *playlist); +int +playlist_get_length(const struct playlist *playlist); unsigned long -getPlaylistVersion(const struct playlist *playlist); +playlist_get_version(const struct playlist *playlist); enum playlist_result -seekSongInPlaylist(struct playlist *playlist, unsigned song, float seek_time); +playlist_seek_song(struct playlist *playlist, unsigned song, float seek_time); enum playlist_result -seekSongInPlaylistById(struct playlist *playlist, +playlist_seek_song_id(struct playlist *playlist, unsigned id, float seek_time); -void playlistVersionChange(struct playlist *playlist); - -int is_valid_playlist_name(const char *utf8path); +void +playlist_increment_version_all(struct playlist *playlist); #endif diff --git a/src/playlist/asx_playlist_plugin.c b/src/playlist/asx_playlist_plugin.c new file mode 100644 index 000000000..f79e3280d --- /dev/null +++ b/src/playlist/asx_playlist_plugin.c @@ -0,0 +1,313 @@ +/* + * 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 "playlist/asx_playlist_plugin.h" +#include "playlist_plugin.h" +#include "input_stream.h" +#include "song.h" +#include "tag.h" + +#include <glib.h> + +#include <assert.h> +#include <string.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "asx" + +/** + * This is the state object for the GLib XML parser. + */ +struct asx_parser { + /** + * The list of songs (in reverse order because that's faster + * while adding). + */ + GSList *songs; + + /** + * The current position in the XML file. + */ + enum { + ROOT, ENTRY, + } state; + + /** + * The current tag within the "entry" element. This is only + * valid if state==ENTRY. TAG_NUM_OF_ITEM_TYPES means there + * is no (known) tag. + */ + enum tag_type tag; + + /** + * The current song. It is allocated after the "location" + * element. + */ + struct song *song; +}; + +static const gchar * +get_attribute(const gchar **attribute_names, const gchar **attribute_values, + const gchar *name) +{ + for (unsigned i = 0; attribute_names[i] != NULL; ++i) + if (g_ascii_strcasecmp(attribute_names[i], name) == 0) + return attribute_values[i]; + + return NULL; +} + +static void +asx_start_element(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct asx_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + if (g_ascii_strcasecmp(element_name, "entry") == 0) { + parser->state = ENTRY; + parser->song = song_remote_new("asx:"); + parser->tag = TAG_NUM_OF_ITEM_TYPES; + } + + break; + + case ENTRY: + if (g_ascii_strcasecmp(element_name, "ref") == 0) { + const gchar *href = get_attribute(attribute_names, + attribute_values, + "href"); + if (href != NULL) { + /* create new song object, and copy + the existing tag over; we cannot + replace the existing song's URI, + because that attribute is + immutable */ + struct song *song = song_remote_new(href); + + if (parser->song != NULL) { + song->tag = parser->song->tag; + parser->song->tag = NULL; + song_free(parser->song); + } + + parser->song = song; + } + } else if (g_ascii_strcasecmp(element_name, "author") == 0) + /* is that correct? or should it be COMPOSER + or PERFORMER? */ + parser->tag = TAG_ARTIST; + else if (g_ascii_strcasecmp(element_name, "title") == 0) + parser->tag = TAG_TITLE; + + break; + } +} + +static void +asx_end_element(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct asx_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + break; + + case ENTRY: + if (g_ascii_strcasecmp(element_name, "entry") == 0) { + if (strcmp(parser->song->uri, "asx:") != 0) + parser->songs = g_slist_prepend(parser->songs, + parser->song); + else + song_free(parser->song); + + parser->state = ROOT; + } else + parser->tag = TAG_NUM_OF_ITEM_TYPES; + + break; + } +} + +static void +asx_text(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *text, gsize text_len, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct asx_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + break; + + case ENTRY: + if (parser->tag != TAG_NUM_OF_ITEM_TYPES) { + if (parser->song->tag == NULL) + parser->song->tag = tag_new(); + tag_add_item_n(parser->song->tag, parser->tag, + text, text_len); + } + + break; + } +} + +static const GMarkupParser asx_parser = { + .start_element = asx_start_element, + .end_element = asx_end_element, + .text = asx_text, +}; + +static void +song_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct song *song = data; + + song_free(song); +} + +static void +asx_parser_destroy(gpointer data) +{ + struct asx_parser *parser = data; + + if (parser->state >= ENTRY) + song_free(parser->song); + + g_slist_foreach(parser->songs, song_free_callback, NULL); + g_slist_free(parser->songs); +} + +/* + * The playlist object + * + */ + +struct asx_playlist { + struct playlist_provider base; + + GSList *songs; +}; + +static struct playlist_provider * +asx_open_stream(struct input_stream *is) +{ + struct asx_parser parser = { + .songs = NULL, + .state = ROOT, + }; + struct asx_playlist *playlist; + GMarkupParseContext *context; + char buffer[1024]; + size_t nbytes; + bool success; + GError *error = NULL; + + /* parse the ASX XML file */ + + context = g_markup_parse_context_new(&asx_parser, + G_MARKUP_TREAT_CDATA_AS_TEXT, + &parser, asx_parser_destroy); + + while (true) { + nbytes = input_stream_read(is, buffer, sizeof(buffer)); + if (nbytes == 0) + break; + + success = g_markup_parse_context_parse(context, buffer, nbytes, + &error); + if (!success) { + g_warning("XML parser failed: %s", error->message); + g_error_free(error); + g_markup_parse_context_free(context); + return NULL; + } + } + + success = g_markup_parse_context_end_parse(context, &error); + if (!success) { + g_warning("XML parser failed: %s", error->message); + g_error_free(error); + g_markup_parse_context_free(context); + return NULL; + } + + /* create a #asx_playlist object from the parsed song list */ + + playlist = g_new(struct asx_playlist, 1); + playlist_provider_init(&playlist->base, &asx_playlist_plugin); + playlist->songs = g_slist_reverse(parser.songs); + parser.songs = NULL; + + g_markup_parse_context_free(context); + + return &playlist->base; +} + +static void +asx_close(struct playlist_provider *_playlist) +{ + struct asx_playlist *playlist = (struct asx_playlist *)_playlist; + + g_slist_foreach(playlist->songs, song_free_callback, NULL); + g_slist_free(playlist->songs); + g_free(playlist); +} + +static struct song * +asx_read(struct playlist_provider *_playlist) +{ + struct asx_playlist *playlist = (struct asx_playlist *)_playlist; + struct song *song; + + if (playlist->songs == NULL) + return NULL; + + song = playlist->songs->data; + playlist->songs = g_slist_remove(playlist->songs, song); + + return song; +} + +static const char *const asx_suffixes[] = { + "asx", + NULL +}; + +static const char *const asx_mime_types[] = { + "video/x-ms-asf", + NULL +}; + +const struct playlist_plugin asx_playlist_plugin = { + .name = "asx", + + .open_stream = asx_open_stream, + .close = asx_close, + .read = asx_read, + + .suffixes = asx_suffixes, + .mime_types = asx_mime_types, +}; diff --git a/src/input/lastfm_input_plugin.h b/src/playlist/asx_playlist_plugin.h index d0eaf5a55..021e97f56 100644 --- a/src/input/lastfm_input_plugin.h +++ b/src/playlist/asx_playlist_plugin.h @@ -17,9 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#ifndef LASTFM_INPUT_PLUGIN_H -#define LASTFM_INPUT_PLUGIN_H +#ifndef MPD_PLAYLIST_ASX_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_ASX_PLAYLIST_PLUGIN_H -extern const struct input_plugin lastfm_input_plugin; +extern const struct playlist_plugin asx_playlist_plugin; #endif diff --git a/src/playlist/lastfm_playlist_plugin.c b/src/playlist/lastfm_playlist_plugin.c new file mode 100644 index 000000000..d7de25375 --- /dev/null +++ b/src/playlist/lastfm_playlist_plugin.c @@ -0,0 +1,300 @@ +/* + * 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 "playlist/lastfm_playlist_plugin.h" +#include "playlist_plugin.h" +#include "playlist_list.h" +#include "conf.h" +#include "uri.h" +#include "song.h" +#include "input_stream.h" + +#include <glib.h> + +#include <assert.h> +#include <string.h> + +struct lastfm_playlist { + struct playlist_provider base; + + struct input_stream is; + + struct playlist_provider *xspf; +}; + +static struct { + char *user; + char *md5; +} lastfm_config; + +static bool +lastfm_init(const struct config_param *param) +{ + const char *user = config_get_block_string(param, "user", NULL); + const char *passwd = config_get_block_string(param, "password", NULL); + + if (user == NULL || passwd == NULL) { + g_debug("disabling the last.fm playlist plugin " + "because account is not configured"); + return false; + } + +#if GLIB_CHECK_VERSION(2,16,0) + lastfm_config.user = g_uri_escape_string(user, NULL, false); +#else + lastfm_config.user = g_strdup(user); +#endif + +#if GLIB_CHECK_VERSION(2,16,0) + if (strlen(passwd) != 32) + lastfm_config.md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, + passwd, strlen(passwd)); + else +#endif + lastfm_config.md5 = g_strdup(passwd); + + return true; +} + +static void +lastfm_finish(void) +{ + g_free(lastfm_config.user); + g_free(lastfm_config.md5); +} + +/** + * Simple data fetcher. + * @param url path or url of data to fetch. + * @return data fetched, or NULL on error. Must be freed with g_free. + */ +static char * +lastfm_get(const char *url) +{ + struct input_stream input_stream; + bool success; + int ret; + char buffer[4096]; + size_t length = 0, nbytes; + + success = input_stream_open(&input_stream, url); + if (!success) + return NULL; + + while (!input_stream.ready) { + ret = input_stream_buffer(&input_stream); + if (ret < 0) { + input_stream_close(&input_stream); + return NULL; + } + } + + do { + nbytes = input_stream_read(&input_stream, buffer + length, + sizeof(buffer) - length); + if (nbytes == 0) { + if (input_stream_eof(&input_stream)) + break; + + /* I/O error */ + input_stream_close(&input_stream); + return NULL; + } + + length += nbytes; + } while (length < sizeof(buffer)); + + input_stream_close(&input_stream); + return g_strndup(buffer, length); +} + +/** + * Ini-style value fetcher. + * @param response data through which to search. + * @param name name of value to search for. + * @return value for param name in param reponse or NULL on error. Free with g_free. + */ +static char * +lastfm_find(const char *response, const char *name) +{ + size_t name_length = strlen(name); + + while (true) { + const char *eol = strchr(response, '\n'); + if (eol == NULL) + return NULL; + + if (strncmp(response, name, name_length) == 0 && + response[name_length] == '=') { + response += name_length + 1; + return g_strndup(response, eol - response); + } + + response = eol + 1; + } +} + +static struct playlist_provider * +lastfm_open_uri(const char *uri) +{ + struct lastfm_playlist *playlist; + char *p, *q, *response, *session; + bool success; + + /* handshake */ + + p = g_strconcat("http://ws.audioscrobbler.com/radio/handshake.php?" + "version=1.1.1&platform=linux&" + "username=", lastfm_config.user, "&" + "passwordmd5=", lastfm_config.md5, "&" + "debug=0&partner=", NULL); + response = lastfm_get(p); + g_free(p); + if (response == NULL) + return NULL; + + /* extract session id from response */ + + session = lastfm_find(response, "session"); + g_free(response); + if (session == NULL) { + g_warning("last.fm handshake failed"); + return NULL; + } + +#if GLIB_CHECK_VERSION(2,16,0) + q = g_uri_escape_string(session, NULL, false); + g_free(session); + session = q; +#endif + + g_debug("session='%s'", session); + + /* "adjust" last.fm radio */ + + if (strlen(uri) > 9) { + char *escaped_uri; + +#if GLIB_CHECK_VERSION(2,16,0) + escaped_uri = g_uri_escape_string(uri, NULL, false); +#else + escaped_uri = g_strdup(uri); +#endif + + p = g_strconcat("http://ws.audioscrobbler.com/radio/adjust.php?" + "session=", session, "&url=", escaped_uri, "&debug=0", + NULL); + g_free(escaped_uri); + + response = lastfm_get(p); + g_free(response); + g_free(p); + + if (response == NULL) { + g_free(session); + return NULL; + } + } + + /* create the playlist object */ + + playlist = g_new(struct lastfm_playlist, 1); + playlist_provider_init(&playlist->base, &lastfm_playlist_plugin); + + /* open the last.fm playlist */ + + p = g_strconcat("http://ws.audioscrobbler.com/radio/xspf.php?" + "sk=", session, "&discovery=0&desktop=1.5.1.31879", + NULL); + g_free(session); + + success = input_stream_open(&playlist->is, p); + g_free(p); + + if (!success) { + g_warning("Failed to load XSPF playlist"); + g_free(playlist); + return NULL; + } + + while (!playlist->is.ready) { + int ret = input_stream_buffer(&playlist->is); + if (ret < 0) { + input_stream_close(&playlist->is); + g_free(playlist); + return NULL; + } + + if (ret == 0) + /* nothing was buffered - wait */ + g_usleep(10000); + } + + /* last.fm does not send a MIME type, we have to fake it here + :-( */ + g_free(playlist->is.mime); + playlist->is.mime = g_strdup("application/xspf+xml"); + + /* parse the XSPF playlist */ + + playlist->xspf = playlist_list_open_stream(&playlist->is, NULL); + if (playlist->xspf == NULL) { + input_stream_close(&playlist->is); + g_free(playlist); + g_warning("Failed to parse XSPF playlist"); + return NULL; + } + + return &playlist->base; +} + +static void +lastfm_close(struct playlist_provider *_playlist) +{ + struct lastfm_playlist *playlist = (struct lastfm_playlist *)_playlist; + + playlist_plugin_close(playlist->xspf); + input_stream_close(&playlist->is); + g_free(playlist); +} + +static struct song * +lastfm_read(struct playlist_provider *_playlist) +{ + struct lastfm_playlist *playlist = (struct lastfm_playlist *)_playlist; + + return playlist_plugin_read(playlist->xspf); +} + +static const char *const lastfm_schemes[] = { + "lastfm", + NULL +}; + +const struct playlist_plugin lastfm_playlist_plugin = { + .name = "lastfm", + + .init = lastfm_init, + .finish = lastfm_finish, + .open_uri = lastfm_open_uri, + .close = lastfm_close, + .read = lastfm_read, + + .schemes = lastfm_schemes, +}; diff --git a/src/playlist/lastfm_playlist_plugin.h b/src/playlist/lastfm_playlist_plugin.h new file mode 100644 index 000000000..871a4a043 --- /dev/null +++ b/src/playlist/lastfm_playlist_plugin.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_LASTFM_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_LASTFM_PLAYLIST_PLUGIN_H + +extern const struct playlist_plugin lastfm_playlist_plugin; + +#endif diff --git a/src/playlist/m3u_playlist_plugin.c b/src/playlist/m3u_playlist_plugin.c new file mode 100644 index 000000000..6daea41f1 --- /dev/null +++ b/src/playlist/m3u_playlist_plugin.c @@ -0,0 +1,91 @@ +/* + * 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 "playlist/m3u_playlist_plugin.h" +#include "playlist_plugin.h" +#include "text_input_stream.h" +#include "uri.h" +#include "song.h" + +#include <glib.h> + +struct m3u_playlist { + struct playlist_provider base; + + struct text_input_stream *tis; +}; + +static struct playlist_provider * +m3u_open_stream(struct input_stream *is) +{ + struct m3u_playlist *playlist = g_new(struct m3u_playlist, 1); + + playlist_provider_init(&playlist->base, &m3u_playlist_plugin); + playlist->tis = text_input_stream_new(is); + + return &playlist->base; +} + +static void +m3u_close(struct playlist_provider *_playlist) +{ + struct m3u_playlist *playlist = (struct m3u_playlist *)_playlist; + + text_input_stream_free(playlist->tis); + g_free(playlist); +} + +static struct song * +m3u_read(struct playlist_provider *_playlist) +{ + struct m3u_playlist *playlist = (struct m3u_playlist *)_playlist; + const char *line; + + do { + line = text_input_stream_read(playlist->tis); + if (line == NULL) + return NULL; + + while (*line != 0 && g_ascii_isspace(*line)) + ++line; + } while (line[0] == '#' || !uri_has_scheme(line)); + + return song_remote_new(line); +} + +static const char *const m3u_suffixes[] = { + "m3u", + NULL +}; + +static const char *const m3u_mime_types[] = { + "audio/x-mpegurl", + NULL +}; + +const struct playlist_plugin m3u_playlist_plugin = { + .name = "m3u", + + .open_stream = m3u_open_stream, + .close = m3u_close, + .read = m3u_read, + + .suffixes = m3u_suffixes, + .mime_types = m3u_mime_types, +}; diff --git a/src/playlist/m3u_playlist_plugin.h b/src/playlist/m3u_playlist_plugin.h new file mode 100644 index 000000000..3cb4b8874 --- /dev/null +++ b/src/playlist/m3u_playlist_plugin.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_M3U_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_M3U_PLAYLIST_PLUGIN_H + +extern const struct playlist_plugin m3u_playlist_plugin; + +#endif diff --git a/src/playlist/pls_playlist_plugin.c b/src/playlist/pls_playlist_plugin.c new file mode 100644 index 000000000..ede057acc --- /dev/null +++ b/src/playlist/pls_playlist_plugin.c @@ -0,0 +1,215 @@ +/* + * 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 "playlist/pls_playlist_plugin.h" +#include "playlist_plugin.h" +#include "input_stream.h" +#include "uri.h" +#include "song.h" +#include "tag.h" +#include <glib.h> + +struct pls_playlist { + struct playlist_provider base; + + GSList *songs; +}; + +static void pls_parser(GKeyFile *keyfile, struct pls_playlist *playlist) +{ + gchar *key; + gchar *value; + int length; + GError *error = NULL; + int num_entries = g_key_file_get_integer(keyfile, "playlist", + "NumberOfEntries", &error); + if (error) { + g_debug("Invalid PLS file: '%s'", error->message); + g_error_free(error); + error = NULL; + + /* Hack to work around shoutcast failure to comform to spec */ + num_entries = g_key_file_get_integer(keyfile, "playlist", + "numberofentries", &error); + if (error) { + g_error_free(error); + error = NULL; + } + } + + while (num_entries > 0) { + struct song *song; + key = g_strdup_printf("File%i", num_entries); + value = g_key_file_get_string(keyfile, "playlist", key, + &error); + if(error) { + g_debug("Invalid PLS entry %s: '%s'",key, error->message); + g_error_free(error); + g_free(key); + return; + } + g_free(key); + + /* Don't load local path */ + if(!uri_has_scheme(value)){ + g_free(value); + continue; + } + + song = song_remote_new(value); + g_free(value); + + key = g_strdup_printf("Title%i", num_entries); + value = g_key_file_get_string(keyfile, "playlist", key, + &error); + g_free(key); + if(error == NULL && value){ + if (song->tag == NULL) + song->tag = tag_new(); + tag_add_item(song->tag,TAG_TITLE, value); + } + /* Ignore errors? Most likely value not present */ + if(error) g_error_free(error); + error = NULL; + g_free(value); + + key = g_strdup_printf("Length%i", num_entries); + length = g_key_file_get_integer(keyfile, "playlist", key, + &error); + g_free(key); + if(error == NULL && length > 0){ + if (song->tag == NULL) + song->tag = tag_new(); + song->tag->time = length; + } + /* Ignore errors? Most likely value not present */ + if(error) g_error_free(error); + error = NULL; + + playlist->songs = g_slist_prepend(playlist->songs, song); + num_entries--; + } + +} + +static struct playlist_provider * +pls_open_stream(struct input_stream *is) +{ + GError *error = NULL; + size_t nbytes; + char buffer[1024]; + bool success; + GKeyFile *keyfile; + struct pls_playlist *playlist; + GString *kf_data = g_string_new(""); + + do { + nbytes = input_stream_read(is, buffer, sizeof(buffer)); + if(nbytes ==0) + break; + kf_data = g_string_append_len(kf_data, buffer,nbytes); + /* Limit to 64k */ + } while(kf_data->len < 65536); + + if (kf_data->len == 0) { + g_warning("KeyFile parser failed: No Data"); + g_string_free(kf_data, TRUE); + return NULL; + } + + keyfile = g_key_file_new(); + success = g_key_file_load_from_data(keyfile, + kf_data->str, kf_data->len, + G_KEY_FILE_NONE, &error); + + g_string_free(kf_data, TRUE); + + if (!success) { + g_warning("KeyFile parser failed: %s", error->message); + g_error_free(error); + g_key_file_free(keyfile); + return NULL; + } + + playlist = g_new(struct pls_playlist, 1); + playlist_provider_init(&playlist->base, &pls_playlist_plugin); + playlist->songs = NULL; + + pls_parser(keyfile, playlist); + + g_key_file_free(keyfile); + return &playlist->base; +} + + +static void +song_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct song *song = data; + + song_free(song); +} + +static void +pls_close(struct playlist_provider *_playlist) +{ + struct pls_playlist *playlist = (struct pls_playlist *)_playlist; + + g_slist_foreach(playlist->songs, song_free_callback, NULL); + g_slist_free(playlist->songs); + + g_free(playlist); + +} + +static struct song * +pls_read(struct playlist_provider *_playlist) +{ + struct pls_playlist *playlist = (struct pls_playlist *)_playlist; + struct song *song; + + if (playlist->songs == NULL) + return NULL; + + song = playlist->songs->data; + playlist->songs = g_slist_remove(playlist->songs, song); + + return song; +} + +static const char *const pls_suffixes[] = { + "pls", + NULL +}; + +static const char *const pls_mime_types[] = { + "audio/x-scpls", + NULL +}; + +const struct playlist_plugin pls_playlist_plugin = { + .name = "pls", + + .open_stream = pls_open_stream, + .close = pls_close, + .read = pls_read, + + .suffixes = pls_suffixes, + .mime_types = pls_mime_types, +}; diff --git a/src/playlist/pls_playlist_plugin.h b/src/playlist/pls_playlist_plugin.h new file mode 100644 index 000000000..7bf1951b8 --- /dev/null +++ b/src/playlist/pls_playlist_plugin.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_PLS_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_PLS_PLAYLIST_PLUGIN_H + +extern const struct playlist_plugin pls_playlist_plugin; + +#endif diff --git a/src/playlist/xspf_playlist_plugin.c b/src/playlist/xspf_playlist_plugin.c new file mode 100644 index 000000000..6c81d979d --- /dev/null +++ b/src/playlist/xspf_playlist_plugin.c @@ -0,0 +1,333 @@ +/* + * 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 "playlist/xspf_playlist_plugin.h" +#include "playlist_plugin.h" +#include "input_stream.h" +#include "uri.h" +#include "song.h" +#include "tag.h" + +#include <glib.h> + +#include <assert.h> +#include <string.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "xspf" + +/** + * This is the state object for the GLib XML parser. + */ +struct xspf_parser { + /** + * The list of songs (in reverse order because that's faster + * while adding). + */ + GSList *songs; + + /** + * The current position in the XML file. + */ + enum { + ROOT, PLAYLIST, TRACKLIST, TRACK, + LOCATION, + } state; + + /** + * The current tag within the "track" element. This is only + * valid if state==TRACK. TAG_NUM_OF_ITEM_TYPES means there + * is no (known) tag. + */ + enum tag_type tag; + + /** + * The current song. It is allocated after the "location" + * element. + */ + struct song *song; +}; + +static void +xspf_start_element(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *element_name, + G_GNUC_UNUSED const gchar **attribute_names, + G_GNUC_UNUSED const gchar **attribute_values, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct xspf_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + if (strcmp(element_name, "playlist") == 0) + parser->state = PLAYLIST; + + break; + + case PLAYLIST: + if (strcmp(element_name, "trackList") == 0) + parser->state = TRACKLIST; + + break; + + case TRACKLIST: + if (strcmp(element_name, "track") == 0) { + parser->state = TRACK; + parser->song = NULL; + parser->tag = TAG_NUM_OF_ITEM_TYPES; + } + + break; + + case TRACK: + if (strcmp(element_name, "location") == 0) + parser->state = LOCATION; + else if (strcmp(element_name, "title") == 0) + parser->tag = TAG_TITLE; + else if (strcmp(element_name, "creator") == 0) + /* TAG_COMPOSER would be more correct + according to the XSPF spec */ + parser->tag = TAG_ARTIST; + else if (strcmp(element_name, "annotation") == 0) + parser->tag = TAG_COMMENT; + else if (strcmp(element_name, "album") == 0) + parser->tag = TAG_ALBUM; + else if (strcmp(element_name, "trackNum") == 0) + parser->tag = TAG_TRACK; + + break; + + case LOCATION: + break; + } +} + +static void +xspf_end_element(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct xspf_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + break; + + case PLAYLIST: + if (strcmp(element_name, "playlist") == 0) + parser->state = ROOT; + + break; + + case TRACKLIST: + if (strcmp(element_name, "tracklist") == 0) + parser->state = PLAYLIST; + + break; + + case TRACK: + if (strcmp(element_name, "track") == 0) { + if (parser->song != NULL) + parser->songs = g_slist_prepend(parser->songs, + parser->song); + + parser->state = TRACKLIST; + } else + parser->tag = TAG_NUM_OF_ITEM_TYPES; + + break; + + case LOCATION: + parser->state = TRACK; + break; + } +} + +static void +xspf_text(G_GNUC_UNUSED GMarkupParseContext *context, + const gchar *text, gsize text_len, + gpointer user_data, G_GNUC_UNUSED GError **error) +{ + struct xspf_parser *parser = user_data; + + switch (parser->state) { + case ROOT: + case PLAYLIST: + case TRACKLIST: + break; + + case TRACK: + if (parser->song != NULL && + parser->tag != TAG_NUM_OF_ITEM_TYPES) { + if (parser->song->tag == NULL) + parser->song->tag = tag_new(); + tag_add_item_n(parser->song->tag, parser->tag, + text, text_len); + } + + break; + + case LOCATION: + if (parser->song == NULL) { + char *uri = g_strndup(text, text_len); + parser->song = song_remote_new(uri); + g_free(uri); + } + + break; + } +} + +static const GMarkupParser xspf_parser = { + .start_element = xspf_start_element, + .end_element = xspf_end_element, + .text = xspf_text, +}; + +static void +song_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + struct song *song = data; + + song_free(song); +} + +static void +xspf_parser_destroy(gpointer data) +{ + struct xspf_parser *parser = data; + + if (parser->state >= TRACK && parser->song != NULL) + song_free(parser->song); + + g_slist_foreach(parser->songs, song_free_callback, NULL); + g_slist_free(parser->songs); +} + +/* + * The playlist object + * + */ + +struct xspf_playlist { + struct playlist_provider base; + + GSList *songs; +}; + +static struct playlist_provider * +xspf_open_stream(struct input_stream *is) +{ + struct xspf_parser parser = { + .songs = NULL, + .state = ROOT, + }; + struct xspf_playlist *playlist; + GMarkupParseContext *context; + char buffer[1024]; + size_t nbytes; + bool success; + GError *error = NULL; + + /* parse the XSPF XML file */ + + context = g_markup_parse_context_new(&xspf_parser, + G_MARKUP_TREAT_CDATA_AS_TEXT, + &parser, xspf_parser_destroy); + + while (true) { + nbytes = input_stream_read(is, buffer, sizeof(buffer)); + if (nbytes == 0) + break; + + success = g_markup_parse_context_parse(context, buffer, nbytes, + &error); + if (!success) { + g_warning("XML parser failed: %s", error->message); + g_error_free(error); + g_markup_parse_context_free(context); + return NULL; + } + } + + success = g_markup_parse_context_end_parse(context, &error); + if (!success) { + g_warning("XML parser failed: %s", error->message); + g_error_free(error); + g_markup_parse_context_free(context); + return NULL; + } + + /* create a #xspf_playlist object from the parsed song list */ + + playlist = g_new(struct xspf_playlist, 1); + playlist_provider_init(&playlist->base, &xspf_playlist_plugin); + playlist->songs = g_slist_reverse(parser.songs); + parser.songs = NULL; + + g_markup_parse_context_free(context); + + return &playlist->base; +} + +static void +xspf_close(struct playlist_provider *_playlist) +{ + struct xspf_playlist *playlist = (struct xspf_playlist *)_playlist; + + g_slist_foreach(playlist->songs, song_free_callback, NULL); + g_slist_free(playlist->songs); + g_free(playlist); +} + +static struct song * +xspf_read(struct playlist_provider *_playlist) +{ + struct xspf_playlist *playlist = (struct xspf_playlist *)_playlist; + struct song *song; + + if (playlist->songs == NULL) + return NULL; + + song = playlist->songs->data; + playlist->songs = g_slist_remove(playlist->songs, song); + + return song; +} + +static const char *const xspf_suffixes[] = { + "xspf", + NULL +}; + +static const char *const xspf_mime_types[] = { + "application/xspf+xml", + NULL +}; + +const struct playlist_plugin xspf_playlist_plugin = { + .name = "xspf", + + .open_stream = xspf_open_stream, + .close = xspf_close, + .read = xspf_read, + + .suffixes = xspf_suffixes, + .mime_types = xspf_mime_types, +}; diff --git a/src/playlist/xspf_playlist_plugin.h b/src/playlist/xspf_playlist_plugin.h new file mode 100644 index 000000000..c2c36fbed --- /dev/null +++ b/src/playlist/xspf_playlist_plugin.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_XSPF_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_XSPF_PLAYLIST_PLUGIN_H + +extern const struct playlist_plugin xspf_playlist_plugin; + +#endif diff --git a/src/playlist_control.c b/src/playlist_control.c index 4359611fd..66e7934dc 100644 --- a/src/playlist_control.c +++ b/src/playlist_control.c @@ -30,15 +30,7 @@ #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "playlist" -enum { - /** - * When the "prev" command is received, rewind the current - * track if this number of seconds has already elapsed. - */ - PLAYLIST_PREV_UNLESS_ELAPSED = 10, -}; - -void stopPlaylist(struct playlist *playlist) +void playlist_stop(struct playlist *playlist) { if (!playlist->playing) return; @@ -46,7 +38,7 @@ void stopPlaylist(struct playlist *playlist) assert(playlist->current >= 0); g_debug("stop"); - playerWait(); + pc_stop(); playlist->queued = -1; playlist->playing = false; @@ -68,11 +60,11 @@ void stopPlaylist(struct playlist *playlist) } } -enum playlist_result playPlaylist(struct playlist *playlist, int song) +enum playlist_result playlist_play(struct playlist *playlist, int song) { unsigned i = song; - clearPlayerError(); + pc_clear_error(); if (song == -1) { /* play any song ("current" song, or the first song */ @@ -83,7 +75,7 @@ enum playlist_result playPlaylist(struct playlist *playlist, int song) if (playlist->playing) { /* already playing: unpause playback, just in case it was paused, and return */ - playerSetPause(0); + pc_set_pause(false); return PLAYLIST_RESULT_SUCCESS; } @@ -115,28 +107,28 @@ enum playlist_result playPlaylist(struct playlist *playlist, int song) playlist->stop_on_error = false; playlist->error_count = 0; - playPlaylistOrderNumber(playlist, i); + playlist_play_order(playlist, i); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result -playPlaylistById(struct playlist *playlist, int id) +playlist_play_id(struct playlist *playlist, int id) { int song; if (id == -1) { - return playPlaylist(playlist, id); + return playlist_play(playlist, id); } song = queue_id_to_position(&playlist->queue, id); if (song < 0) return PLAYLIST_RESULT_NO_SUCH_SONG; - return playPlaylist(playlist, song); + return playlist_play(playlist, song); } void -nextSongInPlaylist(struct playlist *playlist) +playlist_next(struct playlist *playlist) { int next_order; int current; @@ -157,7 +149,7 @@ nextSongInPlaylist(struct playlist *playlist) /* cancel single */ playlist->queue.single = false; /* no song after this one: stop playback */ - stopPlaylist(playlist); + playlist_stop(playlist); /* reset "current song" */ playlist->current = -1; @@ -174,50 +166,42 @@ nextSongInPlaylist(struct playlist *playlist) queue_shuffle_order(&playlist->queue); /* note that playlist->current and playlist->queued are - now invalid, but playPlaylistOrderNumber() will + now invalid, but playlist_play_order() will discard them anyway */ } - playPlaylistOrderNumber(playlist, next_order); + playlist_play_order(playlist, next_order); } /* Consume mode removes each played songs. */ if(playlist->queue.consume) - deleteFromPlaylist(playlist, queue_order_to_position(&playlist->queue, current)); + playlist_delete(playlist, queue_order_to_position(&playlist->queue, current)); } -void previousSongInPlaylist(struct playlist *playlist) +void playlist_previous(struct playlist *playlist) { if (!playlist->playing) return; - if (g_timer_elapsed(playlist->prev_elapsed, NULL) >= 1.0 && - getPlayerElapsedTime() > PLAYLIST_PREV_UNLESS_ELAPSED) { - /* re-start playing the current song (just like the - "prev" button on CD players) */ + assert(queue_length(&playlist->queue) > 0); - playPlaylistOrderNumber(playlist, playlist->current); + if (playlist->current > 0) { + /* play the preceding song */ + playlist_play_order(playlist, + playlist->current - 1); + } else if (playlist->queue.repeat) { + /* play the last song in "repeat" mode */ + playlist_play_order(playlist, + queue_length(&playlist->queue) - 1); } else { - if (playlist->current > 0) { - /* play the preceding song */ - playPlaylistOrderNumber(playlist, - playlist->current - 1); - } else if (playlist->queue.repeat) { - /* play the last song in "repeat" mode */ - playPlaylistOrderNumber(playlist, - queue_length(&playlist->queue) - 1); - } else { - /* re-start playing the current song if it's - the first one */ - playPlaylistOrderNumber(playlist, playlist->current); - } + /* re-start playing the current song if it's + the first one */ + playlist_play_order(playlist, playlist->current); } - - g_timer_start(playlist->prev_elapsed); } enum playlist_result -seekSongInPlaylist(struct playlist *playlist, unsigned song, float seek_time) +playlist_seek_song(struct playlist *playlist, unsigned song, float seek_time) { const struct song *queued; unsigned i; @@ -233,7 +217,7 @@ seekSongInPlaylist(struct playlist *playlist, unsigned song, float seek_time) else i = song; - clearPlayerError(); + pc_clear_error(); playlist->stop_on_error = true; playlist->error_count = 0; @@ -241,7 +225,7 @@ seekSongInPlaylist(struct playlist *playlist, unsigned song, float seek_time) /* seeking is not within the current song - first start playing the new song */ - playPlaylistOrderNumber(playlist, i); + playlist_play_order(playlist, i); queued = NULL; } @@ -259,11 +243,11 @@ seekSongInPlaylist(struct playlist *playlist, unsigned song, float seek_time) } enum playlist_result -seekSongInPlaylistById(struct playlist *playlist, unsigned id, float seek_time) +playlist_seek_song_id(struct playlist *playlist, unsigned id, float seek_time) { int song = queue_id_to_position(&playlist->queue, id); if (song < 0) return PLAYLIST_RESULT_NO_SUCH_SONG; - return seekSongInPlaylist(playlist, song, seek_time); + return playlist_seek_song(playlist, song, seek_time); } diff --git a/src/playlist_edit.c b/src/playlist_edit.c index b83dc0933..8052f8470 100644 --- a/src/playlist_edit.c +++ b/src/playlist_edit.c @@ -35,16 +35,16 @@ #include <unistd.h> #include <stdlib.h> -static void incrPlaylistVersion(struct playlist *playlist) +static void playlist_increment_version(struct playlist *playlist) { queue_increment_version(&playlist->queue); idle_add(IDLE_PLAYLIST); } -void clearPlaylist(struct playlist *playlist) +void playlist_clear(struct playlist *playlist) { - stopPlaylist(playlist); + playlist_stop(playlist); /* make sure there are no references to allocated songs anymore */ @@ -58,7 +58,7 @@ void clearPlaylist(struct playlist *playlist) playlist->current = -1; - incrPlaylistVersion(playlist); + playlist_increment_version(playlist); } #ifndef WIN32 @@ -86,41 +86,12 @@ playlist_append_file(struct playlist *playlist, const char *path, int uid, if (song == NULL) return PLAYLIST_RESULT_NO_SUCH_SONG; - return addSongToPlaylist(playlist, song, added_id); + return playlist_append_song(playlist, song, added_id); } #endif -static struct song * -song_by_url(const char *url) -{ - struct song *song; - - song = db_get_song(url); - if (song != NULL) - return song; - - if (uri_has_scheme(url)) - return song_remote_new(url); - - return NULL; -} - -enum playlist_result -addToPlaylist(struct playlist *playlist, const char *url, unsigned *added_id) -{ - struct song *song; - - g_debug("add to playlist: %s", url); - - song = song_by_url(url); - if (song == NULL) - return PLAYLIST_RESULT_NO_SUCH_SONG; - - return addSongToPlaylist(playlist, song, added_id); -} - enum playlist_result -addSongToPlaylist(struct playlist *playlist, +playlist_append_song(struct playlist *playlist, struct song *song, unsigned *added_id) { const struct song *queued; @@ -147,7 +118,7 @@ addSongToPlaylist(struct playlist *playlist, queue_length(&playlist->queue)); } - incrPlaylistVersion(playlist); + playlist_increment_version(playlist); playlist_update_queued_song(playlist, queued); @@ -157,8 +128,38 @@ addSongToPlaylist(struct playlist *playlist, return PLAYLIST_RESULT_SUCCESS; } +static struct song * +song_by_uri(const char *uri) +{ + struct song *song; + + song = db_get_song(uri); + if (song != NULL) + return song; + + if (uri_has_scheme(uri)) + return song_remote_new(uri); + + return NULL; +} + +enum playlist_result +playlist_append_uri(struct playlist *playlist, const char *uri, + unsigned *added_id) +{ + struct song *song; + + g_debug("add to playlist: %s", uri); + + song = song_by_uri(uri); + if (song == NULL) + return PLAYLIST_RESULT_NO_SUCH_SONG; + + return playlist_append_song(playlist, song, added_id); +} + enum playlist_result -swapSongsInPlaylist(struct playlist *playlist, unsigned song1, unsigned song2) +playlist_swap_songs(struct playlist *playlist, unsigned song1, unsigned song2) { const struct song *queued; @@ -188,7 +189,7 @@ swapSongsInPlaylist(struct playlist *playlist, unsigned song1, unsigned song2) playlist->current = song1; } - incrPlaylistVersion(playlist); + playlist_increment_version(playlist); playlist_update_queued_song(playlist, queued); @@ -196,7 +197,7 @@ swapSongsInPlaylist(struct playlist *playlist, unsigned song1, unsigned song2) } enum playlist_result -swapSongsInPlaylistById(struct playlist *playlist, unsigned id1, unsigned id2) +playlist_swap_songs_id(struct playlist *playlist, unsigned id1, unsigned id2) { int song1 = queue_id_to_position(&playlist->queue, id1); int song2 = queue_id_to_position(&playlist->queue, id2); @@ -204,28 +205,25 @@ swapSongsInPlaylistById(struct playlist *playlist, unsigned id1, unsigned id2) if (song1 < 0 || song2 < 0) return PLAYLIST_RESULT_NO_SUCH_SONG; - return swapSongsInPlaylist(playlist, song1, song2); + return playlist_swap_songs(playlist, song1, song2); } -enum playlist_result -deleteFromPlaylist(struct playlist *playlist, unsigned song) +static void +playlist_delete_internal(struct playlist *playlist, unsigned song, + const struct song **queued_p) { - const struct song *queued; unsigned songOrder; - if (song >= queue_length(&playlist->queue)) - return PLAYLIST_RESULT_BAD_RANGE; - - queued = playlist_get_queued_song(playlist); + assert(song < queue_length(&playlist->queue)); songOrder = queue_position_to_order(&playlist->queue, song); if (playlist->playing && playlist->current == (int)songOrder) { - bool paused = getPlayerState() == PLAYER_STATE_PAUSE; + bool paused = pc_get_state() == PLAYER_STATE_PAUSE; /* the current song is going to be deleted: stop the player */ - playerWait(); + pc_stop(); playlist->playing = false; /* see which song is going to be played instead */ @@ -237,13 +235,13 @@ deleteFromPlaylist(struct playlist *playlist, unsigned song) if (playlist->current >= 0 && !paused) /* play the song after the deleted one */ - playPlaylistOrderNumber(playlist, playlist->current); + playlist_play_order(playlist, playlist->current); else /* no songs left to play, stop playback completely */ - stopPlaylist(playlist); + playlist_stop(playlist); - queued = NULL; + *queued_p = NULL; } else if (playlist->current == (int)songOrder) /* there's a "current song" but we're not playing currently - clear "current" */ @@ -256,41 +254,80 @@ deleteFromPlaylist(struct playlist *playlist, unsigned song) queue_delete(&playlist->queue, song); - incrPlaylistVersion(playlist); - /* update the "current" and "queued" variables */ if (playlist->current > (int)songOrder) { playlist->current--; } +} + +enum playlist_result +playlist_delete(struct playlist *playlist, unsigned song) +{ + const struct song *queued; + + if (song >= queue_length(&playlist->queue)) + return PLAYLIST_RESULT_BAD_RANGE; + + queued = playlist_get_queued_song(playlist); + + playlist_delete_internal(playlist, song, &queued); + playlist_increment_version(playlist); playlist_update_queued_song(playlist, queued); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result -deleteFromPlaylistById(struct playlist *playlist, unsigned id) +playlist_delete_range(struct playlist *playlist, unsigned start, unsigned end) +{ + const struct song *queued; + + if (start >= queue_length(&playlist->queue)) + return PLAYLIST_RESULT_BAD_RANGE; + + if (end > queue_length(&playlist->queue)) + end = queue_length(&playlist->queue); + + if (start >= end) + return PLAYLIST_RESULT_SUCCESS; + + queued = playlist_get_queued_song(playlist); + + do { + playlist_delete_internal(playlist, --end, &queued); + } while (end != start); + + playlist_increment_version(playlist); + playlist_update_queued_song(playlist, queued); + + return PLAYLIST_RESULT_SUCCESS; +} + +enum playlist_result +playlist_delete_id(struct playlist *playlist, unsigned id) { int song = queue_id_to_position(&playlist->queue, id); if (song < 0) return PLAYLIST_RESULT_NO_SUCH_SONG; - return deleteFromPlaylist(playlist, song); + return playlist_delete(playlist, song); } void -deleteASongFromPlaylist(struct playlist *playlist, const struct song *song) +playlist_delete_song(struct playlist *playlist, const struct song *song) { for (int i = queue_length(&playlist->queue) - 1; i >= 0; --i) if (song == queue_get(&playlist->queue, i)) - deleteFromPlaylist(playlist, i); + playlist_delete(playlist, i); pc_song_deleted(song); } enum playlist_result -moveSongRangeInPlaylist(struct playlist *playlist, unsigned start, unsigned end, int to) +playlist_move_range(struct playlist *playlist, + unsigned start, unsigned end, int to) { const struct song *queued; int currentSong; @@ -342,7 +379,7 @@ moveSongRangeInPlaylist(struct playlist *playlist, unsigned start, unsigned end, } } - incrPlaylistVersion(playlist); + playlist_increment_version(playlist); playlist_update_queued_song(playlist, queued); @@ -350,16 +387,17 @@ moveSongRangeInPlaylist(struct playlist *playlist, unsigned start, unsigned end, } enum playlist_result -moveSongInPlaylistById(struct playlist *playlist, unsigned id1, int to) +playlist_move_id(struct playlist *playlist, unsigned id1, int to) { int song = queue_id_to_position(&playlist->queue, id1); if (song < 0) return PLAYLIST_RESULT_NO_SUCH_SONG; - return moveSongRangeInPlaylist(playlist, song, song+1, to); + return playlist_move_range(playlist, song, song+1, to); } -void shufflePlaylist(struct playlist *playlist, unsigned start, unsigned end) +void +playlist_shuffle(struct playlist *playlist, unsigned start, unsigned end) { const struct song *queued; @@ -399,7 +437,7 @@ void shufflePlaylist(struct playlist *playlist, unsigned start, unsigned end) queue_shuffle_range(&playlist->queue, start, end); - incrPlaylistVersion(playlist); + playlist_increment_version(playlist); playlist_update_queued_song(playlist, queued); } diff --git a/src/playlist_global.c b/src/playlist_global.c index fa810bbc3..dcb972490 100644 --- a/src/playlist_global.c +++ b/src/playlist_global.c @@ -37,10 +37,11 @@ playlist_tag_event(void) static void playlist_event(void) { - syncPlayerAndPlaylist(&g_playlist); + playlist_sync(&g_playlist); } -void initPlaylist(void) +void +playlist_global_init(void) { playlist_init(&g_playlist); @@ -48,17 +49,8 @@ void initPlaylist(void) event_pipe_register(PIPE_EVENT_PLAYLIST, playlist_event); } -void finishPlaylist(void) +void +playlist_global_finish(void) { playlist_finish(&g_playlist); } - -void savePlaylistState(FILE *fp) -{ - playlist_state_save(fp, &g_playlist); -} - -void readPlaylistState(FILE *fp) -{ - playlist_state_restore(fp, &g_playlist); -} diff --git a/src/playlist_internal.h b/src/playlist_internal.h index af880691b..8b2f780cc 100644 --- a/src/playlist_internal.h +++ b/src/playlist_internal.h @@ -47,6 +47,6 @@ playlist_update_queued_song(struct playlist *playlist, const struct song *prev); void -playPlaylistOrderNumber(struct playlist *playlist, int orderNum); +playlist_play_order(struct playlist *playlist, int orderNum); #endif diff --git a/src/playlist_list.c b/src/playlist_list.c new file mode 100644 index 000000000..f8d2839d6 --- /dev/null +++ b/src/playlist_list.c @@ -0,0 +1,195 @@ +/* + * 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 "playlist_list.h" +#include "playlist_plugin.h" +#include "playlist/m3u_playlist_plugin.h" +#include "playlist/xspf_playlist_plugin.h" +#include "playlist/lastfm_playlist_plugin.h" +#include "playlist/pls_playlist_plugin.h" +#include "playlist/asx_playlist_plugin.h" +#include "input_stream.h" +#include "uri.h" +#include "utils.h" +#include "conf.h" +#include "config.h" + +#include <glib.h> + +#include <assert.h> +#include <string.h> + +static const struct playlist_plugin *const playlist_plugins[] = { + &m3u_playlist_plugin, + &xspf_playlist_plugin, + &pls_playlist_plugin, + &asx_playlist_plugin, +#ifdef ENABLE_LASTFM + &lastfm_playlist_plugin, +#endif + NULL +}; + +/** which plugins have been initialized successfully? */ +static bool playlist_plugins_enabled[G_N_ELEMENTS(playlist_plugins)]; + +/** + * Find the "playlist" configuration block for the specified plugin. + * + * @param plugin_name the name of the playlist plugin + * @return the configuration block, or NULL if none was configured + */ +static const struct config_param * +playlist_plugin_config(const char *plugin_name) +{ + const struct config_param *param = NULL; + + assert(plugin_name != NULL); + + while ((param = config_get_next_param(CONF_PLAYLIST_PLUGIN, param)) != NULL) { + const char *name = + config_get_block_string(param, "name", NULL); + if (name == NULL) + g_error("playlist configuration without 'plugin' name in line %d", + param->line); + + if (strcmp(name, plugin_name) == 0) + return param; + } + + return NULL; +} + +void +playlist_list_global_init(void) +{ + for (unsigned i = 0; playlist_plugins[i] != NULL; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + const struct config_param *param = + playlist_plugin_config(plugin->name); + + if (!config_get_block_bool(param, "enabled", true)) + /* the plugin is disabled in mpd.conf */ + continue; + + playlist_plugins_enabled[i] = + playlist_plugin_init(playlist_plugins[i], param); + } +} + +void +playlist_list_global_finish(void) +{ + for (unsigned i = 0; playlist_plugins[i] != NULL; ++i) + if (playlist_plugins_enabled[i]) + playlist_plugin_finish(playlist_plugins[i]); +} + +struct playlist_provider * +playlist_list_open_uri(const char *uri) +{ + char *scheme; + struct playlist_provider *playlist = NULL; + + assert(uri != NULL); + + scheme = g_uri_parse_scheme(uri); + if (scheme == NULL) + return NULL; + + for (unsigned i = 0; playlist_plugins[i] != NULL; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + + if (playlist_plugins_enabled[i] && + stringFoundInStringArray(plugin->schemes, scheme)) { + playlist = playlist_plugin_open_uri(plugin, uri); + if (playlist != NULL) + break; + } + } + + g_free(scheme); + return playlist; +} + +static struct playlist_provider * +playlist_list_open_stream_mime(struct input_stream *is) +{ + struct playlist_provider *playlist; + + assert(is != NULL); + assert(is->mime != NULL); + + for (unsigned i = 0; playlist_plugins[i] != NULL; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + + if (playlist_plugins_enabled[i] && + stringFoundInStringArray(plugin->mime_types, is->mime)) { + playlist = playlist_plugin_open_stream(plugin, is); + if (playlist != NULL) + return playlist; + } + } + + return NULL; +} + +static struct playlist_provider * +playlist_list_open_stream_suffix(struct input_stream *is, const char *suffix) +{ + struct playlist_provider *playlist; + + assert(is != NULL); + assert(suffix != NULL); + + for (unsigned i = 0; playlist_plugins[i] != NULL; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + + if (playlist_plugins_enabled[i] && + stringFoundInStringArray(plugin->suffixes, suffix)) { + playlist = playlist_plugin_open_stream(plugin, is); + if (playlist != NULL) + return playlist; + } + } + + return NULL; +} + +struct playlist_provider * +playlist_list_open_stream(struct input_stream *is, const char *uri) +{ + const char *suffix; + struct playlist_provider *playlist; + + if (is->mime != NULL) { + playlist = playlist_list_open_stream_mime(is); + if (playlist != NULL) + return playlist; + } + + suffix = uri != NULL ? uri_get_suffix(uri) : NULL; + if (suffix != NULL) { + playlist = playlist_list_open_stream_suffix(is, suffix); + if (playlist != NULL) + return playlist; + } + + return NULL; +} diff --git a/src/playlist_list.h b/src/playlist_list.h new file mode 100644 index 000000000..b5ac52a6f --- /dev/null +++ b/src/playlist_list.h @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_LIST_H +#define MPD_PLAYLIST_LIST_H + +struct playlist_provider; +struct input_stream; + +/** + * Initializes all playlist plugins. + */ +void +playlist_list_global_init(void); + +/** + * Deinitializes all playlist plugins. + */ +void +playlist_list_global_finish(void); + +/** + * Opens a playlist by its URI. + */ +struct playlist_provider * +playlist_list_open_uri(const char *uri); + +/** + * Opens a playlist from an input stream. + * + * @param is an #input_stream object which is open and ready + * @param uri optional URI which was used to open the stream; may be + * used to select the appropriate playlist plugin + */ +struct playlist_provider * +playlist_list_open_stream(struct input_stream *is, const char *uri); + +#endif diff --git a/src/playlist_plugin.h b/src/playlist_plugin.h new file mode 100644 index 000000000..3515af109 --- /dev/null +++ b/src/playlist_plugin.h @@ -0,0 +1,137 @@ +/* + * 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. + */ + +#ifndef MPD_PLAYLIST_PLUGIN_H +#define MPD_PLAYLIST_PLUGIN_H + +#include <stdbool.h> +#include <stddef.h> + +struct config_param; +struct input_stream; +struct tag; + +/** + * An object which provides the contents of a playlist. + */ +struct playlist_provider { + const struct playlist_plugin *plugin; +}; + +static inline void +playlist_provider_init(struct playlist_provider *playlist, + const struct playlist_plugin *plugin) +{ + playlist->plugin = plugin; +} + +struct playlist_plugin { + const char *name; + + /** + * Initialize the plugin. Optional method. + * + * @param param a configuration block for this plugin, or NULL + * if none is configured + * @return true if the plugin was initialized successfully, + * false if the plugin is not available + */ + bool (*init)(const struct config_param *param); + + /** + * Deinitialize a plugin which was initialized successfully. + * Optional method. + */ + void (*finish)(void); + + /** + * Opens the playlist on the specified URI. This URI has + * either matched one of the schemes or one of the suffixes. + */ + struct playlist_provider *(*open_uri)(const char *uri); + + /** + * Opens the playlist in the specified input stream. It has + * either matched one of the suffixes or one of the MIME + * types. + */ + struct playlist_provider *(*open_stream)(struct input_stream *is); + + void (*close)(struct playlist_provider *playlist); + + struct song *(*read)(struct playlist_provider *playlist); + + const char *const*schemes; + const char *const*suffixes; + const char *const*mime_types; +}; + +/** + * Initialize a plugin. + * + * @param param a configuration block for this plugin, or NULL if none + * is configured + * @return true if the plugin was initialized successfully, false if + * the plugin is not available + */ +static inline bool +playlist_plugin_init(const struct playlist_plugin *plugin, + const struct config_param *param) +{ + return plugin->init != NULL + ? plugin->init(param) + : true; +} + +/** + * Deinitialize a plugin which was initialized successfully. + */ +static inline void +playlist_plugin_finish(const struct playlist_plugin *plugin) +{ + if (plugin->finish != NULL) + plugin->finish(); +} + +static inline struct playlist_provider * +playlist_plugin_open_uri(const struct playlist_plugin *plugin, const char *uri) +{ + return plugin->open_uri(uri); +} + +static inline struct playlist_provider * +playlist_plugin_open_stream(const struct playlist_plugin *plugin, + struct input_stream *is) +{ + return plugin->open_stream(is); +} + +static inline void +playlist_plugin_close(struct playlist_provider *playlist) +{ + playlist->plugin->close(playlist); +} + +static inline struct song * +playlist_plugin_read(struct playlist_provider *playlist) +{ + return playlist->plugin->read(playlist); +} + +#endif diff --git a/src/playlist_print.c b/src/playlist_print.c index fd61ab62c..1ca11e4c1 100644 --- a/src/playlist_print.c +++ b/src/playlist_print.c @@ -69,7 +69,7 @@ playlist_print_id(struct client *client, const struct playlist *playlist, bool playlist_print_current(struct client *client, const struct playlist *playlist) { - int current_position = getPlaylistCurrentSong(playlist); + int current_position = playlist_get_current_song(playlist); if (current_position < 0) return false; diff --git a/src/playlist_queue.c b/src/playlist_queue.c new file mode 100644 index 000000000..f31eb9f42 --- /dev/null +++ b/src/playlist_queue.c @@ -0,0 +1,94 @@ +/* + * 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 "playlist_queue.h" +#include "playlist_list.h" +#include "playlist_plugin.h" +#include "song.h" +#include "uri.h" +#include "ls.h" +#include "input_stream.h" + +/** + * Determins if it's allowed to add this song to the playlist. For + * safety reasons, we disallow local files. + */ +static inline bool +accept_song(const struct song *song) +{ + return !song_is_file(song) && uri_has_scheme(song->uri) && + uri_supported_scheme(song->uri); +} + +enum playlist_result +playlist_load_into_queue(struct playlist_provider *source, + struct playlist *dest) +{ + enum playlist_result result; + struct song *song; + + while ((song = playlist_plugin_read(source)) != NULL) { + if (!accept_song(song)) { + song_free(song); + continue; + } + + result = playlist_append_song(dest, song, NULL); + if (result != PLAYLIST_RESULT_SUCCESS) { + song_free(song); + return result; + } + } + + return PLAYLIST_RESULT_SUCCESS; +} + +enum playlist_result +playlist_open_into_queue(const char *uri, struct playlist *dest) +{ + struct playlist_provider *playlist; + bool stream = false; + struct input_stream is; + enum playlist_result result; + + if (!uri_has_scheme(uri)) + /* don't allow local playlist files for now */ + return PLAYLIST_RESULT_NO_SUCH_LIST; + + playlist = playlist_list_open_uri(uri); + if (playlist == NULL) { + stream = input_stream_open(&is, uri); + if (!stream) + return PLAYLIST_RESULT_NO_SUCH_LIST; + + playlist = playlist_list_open_stream(&is, uri); + if (playlist == NULL) { + input_stream_close(&is); + return PLAYLIST_RESULT_NO_SUCH_LIST; + } + } + + result = playlist_load_into_queue(playlist, dest); + playlist_plugin_close(playlist); + + if (stream) + input_stream_close(&is); + + return result; +} diff --git a/src/playlist_queue.h b/src/playlist_queue.h new file mode 100644 index 000000000..b571cd63a --- /dev/null +++ b/src/playlist_queue.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +/*! \file + * \brief Glue between playlist plugin and the play queue + */ + +#ifndef MPD_PLAYLIST_QUEUE_H +#define MPD_PLAYLIST_QUEUE_H + +#include "playlist.h" + +struct playlist_provider; +struct playlist; + +/** + * Loads the contents of a playlist and append it to the specified + * play queue. + */ +enum playlist_result +playlist_load_into_queue(struct playlist_provider *source, + struct playlist *dest); + +/** + * Opens a playlist with a playlist plugin and append to the specified + * play queue. + */ +enum playlist_result +playlist_open_into_queue(const char *uri, struct playlist *dest); + +#endif + diff --git a/src/playlist_save.c b/src/playlist_save.c index 776d3c385..d56c0f324 100644 --- a/src/playlist_save.c +++ b/src/playlist_save.c @@ -54,7 +54,7 @@ playlist_print_uri(FILE *file, const char *uri) char *s; if (playlist_saveAbsolutePaths && !uri_has_scheme(uri) && - uri[0] != '/') + !g_path_is_absolute(uri)) s = map_uri_fs(uri); else s = utf8_to_fs_charset(uri); @@ -115,7 +115,7 @@ playlist_load_spl(struct playlist *playlist, const char *name_utf8) for (unsigned i = 0; i < list->len; ++i) { const char *temp = g_ptr_array_index(list, i); - if ((addToPlaylist(playlist, temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { + if ((playlist_append_uri(playlist, temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { /* for windows compatibility, convert slashes */ char *temp2 = g_strdup(temp); char *p = temp2; @@ -124,7 +124,7 @@ playlist_load_spl(struct playlist *playlist, const char *name_utf8) *p = '/'; p++; } - if ((addToPlaylist(playlist, temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { + if ((playlist_append_uri(playlist, temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { g_warning("can't add file \"%s\"", temp2); } g_free(temp2); diff --git a/src/playlist_state.c b/src/playlist_state.c index af0f7982b..b0cf961f7 100644 --- a/src/playlist_state.c +++ b/src/playlist_state.c @@ -51,10 +51,14 @@ void playlist_state_save(FILE *fp, const struct playlist *playlist) { + struct player_status player_status; + + pc_get_status(&player_status); + fprintf(fp, "%s", PLAYLIST_STATE_FILE_STATE); if (playlist->playing) { - switch (getPlayerState()) { + switch (player_status.state) { case PLAYER_STATE_PAUSE: fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_STATE_PAUSE); break; @@ -65,10 +69,16 @@ playlist_state_save(FILE *fp, const struct playlist *playlist) queue_order_to_position(&playlist->queue, playlist->current)); fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_TIME, - getPlayerElapsedTime()); - } else + (int)player_status.elapsed_time); + } else { fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_STATE_STOP); + if (playlist->current >= 0) + fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_CURRENT, + queue_order_to_position(&playlist->queue, + playlist->current)); + } + fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_RANDOM, playlist->queue.random); fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_REPEAT, @@ -78,7 +88,7 @@ playlist_state_save(FILE *fp, const struct playlist *playlist) fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_CONSUME, playlist->queue.consume); fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_CROSSFADE, - (int)(getPlayerCrossFade())); + (int)(pc_get_cross_fade())); fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_PLAYLIST_BEGIN); queue_save(fp, &playlist->queue); fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_PLAYLIST_END); @@ -109,8 +119,8 @@ playlist_state_load(FILE *fp, struct playlist *playlist, char *buffer) queue_increment_version(&playlist->queue); } -void -playlist_state_restore(FILE *fp, struct playlist *playlist) +bool +playlist_state_restore(const char *line, FILE *fp, struct playlist *playlist) { int current = -1; int seek_time = 0; @@ -118,50 +128,45 @@ playlist_state_restore(FILE *fp, struct playlist *playlist) char buffer[PLAYLIST_BUFFER_SIZE]; bool random_mode = false; + if (!g_str_has_prefix(line, PLAYLIST_STATE_FILE_STATE)) + return false; + + line += sizeof(PLAYLIST_STATE_FILE_STATE) - 1; + + if (strcmp(line, PLAYLIST_STATE_FILE_STATE_PLAY) == 0) + state = PLAYER_STATE_PLAY; + else if (strcmp(line, PLAYLIST_STATE_FILE_STATE_PAUSE) == 0) + state = PLAYER_STATE_PAUSE; + while (fgets(buffer, sizeof(buffer), fp)) { g_strchomp(buffer); - if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_STATE)) { - if (strcmp(&(buffer[strlen(PLAYLIST_STATE_FILE_STATE)]), - PLAYLIST_STATE_FILE_STATE_PLAY) == 0) { - state = PLAYER_STATE_PLAY; - } else - if (strcmp - (&(buffer[strlen(PLAYLIST_STATE_FILE_STATE)]), - PLAYLIST_STATE_FILE_STATE_PAUSE) - == 0) { - state = PLAYER_STATE_PAUSE; - } - } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_TIME)) { + if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_TIME)) { seek_time = atoi(&(buffer[strlen(PLAYLIST_STATE_FILE_TIME)])); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_REPEAT)) { if (strcmp (&(buffer[strlen(PLAYLIST_STATE_FILE_REPEAT)]), "1") == 0) { - setPlaylistRepeatStatus(playlist, true); + playlist_set_repeat(playlist, true); } else - setPlaylistRepeatStatus(playlist, false); + playlist_set_repeat(playlist, false); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_SINGLE)) { if (strcmp (&(buffer[strlen(PLAYLIST_STATE_FILE_SINGLE)]), "1") == 0) { - setPlaylistSingleStatus(playlist, true); + playlist_set_single(playlist, true); } else - setPlaylistSingleStatus(playlist, false); + playlist_set_single(playlist, false); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_CONSUME)) { if (strcmp (&(buffer[strlen(PLAYLIST_STATE_FILE_CONSUME)]), "1") == 0) { - setPlaylistConsumeStatus(playlist, true); + playlist_set_consume(playlist, true); } else - setPlaylistConsumeStatus(playlist, false); + playlist_set_consume(playlist, false); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_CROSSFADE)) { - setPlayerCrossFade(atoi - (& - (buffer - [strlen - (PLAYLIST_STATE_FILE_CROSSFADE)]))); + pc_set_cross_fade(atoi(buffer + strlen(PLAYLIST_STATE_FILE_CROSSFADE))); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_RANDOM)) { random_mode = strcmp(buffer + strlen(PLAYLIST_STATE_FILE_RANDOM), @@ -172,24 +177,56 @@ playlist_state_restore(FILE *fp, struct playlist *playlist) (PLAYLIST_STATE_FILE_CURRENT)])); } else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_PLAYLIST_BEGIN)) { - if (state == PLAYER_STATE_STOP) - current = -1; playlist_state_load(fp, playlist, buffer); } } - setPlaylistRandomStatus(playlist, random_mode); + playlist_set_random(playlist, random_mode); - if (state != PLAYER_STATE_STOP && !queue_is_empty(&playlist->queue)) { + if (!queue_is_empty(&playlist->queue)) { if (!queue_valid_position(&playlist->queue, current)) current = 0; - if (seek_time == 0) - playPlaylist(playlist, current); + /* enable all devices for the first time; this must be + called here, after the audio output states were + restored, before playback begins */ + if (state != PLAYER_STATE_STOP) + pc_update_audio(); + + if (state == PLAYER_STATE_STOP /* && config_option */) + playlist->current = current; + else if (seek_time == 0) + playlist_play(playlist, current); else - seekSongInPlaylist(playlist, current, seek_time); + playlist_seek_song(playlist, current, seek_time); if (state == PLAYER_STATE_PAUSE) - playerPause(); + pc_pause(); } + + return true; +} + +unsigned +playlist_state_get_hash(const struct playlist *playlist) +{ + struct player_status player_status; + + pc_get_status(&player_status); + + return playlist->queue.version ^ + (player_status.state != PLAYER_STATE_STOP + ? ((int)player_status.elapsed_time << 8) + : 0) ^ + (playlist->current >= 0 + ? (queue_order_to_position(&playlist->queue, + playlist->current) << 16) + : 0) ^ + ((int)pc_get_cross_fade() << 20) ^ + (player_status.state << 24) ^ + (playlist->queue.random << 27) ^ + (playlist->queue.repeat << 28) ^ + (playlist->queue.single << 29) ^ + (playlist->queue.consume << 30) ^ + (playlist->queue.random << 31); } diff --git a/src/playlist_state.h b/src/playlist_state.h index 989430264..d116aaeb1 100644 --- a/src/playlist_state.h +++ b/src/playlist_state.h @@ -25,6 +25,7 @@ #ifndef PLAYLIST_STATE_H #define PLAYLIST_STATE_H +#include <stdbool.h> #include <stdio.h> struct playlist; @@ -32,7 +33,16 @@ struct playlist; void playlist_state_save(FILE *fp, const struct playlist *playlist); -void -playlist_state_restore(FILE *fp, struct playlist *playlist); +bool +playlist_state_restore(const char *line, FILE *fp, struct playlist *playlist); + +/** + * Generates a hash number for the current state of the playlist and + * the playback options. This is used by timer_save_state_file() to + * determine whether the state has changed and the state file should + * be saved. + */ +unsigned +playlist_state_get_hash(const struct playlist *playlist); #endif diff --git a/src/queue.c b/src/queue.c index 141222a80..c43d1c137 100644 --- a/src/queue.c +++ b/src/queue.c @@ -33,7 +33,7 @@ queue_generate_id(const struct queue *queue) if (cur >= queue->max_length * QUEUE_HASH_MULT) cur = 0; - } while (queue->idToPosition[cur] != -1); + } while (queue->id_to_position[cur] != -1); return cur; } @@ -111,7 +111,7 @@ queue_append(struct queue *queue, struct song *song) }; queue->order[queue->length] = queue->length; - queue->idToPosition[id] = queue->length; + queue->id_to_position[id] = queue->length; ++queue->length; @@ -132,8 +132,8 @@ queue_swap(struct queue *queue, unsigned position1, unsigned position2) queue->items[position1].version = queue->version; queue->items[position2].version = queue->version; - queue->idToPosition[id1] = position2; - queue->idToPosition[id2] = position1; + queue->id_to_position[id1] = position2; + queue->id_to_position[id2] = position1; } static void @@ -143,7 +143,7 @@ queue_move_song_to(struct queue *queue, unsigned from, unsigned to) queue->items[to] = queue->items[from]; queue->items[to].version = queue->version; - queue->idToPosition[from_id] = to; + queue->id_to_position[from_id] = to; } void @@ -163,7 +163,7 @@ queue_move(struct queue *queue, unsigned from, unsigned to) /* put song at _to_ */ - queue->idToPosition[item.id] = to; + queue->id_to_position[item.id] = to; queue->items[to] = item; queue->items[to].version = queue->version; @@ -203,7 +203,7 @@ queue_move_range(struct queue *queue, unsigned start, unsigned end, unsigned to) // Copy the original block back in, starting at to. for (unsigned i = start; i< end; i++) { - queue->idToPosition[items[i-start].id] = to + i - start; + queue->id_to_position[items[i-start].id] = to + i - start; queue->items[to + i - start] = items[i-start]; queue->items[to + i - start].version = queue->version; } @@ -243,7 +243,7 @@ queue_delete(struct queue *queue, unsigned position) /* release the song id */ - queue->idToPosition[id] = -1; + queue->id_to_position[id] = -1; /* delete song from songs array */ @@ -271,7 +271,7 @@ queue_clear(struct queue *queue) if (!song_in_database(item->song)) song_free(item->song); - queue->idToPosition[item->id] = -1; + queue->id_to_position[item->id] = -1; } queue->length = 0; @@ -291,11 +291,11 @@ queue_init(struct queue *queue, unsigned max_length) queue->items = g_new(struct queue_item, max_length); queue->order = g_malloc(sizeof(queue->order[0]) * max_length); - queue->idToPosition = g_malloc(sizeof(queue->idToPosition[0]) * + queue->id_to_position = g_malloc(sizeof(queue->id_to_position[0]) * max_length * QUEUE_HASH_MULT); for (unsigned i = 0; i < max_length * QUEUE_HASH_MULT; ++i) - queue->idToPosition[i] = -1; + queue->id_to_position[i] = -1; queue->rand = g_rand_new(); } @@ -307,7 +307,7 @@ queue_finish(struct queue *queue) g_free(queue->items); g_free(queue->order); - g_free(queue->idToPosition); + g_free(queue->id_to_position); g_rand_free(queue->rand); } diff --git a/src/queue.h b/src/queue.h index 9c7228fd8..032a812c2 100644 --- a/src/queue.h +++ b/src/queue.h @@ -74,8 +74,8 @@ struct queue { /** map order numbers to positions */ unsigned *order; - /** map song ids to posiitons */ - int *idToPosition; + /** map song ids to positions */ + int *id_to_position; /** repeat playback when the end of the queue has been reached? */ @@ -146,10 +146,10 @@ queue_id_to_position(const struct queue *queue, unsigned id) if (id >= queue->max_length * QUEUE_HASH_MULT) return -1; - assert(queue->idToPosition[id] >= -1); - assert(queue->idToPosition[id] < (int)queue->length); + assert(queue->id_to_position[id] >= -1); + assert(queue->id_to_position[id] < (int)queue->length); - return queue->idToPosition[id]; + return queue->id_to_position[id]; } static inline int diff --git a/src/replay_gain.c b/src/replay_gain.c index bcb501e54..a59cfa245 100644 --- a/src/replay_gain.c +++ b/src/replay_gain.c @@ -26,6 +26,8 @@ #include "pcm_volume.h" #include <glib.h> + +#include <assert.h> #include <stdlib.h> #include <string.h> #include <math.h> @@ -38,19 +40,49 @@ static const char *const replay_gain_mode_names[] = { enum replay_gain_mode replay_gain_mode = REPLAY_GAIN_OFF; static float replay_gain_preamp = 1.0; +static float replay_gain_missing_preamp = 1.0; -void replay_gain_global_init(void) +const char * +replay_gain_get_mode_string(void) { - const struct config_param *param = config_get_param(CONF_REPLAYGAIN); + switch (replay_gain_mode) { + case REPLAY_GAIN_OFF: + return "off"; - if (!param) - return; + case REPLAY_GAIN_TRACK: + return "track"; + + case REPLAY_GAIN_ALBUM: + return "album"; + } + + /* unreachable */ + assert(false); + return "off"; +} + +bool +replay_gain_set_mode_string(const char *p) +{ + assert(p != NULL); - if (strcmp(param->value, "track") == 0) { + if (strcmp(p, "off") == 0) + replay_gain_mode = REPLAY_GAIN_OFF; + else if (strcmp(p, "track") == 0) replay_gain_mode = REPLAY_GAIN_TRACK; - } else if (strcmp(param->value, "album") == 0) { + else if (strcmp(p, "album") == 0) replay_gain_mode = REPLAY_GAIN_ALBUM; - } else { + else + return false; + + return true; +} + +void replay_gain_global_init(void) +{ + const struct config_param *param = config_get_param(CONF_REPLAYGAIN); + + if (param != NULL && !replay_gain_set_mode_string(param->value)) { g_error("replaygain value \"%s\" at line %i is invalid\n", param->value, param->line); } @@ -73,6 +105,25 @@ void replay_gain_global_init(void) replay_gain_preamp = pow(10, f / 20.0); } + + param = config_get_param(CONF_REPLAYGAIN_MISSING_PREAMP); + + if (param) { + char *test; + float f = strtod(param->value, &test); + + if (*test != '\0') { + g_error("Replaygain missing preamp \"%s\" is not a number at " + "line %i\n", param->value, param->line); + } + + if (f < -15 || f > 15) { + g_error("Replaygain missing preamp \"%s\" is not between -15 and" + "15 at line %i\n", param->value, param->line); + } + + replay_gain_missing_preamp = pow(10, f / 20.0); + } } static float calc_replay_gain_scale(float gain, float peak) @@ -116,19 +167,28 @@ void replay_gain_apply(struct replay_gain_info *info, char *buffer, int size, const struct audio_format *format) { - if (replay_gain_mode == REPLAY_GAIN_OFF || !info) + float scale; + + if (replay_gain_mode == REPLAY_GAIN_OFF) return; - if (info->scale < 0) { - const struct replay_gain_tuple *tuple = - &info->tuples[replay_gain_mode]; + if (info) { + if (info->scale < 0) { + const struct replay_gain_tuple *tuple = + &info->tuples[replay_gain_mode]; - g_debug("computing ReplayGain %s scale with gain %f, peak %f\n", - replay_gain_mode_names[replay_gain_mode], - tuple->gain, tuple->peak); + g_debug("computing ReplayGain %s scale with gain %f, peak %f\n", + replay_gain_mode_names[replay_gain_mode], + tuple->gain, tuple->peak); - info->scale = calc_replay_gain_scale(tuple->gain, tuple->peak); + info->scale = calc_replay_gain_scale(tuple->gain, tuple->peak); + } + scale = info->scale; + } + else { + scale = replay_gain_missing_preamp; + g_debug("ReplayGain is missing, computing scale %f\n", scale); } - pcm_volume(buffer, size, format, pcm_float_to_volume(info->scale)); + pcm_volume(buffer, size, format, pcm_float_to_volume(scale)); } diff --git a/src/replay_gain.h b/src/replay_gain.h index aa48f3f14..5d0492ad8 100644 --- a/src/replay_gain.h +++ b/src/replay_gain.h @@ -23,6 +23,8 @@ #ifndef MPD_REPLAY_GAIN_H #define MPD_REPLAY_GAIN_H +#include <stdbool.h> + enum replay_gain_mode { REPLAY_GAIN_OFF = -1, REPLAY_GAIN_ALBUM, @@ -52,6 +54,20 @@ void replay_gain_info_free(struct replay_gain_info *info); void replay_gain_global_init(void); +/** + * Returns the current replay gain mode as a machine-readable string. + */ +const char * +replay_gain_get_mode_string(void); + +/** + * Sets the replay gain mode, parsed from a string. + * + * @return true on success, false if the string could not be parsed + */ +bool +replay_gain_set_mode_string(const char *p); + void replay_gain_apply(struct replay_gain_info *info, char *buffer, int bufferSize, const struct audio_format *format); diff --git a/src/song.c b/src/song.c index 76c25f44f..942beb5d7 100644 --- a/src/song.c +++ b/src/song.c @@ -20,34 +20,25 @@ #include "song.h" #include "uri.h" #include "directory.h" -#include "mapper.h" -#include "decoder_list.h" -#include "decoder_plugin.h" -#include "tag_ape.h" -#include "tag_id3.h" #include "tag.h" #include <glib.h> #include <assert.h> -#include <sys/types.h> -#include <sys/stat.h> -#include <unistd.h> -#include <string.h> static struct song * -song_alloc(const char *url, struct directory *parent) +song_alloc(const char *uri, struct directory *parent) { - size_t urllen; + size_t uri_length; struct song *song; - assert(url); - urllen = strlen(url); - assert(urllen); - song = g_malloc(sizeof(*song) - sizeof(song->url) + urllen + 1); + assert(uri); + uri_length = strlen(uri); + assert(uri_length); + song = g_malloc(sizeof(*song) - sizeof(song->uri) + uri_length + 1); song->tag = NULL; - memcpy(song->url, url, urllen + 1); + memcpy(song->uri, uri, uri_length + 1); song->parent = parent; song->mtime = 0; @@ -55,9 +46,9 @@ song_alloc(const char *url, struct directory *parent) } struct song * -song_remote_new(const char *url) +song_remote_new(const char *uri) { - return song_alloc(url, NULL); + return song_alloc(uri, NULL); } struct song * @@ -68,32 +59,6 @@ song_file_new(const char *path, struct directory *parent) return song_alloc(path, parent); } -struct song * -song_file_load(const char *path, struct directory *parent) -{ - struct song *song; - bool ret; - - assert((parent == NULL) == (*path == '/')); - assert(!uri_has_scheme(path)); - assert(strchr(path, '\n') == NULL); - - song = song_file_new(path, parent); - - //in archive ? - if (parent != NULL && parent->device == DEVICE_INARCHIVE) { - ret = song_file_update_inarchive(song); - } else { - ret = song_file_update(song); - } - if (!ret) { - song_free(song); - return NULL; - } - - return song; -} - void song_free(struct song *song) { @@ -102,127 +67,15 @@ song_free(struct song *song) g_free(song); } -/** - * Attempts to load APE or ID3 tags from the specified file. - */ -static struct tag * -tag_load_fallback(const char *path) -{ - struct tag *tag = tag_ape_load(path); - if (tag == NULL) - tag = tag_id3_load(path); - return tag; -} - -/** - * The decoder plugin failed to load any tags: fall back to the APE or - * ID3 tag loader. - */ -static struct tag * -tag_fallback(const char *path, struct tag *tag) -{ - struct tag *fallback = tag_load_fallback(path); - - if (fallback != NULL) { - /* tag was successfully loaded: copy the song - duration, and destroy the old (empty) tag */ - fallback->time = tag->time; - tag_free(tag); - return fallback; - } else - /* no APE/ID3 tag found: return the empty tag */ - return tag; -} - -bool -song_file_update(struct song *song) -{ - const char *suffix; - char *path_fs; - const struct decoder_plugin *plugin; - struct stat st; - - assert(song_is_file(song)); - - /* check if there's a suffix and a plugin */ - - suffix = uri_get_suffix(song->url); - if (suffix == NULL) - return false; - - plugin = decoder_plugin_from_suffix(suffix, false); - if (plugin == NULL) - return false; - - path_fs = map_song_fs(song); - if (path_fs == NULL) - return false; - - if (song->tag != NULL) { - tag_free(song->tag); - song->tag = NULL; - } - - if (stat(path_fs, &st) < 0 || !S_ISREG(st.st_mode)) { - g_free(path_fs); - return false; - } - - song->mtime = st.st_mtime; - - do { - song->tag = plugin->tag_dup(path_fs); - if (song->tag != NULL) - break; - - plugin = decoder_plugin_from_suffix(suffix, true); - } while (plugin != NULL); - - if (song->tag != NULL && tag_is_empty(song->tag)) - song->tag = tag_fallback(path_fs, song->tag); - - g_free(path_fs); - return song->tag != NULL; -} - -bool -song_file_update_inarchive(struct song *song) -{ - const char *suffix; - const struct decoder_plugin *plugin; - - assert(song_is_file(song)); - - /* check if there's a suffix and a plugin */ - - suffix = uri_get_suffix(song->url); - if (suffix == NULL) - return false; - - plugin = decoder_plugin_from_suffix(suffix, false); - if (plugin == NULL) - return false; - - if (song->tag != NULL) - tag_free(song->tag); - - //accept every file that has music suffix - //because we dont support tag reading throught - //input streams - song->tag = tag_new(); - - return true; -} - char * song_get_uri(const struct song *song) { assert(song != NULL); - assert(*song->url); + assert(*song->uri); if (!song_in_database(song) || directory_is_root(song->parent)) - return g_strdup(song->url); + return g_strdup(song->uri); else return g_strconcat(directory_get_path(song->parent), - "/", song->url, NULL); + "/", song->uri, NULL); } diff --git a/src/song.h b/src/song.h index 3044e910f..bfa33e23d 100644 --- a/src/song.h +++ b/src/song.h @@ -34,12 +34,12 @@ struct song { struct tag *tag; struct directory *parent; time_t mtime; - char url[sizeof(int)]; + char uri[sizeof(int)]; }; /** allocate a new song with a remote URL */ struct song * -song_remote_new(const char *url); +song_remote_new(const char *uri); /** allocate a new song with a local file name */ struct song * @@ -81,7 +81,7 @@ song_in_database(const struct song *song) static inline bool song_is_file(const struct song *song) { - return song_in_database(song) || song->url[0] == '/'; + return song_in_database(song) || song->uri[0] == '/'; } #endif diff --git a/src/song_print.c b/src/song_print.c index 64ab9f6b1..3420b0d34 100644 --- a/src/song_print.c +++ b/src/song_print.c @@ -26,18 +26,18 @@ #include "uri.h" void -song_print_url(struct client *client, struct song *song) +song_print_uri(struct client *client, struct song *song) { if (song_in_database(song) && !directory_is_root(song->parent)) { client_printf(client, "%s%s/%s\n", SONG_FILE, - directory_get_path(song->parent), song->url); + directory_get_path(song->parent), song->uri); } else { char *allocated; const char *uri; - uri = allocated = uri_remove_auth(song->url); + uri = allocated = uri_remove_auth(song->uri); if (uri == NULL) - uri = song->url; + uri = song->uri; client_printf(client, "%s%s\n", SONG_FILE, uri); @@ -48,7 +48,28 @@ song_print_url(struct client *client, struct song *song) int song_print_info(struct client *client, struct song *song) { - song_print_url(client, song); + song_print_uri(client, song); + + if (song->mtime > 0) { +#ifndef G_OS_WIN32 + struct tm tm; +#endif + const struct tm *tm2; + +#ifdef G_OS_WIN32 + tm2 = gmtime(&song->mtime); +#else + tm2 = gmtime_r(&song->mtime, &tm); +#endif + + if (tm2 != NULL) { + char timestamp[32]; + + strftime(timestamp, sizeof(timestamp), "%FT%TZ", tm2); + client_printf(client, "Last-Modified: %s\n", + timestamp); + } + } if (song->tag) tag_print(client, song->tag); diff --git a/src/song_print.h b/src/song_print.h index 291fd81c8..c16bc7387 100644 --- a/src/song_print.h +++ b/src/song_print.h @@ -30,6 +30,6 @@ song_print_info(struct client *client, struct song *song); int songvec_print(struct client *client, const struct songvec *sv); void -song_print_url(struct client *client, struct song *song); +song_print_uri(struct client *client, struct song *song); #endif diff --git a/src/song_save.c b/src/song_save.c index 8f4e1614d..b18057201 100644 --- a/src/song_save.c +++ b/src/song_save.c @@ -34,15 +34,20 @@ #define SONG_KEY "key: " #define SONG_MTIME "mtime: " +static GQuark +song_save_quark(void) +{ + return g_quark_from_static_string("song_save"); +} + static void -song_save_url(FILE *fp, struct song *song) +song_save_uri(FILE *fp, struct song *song) { if (song->parent != NULL && song->parent->path != NULL) fprintf(fp, SONG_FILE "%s/%s\n", - directory_get_path(song->parent), song->url); + directory_get_path(song->parent), song->uri); else - fprintf(fp, SONG_FILE "%s\n", - song->url); + fprintf(fp, SONG_FILE "%s\n", song->uri); } static int @@ -50,9 +55,9 @@ song_save(struct song *song, void *data) { FILE *fp = data; - fprintf(fp, SONG_KEY "%s\n", song->url); + fprintf(fp, SONG_KEY "%s\n", song->uri); - song_save_url(fp, song); + song_save_uri(fp, song); if (song->tag != NULL) tag_save(fp, song->tag); @@ -70,9 +75,9 @@ void songvec_save(FILE *fp, struct songvec *sv) } static void -insertSongIntoList(struct songvec *sv, struct song *newsong) +commit_song(struct songvec *sv, struct song *newsong) { - struct song *existing = songvec_find(sv, newsong->url); + struct song *existing = songvec_find(sv, newsong->uri); if (!existing) { songvec_add(sv, newsong); @@ -93,7 +98,7 @@ insertSongIntoList(struct songvec *sv, struct song *newsong) } static char * -matchesAnMpdTagItemKey(char *buffer, enum tag_type *itemType) +parse_tag_value(char *buffer, enum tag_type *type_r) { int i; @@ -102,7 +107,7 @@ matchesAnMpdTagItemKey(char *buffer, enum tag_type *itemType) if (0 == strncmp(tag_item_names[i], buffer, len) && buffer[len] == ':') { - *itemType = i; + *type_r = i; return g_strchug(buffer + len + 1); } } @@ -110,12 +115,13 @@ matchesAnMpdTagItemKey(char *buffer, enum tag_type *itemType) return NULL; } -void readSongInfoIntoList(FILE *fp, struct songvec *sv, - struct directory *parent) +bool +songvec_load(FILE *fp, struct songvec *sv, struct directory *parent, + GError **error_r) { char buffer[MPD_PATH_MAX + 1024]; struct song *song = NULL; - enum tag_type itemType; + enum tag_type type; const char *value; while (fgets(buffer, sizeof(buffer), fp) && @@ -124,24 +130,26 @@ void readSongInfoIntoList(FILE *fp, struct songvec *sv, if (0 == strncmp(SONG_KEY, buffer, strlen(SONG_KEY))) { if (song) - insertSongIntoList(sv, song); + commit_song(sv, song); song = song_file_new(buffer + strlen(SONG_KEY), parent); } else if (*buffer == 0) { /* ignore empty lines (starting with '\0') */ } else if (song == NULL) { - g_error("Problems reading song info"); + g_set_error(error_r, song_save_quark(), 0, + "Problems reading song info"); + return false; } else if (0 == strncmp(SONG_FILE, buffer, strlen(SONG_FILE))) { /* we don't need this info anymore */ - } else if ((value = matchesAnMpdTagItemKey(buffer, - &itemType)) != NULL) { + } else if ((value = parse_tag_value(buffer, + &type)) != NULL) { if (!song->tag) { song->tag = tag_new(); tag_begin_add(song->tag); } - tag_add_item(song->tag, itemType, value); + tag_add_item(song->tag, type, value); } else if (0 == strncmp(SONG_TIME, buffer, strlen(SONG_TIME))) { if (!song->tag) { song->tag = tag_new(); @@ -151,11 +159,15 @@ void readSongInfoIntoList(FILE *fp, struct songvec *sv, song->tag->time = atoi(&(buffer[strlen(SONG_TIME)])); } else if (0 == strncmp(SONG_MTIME, buffer, strlen(SONG_MTIME))) { song->mtime = atoi(&(buffer[strlen(SONG_MTIME)])); + } else { + g_set_error(error_r, song_save_quark(), 0, + "unknown line in db: %s", buffer); + return false; } - else - g_error("unknown line in db: %s", buffer); } if (song) - insertSongIntoList(sv, song); + commit_song(sv, song); + + return true; } diff --git a/src/song_save.h b/src/song_save.h index 370e42730..36e03584e 100644 --- a/src/song_save.h +++ b/src/song_save.h @@ -20,6 +20,9 @@ #ifndef MPD_SONG_SAVE_H #define MPD_SONG_SAVE_H +#include <glib.h> + +#include <stdbool.h> #include <stdio.h> struct songvec; @@ -27,7 +30,16 @@ struct directory; void songvec_save(FILE *fp, struct songvec *sv); -void readSongInfoIntoList(FILE * fp, struct songvec *sv, - struct directory *parent); +/** + * Loads songs from the input file and add the to the specified + * directory. + * + * @param error_r location to store the error occuring, or NULL to + * ignore errors + * @return true on success, false on error + */ +bool +songvec_load(FILE *file, struct songvec *sv, struct directory *parent, + GError **error_r); #endif diff --git a/src/song_update.c b/src/song_update.c new file mode 100644 index 000000000..cfef5861f --- /dev/null +++ b/src/song_update.c @@ -0,0 +1,172 @@ +/* + * 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 "song.h" +#include "uri.h" +#include "directory.h" +#include "mapper.h" +#include "decoder_list.h" +#include "decoder_plugin.h" +#include "tag_ape.h" +#include "tag_id3.h" +#include "tag.h" + +#include <glib.h> + +#include <assert.h> +#include <sys/types.h> +#include <sys/stat.h> + +struct song * +song_file_load(const char *path, struct directory *parent) +{ + struct song *song; + bool ret; + + assert((parent == NULL) == g_path_is_absolute(path)); + assert(!uri_has_scheme(path)); + assert(strchr(path, '\n') == NULL); + + song = song_file_new(path, parent); + + //in archive ? + if (parent != NULL && parent->device == DEVICE_INARCHIVE) { + ret = song_file_update_inarchive(song); + } else { + ret = song_file_update(song); + } + if (!ret) { + song_free(song); + return NULL; + } + + return song; +} + +/** + * Attempts to load APE or ID3 tags from the specified file. + */ +static struct tag * +tag_load_fallback(const char *path) +{ + struct tag *tag = tag_ape_load(path); + if (tag == NULL) + tag = tag_id3_load(path); + return tag; +} + +/** + * The decoder plugin failed to load any tags: fall back to the APE or + * ID3 tag loader. + */ +static struct tag * +tag_fallback(const char *path, struct tag *tag) +{ + struct tag *fallback = tag_load_fallback(path); + + if (fallback != NULL) { + /* tag was successfully loaded: copy the song + duration, and destroy the old (empty) tag */ + fallback->time = tag->time; + tag_free(tag); + return fallback; + } else + /* no APE/ID3 tag found: return the empty tag */ + return tag; +} + +bool +song_file_update(struct song *song) +{ + const char *suffix; + char *path_fs; + const struct decoder_plugin *plugin; + struct stat st; + + assert(song_is_file(song)); + + /* check if there's a suffix and a plugin */ + + suffix = uri_get_suffix(song->uri); + if (suffix == NULL) + return false; + + plugin = decoder_plugin_from_suffix(suffix, false); + if (plugin == NULL) + return false; + + path_fs = map_song_fs(song); + if (path_fs == NULL) + return false; + + if (song->tag != NULL) { + tag_free(song->tag); + song->tag = NULL; + } + + if (stat(path_fs, &st) < 0 || !S_ISREG(st.st_mode)) { + g_free(path_fs); + return false; + } + + song->mtime = st.st_mtime; + + do { + song->tag = plugin->tag_dup(path_fs); + if (song->tag != NULL) + break; + + plugin = decoder_plugin_from_suffix(suffix, true); + } while (plugin != NULL); + + if (song->tag != NULL && tag_is_empty(song->tag)) + song->tag = tag_fallback(path_fs, song->tag); + + g_free(path_fs); + return song->tag != NULL; +} + +bool +song_file_update_inarchive(struct song *song) +{ + const char *suffix; + const struct decoder_plugin *plugin; + + assert(song_is_file(song)); + + /* check if there's a suffix and a plugin */ + + suffix = uri_get_suffix(song->uri); + if (suffix == NULL) + return false; + + plugin = decoder_plugin_from_suffix(suffix, false); + if (plugin == NULL) + return false; + + if (song->tag != NULL) + tag_free(song->tag); + + //accept every file that has music suffix + //because we dont support tag reading throught + //input streams + song->tag = tag_new(); + + return true; +} diff --git a/src/songvec.c b/src/songvec.c index efef02216..f8f83e6e8 100644 --- a/src/songvec.c +++ b/src/songvec.c @@ -29,6 +29,38 @@ static GMutex *nr_lock = NULL; +static const char * +tag_get_value_checked(const struct tag *tag, enum tag_type type) +{ + return tag != NULL + ? tag_get_value(tag, type) + : NULL; +} + +static int +compare_utf8_string(const char *a, const char *b) +{ + if (a == NULL) + return b == NULL ? 0 : -1; + + if (b == NULL) + return 1; + + return g_utf8_collate(a, b); +} + +/** + * Compare two string tag values, ignoring case. Either one may be + * NULL. + */ +static int +compare_string_tag_item(const struct tag *a, const struct tag *b, + enum tag_type type) +{ + return compare_utf8_string(tag_get_value_checked(a, type), + tag_get_value_checked(b, type)); +} + /** * Compare two tag values which should contain an integer value * (e.g. disc or track number). Either one may be NULL. @@ -51,14 +83,8 @@ compare_number_string(const char *a, const char *b) static int compare_tag_item(const struct tag *a, const struct tag *b, enum tag_type type) { - if (a == NULL) - return b == NULL ? 0 : -1; - - if (b == NULL) - return 1; - - return compare_number_string(tag_get_value(a, type), - tag_get_value(b, type)); + return compare_number_string(tag_get_value_checked(a, type), + tag_get_value_checked(b, type)); } /* Only used for sorting/searchin a songvec, not general purpose compares */ @@ -68,18 +94,23 @@ static int songvec_cmp(const void *s1, const void *s2) const struct song *b = ((const struct song * const *)s2)[0]; int ret; - /* first sort by disc */ - ret = compare_tag_item(a->tag, b->tag, TAG_ITEM_DISC); + /* first sort by album */ + ret = compare_string_tag_item(a->tag, b->tag, TAG_ALBUM); + if (ret != 0) + return ret; + + /* then sort by disc */ + ret = compare_tag_item(a->tag, b->tag, TAG_DISC); if (ret != 0) return ret; /* then by track number */ - ret = compare_tag_item(a->tag, b->tag, TAG_ITEM_TRACK); + ret = compare_tag_item(a->tag, b->tag, TAG_TRACK); if (ret != 0) return ret; /* still no difference? compare file name */ - return g_utf8_collate(a->url, b->url); + return g_utf8_collate(a->uri, b->uri); } static size_t sv_size(const struct songvec *sv) @@ -108,14 +139,14 @@ void songvec_sort(struct songvec *sv) } struct song * -songvec_find(const struct songvec *sv, const char *url) +songvec_find(const struct songvec *sv, const char *uri) { int i; struct song *ret = NULL; g_mutex_lock(nr_lock); for (i = sv->nr; --i >= 0; ) { - if (strcmp(sv->base[i]->url, url)) + if (strcmp(sv->base[i]->uri, uri)) continue; ret = sv->base[i]; break; @@ -182,7 +213,7 @@ songvec_for_each(const struct songvec *sv, struct song *song = sv->base[i]; assert(song); - assert(*song->url); + assert(*song->uri); prev_nr = sv->nr; g_mutex_unlock(nr_lock); /* fn() may block */ diff --git a/src/songvec.h b/src/songvec.h index 0fd207ed0..1d07dd6e9 100644 --- a/src/songvec.h +++ b/src/songvec.h @@ -34,7 +34,7 @@ void songvec_deinit(void); void songvec_sort(struct songvec *sv); struct song * -songvec_find(const struct songvec *sv, const char *url); +songvec_find(const struct songvec *sv, const char *uri); int songvec_delete(struct songvec *sv, const struct song *del); diff --git a/src/state_file.c b/src/state_file.c index 9c6475cc8..aa66720c1 100644 --- a/src/state_file.c +++ b/src/state_file.c @@ -20,6 +20,7 @@ #include "state_file.h" #include "output_state.h" #include "playlist.h" +#include "playlist_state.h" #include "volume.h" #include <glib.h> @@ -30,28 +31,26 @@ #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "state_file" -static struct _sf_cb { - void (*reader)(FILE *); - void (*writer)(FILE *); -} sf_callbacks [] = { - { read_sw_volume_state, save_sw_volume_state }, - { readAudioDevicesState, saveAudioDevicesState }, - { readPlaylistState, savePlaylistState }, -}; - static char *state_file_path; /** the GLib source id for the save timer */ static guint save_state_source_id; +/** + * These version numbers determine whether we need to save the state + * file. If nothing has changed, we won't let the hard drive spin up. + */ +static unsigned prev_volume_version, prev_output_version, + prev_playlist_version; + static void state_file_write(void) { - unsigned int i; FILE *fp; - if (state_file_path == NULL) - return; + assert(state_file_path != NULL); + + g_debug("Saving state file %s", state_file_path); fp = fopen(state_file_path, "w"); if (G_UNLIKELY(!fp)) { @@ -60,21 +59,27 @@ state_file_write(void) return; } - for (i = 0; i < G_N_ELEMENTS(sf_callbacks); i++) - sf_callbacks[i].writer(fp); + save_sw_volume_state(fp); + audio_output_state_save(fp); + playlist_state_save(fp, &g_playlist); while(fclose(fp) && errno == EINTR) /* nothing */; + + prev_volume_version = sw_volume_state_get_hash(); + prev_output_version = audio_output_state_get_version(); + prev_playlist_version = playlist_state_get_hash(&g_playlist); } static void state_file_read(void) { - unsigned int i; FILE *fp; + char line[1024]; + bool success; assert(state_file_path != NULL); - g_debug("Saving state file"); + g_debug("Loading state file %s", state_file_path); fp = fopen(state_file_path, "r"); if (G_UNLIKELY(!fp)) { @@ -82,12 +87,22 @@ state_file_read(void) state_file_path, strerror(errno)); return; } - for (i = 0; i < G_N_ELEMENTS(sf_callbacks); i++) { - sf_callbacks[i].reader(fp); - rewind(fp); + + while (fgets(line, sizeof(line), fp) != NULL) { + g_strchomp(line); + + success = read_sw_volume_state(line) || + audio_output_state_read(line) || + playlist_state_restore(line, fp, &g_playlist); + if (!success) + g_warning("Unrecognized line in state file: %s", line); } while(fclose(fp) && errno == EINTR) /* nothing */; + + prev_volume_version = sw_volume_state_get_hash(); + prev_output_version = audio_output_state_get_version(); + prev_playlist_version = playlist_state_get_hash(&g_playlist); } /** @@ -97,6 +112,13 @@ state_file_read(void) static gboolean timer_save_state_file(G_GNUC_UNUSED gpointer data) { + if (prev_volume_version == sw_volume_state_get_hash() && + prev_output_version == audio_output_state_get_version() && + prev_playlist_version == playlist_state_get_hash(&g_playlist)) + /* nothing has changed - don't save the state file, + don't spin up the hard disk */ + return true; + state_file_write(); return true; } @@ -112,18 +134,22 @@ state_file_init(const char *path) state_file_path = g_strdup(path); state_file_read(); - save_state_source_id = g_timeout_add(5 * 60 * 1000, - timer_save_state_file, NULL); + save_state_source_id = g_timeout_add_seconds(5 * 60, + timer_save_state_file, + NULL); } void state_file_finish(void) { + if (state_file_path == NULL) + /* no state file configured, no cleanup required */ + return; + if (save_state_source_id != 0) g_source_remove(save_state_source_id); - if (state_file_path != NULL) - state_file_write(); + state_file_write(); g_free(state_file_path); } diff --git a/src/stats.c b/src/stats.c index 01f6761f3..303d23c8e 100644 --- a/src/stats.c +++ b/src/stats.c @@ -52,11 +52,11 @@ visit_tag(struct visit_data *data, const struct tag *tag) const struct tag_item *item = tag->items[i]; switch (item->type) { - case TAG_ITEM_ARTIST: + case TAG_ARTIST: strset_add(data->artists, item->value); break; - case TAG_ITEM_ALBUM: + case TAG_ALBUM: strset_add(data->albums, item->value); break; @@ -113,7 +113,7 @@ int stats_print(struct client *client) stats.album_count, stats.song_count, (long)g_timer_elapsed(stats.timer, NULL), - (long)(getPlayerTotalPlayTime() + 0.5), + (long)(pc_get_total_play_time() + 0.5), stats.song_duration, db_get_mtime()); return 0; diff --git a/src/sticker.c b/src/sticker.c index 0d30fbb70..4cccd9399 100644 --- a/src/sticker.c +++ b/src/sticker.c @@ -72,50 +72,69 @@ static const char sticker_sql_create[] = static sqlite3 *sticker_db; static sqlite3_stmt *sticker_stmt[G_N_ELEMENTS(sticker_sql)]; +static GQuark +sticker_quark(void) +{ + return g_quark_from_static_string("sticker"); +} + static sqlite3_stmt * -sticker_prepare(const char *sql) +sticker_prepare(const char *sql, GError **error_r) { int ret; sqlite3_stmt *stmt; ret = sqlite3_prepare_v2(sticker_db, sql, -1, &stmt, NULL); - if (ret != SQLITE_OK) - g_error("sqlite3_prepare_v2() failed: %s", - sqlite3_errmsg(sticker_db)); + if (ret != SQLITE_OK) { + g_set_error(error_r, sticker_quark(), ret, + "sqlite3_prepare_v2() failed: %s", + sqlite3_errmsg(sticker_db)); + return NULL; + } return stmt; } -void -sticker_global_init(const char *path) +bool +sticker_global_init(const char *path, GError **error_r) { int ret; if (path == NULL) /* not configured */ - return; + return true; /* open/create the sqlite database */ ret = sqlite3_open(path, &sticker_db); - if (ret != SQLITE_OK) - g_error("Failed to open sqlite database '%s': %s", - path, sqlite3_errmsg(sticker_db)); + if (ret != SQLITE_OK) { + g_set_error(error_r, sticker_quark(), ret, + "Failed to open sqlite database '%s': %s", + path, sqlite3_errmsg(sticker_db)); + return false; + } /* create the table and index */ ret = sqlite3_exec(sticker_db, sticker_sql_create, NULL, NULL, NULL); - if (ret != SQLITE_OK) - g_error("Failed to create sticker table: %s", - sqlite3_errmsg(sticker_db)); + if (ret != SQLITE_OK) { + g_set_error(error_r, sticker_quark(), ret, + "Failed to create sticker table: %s", + sqlite3_errmsg(sticker_db)); + return false; + } /* prepare the statements we're going to use */ for (unsigned i = 0; i < G_N_ELEMENTS(sticker_sql); ++i) { assert(sticker_sql[i] != NULL); - sticker_stmt[i] = sticker_prepare(sticker_sql[i]); + sticker_stmt[i] = sticker_prepare(sticker_sql[i], error_r); + if (sticker_stmt[i] == NULL) + return false; } + + return true; } void diff --git a/src/sticker.h b/src/sticker.h index 8e6410914..30d85fa18 100644 --- a/src/sticker.h +++ b/src/sticker.h @@ -50,9 +50,13 @@ struct sticker; /** * Opens the sticker database (if path is not NULL). + * + * @param error_r location to store the error occuring, or NULL to + * ignore errors + * @return true on success, false on error */ -void -sticker_global_init(const char *path); +bool +sticker_global_init(const char *path, GError **error_r); /** * Close the sticker database. @@ -42,18 +42,20 @@ static struct { } bulk; const char *tag_item_names[TAG_NUM_OF_ITEM_TYPES] = { - "Artist", - "Album", - "AlbumArtist", - "Title", - "Track", - "Name", - "Genre", - "Date", - "Composer", - "Performer", - "Comment", - "Disc", + [TAG_ARTIST] = "Artist", + [TAG_ARTIST_SORT] = "ArtistSort", + [TAG_ALBUM] = "Album", + [TAG_ALBUM_ARTIST] = "AlbumArtist", + [TAG_ALBUM_ARTIST_SORT] = "AlbumArtistSort", + [TAG_TITLE] = "Title", + [TAG_TRACK] = "Track", + [TAG_NAME] = "Name", + [TAG_GENRE] = "Genre", + [TAG_DATE] = "Date", + [TAG_COMPOSER] = "Composer", + [TAG_PERFORMER] = "Performer", + [TAG_COMMENT] = "Comment", + [TAG_DISC] = "Disc", /* MusicBrainz tags from http://musicbrainz.org/doc/MusicBrainzTag */ [TAG_MUSICBRAINZ_ARTISTID] = "MUSICBRAINZ_ARTISTID", @@ -81,7 +83,7 @@ void tag_lib_init(void) /* parse the "metadata_to_use" config parameter below */ /* ignore comments by default */ - ignore_tag_items[TAG_ITEM_COMMENT] = true; + ignore_tag_items[TAG_COMMENT] = true; value = config_get_string(CONF_METADATA_TO_USE, NULL); if (value == NULL) @@ -31,18 +31,20 @@ * Codes for the type of a tag item. */ enum tag_type { - TAG_ITEM_ARTIST, - TAG_ITEM_ALBUM, - TAG_ITEM_ALBUM_ARTIST, - TAG_ITEM_TITLE, - TAG_ITEM_TRACK, - TAG_ITEM_NAME, - TAG_ITEM_GENRE, - TAG_ITEM_DATE, - TAG_ITEM_COMPOSER, - TAG_ITEM_PERFORMER, - TAG_ITEM_COMMENT, - TAG_ITEM_DISC, + TAG_ARTIST, + TAG_ARTIST_SORT, + TAG_ALBUM, + TAG_ALBUM_ARTIST, + TAG_ALBUM_ARTIST_SORT, + TAG_TITLE, + TAG_TRACK, + TAG_NAME, + TAG_GENRE, + TAG_DATE, + TAG_COMPOSER, + TAG_PERFORMER, + TAG_COMMENT, + TAG_DISC, TAG_MUSICBRAINZ_ARTISTID, TAG_MUSICBRAINZ_ALBUMID, diff --git a/src/tag_ape.c b/src/tag_ape.c index 7cbf32208..fb210ca46 100644 --- a/src/tag_ape.c +++ b/src/tag_ape.c @@ -25,6 +25,36 @@ #include <assert.h> #include <stdio.h> +static const char *const ape_tag_names[] = { + [TAG_TITLE] = "title", + [TAG_ARTIST] = "artist", + [TAG_ALBUM] = "album", + [TAG_COMMENT] = "comment", + [TAG_GENRE] = "genre", + [TAG_TRACK] = "track", + [TAG_DATE] = "year" +}; + +static struct tag * +tag_ape_import_item(struct tag *tag, unsigned long flags, + const char *key, const char *value, size_t value_length) +{ + /* we only care about utf-8 text tags */ + if ((flags & (0x3 << 1)) != 0) + return tag; + + for (unsigned i = 0; i < G_N_ELEMENTS(ape_tag_names); i++) { + if (ape_tag_names[i] != NULL && + g_ascii_strcasecmp(key, ape_tag_names[i]) == 0) { + if (tag == NULL) + tag = tag_new(); + tag_add_item_n(tag, i, value, value_length); + } + } + + return tag; +} + struct tag * tag_ape_load(const char *file) { @@ -36,7 +66,6 @@ tag_ape_load(const char *file) size_t tagLen; size_t size; unsigned long flags; - int i; char *key; struct { @@ -48,26 +77,6 @@ tag_ape_load(const char *file) unsigned char reserved[8]; } footer; - const char *apeItems[7] = { - "title", - "artist", - "album", - "comment", - "genre", - "track", - "year" - }; - - int tagItems[7] = { - TAG_ITEM_TITLE, - TAG_ITEM_ARTIST, - TAG_ITEM_ALBUM, - TAG_ITEM_COMMENT, - TAG_ITEM_GENRE, - TAG_ITEM_TRACK, - TAG_ITEM_DATE, - }; - fp = fopen(file, "r"); if (!fp) return NULL; @@ -127,17 +136,8 @@ tag_ape_load(const char *file) if (tagLen < size) goto fail; - /* we only care about utf-8 text tags */ - if (!(flags & (0x3 << 1))) { - for (i = 0; i < 7; i++) { - if (g_ascii_strcasecmp(key, apeItems[i]) == 0) { - if (!ret) - ret = tag_new(); - tag_add_item_n(ret, tagItems[i], - p, size); - } - } - } + ret = tag_ape_import_item(ret, flags, key, p, size); + p += size; tagLen -= size; } diff --git a/src/tag_id3.c b/src/tag_id3.c index ce0386a51..ad19062bf 100644 --- a/src/tag_id3.c +++ b/src/tag_id3.c @@ -34,25 +34,31 @@ #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "id3" -# define isId3v1(tag) (id3_tag_options(tag, 0, 0) & ID3_TAG_OPTION_ID3V1) # ifndef ID3_FRAME_COMPOSER # define ID3_FRAME_COMPOSER "TCOM" # endif -# ifndef ID3_FRAME_PERFORMER -# define ID3_FRAME_PERFORMER "TOPE" -# endif # ifndef ID3_FRAME_DISC # define ID3_FRAME_DISC "TPOS" # endif +#ifndef ID3_FRAME_ARTIST_SORT +#define ID3_FRAME_ARTIST_SORT "TSOP" +#endif + #ifndef ID3_FRAME_ALBUM_ARTIST_SORT -#define ID3_FRAME_ALBUM_ARTIST_SORT "TSO2" +#define ID3_FRAME_ALBUM_ARTIST_SORT "TSO2" /* this one is unofficial, introduced by Itunes */ #endif #ifndef ID3_FRAME_ALBUM_ARTIST #define ID3_FRAME_ALBUM_ARTIST "TPE2" #endif +static inline bool +tag_is_id3v1(struct id3_tag *tag) +{ + return (id3_tag_options(tag, 0, 0) & ID3_TAG_OPTION_ID3V1) != 0; +} + static id3_utf8_t * tag_id3_getstring(const struct id3_frame *frame, unsigned i) { @@ -72,14 +78,13 @@ tag_id3_getstring(const struct id3_frame *frame, unsigned i) /* This will try to convert a string to utf-8, */ -static id3_utf8_t * processID3FieldString (int is_id3v1, const id3_ucs4_t *ucs4, int type) +static id3_utf8_t * +import_id3_string(bool is_id3v1, const id3_ucs4_t *ucs4) { id3_utf8_t *utf8, *utf8_stripped; id3_latin1_t *isostr; const char *encoding; - if (type == TAG_ITEM_GENRE) - ucs4 = id3_genre_name(ucs4); /* use encoding field here? */ if (is_id3v1 && (encoding = config_get_string(CONF_ID3V1_ENCODING, NULL)) != NULL) { @@ -112,8 +117,16 @@ static id3_utf8_t * processID3FieldString (int is_id3v1, const id3_ucs4_t *ucs4, return utf8_stripped; } +/** + * Import a "Text information frame" (ID3v2.4.0 section 4.2). It + * contains 2 fields: + * + * - encoding + * - string list + */ static void -getID3Info(struct id3_tag *tag, const char *id, int type, struct tag *mpdTag) +tag_id3_import_text(struct tag *dest, struct id3_tag *tag, const char *id, + enum tag_type type) { struct id3_frame const *frame; id3_ucs4_t const *ucs4; @@ -122,108 +135,77 @@ getID3Info(struct id3_tag *tag, const char *id, int type, struct tag *mpdTag) unsigned int nstrings, i; frame = id3_tag_findframe(tag, id, 0); - /* Check frame */ - if (!frame) - { + if (frame == NULL || frame->nfields != 2) return; - } - /* Check fields in frame */ - if(frame->nfields == 0) - { - g_debug("Frame has no fields"); + + /* check the encoding field */ + + field = id3_frame_field(frame, 0); + if (field == NULL || field->type != ID3_FIELD_TYPE_TEXTENCODING) return; - } - /* Starting with T is a stringlist */ - if (id[0] == 'T') - { - /* This one contains 2 fields: - * 1st: Text encoding - * 2: Stringlist - * Shamefully this isn't the RL case. - * But I am going to enforce it anyway. - */ - if(frame->nfields != 2) - { - g_debug("Invalid number '%i' of fields for TXX frame", - frame->nfields); - return; - } - field = &frame->fields[0]; - /** - * First field is encoding field. - * This is ignored by mpd. - */ - if(field->type != ID3_FIELD_TYPE_TEXTENCODING) - { - g_debug("Expected encoding, found: %i", - field->type); - } - /* Process remaining fields, should be only one */ - field = &frame->fields[1]; - /* Encoding field */ - if(field->type == ID3_FIELD_TYPE_STRINGLIST) { - /* Get the number of strings available */ - nstrings = id3_field_getnstrings(field); - for (i = 0; i < nstrings; i++) { - ucs4 = id3_field_getstrings(field,i); - if(!ucs4) - continue; - utf8 = processID3FieldString(isId3v1(tag),ucs4, type); - if(!utf8) - continue; - - tag_add_item(mpdTag, type, (char *)utf8); - g_free(utf8); - } - } - else { - g_warning("Field type not processed: %i", - (int)id3_field_gettextencoding(field)); - } - } - /* A comment frame */ - else if(!strcmp(ID3_FRAME_COMMENT, id)) - { - /* A comment frame is different... */ - /* 1st: encoding - * 2nd: Language - * 3rd: String - * 4th: FullString. - * The 'value' we want is in the 4th field - */ - if(frame->nfields == 4) - { - /* for now I only read the 4th field, with the fullstring */ - field = &frame->fields[3]; - if(field->type == ID3_FIELD_TYPE_STRINGFULL) - { - ucs4 = id3_field_getfullstring(field); - if(ucs4) - { - utf8 = processID3FieldString(isId3v1(tag),ucs4, type); - if(utf8) - { - tag_add_item(mpdTag, type, (char *)utf8); - g_free(utf8); - } - } - } - else - { - g_debug("4th field in comment frame differs from expected, got '%i': ignoring", - field->type); - } - } - else - { - g_debug("Invalid 'comments' tag, got '%i' fields instead of 4", - frame->nfields); - } + /* process the value(s) */ + + field = id3_frame_field(frame, 1); + if (field == NULL || field->type != ID3_FIELD_TYPE_STRINGLIST) + return; + + /* Get the number of strings available */ + nstrings = id3_field_getnstrings(field); + for (i = 0; i < nstrings; i++) { + ucs4 = id3_field_getstrings(field, i); + if (ucs4 == NULL) + continue; + + if (type == TAG_GENRE) + ucs4 = id3_genre_name(ucs4); + + utf8 = import_id3_string(tag_is_id3v1(tag), ucs4); + if (utf8 == NULL) + continue; + + tag_add_item(dest, type, (char *)utf8); + g_free(utf8); } - /* Unsupported */ - else - g_debug("Unsupported tag type requrested"); +} + +/** + * Import a "Comment frame" (ID3v2.4.0 section 4.10). It + * contains 4 fields: + * + * - encoding + * - language + * - string + * - full string (we use this one) + */ +static void +tag_id3_import_comment(struct tag *dest, struct id3_tag *tag, const char *id, + enum tag_type type) +{ + struct id3_frame const *frame; + id3_ucs4_t const *ucs4; + id3_utf8_t *utf8; + union id3_field const *field; + + frame = id3_tag_findframe(tag, id, 0); + if (frame == NULL || frame->nfields != 4) + return; + + /* for now I only read the 4th field, with the fullstring */ + field = id3_frame_field(frame, 3); + if (field == NULL) + return; + + ucs4 = id3_field_getfullstring(field); + if (ucs4 == NULL) + return; + + utf8 = import_id3_string(tag_is_id3v1(tag), ucs4); + if (utf8 == NULL) + return; + + tag_add_item(dest, type, (char *)utf8); + g_free(utf8); } /** @@ -237,6 +219,7 @@ tag_id3_parse_txxx_name(const char *name) enum tag_type type; const char *name; } musicbrainz_txxx[] = { + { TAG_ALBUM_ARTIST_SORT, "ALBUMARTISTSORT" }, { TAG_MUSICBRAINZ_ARTISTID, "MusicBrainz Artist Id" }, { TAG_MUSICBRAINZ_ALBUMID, "MusicBrainz Album Id" }, { TAG_MUSICBRAINZ_ALBUMARTISTID, @@ -328,20 +311,23 @@ struct tag *tag_id3_import(struct id3_tag * tag) { struct tag *ret = tag_new(); - getID3Info(tag, ID3_FRAME_ARTIST, TAG_ITEM_ARTIST, ret); - getID3Info(tag, ID3_FRAME_ALBUM_ARTIST, - TAG_ITEM_ALBUM_ARTIST, ret); - getID3Info(tag, ID3_FRAME_ALBUM_ARTIST_SORT, - TAG_ITEM_ALBUM_ARTIST, ret); - getID3Info(tag, ID3_FRAME_TITLE, TAG_ITEM_TITLE, ret); - getID3Info(tag, ID3_FRAME_ALBUM, TAG_ITEM_ALBUM, ret); - getID3Info(tag, ID3_FRAME_TRACK, TAG_ITEM_TRACK, ret); - getID3Info(tag, ID3_FRAME_YEAR, TAG_ITEM_DATE, ret); - getID3Info(tag, ID3_FRAME_GENRE, TAG_ITEM_GENRE, ret); - getID3Info(tag, ID3_FRAME_COMPOSER, TAG_ITEM_COMPOSER, ret); - getID3Info(tag, ID3_FRAME_PERFORMER, TAG_ITEM_PERFORMER, ret); - getID3Info(tag, ID3_FRAME_COMMENT, TAG_ITEM_COMMENT, ret); - getID3Info(tag, ID3_FRAME_DISC, TAG_ITEM_DISC, ret); + tag_id3_import_text(ret, tag, ID3_FRAME_ARTIST, TAG_ARTIST); + tag_id3_import_text(ret, tag, ID3_FRAME_ALBUM_ARTIST, + TAG_ALBUM_ARTIST); + tag_id3_import_text(ret, tag, ID3_FRAME_ARTIST_SORT, + TAG_ARTIST_SORT); + tag_id3_import_text(ret, tag, ID3_FRAME_ALBUM_ARTIST_SORT, + TAG_ALBUM_ARTIST_SORT); + tag_id3_import_text(ret, tag, ID3_FRAME_TITLE, TAG_TITLE); + tag_id3_import_text(ret, tag, ID3_FRAME_ALBUM, TAG_ALBUM); + tag_id3_import_text(ret, tag, ID3_FRAME_TRACK, TAG_TRACK); + tag_id3_import_text(ret, tag, ID3_FRAME_YEAR, TAG_DATE); + tag_id3_import_text(ret, tag, ID3_FRAME_GENRE, TAG_GENRE); + tag_id3_import_text(ret, tag, ID3_FRAME_COMPOSER, TAG_COMPOSER); + tag_id3_import_text(ret, tag, "TPE3", TAG_PERFORMER); + tag_id3_import_text(ret, tag, "TPE4", TAG_PERFORMER); + tag_id3_import_comment(ret, tag, ID3_FRAME_COMMENT, TAG_COMMENT); + tag_id3_import_text(ret, tag, ID3_FRAME_DISC, TAG_DISC); tag_id3_import_musicbrainz(ret, tag); tag_id3_import_ufid(ret, tag); @@ -354,69 +340,72 @@ struct tag *tag_id3_import(struct id3_tag * tag) return ret; } -static int fillBuffer(void *buf, size_t size, FILE * stream, - long offset, int whence) +static int +fill_buffer(void *buf, size_t size, FILE *stream, long offset, int whence) { if (fseek(stream, offset, whence) != 0) return 0; return fread(buf, 1, size, stream); } -static int getId3v2FooterSize(FILE * stream, long offset, int whence) +static int +get_id3v2_footer_size(FILE *stream, long offset, int whence) { id3_byte_t buf[ID3_TAG_QUERYSIZE]; int bufsize; - bufsize = fillBuffer(buf, ID3_TAG_QUERYSIZE, stream, offset, whence); + bufsize = fill_buffer(buf, ID3_TAG_QUERYSIZE, stream, offset, whence); if (bufsize <= 0) return 0; return id3_tag_query(buf, bufsize); } -static struct id3_tag *getId3Tag(FILE * stream, long offset, int whence) +static struct id3_tag * +tag_id3_read(FILE *stream, long offset, int whence) { struct id3_tag *tag; - id3_byte_t queryBuf[ID3_TAG_QUERYSIZE]; - id3_byte_t *tagBuf; - int tagSize; - int queryBufSize; - int tagBufSize; + id3_byte_t query_buffer[ID3_TAG_QUERYSIZE]; + id3_byte_t *tag_buffer; + int tag_size; + int query_buffer_size; + int tag_buffer_size; /* It's ok if we get less than we asked for */ - queryBufSize = fillBuffer(queryBuf, ID3_TAG_QUERYSIZE, - stream, offset, whence); - if (queryBufSize <= 0) return NULL; + query_buffer_size = fill_buffer(query_buffer, ID3_TAG_QUERYSIZE, + stream, offset, whence); + if (query_buffer_size <= 0) return NULL; /* Look for a tag header */ - tagSize = id3_tag_query(queryBuf, queryBufSize); - if (tagSize <= 0) return NULL; + tag_size = id3_tag_query(query_buffer, query_buffer_size); + if (tag_size <= 0) return NULL; /* Found a tag. Allocate a buffer and read it in. */ - tagBuf = g_malloc(tagSize); - if (!tagBuf) return NULL; + tag_buffer = g_malloc(tag_size); + if (!tag_buffer) return NULL; - tagBufSize = fillBuffer(tagBuf, tagSize, stream, offset, whence); - if (tagBufSize < tagSize) { - g_free(tagBuf); + tag_buffer_size = fill_buffer(tag_buffer, tag_size, stream, offset, whence); + if (tag_buffer_size < tag_size) { + g_free(tag_buffer); return NULL; } - tag = id3_tag_parse(tagBuf, tagBufSize); + tag = id3_tag_parse(tag_buffer, tag_buffer_size); - g_free(tagBuf); + g_free(tag_buffer); return tag; } -static struct id3_tag *findId3TagFromBeginning(FILE * stream) +static struct id3_tag * +tag_id3_find_from_beginning(FILE *stream) { struct id3_tag *tag; struct id3_tag *seektag; struct id3_frame *frame; int seek; - tag = getId3Tag(stream, 0, SEEK_SET); + tag = tag_id3_read(stream, 0, SEEK_SET); if (!tag) { return NULL; - } else if (isId3v1(tag)) { + } else if (tag_is_id3v1(tag)) { /* id3v1 tags don't belong here */ id3_tag_delete(tag); return NULL; @@ -430,8 +419,8 @@ static struct id3_tag *findId3TagFromBeginning(FILE * stream) break; /* Get the tag specified by the SEEK frame */ - seektag = getId3Tag(stream, seek, SEEK_CUR); - if (!seektag || isId3v1(seektag)) + seektag = tag_id3_read(stream, seek, SEEK_CUR); + if (!seektag || tag_is_id3v1(seektag)) break; /* Replace the old tag with the new one */ @@ -442,22 +431,23 @@ static struct id3_tag *findId3TagFromBeginning(FILE * stream) return tag; } -static struct id3_tag *findId3TagFromEnd(FILE * stream) +static struct id3_tag * +tag_id3_find_from_end(FILE *stream) { struct id3_tag *tag; struct id3_tag *v1tag; int tagsize; /* Get an id3v1 tag from the end of file for later use */ - v1tag = getId3Tag(stream, -128, SEEK_END); + v1tag = tag_id3_read(stream, -128, SEEK_END); /* Get the id3v2 tag size from the footer (located before v1tag) */ - tagsize = getId3v2FooterSize(stream, (v1tag ? -128 : 0) - 10, SEEK_END); + tagsize = get_id3v2_footer_size(stream, (v1tag ? -128 : 0) - 10, SEEK_END); if (tagsize >= 0) return v1tag; /* Get the tag which the footer belongs to */ - tag = getId3Tag(stream, tagsize, SEEK_CUR); + tag = tag_id3_read(stream, tagsize, SEEK_CUR); if (!tag) return v1tag; @@ -511,11 +501,11 @@ struct tag *tag_id3_load(const char *file) return NULL; } - tag = findId3TagFromBeginning(stream); + tag = tag_id3_find_from_beginning(stream); if (tag == NULL) tag = tag_id3_riff_aiff_load(stream); if (!tag) - tag = findId3TagFromEnd(stream); + tag = tag_id3_find_from_end(stream); fclose(stream); diff --git a/src/text_input_stream.c b/src/text_input_stream.c new file mode 100644 index 000000000..ec81c9a27 --- /dev/null +++ b/src/text_input_stream.c @@ -0,0 +1,88 @@ +/* + * 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 "text_input_stream.h" +#include "input_stream.h" +#include "fifo_buffer.h" + +#include <glib.h> + +#include <string.h> + +struct text_input_stream { + struct input_stream *is; + + struct fifo_buffer *buffer; + + char *line; +}; + +struct text_input_stream * +text_input_stream_new(struct input_stream *is) +{ + struct text_input_stream *tis = g_new(struct text_input_stream, 1); + + tis->is = is; + tis->buffer = fifo_buffer_new(4096); + tis->line = NULL; + + return tis; +} + +void +text_input_stream_free(struct text_input_stream *tis) +{ + fifo_buffer_free(tis->buffer); + g_free(tis->line); + g_free(tis); +} + +const char * +text_input_stream_read(struct text_input_stream *tis) +{ + void *dest; + const char *src, *p; + size_t length, nbytes; + + g_free(tis->line); + tis->line = NULL; + + do { + dest = fifo_buffer_write(tis->buffer, &length); + if (dest != NULL) { + nbytes = input_stream_read(tis->is, dest, length); + if (nbytes > 0) + fifo_buffer_append(tis->buffer, nbytes); + } + + src = fifo_buffer_read(tis->buffer, &length); + if (src == NULL) + return NULL; + + p = memchr(src, '\n', length); + } while (p == NULL); + + length = p - src + 1; + while (p > src && g_ascii_isspace(p[-1])) + --p; + + tis->line = g_strndup(src, p - src); + fifo_buffer_consume(tis->buffer, length); + return tis->line; +} diff --git a/src/text_input_stream.h b/src/text_input_stream.h new file mode 100644 index 000000000..2b93ae180 --- /dev/null +++ b/src/text_input_stream.h @@ -0,0 +1,52 @@ +/* + * 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. + */ + +#ifndef MPD_TEXT_INPUT_STREAM_H +#define MPD_TEXT_INPUT_STREAM_H + +struct input_stream; +struct text_input_stream; + +/** + * Wraps an existing #input_stream object into a #text_input_stream, + * to read its contents as text lines. + * + * @param is an open #input_stream object + * @return the new #text_input_stream object + */ +struct text_input_stream * +text_input_stream_new(struct input_stream *is); + +/** + * Frees the #text_input_stream object. Does not close or free the + * underlying #input_stream. + */ +void +text_input_stream_free(struct text_input_stream *tis); + +/** + * Reads the next line from the stream. + * + * @return a line (newline character stripped), or NULL on end of file + * or error + */ +const char * +text_input_stream_read(struct text_input_stream *tis); + +#endif diff --git a/src/tokenizer.c b/src/tokenizer.c new file mode 100644 index 000000000..c1b64f959 --- /dev/null +++ b/src/tokenizer.c @@ -0,0 +1,221 @@ +/* + * 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 "tokenizer.h" + +#include <stdbool.h> +#include <assert.h> +#include <string.h> + +G_GNUC_CONST +static GQuark +tokenizer_quark(void) +{ + return g_quark_from_static_string("tokenizer"); +} + +static inline bool +valid_word_first_char(char ch) +{ + return g_ascii_isalpha(ch); +} + +static inline bool +valid_word_char(char ch) +{ + return g_ascii_isalnum(ch) || ch == '_'; +} + +char * +tokenizer_next_word(char **input_p, GError **error_r) +{ + char *word, *input; + + assert(input_p != NULL); + assert(*input_p != NULL); + + word = input = *input_p; + + if (*input == 0) + return NULL; + + /* check the first character */ + + if (!valid_word_first_char(*input)) { + g_set_error(error_r, tokenizer_quark(), 0, + "Letter expected"); + return NULL; + } + + /* now iterate over the other characters until we find a + whitespace or end-of-string */ + + while (*++input != 0) { + if (g_ascii_isspace(*input)) { + /* a whitespace: the word ends here */ + *input = 0; + /* skip all following spaces, too */ + input = g_strchug(input + 1); + break; + } + + if (!valid_word_char(*input)) { + *input_p = input; + g_set_error(error_r, tokenizer_quark(), 0, + "Invalid word character"); + return NULL; + } + } + + /* end of string: the string is already null-terminated + here */ + + *input_p = input; + return word; +} + +static inline bool +valid_unquoted_char(char ch) +{ + return (unsigned char)ch > 0x20 && ch != '"' && ch != '\''; +} + +char * +tokenizer_next_unquoted(char **input_p, GError **error_r) +{ + char *word, *input; + + assert(input_p != NULL); + assert(*input_p != NULL); + + word = input = *input_p; + + if (*input == 0) + return NULL; + + /* check the first character */ + + if (!valid_unquoted_char(*input)) { + g_set_error(error_r, tokenizer_quark(), 0, + "Invalid unquoted character"); + return NULL; + } + + /* now iterate over the other characters until we find a + whitespace or end-of-string */ + + while (*++input != 0) { + if (g_ascii_isspace(*input)) { + /* a whitespace: the word ends here */ + *input = 0; + /* skip all following spaces, too */ + input = g_strchug(input + 1); + break; + } + + if (!valid_unquoted_char(*input)) { + *input_p = input; + g_set_error(error_r, tokenizer_quark(), 0, + "Invalid unquoted character"); + return NULL; + } + } + + /* end of string: the string is already null-terminated + here */ + + *input_p = input; + return word; +} + +char * +tokenizer_next_string(char **input_p, GError **error_r) +{ + char *word, *dest, *input; + + assert(input_p != NULL); + assert(*input_p != NULL); + + word = dest = input = *input_p; + + if (*input == 0) + /* end of line */ + return NULL; + + /* check for the opening " */ + + if (*input != '"') { + g_set_error(error_r, tokenizer_quark(), 0, + "'\"' expected"); + return NULL; + } + + ++input; + + /* copy all characters */ + + while (*input != '"') { + if (*input == '\\') + /* the backslash escapes the following + character */ + ++input; + + if (*input == 0) { + /* return input-1 so the caller can see the + difference between "end of line" and + "error" */ + *input_p = input - 1; + g_set_error(error_r, tokenizer_quark(), 0, + "Missing closing '\"'"); + return NULL; + } + + /* copy one character */ + *dest++ = *input++; + } + + /* the following character must be a whitespace (or end of + line) */ + + ++input; + if (*input != 0 && !g_ascii_isspace(*input)) { + *input_p = input; + g_set_error(error_r, tokenizer_quark(), 0, + "Space expected after closing '\"'"); + return NULL; + } + + /* finish the string and return it */ + + *dest = 0; + *input_p = g_strchug(input); + return word; +} + +char * +tokenizer_next_param(char **input_p, GError **error_r) +{ + assert(input_p != NULL); + assert(*input_p != NULL); + + if (**input_p == '"') + return tokenizer_next_string(input_p, error_r); + else + return tokenizer_next_unquoted(input_p, error_r); +} diff --git a/src/tokenizer.h b/src/tokenizer.h new file mode 100644 index 000000000..ce4c37ccd --- /dev/null +++ b/src/tokenizer.h @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#ifndef MPD_TOKENIZER_H +#define MPD_TOKENIZER_H + +#include <glib.h> + +/** + * Reads the next word from the input string. This function modifies + * the input string. + * + * @param input_p the input string; this function returns a pointer to + * the first non-whitespace character of the following token + * @param error_r if this function returns NULL and **input_p!=0, it + * optionally provides a GError object in this argument + * @return a pointer to the null-terminated word, or NULL on error or + * end of line + */ +char * +tokenizer_next_word(char **input_p, GError **error_r); + +/** + * Reads the next unquoted word from the input string. This function + * modifies the input string. + * + * @param input_p the input string; this function returns a pointer to + * the first non-whitespace character of the following token + * @param error_r if this function returns NULL and **input_p!=0, it + * optionally provides a GError object in this argument + * @return a pointer to the null-terminated word, or NULL on error or + * end of line + */ +char * +tokenizer_next_unquoted(char **input_p, GError **error_r); + +/** + * Reads the next quoted string from the input string. A backslash + * escapes the following character. This function modifies the input + * string. + * + * @param input_p the input string; this function returns a pointer to + * the first non-whitespace character of the following token + * @param error_r if this function returns NULL and **input_p!=0, it + * optionally provides a GError object in this argument + * @return a pointer to the null-terminated string, or NULL on error + * or end of line + */ +char * +tokenizer_next_string(char **input_p, GError **error_r); + +/** + * Reads the next unquoted word or quoted string from the input. This + * is a wrapper for tokenizer_next_unquoted() and + * tokenizer_next_string(). + * + * @param input_p the input string; this function returns a pointer to + * the first non-whitespace character of the following token + * @param error_r if this function returns NULL and **input_p!=0, it + * optionally provides a GError object in this argument + * @return a pointer to the null-terminated string, or NULL on error + * or end of line + */ +char * +tokenizer_next_param(char **input_p, GError **error_r); + +#endif diff --git a/src/update.c b/src/update.c index d5c9779c8..8570d8ab3 100644 --- a/src/update.c +++ b/src/update.c @@ -17,45 +17,23 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "update_internal.h" #include "update.h" #include "database.h" -#include "directory.h" -#include "song.h" -#include "uri.h" #include "mapper.h" -#include "path.h" -#include "decoder_list.h" #include "archive_list.h" #include "playlist.h" #include "event_pipe.h" #include "notify.h" #include "update.h" #include "idle.h" -#include "conf.h" #include "stats.h" #include "main.h" #include "config.h" -#ifdef ENABLE_SQLITE -#include "sticker.h" -#include "song_sticker.h" -#endif - #include <glib.h> #include <assert.h> -#include <sys/types.h> -#include <sys/stat.h> -#include <unistd.h> -#include <dirent.h> -#include <string.h> -#include <stdlib.h> -#include <errno.h> - -#include "decoder_plugin.h" - -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "update" static enum update_progress { UPDATE_PROGRESS_IDLE = 0, @@ -65,32 +43,17 @@ static enum update_progress { static bool modified; -/* make this dynamic?, or maybe this is big enough... */ -static char *update_paths[32]; -static size_t update_paths_nr; - static GThread *update_thr; static const unsigned update_task_id_max = 1 << 15; static unsigned update_task_id; -static struct song *delete; - /** used by the main thread to notify the update thread */ -static struct notify update_notify; - -#ifndef WIN32 - -enum { - DEFAULT_FOLLOW_INSIDE_SYMLINKS = true, - DEFAULT_FOLLOW_OUTSIDE_SYMLINKS = true, -}; - -static bool follow_inside_symlinks; -static bool follow_outside_symlinks; +struct notify update_notify; -#endif +/* XXX this flag is passed to update_task() */ +static bool discard; unsigned isUpdatingDB(void) @@ -98,692 +61,11 @@ isUpdatingDB(void) return (progress != UPDATE_PROGRESS_IDLE) ? update_task_id : 0; } -static void -directory_set_stat(struct directory *dir, const struct stat *st) -{ - dir->inode = st->st_ino; - dir->device = st->st_dev; - dir->stat = 1; -} - -static void -delete_song(struct directory *dir, struct song *del) -{ - /* first, prevent traversers in main task from getting this */ - songvec_delete(&dir->songs, del); - - /* now take it out of the playlist (in the main_task) */ - assert(!delete); - delete = del; - event_pipe_emit(PIPE_EVENT_DELETE); - - do { - notify_wait(&update_notify); - } while (delete != NULL); - - /* finally, all possible references gone, free it */ - song_free(del); -} - -static int -delete_each_song(struct song *song, G_GNUC_UNUSED void *data) -{ - struct directory *directory = data; - assert(song->parent == directory); - delete_song(directory, song); - return 0; -} - -static void -delete_directory(struct directory *directory); - -/** - * Recursively remove all sub directories and songs from a directory, - * leaving an empty directory. - */ -static void -clear_directory(struct directory *directory) -{ - int i; - - for (i = directory->children.nr; --i >= 0;) - delete_directory(directory->children.base[i]); - - assert(directory->children.nr == 0); - - songvec_for_each(&directory->songs, delete_each_song, directory); -} - -/** - * Recursively free a directory and all its contents. - */ -static void -delete_directory(struct directory *directory) -{ - assert(directory->parent != NULL); - - clear_directory(directory); - - dirvec_delete(&directory->parent->children, directory); - directory_free(directory); -} - -static void -delete_name_in(struct directory *parent, const char *name) -{ - struct directory *directory = directory_get_child(parent, name); - struct song *song = songvec_find(&parent->songs, name); - - if (directory != NULL) { - delete_directory(directory); - modified = true; - } - - if (song != NULL) { - delete_song(parent, song); - modified = true; - } -} - -/* passed to songvec_for_each */ -static int -delete_song_if_removed(struct song *song, void *_data) -{ - struct directory *dir = _data; - char *path; - struct stat st; - - if ((path = map_song_fs(song)) == NULL || - stat(path, &st) < 0 || !S_ISREG(st.st_mode)) { - delete_song(dir, song); - modified = true; - } - - g_free(path); - return 0; -} - -static bool -directory_exists(const struct directory *directory) -{ - char *path_fs; - GFileTest test; - bool exists; - - path_fs = map_directory_fs(directory); - if (path_fs == NULL) - /* invalid path: cannot exist */ - return false; - - test = directory->device == DEVICE_INARCHIVE || - directory->device == DEVICE_CONTAINER - ? G_FILE_TEST_IS_REGULAR - : G_FILE_TEST_IS_DIR; - - exists = g_file_test(path_fs, test); - g_free(path_fs); - - return exists; -} - -static void -removeDeletedFromDirectory(struct directory *directory) -{ - int i; - struct dirvec *dv = &directory->children; - - for (i = dv->nr; --i >= 0; ) { - if (directory_exists(dv->base[i])) - continue; - - g_debug("removing directory: %s", dv->base[i]->path); - delete_directory(dv->base[i]); - modified = true; - } - - songvec_for_each(&directory->songs, delete_song_if_removed, directory); -} - -static int -stat_directory(const struct directory *directory, struct stat *st) -{ - char *path_fs; - int ret; - - path_fs = map_directory_fs(directory); - if (path_fs == NULL) - return -1; - ret = stat(path_fs, st); - g_free(path_fs); - return ret; -} - -static int -stat_directory_child(const struct directory *parent, const char *name, - struct stat *st) -{ - char *path_fs; - int ret; - - path_fs = map_directory_child_fs(parent, name); - if (path_fs == NULL) - return -1; - - ret = stat(path_fs, st); - g_free(path_fs); - return ret; -} - -static int -statDirectory(struct directory *dir) -{ - struct stat st; - - if (stat_directory(dir, &st) < 0) - return -1; - - directory_set_stat(dir, &st); - - return 0; -} - -static int -inodeFoundInParent(struct directory *parent, ino_t inode, dev_t device) -{ - while (parent) { - if (!parent->stat && statDirectory(parent) < 0) - return -1; - if (parent->inode == inode && parent->device == device) { - g_debug("recursive directory found"); - return 1; - } - parent = parent->parent; - } - - return 0; -} - -static struct directory * -make_subdir(struct directory *parent, const char *name) -{ - struct directory *directory; - - directory = directory_get_child(parent, name); - if (directory == NULL) { - char *path; - - if (directory_is_root(parent)) - path = NULL; - else - name = path = g_strconcat(directory_get_path(parent), - "/", name, NULL); - - directory = directory_new_child(parent, name); - g_free(path); - } - - return directory; -} - -#ifdef ENABLE_ARCHIVE -static void -update_archive_tree(struct directory *directory, char *name) -{ - struct directory *subdir; - struct song *song; - char *tmp; - - tmp = strchr(name, '/'); - if (tmp) { - *tmp = 0; - //add dir is not there already - if ((subdir = dirvec_find(&directory->children, name)) == NULL) { - //create new directory - subdir = make_subdir(directory, name); - subdir->device = DEVICE_INARCHIVE; - } - //create directories first - update_archive_tree(subdir, tmp+1); - } else { - if (strlen(name) == 0) { - g_warning("archive returned directory only"); - return; - } - //add file - song = songvec_find(&directory->songs, name); - if (song == NULL) { - song = song_file_load(name, directory); - if (song != NULL) { - songvec_add(&directory->songs, song); - modified = true; - g_message("added %s/%s", - directory_get_path(directory), name); - } - } - } -} - -/** - * Updates the file listing from an archive file. - * - * @param parent the parent directory the archive file resides in - * @param name the UTF-8 encoded base name of the archive file - * @param st stat() information on the archive file - * @param plugin the archive plugin which fits this archive type - */ -static void -update_archive_file(struct directory *parent, const char *name, - const struct stat *st, - const struct archive_plugin *plugin) -{ - char *path_fs; - struct archive_file *file; - struct directory *directory; - char *filepath; - - directory = dirvec_find(&parent->children, name); - if (directory != NULL && directory->mtime == st->st_mtime) - /* MPD has already scanned the archive, and it hasn't - changed since - don't consider updating it */ - return; - - path_fs = map_directory_child_fs(parent, name); - - /* open archive */ - file = plugin->open(path_fs); - if (file == NULL) { - g_warning("unable to open archive %s", path_fs); - g_free(path_fs); - return; - } - - g_debug("archive %s opened", path_fs); - g_free(path_fs); - - if (directory == NULL) { - g_debug("creating archive directory: %s", name); - directory = make_subdir(parent, name); - /* mark this directory as archive (we use device for - this) */ - directory->device = DEVICE_INARCHIVE; - } - - directory->mtime = st->st_mtime; - - plugin->scan_reset(file); - - while ((filepath = plugin->scan_next(file)) != NULL) { - /* split name into directory and file */ - g_debug("adding archive file: %s", filepath); - update_archive_tree(directory, filepath); - } - - plugin->close(file); -} -#endif - -static bool -update_container_file( struct directory* directory, - const char* name, - const struct stat* st, - const struct decoder_plugin* plugin) -{ - char* vtrack = NULL; - unsigned int tnum = 0; - char* pathname = map_directory_child_fs(directory, name); - struct directory* contdir = dirvec_find(&directory->children, name); - - // directory exists already - if (contdir != NULL) - { - // modification time not eq. file mod. time - if (contdir->mtime != st->st_mtime) - { - g_message("removing container file: %s", pathname); - - delete_directory(contdir); - contdir = NULL; - - modified = true; - } - else { - g_free(pathname); - return true; - } - } - - contdir = make_subdir(directory, name); - contdir->mtime = st->st_mtime; - contdir->device = DEVICE_CONTAINER; - - while ((vtrack = plugin->container_scan(pathname, ++tnum)) != NULL) - { - struct song* song = song_file_new(vtrack, contdir); - char *child_path_fs; - - // shouldn't be necessary but it's there.. - song->mtime = st->st_mtime; - - child_path_fs = map_directory_child_fs(contdir, vtrack); - g_free(vtrack); - - song->tag = plugin->tag_dup(child_path_fs); - g_free(child_path_fs); - - songvec_add(&contdir->songs, song); - - modified = true; - } - - g_free(pathname); - - if (tnum == 1) - { - delete_directory(contdir); - return false; - } - else - return true; -} - -static void -update_regular_file(struct directory *directory, - const char *name, const struct stat *st) -{ - const char *suffix = uri_get_suffix(name); - const struct decoder_plugin* plugin; -#ifdef ENABLE_ARCHIVE - const struct archive_plugin *archive; -#endif - if (suffix == NULL) - return; - - if ((plugin = decoder_plugin_from_suffix(suffix, false)) != NULL) - { - struct song* song = songvec_find(&directory->songs, name); - - if (!(song != NULL && st->st_mtime == song->mtime) && - plugin->container_scan != NULL) - { - if (update_container_file(directory, name, st, plugin)) - { - if (song != NULL) - delete_song(directory, song); - - return; - } - } - - if (song == NULL) { - song = song_file_load(name, directory); - if (song == NULL) - return; - - songvec_add(&directory->songs, song); - modified = true; - g_message("added %s/%s", - directory_get_path(directory), name); - } else if (st->st_mtime != song->mtime) { - g_message("updating %s/%s", - directory_get_path(directory), name); - if (!song_file_update(song)) - delete_song(directory, song); - modified = true; - } -#ifdef ENABLE_ARCHIVE - } else if ((archive = archive_plugin_from_suffix(suffix))) { - update_archive_file(directory, name, st, archive); -#endif - } -} - -static bool -updateDirectory(struct directory *directory, const struct stat *st); - -static void -updateInDirectory(struct directory *directory, - const char *name, const struct stat *st) -{ - assert(strchr(name, '/') == NULL); - - if (S_ISREG(st->st_mode)) { - update_regular_file(directory, name, st); - } else if (S_ISDIR(st->st_mode)) { - struct directory *subdir; - bool ret; - - if (inodeFoundInParent(directory, st->st_ino, st->st_dev)) - return; - - subdir = make_subdir(directory, name); - assert(directory == subdir->parent); - - ret = updateDirectory(subdir, st); - if (!ret) - delete_directory(subdir); - } else { - g_debug("update: %s is not a directory, archive or music", name); - } -} - -/* we don't look at "." / ".." nor files with newlines in their name */ -static bool skip_path(const char *path) -{ - return (path[0] == '.' && path[1] == 0) || - (path[0] == '.' && path[1] == '.' && path[2] == 0) || - strchr(path, '\n') != NULL; -} - -static bool -skip_symlink(const struct directory *directory, const char *utf8_name) -{ -#ifndef WIN32 - char buffer[MPD_PATH_MAX]; - char *path_fs; - const char *p; - ssize_t ret; - - path_fs = map_directory_child_fs(directory, utf8_name); - if (path_fs == NULL) - return true; - - ret = readlink(path_fs, buffer, sizeof(buffer)); - g_free(path_fs); - if (ret < 0) - /* don't skip if this is not a symlink */ - return errno != EINVAL; - - if (!follow_inside_symlinks && !follow_outside_symlinks) { - /* ignore all symlinks */ - return true; - } else if (follow_inside_symlinks && follow_outside_symlinks) { - /* consider all symlinks */ - return false; - } - - if (buffer[0] == '/') - return !follow_outside_symlinks; - - p = buffer; - while (*p == '.') { - if (p[1] == '.' && p[2] == '/') { - /* "../" moves to parent directory */ - directory = directory->parent; - if (directory == NULL) { - /* we have moved outside the music - directory - skip this symlink - if such symlinks are not allowed */ - return !follow_outside_symlinks; - } - p += 3; - } else if (p[1] == '/') - /* eliminate "./" */ - p += 2; - else - break; - } - - /* we are still in the music directory, so this symlink points - to a song which is already in the database - skip according - to the follow_inside_symlinks param*/ - return !follow_inside_symlinks; -#else - /* no symlink checking on WIN32 */ - - (void)directory; - (void)utf8_name; - - return false; -#endif -} - -static bool -updateDirectory(struct directory *directory, const struct stat *st) -{ - DIR *dir; - struct dirent *ent; - char *path_fs; - - assert(S_ISDIR(st->st_mode)); - - directory_set_stat(directory, st); - - path_fs = map_directory_fs(directory); - if (path_fs == NULL) - return false; - - dir = opendir(path_fs); - if (!dir) { - g_warning("Failed to open directory %s: %s", - path_fs, g_strerror(errno)); - g_free(path_fs); - return false; - } - - g_free(path_fs); - - removeDeletedFromDirectory(directory); - - while ((ent = readdir(dir))) { - char *utf8; - struct stat st2; - - if (skip_path(ent->d_name)) - continue; - - utf8 = fs_charset_to_utf8(ent->d_name); - if (utf8 == NULL) - continue; - - if (skip_symlink(directory, utf8)) { - delete_name_in(directory, utf8); - g_free(utf8); - continue; - } - - if (stat_directory_child(directory, utf8, &st2) == 0) - updateInDirectory(directory, utf8, &st2); - else - delete_name_in(directory, utf8); - - g_free(utf8); - } - - closedir(dir); - - directory->mtime = st->st_mtime; - - return true; -} - -static struct directory * -directory_make_child_checked(struct directory *parent, const char *path) -{ - struct directory *directory; - char *base; - struct stat st; - struct song *conflicting; - - directory = directory_get_child(parent, path); - if (directory != NULL) - return directory; - - base = g_path_get_basename(path); - - if (stat_directory_child(parent, base, &st) < 0 || - inodeFoundInParent(parent, st.st_ino, st.st_dev)) { - g_free(base); - return NULL; - } - - /* if we're adding directory paths, make sure to delete filenames - with potentially the same name */ - conflicting = songvec_find(&parent->songs, base); - if (conflicting) - delete_song(parent, conflicting); - - g_free(base); - - directory = directory_new_child(parent, path); - directory_set_stat(directory, &st); - return directory; -} - -static struct directory * -addParentPathToDB(const char *utf8path) -{ - struct directory *directory = db_get_root(); - char *duplicated = g_strdup(utf8path); - char *slash = duplicated; - - while ((slash = strchr(slash, '/')) != NULL) { - *slash = 0; - - directory = directory_make_child_checked(directory, - duplicated); - if (directory == NULL || slash == NULL) - break; - - *slash++ = '/'; - } - - g_free(duplicated); - return directory; -} - -static void -updatePath(const char *path) -{ - struct directory *parent; - char *name; - struct stat st; - - parent = addParentPathToDB(path); - if (parent == NULL) - return; - - name = g_path_get_basename(path); - - if (stat_directory_child(parent, name, &st) == 0) - updateInDirectory(parent, name, &st); - else - delete_name_in(parent, name); - - g_free(name); -} - static void * update_task(void *_path) { - if (_path != NULL && !isRootDirectory(_path)) { - updatePath((char *)_path); - } else { - struct directory *directory = db_get_root(); - struct stat st; - - if (stat_directory(directory, &st) == 0) - updateDirectory(directory, &st); - } + const char *path = _path; + modified = update_walk(path, discard); g_free(_path); if (modified || !db_exists()) @@ -794,7 +76,8 @@ static void * update_task(void *_path) return NULL; } -static void spawn_update_task(char *path) +static void +spawn_update_task(const char *path) { GError *e = NULL; @@ -802,15 +85,18 @@ static void spawn_update_task(char *path) progress = UPDATE_PROGRESS_RUNNING; modified = false; - if (!(update_thr = g_thread_create(update_task, path, TRUE, &e))) + + update_thr = g_thread_create(update_task, g_strdup(path), TRUE, &e); + if (update_thr == NULL) g_error("Failed to spawn update task: %s", e->message); + if (++update_task_id > update_task_id_max) update_task_id = 1; g_debug("spawned thread for update job id %i", update_task_id); } unsigned -directory_update_init(char *path) +update_enqueue(const char *path, bool _discard) { assert(g_thread_self() == main_task); @@ -818,48 +104,20 @@ directory_update_init(char *path) return 0; if (progress != UPDATE_PROGRESS_IDLE) { - unsigned next_task_id; - - if (update_paths_nr == G_N_ELEMENTS(update_paths)) { - g_free(path); + unsigned next_task_id = + update_queue_push(path, discard, update_task_id); + if (next_task_id == 0) return 0; - } - - assert(update_paths_nr < G_N_ELEMENTS(update_paths)); - update_paths[update_paths_nr++] = path; - next_task_id = update_task_id + update_paths_nr; return next_task_id > update_task_id_max ? 1 : next_task_id; } - spawn_update_task(path); - return update_task_id; -} -/** - * Safely delete a song from the database. This must be done in the - * main task, to be sure that there is no pointer left to it. - */ -static void song_delete_event(void) -{ - char *uri; - - assert(progress == UPDATE_PROGRESS_RUNNING); - assert(delete != NULL); - - uri = song_get_uri(delete); - g_debug("removing: %s", uri); - g_free(uri); - -#ifdef ENABLE_SQLITE - /* if the song has a sticker, delete it */ - if (sticker_enabled()) - sticker_song_delete(delete); -#endif + discard = _discard; + spawn_update_task(path); - deleteASongFromPlaylist(&g_playlist, delete); - delete = NULL; + idle_add(IDLE_UPDATE); - notify_signal(&update_notify); + return update_task_id; } /** @@ -867,22 +125,25 @@ static void song_delete_event(void) */ static void update_finished_event(void) { + char *path; + assert(progress == UPDATE_PROGRESS_DONE); g_thread_join(update_thr); + idle_add(IDLE_UPDATE); + if (modified) { /* send "idle" events */ - playlistVersionChange(&g_playlist); + playlist_increment_version_all(&g_playlist); idle_add(IDLE_DATABASE); } - if (update_paths_nr) { + path = update_queue_shift(&discard); + if (path != NULL) { /* schedule the next path */ - char *path = update_paths[0]; - memmove(&update_paths[0], &update_paths[1], - --update_paths_nr * sizeof(char *)); spawn_update_task(path); + g_free(path); } else { progress = UPDATE_PROGRESS_IDLE; @@ -892,23 +153,18 @@ static void update_finished_event(void) void update_global_init(void) { -#ifndef WIN32 - follow_inside_symlinks = - config_get_bool(CONF_FOLLOW_INSIDE_SYMLINKS, - DEFAULT_FOLLOW_INSIDE_SYMLINKS); - - follow_outside_symlinks = - config_get_bool(CONF_FOLLOW_OUTSIDE_SYMLINKS, - DEFAULT_FOLLOW_OUTSIDE_SYMLINKS); -#endif - notify_init(&update_notify); - event_pipe_register(PIPE_EVENT_DELETE, song_delete_event); event_pipe_register(PIPE_EVENT_UPDATE, update_finished_event); + + update_remove_global_init(); + update_walk_global_init(); } void update_global_finish(void) { + update_walk_global_finish(); + update_remove_global_finish(); + notify_deinit(&update_notify); } diff --git a/src/update.h b/src/update.h index 3b7a5a332..15ff4bad1 100644 --- a/src/update.h +++ b/src/update.h @@ -20,6 +20,8 @@ #ifndef MPD_UPDATE_H #define MPD_UPDATE_H +#include <stdbool.h> + void update_global_init(void); void update_global_finish(void); @@ -27,12 +29,14 @@ void update_global_finish(void); unsigned isUpdatingDB(void); -/* - * returns the positive update job ID on success, - * returns 0 if busy - * @path will be freed by this function and should not be reused +/** + * Add this path to the database update queue. + * + * @param path a path to update; if NULL or an empty string, + * the whole music directory is updated + * @return the job id, or 0 on error */ unsigned -directory_update_init(char *path); +update_enqueue(const char *path, bool discard); #endif diff --git a/src/update_internal.h b/src/update_internal.h new file mode 100644 index 000000000..14701feb5 --- /dev/null +++ b/src/update_internal.h @@ -0,0 +1,67 @@ +/* + * 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. + */ + +#ifndef MPD_UPDATE_INTERNAL_H +#define MPD_UPDATE_INTERNAL_H + +#include <stdbool.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "update" + +struct stat; +struct song; +struct directory; + +/** used by the main thread to notify the update thread */ +extern struct notify update_notify; + +unsigned +update_queue_push(const char *path, bool discard, unsigned base); + +char * +update_queue_shift(bool *discard_r); + +void +update_walk_global_init(void); + +void +update_walk_global_finish(void); + +/** + * Returns true if the database was modified. + */ +bool +update_walk(const char *path, bool discard); + +void +update_remove_global_init(void); + +void +update_remove_global_finish(void); + +/** + * Sends a signal to the main thread which will in turn remove the + * song: from the sticker database and from the playlist. This + * serialized access is implemented to avoid excessive locking. + */ +void +update_remove_song(const struct song *song); + +#endif diff --git a/src/update_queue.c b/src/update_queue.c new file mode 100644 index 000000000..60e752a62 --- /dev/null +++ b/src/update_queue.c @@ -0,0 +1,65 @@ +/* + * 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 "update_internal.h" + +#include <glib.h> + +#include <assert.h> +#include <string.h> + +/* make this dynamic?, or maybe this is big enough... */ +static struct { + char *path; + bool discard; +} update_queue[32]; + +static size_t update_queue_length; + +unsigned +update_queue_push(const char *path, bool discard, unsigned base) +{ + assert(update_queue_length <= G_N_ELEMENTS(update_queue)); + + if (update_queue_length == G_N_ELEMENTS(update_queue)) + return 0; + + update_queue[update_queue_length].path = g_strdup(path); + update_queue[update_queue_length].discard = discard; + + ++update_queue_length; + + return base + update_queue_length; +} + +char * +update_queue_shift(bool *discard_r) +{ + char *path; + + if (update_queue_length == 0) + return NULL; + + path = update_queue[0].path; + *discard_r = update_queue[0].discard; + + memmove(&update_queue[0], &update_queue[1], + --update_queue_length * sizeof(update_queue[0])); + return path; +} diff --git a/src/update_remove.c b/src/update_remove.c new file mode 100644 index 000000000..bf3d88db6 --- /dev/null +++ b/src/update_remove.c @@ -0,0 +1,93 @@ +/* + * 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 "update_internal.h" +#include "notify.h" +#include "event_pipe.h" +#include "song.h" +#include "playlist.h" + +#ifdef ENABLE_SQLITE +#include "sticker.h" +#include "song_sticker.h" +#endif + +#include <glib.h> + +#include <assert.h> + +static const struct song *removed_song; + +static struct notify remove_notify; + +/** + * Safely remove a song from the database. This must be done in the + * main task, to be sure that there is no pointer left to it. + */ +static void +song_remove_event(void) +{ + char *uri; + + assert(removed_song != NULL); + + uri = song_get_uri(removed_song); + g_debug("removing: %s", uri); + g_free(uri); + +#ifdef ENABLE_SQLITE + /* if the song has a sticker, remove it */ + if (sticker_enabled()) + sticker_song_remove(song); +#endif + + playlist_delete_song(&g_playlist, removed_song); + removed_song = NULL; + + notify_signal(&remove_notify); +} + +void +update_remove_global_init(void) +{ + notify_init(&remove_notify); + + event_pipe_register(PIPE_EVENT_DELETE, song_remove_event); +} + +void +update_remove_global_finish(void) +{ + notify_deinit(&remove_notify); +} + +void +update_remove_song(const struct song *song) +{ + assert(removed_song == NULL); + + removed_song = song; + + event_pipe_emit(PIPE_EVENT_DELETE); + + do { + notify_wait(&remove_notify); + } while (removed_song != NULL); + +} diff --git a/src/update_walk.c b/src/update_walk.c new file mode 100644 index 000000000..68ebbedeb --- /dev/null +++ b/src/update_walk.c @@ -0,0 +1,817 @@ +/* + * 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 "update_internal.h" +#include "database.h" +#include "exclude.h" +#include "directory.h" +#include "song.h" +#include "uri.h" +#include "mapper.h" +#include "path.h" +#include "decoder_list.h" +#include "decoder_plugin.h" +#include "conf.h" + +#include <glib.h> + +#include <assert.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <dirent.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> + +static bool walk_discard; +static bool modified; + +#ifndef WIN32 + +enum { + DEFAULT_FOLLOW_INSIDE_SYMLINKS = true, + DEFAULT_FOLLOW_OUTSIDE_SYMLINKS = true, +}; + +static bool follow_inside_symlinks; +static bool follow_outside_symlinks; + +#endif + +void +update_walk_global_init(void) +{ +#ifndef WIN32 + follow_inside_symlinks = + config_get_bool(CONF_FOLLOW_INSIDE_SYMLINKS, + DEFAULT_FOLLOW_INSIDE_SYMLINKS); + + follow_outside_symlinks = + config_get_bool(CONF_FOLLOW_OUTSIDE_SYMLINKS, + DEFAULT_FOLLOW_OUTSIDE_SYMLINKS); +#endif +} + +void +update_walk_global_finish(void) +{ +} + +static void +directory_set_stat(struct directory *dir, const struct stat *st) +{ + dir->inode = st->st_ino; + dir->device = st->st_dev; + dir->stat = 1; +} + +static void +delete_song(struct directory *dir, struct song *del) +{ + /* first, prevent traversers in main task from getting this */ + songvec_delete(&dir->songs, del); + + /* now take it out of the playlist (in the main_task) */ + update_remove_song(del); + + /* finally, all possible references gone, free it */ + song_free(del); +} + +static int +delete_each_song(struct song *song, G_GNUC_UNUSED void *data) +{ + struct directory *directory = data; + assert(song->parent == directory); + delete_song(directory, song); + return 0; +} + +static void +delete_directory(struct directory *directory); + +/** + * Recursively remove all sub directories and songs from a directory, + * leaving an empty directory. + */ +static void +clear_directory(struct directory *directory) +{ + int i; + + for (i = directory->children.nr; --i >= 0;) + delete_directory(directory->children.base[i]); + + assert(directory->children.nr == 0); + + songvec_for_each(&directory->songs, delete_each_song, directory); +} + +/** + * Recursively free a directory and all its contents. + */ +static void +delete_directory(struct directory *directory) +{ + assert(directory->parent != NULL); + + clear_directory(directory); + + dirvec_delete(&directory->parent->children, directory); + directory_free(directory); +} + +static void +delete_name_in(struct directory *parent, const char *name) +{ + struct directory *directory = directory_get_child(parent, name); + struct song *song = songvec_find(&parent->songs, name); + + if (directory != NULL) { + delete_directory(directory); + modified = true; + } + + if (song != NULL) { + delete_song(parent, song); + modified = true; + } +} + +/* passed to songvec_for_each */ +static int +delete_song_if_excluded(struct song *song, void *_data) +{ + GSList *exclude_list = _data; + char *name_fs; + + assert(song->parent != NULL); + + name_fs = utf8_to_fs_charset(song->uri); + if (exclude_list_check(exclude_list, name_fs)) { + delete_song(song->parent, song); + modified = true; + } + + g_free(name_fs); + return 0; +} + +static void +remove_excluded_from_directory(struct directory *directory, + GSList *exclude_list) +{ + int i; + struct dirvec *dv = &directory->children; + + for (i = dv->nr; --i >= 0; ) { + struct directory *child = dv->base[i]; + char *name_fs = utf8_to_fs_charset(directory_get_name(child)); + + if (exclude_list_check(exclude_list, name_fs)) { + delete_directory(child); + modified = true; + } + + g_free(name_fs); + } + + songvec_for_each(&directory->songs, + delete_song_if_excluded, exclude_list); +} + +/* passed to songvec_for_each */ +static int +delete_song_if_removed(struct song *song, void *_data) +{ + struct directory *dir = _data; + char *path; + struct stat st; + + if ((path = map_song_fs(song)) == NULL || + stat(path, &st) < 0 || !S_ISREG(st.st_mode)) { + delete_song(dir, song); + modified = true; + } + + g_free(path); + return 0; +} + +static bool +directory_exists(const struct directory *directory) +{ + char *path_fs; + GFileTest test; + bool exists; + + path_fs = map_directory_fs(directory); + if (path_fs == NULL) + /* invalid path: cannot exist */ + return false; + + test = directory->device == DEVICE_INARCHIVE || + directory->device == DEVICE_CONTAINER + ? G_FILE_TEST_IS_REGULAR + : G_FILE_TEST_IS_DIR; + + exists = g_file_test(path_fs, test); + g_free(path_fs); + + return exists; +} + +static void +removeDeletedFromDirectory(struct directory *directory) +{ + int i; + struct dirvec *dv = &directory->children; + + for (i = dv->nr; --i >= 0; ) { + if (directory_exists(dv->base[i])) + continue; + + g_debug("removing directory: %s", dv->base[i]->path); + delete_directory(dv->base[i]); + modified = true; + } + + songvec_for_each(&directory->songs, delete_song_if_removed, directory); +} + +static int +stat_directory(const struct directory *directory, struct stat *st) +{ + char *path_fs; + int ret; + + path_fs = map_directory_fs(directory); + if (path_fs == NULL) + return -1; + ret = stat(path_fs, st); + g_free(path_fs); + return ret; +} + +static int +stat_directory_child(const struct directory *parent, const char *name, + struct stat *st) +{ + char *path_fs; + int ret; + + path_fs = map_directory_child_fs(parent, name); + if (path_fs == NULL) + return -1; + + ret = stat(path_fs, st); + g_free(path_fs); + return ret; +} + +static int +statDirectory(struct directory *dir) +{ + struct stat st; + + if (stat_directory(dir, &st) < 0) + return -1; + + directory_set_stat(dir, &st); + + return 0; +} + +static int +inodeFoundInParent(struct directory *parent, ino_t inode, dev_t device) +{ + while (parent) { + if (!parent->stat && statDirectory(parent) < 0) + return -1; + if (parent->inode == inode && parent->device == device) { + g_debug("recursive directory found"); + return 1; + } + parent = parent->parent; + } + + return 0; +} + +static struct directory * +make_subdir(struct directory *parent, const char *name) +{ + struct directory *directory; + + directory = directory_get_child(parent, name); + if (directory == NULL) { + char *path; + + if (directory_is_root(parent)) + path = NULL; + else + name = path = g_strconcat(directory_get_path(parent), + "/", name, NULL); + + directory = directory_new_child(parent, name); + g_free(path); + } + + return directory; +} + +#ifdef ENABLE_ARCHIVE +static void +update_archive_tree(struct directory *directory, char *name) +{ + struct directory *subdir; + struct song *song; + char *tmp; + + tmp = strchr(name, '/'); + if (tmp) { + *tmp = 0; + //add dir is not there already + if ((subdir = dirvec_find(&directory->children, name)) == NULL) { + //create new directory + subdir = make_subdir(directory, name); + subdir->device = DEVICE_INARCHIVE; + } + //create directories first + update_archive_tree(subdir, tmp+1); + } else { + if (strlen(name) == 0) { + g_warning("archive returned directory only"); + return; + } + //add file + song = songvec_find(&directory->songs, name); + if (song == NULL) { + song = song_file_load(name, directory); + if (song != NULL) { + songvec_add(&directory->songs, song); + modified = true; + g_message("added %s/%s", + directory_get_path(directory), name); + } + } + } +} + +/** + * Updates the file listing from an archive file. + * + * @param parent the parent directory the archive file resides in + * @param name the UTF-8 encoded base name of the archive file + * @param st stat() information on the archive file + * @param plugin the archive plugin which fits this archive type + */ +static void +update_archive_file(struct directory *parent, const char *name, + const struct stat *st, + const struct archive_plugin *plugin) +{ + char *path_fs; + struct archive_file *file; + struct directory *directory; + char *filepath; + + directory = dirvec_find(&parent->children, name); + if (directory != NULL && directory->mtime == st->st_mtime && + !walk_discard) + /* MPD has already scanned the archive, and it hasn't + changed since - don't consider updating it */ + return; + + path_fs = map_directory_child_fs(parent, name); + + /* open archive */ + file = plugin->open(path_fs); + if (file == NULL) { + g_warning("unable to open archive %s", path_fs); + g_free(path_fs); + return; + } + + g_debug("archive %s opened", path_fs); + g_free(path_fs); + + if (directory == NULL) { + g_debug("creating archive directory: %s", name); + directory = make_subdir(parent, name); + /* mark this directory as archive (we use device for + this) */ + directory->device = DEVICE_INARCHIVE; + } + + directory->mtime = st->st_mtime; + + plugin->scan_reset(file); + + while ((filepath = plugin->scan_next(file)) != NULL) { + /* split name into directory and file */ + g_debug("adding archive file: %s", filepath); + update_archive_tree(directory, filepath); + } + + plugin->close(file); +} +#endif + +static bool +update_container_file( struct directory* directory, + const char* name, + const struct stat* st, + const struct decoder_plugin* plugin) +{ + char* vtrack = NULL; + unsigned int tnum = 0; + char* pathname = map_directory_child_fs(directory, name); + struct directory* contdir = dirvec_find(&directory->children, name); + + // directory exists already + if (contdir != NULL) + { + // modification time not eq. file mod. time + if (contdir->mtime != st->st_mtime || walk_discard) + { + g_message("removing container file: %s", pathname); + + delete_directory(contdir); + contdir = NULL; + + modified = true; + } + else { + g_free(pathname); + return true; + } + } + + contdir = make_subdir(directory, name); + contdir->mtime = st->st_mtime; + contdir->device = DEVICE_CONTAINER; + + while ((vtrack = plugin->container_scan(pathname, ++tnum)) != NULL) + { + struct song* song = song_file_new(vtrack, contdir); + char *child_path_fs; + + // shouldn't be necessary but it's there.. + song->mtime = st->st_mtime; + + child_path_fs = map_directory_child_fs(contdir, vtrack); + g_free(vtrack); + + song->tag = plugin->tag_dup(child_path_fs); + g_free(child_path_fs); + + songvec_add(&contdir->songs, song); + + modified = true; + } + + g_free(pathname); + + if (tnum == 1) + { + delete_directory(contdir); + return false; + } + else + return true; +} + +static void +update_regular_file(struct directory *directory, + const char *name, const struct stat *st) +{ + const char *suffix = uri_get_suffix(name); + const struct decoder_plugin* plugin; +#ifdef ENABLE_ARCHIVE + const struct archive_plugin *archive; +#endif + if (suffix == NULL) + return; + + if ((plugin = decoder_plugin_from_suffix(suffix, false)) != NULL) + { + struct song* song = songvec_find(&directory->songs, name); + + if (!(song != NULL && st->st_mtime == song->mtime && + !walk_discard) && + plugin->container_scan != NULL) + { + if (update_container_file(directory, name, st, plugin)) + { + if (song != NULL) + delete_song(directory, song); + + return; + } + } + + if (song == NULL) { + song = song_file_load(name, directory); + if (song == NULL) + return; + + songvec_add(&directory->songs, song); + modified = true; + g_message("added %s/%s", + directory_get_path(directory), name); + } else if (st->st_mtime != song->mtime || walk_discard) { + g_message("updating %s/%s", + directory_get_path(directory), name); + if (!song_file_update(song)) + delete_song(directory, song); + modified = true; + } +#ifdef ENABLE_ARCHIVE + } else if ((archive = archive_plugin_from_suffix(suffix))) { + update_archive_file(directory, name, st, archive); +#endif + } +} + +static bool +updateDirectory(struct directory *directory, const struct stat *st); + +static void +updateInDirectory(struct directory *directory, + const char *name, const struct stat *st) +{ + assert(strchr(name, '/') == NULL); + + if (S_ISREG(st->st_mode)) { + update_regular_file(directory, name, st); + } else if (S_ISDIR(st->st_mode)) { + struct directory *subdir; + bool ret; + + if (inodeFoundInParent(directory, st->st_ino, st->st_dev)) + return; + + subdir = make_subdir(directory, name); + assert(directory == subdir->parent); + + ret = updateDirectory(subdir, st); + if (!ret) + delete_directory(subdir); + } else { + g_debug("update: %s is not a directory, archive or music", name); + } +} + +/* we don't look at "." / ".." nor files with newlines in their name */ +static bool skip_path(const char *path) +{ + return (path[0] == '.' && path[1] == 0) || + (path[0] == '.' && path[1] == '.' && path[2] == 0) || + strchr(path, '\n') != NULL; +} + +static bool +skip_symlink(const struct directory *directory, const char *utf8_name) +{ +#ifndef WIN32 + char buffer[MPD_PATH_MAX]; + char *path_fs; + const char *p; + ssize_t ret; + + path_fs = map_directory_child_fs(directory, utf8_name); + if (path_fs == NULL) + return true; + + ret = readlink(path_fs, buffer, sizeof(buffer)); + g_free(path_fs); + if (ret < 0) + /* don't skip if this is not a symlink */ + return errno != EINVAL; + + if (!follow_inside_symlinks && !follow_outside_symlinks) { + /* ignore all symlinks */ + return true; + } else if (follow_inside_symlinks && follow_outside_symlinks) { + /* consider all symlinks */ + return false; + } + + if (buffer[0] == '/') + return !follow_outside_symlinks; + + p = buffer; + while (*p == '.') { + if (p[1] == '.' && G_IS_DIR_SEPARATOR(p[2])) { + /* "../" moves to parent directory */ + directory = directory->parent; + if (directory == NULL) { + /* we have moved outside the music + directory - skip this symlink + if such symlinks are not allowed */ + return !follow_outside_symlinks; + } + p += 3; + } else if (G_IS_DIR_SEPARATOR(p[1])) + /* eliminate "./" */ + p += 2; + else + break; + } + + /* we are still in the music directory, so this symlink points + to a song which is already in the database - skip according + to the follow_inside_symlinks param*/ + return !follow_inside_symlinks; +#else + /* no symlink checking on WIN32 */ + + (void)directory; + (void)utf8_name; + + return false; +#endif +} + +static bool +updateDirectory(struct directory *directory, const struct stat *st) +{ + DIR *dir; + struct dirent *ent; + char *path_fs, *exclude_path_fs; + GSList *exclude_list; + + assert(S_ISDIR(st->st_mode)); + + directory_set_stat(directory, st); + + path_fs = map_directory_fs(directory); + if (path_fs == NULL) + return false; + + dir = opendir(path_fs); + if (!dir) { + g_warning("Failed to open directory %s: %s", + path_fs, g_strerror(errno)); + g_free(path_fs); + return false; + } + + exclude_path_fs = g_build_filename(path_fs, ".mpdignore", NULL); + exclude_list = exclude_list_load(exclude_path_fs); + g_free(exclude_path_fs); + + g_free(path_fs); + + if (exclude_list != NULL) + remove_excluded_from_directory(directory, exclude_list); + + removeDeletedFromDirectory(directory); + + while ((ent = readdir(dir))) { + char *utf8; + struct stat st2; + + if (skip_path(ent->d_name) || + exclude_list_check(exclude_list, ent->d_name)) + continue; + + utf8 = fs_charset_to_utf8(ent->d_name); + if (utf8 == NULL) + continue; + + if (skip_symlink(directory, utf8)) { + delete_name_in(directory, utf8); + g_free(utf8); + continue; + } + + if (stat_directory_child(directory, utf8, &st2) == 0) + updateInDirectory(directory, utf8, &st2); + else + delete_name_in(directory, utf8); + + g_free(utf8); + } + + exclude_list_free(exclude_list); + + closedir(dir); + + directory->mtime = st->st_mtime; + + return true; +} + +static struct directory * +directory_make_child_checked(struct directory *parent, const char *path) +{ + struct directory *directory; + char *base; + struct stat st; + struct song *conflicting; + + directory = directory_get_child(parent, path); + if (directory != NULL) + return directory; + + base = g_path_get_basename(path); + + if (stat_directory_child(parent, base, &st) < 0 || + inodeFoundInParent(parent, st.st_ino, st.st_dev)) { + g_free(base); + return NULL; + } + + /* if we're adding directory paths, make sure to delete filenames + with potentially the same name */ + conflicting = songvec_find(&parent->songs, base); + if (conflicting) + delete_song(parent, conflicting); + + g_free(base); + + directory = directory_new_child(parent, path); + directory_set_stat(directory, &st); + return directory; +} + +static struct directory * +addParentPathToDB(const char *utf8path) +{ + struct directory *directory = db_get_root(); + char *duplicated = g_strdup(utf8path); + char *slash = duplicated; + + while ((slash = strchr(slash, '/')) != NULL) { + *slash = 0; + + directory = directory_make_child_checked(directory, + duplicated); + if (directory == NULL || slash == NULL) + break; + + *slash++ = '/'; + } + + g_free(duplicated); + return directory; +} + +static void +updatePath(const char *path) +{ + struct directory *parent; + char *name; + struct stat st; + + parent = addParentPathToDB(path); + if (parent == NULL) + return; + + name = g_path_get_basename(path); + + if (stat_directory_child(parent, name, &st) == 0) + updateInDirectory(parent, name, &st); + else + delete_name_in(parent, name); + + g_free(name); +} + +bool +update_walk(const char *path, bool discard) +{ + walk_discard = discard; + modified = false; + + if (path != NULL && !isRootDirectory(path)) { + updatePath(path); + } else { + struct directory *directory = db_get_root(); + struct stat st; + + if (stat_directory(directory, &st) == 0) + updateDirectory(directory, &st); + } + + return modified; +} diff --git a/src/utils.c b/src/utils.c index fc27b13c9..933a70a8c 100644 --- a/src/utils.c +++ b/src/utils.c @@ -44,7 +44,7 @@ char *parsePath(char *path) { #ifndef WIN32 - if (path[0] != '/' && path[0] != '~') { + if (!g_path_is_absolute(path) && path[0] != '~') { g_warning("\"%s\" is not an absolute path", path); return NULL; } else if (path[0] == '~') { diff --git a/src/volume.c b/src/volume.c index e7fa20a62..1be0705af 100644 --- a/src/volume.c +++ b/src/volume.c @@ -26,6 +26,8 @@ #include "output_all.h" #include "mixer_control.h" #include "mixer_all.h" +#include "mixer_type.h" +#include "event_pipe.h" #include <glib.h> @@ -39,132 +41,39 @@ #define SW_VOLUME_STATE "sw_volume: " -static enum { - VOLUME_MIXER_TYPE_SOFTWARE, - VOLUME_MIXER_TYPE_HARDWARE, - VOLUME_MIXER_TYPE_DISABLED, -} volume_mixer_type = VOLUME_MIXER_TYPE_HARDWARE; - -static int volume_software_set = 100; +static unsigned volume_software_set = 100; /** the cached hardware mixer value; invalid if negative */ static int last_hardware_volume = -1; /** the age of #last_hardware_volume */ static GTimer *hardware_volume_timer; -void volume_finish(void) -{ - if (volume_mixer_type == VOLUME_MIXER_TYPE_HARDWARE) - g_timer_destroy(hardware_volume_timer); -} - -/** - * Finds the first audio_output configuration section with the - * specified type. - */ -static struct config_param * -find_output_config(const char *type) -{ - struct config_param *param = NULL; - - while ((param = config_get_next_param(CONF_AUDIO_OUTPUT, - param)) != NULL) { - const char *param_type = - config_get_block_string(param, "type", NULL); - if (param_type != NULL && strcmp(param_type, type) == 0) - return param; - } - - return NULL; -} - /** - * Copy a (top-level) legacy mixer configuration parameter to the - * audio_output section. + * Handler for #PIPE_EVENT_MIXER. */ static void -mixer_copy_legacy_param(const char *type, const char *name) +mixer_event_callback(void) { - const struct config_param *param; - struct config_param *output; - const struct block_param *bp; - - /* see if the deprecated configuration exists */ - - param = config_get_param(name); - if (param == NULL) - return; - - g_warning("deprecated option '%s' found, moving to '%s' audio output", - name, type); - - /* determine the configuration section */ - - output = find_output_config(type); - if (output == NULL) { - /* if there is no output configuration at all, create - a new and empty configuration section for the - legacy mixer */ - - if (config_get_next_param(CONF_AUDIO_OUTPUT, NULL) != NULL) - /* there is an audio_output configuration, but - it does not match the mixer_type setting */ - g_error("no '%s' audio output found", type); - - output = config_new_param(NULL, param->line); - config_add_block_param(output, "type", type, param->line); - config_add_block_param(output, "name", type, param->line); - config_add_param(CONF_AUDIO_OUTPUT, output); - } - - bp = config_get_block_param(output, name); - if (bp != NULL) - g_error("the '%s' audio output already has a '%s' setting", - type, name); - - /* duplicate the parameter in the configuration section */ + /* flush the hardware volume cache */ + last_hardware_volume = -1; - config_add_block_param(output, name, param->value, param->line); + /* notify clients */ + idle_add(IDLE_MIXER); } -static void -mixer_reconfigure(const char *type) +void volume_finish(void) { - mixer_copy_legacy_param(type, CONF_MIXER_DEVICE); - mixer_copy_legacy_param(type, CONF_MIXER_CONTROL); + g_timer_destroy(hardware_volume_timer); } void volume_init(void) { - const struct config_param *param = config_get_param(CONF_MIXER_TYPE); - //hw mixing is by default - if (param) { - if (strcmp(param->value, VOLUME_MIXER_SOFTWARE) == 0) { - volume_mixer_type = VOLUME_MIXER_TYPE_SOFTWARE; - mixer_disable_all(); - } else if (strcmp(param->value, VOLUME_MIXER_DISABLED) == 0) { - volume_mixer_type = VOLUME_MIXER_TYPE_DISABLED; - mixer_disable_all(); - } else if (strcmp(param->value, VOLUME_MIXER_HARDWARE) == 0) { - //nothing to do - } else { - //fallback to old config behaviour - if (strcmp(param->value, VOLUME_MIXER_OSS) == 0) { - mixer_reconfigure(param->value); - } else if (strcmp(param->value, VOLUME_MIXER_ALSA) == 0) { - mixer_reconfigure(param->value); - } else { - g_error("unknown mixer type %s at line %i\n", - param->value, param->line); - } - } - } + hardware_volume_timer = g_timer_new(); - if (volume_mixer_type == VOLUME_MIXER_TYPE_HARDWARE) - hardware_volume_timer = g_timer_new(); + event_pipe_register(PIPE_EVENT_MIXER, mixer_event_callback); } -static int hardware_volume_get(void) +int volume_level_get(void) { assert(hardware_volume_timer != NULL); @@ -178,101 +87,60 @@ static int hardware_volume_get(void) return last_hardware_volume; } -static int software_volume_get(void) -{ - return volume_software_set; -} - -int volume_level_get(void) -{ - switch (volume_mixer_type) { - case VOLUME_MIXER_TYPE_SOFTWARE: - return software_volume_get(); - case VOLUME_MIXER_TYPE_HARDWARE: - return hardware_volume_get(); - case VOLUME_MIXER_TYPE_DISABLED: - return -1; - } - - /* unreachable */ - assert(false); - return -1; -} - -static bool software_volume_change(int change, bool rel) +static bool software_volume_change(unsigned volume) { - int new = change; - - if (rel) - new += volume_software_set; + assert(volume <= 100); - if (new > 100) - new = 100; - else if (new < 0) - new = 0; - - volume_software_set = new; - - /*new = 100.0*(exp(new/50.0)-1)/(M_E*M_E-1)+0.5; */ - if (new >= 100) - new = PCM_VOLUME_1; - else if (new <= 0) - new = 0; - else - new = pcm_float_to_volume((exp(new / 25.0) - 1) / - (54.5981500331F - 1)); - - setPlayerSoftwareVolume(new); + volume_software_set = volume; + mixer_all_set_software_volume(volume); return true; } -static bool hardware_volume_change(int change, bool rel) +static bool hardware_volume_change(unsigned volume) { /* reset the cache */ last_hardware_volume = -1; - return mixer_all_set_volume(change, rel); + return mixer_all_set_volume(volume); } -bool volume_level_change(int change, bool rel) +bool volume_level_change(unsigned volume) { + assert(volume <= 100); + + volume_software_set = volume; + idle_add(IDLE_MIXER); - switch (volume_mixer_type) { - case VOLUME_MIXER_TYPE_HARDWARE: - return hardware_volume_change(change, rel); - case VOLUME_MIXER_TYPE_SOFTWARE: - return software_volume_change(change, rel); - default: - return true; - } + return hardware_volume_change(volume); } -void read_sw_volume_state(FILE *fp) +bool +read_sw_volume_state(const char *line) { - char buf[sizeof(SW_VOLUME_STATE) + sizeof("100") - 1]; char *end = NULL; long int sv; - if (volume_mixer_type != VOLUME_MIXER_TYPE_SOFTWARE) - return; - while (fgets(buf, sizeof(buf), fp)) { - if (!g_str_has_prefix(buf, SW_VOLUME_STATE)) - continue; + if (!g_str_has_prefix(line, SW_VOLUME_STATE)) + return false; - g_strchomp(buf); - sv = strtol(buf + strlen(SW_VOLUME_STATE), &end, 10); - if (G_LIKELY(!*end)) - software_volume_change(sv, 0); - else - g_warning("Can't parse software volume: %s\n", buf); - return; - } + line += sizeof(SW_VOLUME_STATE) - 1; + sv = strtol(line, &end, 10); + if (*end == 0 && sv >= 0 && sv <= 100) + software_volume_change(sv); + else + g_warning("Can't parse software volume: %s\n", line); + return true; } void save_sw_volume_state(FILE *fp) { - if (volume_mixer_type == VOLUME_MIXER_TYPE_SOFTWARE) - fprintf(fp, SW_VOLUME_STATE "%d\n", volume_software_set); + fprintf(fp, SW_VOLUME_STATE "%u\n", volume_software_set); +} + +unsigned +sw_volume_state_get_hash(void) +{ + return volume_software_set; } diff --git a/src/volume.h b/src/volume.h index 99d31da4e..66e7c43f0 100644 --- a/src/volume.h +++ b/src/volume.h @@ -23,22 +23,26 @@ #include <stdbool.h> #include <stdio.h> -#define VOLUME_MIXER_OSS "oss" -#define VOLUME_MIXER_ALSA "alsa" -#define VOLUME_MIXER_SOFTWARE "software" -#define VOLUME_MIXER_HARDWARE "hardware" -#define VOLUME_MIXER_DISABLED "disabled" - void volume_init(void); void volume_finish(void); int volume_level_get(void); -bool volume_level_change(int change, bool rel); +bool volume_level_change(unsigned volume); -void read_sw_volume_state(FILE *fp); +bool +read_sw_volume_state(const char *line); void save_sw_volume_state(FILE *fp); +/** + * Generates a hash number for the current state of the software + * volume control. This is used by timer_save_state_file() to + * determine whether the state has changed and the state file should + * be saved. + */ +unsigned +sw_volume_state_get_hash(void); + #endif |