diff options
Diffstat (limited to 'src')
142 files changed, 8990 insertions, 3415 deletions
diff --git a/src/audio_format.h b/src/audio_format.h index 64087d070..a88fc3a4c 100644 --- a/src/audio_format.h +++ b/src/audio_format.h @@ -23,19 +23,68 @@ #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; @@ -88,7 +137,8 @@ static inline bool audio_format_equals(const struct audio_format *a, { 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; } /** @@ -104,20 +154,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..d29f5f449 100644 --- a/src/audio_parser.c +++ b/src/audio_parser.c @@ -41,6 +41,8 @@ audio_format_parse(struct audio_format *dest, const char *src, GError **error) { char *endptr; unsigned long value; + uint32_t rate; + uint8_t bits, channels; audio_format_clear(dest); @@ -61,7 +63,7 @@ audio_format_parse(struct audio_format *dest, const char *src, GError **error) return false; } - dest->sample_rate = value; + rate = value; /* parse sample format */ @@ -81,7 +83,7 @@ audio_format_parse(struct audio_format *dest, const char *src, GError **error) return false; } - dest->bits = value; + bits = value; /* parse channel count */ @@ -93,7 +95,9 @@ audio_format_parse(struct audio_format *dest, const char *src, GError **error) return false; } - dest->channels = value; + channels = value; + + audio_format_init(dest, rate, bits, channels); return true; } 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..9e3919152 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) { @@ -77,26 +82,26 @@ 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,9 +112,8 @@ 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); @@ -133,18 +137,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 +152,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..165e21c96 100644 --- a/src/command.c +++ b/src/command.c @@ -32,7 +32,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" @@ -58,7 +58,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" @@ -400,7 +399,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 +412,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 +420,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; } @@ -470,7 +469,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,30 +479,32 @@ 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), + 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)(getPlayerCrossFade() + 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(); client_printf(client, COMMAND_STATUS_TIME ": %i:%i\n" + "elapsed: %1.3f\n" COMMAND_STATUS_BITRATE ": %li\n" COMMAND_STATUS_AUDIO ": %u:%u:%u\n", getPlayerElapsedTime(), getPlayerTotalTime(), + pc.elapsed_time, getPlayerBitRate(), af->sample_rate, af->bits, af->channels); } @@ -520,12 +521,12 @@ handle_status(struct client *client, getPlayerErrorStr()); } - 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 +570,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 +606,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 +616,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 +632,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 +651,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 +672,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 +680,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; } @@ -869,6 +870,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 +1018,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 = g_strdup(argv[1]); + path = argv[1]; - ret = directory_update_init(path); + 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 = argv[1]; + + ret = update_enqueue(path, true); if (ret > 0) { client_printf(client, "updating_db: %i\n", ret); return COMMAND_RETURN_OK; @@ -1020,7 +1066,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 +1076,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 +1098,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 +1106,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 +1135,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 +1153,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 +1171,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 +1189,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; } @@ -1241,7 +1273,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 +1287,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 +1301,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 +1315,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 +1330,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 +1345,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); } @@ -1684,6 +1716,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 +1752,7 @@ 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 }, + { "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 +1772,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 +1925,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 */ - 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++; + 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; } + 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..d78f44760 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,90 @@ #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 = "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 +136,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 +157,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 +177,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 +247,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 +345,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 +476,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 +532,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 +601,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..ef10b9f82 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" @@ -69,9 +68,6 @@ #define CONF_INPUT "input" #define CONF_GAPLESS_MP3_PLAYBACK "gapless_mp3_playback" -#define CONF_BOOL_UNSET -1 -#define CONF_BOOL_INVALID -2 - #define DEFAULT_PLAYLIST_MAX_LENGTH (1024*16) #define DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS false @@ -79,6 +75,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 +89,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 +148,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 +180,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 +193,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/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..88122649e 100644 --- a/src/dbUtils.c +++ b/src/dbUtils.c @@ -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; 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..09f7269bd 100644 --- a/src/decoder/_flac_common.c +++ b/src/decoder/_flac_common.c @@ -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 27b0c2507..6bead85fd 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; } @@ -352,13 +354,15 @@ ffmpeg_copy_metadata(struct tag *tag, AVMetadata *m, static bool ffmpeg_tag_internal(struct ffmpeg_context *ctx) { struct tag *tag = (struct tag *) ctx->tag; - const AVFormatContext *f = ctx->format_context; + AVFormatContext *f = ctx->format_context; tag->time = 0; if (f->duration != (int64_t)AV_NOPTS_VALUE) tag->time = f->duration / AV_TIME_BASE; #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"); if (!ffmpeg_copy_metadata(tag, f->metadata, TAG_ITEM_ARTIST, "author")) ffmpeg_copy_metadata(tag, f->metadata, diff --git a/src/decoder/flac_plugin.c b/src/decoder/flac_plugin.c index 1d7a9f868..89a812f52 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 = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); + char* cs_filename; + FILE* cs_file; #endif /* libcue */ FLAC__StreamMetadata* si = FLAC__metadata_object_new(FLAC__METADATA_TYPE_STREAMINFO); FLAC__StreamMetadata* cs = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET); @@ -328,6 +330,18 @@ 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) diff --git a/src/decoder/mad_plugin.c b/src/decoder/mad_plugin.c index 1ef7183fa..c5287564f 100644 --- a/src/decoder/mad_plugin.c +++ b/src/decoder/mad_plugin.c @@ -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..e7b7bfb03 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) * diff --git a/src/decoder/modplug_plugin.c b/src/decoder/modplug_plugin.c index f636f2fa6..6c375e6a0 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,7 +184,7 @@ 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) diff --git a/src/decoder/mp4ff_plugin.c b/src/decoder/mp4ff_plugin.c index cf9382904..d2c63f983 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", 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..7ffeb1155 --- /dev/null +++ b/src/decoder/mpg123_decoder_plugin.c @@ -0,0 +1,210 @@ +/* + * 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 <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..d97abd656 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_ITEM_TITLE, tag_title); + g_free(tag_title); + } else + tag_add_item(tag, TAG_ITEM_TITLE, title); + + /* artist */ if (info.numberOfInfoStrings > 1 && info.infoString[1] != NULL) tag_add_item(tag, TAG_ITEM_ARTIST, info.infoString[1]); + /* track */ + char *track=g_strdup_printf("%d", song_num); + tag_add_item(tag, TAG_ITEM_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..4cc64459f --- /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_ITEM_TITLE, p); + + p = sf_get_string(sf, SF_STR_ARTIST); + if (p != NULL) + tag_add_item(tag, TAG_ITEM_ARTIST, p); + + p = sf_get_string(sf, SF_STR_DATE); + if (p != NULL) + tag_add_item(tag, TAG_ITEM_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..39a075176 100644..100755 --- a/src/decoder/vorbis_plugin.c +++ b/src/decoder/vorbis_plugin.c @@ -324,8 +324,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 +377,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..f3d701144 100644 --- a/src/decoder/wavpack_plugin.c +++ b/src/decoder/wavpack_plugin.c @@ -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..4cff9916c 100644 --- a/src/decoder_api.c +++ b/src/decoder_api.c @@ -57,7 +57,10 @@ void decoder_initialized(G_GNUC_UNUSED struct decoder * decoder, dc.seekable = seekable; dc.total_time = total_time; + decoder_lock(); dc.state = DECODE_STATE_DECODE; + decoder_unlock(); + notify_signal(&pc.notify); g_debug("audio_format=%u:%u:%u, seekable=%s", @@ -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,6 +110,8 @@ void decoder_command_finished(G_GNUC_UNUSED struct decoder * decoder) } dc.command = DECODE_COMMAND_NONE; + decoder_unlock(); + notify_signal(&pc.notify); } @@ -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) { @@ -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) diff --git a/src/decoder_control.c b/src/decoder_control.c index 44bb63e15..3b993431c 100644 --- a/src/decoder_control.c +++ b/src/decoder_control.c @@ -18,6 +18,7 @@ */ #include "decoder_control.h" +#include "notify.h" #include <assert.h> @@ -25,36 +26,63 @@ 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(struct notify *notify) { while (dc.command != DECODE_COMMAND_NONE) { - notify_signal(&dc.notify); + decoder_signal(); + decoder_unlock(); + notify_wait(notify); + + decoder_lock(); } } +void +dc_command_wait(struct notify *notify) +{ + decoder_lock(); + dc_command_wait_locked(notify); + decoder_unlock(); +} + static void -dc_command(struct notify *notify, enum decoder_command cmd) +dc_command_locked(struct notify *notify, enum decoder_command cmd) { dc.command = cmd; - dc_command_wait(notify); + dc_command_wait_locked(notify); +} + +static void +dc_command(struct notify *notify, enum decoder_command cmd) +{ + decoder_lock(); + dc_command_locked(notify, 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 @@ -80,15 +108,19 @@ dc_start_async(struct song *song) void dc_stop(struct notify *notify) { + 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(notify, DECODE_COMMAND_STOP); if (dc.state != DECODE_STATE_STOP && dc.state != DECODE_STATE_ERROR) - dc_command(notify, DECODE_COMMAND_STOP); + dc_command_locked(notify, DECODE_COMMAND_STOP); + + decoder_unlock(); } bool diff --git a/src/decoder_control.h b/src/decoder_control.h index 703ea256c..7e861f970 100644 --- a/src/decoder_control.h +++ b/src/decoder_control.h @@ -22,13 +22,16 @@ #include "decoder_command.h" #include "audio_format.h" -#include "notify.h" + +#include <glib.h> #include <assert.h> #define DECODE_TYPE_FILE 0 #define DECODE_TYPE_URL 1 +struct notify; + enum decoder_state { DECODE_STATE_STOP = 0, DECODE_STATE_START, @@ -48,14 +51,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; @@ -80,6 +94,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 || @@ -100,6 +154,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) { diff --git a/src/decoder_internal.c b/src/decoder_internal.c index 4a56fa5f3..1b064d0aa 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,12 @@ 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); + if ((is == NULL || decoder_input_buffer(is) <= 0) && do_wait) { + decoder_wait(); + + decoder_unlock(); notify_signal(&pc.notify); + decoder_lock(); return dc.command; } @@ -63,7 +84,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 2b1a6299a..be37896c1 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); @@ -73,8 +77,12 @@ decoder_file_decode(const struct decoder_plugin *plugin, assert(path[0] == '/'); 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,40 @@ static void decoder_run_song(const struct song *song, const char *uri) dc.state = DECODE_STATE_START; dc.command = DECODE_COMMAND_NONE; + + decoder_unlock(); notify_signal(&pc.notify); + decoder_lock(); /* 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; } @@ -179,7 +199,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 +214,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 +234,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 +254,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 +282,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 +294,28 @@ static gpointer decoder_task(G_GNUC_UNUSED gpointer arg) decoder_run(); dc.command = DECODE_COMMAND_NONE; + + decoder_unlock(); notify_signal(&pc.notify); + decoder_lock(); break; case DECODE_COMMAND_STOP: dc.command = DECODE_COMMAND_NONE; + + decoder_unlock(); notify_signal(&pc.notify); + decoder_lock(); 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/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..2016d4cba 100644 --- a/src/encoder_list.c +++ b/src/encoder_list.c @@ -25,6 +25,7 @@ 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[] = { #ifdef ENABLE_VORBIS_ENCODER @@ -33,6 +34,9 @@ static const struct encoder_plugin *encoder_plugins[] = { #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..33f92d440 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 */ 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/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..da5a215f8 --- /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_MS = 5000, +}; + +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(INOTIFY_UPDATE_DELAY_MS, + 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/lastfm_input_plugin.c b/src/input/lastfm_input_plugin.c index 0039f7069..4de62dc3b 100644 --- a/src/input/lastfm_input_plugin.c +++ b/src/input/lastfm_input_plugin.c @@ -18,26 +18,72 @@ */ #include "input/lastfm_input_plugin.h" -#include "input/curl_input_plugin.h" #include "input_plugin.h" +#include "tag.h" #include "conf.h" +#include <stdlib.h> #include <string.h> #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "input_lastfm" -static const char *lastfm_user, *lastfm_password; +static struct lastfm_data { + char *user; + char *md5; +} lastfm_data; + +struct lastfm_input { + /* our very own plugin wrapper */ + struct input_plugin wrap_plugin; + + /* pointer to input stream's plugin */ + const struct input_plugin *plugin; + + /* pointer to input stream's data */ + void *data; + + /* current track's tag */ + struct tag *tag; +}; 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); + const char *passwd = config_get_block_string(param, "password", NULL); + const char *user = config_get_block_string(param, "user", NULL); + if (passwd == NULL || user == NULL) + return false; + +#if GLIB_CHECK_VERSION(2,16,0) + lastfm_data.user = g_uri_escape_string(user, NULL, false); +#else + lastfm_data.user = g_strdup(user); +#endif + +#if GLIB_CHECK_VERSION(2,16,0) + if (strlen(passwd) != 32) + lastfm_data.md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, + passwd, strlen(passwd)); + else +#endif + lastfm_data.md5 = g_strdup(passwd); + + return true; +} - return lastfm_user != NULL && lastfm_password != NULL; +static void +lastfm_input_finish(void) +{ + g_free(lastfm_data.user); + g_free(lastfm_data.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) { @@ -78,6 +124,12 @@ lastfm_get(const char *url) 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) { @@ -98,10 +150,191 @@ lastfm_find(const char *response, const char *name) } } +/** + * Replace XML's five predefined entities with the equivalant characters. + * glib doesn't seem to have code to do this, even in the xml parser. + * We don't manage numerical character references such as &#nnnn; or &#xhhhh;. + * @param value XML text to decode. + * @return decoded string, which must be freed with g_free. + * @todo make sure this is ok for utf-8. + */ +static char * +lastfm_xmldecode(const char *value) +{ + struct entity { + const char *text; + char repl; + } entities[] = { + {"&", '&'}, + {""", '"'}, + {"'", '\''}, + {">", '>'}, + {"<", '<'} + }; + char *txt = g_strdup(value); + unsigned int i; + + for (i = 0; i < sizeof(entities)/sizeof(entities[0]); ++i) { + char *p; + int slen = strlen(entities[i].text); + while ((p = strstr(txt, entities[i].text))) { + *p = entities[i].repl; + g_strlcpy(p + 1, p + slen, strlen(p) - slen); + } + } + return txt; +} + +/** + * Extract the text between xml start and end tags specified by param tag. + * Caveat: This function does not handle nested tags properly. + * @param response XML to extract text from. + * @param tag name of tag of which text should be extracted from. + * @return text between tags specified by param tag, NULL on error; Must be freed with g_free. + */ +static char * +lastfm_xmltag(const char *response, const char *tag) +{ + char *tn = g_strconcat("<", tag, ">", NULL); + char *p, *q; + + if (!(p = strstr(response, tn))) { + g_free(tn); + return NULL; + } + + p += strlen(tn); + g_free(tn); + + tn = g_strconcat("</", tag, ">", NULL); + + if (!(q = strstr(p, tn))) { + g_free(tn); + return NULL; + } + + g_free(tn); + + return g_strndup(p, q - p); +} + +/** + * Parses xspf track and generates mpd tag. + * @return tag which must be freed with tag_free. + */ +static struct tag * +lastfm_read_tag(const char *response) +{ + struct tagalias { + enum tag_type type; + const char *xmltag; + } aliases[] = { + {TAG_ITEM_ARTIST, "creator"}, + {TAG_ITEM_TITLE, "title"}, + {TAG_ITEM_ALBUM, "album"} + }; + struct tag *tag = tag_new(); + unsigned int i; + char *track_time = lastfm_xmltag(response, "duration"); + + if (track_time != NULL) { + int mtime = strtol(track_time, 0, 0); + g_free(track_time); + + /* make sure to round up */ + tag->time = ((mtime + 999) / 1000); + } + else + tag->time = 0; + + for (i = 0; i < sizeof(aliases)/sizeof(aliases[0]); ++i) { + char *p, *value = lastfm_xmltag(response, aliases[i].xmltag); + if (value == NULL) + continue; + + p = lastfm_xmldecode(value); + g_free(value); + value = p; + + tag_add_item(tag, aliases[i].type, value); + g_free(value); + } + return tag; +} + +static size_t +lastfm_input_read_wrap(struct input_stream *is, void *ptr, size_t size) +{ + size_t ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->read)(is, ptr, size); + is->data = d; + return ret; +} + +static bool +lastfm_input_eof_wrap(struct input_stream *is) +{ + bool ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->eof)(is); + is->data = d; + return ret; +} + +static bool +lastfm_input_seek_wrap(struct input_stream *is, off_t offset, int whence) +{ + bool ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->seek)(is, offset, whence); + is->data = d; + return ret; +} + +static int +lastfm_input_buffer_wrap(struct input_stream *is) +{ + int ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->buffer)(is); + is->data = d; + return ret; +} + +static struct tag * +lastfm_input_tag(struct input_stream *is) +{ + struct lastfm_input *d = is->data; + struct tag *tag = d->tag; + d->tag = NULL; + return tag; +} + +static void +lastfm_input_close(struct input_stream *is) +{ + struct lastfm_input *d = is->data; + + if (is->plugin->close) { + is->data = d->data; + is->plugin = d->plugin; + (* is->plugin->close)(is); + } + + if (d->tag) + tag_free(d->tag); + g_free(d); +} + static bool lastfm_input_open(struct input_stream *is, const char *url) { - char *md5, *p, *q, *response, *session, *stream_url; + char *p, *q, *response, *track, *session, *stream_url; bool success; if (strncmp(url, "lastfm://", 9) != 0) @@ -109,27 +342,11 @@ lastfm_input_open(struct input_stream *is, const char *url) /* 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); + "username=", lastfm_data.user, "&" + "passwordmd5=", lastfm_data.md5, "&" + "debug=0&partner=", NULL); response = lastfm_get(p); g_free(p); @@ -148,9 +365,9 @@ lastfm_input_open(struct input_stream *is, const char *url) } #if GLIB_CHECK_VERSION(2,16,0) - q = g_uri_escape_string(session, NULL, false); - g_free(session); - session = q; + q = g_uri_escape_string(session, NULL, false); + g_free(session); + session = q; #endif /* "adjust" last.fm radio */ @@ -195,35 +412,71 @@ lastfm_input_open(struct input_stream *is, const char *url) return false; } - p = strstr(response, "<location>"); - if (p == NULL) { - g_free(response); - g_free(stream_url); - return false; - } + /* From here on, we only care about the first track, extract that + * + * Note: if you want to get information about the next track (needed + * for continuous playback) extract the other track info here too. + */ - p += 10; - q = strchr(p, '<'); + g_free(stream_url); + track = lastfm_xmltag(response, "track"); + g_free(response); - if (q == NULL) { - g_free(response); - g_free(stream_url); + /* If there are no tracks in the tracklist, it's possible that the + * station doesn't have enough content. + */ + + if (track == NULL) return false; - } - g_free(stream_url); - stream_url = g_strndup(p, q - p); - g_free(response); + stream_url = lastfm_xmltag(track, "location"); + if (stream_url == NULL) { + g_free(track); + return false; + } /* now really open the last.fm radio stream */ success = input_stream_open(is, stream_url); + if (success) { + /* instantiate our transparent wrapper plugin + * this is needed so that the backend knows what functions are + * properly available. + */ + + struct lastfm_input *d = g_new0(struct lastfm_input, 1); + d->wrap_plugin.name = "lastfm"; + d->wrap_plugin.open = lastfm_input_open; + d->wrap_plugin.close = lastfm_input_close; + d->wrap_plugin.read = lastfm_input_read_wrap; + d->wrap_plugin.eof = lastfm_input_eof_wrap; + d->wrap_plugin.tag = lastfm_input_tag; + if (is->seekable) + d->wrap_plugin.seek = lastfm_input_seek_wrap; + if (is->plugin->buffer) + d->wrap_plugin.buffer = lastfm_input_buffer_wrap; + + d->tag = lastfm_read_tag(track); + d->plugin = is->plugin; + d->data = is->data; + + /* give the backend our wrapper plugin */ + + is->plugin = &d->wrap_plugin; + is->data = d; + } + g_free(stream_url); + g_free(track); + return success; } const struct input_plugin lastfm_input_plugin = { .name = "lastfm", .init = lastfm_input_init, + .finish = lastfm_input_finish, .open = lastfm_input_open, + .close = lastfm_input_close, + .tag = lastfm_input_tag, }; diff --git a/src/input_stream.c b/src/input_stream.c index 69dc644a2..6c07da98f 100644 --- a/src/input_stream.c +++ b/src/input_stream.c @@ -27,7 +27,7 @@ #include "input/archive_input_plugin.h" #endif -#ifdef HAVE_CURL +#ifdef ENABLE_CURL #include "input/curl_input_plugin.h" #endif @@ -46,7 +46,7 @@ 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 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,7 +32,7 @@ * connected by IPC socket. */ static const char *remoteUrlPrefixes[] = { -#ifdef HAVE_CURL +#ifdef ENABLE_CURL "http://", #endif #ifdef ENABLE_LASTFM diff --git a/src/main.c b/src/main.c index 5035a4836..bb0d09eaf 100644 --- a/src/main.c +++ b/src/main.c @@ -55,6 +55,7 @@ #include "dirvec.h" #include "songvec.h" #include "tag_pool.h" +#include "inotify_update.h" #ifdef ENABLE_SQLITE #include "sticker.h" @@ -90,13 +91,40 @@ GMainLoop *main_loop; struct notify main_notify; +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 GLIB_CHECK_VERSION(2,14,0) + if (music_dir == NULL) + music_dir = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC); +#endif + + 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 * succeeded, and the caller should create the database after the * 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 +143,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,6 +161,29 @@ 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 @@ -228,9 +271,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(); @@ -251,19 +296,25 @@ int main(int argc, char *argv[]) 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(); @@ -275,9 +326,9 @@ int main(int argc, char *argv[]) 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 +336,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(); @@ -303,7 +352,7 @@ int main(int argc, char *argv[]) daemonize(options.daemon); - setup_log_output(options.stdOutput); + setup_log_output(options.log_stderr); initSigHandlers(); @@ -312,15 +361,19 @@ 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)); + if (mapper_has_music_directory()) + mpd_inotify_init(); + + config_global_check(); /* run the main loop */ @@ -330,12 +383,14 @@ int main(int argc, char *argv[]) g_main_loop_unref(main_loop); + mpd_inotify_finish(); + state_file_finish(); playerKill(); finishZeroconf(); client_manager_deinit(); listen_global_finish(); - finishPlaylist(); + playlist_global_finish(); start = clock(); db_finish(); diff --git a/src/mapper.c b/src/mapper.c index aac7c0c48..9a122d068 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) 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/software_mixer_plugin.c b/src/mixer/software_mixer_plugin.c new file mode 100644 index 000000000..661334e1b --- /dev/null +++ b/src/mixer/software_mixer_plugin.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 "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 const struct config_param *param) +{ + 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 bool +software_mixer_open(struct mixer *data) +{ + struct software_mixer *sm = (struct software_mixer *)data; + + (void)sm; + return true; +} + +static void +software_mixer_close(struct mixer *data) +{ + struct software_mixer *sm = (struct software_mixer *)data; + + (void)sm; +} + +static int +software_mixer_get_volume(struct mixer *mixer) +{ + struct software_mixer *sm = (struct software_mixer *)mixer; + + return sm->volume; +} + +static bool +software_mixer_set_volume(struct mixer *mixer, unsigned volume) +{ + 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, + .open = software_mixer_open, + .close = software_mixer_close, + .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..cd05eec85 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> @@ -70,12 +73,13 @@ 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; assert(i < audio_output_count()); + assert(volume <= 100); output = audio_output_get(i); if (!output->enabled) @@ -85,31 +89,74 @@ 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; - } - - if (volume > 100) - volume = 100; - else if (volume < 0) - volume = 0; - return mixer_set_volume(mixer, volume); } 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); +} + +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); + } +} 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..927a1276c 100644 --- a/src/mixer_control.c +++ b/src/mixer_control.c @@ -28,24 +28,11 @@ #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) { struct mixer *mixer; - //mixers are disabled (by using software volume) - if (!mixers_enabled) { - return NULL; - } assert(plugin != NULL); mixer = plugin->init(param); diff --git a/src/mixer_control.h b/src/mixer_control.h index 0f73e8f75..b8997a795 100644 --- a/src/mixer_control.h +++ b/src/mixer_control.h @@ -31,9 +31,6 @@ 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); diff --git a/src/mixer_list.h b/src/mixer_list.h index 7db4a00d8..4e81c57b2 100644 --- a/src/mixer_list.h +++ b/src/mixer_list.h @@ -25,6 +25,7 @@ #ifndef MPD_MIXER_LIST_H #define MPD_MIXER_LIST_H +extern const struct mixer_plugin software_mixer_plugin; extern const struct mixer_plugin alsa_mixer; extern const struct mixer_plugin oss_mixer; extern const struct mixer_plugin pulse_mixer; 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..f271668b1 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, 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/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_control.c b/src/output_control.c index 16c0dbb75..ef77bf4fa 100644 --- a/src/output_control.c +++ b/src/output_control.c @@ -23,6 +23,7 @@ #include "output_thread.h" #include "mixer_control.h" #include "mixer_plugin.h" +#include "filter_plugin.h" #include <assert.h> #include <stdlib.h> @@ -93,26 +94,13 @@ 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); @@ -195,4 +183,6 @@ void audio_output_finish(struct audio_output *ao) notify_deinit(&ao->notify); g_mutex_free(ao->mutex); + + filter_free(ao->filter); } diff --git a/src/output_init.c b/src/output_init.c index 927424324..66444e9a5 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,28 +64,81 @@ 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(const struct config_param *param, + const struct mixer_plugin *plugin, + struct filter *filter_chain) +{ + 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, param); + + case MIXER_TYPE_SOFTWARE: + mixer = mixer_new(&software_mixer_plugin, 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) { - const char *format; const struct audio_output_plugin *plugin = NULL; if (param) { - const char *type = NULL; + const char *p; - type = config_get_block_string(param, AUDIO_OUTPUT_TYPE, NULL); - if (type == NULL) { + p = config_get_block_string(param, AUDIO_OUTPUT_TYPE, NULL); + if (p == NULL) { g_set_error(error, 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); + "No such audio output plugin: %s", p); return false; } @@ -89,8 +150,16 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, return false; } - format = config_get_block_string(param, AUDIO_OUTPUT_FORMAT, + p = config_get_block_string(param, AUDIO_OUTPUT_FORMAT, NULL); + ao->config_audio_format = p != NULL; + if (p != NULL) { + bool success = + audio_format_parse(&ao->out_audio_format, + p, error); + if (!success) + return false; + } } else { g_warning("No \"%s\" defined in config file\n", CONF_AUDIO_OUTPUT); @@ -103,7 +172,7 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, plugin->name); ao->name = "default detected output"; - format = NULL; + ao->config_audio_format = false; } ao->plugin = plugin; @@ -112,17 +181,10 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, 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); @@ -136,11 +198,17 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, 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(param, plugin->mixer_plugin, + ao->filter); + + /* 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..4eb77cc49 100644 --- a/src/output_internal.h +++ b/src/output_internal.h @@ -21,7 +21,6 @@ #define MPD_OUTPUT_INTERNAL_H #include "audio_format.h" -#include "pcm_convert.h" #include "notify.h" #include <time.h> @@ -29,6 +28,13 @@ enum audio_output_command { AO_COMMAND_NONE = 0, 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, @@ -107,7 +113,19 @@ 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 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. + */ + struct filter *convert_filter; /** * The thread handle, or NULL if the output thread isn't diff --git a/src/output_list.c b/src/output_list.c index 81de16649..476701a1a 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 mvp_output_plugin; extern const struct audio_output_plugin jackPlugin; extern const struct audio_output_plugin httpd_output_plugin; +extern const struct audio_output_plugin recorder_output_plugin; const struct audio_output_plugin *audio_output_plugins[] = { #ifdef HAVE_SHOUT @@ -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 @@ -73,6 +78,9 @@ const struct audio_output_plugin *audio_output_plugins[] = { #ifdef ENABLE_HTTPD_OUTPUT &httpd_output_plugin, #endif +#ifdef ENABLE_RECORDER_OUTPUT + &recorder_output_plugin, +#endif NULL }; diff --git a/src/output_state.c b/src/output_state.c index c7e6c8579..5efae3626 100644 --- a/src/output_state.c +++ b/src/output_state.c @@ -49,35 +49,34 @@ saveAudioDevicesState(FILE *fp) } } -void -readAudioDevicesState(FILE *fp) +bool +readAudioDevicesState(const char *line) { - char buffer[1024]; - - while (fgets(buffer, sizeof(buffer), fp)) { - char *c, *name; - struct audio_output *ao; + long value; + char *endptr; + const char *name; + struct audio_output *ao; - g_strchomp(buffer); + if (!g_str_has_prefix(line, AUDIO_DEVICE_STATE)) + return false; - if (!g_str_has_prefix(buffer, AUDIO_DEVICE_STATE)) - continue; + line += sizeof(AUDIO_DEVICE_STATE) - 1; - c = strchr(buffer, ':'); - if (!c || !(++c)) - goto errline; + value = strtol(line, &endptr, 10); + if (*endptr != ':' || (value != 0 && value != 1)) + return false; - name = strchr(c, ':'); - if (!name || !(++name)) - goto errline; + if (value != 0) + /* state is "enabled": no-op */ + return true; - ao = audio_output_find(name); - if (ao != NULL && atoi(c) == 0) - ao->enabled = false; - - continue; -errline: - /* nonfatal */ - g_warning("invalid line in state_file: %s\n", buffer); + name = endptr + 1; + ao = audio_output_find(name); + if (ao == NULL) { + g_debug("Ignoring device state for '%s'", name); + return true; } + + ao->enabled = false; + return true; } diff --git a/src/output_state.h b/src/output_state.h index 8592574ab..b171fed18 100644 --- a/src/output_state.h +++ b/src/output_state.h @@ -25,10 +25,11 @@ #ifndef OUTPUT_STATE_H #define OUTPUT_STATE_H +#include <stdbool.h> #include <stdio.h> -void -readAudioDevicesState(FILE *fp); +bool +readAudioDevicesState(const char *line); void saveAudioDevicesState(FILE *fp); diff --git a/src/output_thread.c b/src/output_thread.c index 785ac808f..e1f20e580 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> @@ -41,6 +43,73 @@ static void ao_command_finished(struct audio_output *ao) } 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); + + /* 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; + } + + if (!ao->config_audio_format) + ao->out_audio_format = *filter_audio_format; + + success = ao_plugin_open(ao->plugin, ao->data, + &ao->out_audio_format, + &error); + + 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); + + 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:%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) { assert(ao->open); @@ -53,11 +122,69 @@ ao_close(struct audio_output *ao) g_mutex_unlock(ao->mutex); ao_plugin_close(ao->plugin, ao->data); - pcm_convert_deinit(&ao->convert_state); + filter_close(ao->filter); 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; + + g_mutex_lock(ao->mutex); + ao->chunk = NULL; + ao->open = false; + g_mutex_unlock(ao->mutex); + + ao_plugin_close(ao->plugin, ao->data); + + ao->fail_timer = g_timer_new(); + return; + } + + convert_filter_set(ao->convert_filter, &ao->out_audio_format); +} + +static void +ao_reopen(struct audio_output *ao) +{ + if (!ao->config_audio_format) { + if (ao->open) { + const struct music_pipe *mp = ao->pipe; + ao_close(ao); + 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; + } + + 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,6 +192,8 @@ 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); @@ -75,18 +204,19 @@ ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk) 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_plugin_cancel(ao->plugin, ao->data); + ao_close(ao); + + /* 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) { @@ -182,8 +312,6 @@ static void ao_pause(struct audio_output *ao) static gpointer audio_output_task(gpointer arg) { struct audio_output *ao = arg; - bool ret; - GError *error; while (1) { switch (ao->command) { @@ -191,47 +319,12 @@ static gpointer audio_output_task(gpointer arg) break; case AO_COMMAND_OPEN: - assert(!ao->open); - assert(ao->fail_timer == NULL); - assert(ao->pipe != NULL); - assert(ao->chunk == NULL); - - 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(); - } + ao_open(ao); + ao_command_finished(ao); + break; + case AO_COMMAND_REOPEN: + ao_reopen(ao); ao_command_finished(ao); break; @@ -244,6 +337,7 @@ static gpointer audio_output_task(gpointer arg) ao_plugin_cancel(ao->plugin, ao->data); ao_close(ao); + filter_close(ao->filter); ao_command_finished(ao); break; 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/player_control.c b/src/player_control.c index ac4b006dd..df80ac4ff 100644 --- a/src/player_control.c +++ b/src/player_control.c @@ -40,7 +40,6 @@ void pc_init(unsigned buffer_chunks, unsigned int buffered_before_play) 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) @@ -253,16 +252,6 @@ void setPlayerCrossFade(float crossFadeInSeconds) 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) { return pc.total_play_time; diff --git a/src/player_control.h b/src/player_control.h index b1f7481cd..0cc3c73a8 100644 --- a/src/player_control.h +++ b/src/player_control.h @@ -81,7 +81,6 @@ struct player_control { struct song *errored_song; volatile double seek_where; float cross_fade_seconds; - uint16_t software_volume; double total_play_time; }; @@ -145,8 +144,6 @@ void setPlayerCrossFade(float crossFadeInSeconds); float getPlayerCrossFade(void); -void setPlayerSoftwareVolume(int volume); - double getPlayerTotalPlayTime(void); static inline const struct audio_format * diff --git a/src/player_thread.c b/src/player_thread.c index 7fc55d3d1..b501937b6 100644 --- a/src/player_thread.c +++ b/src/player_thread.c @@ -93,12 +93,6 @@ struct player { * The current audio format for the audio outputs. */ struct audio_format play_audio_format; - - /** - * Coefficient for converting a PCM buffer size into a time - * span. - */ - double size_to_time; }; static struct music_buffer *player_buffer; @@ -141,7 +135,7 @@ player_wait_for_decoder(struct player *player) { dc_command_wait(&pc.notify); - if (decoder_has_failed()) { + if (decoder_lock_has_failed()) { assert(dc.next_song == NULL || dc.next_song->url != NULL); pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_FILE; @@ -180,10 +174,14 @@ 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); + decoder_unlock(); + pc.errored_song = dc.next_song; pc.error = PLAYER_ERROR_FILE; @@ -191,6 +189,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 +200,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 && @@ -227,6 +225,7 @@ player_check_decoder_startup(struct player *player) } else { /* the decoder is not yet ready; wait some more */ + decoder_unlock(); notify_wait(&pc.notify); return true; @@ -414,6 +413,31 @@ static void player_process_command(struct player *player) } } +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 @@ -421,31 +445,12 @@ static void player_process_command(struct player *player) */ 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); @@ -455,18 +460,6 @@ play_chunk(struct song *song, struct music_chunk *chunk, 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)) { @@ -475,7 +468,8 @@ play_chunk(struct song *song, struct music_chunk *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,13 +519,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); + decoder_signal(); + decoder_unlock(); + notify_wait(&pc.notify); return true; @@ -546,8 +547,7 @@ 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); @@ -563,10 +563,12 @@ play_next_chunk(struct player *player) /* 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; } @@ -581,8 +583,14 @@ play_next_chunk(struct player *player) 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,7 +616,6 @@ static void do_play(void) .xfade = XFADE_UNKNOWN, .cross_fading = false, .cross_fade_chunks = 0, - .size_to_time = 0.0, }; player.pipe = music_pipe_new(); @@ -643,7 +650,7 @@ static void do_play(void) 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 && @@ -678,7 +685,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 +698,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 */ @@ -729,7 +736,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 */ diff --git a/src/playlist.c b/src/playlist.c index 35c09329a..162103c2e 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); @@ -98,7 +99,8 @@ playlist_queue_song_order(struct playlist *playlist, unsigned order) * 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 +111,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 +180,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; @@ -197,13 +199,14 @@ playPlaylistOrderNumber(struct playlist *playlist, int 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 @@ -215,11 +218,11 @@ void syncPlayerAndPlaylist(struct playlist *playlist) 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,7 +236,7 @@ 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; @@ -251,37 +254,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 +300,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 +311,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 +327,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 +337,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 +373,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 +390,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 +414,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..311163e8b 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 '#' @@ -94,9 +93,11 @@ struct playlist { /** 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 +117,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 +131,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 -deleteFromPlaylistById(struct playlist *playlist, unsigned song); +playlist_delete_range(struct playlist *playlist, unsigned start, unsigned end); -void stopPlaylist(struct playlist *playlist); +enum playlist_result +playlist_delete_id(struct playlist *playlist, unsigned song); + +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_control.c b/src/playlist_control.c index 4359611fd..cfbcd19f1 100644 --- a/src/playlist_control.c +++ b/src/playlist_control.c @@ -38,7 +38,7 @@ enum { PLAYLIST_PREV_UNLESS_ELAPSED = 10, }; -void stopPlaylist(struct playlist *playlist) +void playlist_stop(struct playlist *playlist) { if (!playlist->playing) return; @@ -68,7 +68,7 @@ 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; @@ -115,28 +115,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 +157,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,19 +174,19 @@ 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; @@ -196,20 +196,20 @@ void previousSongInPlaylist(struct playlist *playlist) /* re-start playing the current song (just like the "prev" button on CD players) */ - playPlaylistOrderNumber(playlist, playlist->current); + playlist_play_order(playlist, playlist->current); } else { if (playlist->current > 0) { /* play the preceding song */ - playPlaylistOrderNumber(playlist, + playlist_play_order(playlist, playlist->current - 1); } else if (playlist->queue.repeat) { /* play the last song in "repeat" mode */ - playPlaylistOrderNumber(playlist, + playlist_play_order(playlist, queue_length(&playlist->queue) - 1); } else { /* re-start playing the current song if it's the first one */ - playPlaylistOrderNumber(playlist, playlist->current); + playlist_play_order(playlist, playlist->current); } } @@ -217,7 +217,7 @@ void previousSongInPlaylist(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) { const struct song *queued; unsigned i; @@ -241,7 +241,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 +259,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..473305f17 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,19 +205,16 @@ 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); @@ -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_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_save.c b/src/playlist_save.c index 776d3c385..103a810fb 100644 --- a/src/playlist_save.c +++ b/src/playlist_save.c @@ -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..f288a5738 100644 --- a/src/playlist_state.c +++ b/src/playlist_state.c @@ -66,9 +66,15 @@ playlist_state_save(FILE *fp, const struct playlist *playlist) playlist->current)); fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_TIME, getPlayerElapsedTime()); - } else + } 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, @@ -109,8 +115,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,44 +124,43 @@ 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 (& @@ -172,24 +177,26 @@ 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); + 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(); } + + return true; } diff --git a/src/playlist_state.h b/src/playlist_state.h index 989430264..7ed7e8c8e 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,7 @@ 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); #endif diff --git a/src/replay_gain.c b/src/replay_gain.c index bcb501e54..d21b94e0a 100644 --- a/src/replay_gain.c +++ b/src/replay_gain.c @@ -38,6 +38,7 @@ 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) { @@ -73,6 +74,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 +136,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/song_print.c b/src/song_print.c index 64ab9f6b1..2c4da6cb0 100644 --- a/src/song_print.c +++ b/src/song_print.c @@ -50,6 +50,27 @@ song_print_info(struct client *client, struct song *song) { song_print_url(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_save.c b/src/song_save.c index 8f4e1614d..3e54ce222 100644 --- a/src/song_save.c +++ b/src/song_save.c @@ -34,6 +34,12 @@ #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) { @@ -70,7 +76,7 @@ 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); @@ -93,7 +99,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 +108,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 +116,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 +131,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 +160,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/state_file.c b/src/state_file.c index 9c6475cc8..18548d03c 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,15 +31,6 @@ #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 */ @@ -47,11 +39,11 @@ static guint save_state_source_id; 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,8 +52,9 @@ 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); + saveAudioDevicesState(fp); + playlist_state_save(fp, &g_playlist); while(fclose(fp) && errno == EINTR) /* nothing */; } @@ -69,12 +62,13 @@ state_file_write(void) 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,9 +76,15 @@ 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) || + readAudioDevicesState(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 */; @@ -119,11 +119,14 @@ state_file_init(const char *path) 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/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. @@ -43,8 +43,10 @@ static struct { const char *tag_item_names[TAG_NUM_OF_ITEM_TYPES] = { "Artist", + [TAG_ARTIST_SORT] = "ArtistSort", "Album", "AlbumArtist", + [TAG_ALBUM_ARTIST_SORT] = "AlbumArtistSort", "Title", "Track", "Name", @@ -32,8 +32,10 @@ */ enum tag_type { TAG_ITEM_ARTIST, + TAG_ARTIST_SORT, TAG_ITEM_ALBUM, TAG_ITEM_ALBUM_ARTIST, + TAG_ALBUM_ARTIST_SORT, TAG_ITEM_TITLE, TAG_ITEM_TRACK, TAG_ITEM_NAME, diff --git a/src/tag_ape.c b/src/tag_ape.c index 7cbf32208..e3b848bfc 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_ITEM_TITLE] = "title", + [TAG_ITEM_ARTIST] = "artist", + [TAG_ITEM_ALBUM] = "album", + [TAG_ITEM_COMMENT] = "comment", + [TAG_ITEM_GENRE] = "genre", + [TAG_ITEM_TRACK] = "track", + [TAG_ITEM_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..c78983615 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_ITEM_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_ITEM_ARTIST); + tag_id3_import_text(ret, tag, ID3_FRAME_ALBUM_ARTIST, + TAG_ITEM_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_ITEM_TITLE); + tag_id3_import_text(ret, tag, ID3_FRAME_ALBUM, TAG_ITEM_ALBUM); + tag_id3_import_text(ret, tag, ID3_FRAME_TRACK, TAG_ITEM_TRACK); + tag_id3_import_text(ret, tag, ID3_FRAME_YEAR, TAG_ITEM_DATE); + tag_id3_import_text(ret, tag, ID3_FRAME_GENRE, TAG_ITEM_GENRE); + tag_id3_import_text(ret, tag, ID3_FRAME_COMPOSER, TAG_ITEM_COMPOSER); + tag_id3_import_text(ret, tag, "TPE3", TAG_ITEM_PERFORMER); + tag_id3_import_text(ret, tag, "TPE4", TAG_ITEM_PERFORMER); + tag_id3_import_comment(ret, tag, ID3_FRAME_COMMENT, TAG_ITEM_COMMENT); + tag_id3_import_text(ret, tag, ID3_FRAME_DISC, TAG_ITEM_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/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 593198cb9..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,688 +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); - if (song == NULL) - return true; - - // shouldn't be necessary but it's there.. - song->mtime = st->st_mtime; - - song->tag = plugin->tag_dup(map_directory_child_fs(contdir, vtrack)); - - songvec_add(&contdir->songs, song); - song = NULL; - - modified = true; - - g_free(vtrack); - } - - 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 || skip_symlink(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()) @@ -790,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; @@ -798,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); @@ -814,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; } /** @@ -863,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; @@ -888,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..b79e95ae0 --- /dev/null +++ b/src/update_walk.c @@ -0,0 +1,759 @@ +/* + * 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 "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_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); + if (song == NULL) + return true; + + // shouldn't be necessary but it's there.. + song->mtime = st->st_mtime; + + song->tag = plugin->tag_dup(map_directory_child_fs(contdir, vtrack)); + + songvec_add(&contdir->songs, song); + song = NULL; + + modified = true; + + g_free(vtrack); + } + + 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] == '.' && 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 || skip_symlink(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); +} + +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/volume.c b/src/volume.c index e7fa20a62..5f4a66837 100644 --- a/src/volume.c +++ b/src/volume.c @@ -26,6 +26,7 @@ #include "output_all.h" #include "mixer_control.h" #include "mixer_all.h" +#include "mixer_type.h" #include <glib.h> @@ -39,13 +40,7 @@ #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; @@ -54,117 +49,15 @@ 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. - */ -static void -mixer_copy_legacy_param(const char *type, const char *name) -{ - 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 */ - - config_add_block_param(output, name, param->value, param->line); -} - -static void -mixer_reconfigure(const char *type) -{ - 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); - } - } - } - - if (volume_mixer_type == VOLUME_MIXER_TYPE_HARDWARE) - hardware_volume_timer = g_timer_new(); + hardware_volume_timer = g_timer_new(); } -static int hardware_volume_get(void) +int volume_level_get(void) { assert(hardware_volume_timer != NULL); @@ -178,101 +71,54 @@ static int hardware_volume_get(void) return last_hardware_volume; } -static int software_volume_get(void) +static bool software_volume_change(unsigned volume) { - return volume_software_set; -} + assert(volume <= 100); -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) -{ - int new = change; - - if (rel) - new += volume_software_set; - - 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); } diff --git a/src/volume.h b/src/volume.h index 99d31da4e..0db231ef6 100644 --- a/src/volume.h +++ b/src/volume.h @@ -23,21 +23,16 @@ #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); |