diff options
Diffstat (limited to 'src')
110 files changed, 2336 insertions, 1249 deletions
diff --git a/src/audacious/Makefile b/src/audacious/Makefile index 89a017e..e4678ba 100644 --- a/src/audacious/Makefile +++ b/src/audacious/Makefile @@ -28,10 +28,11 @@ CPPFLAGS := -I.. -I../.. \ ${CPPFLAGS} \ ${GLIB_CFLAGS} -CPPFLAGS += -D_AUDACIOUS_CORE - LIBS := -L../libaudcore -laudcore \ ${LIBS} -lm \ ${LIBINTL} \ ${GLIB_LIBS} +ifeq ($(HAVE_MSWINDOWS),yes) +LDFLAGS := ${LDFLAGS} -Wl,-subsystem,windows +endif diff --git a/src/audacious/dbus-server.cc b/src/audacious/dbus-server.cc index c6cf76a..f208a4c 100644 --- a/src/audacious/dbus-server.cc +++ b/src/audacious/dbus-server.cc @@ -17,10 +17,12 @@ * the use of this software. */ +#include <libaudcore/audstrings.h> #include <libaudcore/drct.h> #include <libaudcore/equalizer.h> #include <libaudcore/interface.h> #include <libaudcore/playlist.h> +#include <libaudcore/plugins.h> #include <libaudcore/runtime.h> #include <libaudcore/tuple.h> @@ -347,6 +349,33 @@ static gboolean do_playqueue_remove (Obj * obj, Invoc * invoc, int pos) return true; } +static gboolean do_plugin_enable (Obj * obj, Invoc * invoc, const char * name, gboolean enable) +{ + PluginHandle * plugin = aud_plugin_lookup_basename (name); + if (! plugin) + { + AUDERR ("No such plugin: %s\n", name); + return false; + } + + aud_plugin_enable (plugin, enable); + FINISH (plugin_enable); + return true; +} + +static gboolean do_plugin_is_enabled (Obj * obj, Invoc * invoc, const char * name) +{ + PluginHandle * plugin = aud_plugin_lookup_basename (name); + if (! plugin) + { + AUDERR ("No such plugin: %s\n", name); + return false; + } + + FINISH2 (plugin_is_enabled, aud_plugin_get_enabled (plugin)); + return true; +} + static gboolean do_position (Obj * obj, Invoc * invoc) { FINISH2 (position, aud_playlist_get_position (CURRENT)); @@ -372,6 +401,19 @@ static gboolean do_quit (Obj * obj, Invoc * invoc) return true; } +static gboolean do_record (Obj * obj, Invoc * invoc) +{ + aud_drct_enable_record (! aud_drct_get_record_enabled ()); + FINISH (record); + return true; +} + +static gboolean do_recording (Obj * obj, Invoc * invoc) +{ + FINISH2 (recording, aud_drct_get_record_enabled ()); + return true; +} + static gboolean do_repeat (Obj * obj, Invoc * invoc) { FINISH2 (repeat, aud_get_bool (nullptr, "repeat")); @@ -554,35 +596,37 @@ static gboolean do_song_tuple (Obj * obj, Invoc * invoc, unsigned pos, const cha { Tuple::Field field = Tuple::field_by_name (key); Tuple tuple; - GVariant * var = nullptr; + GVariant * var; if (field >= 0) tuple = aud_playlist_entry_get_tuple (CURRENT, pos); - if (tuple) + switch (tuple.get_value_type (field)) { - switch (tuple.get_value_type (field)) - { - case Tuple::String: - var = g_variant_new_string (tuple.get_str (field)); - break; - - case Tuple::Int: - var = g_variant_new_int32 (tuple.get_int (field)); - break; - - default: - break; - } - } + case Tuple::String: + var = g_variant_new_string (tuple.get_str (field)); + break; + + case Tuple::Int: + var = g_variant_new_int32 (tuple.get_int (field)); + break; - if (! var) + default: var = g_variant_new_string (""); + break; + } FINISH2 (song_tuple, g_variant_new_variant (var)); return true; } +static gboolean do_startup_notify (Obj * obj, Invoc * invoc, const char * id) +{ + aud_ui_startup_notify (id); + FINISH (startup_notify); + return true; +} + static gboolean do_status (Obj * obj, Invoc * invoc) { const char * status = "stopped"; @@ -706,10 +750,14 @@ handlers[] = {"handle-playqueue-clear", (GCallback) do_playqueue_clear}, {"handle-playqueue-is-queued", (GCallback) do_playqueue_is_queued}, {"handle-playqueue-remove", (GCallback) do_playqueue_remove}, + {"handle-plugin-enable", (GCallback) do_plugin_enable}, + {"handle-plugin-is-enabled", (GCallback) do_plugin_is_enabled}, {"handle-position", (GCallback) do_position}, {"handle-queue-get-list-pos", (GCallback) do_queue_get_list_pos}, {"handle-queue-get-queue-pos", (GCallback) do_queue_get_queue_pos}, {"handle-quit", (GCallback) do_quit}, + {"handle-recording", (GCallback) do_recording}, + {"handle-record", (GCallback) do_record}, {"handle-repeat", (GCallback) do_repeat}, {"handle-reverse", (GCallback) do_reverse}, {"handle-seek", (GCallback) do_seek}, @@ -730,6 +778,7 @@ handlers[] = {"handle-song-length", (GCallback) do_song_length}, {"handle-song-title", (GCallback) do_song_title}, {"handle-song-tuple", (GCallback) do_song_tuple}, + {"handle-startup-notify", (GCallback) do_startup_notify}, {"handle-status", (GCallback) do_status}, {"handle-stop", (GCallback) do_stop}, {"handle-stop-after", (GCallback) do_stop_after}, @@ -748,16 +797,16 @@ static unsigned owner_id = 0; static GDBusInterfaceSkeleton * skeleton = nullptr; -static void name_acquired (GDBusConnection *, const char *, void *) +static void name_acquired (GDBusConnection *, const char * name, void *) { - AUDINFO ("Owned D-Bus name (org.atheme.audacious) on session bus.\n"); + AUDINFO ("Owned D-Bus name (%s) on session bus.\n", name); g_main_loop_quit (mainloop); } -static void name_lost (GDBusConnection *, const char *, void *) +static void name_lost (GDBusConnection *, const char * name, void *) { - AUDINFO ("Owning D-Bus name (org.atheme.audacious) failed, already taken?\n"); + AUDINFO ("Owning D-Bus name (%s) failed, already taken?\n", name); g_bus_unown_name (owner_id); owner_id = 0; @@ -765,8 +814,17 @@ static void name_lost (GDBusConnection *, const char *, void *) g_main_loop_quit (mainloop); } +StringBuf dbus_server_name () +{ + int instance = aud_get_instance (); + return (instance == 1) ? str_copy ("org.atheme.audacious") : + str_printf ("org.atheme.audacious-%d", instance); +} + StartupType dbus_server_init () { + auto startup = StartupType::Unknown; + GError * error = nullptr; GDBusConnection * bus = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, & error); GMainContext * context; @@ -785,7 +843,7 @@ StartupType dbus_server_init () context = g_main_context_new (); g_main_context_push_thread_default (context); - owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, "org.atheme.audacious", + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, dbus_server_name (), (GBusNameOwnerFlags) 0, nullptr, name_acquired, name_lost, nullptr, nullptr); mainloop = g_main_loop_new (context, true); @@ -793,14 +851,13 @@ StartupType dbus_server_init () g_main_loop_unref (mainloop); mainloop = nullptr; - g_main_context_pop_thread_default (context); - g_main_context_unref (context); - if (owner_id) - return StartupType::Server; + startup = StartupType::Server; + else + startup = StartupType::Client; - dbus_server_cleanup (); - return StartupType::Client; + g_main_context_pop_thread_default (context); + g_main_context_unref (context); ERROR: if (error) @@ -809,8 +866,10 @@ ERROR: g_error_free (error); } - dbus_server_cleanup (); - return StartupType::Unknown; + if (startup != StartupType::Server) + dbus_server_cleanup (); + + return startup; } void dbus_server_cleanup () diff --git a/src/audacious/main.cc b/src/audacious/main.cc index 8aa7e02..f3c4a5f 100644 --- a/src/audacious/main.cc +++ b/src/audacious/main.cc @@ -107,13 +107,16 @@ static bool parse_options (int argc, char * * argv) else uri = String (filename_to_uri (filename_build ({cur, arg}))); - if (uri) - filenames.append (uri); + filenames.append (uri); } else if (! arg[1]) /* "-" (standard input) */ { filenames.append (String ("stdin://")); } + else if (arg[1] >= '1' && arg[1] <= '9') /* instance number */ + { + aud_set_instance (arg[1] - '0'); + } else if (arg[1] == '-') /* long option */ { bool found = false; @@ -181,6 +184,7 @@ static void print_help () static const char pad[21] = " "; fprintf (stderr, _("Usage: audacious [OPTION] ... [FILE] ...\n\n")); + fprintf (stderr, " -1, -2, -3, etc. %s\n", _("Select instance to run/control")); for (auto & arg_info : arg_map) fprintf (stderr, " -%c, --%s%.*s%s\n", arg_info.short_arg, @@ -201,16 +205,18 @@ static void do_remote () g_type_init (); #endif - /* check whether this is the first instance */ + /* check whether the selected instance is running */ if (dbus_server_init () != StartupType::Client) return; - if (! (bus = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, & error))) - goto ERR; - - if (! (obj = obj_audacious_proxy_new_sync (bus, (GDBusProxyFlags) 0, - "org.atheme.audacious", "/org/atheme/audacious", nullptr, & error))) - goto ERR; + if (! (bus = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, & error)) || + ! (obj = obj_audacious_proxy_new_sync (bus, (GDBusProxyFlags) 0, + dbus_server_name (), "/org/atheme/audacious", nullptr, & error))) + { + AUDERR ("D-Bus error: %s\n", error->message); + g_error_free (error); + return; + } AUDINFO ("Connected to remote session.\n"); @@ -254,16 +260,13 @@ static void do_remote () if (options.mainwin) obj_audacious_call_show_main_win_sync (obj, true, nullptr, nullptr); + const char * startup_id = getenv ("DESKTOP_STARTUP_ID"); + if (startup_id) + obj_audacious_call_startup_notify_sync (obj, startup_id, nullptr, nullptr); + g_object_unref (obj); exit (EXIT_SUCCESS); - -ERR: - if (error) - { - AUDERR ("D-Bus error: %s\n", error->message); - g_error_free (error); - } } #endif @@ -321,7 +324,6 @@ static void main_cleanup () } filenames.clear (); - aud_cleanup_paths (); aud_leak_check (); } @@ -345,7 +347,6 @@ int main (int argc, char * * argv) signals_init_one (); #endif - aud_init_paths (); aud_init_i18n (); if (! parse_options (argc, argv)) diff --git a/src/audacious/main.h b/src/audacious/main.h index 58243c8..26d3b89 100644 --- a/src/audacious/main.h +++ b/src/audacious/main.h @@ -20,6 +20,8 @@ #ifndef _AUDACIOUS_MAIN_H #define _AUDACIOUS_MAIN_H +#include <libaudcore/objects.h> + /* dbus-server.c */ #ifdef USE_DBUS @@ -29,6 +31,7 @@ enum class StartupType { Unknown }; +StringBuf dbus_server_name (); StartupType dbus_server_init (); void dbus_server_cleanup (); diff --git a/src/audtool/audtool.h b/src/audtool/audtool.h index cb62488..c0bb88d 100644 --- a/src/audtool/audtool.h +++ b/src/audtool/audtool.h @@ -114,6 +114,8 @@ void playback_stopped (int, char * *); void playback_status (int, char * *); void playback_seek (int, char * *); void playback_seek_relative (int, char * *); +void playback_record (int, char * *); +void playback_recording (int, char * *); void mainwin_show (int, char * *); void show_preferences_window (int, char * *); @@ -123,6 +125,8 @@ void shutdown_audacious_server (int, char * *); void show_about_window (int, char * *); void get_version (int argc, char * * argv); +void plugin_is_enabled (int argc, char * * argv); +void plugin_enable (int argc, char * * argv); void equalizer_get_eq (int argc, char * * argv); void equalizer_get_eq_preamp (int argc, char * * argv); diff --git a/src/audtool/handlers_general.c b/src/audtool/handlers_general.c index 67e00f3..68c9da5 100644 --- a/src/audtool/handlers_general.c +++ b/src/audtool/handlers_general.c @@ -88,6 +88,10 @@ void shutdown_audacious_server (int argc, char * * argv) void get_handlers_list (int argc, char * * argv) { + audtool_report ("Usage: audtool [-#] COMMAND ..."); + audtool_report (" where # (1-9) selects the instance of Audacious to control"); + audtool_report (""); + for (int i = 0; handlers[i].name; i ++) { if (! g_ascii_strcasecmp ("<sep>", handlers[i].name)) @@ -98,6 +102,7 @@ void get_handlers_list (int argc, char * * argv) audtool_report (""); audtool_report ("Commands may be prefixed with '--' (GNU-style long options) or not, your choice."); + audtool_report ("Show/hide and enable/disable commands take an optional 'on' or 'off' argument."); audtool_report ("Report bugs to http://redmine.audacious-media-player.org/projects/audacious"); } @@ -112,3 +117,36 @@ void get_version (int argc, char * * argv) audtool_report ("Audacious %s", version); g_free (version); } + +void plugin_is_enabled (int argc, char * * argv) +{ + if (argc != 2) + { + audtool_whine_args (argv[0], "<plugin>"); + exit (1); + } + + gboolean enabled = FALSE; + obj_audacious_call_plugin_is_enabled_sync (dbus_proxy, argv[1], & enabled, NULL, NULL); + + exit (! enabled); +} + +void plugin_enable (int argc, char * * argv) +{ + gboolean enable = TRUE; + + if (argc == 2) + enable = TRUE; + else if (argc == 3 && ! g_ascii_strcasecmp (argv[2], "on")) + enable = TRUE; + else if (argc == 3 && ! g_ascii_strcasecmp (argv[2], "off")) + enable = FALSE; + else + { + audtool_whine_args (argv[0], "<plugin> <on/off>"); + exit (1); + } + + obj_audacious_call_plugin_enable_sync (dbus_proxy, argv[1], enable, NULL, NULL); +} diff --git a/src/audtool/handlers_playback.c b/src/audtool/handlers_playback.c index 1b3a90a..8a812b7 100644 --- a/src/audtool/handlers_playback.c +++ b/src/audtool/handlers_playback.c @@ -101,3 +101,16 @@ void playback_seek_relative (int argc, char * * argv) obj_audacious_call_time_sync (dbus_proxy, & oldtime, NULL, NULL); obj_audacious_call_seek_sync (dbus_proxy, MAX (0, oldtime + atof (argv[1]) * 1000), NULL, NULL); } + +void playback_record (int argc, char * * argv) +{ + obj_audacious_call_record_sync (dbus_proxy, NULL, NULL); +} + +void playback_recording (int argc, char * * argv) +{ + gboolean recording = FALSE; + obj_audacious_call_recording_sync (dbus_proxy, & recording, NULL, NULL); + + exit (! recording); +} diff --git a/src/audtool/main.c b/src/audtool/main.c index 2ed0fa9..c49d62e 100644 --- a/src/audtool/main.c +++ b/src/audtool/main.c @@ -20,6 +20,7 @@ #include <stdio.h> #include <stdlib.h> +#include <string.h> #include <locale.h> #include "audtool.h" @@ -54,6 +55,8 @@ const struct commandhandler handlers[] = {"playback-status", playback_status, "print status (playing/paused/stopped)", 0}, {"playback-seek", playback_seek, "seek to given time", 1}, {"playback-seek-relative", playback_seek_relative, "seek to relative time offset", 1}, + {"playback-record", playback_record, "toggle stream recording", 0}, + {"playback-recording", playback_recording, "exit code = 0 if recording", 0}, {"<sep>", NULL, "Playlist commands", 0}, {"playlist-advance", playlist_advance, "skip to next song", 0}, @@ -121,6 +124,8 @@ const struct commandhandler handlers[] = {"about-show", show_about_window, "show/hide About window", 1}, {"version", get_version, "print Audacious version", 0}, + {"plugin-is-enabled", plugin_is_enabled, "exit code = 0 if plugin is enabled", 1}, + {"plugin-enable", plugin_enable, "enable/disable plugin", 2}, {"shutdown", shutdown_audacious_server, "shut down Audacious", 0}, {"help", get_handlers_list, "print this help", 0}, @@ -140,7 +145,7 @@ static void audtool_disconnect (void) connection = NULL; } -static void audtool_connect (void) +static void audtool_connect (int instance) { GError * error = NULL; @@ -153,8 +158,14 @@ static void audtool_connect (void) exit (EXIT_FAILURE); } - dbus_proxy = obj_audacious_proxy_new_sync (connection, 0, - "org.atheme.audacious", "/org/atheme/audacious", NULL, & error); + char name[32]; + if (instance == 1) + strcpy (name, "org.atheme.audacious"); + else + sprintf (name, "org.atheme.audacious-%d", instance); + + dbus_proxy = obj_audacious_proxy_new_sync (connection, 0, name, + "/org/atheme/audacious", NULL, & error); if (! dbus_proxy) { @@ -169,7 +180,8 @@ static void audtool_connect (void) int main (int argc, char * * argv) { - int i, j = 0, k = 0; + int instance = 1; + int i, j, k = 0; setlocale (LC_CTYPE, ""); @@ -177,7 +189,15 @@ int main (int argc, char * * argv) g_type_init(); #endif - audtool_connect (); + // parse instance number (must come first) + if (argc >= 2 && argv[1][0] == '-' && argv[1][1] >= '1' && argv[1][1] <= '9' && ! argv[1][2]) + { + instance = argv[1][1] - '0'; + argc --; + argv ++; + } + + audtool_connect (instance); if (argc < 2) { @@ -193,7 +213,7 @@ int main (int argc, char * * argv) ! g_ascii_strcasecmp (g_strconcat ("--", handlers[i].name, NULL), argv[j])) && g_ascii_strcasecmp ("<sep>", handlers[i].name)) { - int numargs = handlers[i].args + 1 < argc - j ? handlers[i].args + 1 : argc - j; + int numargs = MIN (handlers[i].args + 1, argc - j); handlers[i].handler (numargs, & argv[j]); j += handlers[i].args; k ++; diff --git a/src/audtool/wrappers.c b/src/audtool/wrappers.c index c255954..35afa35 100644 --- a/src/audtool/wrappers.c +++ b/src/audtool/wrappers.c @@ -24,19 +24,18 @@ void generic_on_off (int argc, char * * argv, OnOffFunc func) { - gboolean show = TRUE; - - if (argc >= 2) + gboolean show; + + if (argc == 1) + show = TRUE; + else if (argc == 2 && ! g_ascii_strcasecmp (argv[1], "on")) + show = TRUE; + else if (argc == 2 && ! g_ascii_strcasecmp (argv[1], "off")) + show = FALSE; + else { - if (! g_ascii_strcasecmp (argv[1], "on")) - show = TRUE; - else if (! g_ascii_strcasecmp (argv[1], "off")) - show = FALSE; - else - { - audtool_whine_args (argv[0], "<on/off>"); - exit (1); - } + audtool_whine_args (argv[0], "<on/off>"); + exit (1); } func (dbus_proxy, show, NULL, NULL); diff --git a/src/dbus/aud-dbus.xml b/src/dbus/aud-dbus.xml index 36e2184..d028441 100644 --- a/src/dbus/aud-dbus.xml +++ b/src/dbus/aud-dbus.xml @@ -2,7 +2,7 @@ <!-- * aud-dbus.xml - * Copyright 2007-2014 Ben Tucker, Yoshiki Yazawa, Matti Hämäläinen, and + * Copyright 2007-2016 Ben Tucker, Yoshiki Yazawa, Matti Hämäläinen, and * John Lindgren * * Redistribution and use in source and binary forms, with or without @@ -28,6 +28,16 @@ <arg type="s" direction="out" name="version"/> </method> + <method name="PluginIsEnabled"> + <arg type="s" direction="in" name="plugin" /> + <arg type="b" direction="out" name="enabled" /> + </method> + + <method name="PluginEnable"> + <arg type="s" direction="in" name="plugin" /> + <arg type="b" direction="in" name="enable" /> + </method> + <!-- Quit Audacious --> <method name="Quit" /> @@ -44,6 +54,11 @@ <arg type="b" direction="in" name="show"/> </method> + <!-- Send startup notification --> + <method name="StartupNotify"> + <arg type="s" direction="in" name="id"/> + </method> + <!-- Get names of available 'standard' tuple fields --> <method name="GetTupleFields"> <!-- Return array of tuple field names --> @@ -57,6 +72,9 @@ <!-- Pause playback --> <method name="Pause" /> + <!-- Either play or pause --> + <method name="PlayPause" /> + <!-- Stop playback --> <method name="Stop" /> @@ -85,6 +103,14 @@ <arg type="s" direction="out" name="status"/> </method> + <!-- Toggle recording of stream --> + <method name="Record" /> + + <!-- Is stream recording enabled? --> + <method name="Recording"> + <arg type="b" direction="out" name="is_recording"/> + </method> + <!-- What is the bitrate, frequency, and number of channels of the --> <!-- current audio format? --> <method name="Info"> @@ -291,9 +317,6 @@ <arg type="b" direction="in" name="show"/> </method> - <!-- Either play or pause --> - <method name="PlayPause" /> - <!-- Playqueue get playlist pos --> <method name="QueueGetListPos"> <arg type="u" direction="in" name="qpos"/> diff --git a/src/libaudcore/Makefile b/src/libaudcore/Makefile index 7070823..68768ed 100644 --- a/src/libaudcore/Makefile +++ b/src/libaudcore/Makefile @@ -1,5 +1,5 @@ SHARED_LIB = ${LIB_PREFIX}audcore${LIB_SUFFIX} -LIB_MAJOR = 3 +LIB_MAJOR = 4 LIB_MINOR = 1 SRCS = adder.cc \ @@ -9,6 +9,7 @@ SRCS = adder.cc \ audstrings.cc \ charset.cc \ config.cc \ + cue-cache.cc \ drct.cc \ effect.cc \ equalizer.cc \ @@ -27,6 +28,7 @@ SRCS = adder.cc \ output.cc \ playback.cc \ playlist.cc \ + playlist-cache.cc \ playlist-files.cc \ playlist-utils.cc \ plugin-init.cc \ @@ -55,6 +57,7 @@ INCLUDES = audio.h \ audstrings.h \ drct.h \ equalizer.h \ + export.h \ hook.h \ i18n.h \ index.h \ @@ -95,7 +98,8 @@ CPPFLAGS := -I.. -I../.. \ -DHARDCODE_PLUGINDIR=\"${plugindir}\" \ -DHARDCODE_LOCALEDIR=\"${localedir}\" \ -DHARDCODE_DESKTOPFILE=\"${datarootdir}/applications/audacious.desktop\" \ - -DHARDCODE_ICONFILE=\"${datarootdir}/icons/hicolor/48x48/apps/audacious.png\" + -DHARDCODE_ICONFILE=\"${datarootdir}/icons/hicolor/48x48/apps/audacious.png\" \ + -DLIBAUDCORE_BUILD CFLAGS += ${LIB_CFLAGS} diff --git a/src/libaudcore/adder.cc b/src/libaudcore/adder.cc index a689e3b..2866036 100644 --- a/src/libaudcore/adder.cc +++ b/src/libaudcore/adder.cc @@ -1,6 +1,6 @@ /* * adder.c - * Copyright 2011-2013 John Lindgren + * Copyright 2011-2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -21,22 +21,30 @@ #include "internal.h" #include <pthread.h> +#include <stdio.h> #include <string.h> -#include <sys/stat.h> - -#include <glib/gstdio.h> #include "audstrings.h" #include "hook.h" #include "i18n.h" -#include "internal.h" #include "list.h" #include "mainloop.h" #include "plugins-internal.h" +#include "probe.h" #include "runtime.h" #include "tuple.h" +#include "interface.h" #include "vfs.h" +#ifdef _WIN32 +// regrettably, strcmp_nocase can't be used directly as a +// callback for Index::sort due to taking a third argument +static int filename_compare (const char * a, const char * b) + { return strcmp_nocase (a, b); } +#else +#define filename_compare strcmp +#endif + struct AddTask : public ListNode { int playlist_id, at; @@ -52,6 +60,7 @@ struct AddResult : public ListNode bool play; String title; Index<PlaylistAddItem> items; + bool saw_folder, filtered; }; static void * add_worker (void * unused); @@ -114,52 +123,54 @@ static void status_done_locked () hook_call ("ui hide progress", nullptr); } -static void add_file (const char * filename, Tuple && tuple, - PluginHandle * decoder, PlaylistFilterFunc filter, void * user, - AddResult * result, bool validate) +static void add_file (PlaylistAddItem && item, PlaylistFilterFunc filter, + void * user, AddResult * result, bool validate) { - if (filter && ! filter (filename, user)) - return; - - AUDINFO ("Adding file: %s\n", filename); - status_update (filename, result->items.len ()); - - if (! tuple) + AUDINFO ("Adding file: %s\n", (const char *) item.filename); + status_update (item.filename, result->items.len ()); + + /* If the item doesn't already have a valid tuple, and isn't a subtune + * itself, then probe it to expand any subtunes. The "validate" check (used + * to skip non-audio files when adding folders) is also nested within this + * block; note that "validate" is always false for subtunes. */ + if (! item.tuple.valid () && ! is_subtune (item.filename)) { VFSFile file; - if (! decoder) + if (! item.decoder) { bool fast = ! aud_get_bool (nullptr, "slow_probe"); - decoder = file_find_decoder (filename, fast, file); - if (validate && ! decoder) + item.decoder = aud_file_find_decoder (item.filename, fast, file); + if (validate && ! item.decoder) return; } - if (decoder && input_plugin_has_subtunes (decoder) && ! strchr (filename, '?')) - file_read_tag (filename, decoder, file, & tuple, nullptr); + if (item.decoder && input_plugin_has_subtunes (item.decoder)) + aud_file_read_tag (item.filename, item.decoder, file, item.tuple); } - int n_subtunes = tuple.get_n_subtunes (); + int n_subtunes = item.tuple.get_n_subtunes (); if (n_subtunes) { for (int sub = 0; sub < n_subtunes; sub ++) { - StringBuf subname = str_printf ("%s?%d", filename, tuple.get_nth_subtune (sub)); - add_file (subname, Tuple (), decoder, filter, user, result, false); + StringBuf subname = str_printf ("%s?%d", + (const char *) item.filename, item.tuple.get_nth_subtune (sub)); + + if (! filter || filter (subname, user)) + add_file ({String (subname), Tuple (), item.decoder}, filter, user, result, false); + else + result->filtered = true; } } else - result->items.append (String (filename), std::move (tuple), decoder); + result->items.append (std::move (item)); } static void add_playlist (const char * filename, PlaylistFilterFunc filter, void * user, AddResult * result, bool is_single) { - if (filter && ! filter (filename, user)) - return; - AUDINFO ("Adding playlist: %s\n", filename); status_update (filename, result->items.len ()); @@ -173,99 +184,156 @@ static void add_playlist (const char * filename, PlaylistFilterFunc filter, result->title = title; for (auto & item : items) - add_file (item.filename, std::move (item.tuple), nullptr, filter, user, result, false); + { + if (! filter || filter (item.filename, user)) + add_file (std::move (item), filter, user, result, false); + else + result->filtered = true; + } } -static void add_folder (const char * filename, PlaylistFilterFunc filter, - void * user, AddResult * result, bool is_single) +static void add_cuesheets (Index<String> & files, PlaylistFilterFunc filter, + void * user, AddResult * result) { - Index<String> cuesheets, files; - GDir * folder; - - if (filter && ! filter (filename, user)) - return; + Index<String> cuesheets; - AUDINFO ("Adding folder: %s\n", filename); - status_update (filename, result->items.len ()); + for (int i = 0; i < files.len ();) + { + if (str_has_suffix_nocase (files[i], ".cue")) + cuesheets.move_from (files, i, -1, 1, true, true); + else + i ++; + } - StringBuf path = uri_to_filename (filename); - if (! path) + if (! cuesheets.len ()) return; - if (! (folder = g_dir_open (path, 0, nullptr))) - return; + // sort cuesheet list in natural order + cuesheets.sort (str_compare_encoded); + + // sort file list in system-dependent order for duplicate removal + files.sort (filename_compare); - const char * name; - while ((name = g_dir_read_name (folder))) + for (String & cuesheet : cuesheets) { - if (str_has_suffix_nocase (name, ".cue")) - cuesheets.append (name); - else - files.append (name); - } + AUDINFO ("Adding cuesheet: %s\n", (const char *) cuesheet); + status_update (cuesheet, result->items.len ()); - g_dir_close (folder); + String title; // ignored + Index<PlaylistAddItem> items; - for (const char * cuesheet : cuesheets) - { - AUDINFO ("Found cuesheet: %s\n", cuesheet); + if (! playlist_load (cuesheet, title, items)) + continue; + + String prev_filename; + for (auto & item : items) + { + String filename = item.tuple.get_str (Tuple::AudioFile); + if (! filename) + continue; // shouldn't happen - auto is_match = [=] (const char * name) - { return same_basename (name, cuesheet); }; + if (! filter || filter (item.filename, user)) + add_file (std::move (item), filter, user, result, false); + else + result->filtered = true; + + // remove duplicates from file list + if (prev_filename && ! filename_compare (filename, prev_filename)) + continue; - files.remove_if (is_match); + int idx = files.bsearch ((const char *) filename, filename_compare); + if (idx >= 0) + files.remove (idx, 1); + + prev_filename = std::move (filename); + } } +} - files.move_from (cuesheets, 0, -1, -1, true, true); +static void add_folder (const char * filename, PlaylistFilterFunc filter, + void * user, AddResult * result, bool is_single) +{ + AUDINFO ("Adding folder: %s\n", filename); + status_update (filename, result->items.len ()); + + String error; + Index<String> files = VFSFile::read_folder (filename, error); + + if (error) + aud_ui_show_error (str_printf (_("Error reading %s:\n%s"), filename, (const char *) error)); if (! files.len ()) return; if (is_single) { - const char * last = last_path_element (path); - result->title = String (last ? last : path); + const char * slash = strrchr (filename, '/'); + if (slash) + result->title = String (str_decode_percent (slash + 1)); } - auto compare_wrapper = [] (const String & a, const String & b, void *) - { return str_compare (a, b); }; + add_cuesheets (files, filter, user, result); - files.sort (compare_wrapper, nullptr); + // sort file list in natural order (must come after add_cuesheets) + files.sort (str_compare_encoded); - for (const char * name : files) + for (const char * file : files) { - StringBuf filepath = filename_build ({path, name}); - StringBuf uri = filename_to_uri (filepath); - if (! uri) + if (filter && ! filter (file, user)) + { + result->filtered = true; continue; + } + + String error; + VFSFileTest mode = VFSFile::test_file (file, + VFSFileTest (VFS_IS_REGULAR | VFS_IS_SYMLINK | VFS_IS_DIR), error); - GStatBuf info; - if (g_lstat (filepath, & info) < 0) + if (error) + AUDERR ("%s: %s\n", file, (const char *) error); + + if (mode & VFS_IS_SYMLINK) continue; - if (S_ISREG (info.st_mode)) - { - if (str_has_suffix_nocase (name, ".cue")) - add_playlist (uri, filter, user, result, false); - else - add_file (uri, Tuple (), nullptr, filter, user, result, true); - } - else if (S_ISDIR (info.st_mode)) - add_folder (uri, filter, user, result, false); + if (mode & VFS_IS_REGULAR) + add_file ({String (file)}, filter, user, result, true); + else if (mode & VFS_IS_DIR) + add_folder (file, filter, user, result, false); } } -static void add_generic (const char * filename, Tuple && tuple, - PlaylistFilterFunc filter, void * user, AddResult * result, bool is_single) +static void add_generic (PlaylistAddItem && item, PlaylistFilterFunc filter, + void * user, AddResult * result, bool is_single) { - if (tuple) - add_file (filename, std::move (tuple), nullptr, filter, user, result, false); - else if (VFSFile::test_file (filename, VFS_IS_DIR)) - add_folder (filename, filter, user, result, is_single); - else if (aud_filename_is_playlist (filename)) - add_playlist (filename, filter, user, result, is_single); + if (filter && ! filter (item.filename, user)) + { + result->filtered = true; + return; + } + + /* If the item has a valid tuple or known decoder, or it's a subtune, then + * assume it's a playable file and skip some checks. */ + if (item.tuple.valid () || item.decoder || is_subtune (item.filename)) + add_file (std::move (item), filter, user, result, false); else - add_file (filename, Tuple (), nullptr, filter, user, result, false); + { + String error; + VFSFileTest mode = VFSFile::test_file (item.filename, + VFSFileTest (VFS_IS_DIR | VFS_NO_ACCESS), error); + + if (mode & VFS_NO_ACCESS) + aud_ui_show_error (str_printf (_("Error reading %s:\n%s"), + (const char *) item.filename, (const char *) error)); + else if (mode & VFS_IS_DIR) + { + add_folder (item.filename, filter, user, result, is_single); + result->saw_folder = true; + } + else if (aud_filename_is_playlist (item.filename)) + add_playlist (item.filename, filter, user, result, is_single); + else + add_file (std::move (item), filter, user, result, false); + } } static void start_thread_locked () @@ -308,10 +376,25 @@ static void add_finish (void * unused) int playlist, count; + if (! result->items.len ()) + { + if (result->saw_folder && ! result->filtered) + aud_ui_show_error (_("No files found.")); + goto FREE; + } + playlist = aud_playlist_by_unique_id (result->playlist_id); if (playlist < 0) /* playlist deleted */ goto FREE; + if (result->play) + { + if (aud_get_bool (nullptr, "clear_playlist")) + aud_playlist_entry_delete (playlist, 0, aud_playlist_entry_count (playlist)); + else + aud_playlist_queue_delete (playlist, 0, aud_playlist_queue_count (playlist)); + } + count = aud_playlist_entry_count (playlist); if (result->at < 0 || result->at > count) result->at = count; @@ -330,7 +413,7 @@ static void add_finish (void * unused) playlist_enable_scan (false); playlist_entry_insert_batch_raw (playlist, result->at, std::move (result->items)); - if (result->play && aud_playlist_entry_count (playlist) > count) + if (result->play) { if (! aud_get_bool (0, "shuffle")) aud_playlist_set_position (playlist, result->at); @@ -367,6 +450,8 @@ static void * add_worker (void * unused) current_playlist_id = task->playlist_id; pthread_mutex_unlock (& mutex); + playlist_cache_load (task->items); + AddResult * result = new AddResult (); result->playlist_id = task->playlist_id; @@ -376,8 +461,7 @@ static void * add_worker (void * unused) bool is_single = (task->items.len () == 1); for (auto & item : task->items) - add_generic (item.filename, std::move (item.tuple), task->filter, - task->user, result, is_single); + add_generic (std::move (item), task->filter, task->user, result, is_single); delete task; diff --git a/src/libaudcore/art-search.cc b/src/libaudcore/art-search.cc index 5188d82..0fe71e9 100644 --- a/src/libaudcore/art-search.cc +++ b/src/libaudcore/art-search.cc @@ -148,8 +148,5 @@ String art_search (const char * filename) cut_path_element (local, elem - local); String image_local = fileinfo_recursive_get_image (local, & params, 0); - if (! image_local) - return String (); - - return String (filename_to_uri (image_local)); + return image_local ? String (filename_to_uri (image_local)) : String (); } diff --git a/src/libaudcore/art.cc b/src/libaudcore/art.cc index f8ff004..6fdbda7 100644 --- a/src/libaudcore/art.cc +++ b/src/libaudcore/art.cc @@ -87,11 +87,6 @@ static void send_requests (void *) for (const String & file : queued) { hook_call ("art ready", (void *) (const char *) file); - - /* this hook is deprecated in 3.7 but kept for compatibility */ - if (file == current_ref) - hook_call ("current art ready", (void *) (const char *) file); - aud_art_unref (file); /* release temporary reference */ } } diff --git a/src/libaudcore/audstrings.cc b/src/libaudcore/audstrings.cc index cb2be04..6a89121 100644 --- a/src/libaudcore/audstrings.cc +++ b/src/libaudcore/audstrings.cc @@ -33,6 +33,10 @@ #include "internal.h" #include "runtime.h" +#define MAX_POW10 9 +static const unsigned int_pow10[MAX_POW10 + 1] = + {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; + static const char ascii_to_hex[256] = "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0" "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0" @@ -68,6 +72,12 @@ static const char swap_case[256] = #define IS_LEGAL(c) (uri_legal_table[(unsigned char) (c)]) #define SWAP_CASE(c) (swap_case[(unsigned char) (c)]) +#ifdef _WIN32 +#define IS_SEP(c) ((c) == '/' || (c) == '\\') +#else +#define IS_SEP(c) ((c) == '/') +#endif + /* strcmp() that handles nullptr safely */ EXPORT int strcmp_safe (const char * a, const char * b, int len) { @@ -184,13 +194,13 @@ EXPORT unsigned str_calc_hash (const char * s) while (len >= 8) { h = h * 1954312449 + - s[0] * 3963737313 + - s[1] * 1291467969 + - s[2] * 39135393 + - s[3] * 1185921 + - s[4] * 35937 + - s[5] * 1089 + - s[6] * 33 + + (unsigned) s[0] * 3963737313 + + (unsigned) s[1] * 1291467969 + + (unsigned) s[2] * 39135393 + + (unsigned) s[3] * 1185921 + + (unsigned) s[4] * 35937 + + (unsigned) s[5] * 1089 + + (unsigned) s[6] * 33 + s[7]; s += 8; @@ -200,9 +210,9 @@ EXPORT unsigned str_calc_hash (const char * s) if (len >= 4) { h = h * 1185921 + - s[0] * 35937 + - s[1] * 1089 + - s[2] * 33 + + (unsigned) s[0] * 35937 + + (unsigned) s[1] * 1089 + + (unsigned) s[2] * 33 + s[3]; s += 4; @@ -415,6 +425,43 @@ EXPORT StringBuf filename_normalize (StringBuf && filename) return std::move (filename); } +/* note #1: recommended order is filename_contract(filename_normalize(f)) */ +/* note #2: currently assumes filename is UTF-8 (intended for display) */ +EXPORT StringBuf filename_contract (StringBuf && filename) +{ + /* replace home folder with '~' */ + const char * home = get_home_utf8 (); + int homelen = home ? strlen (home) : 0; + + if (homelen && ! strncmp (filename, home, homelen) && + (! filename[homelen] || IS_SEP (filename[homelen]))) + { + filename[0] = '~'; + filename.remove (1, homelen - 1); + } + + return std::move (filename); +} + +/* note #1: recommended order is filename_normalize(filename_expand(f)) */ +/* note #2: currently assumes filename is UTF-8 (intended for display) */ +EXPORT StringBuf filename_expand (StringBuf && filename) +{ + /* expand leading '~' */ + if (filename[0] == '~' && (! filename[1] || IS_SEP(filename[1]))) + { + const char * home = get_home_utf8 (); + + if (home && home[0]) + { + filename[0] = home[0]; + filename.insert (1, home + 1, -1); + } + } + + return std::move (filename); +} + EXPORT StringBuf filename_get_parent (const char * filename) { StringBuf buf = filename_normalize (str_copy (filename)); @@ -454,25 +501,14 @@ EXPORT StringBuf filename_build (const std::initializer_list<const char *> & ele for (const char * s : elems) { -#ifdef _WIN32 - if (set > str && set[-1] != '/' && set[-1] != '\\') + if (set > str && ! IS_SEP (set[-1])) { if (! left) throw std::bad_alloc (); - * set ++ = '\\'; + * set ++ = G_DIR_SEPARATOR; left --; } -#else - if (set > str && set[-1] != '/') - { - if (! left) - throw std::bad_alloc (); - - * set ++ = '/'; - left --; - } -#endif int len = strlen (s); if (len > left) @@ -547,6 +583,14 @@ EXPORT StringBuf uri_to_filename (const char * uri, bool use_locale) } #endif + /* if UTF-8 was requested, make sure the result is valid */ + if (! use_locale) + { + buf.steal (str_to_utf8 (std::move (buf))); + if (! buf) + return StringBuf (); + } + return filename_normalize (std::move (buf)); } @@ -564,20 +608,10 @@ EXPORT StringBuf uri_to_display (const char * uri) if (! buf) return str_copy (_("(character encoding error)")); - if (strncmp (buf, URI_PREFIX, URI_PREFIX_LEN)) - return buf; - - buf.remove (0, URI_PREFIX_LEN); - buf.steal (filename_normalize (std::move (buf))); - - const char * home = get_home_utf8 (); - int homelen = home ? strlen (home) : 0; - - if (homelen && ! strncmp (buf, home, homelen) && - (! buf[homelen] || buf[homelen] == G_DIR_SEPARATOR)) + if (! strncmp (buf, URI_PREFIX, URI_PREFIX_LEN)) { - buf[0] = '~'; - buf.remove (1, homelen - 1); + buf.remove (0, URI_PREFIX_LEN); + return filename_contract (filename_normalize (std::move (buf))); } return buf; @@ -655,7 +689,7 @@ EXPORT StringBuf uri_construct (const char * path, const char * reference) /* absolute filename */ #ifdef _WIN32 - if (path[0] && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) + if (path[0] && path[1] == ':' && IS_SEP (path[2])) #else if (path[0] == '/') #endif @@ -860,24 +894,44 @@ EXPORT StringBuf index_to_str_list (const Index<String> & index, const char * se * have an accuracy of 6 decimal places. */ -static int str_to_uint (const char * string) +static unsigned str_to_uint (const char * string, const char * * end = nullptr, + const char * stop = nullptr) { - int val = 0; - char c; - - while ((c = * string ++) && c >= '0' && c <= '9') + unsigned val = 0; + for (char c; string != stop && (c = * string) >= '0' && c <= '9'; string ++) val = val * 10 + (c - '0'); + if (end) + * end = string; + return val; } +static int digits_for (unsigned val) +{ + int digits = 1; + + for (; val >= 1000; val /= 1000) + digits += 3; + for (; val >= 10; val /= 10) + digits ++; + + return digits; +} + +static void uint_to_str (unsigned val, char * buf, int digits) +{ + for (char * rev = buf + digits; rev > buf; val /= 10) + * (-- rev) = '0' + val % 10; +} + EXPORT int str_to_int (const char * string) { bool neg = (string[0] == '-'); if (neg || string[0] == '+') string ++; - int val = str_to_uint (string); + unsigned val = str_to_uint (string); return neg ? -val : val; } @@ -887,14 +941,14 @@ EXPORT double str_to_double (const char * string) if (neg || string[0] == '+') string ++; - double val = str_to_uint (string); - const char * p = strchr (string, '.'); + const char * p; + double val = str_to_uint (string, & p); - if (p) + if (* (p ++) == '.') { - char buf[7] = "000000"; - memcpy (buf, p + 1, strlen_bounded (p + 1, 6)); - val += str_to_uint (buf) / 1000000.0; + const char * end; + double decimal = str_to_uint (p, & end, p + MAX_POW10); + val += decimal / int_pow10[end - p]; } return neg ? -val : val; @@ -903,26 +957,18 @@ EXPORT double str_to_double (const char * string) EXPORT StringBuf int_to_str (int val) { bool neg = (val < 0); - if (neg) - val = -val; + unsigned absval = neg ? -val : val; - char buf[16]; - char * rev = buf + sizeof buf; + int digits = digits_for (absval); + StringBuf buf ((neg ? 1 : 0) + digits); - while (rev > buf) - { - * (-- rev) = '0' + val % 10; - if (! (val /= 10)) - break; - } + char * set = buf; + if (neg) + * (set ++) = '-'; - if (neg && rev > buf) - * (-- rev) = '-'; + uint_to_str (absval, set, digits); - int len = buf + sizeof buf - rev; - StringBuf buf2 (len); - memcpy (buf2, rev, len); - return buf2; + return buf; } EXPORT StringBuf double_to_str (double val) @@ -931,8 +977,8 @@ EXPORT StringBuf double_to_str (double val) if (neg) val = -val; - int i = floor (val); - int f = round ((val - i) * 1000000); + unsigned i = floor (val); + unsigned f = round ((val - i) * 1000000); if (f == 1000000) { @@ -940,15 +986,26 @@ EXPORT StringBuf double_to_str (double val) f = 0; } - StringBuf buf = str_printf ("%s%d.%06d", neg ? "-" : "", i, f); + int decimals = f ? 6 : 0; + for (; decimals && ! (f % 10); f /= 10) + decimals --; + + int digits = digits_for (i); + StringBuf buf ((neg ? 1 : 0) + digits + (decimals ? 1 : 0) + decimals); + + char * set = buf; + if (neg) + * (set ++) = '-'; + + uint_to_str (i, set, digits); - char * c = buf + buf.len (); - while (c[-1] == '0') - c --; - if (c[-1] == '.') - c --; + if (decimals) + { + set += digits; + * (set ++) = '.'; + uint_to_str (f, set, decimals); + } - buf.resize (c - buf); return buf; } @@ -1001,11 +1058,11 @@ EXPORT StringBuf double_array_to_str (const double * array, int count) EXPORT StringBuf str_format_time (int64_t milliseconds) { int hours = milliseconds / 3600000; - int minutes = (milliseconds / 60000) % 60; + int minutes = milliseconds / 60000; int seconds = (milliseconds / 1000) % 60; - if (hours) - return str_printf ("%d:%02d:%02d", hours, minutes, seconds); + if (hours && aud_get_bool (nullptr, "show_hours")) + return str_printf ("%d:%02d:%02d", hours, minutes % 60, seconds); else { bool zero = aud_get_bool (nullptr, "leading_zero"); diff --git a/src/libaudcore/audstrings.h b/src/libaudcore/audstrings.h index de47590..679fe5f 100644 --- a/src/libaudcore/audstrings.h +++ b/src/libaudcore/audstrings.h @@ -67,6 +67,8 @@ StringBuf str_to_utf8 (const char * str, int len); // no "len = -1" to avoid amb StringBuf str_to_utf8 (StringBuf && str); StringBuf filename_normalize (StringBuf && filename); +StringBuf filename_contract (StringBuf && filename); +StringBuf filename_expand (StringBuf && filename); StringBuf filename_get_parent (const char * filename); StringBuf filename_get_base (const char * filename); StringBuf filename_build (const std::initializer_list<const char *> & elems); diff --git a/src/libaudcore/config.cc b/src/libaudcore/config.cc index 9db3008..2718a6b 100644 --- a/src/libaudcore/config.cc +++ b/src/libaudcore/config.cc @@ -69,8 +69,9 @@ static const char * const core_defaults[] = { "default_gain", "0", "enable_replay_gain", "TRUE", "enable_clipping_prevention", "TRUE", - "output_bit_depth", "16", + "output_bit_depth", "-1", "output_buffer_size", "500", + "record_stream", aud::numeric_string<(int) OutputStream::AfterReplayGain>::str, "replay_gain_album", "FALSE", "replay_gain_preamp", "0", "soft_clipping", "FALSE", @@ -94,6 +95,7 @@ static const char * const core_defaults[] = { #endif "generic_title_format", "${?artist:${artist} - }${?album:${album} - }${title}", "leading_zero", "FALSE", + "show_hours", "TRUE", "metadata_fallbacks", "TRUE", "metadata_on_play", "FALSE", "show_numbers_in_pl", "FALSE", @@ -134,7 +136,7 @@ struct SaveState { Index<ConfigItem> list; }; -static int item_compare (const ConfigItem & a, const ConfigItem & b, void *) +static int item_compare (const ConfigItem & a, const ConfigItem & b) { if (a.section == b.section) return strcmp (a.key, b.key); @@ -283,7 +285,7 @@ void config_save () SaveState state = SaveState (); config.iterate (add_to_save_list, & state); - state.list.sort (item_compare, nullptr); + state.list.sort (item_compare); StringBuf path = filename_to_uri (aud_get_path (AudPath::UserDir)); path.insert (-1, "/config"); diff --git a/src/libaudcore/cue-cache.cc b/src/libaudcore/cue-cache.cc new file mode 100644 index 0000000..b821173 --- /dev/null +++ b/src/libaudcore/cue-cache.cc @@ -0,0 +1,95 @@ +/* + * cue-cache.cc + * Copyright 2016 John Lindgren + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions, and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions, and the following disclaimer in the documentation + * provided with the distribution. + * + * This software is provided "as is" and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising from + * the use of this software. + */ + +#include "cue-cache.h" +#include "multihash.h" +#include "playlist-internal.h" + +#include <pthread.h> + +enum NodeState {NotLoaded, Loading, Loaded}; + +struct CueCacheNode { + Index<PlaylistAddItem> items; + NodeState state = NotLoaded; + int refcount = 0; +}; + +static SimpleHash<String, CueCacheNode> cache; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; + +CueCacheRef::CueCacheRef (const char * filename) : + m_filename (filename) +{ + pthread_mutex_lock (& mutex); + + m_node = cache.lookup (m_filename); + if (! m_node) + m_node = cache.add (m_filename, CueCacheNode ()); + + m_node->refcount ++; + + pthread_mutex_unlock (& mutex); +} + +CueCacheRef::~CueCacheRef () +{ + pthread_mutex_lock (& mutex); + + m_node->refcount --; + if (! m_node->refcount) + cache.remove (m_filename); + + pthread_mutex_unlock (& mutex); +} + +const Index<PlaylistAddItem> & CueCacheRef::load () +{ + String title; // not used + pthread_mutex_lock (& mutex); + + switch (m_node->state) + { + case NotLoaded: + // load the cuesheet in this thread + m_node->state = Loading; + pthread_mutex_unlock (& mutex); + playlist_load (m_filename, title, m_node->items); + pthread_mutex_lock (& mutex); + + m_node->state = Loaded; + pthread_cond_broadcast (& cond); + break; + + case Loading: + // wait for cuesheet to load in another thread + while (m_node->state != Loaded) + pthread_cond_wait (& cond, & mutex); + + break; + + case Loaded: + // cuesheet already loaded + break; + } + + pthread_mutex_unlock (& mutex); + return m_node->items; +} diff --git a/src/libaudqt/volumebutton.h b/src/libaudcore/cue-cache.h index d27281d..84ab516 100644 --- a/src/libaudqt/volumebutton.h +++ b/src/libaudcore/cue-cache.h @@ -1,6 +1,6 @@ /* - * volumebutton.h - * Copyright 2014 William Pitcock + * cue-cache.h + * Copyright 2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -17,34 +17,25 @@ * the use of this software. */ -#ifndef LIBAUDQT_VOLUMEBUTTON_H -#define LIBAUDQT_VOLUMEBUTTON_H +#ifndef LIBAUDCORE_CUE_CACHE_H +#define LIBAUDCORE_CUE_CACHE_H -#include <QToolButton> +#include "index.h" +#include "tuple.h" -class QFrame; -class QSlider; +struct CueCacheNode; -namespace audqt { - -class VolumeButton : public QToolButton +class CueCacheRef { public: - VolumeButton (QWidget * parent = nullptr); - -private: - void updateIcon (int val); - void updateVolume (); - void showSlider (); - void setVolume (int val); - QToolButton * newSliderButton (int delta); + CueCacheRef (const char * filename); + ~CueCacheRef (); - void wheelEvent (QWheelEvent * e); + const Index<PlaylistAddItem> & load (); - QSlider * m_slider; - QFrame * m_container; +private: + String m_filename; + CueCacheNode * m_node; }; -} // namespace audqt - -#endif +#endif // LIBAUDCORE_CUE_CACHE_H diff --git a/src/libaudcore/drct.cc b/src/libaudcore/drct.cc index 0f957bd..d8a0589 100644 --- a/src/libaudcore/drct.cc +++ b/src/libaudcore/drct.cc @@ -199,19 +199,7 @@ static void add_list (Index<PlaylistAddItem> && items, int at, bool to_temp, boo if (to_temp) aud_playlist_set_active (aud_playlist_get_temporary ()); - int playlist = aud_playlist_get_active (); - - /* queue the new entries before deleting the old ones */ - /* this is to avoid triggering the --quit-after-play condition */ - aud_playlist_entry_insert_batch (playlist, at, std::move (items), play); - - if (play) - { - if (aud_get_bool (nullptr, "clear_playlist")) - aud_playlist_entry_delete (playlist, 0, aud_playlist_entry_count (playlist)); - else - aud_playlist_queue_delete (playlist, 0, aud_playlist_queue_count (playlist)); - } + aud_playlist_entry_insert_batch (aud_playlist_get_active (), at, std::move (items), play); } EXPORT void aud_drct_pl_add (const char * filename, int at) diff --git a/src/libaudcore/export.h b/src/libaudcore/export.h new file mode 100644 index 0000000..f91ed7c --- /dev/null +++ b/src/libaudcore/export.h @@ -0,0 +1,33 @@ +/* + * export.h + * Copyright 2016 John Lindgren + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions, and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions, and the following disclaimer in the documentation + * provided with the distribution. + * + * This software is provided "as is" and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising from + * the use of this software. + */ + +#ifndef LIBAUDCORE_EXPORT_H +#define LIBAUDCORE_EXPORT_H + +#ifdef _WIN32 + #ifdef LIBAUDCORE_BUILD + #define LIBAUDCORE_PUBLIC __declspec(dllexport) + #else + #define LIBAUDCORE_PUBLIC __declspec(dllimport) + #endif +#else + #define LIBAUDCORE_PUBLIC __attribute__ ((visibility ("default"))) +#endif + +#endif // LIBAUDCORE_EXPORT_H diff --git a/src/libaudcore/index.cc b/src/libaudcore/index.cc index 75aa559..84ca64d 100644 --- a/src/libaudcore/index.cc +++ b/src/libaudcore/index.cc @@ -1,6 +1,6 @@ /* * index.cc - * Copyright 2014 John Lindgren + * Copyright 2014-2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -42,6 +42,9 @@ static void do_erase (void * data, int len, aud::EraseFunc erase_func) EXPORT void IndexBase::clear (aud::EraseFunc erase_func) { + if (! m_data) + return; + __sync_sub_and_fetch (& misc_bytes_allocated, m_size); do_erase (m_data, m_len, erase_func); @@ -57,6 +60,9 @@ EXPORT void * IndexBase::insert (int pos, int len) assert (pos <= m_len); assert (len >= 0); + if (! len) + goto out; + if (pos < 0) pos = m_len; /* insert at end */ @@ -86,6 +92,7 @@ EXPORT void * IndexBase::insert (int pos, int len) memmove ((char *) m_data + pos + len, (char *) m_data + pos, m_len - pos); m_len += len; +out: return (char *) m_data + pos; } @@ -93,6 +100,9 @@ EXPORT void IndexBase::insert (int pos, int len, aud::FillFunc fill_func) { void * to = insert (pos, len); + if (! len) + return; + if (fill_func) fill_func (to, len); else @@ -103,6 +113,9 @@ EXPORT void IndexBase::insert (const void * from, int pos, int len, aud::CopyFun { void * to = insert (pos, len); + if (! len) + return; + if (copy_func) copy_func (from, to, len); else @@ -117,6 +130,9 @@ EXPORT void IndexBase::remove (int pos, int len, aud::EraseFunc erase_func) if (len < 0) len = m_len - pos; /* remove all following */ + if (! len) + return; + do_erase ((char *) m_data + pos, len, erase_func); memmove ((char *) m_data + pos, (char *) m_data + pos + len, m_len - pos - len); m_len -= len; @@ -130,6 +146,9 @@ EXPORT void IndexBase::erase (int pos, int len, aud::FillFunc fill_func, aud::Er if (len < 0) len = m_len - pos; /* erase all following */ + if (! len) + return; + do_erase ((char *) m_data + pos, len, erase_func); do_fill ((char *) m_data + pos, len, fill_func); } @@ -140,6 +159,9 @@ EXPORT void IndexBase::shift (int from, int to, int len, aud::FillFunc fill_func assert (from >= 0 && from + len <= m_len); assert (to >= 0 && to + len <= m_len); + if (! len) + return; + int erase_len = aud::min (len, abs (to - from)); if (to < from) @@ -165,6 +187,9 @@ EXPORT void IndexBase::move_from (IndexBase & b, int from, int to, int len, if (len < 0) len = b.m_len - from; /* copy all following */ + if (! len) + return; + if (expand) { assert (to <= m_len); @@ -192,5 +217,31 @@ EXPORT void IndexBase::move_from (IndexBase & b, int from, int to, int len, EXPORT void IndexBase::sort (CompareFunc compare, int elemsize, void * userdata) { + if (! m_len) + return; + + // since we require GLib >= 2.32, g_qsort_with_data performs a stable sort g_qsort_with_data (m_data, m_len / elemsize, elemsize, compare, userdata); } + +EXPORT int IndexBase::bsearch (const void * key, CompareFunc compare, + int elemsize, void * userdata) const +{ + int top = 0; + int bottom = m_len / elemsize; + + while (top < bottom) + { + int middle = top + (bottom - top) / 2; + int match = compare (key, (char *) m_data + middle * elemsize, userdata); + + if (match < 0) + bottom = middle; + else if (match > 0) + top = middle + 1; + else + return middle; + } + + return -1; +} diff --git a/src/libaudcore/index.h b/src/libaudcore/index.h index 56dbdc5..68957c3 100644 --- a/src/libaudcore/index.h +++ b/src/libaudcore/index.h @@ -1,6 +1,6 @@ /* * index.h - * Copyright 2014 John Lindgren + * Copyright 2014-2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -85,6 +85,7 @@ public: bool collapse, aud::FillFunc fill_func, aud::EraseFunc erase_func); void sort (CompareFunc compare, int elemsize, void * userdata); + int bsearch (const void * key, CompareFunc search, int elemsize, void * userdata) const; private: void * m_data; @@ -94,9 +95,15 @@ private: template<class T> class Index : private IndexBase { -public: - typedef int (* CompareFunc) (const T & a, const T & b, void * userdata); +private: + // provides C-style callback to generic comparison functor + template<class Key, class F> + struct WrapCompare { + static int run (const void * key, const void * val, void * func) + { return (* (F *) func) (* (const Key *) key, * (const T *) val); } + }; +public: constexpr Index () : IndexBase () {} @@ -164,6 +171,7 @@ public: return -1; } + // func(val) returns true to remove val, false to keep it template<class F> void remove_if (F func, bool clear_if_empty = false) { @@ -180,22 +188,15 @@ public: clear (); } - void sort (CompareFunc compare, void * userdata) - { - struct state_t { - CompareFunc compare; - void * userdata; - }; - - auto wrapper = [] (const void * a, const void * b, void * userdata) -> int - { - auto state = (const state_t *) userdata; - return state->compare (* (const T *) a, * (const T *) b, state->userdata); - }; + // compare(a, b) returns <0 if a<b, 0 if a=b, >0 if a>b + template<class F> + void sort (F compare) + { IndexBase::sort (WrapCompare<T, F>::run, sizeof (T), & compare); } - const state_t state = {compare, userdata}; - IndexBase::sort (wrapper, sizeof (T), (void *) & state); - } + // compare(key, val) returns <0 if key<val, 0 if key=val, >0 if key>val + template<class Key, class F> + int bsearch (const Key & key, F compare) + { return IndexBase::bsearch (& key, WrapCompare<Key, F>::run, sizeof (T), & compare); } // for use of Index as a raw data buffer // unlike insert(), does not zero-fill any added space diff --git a/src/libaudcore/inifile.h b/src/libaudcore/inifile.h index f9ee049..273d2c6 100644 --- a/src/libaudcore/inifile.h +++ b/src/libaudcore/inifile.h @@ -20,9 +20,11 @@ #ifndef LIBAUDCORE_INIFILE_H #define LIBAUDCORE_INIFILE_H +#include <libaudcore/export.h> + class VFSFile; -class IniParser +class LIBAUDCORE_PUBLIC IniParser { public: virtual ~IniParser () {} diff --git a/src/libaudcore/interface.cc b/src/libaudcore/interface.cc index bdea1ec..2d2db3d 100644 --- a/src/libaudcore/interface.cc +++ b/src/libaudcore/interface.cc @@ -115,6 +115,12 @@ EXPORT bool aud_ui_is_shown () return aud_get_bool (0, "show_interface"); } +EXPORT void aud_ui_startup_notify (const char * id) +{ + if (current_interface) + current_interface->startup_notify (id); +} + EXPORT void aud_ui_show_error (const char * message) { if (aud_get_headless_mode ()) diff --git a/src/libaudcore/interface.h b/src/libaudcore/interface.h index 43ccb38..b918a38 100644 --- a/src/libaudcore/interface.h +++ b/src/libaudcore/interface.h @@ -33,6 +33,7 @@ enum class AudMenuID { void aud_ui_show (bool show); bool aud_ui_is_shown (); +void aud_ui_startup_notify (const char * id); void aud_ui_show_error (const char * message); /* thread-safe */ void aud_ui_show_about_window (); diff --git a/src/libaudcore/internal.h b/src/libaudcore/internal.h index 9f05c25..534ba69 100644 --- a/src/libaudcore/internal.h +++ b/src/libaudcore/internal.h @@ -103,13 +103,6 @@ bool open_input_file (const char * filename, const char * mode, InputPlugin * ip, VFSFile & file, String * error = nullptr); InputPlugin * load_input_plugin (PluginHandle * decoder, String * error = nullptr); -/* internal versions of aud_file_* functions; - * these allow reuse of the same file handle during probing */ -PluginHandle * file_find_decoder (const char * filename, bool fast, - VFSFile & file, String * error = nullptr); -bool file_read_tag (const char * filename, PluginHandle * decoder, - VFSFile & file, Tuple * tuple, Index<char> * image, String * error = nullptr); - /* runtime.cc */ extern size_t misc_bytes_allocated; @@ -128,6 +121,10 @@ bool same_basename (const char * a, const char * b); const char * last_path_element (const char * path); void cut_path_element (char * path, int pos); +bool is_cuesheet_entry (const char * filename); +bool is_subtune (const char * filename); +StringBuf strip_subtune (const char * filename); + unsigned int32_hash (unsigned val); unsigned ptr_hash (const void * ptr); diff --git a/src/libaudcore/mainloop.h b/src/libaudcore/mainloop.h index 4d6ec59..dabf9e3 100644 --- a/src/libaudcore/mainloop.h +++ b/src/libaudcore/mainloop.h @@ -49,20 +49,17 @@ public: // true if a periodic timer is running // does not apply to one-time callbacks - bool running () + bool running () const { return _running; } constexpr QueuedFunc () = default; QueuedFunc (const QueuedFunc &) = delete; void operator= (const QueuedFunc &) = delete; - // added in Audacious 3.7 - // previously, all instances had to be declared static ~QueuedFunc () { stop (); } private: - int serial = 0; // no longer used, kept for ABI compatibility bool _running = false; void start (const QueuedFuncParams & params); diff --git a/src/libaudcore/objects.h b/src/libaudcore/objects.h index fd57f5e..4b98cc6 100644 --- a/src/libaudcore/objects.h +++ b/src/libaudcore/objects.h @@ -250,7 +250,7 @@ public: } // only allowed for top (or null) string - ~StringBuf (); + ~StringBuf () noexcept (false); // only allowed for top (or null) string void resize (int size); diff --git a/src/libaudcore/output.cc b/src/libaudcore/output.cc index 50e890f..4ac6ab4 100644 --- a/src/libaudcore/output.cc +++ b/src/libaudcore/output.cc @@ -25,6 +25,9 @@ #include <string.h> #include "equalizer.h" +#include "hook.h" +#include "i18n.h" +#include "interface.h" #include "internal.h" #include "plugin.h" #include "plugins.h" @@ -79,6 +82,8 @@ static pthread_cond_t cond_minor = PTHREAD_COND_INITIALIZER; static OutputPlugin * cop; /* current (primary) output plugin */ static OutputPlugin * sop; /* secondary output plugin */ +static OutputStream record_stream; + static int seek_time; static String in_filename; static Tuple in_tuple; @@ -93,13 +98,18 @@ static ReplayGainInfo gain_info; static Index<float> buffer1; static Index<char> buffer2; -static inline int get_format () +static inline int get_format (bool & automatic) { + automatic = false; + switch (aud_get_int (0, "output_bit_depth")) { case 16: return FMT_S16_NE; case 24: return FMT_S24_NE; case 32: return FMT_S32_NE; + + // return FMT_FLOAT for "auto" as well + case -1: automatic = true; default: return FMT_FLOAT; } } @@ -159,7 +169,8 @@ static void setup_output (bool new_input) if (! cop) return; - int format = get_format (); + bool automatic; + int format = get_format (automatic); AUDINFO ("Setup output, format %d, %d channels, %d Hz.\n", format, effect_channels, effect_rate); @@ -170,8 +181,25 @@ static void setup_output (bool new_input) cleanup_output (); cop->set_info (in_filename, in_tuple); - if (! cop->open_audio (format, effect_rate, effect_channels)) - return; + String error, tmp_error; + while (! cop->open_audio (format, effect_rate, effect_channels, tmp_error)) + { + /* display only the error from the first attempt */ + if (! automatic || format == FMT_FLOAT) + error = std::move (tmp_error); + + if (automatic && format == FMT_FLOAT) + format = FMT_S32_NE; + else if (automatic && format == FMT_S32_NE) + format = FMT_S16_NE; + else + { + aud_ui_show_error (error ? (const char *) error : _("Error opening output stream")); + return; + } + + AUDINFO ("Falling back to format %d.\n", format); + } s_output = true; @@ -195,20 +223,38 @@ static void setup_secondary (bool new_input) if (! sop) return; - if (s_secondary && in_channels == sec_channels && in_rate == sec_rate && + int rate, channels; + record_stream = (OutputStream) aud_get_int (0, "record_stream"); + + if (record_stream < OutputStream::AfterEffects) + { + rate = in_rate; + channels = in_channels; + } + else + { + rate = effect_rate; + channels = effect_channels; + } + + if (s_secondary && channels == sec_channels && rate == sec_rate && ! (new_input && sop->force_reopen)) return; cleanup_secondary (); sop->set_info (in_filename, in_tuple); - if (! sop->open_audio (FMT_FLOAT, in_rate, in_channels)) + String error; + if (! sop->open_audio (FMT_FLOAT, rate, channels, error)) + { + aud_ui_show_error (error ? (const char *) error : _("Error opening output stream")); return; + } s_secondary = true; - sec_channels = in_channels; - sec_rate = in_rate; + sec_channels = channels; + sec_rate = rate; } /* assumes LOCK_MINOR, s_output */ @@ -269,11 +315,16 @@ static void write_output (Index<float> & data) if (! data.len ()) return; + if (s_secondary && record_stream == OutputStream::AfterEffects) + write_secondary (data); + int out_time = aud::rescale<int64_t> (out_bytes_written, out_bytes_per_sec, 1000); vis_runner_pass_audio (out_time, data, out_channels, out_rate); eq_filter (data.begin (), data.len ()); + if (s_secondary && record_stream == OutputStream::AfterEqualizer) + write_secondary (data); if (aud_get_bool (0, "software_volume_control")) { @@ -339,9 +390,12 @@ static bool process_audio (const void * data, int size, int stop_time) else audio_from_int (data, in_format, buffer1.begin (), samples); + if (s_secondary && record_stream == OutputStream::AsDecoded) + write_secondary (buffer1); + apply_replay_gain (buffer1); - if (s_secondary) + if (s_secondary && record_stream == OutputStream::AfterReplayGain) write_secondary (buffer1); write_output (effect_process (buffer1)); @@ -701,3 +755,23 @@ bool output_plugin_set_secondary (PluginHandle * plugin) UNLOCK_MINOR; return (! plugin || sop); } + +static void record_stream_changed (void *, void *) +{ + LOCK_MINOR; + + if (s_input) + setup_secondary (false); + + UNLOCK_MINOR; +} + +void output_init () +{ + hook_associate ("set record_stream", record_stream_changed, nullptr); +} + +void output_cleanup () +{ + hook_dissociate ("set record_stream", record_stream_changed, nullptr); +} diff --git a/src/libaudcore/output.h b/src/libaudcore/output.h index 391d06a..a9fafee 100644 --- a/src/libaudcore/output.h +++ b/src/libaudcore/output.h @@ -26,6 +26,9 @@ class PluginHandle; class Tuple; +void output_init (); +void output_cleanup (); + bool output_open_audio (const String & filename, const Tuple & tuple, int format, int rate, int channels, int start_time); void output_set_tuple (const Tuple & tuple); diff --git a/src/libaudcore/playback.cc b/src/libaudcore/playback.cc index 1391990..b7e94a0 100644 --- a/src/libaudcore/playback.cc +++ b/src/libaudcore/playback.cc @@ -144,7 +144,7 @@ void playback_set_info (int entry, Tuple && tuple) if (! lock_if (in_sync)) return; - if (tuple && tuple != pb_info.tuple) + if (tuple.valid () && tuple != pb_info.tuple) { pb_info.tuple = std::move (tuple); @@ -294,10 +294,13 @@ static void run_playback () if (! lock_if (in_sync)) return; - pb_info.filename = std::move (dec.filename); + // for a cuesheet entry, determine the source filename + pb_info.filename = pb_info.tuple.get_str (Tuple::AudioFile); + if (! pb_info.filename) + pb_info.filename = std::move (dec.filename); // check that we have all the necessary data - if (! pb_info.filename || ! pb_info.tuple || ! dec.ip || + if (! pb_info.filename || ! pb_info.tuple.valid () || ! dec.ip || (! dec.ip->input_info.keys[InputKey::Scheme] && ! dec.file)) { pb_info.error = true; @@ -616,25 +619,6 @@ EXPORT int InputPlugin::check_seek () return seek; } -/* compatibility (non-virtual) implementation of InputPlugin::read_tag(). */ -EXPORT bool InputPlugin::default_read_tag (const char * filename, - VFSFile & file, Tuple * tuple, Index<char> * image) -{ - /* just call read_tuple() and read_image() */ - if (tuple) - { - if (! (* tuple = read_tuple (filename, file))) - return false; - if (image && file && file.fseek (0, VFS_SEEK_SET) != 0) - return true; /* true: tuple was read */ - } - - if (image) - * image = read_image (filename, file); - - return true; -} - // thread-safe EXPORT bool aud_drct_get_playing () { diff --git a/src/libaudcore/playlist-cache.cc b/src/libaudcore/playlist-cache.cc new file mode 100644 index 0000000..9711235 --- /dev/null +++ b/src/libaudcore/playlist-cache.cc @@ -0,0 +1,88 @@ +/* + * playlist-cache.h + * Copyright 2016 John Lindgren + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions, and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions, and the following disclaimer in the documentation + * provided with the distribution. + * + * This software is provided "as is" and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising from + * the use of this software. + */ + +#include "playlist-internal.h" +#include "mainloop.h" +#include "multihash.h" + +#include <pthread.h> + +static SimpleHash<String, PlaylistAddItem> cache; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static QueuedFunc clear_timer; + +EXPORT void aud_playlist_cache_selected (int playlist) +{ + pthread_mutex_lock (& mutex); + + int entries = aud_playlist_entry_count (playlist); + + for (int i = 0; i < entries; i ++) + { + if (! aud_playlist_entry_get_selected (playlist, i)) + continue; + + String filename = aud_playlist_entry_get_filename (playlist, i); + Tuple tuple = aud_playlist_entry_get_tuple (playlist, i, Playlist::NoWait); + PluginHandle * decoder = aud_playlist_entry_get_decoder (playlist, i, Playlist::NoWait); + + if (tuple.valid () || decoder) + cache.add (filename, {filename, std::move (tuple), decoder}); + } + + clear_timer.queue (30000, (QueuedFunc::Func) playlist_cache_clear, nullptr); + + pthread_mutex_unlock (& mutex); +} + +void playlist_cache_load (Index<PlaylistAddItem> & items) +{ + pthread_mutex_lock (& mutex); + + if (! cache.n_items ()) + goto out; + + for (auto & item : items) + { + if (item.tuple.valid () && item.decoder) + continue; + + auto node = cache.lookup (item.filename); + if (! node) + continue; + + if (! item.tuple.valid () && node->tuple.valid ()) + item.tuple = node->tuple.ref (); + if (! item.decoder && node->decoder) + item.decoder = node->decoder; + } + +out: + pthread_mutex_unlock (& mutex); +} + +void playlist_cache_clear () +{ + pthread_mutex_lock (& mutex); + + cache.clear (); + clear_timer.stop (); + + pthread_mutex_unlock (& mutex); +} diff --git a/src/libaudcore/playlist-files.cc b/src/libaudcore/playlist-files.cc index 4fa9303..872e9ff 100644 --- a/src/libaudcore/playlist-files.cc +++ b/src/libaudcore/playlist-files.cc @@ -59,7 +59,7 @@ bool playlist_load (const char * filename, String & title, Index<PlaylistAddItem AUDINFO ("Trying playlist plugin %s.\n", aud_plugin_get_name (plugin)); plugin_found = true; - PlaylistPlugin * pp = (PlaylistPlugin *) aud_plugin_get_header (plugin); + auto pp = (PlaylistPlugin *) aud_plugin_get_header (plugin); if (! pp) continue; @@ -88,6 +88,11 @@ bool playlist_load (const char * filename, String & title, Index<PlaylistAddItem return false; } +// This procedure is only used when loading playlists from ~/.config/audacious; +// hence, it is drastically simpler than the full-featured routines in adder.cc. +// All support for adding folders, cuesheets, subtunes, etc. is omitted here. +// Additionally, in order to avoid heavy I/O at startup, failed entries are not +// rescanned; they can be rescanned later by refreshing the playlist. */ bool playlist_insert_playlist_raw (int list, int at, const char * filename) { String title; diff --git a/src/libaudcore/playlist-internal.h b/src/libaudcore/playlist-internal.h index f91b183..781cd4c 100644 --- a/src/libaudcore/playlist-internal.h +++ b/src/libaudcore/playlist-internal.h @@ -53,6 +53,10 @@ bool playlist_next_song (int playlist, bool repeat); DecodeInfo playback_entry_read (int serial); void playback_entry_set_tuple (int serial, Tuple && tuple); +/* playlist-cache.cc */ +void playlist_cache_load (Index<PlaylistAddItem> & items); +void playlist_cache_clear (); + /* playlist-files.cc */ bool playlist_load (const char * filename, String & title, Index<PlaylistAddItem> & items); bool playlist_insert_playlist_raw (int list, int at, const char * filename); diff --git a/src/libaudcore/playlist-utils.cc b/src/libaudcore/playlist-utils.cc index 8622c91..d1f8ffd 100644 --- a/src/libaudcore/playlist-utils.cc +++ b/src/libaudcore/playlist-utils.cc @@ -196,7 +196,7 @@ EXPORT void aud_playlist_remove_duplicates_by_scheme (int playlist, Playlist::So { Tuple current = aud_playlist_entry_get_tuple (playlist, count); - if (last && current && compare (last, current) == 0) + if (last.valid () && current.valid () && compare (last, current) == 0) aud_playlist_entry_set_selected (playlist, count, true); last = std::move (current); @@ -209,16 +209,16 @@ EXPORT void aud_playlist_remove_duplicates_by_scheme (int playlist, Playlist::So EXPORT void aud_playlist_remove_failed (int playlist) { int entries = aud_playlist_entry_count (playlist); - int count; aud_playlist_select_all (playlist, false); - for (count = 0; count < entries; count ++) + for (int count = 0; count < entries; count ++) { String filename = aud_playlist_entry_get_filename (playlist, count); - /* vfs_file_test() only works for file:// URIs currently */ - if (! strncmp (filename, "file://", 7) && ! VFSFile::test_file (filename, VFS_EXISTS)) + /* use VFS_NO_ACCESS since VFS_EXISTS doesn't distinguish between + * inaccessible files and URI schemes that don't support file_test() */ + if (VFSFile::test_file (filename, VFS_NO_ACCESS)) aud_playlist_entry_set_selected (playlist, count, true); } @@ -342,7 +342,7 @@ static void save_playlists_real () if (playlist_get_modified (i)) { StringBuf path = filename_build ({folder, name}); - aud_playlist_save (i, filename_to_uri (path), Playlist::Nothing); + aud_playlist_save (i, filename_to_uri (path), Playlist::NoWait); playlist_set_modified (i, false); } diff --git a/src/libaudcore/playlist.cc b/src/libaudcore/playlist.cc index a2b413c..9bfc718 100644 --- a/src/libaudcore/playlist.cc +++ b/src/libaudcore/playlist.cc @@ -112,7 +112,6 @@ struct Entry { void format (); void set_tuple (Tuple && new_tuple); - void set_failed (const String & new_error); String filename; PluginHandle * decoder; @@ -217,16 +216,17 @@ void Entry::format () void Entry::set_tuple (Tuple && new_tuple) { - /* Hack: We cannot refresh segmented entries (since their info is read from - * the cue sheet when it is first loaded), so leave them alone. -jlindgren */ - if (tuple.get_value_type (Tuple::StartTime) == Tuple::Int) + /* Since 3.8, cuesheet entries are handled differently. The entry filename + * points to the .cue file, and the path to the actual audio file is stored + * in the Tuple::AudioFile. If Tuple::AudioFile is not set, then assume + * that the playlist was created by an older version of Audacious, and + * revert to the former behavior (don't refresh this entry). */ + if (tuple.is_set (Tuple::StartTime) && ! tuple.is_set (Tuple::AudioFile)) return; - scanned = (bool) new_tuple; - failed = false; error = String (); - if (! new_tuple) + if (! new_tuple.valid ()) new_tuple.set_filename (filename); length = aud::max (0, new_tuple.get_int (Tuple::Length)); @@ -248,21 +248,12 @@ void PlaylistData::set_entry_tuple (Entry * entry, Tuple && tuple) selected_length += entry->length; } -void Entry::set_failed (const String & new_error) -{ - scanned = true; - failed = true; - error = new_error; -} - Entry::Entry (PlaylistAddItem && item) : filename (item.filename), decoder (item.decoder), number (-1), length (0), shuffle_num (0), - scanned (false), - failed (false), selected (false), queued (false) { @@ -351,14 +342,6 @@ static void update (void *) hook_call ("playlist update", aud::to_ptr (level)); } -static void send_playback_info (Entry * entry) -{ - // if the entry was not scanned or failed to scan, we must still call - // playback_set_info() in order to update the entry number - Tuple tuple = (entry->scanned && ! entry->failed) ? entry->tuple.ref () : Tuple (); - playback_set_info (entry->number, std::move (tuple)); -} - static void queue_update (UpdateLevel level, PlaylistData * p, int at, int count, int flags = 0) { if (p) @@ -369,7 +352,7 @@ static void queue_update (UpdateLevel level, PlaylistData * p, int at, int count if (level >= Metadata) { if (p == playing_playlist && p->position) - send_playback_info (p->position); + playback_set_info (p->position->number, p->position->tuple.ref ()); p->modified = true; } @@ -497,12 +480,15 @@ static ScanItem * scan_list_find_request (ScanRequest * request) static void scan_queue_entry (PlaylistData * playlist, Entry * entry, bool for_playback = false) { int flags = 0; - if (! entry->scanned || entry->failed) + if (! entry->tuple.valid ()) flags |= SCAN_TUPLE; if (for_playback) flags |= (SCAN_IMAGE | SCAN_FILE); - auto request = new ScanRequest (entry->filename, flags, scan_finish, entry->decoder); + /* scanner uses Tuple::AudioFile from existing tuple, if valid */ + auto request = new ScanRequest (entry->filename, flags, scan_finish, + entry->decoder, (flags & SCAN_TUPLE) ? Tuple () : entry->tuple.ref ()); + scan_list.append (new ScanItem (playlist, entry, request, for_playback)); /* playback entry will be scanned by the playback thread */ @@ -558,7 +544,8 @@ static bool scan_queue_next_entry () Entry * entry = playlist->entries[scan_row ++].get (); // blacklist stdin - if (! entry->scanned && ! scan_list_find_entry (entry) && + if (entry->tuple.state () == Tuple::Initial && + ! scan_list_find_entry (entry) && strncmp (entry->filename, "stdin://", 8)) { scan_queue_entry (playlist, entry); @@ -611,14 +598,20 @@ static void scan_finish (ScanRequest * request) if (! entry->decoder) entry->decoder = request->decoder; - if ((! entry->scanned || entry->failed) && request->tuple) + if (! entry->tuple.valid () && request->tuple.valid ()) { playlist->set_entry_tuple (entry, std::move (request->tuple)); queue_update (Metadata, playlist, entry->number, 1, DelayedUpdate); } - if (! entry->decoder || ! entry->scanned) - entry->set_failed (request->error); + if (! entry->decoder || ! entry->tuple.valid ()) + entry->error = request->error; + + if (entry->tuple.state () == Tuple::Initial) + { + entry->tuple.set_state (Tuple::Failed); + queue_update (Metadata, playlist, entry->number, 1, DelayedUpdate); + } delete item; @@ -690,8 +683,7 @@ static Entry * get_entry (int playlist_num, int entry_num, return entry; // check whether requested data (decoder and/or tuple) has been read - if ((! need_decoder || entry->decoder) && - (! need_tuple || (entry->scanned && ! entry->failed))) + if ((! need_decoder || entry->decoder) && (! need_tuple || entry->tuple.valid ())) return entry; // start scan if not already running ... @@ -717,6 +709,8 @@ static void start_playback (int seek_time, bool pause) playback_play (seek_time, pause); + // playback always begins with a rescan of the current entry in order to + // open the file, ensure a valid tuple, and read album art scan_cancel (playing_playlist->position); scan_queue_entry (playing_playlist, playing_playlist->position, true); } @@ -752,6 +746,7 @@ void playlist_init () hook_associate ("set metadata_on_play", (HookFunction) playlist_trigger_scan, nullptr); hook_associate ("set generic_title_format", (HookFunction) playlist_reformat_titles, nullptr); hook_associate ("set leading_zero", (HookFunction) playlist_reformat_titles, nullptr); + hook_associate ("set show_hours", (HookFunction) playlist_reformat_titles, nullptr); hook_associate ("set metadata_fallbacks", (HookFunction) playlist_reformat_titles, nullptr); hook_associate ("set show_numbers_in_pl", (HookFunction) playlist_reformat_titles, nullptr); } @@ -772,9 +767,12 @@ void playlist_end () hook_dissociate ("set metadata_on_play", (HookFunction) playlist_trigger_scan); hook_dissociate ("set generic_title_format", (HookFunction) playlist_reformat_titles); hook_dissociate ("set leading_zero", (HookFunction) playlist_reformat_titles); + hook_dissociate ("set show_hours", (HookFunction) playlist_reformat_titles); hook_dissociate ("set metadata_fallbacks", (HookFunction) playlist_reformat_titles); hook_dissociate ("set show_numbers_in_pl", (HookFunction) playlist_reformat_titles); + playlist_cache_clear (); + ENTER; /* playback should already be stopped */ @@ -1239,9 +1237,7 @@ EXPORT PluginHandle * aud_playlist_entry_get_decoder (int playlist_num, { ENTER; - const bool wait = (mode == Wait || mode == WaitGuess); - - Entry * entry = get_entry (playlist_num, entry_num, wait, false); + Entry * entry = get_entry (playlist_num, entry_num, (mode == Wait), false); PluginHandle * decoder = entry ? entry->decoder : nullptr; if (error) @@ -1255,14 +1251,8 @@ EXPORT Tuple aud_playlist_entry_get_tuple (int playlist_num, int entry_num, { ENTER; - const bool wait = (mode == Wait || mode == WaitGuess); - const bool guess = (mode == Guess || mode == WaitGuess); - - Entry * entry = get_entry (playlist_num, entry_num, false, wait); - - Tuple tuple; - if (entry && ((entry->scanned && ! entry->failed) || guess)) - tuple = entry->tuple.ref (); + Entry * entry = get_entry (playlist_num, entry_num, false, (mode == Wait)); + Tuple tuple = entry ? entry->tuple.ref () : Tuple (); if (error) * error = entry ? entry->error : String (); @@ -1660,27 +1650,19 @@ struct CompareData { PlaylistTupleCompareFunc tuple_compare; }; -static int compare_cb (const SmartPtr<Entry> & a, const SmartPtr<Entry> & b, void * _data) +static void sort_entries (Index<SmartPtr<Entry>> & entries, CompareData * data) { - CompareData * data = (CompareData *) _data; - - int diff = 0; - - if (data->filename_compare) - diff = data->filename_compare (a->filename, b->filename); - else if (data->tuple_compare) - diff = data->tuple_compare (a->tuple, b->tuple); - - if (diff) - return diff; - - /* preserve order of "equal" entries */ - return a->number - b->number; + entries.sort ([data] (const SmartPtr<Entry> & a, const SmartPtr<Entry> & b) { + if (data->filename_compare) + return data->filename_compare (a->filename, b->filename); + else + return data->tuple_compare (a->tuple, b->tuple); + }); } static void sort (PlaylistData * playlist, CompareData * data) { - playlist->entries.sort (compare_cb, data); + sort_entries (playlist->entries, data); number_entries (playlist, 0, playlist->entries.len ()); queue_update (Structure, playlist, 0, playlist->entries.len ()); @@ -1698,7 +1680,7 @@ static void sort_selected (PlaylistData * playlist, CompareData * data) selected.append (std::move (entry)); } - selected.sort (compare_cb, data); + sort_entries (selected, data); int i = 0; for (auto & entry : playlist->entries) @@ -1718,7 +1700,7 @@ static bool entries_are_scanned (PlaylistData * playlist, bool selected) if (selected && ! entry->selected) continue; - if (! entry->scanned) + if (entry->tuple.state () == Tuple::Initial) { aud_ui_show_error (_("The playlist cannot be sorted because " "metadata scanning is still in progress (or has been disabled).")); @@ -2231,12 +2213,12 @@ DecodeInfo playback_entry_read (int serial) item->handled_by_playback = true; LEAVE; - scanner_run (request); + request->run (); ENTER; if ((entry = get_playback_entry (serial))) { - send_playback_info (entry); + playback_set_info (entry->number, entry->tuple.ref ()); art_cache_current (entry->filename, std::move (request->image_data), std::move (request->image_file)); @@ -2258,7 +2240,8 @@ void playback_entry_set_tuple (int serial, Tuple && tuple) ENTER; Entry * entry = get_playback_entry (serial); - if (entry) + /* don't update cuesheet entries with stream metadata */ + if (entry && ! entry->tuple.is_set (Tuple::StartTime)) { playing_playlist->set_entry_tuple (entry, std::move (tuple)); queue_update (Metadata, playing_playlist, entry->number, 1); diff --git a/src/libaudcore/playlist.h b/src/libaudcore/playlist.h index 36e1263..ef63a28 100644 --- a/src/libaudcore/playlist.h +++ b/src/libaudcore/playlist.h @@ -70,10 +70,8 @@ enum SortType { /* Possible behaviors for playlist_entry_get_{decoder, tuple}. */ enum GetMode { - Nothing, // immediately return nullptr or Tuple() if not yet scanned - Guess, // immediately return a best guess if not yet scanned - Wait, // wait for the entry to be scanned; return nullptr or Tuple() on failure - WaitGuess // wait for the entry to be scanned; return a best guess on failure + NoWait, // non-blocking call; returned tuple will be in Initial state if not yet scanned + Wait // blocking call; returned tuple will be either Valid or Failed }; /* Format descriptor returned by playlist_save_formats() */ @@ -205,14 +203,13 @@ String aud_playlist_entry_get_filename (int playlist, int entry); * or if the entry has not yet been scanned, returns nullptr according to * <mode>. On error, an error message is optionally returned. */ PluginHandle * aud_playlist_entry_get_decoder (int playlist, int entry, - Playlist::GetMode mode = Playlist::WaitGuess, String * error = nullptr); + Playlist::GetMode mode = Playlist::Wait, String * error = nullptr); -/* Returns the tuple associated with an entry. On error, or if the entry has - * not yet been scanned, returns either a blank tuple or a tuple filled with - * "best guess" values, according to <mode>. On error, an error message is - * optionally returned. */ +/* Returns the tuple associated with an entry. The state of the returned tuple + * may indicate that the entry has not yet been scanned, or an error occurred, + * according to <mode>. On error, an error message is optionally returned. */ Tuple aud_playlist_entry_get_tuple (int playlist, int entry, - Playlist::GetMode mode = Playlist::WaitGuess, String * error = nullptr); + Playlist::GetMode mode = Playlist::Wait, String * error = nullptr); /* Moves the playback position to the beginning of the entry at <position>. If * <position> is -1, unsets the playback position. If <playlist> is the @@ -371,8 +368,7 @@ void aud_playlist_sort_selected_by_scheme (int playlist, Playlist::SortType sche void aud_playlist_remove_duplicates_by_scheme (int playlist, Playlist::SortType scheme); /* Removes all entries referring to unavailable files in a playlist. ("Remove - * failed" is something of a misnomer for the current behavior.) As currently - * implemented, only works for file:// URIs. */ + * failed" is something of a misnomer for the current behavior.) */ void aud_playlist_remove_failed (int playlist); /* Selects all the entries in a playlist that match regular expressions stored @@ -381,6 +377,10 @@ void aud_playlist_remove_failed (int playlist); * create a blank tuple and set its title field to "^A". */ void aud_playlist_select_by_patterns (int playlist, const Tuple & patterns); +/* Saves metadata for the selected entries in a playlist to an internal cache, + * which is used to speed up adding these entries to another playlist. */ +void aud_playlist_cache_selected (int playlist); + /* Returns true if <filename> refers to a playlist file. */ bool aud_filename_is_playlist (const char * filename); diff --git a/src/libaudcore/plugin-load.cc b/src/libaudcore/plugin-load.cc index 3117c6f..c249fea 100644 --- a/src/libaudcore/plugin-load.cc +++ b/src/libaudcore/plugin-load.cc @@ -50,15 +50,15 @@ struct LoadedModule { static Index<LoadedModule> loaded_modules; -bool plugin_check_flags (int version) +bool plugin_check_flags (int flags) { switch (aud_get_mainloop_type ()) { - case MainloopType::GLib: version &= ~_AUD_PLUGIN_GLIB_ONLY; break; - case MainloopType::Qt: version &= ~_AUD_PLUGIN_QT_ONLY; break; + case MainloopType::GLib: flags &= ~PluginGLibOnly; break; + case MainloopType::Qt: flags &= ~PluginQtOnly; break; } - return ! (version & 0xffff0000); + return ! flags; } Plugin * plugin_load (const char * filename) @@ -84,16 +84,15 @@ Plugin * plugin_load (const char * filename) return nullptr; } - /* flags are stored in high 16 bits of version field */ - if ((header->version & 0xffff) < _AUD_PLUGIN_VERSION_MIN || - (header->version & 0xffff) > _AUD_PLUGIN_VERSION) + if (header->version < _AUD_PLUGIN_VERSION_MIN || + header->version > _AUD_PLUGIN_VERSION) { AUDERR ("%s is not compatible with this version of Audacious.\n", filename); g_module_close (module); return nullptr; } - if (plugin_check_flags (header->version) && + if (plugin_check_flags (header->info.flags) && (header->type == PluginType::Transport || header->type == PluginType::Playlist || header->type == PluginType::Input || @@ -114,7 +113,7 @@ Plugin * plugin_load (const char * filename) static void plugin_unload (LoadedModule & loaded) { - if (plugin_check_flags (loaded.header->version) && + if (plugin_check_flags (loaded.header->info.flags) && (loaded.header->type == PluginType::Transport || loaded.header->type == PluginType::Playlist || loaded.header->type == PluginType::Input || diff --git a/src/libaudcore/plugin-registry.cc b/src/libaudcore/plugin-registry.cc index 88c6061..00114a9 100644 --- a/src/libaudcore/plugin-registry.cc +++ b/src/libaudcore/plugin-registry.cc @@ -35,7 +35,7 @@ /* Increment this when the format of the plugin-registry file changes. * Add 10 if the format changes in a way that will break parse_plugins_fallback(). */ -#define FORMAT 10 +#define FORMAT 11 /* Oldest file format supported by parse_plugins_fallback() */ #define MIN_FORMAT 2 // "enabled" flag was added in Audacious 2.4 @@ -50,7 +50,7 @@ class PluginHandle public: String basename, path; bool loaded; - int timestamp, version; + int timestamp, version, flags; PluginType type; Plugin * header; String name, domain; @@ -71,12 +71,13 @@ public: int has_subtunes, writes_tag; PluginHandle (const char * basename, const char * path, bool loaded, - int timestamp, int version, PluginType type, Plugin * header) : + int timestamp, int version, int flags, PluginType type, Plugin * header) : basename (basename), path (path), loaded (loaded), timestamp (timestamp), version (version), + flags (flags), type (type), header (header), priority (0), @@ -167,6 +168,7 @@ static void plugin_save (PluginHandle * plugin, FILE * handle) fprintf (handle, "%s %s\n", plugin_type_names[plugin->type], (const char *) plugin->path); fprintf (handle, "stamp %d\n", plugin->timestamp); fprintf (handle, "version %d\n", plugin->version); + fprintf (handle, "flags %d\n", plugin->flags); fprintf (handle, "name %s\n", (const char *) plugin->name); if (plugin->domain) @@ -319,11 +321,15 @@ static bool plugin_parse (FILE * handle) parse_next (handle); - int version = 0; + int version = 0, flags = 0; if (parse_integer ("version", & version)) parse_next (handle); + if (parse_integer ("flags", & flags)) + parse_next (handle); + + auto plugin = new PluginHandle (basename, String (), false, timestamp, + version, flags, type, nullptr); - auto plugin = new PluginHandle (basename, String (), false, timestamp, version, type, nullptr); plugins[type].append (plugin); plugin->name = parse_string ("name"); @@ -393,7 +399,7 @@ static void parse_plugins_fallback (FILE * handle) return; // setting timestamp to zero forces a rescan - auto plugin = new PluginHandle (basename, String (), false, 0, 0, type, nullptr); + auto plugin = new PluginHandle (basename, String (), false, 0, 0, 0, type, nullptr); plugins[type].append (plugin); plugin->enabled = (PluginEnabled) enabled; } @@ -425,7 +431,7 @@ ERR: fclose (handle); } -static int plugin_compare (PluginHandle * const & a, PluginHandle * const & b, void *) +static int plugin_compare (PluginHandle * const & a, PluginHandle * const & b) { if (a->type < b->type) return -1; @@ -457,7 +463,7 @@ void plugin_registry_prune () auto check_incompatible = [] (PluginHandle * plugin) { - if (plugin_check_flags (plugin->version)) + if (plugin_check_flags (plugin->flags)) return false; AUDINFO ("Incompatible plugin flags: %s\n", (const char *) plugin->basename); @@ -467,7 +473,7 @@ void plugin_registry_prune () for (auto type : aud::range<PluginType> ()) { plugins[type].remove_if (check_not_found); - plugins[type].sort (plugin_compare, nullptr); + plugins[type].sort (plugin_compare); compatible[type].insert (plugins[type].begin (), 0, plugins[type].len ()); compatible[type].remove_if (check_incompatible); } @@ -499,6 +505,7 @@ static void plugin_get_info (PluginHandle * plugin, bool is_new) Plugin * header = plugin->header; plugin->version = header->version; + plugin->flags = header->info.flags; plugin->name = String (header->info.name); plugin->domain = String (header->info.domain); plugin->has_about = (bool) header->info.about; @@ -590,7 +597,7 @@ void plugin_register (const char * path, int timestamp) return; plugin = new PluginHandle (basename, path, true, timestamp, - header->version, header->type, header); + header->version, header->info.flags, header->type, header); plugins[plugin->type].append (plugin); plugin_get_info (plugin, true); diff --git a/src/libaudcore/plugin.h b/src/libaudcore/plugin.h index 66077ad..65d0428 100644 --- a/src/libaudcore/plugin.h +++ b/src/libaudcore/plugin.h @@ -22,6 +22,7 @@ #define LIBAUDCORE_PLUGIN_H #include <libaudcore/audio.h> +#include <libaudcore/export.h> #include <libaudcore/plugins.h> #include <libaudcore/tuple.h> #include <libaudcore/visualizer.h> @@ -46,12 +47,8 @@ struct PluginPreferences; * the API tables), increment _AUD_PLUGIN_VERSION *and* set * _AUD_PLUGIN_VERSION_MIN to the same value. */ -#define _AUD_PLUGIN_VERSION_MIN 46 /* 3.6-devel */ -#define _AUD_PLUGIN_VERSION 47 /* 3.7-devel */ - -/* compatibility flags ORed into the version field */ -#define _AUD_PLUGIN_GLIB_ONLY 0x10000 /* plugin requires GLib mainloop */ -#define _AUD_PLUGIN_QT_ONLY 0x20000 /* plugin requires Qt mainloop */ +#define _AUD_PLUGIN_VERSION_MIN 48 /* 3.8-devel */ +#define _AUD_PLUGIN_VERSION 48 /* 3.8-devel */ /* A NOTE ON THREADS * @@ -99,14 +96,22 @@ struct PluginPreferences; * For the time being, aud_plugin_send_message() should only be called from the * program's main thread. */ -struct PluginInfo { +/* plugin flags */ +enum { + PluginGLibOnly = 0x1, // plugin requires GLib main loop + PluginQtOnly = 0x2 // plugin requires Qt main loop +}; + +struct PluginInfo +{ const char * name; const char * domain; // for gettext const char * about; const PluginPreferences * prefs; + int flags; }; -class Plugin +class LIBAUDCORE_PUBLIC Plugin { public: constexpr Plugin (PluginType type, PluginInfo info) : @@ -114,14 +119,7 @@ public: info (info) {} const int magic = _AUD_PLUGIN_MAGIC; - const int version = _AUD_PLUGIN_VERSION -#ifdef AUD_PLUGIN_GLIB_ONLY - | _AUD_PLUGIN_GLIB_ONLY -#endif -#ifdef AUD_PLUGIN_QT_ONLY - | _AUD_PLUGIN_QT_ONLY -#endif - ; + const int version = _AUD_PLUGIN_VERSION; const PluginType type; const PluginInfo info; @@ -132,7 +130,7 @@ public: virtual int take_message (const char * code, const void * data, int size) { return -1; } }; -class TransportPlugin : public Plugin +class LIBAUDCORE_PUBLIC TransportPlugin : public Plugin { public: constexpr TransportPlugin (const PluginInfo info, @@ -143,11 +141,16 @@ public: /* supported URI schemes (without "://") */ const ArrayRef<const char *> schemes; - /* fopen() implementation */ virtual VFSImpl * fopen (const char * filename, const char * mode, String & error) = 0; + + virtual VFSFileTest test_file (const char * filename, VFSFileTest test, String & error) + { return VFSFileTest (0); } + + virtual Index<String> read_folder (const char * filename, String & error) + { return Index<String> (); } }; -class PlaylistPlugin : public Plugin +class LIBAUDCORE_PUBLIC PlaylistPlugin : public Plugin { public: constexpr PlaylistPlugin (const PluginInfo info, @@ -177,7 +180,7 @@ public: const Index<PlaylistAddItem> & items) { return false; } }; -class OutputPlugin : public Plugin +class LIBAUDCORE_PUBLIC OutputPlugin : public Plugin { public: constexpr OutputPlugin (const PluginInfo info, int priority, bool force_reopen = false) : @@ -206,7 +209,7 @@ public: /* Begins playback of a PCM stream. <format> is one of the FMT_* * enumeration values defined in libaudcore/audio.h. Returns true on * success. */ - virtual bool open_audio (int format, int rate, int chans) = 0; + virtual bool open_audio (int format, int rate, int chans, String & error) = 0; /* Ends playback. Any buffered audio data is discarded. */ virtual void close_audio () = 0; @@ -237,7 +240,7 @@ public: virtual void flush () = 0; }; -class EffectPlugin : public Plugin +class LIBAUDCORE_PUBLIC EffectPlugin : public Plugin { public: constexpr EffectPlugin (const PluginInfo info, int order, bool preserves_format) : @@ -294,7 +297,7 @@ enum class InputKey { count }; -class InputPlugin : public Plugin +class LIBAUDCORE_PUBLIC InputPlugin : public Plugin { public: enum { @@ -368,9 +371,13 @@ public: /* Returns true if the plugin can handle the file. */ virtual bool is_our_file (const char * filename, VFSFile & file) = 0; - /* Reads metadata from the file. Optional if the plugin implements read_tag(). */ - virtual Tuple read_tuple (const char * filename, VFSFile & file) - { return Tuple(); } + /* Reads metadata and album art (if requested and available) from the file. + * The filename fields of the tuple are already set before the function is + * called. If album art is not needed, <image> will be nullptr. The return + * value should be true if <tuple> was successfully read, regardless of + * whether album art was read. */ + virtual bool read_tag (const char * filename, VFSFile & file, Tuple & tuple, + Index<char> * image) = 0; /* Plays the file. Returns false on error. Also see input-api.h. */ virtual bool play (const char * filename, VFSFile & file) = 0; @@ -379,29 +386,12 @@ public: virtual bool write_tuple (const char * filename, VFSFile & file, const Tuple & tuple) { return false; } - /* Optional. Reads an album art image (JPEG or PNG data) from the file. - * Returns an empty buffer on error. */ - virtual Index<char> read_image (const char * filename, VFSFile & file) - { return Index<char> (); } - /* Optional. Displays a window showing info about the file. In general, * this function should be avoided since Audacious already provides a file * info window. */ virtual bool file_info_box (const char * filename, VFSFile & file) { return false; } - /* Optional. Reads metadata and/or an album art from the file. - * Providing this function is encouraged over providing a separate - * read_tuple() and read_image(). The filename fields of the tuple - * (if not null) are already set before the function is called. */ - virtual bool read_tag (const char * filename, VFSFile & file, Tuple * tuple, - Index<char> * image) - { return default_read_tag (filename, file, tuple, image); } - - /* compatibility (non-virtual) implementation of read_tag(); do not use. */ - bool default_read_tag (const char * filename, VFSFile & file, Tuple * tuple, - Index<char> * image); - protected: /* Prepares the output system for playback in the specified format. Also * triggers the "playback ready" hook. Hence, if you call set_replay_gain, @@ -441,7 +431,7 @@ protected: static int check_seek (); }; -class DockablePlugin : public Plugin +class LIBAUDCORE_PUBLIC DockablePlugin : public Plugin { public: constexpr DockablePlugin (PluginType type, PluginInfo info) : @@ -454,7 +444,7 @@ public: virtual void * get_qt_widget () { return nullptr; } }; -class GeneralPlugin : public DockablePlugin +class LIBAUDCORE_PUBLIC GeneralPlugin : public DockablePlugin { public: constexpr GeneralPlugin (PluginInfo info, bool enabled_by_default) : @@ -464,7 +454,7 @@ public: const bool enabled_by_default; }; -class VisPlugin : public DockablePlugin, public Visualizer +class LIBAUDCORE_PUBLIC VisPlugin : public DockablePlugin, public Visualizer { public: constexpr VisPlugin (PluginInfo info, int type_mask) : @@ -472,7 +462,7 @@ public: Visualizer (type_mask) {} }; -class IfacePlugin : public Plugin +class LIBAUDCORE_PUBLIC IfacePlugin : public Plugin { public: constexpr IfacePlugin (PluginInfo info) : @@ -492,6 +482,8 @@ public: virtual void hide_prefs_window () = 0; virtual void plugin_menu_add (AudMenuID id, void func (), const char * name, const char * icon) = 0; virtual void plugin_menu_remove (AudMenuID id, void func ()) = 0; + + virtual void startup_notify (const char * id) {} }; #endif diff --git a/src/libaudcore/plugins-internal.h b/src/libaudcore/plugins-internal.h index c03e1e1..ef293d5 100644 --- a/src/libaudcore/plugins-internal.h +++ b/src/libaudcore/plugins-internal.h @@ -43,7 +43,7 @@ bool plugin_enable_secondary (PluginHandle * plugin, bool enable); /* plugin-load.c */ void plugin_system_init (); void plugin_system_cleanup (); -bool plugin_check_flags (int version); +bool plugin_check_flags (int flags); Plugin * plugin_load (const char * path); /* plugin-registry.c */ diff --git a/src/libaudcore/preferences.h b/src/libaudcore/preferences.h index 750679c..8946b01 100644 --- a/src/libaudcore/preferences.h +++ b/src/libaudcore/preferences.h @@ -24,6 +24,11 @@ struct PreferencesWidget; +enum class FileSelectMode { + File, + Folder +}; + struct ComboItem { const char * label; const char * str; @@ -66,6 +71,10 @@ struct WidgetVEntry { bool password; }; +struct WidgetVFileEntry { + FileSelectMode mode; +}; + struct WidgetVCombo { /* static init */ ArrayRef<ComboItem> elems; @@ -101,6 +110,7 @@ union WidgetVariant { WidgetVTable table; WidgetVFonts font_btn; WidgetVEntry entry; + WidgetVFileEntry file_entry; WidgetVCombo combo; WidgetVBox box; WidgetVNotebook notebook; @@ -117,6 +127,7 @@ union WidgetVariant { constexpr WidgetVariant (WidgetVTable table) : table (table) {} constexpr WidgetVariant (WidgetVFonts fonts) : font_btn (fonts) {} constexpr WidgetVariant (WidgetVEntry entry) : entry (entry) {} + constexpr WidgetVariant (WidgetVFileEntry file_entry) : file_entry (file_entry) {} constexpr WidgetVariant (WidgetVCombo combo) : combo (combo) {} constexpr WidgetVariant (WidgetVBox box) : box (box) {} constexpr WidgetVariant (WidgetVNotebook notebook) : notebook (notebook) {} @@ -209,6 +220,7 @@ struct PreferencesWidget RadioButton, SpinButton, Entry, + FileEntry, ComboBox, FontButton, Box, @@ -261,6 +273,12 @@ constexpr PreferencesWidget WidgetEntry (const char * label, { return {PreferencesWidget::Entry, label, (child == WIDGET_CHILD), cfg, entry}; } +constexpr PreferencesWidget WidgetFileEntry (const char * label, + WidgetConfig cfg, WidgetVFileEntry file_entry = WidgetVFileEntry(), + WidgetIsChild child = WIDGET_NOT_CHILD) + { return {PreferencesWidget::FileEntry, label, + (child == WIDGET_CHILD), cfg, file_entry}; } + constexpr PreferencesWidget WidgetCombo (const char * label, WidgetConfig cfg, WidgetVCombo combo, WidgetIsChild child = WIDGET_NOT_CHILD) { return {PreferencesWidget::ComboBox, label, @@ -284,11 +302,13 @@ constexpr PreferencesWidget WidgetNotebook (WidgetVNotebook notebook) constexpr PreferencesWidget WidgetSeparator (WidgetVSeparator separator = WidgetVSeparator ()) { return {PreferencesWidget::Separator, 0, 0, {}, separator}; } -constexpr PreferencesWidget WidgetCustomGTK (void * (* populate) ()) - { return {PreferencesWidget::CustomGTK, 0, 0, {}, populate}; } +constexpr PreferencesWidget WidgetCustomGTK (void * (* populate) (), + WidgetIsChild child = WIDGET_NOT_CHILD) + { return {PreferencesWidget::CustomGTK, 0, (child == WIDGET_CHILD), {}, populate}; } -constexpr PreferencesWidget WidgetCustomQt (void * (* populate) ()) - { return {PreferencesWidget::CustomQt, 0, 0, {}, populate}; } +constexpr PreferencesWidget WidgetCustomQt (void * (* populate) (), + WidgetIsChild child = WIDGET_NOT_CHILD) + { return {PreferencesWidget::CustomQt, 0, (child == WIDGET_CHILD), {}, populate}; } struct PluginPreferences { ArrayRef<PreferencesWidget> widgets; diff --git a/src/libaudcore/probe-buffer.cc b/src/libaudcore/probe-buffer.cc index 0ea6d6b..e23992c 100644 --- a/src/libaudcore/probe-buffer.cc +++ b/src/libaudcore/probe-buffer.cc @@ -24,9 +24,9 @@ static constexpr int MAXBUF = 256 * 1024; -ProbeBuffer::ProbeBuffer (const char * filename, SmartPtr<VFSImpl> && file) : +ProbeBuffer::ProbeBuffer (const char * filename, VFSImpl * file) : m_filename (filename), - m_file (std::move (file)) + m_file (file) { AUDINFO ("<%p> buffering enabled for %s\n", this, (const char *) m_filename); } diff --git a/src/libaudcore/probe-buffer.h b/src/libaudcore/probe-buffer.h index 8bec69f..5cdfc29 100644 --- a/src/libaudcore/probe-buffer.h +++ b/src/libaudcore/probe-buffer.h @@ -39,7 +39,7 @@ class ProbeBuffer : public VFSImpl { public: - ProbeBuffer (const char * filename, SmartPtr<VFSImpl> && file); + ProbeBuffer (const char * filename, VFSImpl * file); ~ProbeBuffer (); int64_t fread (void * ptr, int64_t size, int64_t nmemb); diff --git a/src/libaudcore/probe.cc b/src/libaudcore/probe.cc index 0c590a3..b2fff60 100644 --- a/src/libaudcore/probe.cc +++ b/src/libaudcore/probe.cc @@ -56,7 +56,8 @@ InputPlugin * load_input_plugin (PluginHandle * decoder, String * error) return ip; } -PluginHandle * file_find_decoder (const char * filename, bool fast, VFSFile & file, String * error) +EXPORT PluginHandle * aud_file_find_decoder (const char * filename, bool fast, + VFSFile & file, String * error) { AUDINFO ("%s %s.\n", fast ? "Fast-probing" : "Probing", filename); @@ -155,14 +156,8 @@ PluginHandle * file_find_decoder (const char * filename, bool fast, VFSFile & fi return nullptr; } -EXPORT PluginHandle * aud_file_find_decoder (const char * filename, bool fast, String * error) -{ - VFSFile file; - return file_find_decoder (filename, fast, file, error); -} - -bool file_read_tag (const char * filename, PluginHandle * decoder, - VFSFile & file, Tuple * tuple, Index<char> * image, String * error) +EXPORT bool aud_file_read_tag (const char * filename, PluginHandle * decoder, + VFSFile & file, Tuple & tuple, Index<char> * image, String * error) { auto ip = load_input_plugin (decoder, error); if (! ip) @@ -171,41 +166,21 @@ bool file_read_tag (const char * filename, PluginHandle * decoder, if (! open_input_file (filename, "r", ip, file, error)) return false; - if (tuple) - tuple->set_filename (filename); - - bool success; + Tuple new_tuple; + new_tuple.set_filename (filename); - /* read_tag() was added in 3.7 */ - if (ip->version >= 47) - success = ip->read_tag (filename, file, tuple, image); - else - success = ip->default_read_tag (filename, file, tuple, image); + if (ip->read_tag (filename, file, new_tuple, image)) + { + // cleanly replace existing tuple + new_tuple.set_state (Tuple::Valid); + tuple = std::move (new_tuple); + return true; + } - if (! success && error) + if (error) * error = String (_("Error reading metadata")); - if (! success && tuple) - * tuple = Tuple (); - - return success; -} - -EXPORT Tuple aud_file_read_tuple (const char * filename, PluginHandle * decoder, String * error) -{ - VFSFile file; - Tuple tuple; - - file_read_tag (filename, decoder, file, & tuple, nullptr, error); - return tuple; -} - -EXPORT Index<char> aud_file_read_image (const char * filename, PluginHandle * decoder) -{ - VFSFile file; - Index<char> image; - file_read_tag (filename, decoder, file, nullptr, & image, nullptr); - return image; + return false; } EXPORT bool aud_file_can_write_tuple (const char * filename, PluginHandle * decoder) diff --git a/src/libaudcore/probe.h b/src/libaudcore/probe.h index 641f415..8944289 100644 --- a/src/libaudcore/probe.h +++ b/src/libaudcore/probe.h @@ -33,9 +33,9 @@ class VFSFile; * ready" hook is called, with <file> as a parameter. If no album art could be * loaded, sets *queued to false and returns nullptr. * - * Since Audacious 3.7, album art for the currently playing song is always - * loaded before the "playback ready" hook is called. Hence the "current art - * ready" hook from previous versions is deprecated. */ + * If you only want to display album art for the currently playing song, you can + * call this function from the "playback ready" hook without any need for a + * separate "art ready" handler, since the album art is already at that point. */ const Index<char> * aud_art_request_data (const char * file, bool * queued = nullptr); /* Similar to art_request_data() but returns the URI of an image file. @@ -45,9 +45,16 @@ const char * aud_art_request_file (const char * file, bool * queued = nullptr); /* Releases album art returned by art_request_data() or art_request_file(). */ void aud_art_unref (const char * file); -PluginHandle * aud_file_find_decoder (const char * filename, bool fast, String * error = nullptr); -Tuple aud_file_read_tuple (const char * filename, PluginHandle * decoder, String * error = nullptr); -Index<char> aud_file_read_image (const char * filename, PluginHandle * decoder); +/* The following two functions take an additional VFSFile parameter to allow + * opening a file, probing for a decoder, and then reading the song metadata + * without opening the file a second time. If you don't already have a file + * handle open, then just pass in a null VFSFile and it will be opened for you. */ +PluginHandle * aud_file_find_decoder (const char * filename, bool fast, + VFSFile & file, String * error = nullptr); +bool aud_file_read_tag (const char * filename, PluginHandle * decoder, + VFSFile & file, Tuple & tuple, Index<char> * image = nullptr, + String * error = nullptr); + bool aud_file_can_write_tuple (const char * filename, PluginHandle * decoder); bool aud_file_write_tuple (const char * filename, PluginHandle * decoder, const Tuple & tuple); bool aud_custom_infowin (const char * filename, PluginHandle * decoder); diff --git a/src/libaudcore/runtime.cc b/src/libaudcore/runtime.cc index 9180deb..5c3660f 100644 --- a/src/libaudcore/runtime.cc +++ b/src/libaudcore/runtime.cc @@ -43,6 +43,7 @@ #include "hook.h" #include "internal.h" #include "mainloop.h" +#include "output.h" #include "playlist-internal.h" #include "plugins-internal.h" #include "scanner.h" @@ -64,6 +65,7 @@ size_t misc_bytes_allocated; static bool headless_mode; +static int instance_number = 1; #if defined(USE_QT) && ! defined(USE_GTK) static MainloopType mainloop_type = MainloopType::Qt; @@ -74,28 +76,23 @@ static MainloopType mainloop_type = MainloopType::GLib; static aud::array<AudPath, String> aud_paths; EXPORT void aud_set_headless_mode (bool headless) -{ - headless_mode = headless; -} - + { headless_mode = headless; } EXPORT bool aud_get_headless_mode () -{ - return headless_mode; -} + { return headless_mode; } -EXPORT void aud_set_mainloop_type (MainloopType type) -{ - mainloop_type = type; -} +EXPORT void aud_set_instance (int instance) + { instance_number = instance; } +EXPORT int aud_get_instance () + { return instance_number; } +EXPORT void aud_set_mainloop_type (MainloopType type) + { mainloop_type = type; } EXPORT MainloopType aud_get_mainloop_type () -{ - return mainloop_type; -} + { return mainloop_type; } static StringBuf get_path_to_self () { -#ifdef HAVE_PROC_SELF_EXE +#ifdef __linux__ StringBuf buf (-1); int len = readlink ("/proc/self/exe", buf, buf.len ()); @@ -180,7 +177,7 @@ static void set_default_paths () aud_paths[AudPath::IconFile] = String (HARDCODE_ICONFILE); } -static void relocate_all_paths () +static void set_install_paths () { StringBuf bindir = filename_normalize (str_copy (HARDCODE_BINDIR)); StringBuf datadir = filename_normalize (str_copy (HARDCODE_DATADIR)); @@ -236,17 +233,16 @@ static void relocate_all_paths () aud_paths[AudPath::IconFile] = relocate_path (iconfile, from, to); } -EXPORT void aud_init_paths () +static void set_config_paths () { - relocate_all_paths (); - const char * xdg_config_home = g_get_user_config_dir (); + StringBuf name = (instance_number == 1) ? str_copy ("audacious") : + str_printf ("audacious-%d", instance_number); - aud_paths[AudPath::UserDir] = String (filename_build ({xdg_config_home, "audacious"})); + aud_paths[AudPath::UserDir] = String (filename_build ({xdg_config_home, name})); aud_paths[AudPath::PlaylistDir] = String (filename_build ({aud_paths[AudPath::UserDir], "playlists"})); - /* create ~/.config/audacious/playlists */ if (g_mkdir_with_parents (aud_paths[AudPath::PlaylistDir], DIRMODE) < 0) AUDERR ("Failed to create %s: %s\n", (const char *) aud_paths[AudPath::PlaylistDir], strerror (errno)); @@ -260,14 +256,16 @@ EXPORT void aud_init_paths () #endif } -EXPORT void aud_cleanup_paths () -{ - for (String & path : aud_paths) - path = String (); -} - EXPORT const char * aud_get_path (AudPath id) { + if (! aud_paths[id]) + { + if (id <= AudPath::IconFile) + set_install_paths (); + else + set_config_paths (); + } + return aud_paths[id]; } @@ -291,6 +289,7 @@ EXPORT void aud_init () chardet_init (); eq_init (); + output_init (); playlist_init (); start_plugins_one (); @@ -343,6 +342,7 @@ EXPORT void aud_cleanup () art_cleanup (); chardet_cleanup (); eq_cleanup (); + output_cleanup (); playlist_end (); event_queue_cancel_all (); @@ -355,8 +355,11 @@ EXPORT void aud_cleanup () EXPORT void aud_leak_check () { + for (String & path : aud_paths) + path = String (); + string_leak_check (); if (misc_bytes_allocated) - AUDWARN ("Bytes allocated at exit: %zd\n", misc_bytes_allocated); + AUDWARN ("Bytes allocated at exit: %ld\n", (long) misc_bytes_allocated); } diff --git a/src/libaudcore/runtime.h b/src/libaudcore/runtime.h index c599e04..570f948 100644 --- a/src/libaudcore/runtime.h +++ b/src/libaudcore/runtime.h @@ -45,6 +45,13 @@ enum class OutputReset { ResetPlugin }; +enum class OutputStream { + AsDecoded, + AfterReplayGain, + AfterEffects, + AfterEqualizer +}; + namespace audlog { enum Level { @@ -62,7 +69,7 @@ namespace audlog void unsubscribe (Handler handler); void log (Level level, const char * file, int line, const char * func, - const char * format, ...); + const char * format, ...) __attribute__ ((__format__ (__printf__, 5, 6))); const char * get_level_name (Level level); } @@ -72,18 +79,20 @@ namespace audlog #define AUDINFO(...) do { audlog::log (audlog::Info, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); } while (0) #define AUDDBG(...) do { audlog::log (audlog::Debug, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); } while (0) -void aud_init_paths (); -void aud_cleanup_paths (); - const char * aud_get_path (AudPath id); void aud_set_headless_mode (bool headless); bool aud_get_headless_mode (); +// Note that the UserDir and PlaylistDir paths vary depending on the instance +// number. Therefore, calling aud_set_instance() after these paths have been +// referenced, or after aud_init(), is an error. +void aud_set_instance (int instance); +int aud_get_instance (); + void aud_set_mainloop_type (MainloopType type); MainloopType aud_get_mainloop_type (); -/* Requires: aud_init_paths() */ void aud_init_i18n (); void aud_config_set_defaults (const char * section, const char * const * entries); diff --git a/src/libaudcore/scanner.cc b/src/libaudcore/scanner.cc index 21df3c0..75cb04a 100644 --- a/src/libaudcore/scanner.cc +++ b/src/libaudcore/scanner.cc @@ -1,6 +1,6 @@ /* * scanner.c - * Copyright 2012 John Lindgren + * Copyright 2012-2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -21,52 +21,96 @@ #include <glib.h> /* for GThreadPool */ +#include "audstrings.h" +#include "cue-cache.h" #include "i18n.h" #include "internal.h" #include "plugins.h" +#include "probe.h" #include "tuple.h" #include "vfs.h" static GThreadPool * pool; -void scanner_run (ScanRequest * r) +ScanRequest::ScanRequest (const String & filename, int flags, Callback callback, + PluginHandle * decoder, Tuple && tuple) : + filename (filename), + flags (flags), + callback (callback), + decoder (decoder), + tuple (std::move (tuple)), + ip (nullptr) { - if (! r->decoder) - r->decoder = file_find_decoder (r->filename, false, r->file, & r->error); - if (! r->decoder) + /* If this is a cuesheet entry (and it has not already been loaded), capture + * a reference to the cache immediately. During a playlist scan, requests + * have overlapping lifecycles--each new ScanRequest is created by the + * callback of the previous request--so the cached cuesheet persists as long + * as consecutive playlist entries reference it. */ + if (! this->tuple.valid () && is_cuesheet_entry (filename)) + cue_cache.capture (new CueCacheRef (strip_subtune (filename))); +} + +void ScanRequest::read_cuesheet_entry () +{ + for (auto & item : cue_cache->load ()) + { + if (item.filename == filename) + { + decoder = item.decoder; + tuple = item.tuple.ref (); + break; + } + } +} + +void ScanRequest::run () +{ + /* load cuesheet entry (possibly cached) */ + if (cue_cache) + read_cuesheet_entry (); + + /* for a cuesheet entry, determine the source filename */ + String audio_file = tuple.get_str (Tuple::AudioFile); + if (! audio_file) + audio_file = filename; + + bool need_tuple = (flags & SCAN_TUPLE) && ! tuple.valid (); + bool need_image = (flags & SCAN_IMAGE); + + if (! decoder) + decoder = aud_file_find_decoder (audio_file, false, file, & error); + if (! decoder) goto err; - if ((r->flags & (SCAN_TUPLE | SCAN_IMAGE))) + if (need_tuple || need_image) { - if (! (r->ip = load_input_plugin (r->decoder, & r->error))) + if (! (ip = load_input_plugin (decoder, & error))) goto err; - Tuple * ptuple = (r->flags & SCAN_TUPLE) ? & r->tuple : nullptr; - Index<char> * pimage = (r->flags & SCAN_IMAGE) ? & r->image_data : nullptr; - - if (! file_read_tag (r->filename, r->decoder, r->file, ptuple, pimage, & r->error)) + Index<char> * pimage = need_image ? & image_data : nullptr; + if (! aud_file_read_tag (audio_file, decoder, file, tuple, pimage, & error)) goto err; - if ((r->flags & SCAN_IMAGE) && ! r->image_data.len ()) - r->image_file = art_search (r->filename); + if ((flags & SCAN_IMAGE) && ! image_data.len ()) + image_file = art_search (audio_file); } /* rewind/reopen the input file */ - if ((r->flags & SCAN_FILE)) - open_input_file (r->filename, "r", r->ip, r->file, & r->error); + if ((flags & SCAN_FILE)) + open_input_file (audio_file, "r", ip, file, & error); else { err: /* close file if not needed or if an error occurred */ - r->file = VFSFile (); + file = VFSFile (); } - r->callback (r); + callback (this); } static void scan_worker (void * data, void *) { - scanner_run ((ScanRequest *) data); + ((ScanRequest *) data)->run (); delete (ScanRequest *) data; } diff --git a/src/libaudcore/scanner.h b/src/libaudcore/scanner.h index 18a6418..667afb3 100644 --- a/src/libaudcore/scanner.h +++ b/src/libaudcore/scanner.h @@ -1,6 +1,6 @@ /* * scanner.h - * Copyright 2012 John Lindgren + * Copyright 2012-2016 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -20,7 +20,9 @@ #ifndef LIBAUDCORE_SCANNER_H #define LIBAUDCORE_SCANNER_H +#include "cue-cache.h" #include "index.h" +#include "objects.h" #include "tuple.h" #include "vfs.h" @@ -37,31 +39,33 @@ struct ScanRequest { typedef void (* Callback) (ScanRequest * request); - ScanRequest (const String & filename, int flags, Callback callback, - PluginHandle * decoder = nullptr) : - filename (filename), - flags (flags), - callback (callback), - decoder (decoder), - ip (nullptr) {} - const String filename; const int flags; const Callback callback; PluginHandle * decoder; + Tuple tuple; + InputPlugin * ip; VFSFile file; - Tuple tuple; Index<char> image_data; String image_file; String error; + + ScanRequest (const String & filename, int flags, Callback callback, + PluginHandle * decoder = nullptr, Tuple && tuple = Tuple ()); + + void run (); + +private: + SmartPtr<CueCacheRef> cue_cache; + + void read_cuesheet_entry (); }; void scanner_init (); void scanner_request (ScanRequest * request); -void scanner_run (ScanRequest * request); void scanner_cleanup (); #endif diff --git a/src/libaudcore/stringbuf.cc b/src/libaudcore/stringbuf.cc index 041b1e9..fc646f6 100644 --- a/src/libaudcore/stringbuf.cc +++ b/src/libaudcore/stringbuf.cc @@ -139,7 +139,7 @@ EXPORT void StringBuf::resize (int len) } } -EXPORT StringBuf::~StringBuf () +EXPORT StringBuf::~StringBuf () noexcept (false) { if (m_data) { diff --git a/src/libaudcore/templates.h b/src/libaudcore/templates.h index 1a78251..e78aea0 100644 --- a/src/libaudcore/templates.h +++ b/src/libaudcore/templates.h @@ -24,10 +24,10 @@ #include <type_traits> #include <utility> -#ifdef _WIN32 +// #undef POSIX and Windows macros to avoid name conflicts +#undef abs #undef min #undef max -#endif namespace aud { @@ -110,6 +110,13 @@ struct array constexpr array (Args && ... args) : vals { static_cast<Args &&> (args) ...} {} + // Due to GCC bug #63707, the forwarding constructor given above cannot be + // used to initialize the array when V is a class with no copy constructor. + // As a very limited workaround, provide a second constructor which can be + // used to initialize the array to default values in this case. + constexpr array () : + vals () {} + constexpr const V & operator[] (T t) const { return vals[(int) t]; } constexpr const V * begin () const diff --git a/src/libaudcore/tests/test.cc b/src/libaudcore/tests/test.cc index 9efe189..3d33332 100644 --- a/src/libaudcore/tests/test.cc +++ b/src/libaudcore/tests/test.cc @@ -49,6 +49,65 @@ static void test_audio_conversion () assert (in[i] == out[i]); } +static void test_numeric_conversion () +{ + static const char * in[] = { + "", + "x1234", + "+2147483647", + "-2147483648", + "999999999.999999", + "999999999.9999996", + "000000000000000000000000100000.000001000000000000000000000000", + "--5", + "3.+5", + "-6.7 dB" + }; + + static const char * out_double[] = { + "0", + "0", + "2147483647", + "-2147483648", + "999999999.999999", + "1000000000", + "100000.000001", + "0", + "3", + "-6.7" + }; + + static const char * out_int[] = { + "0", + "0", + "2147483647", + "-2147483648", + "999999999", + "999999999", + "100000", + "0", + "3", + "-6" + }; + + for (int i = 0; i < aud::n_elems (in); i ++) + { + double d_val = str_to_double (in[i]); + int i_val = str_to_int (in[i]); + StringBuf via_double = double_to_str (d_val); + StringBuf via_int = int_to_str (i_val); + + if (strcmp (via_double, out_double[i]) || strcmp (via_int, out_int[i])) + { + printf ("Converting [%s]\n", in[i]); + printf ("Expected [%s] and [%s]\n", out_double[i], out_int[i]); + printf ("Via [%g] and [%d]\n", d_val, i_val); + printf ("Got [%s] and [%s]\n", (const char *) via_double, (const char *) via_int); + exit (1); + } + } +} + static void test_filename_split () { /* expected results differ slightly from POSIX dirname/basename */ @@ -326,6 +385,7 @@ static void test_ringbuf () int main () { test_audio_conversion (); + test_numeric_conversion (); test_filename_split (); test_tuple_formats (); test_ringbuf (); diff --git a/src/libaudcore/timer.cc b/src/libaudcore/timer.cc index 173eb9a..4de6abf 100644 --- a/src/libaudcore/timer.cc +++ b/src/libaudcore/timer.cc @@ -35,13 +35,10 @@ struct TimerItem { struct TimerList { - QueuedFunc & source; + QueuedFunc source; Index<TimerItem> items; int use_count = 0; - TimerList (QueuedFunc & source) : - source (source) {} - bool contains (TimerFunc func, void * data) const { for (auto & item : items) @@ -69,10 +66,7 @@ struct TimerList }; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; - -// QueuedFunc cannot be used in aud::array due to lack of a move constructor -static QueuedFunc qf_1Hz, qf_4Hz, qf_10Hz, qf_30Hz; -static aud::array<TimerRate, TimerList> lists {qf_1Hz, qf_4Hz, qf_10Hz, qf_30Hz}; +static aud::array<TimerRate, TimerList> lists; static void timer_run (void * list_) { diff --git a/src/libaudcore/tuple.cc b/src/libaudcore/tuple.cc index 52fd29b..4cd29b6 100644 --- a/src/libaudcore/tuple.cc +++ b/src/libaudcore/tuple.cc @@ -62,13 +62,14 @@ struct TupleData uint64_t setmask; // which fields are present Index<TupleVal> vals; // ordered list of field values - int *subtunes; /**< Array of int containing subtune index numbers. + short * subtunes; /**< Array of int containing subtune index numbers. Can be nullptr if indexing is linear or if there are no subtunes. */ - int nsubtunes; /**< Number of subtunes, if any. Values greater than 0 + short nsubtunes; /**< Number of subtunes, if any. Values greater than 0 mean that there are subtunes and #subtunes array may be set. */ + short state; int refcount; TupleData (); @@ -85,7 +86,7 @@ struct TupleData TupleVal * lookup (int field, bool add, bool remove); void set_int (int field, int x); void set_str (int field, const char * str); - void set_subtunes (int nsubs, const int * subs); + void set_subtunes (short nsubs, const short * subs); static TupleData * ref (TupleData * tuple); static void unref (TupleData * tuple); @@ -107,27 +108,28 @@ static const struct { {"title", Tuple::String, FallbackTitle}, {"artist", Tuple::String, FallbackArtist}, {"album", Tuple::String, FallbackAlbum}, + {"album-artist", Tuple::String, -1}, {"comment", Tuple::String, -1}, {"genre", Tuple::String, -1}, + {"year", Tuple::Int, -1}, + + {"composer", Tuple::String, -1}, + {"performer", Tuple::String, -1}, + {"copyright", Tuple::String, -1}, + {"date", Tuple::String, -1}, {"track-number", Tuple::Int, -1}, {"length", Tuple::Int, -1}, - {"year", Tuple::Int, -1}, - {"quality", Tuple::String, -1}, + + {"bitrate", Tuple::Int, -1}, {"codec", Tuple::String, -1}, + {"quality", Tuple::String, -1}, {"file-name", Tuple::String, -1}, {"file-path", Tuple::String, -1}, {"file-ext", Tuple::String, -1}, - {"album-artist", Tuple::String, -1}, - {"composer", Tuple::String, -1}, - {"performer", Tuple::String, -1}, - {"copyright", Tuple::String, -1}, - {"date", Tuple::String, -1}, - {"mbid", Tuple::String, -1}, - {"mime-type", Tuple::String, -1}, - {"bitrate", Tuple::Int, -1}, + {"audio-file", Tuple::String, -1}, {"subsong-id", Tuple::Int, -1}, {"subsong-num", Tuple::Int, -1}, @@ -162,6 +164,7 @@ static const FieldDictEntry field_dict[] = { {"album", Tuple::Album}, {"album-artist", Tuple::AlbumArtist}, {"artist", Tuple::Artist}, + {"audio-file", Tuple::AudioFile}, {"bitrate", Tuple::Bitrate}, {"codec", Tuple::Codec}, {"comment", Tuple::Comment}, @@ -180,8 +183,6 @@ static const FieldDictEntry field_dict[] = { {"gain-track-peak", Tuple::TrackPeak}, {"genre", Tuple::Genre}, {"length", Tuple::Length}, - {"mbid", Tuple::MusicBrainz}, - {"mime-type", Tuple::MIMEType}, {"performer", Tuple::Performer}, {"quality", Tuple::Quality}, {"segment-end", Tuple::EndTime}, @@ -277,17 +278,17 @@ void TupleData::set_str (int field, const char * str) new (& val->str) String (str); } -void TupleData::set_subtunes (int nsubs, const int * subs) +void TupleData::set_subtunes (short nsubs, const short * subs) { nsubtunes = nsubs; - delete subtunes; + delete[] subtunes; subtunes = nullptr; - if (subs) + if (nsubs && subs) { - subtunes = new int[nsubs]; - memcpy (subtunes, subs, sizeof (int) * nsubs); + subtunes = new short[nsubs]; + memcpy (subtunes, subs, sizeof subtunes[0] * nsubs); } } @@ -295,12 +296,14 @@ TupleData::TupleData () : setmask (0), subtunes (nullptr), nsubtunes (0), + state (Tuple::Initial), refcount (1) {} TupleData::TupleData (const TupleData & other) : setmask (other.setmask), subtunes (nullptr), nsubtunes (0), + state (other.state), refcount (1) { vals.insert (0, other.vals.len ()); @@ -345,8 +348,8 @@ TupleData::~TupleData () bool TupleData::is_same (const TupleData & other) { - if (setmask != other.setmask || nsubtunes != other.nsubtunes || - (! subtunes) != (! other.subtunes)) + if (state != other.state || setmask != other.setmask || + nsubtunes != other.nsubtunes || (! subtunes) != (! other.subtunes)) return false; auto a = vals.begin (); @@ -371,7 +374,7 @@ bool TupleData::is_same (const TupleData & other) } } - if (subtunes && memcmp (subtunes, other.subtunes, sizeof (int) * nsubtunes)) + if (subtunes && memcmp (subtunes, other.subtunes, sizeof subtunes[0] * nsubtunes)) return false; return true; @@ -427,6 +430,17 @@ EXPORT Tuple Tuple::ref () const return tuple; } +EXPORT Tuple::State Tuple::state () const +{ + return data ? (Tuple::State) data->state : Initial; +} + +EXPORT void Tuple::set_state (State st) +{ + data = TupleData::copy_on_write (data); + data->state = st; +} + EXPORT Tuple::ValueType Tuple::get_value_type (Field field) const { assert (is_valid_field (field)); @@ -553,18 +567,18 @@ EXPORT void Tuple::set_format (const char * format, int chans, int rate, int bra set_int (Bitrate, brate); } -EXPORT void Tuple::set_subtunes (int n_subtunes, const int * subtunes) +EXPORT void Tuple::set_subtunes (short n_subtunes, const short * subtunes) { data = TupleData::copy_on_write (data); data->set_subtunes (n_subtunes, subtunes); } -EXPORT int Tuple::get_n_subtunes () const +EXPORT short Tuple::get_n_subtunes() const { return data ? data->nsubtunes : 0; } -EXPORT int Tuple::get_nth_subtune (int n) const +EXPORT short Tuple::get_nth_subtune (short n) const { if (! data || n < 0 || n >= data->nsubtunes) return -1; @@ -644,10 +658,10 @@ EXPORT bool Tuple::fetch_stream_info (VFSFile & stream) * be modified, and the string returned will use the same memory. May return * nullptr. */ -static char * split_folder (char * path) +static char * split_folder (char * path, char sep) { char * c; - while ((c = strrchr (path, G_DIR_SEPARATOR))) + while ((c = strrchr (path, sep))) { * c = 0; if (c[1]) @@ -657,31 +671,33 @@ static char * split_folder (char * path) return path[0] ? path : nullptr; } -/* Separates the domain name from an internet URI. The string passed will be - * modified, and the string returned will share the same memory. May return - * nullptr. Examples: +/* These two functions separate the domain name from an internet URL. Examples: * "http://some.domain.org/folder/file.mp3" -> "some.domain.org" * "http://some.stream.fm:8000" -> "some.stream.fm" */ -static char * domain_name (char * name) +static const char * find_domain (const char * name) { if (! strncmp (name, "http://", 7)) - name += 7; - else if (! strncmp (name, "https://", 8)) - name += 8; - else if (! strncmp (name, "mms://", 6)) - name += 6; - else - return nullptr; + return name + 7; + if (! strncmp (name, "https://", 8)) + return name + 8; + if (! strncmp (name, "mms://", 6)) + return name + 6; + + return nullptr; +} +static StringBuf extract_domain (const char * start) +{ + StringBuf name = str_copy (start); char * c; if ((c = strchr (name, '/'))) - * c = 0; + name.resize (c - name); if ((c = strchr (name, ':'))) - * c = 0; + name.resize (c - name); if ((c = strchr (name, '?'))) - * c = 0; + name.resize (c - name); return name; } @@ -701,10 +717,22 @@ EXPORT void Tuple::generate_fallbacks () data = TupleData::copy_on_write (data); + // use album artist, if present + if (! artist && (artist = get_str (AlbumArtist))) + { + data->set_str (FallbackArtist, artist); + + if (album) + return; // nothing left to do + } + auto filepath = get_str (Path); if (! filepath) return; + const char * s; + char sep; + if (! strcmp (filepath, "cdda://")) { // audio CD: @@ -713,38 +741,40 @@ EXPORT void Tuple::generate_fallbacks () if (! album) data->set_str (FallbackAlbum, _("Audio CD")); } - else if (strstr (filepath, "://")) + else if ((s = find_domain (filepath))) { - // URL: + // internet URL: // use the domain name as the album - if (album) - return; - - StringBuf buf = str_copy (filepath); - const char * domain = domain_name (buf); - - if (domain) - data->set_str (FallbackAlbum, domain); + if (! album) + data->set_str (FallbackAlbum, extract_domain (s)); } else { - // local file: + // any other URI: // use the top two path elements as the artist and album - if (artist && album) - return; - - StringBuf buf; -#ifdef _WIN32 - if (filepath[0] && filepath[1] == ':' && filepath[2] == '\\') - buf.steal (str_copy (filepath + 3)); + if ((s = strstr (filepath, "://"))) + { + s += 3; + sep = '/'; + } else + { +#ifdef _WIN32 + if (g_ascii_isalpha (filepath[0]) && filepath[1] == ':') + s = filepath + 2; + else #endif - buf.steal (str_copy (filepath)); + s = filepath; + + sep = G_DIR_SEPARATOR; + } + + StringBuf buf = str_copy (s); - char * first = split_folder (buf); - char * second = (first && first > buf) ? split_folder (buf) : nullptr; + char * first = split_folder (buf, sep); + char * second = (first && first > buf) ? split_folder (buf, sep) : nullptr; // skip common strings and avoid duplicates for (auto skip : (const char *[]) {"~", "music", artist, album, get_str (Genre)}) diff --git a/src/libaudcore/tuple.h b/src/libaudcore/tuple.h index a70fcdc..6d45d73 100644 --- a/src/libaudcore/tuple.h +++ b/src/libaudcore/tuple.h @@ -30,6 +30,7 @@ struct ReplayGainInfo; struct TupleData; +class PluginHandle; class VFSFile; class Tuple @@ -38,39 +39,48 @@ public: /* Smart pointer to the actual TupleData struct. * Uses create-on-write and copy-on-write. */ + enum State { + Initial, /* Song info has not yet been read */ + Valid, /* Song info has been successfully read */ + Failed /* Song info could not be read */ + }; + enum Field { Invalid = -1, Title = 0, /* Song title */ Artist, /* Song artist */ Album, /* Album name */ + AlbumArtist, /* Artist for entire album, if different than song artist */ Comment, /* Freeform comment */ Genre, /* Song's genre */ + Year, /* Year of production, performance, etc. */ + + Composer, /* Composer, if different than artist */ + Performer, /* Performer, if different than artist */ + Copyright, /* Copyright declaration */ + Date, /* Date of production, performance, etc. */ Track, /* Track number */ Length, /* Track length in milliseconds */ - Year, /* Year of production, performance, etc. */ - Quality, /* String representing quality, such as "Stereo, 44 kHz" */ + + Bitrate, /* Bitrate in kilobits (1000 bits)/sec */ Codec, /* Codec name, such as "Ogg Vorbis" */ + Quality, /* String representing quality, such as "Stereo, 44 kHz" */ Basename, /* Base filename, not including the folder path */ Path, /* Folder path, including the trailing "/" */ Suffix, /* Filename extension, not including the "." */ - AlbumArtist, /* Artist for entire album, if different than song artist */ - Composer, /* Composer of song, if different than artist */ - Performer, - Copyright, - Date, - MusicBrainz, /* MusicBrainz identifer for the song */ - MIMEType, - Bitrate, /* Bitrate in kbits/sec */ + AudioFile, /* URI of audio file, if different from the nominal URI + * (e.g. for a cuesheet entry, where the nominal URI + * points to the .cue file) */ Subtune, /* Index number of subtune */ NumSubtunes, /* Total number of subtunes in the file */ - StartTime, - EndTime, + StartTime, /* Playback start point (used for cuesheets) */ + EndTime, /* Playback end point (used for cuesheets) */ /* Preserving replay gain information accurately is a challenge since there * are several differents formats around. We use an integer fraction, with @@ -91,7 +101,7 @@ public: n_fields }; - typedef aud::range<Field, Title, FormattedTitle> all_fields; + typedef aud::range<Field, Field (0), Field (n_fields - 1)> all_fields; enum ValueType { String, @@ -125,19 +135,23 @@ public: return * this; } - explicit operator bool () const - { return (bool) data; } - bool operator== (const Tuple & b) const; bool operator!= (const Tuple & b) const { return ! operator== (b); } Tuple ref () const; + /* Gets/sets the state of the song info. Before setting the state to Valid, + * you should ensure that, at a minimum, set_filename() has been called. */ + State state () const; + void set_state (State st); + /* Returns the value type of a field if set, otherwise Empty. */ ValueType get_value_type (Field field) const; - /* Convenience function to determine whether a field is set. */ + /* Convenience functions */ + bool valid () const + { return state () == Valid; } bool is_set (Field field) const { return get_value_type (field) != Empty; } @@ -174,16 +188,16 @@ public: /* In addition to the normal fields, tuples contain an integer array of * subtune ID numbers. This function sets that array. It also sets * NumSubtunes to the value <n_subtunes>. */ - void set_subtunes (int n_subtunes, const int * subtunes); + void set_subtunes (short n_subtunes, const short * subtunes); /* Returns the length of the subtune array. If the array has not been set, * returns zero. Note that if NumSubtunes is changed after * set_subtunes() is called, this function returns the value <n_subtunes> * passed to set_subtunes(), not the value of NumSubtunes. */ - int get_n_subtunes () const; + short get_n_subtunes () const; /* Returns the <n>th member of the subtune array. */ - int get_nth_subtune (int n) const; + short get_nth_subtune (short n) const; /* Sets a Replay Gain field pair from a decimal string. */ void set_gain (Field field, Field unit_field, const char * str); @@ -211,11 +225,14 @@ private: }; /* somewhat out of place here */ -class PluginHandle; -struct PlaylistAddItem { +struct PlaylistAddItem +{ String filename; Tuple tuple; PluginHandle * decoder; + + PlaylistAddItem copy () const + { return {filename, tuple.ref (), decoder}; } }; #endif /* LIBAUDCORE_TUPLE_H */ diff --git a/src/libaudcore/util.cc b/src/libaudcore/util.cc index c0cdc63..4d92ded 100644 --- a/src/libaudcore/util.cc +++ b/src/libaudcore/util.cc @@ -121,6 +121,27 @@ void cut_path_element (char * path, int pos) path[pos] = 0; /* leave [drive letter and] leading slash */ } +bool is_cuesheet_entry (const char * filename) +{ + const char * ext, * sub; + uri_parse (filename, nullptr, & ext, & sub, nullptr); + return sub[0] && sub - ext == 4 && ! strcmp_nocase (ext, ".cue", 4); +} + +bool is_subtune (const char * filename) +{ + const char * sub; + uri_parse (filename, nullptr, nullptr, & sub, nullptr); + return sub[0]; +} + +StringBuf strip_subtune (const char * filename) +{ + const char * sub; + uri_parse (filename, nullptr, nullptr, & sub, nullptr); + return str_copy (filename, sub - filename); +} + /* Thomas Wang's 32-bit mix function. See: * http://web.archive.org/web/20070307172248/http://www.concentric.net/~Ttwang/tech/inthash.htm */ diff --git a/src/libaudcore/vfs.cc b/src/libaudcore/vfs.cc index f4b9e48..8748fd4 100644 --- a/src/libaudcore/vfs.cc +++ b/src/libaudcore/vfs.cc @@ -23,31 +23,66 @@ #define __STDC_FORMAT_MACROS #include <inttypes.h> #include <string.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <unistd.h> - -#include <glib/gstdio.h> #include "audstrings.h" #include "i18n.h" +#include "internal.h" #include "plugin.h" #include "plugins-internal.h" #include "probe-buffer.h" #include "runtime.h" #include "vfs_local.h" -static TransportPlugin * lookup_transport (const char * scheme) +/* embedded plugins */ +static LocalTransport local_transport; +static StdinTransport stdin_transport; + +static TransportPlugin * lookup_transport (const char * filename, + String & error, bool * custom_input = nullptr) { + StringBuf scheme = uri_get_scheme (filename); + if (! scheme) + { + AUDERR ("Invalid URI: %s\n", filename); + error = String (_("Invalid URI")); + return nullptr; + } + + if (! strcmp (scheme, "file")) + return & local_transport; + if (! strcmp (scheme, "stdin")) + return & stdin_transport; + for (PluginHandle * plugin : aud_plugin_list (PluginType::Transport)) { if (! aud_plugin_get_enabled (plugin)) continue; if (transport_plugin_has_scheme (plugin, scheme)) - return (TransportPlugin *) aud_plugin_get_header (plugin); + { + auto tp = (TransportPlugin *) aud_plugin_get_header (plugin); + if (tp) + return tp; + } } + if (custom_input) + { + for (PluginHandle * plugin : aud_plugin_list (PluginType::Input)) + { + if (! aud_plugin_get_enabled (plugin)) + continue; + + if (input_plugin_has_key (plugin, InputKey::Scheme, scheme)) + { + * custom_input = true; + return nullptr; + } + } + } + + AUDERR ("Unknown URI scheme: %s://\n", (const char *) scheme); + error = String (_("Unknown URI scheme")); return nullptr; } @@ -61,44 +96,28 @@ static TransportPlugin * lookup_transport (const char * scheme) */ EXPORT VFSFile::VFSFile (const char * filename, const char * mode) { - StringBuf scheme = uri_get_scheme (filename); - if (! scheme) - { - AUDERR ("Invalid URI: %s\n", filename); - m_error = String (_("Invalid URI")); + auto tp = lookup_transport (filename, m_error); + if (! tp) return; - } - const char * sub; - uri_parse (filename, nullptr, nullptr, & sub, nullptr); - StringBuf nosub = str_copy (filename, sub - filename); - - if (! strcmp (scheme, "file")) - m_impl.capture (vfs_local_fopen (nosub, mode, m_error)); - else if (! strcmp (scheme, "stdin")) - m_impl.capture (vfs_stdin_fopen (mode, m_error)); - else - { - TransportPlugin * tp = lookup_transport (scheme); - if (! tp) - { - AUDERR ("Unknown URI scheme: %s://", (const char *) scheme); - m_error = String (_("Unknown URI scheme")); - return; - } - - m_impl.capture (tp->fopen (nosub, mode, m_error)); - } - - if (! m_impl) + VFSImpl * impl = tp->fopen (strip_subtune (filename), mode, m_error); + if (! impl) return; /* enable buffering for read-only handles */ if (mode[0] == 'r' && ! strchr (mode, '+')) - m_impl.capture (new ProbeBuffer (filename, std::move (m_impl))); + impl = new ProbeBuffer (filename, impl); - AUDINFO ("<%p> open (mode %s) %s\n", m_impl.get (), mode, filename); + AUDINFO ("<%p> open (mode %s) %s\n", impl, mode, filename); m_filename = String (filename); + m_impl.capture (impl); +} + +EXPORT VFSFile VFSFile::tmpfile () +{ + VFSFile file; + file.m_impl.capture (vfs_tmpfile (file.m_error)); + return file; } /** @@ -155,8 +174,8 @@ EXPORT int64_t VFSFile::fwrite (const void * ptr, int64_t size, int64_t nmemb) EXPORT int VFSFile::fseek (int64_t offset, VFSSeekType whence) { AUDDBG ("<%p> seek to %" PRId64 " from %s\n", m_impl.get (), offset, - whence == SEEK_CUR ? "current" : whence == SEEK_SET ? "beginning" : - whence == SEEK_END ? "end" : "invalid"); + whence == VFS_SEEK_CUR ? "current" : whence == VFS_SEEK_SET ? "beginning" : + whence == VFS_SEEK_END ? "end" : "invalid"); if (! m_impl->fseek (offset, whence)) return 0; @@ -303,54 +322,72 @@ EXPORT Index<char> VFSFile::read_all () return buf; } -/** - * Wrapper for g_file_test(). - * - * @param path A path to test. - * @param test A GFileTest to run. - * @return The result of g_file_test(). - */ -EXPORT bool VFSFile::test_file (const char * path, VFSFileTest test) +EXPORT bool VFSFile::copy_from (VFSFile & source, int64_t size) { - if (strncmp (path, "file://", 7)) - return false; /* only local files are handled */ + constexpr int bufsize = 65536; - const char * sub; - uri_parse (path, nullptr, nullptr, & sub, nullptr); + Index<char> buf; + buf.resize (bufsize); - StringBuf no_sub = str_copy (path, sub - path); + while (size < 0 || size > 0) + { + int64_t to_read = (size > 0 && size < bufsize) ? size : bufsize; + int64_t readsize = source.fread (buf.begin (), 1, to_read); - StringBuf path2 = uri_to_filename (no_sub); - if (! path2) - return false; + if (size > 0) + size -= readsize; -#ifdef S_ISLNK - if (test & VFS_IS_SYMLINK) - { - GStatBuf st; - if (g_lstat (path2, & st) < 0) + if (fwrite (buf.begin (), 1, readsize) != readsize) return false; - if (S_ISLNK (st.st_mode)) - test = (VFSFileTest) (test & ~VFS_IS_SYMLINK); + if (readsize < to_read) + break; } -#endif - if (test & (VFS_IS_REGULAR | VFS_IS_DIR | VFS_IS_EXECUTABLE | VFS_EXISTS)) - { - GStatBuf st; - if (g_stat (path2, & st) < 0) - return false; + /* if a fixed size was requested, return true only if all the data was read. + * otherwise, return true only if the end of the source file was reached. */ + return size == 0 || (size < 0 && source.feof ()); +} + +EXPORT bool VFSFile::replace_with (VFSFile & source) +{ + if (source.fseek (0, VFS_SEEK_SET) < 0) + return false; - if (S_ISREG (st.st_mode)) - test = (VFSFileTest) (test & ~VFS_IS_REGULAR); - if (S_ISDIR (st.st_mode)) - test = (VFSFileTest) (test & ~VFS_IS_DIR); - if (st.st_mode & S_IXUSR) - test = (VFSFileTest) (test & ~VFS_IS_EXECUTABLE); + if (fseek (0, VFS_SEEK_SET) < 0) + return false; - test = (VFSFileTest) (test & ~VFS_EXISTS); - } + if (ftruncate (0) < 0) + return false; + + return copy_from (source, -1); +} + +EXPORT bool VFSFile::test_file (const char * filename, VFSFileTest test) +{ + String error; /* discarded */ + return test_file (filename, test, error) == test; +} + +EXPORT VFSFileTest VFSFile::test_file (const char * filename, VFSFileTest test, String & error) +{ + bool custom_input = false; + auto tp = lookup_transport (filename, error, & custom_input); + + /* for URI schemes handled by input plugins, return 0, indicating that we + * have no way of testing file attributes */ + if (custom_input) + return VFSFileTest (0); - return ! test; + /* for unsupported URI schemes, return VFS_NO_ACCESS */ + if (! tp) + return VFSFileTest (test & VFS_NO_ACCESS); + + return tp->test_file (strip_subtune (filename), test, error); +} + +EXPORT Index<String> VFSFile::read_folder (const char * filename, String & error) +{ + auto tp = lookup_transport (filename, error); + return tp ? tp->read_folder (filename, error) : Index<String> (); } diff --git a/src/libaudcore/vfs.h b/src/libaudcore/vfs.h index c6a0a16..e51cf84 100644 --- a/src/libaudcore/vfs.h +++ b/src/libaudcore/vfs.h @@ -29,6 +29,7 @@ #include <stdint.h> +#include <libaudcore/export.h> #include <libaudcore/index.h> #include <libaudcore/objects.h> @@ -37,7 +38,8 @@ enum VFSFileTest { VFS_IS_SYMLINK = (1 << 1), VFS_IS_DIR = (1 << 2), VFS_IS_EXECUTABLE = (1 << 3), - VFS_EXISTS = (1 << 4) + VFS_EXISTS = (1 << 4), + VFS_NO_ACCESS = (1 << 5) }; enum VFSSeekType { @@ -66,7 +68,17 @@ constexpr VFSSeekType to_vfs_seek_type (int whence) #endif // WANT_VFS_STDIO_COMPAT -class VFSImpl +// #undef POSIX functions/macros to avoid name conflicts +#undef fread +#undef fseek +#undef ftell +#undef fsize +#undef feof +#undef fwrite +#undef ftruncate +#undef fflush + +class LIBAUDCORE_PUBLIC VFSImpl { public: VFSImpl () {} @@ -100,6 +112,9 @@ public: VFSFile (const char * filename, const char * mode); + /* creates a temporary file (deleted when closed) */ + static VFSFile tmpfile (); + explicit operator bool () const { return (bool) m_impl; } const char * filename () const @@ -120,15 +135,33 @@ public: int ftruncate (int64_t length) __attribute__ ((warn_unused_result)); int fflush () __attribute__ ((warn_unused_result)); + /* used to read e.g. ICY metadata */ String get_metadata (const char * field); - void set_limit_to_buffer (bool limit); // added in 3.7 + /* the VFS layer buffers up to 256 KB of data at the beginning of files + * opened in read-only mode; this function disallows reading outside the + * buffered region (useful for probing the file type) */ + void set_limit_to_buffer (bool limit); /* utility functions */ + /* reads the entire file into memory (limited to 16 MB) */ Index<char> read_all (); - static bool test_file (const char * path, VFSFileTest test); + /* reads data from another open file and appends it to this one */ + bool copy_from (VFSFile & source, int64_t size = -1); + + /* overwrites the entire file with the contents of another */ + bool replace_with (VFSFile & source); + + /* tests certain attributes of a file without opening it. + * the 2-argument version returns true if all requested tests passed. + * the 3-argument version returns a bitmask indicating which tests passed. */ + static bool test_file (const char * filename, VFSFileTest test); + static VFSFileTest test_file (const char * filename, VFSFileTest test, String & error); + + /* returns a sorted list of folder entries (as full URIs) */ + static Index<String> read_folder (const char * filename, String & error); private: String m_filename, m_error; diff --git a/src/libaudcore/vfs_local.cc b/src/libaudcore/vfs_local.cc index c1b10e5..7af685f 100644 --- a/src/libaudcore/vfs_local.cc +++ b/src/libaudcore/vfs_local.cc @@ -17,15 +17,16 @@ * the use of this software. */ -#define WANT_VFS_STDIO_COMPAT -#include "vfs_local.h" - #include <errno.h> #include <string.h> #include <unistd.h> #include <glib/gstdio.h> +/* needs to be after system headers for #undef's to take effect */ +#define WANT_VFS_STDIO_COMPAT +#include "vfs_local.h" + #include "audstrings.h" #include "i18n.h" #include "runtime.h" @@ -75,7 +76,7 @@ private: LocalOp m_last_op; }; -VFSImpl * vfs_local_fopen (const char * uri, const char * mode, String & error) +VFSImpl * LocalTransport::fopen (const char * uri, const char * mode, String & error) { StringBuf path = uri_to_filename (uri); @@ -97,7 +98,7 @@ VFSImpl * vfs_local_fopen (const char * uri, const char * mode, String & error) StringBuf mode2 = str_concat ({mode, suffix}); - FILE * stream = g_fopen (path, mode2); + FILE * stream = ::g_fopen (path, mode2); if (! stream) { @@ -108,9 +109,9 @@ VFSImpl * vfs_local_fopen (const char * uri, const char * mode, String & error) * 2) UTF-8 filesystem mounted on legacy system */ if (errsave == ENOENT) { - StringBuf path2 = str_to_utf8 (uri_to_filename (uri, false)); + StringBuf path2 = uri_to_filename (uri, false); if (path2 && strcmp (path, path2)) - stream = g_fopen (path2, mode2); + stream = ::g_fopen (path2, mode2); } if (! stream) @@ -124,7 +125,7 @@ VFSImpl * vfs_local_fopen (const char * uri, const char * mode, String & error) return new LocalFile (path, stream); } -VFSImpl * vfs_stdin_fopen (const char * mode, String & error) +VFSImpl * StdinTransport::fopen (const char * uri, const char * mode, String & error) { if (mode[0] != 'r' || strchr (mode, '+')) { @@ -135,6 +136,21 @@ VFSImpl * vfs_stdin_fopen (const char * mode, String & error) return new LocalFile ("(stdin)", stdin); } +VFSImpl * vfs_tmpfile (String & error) +{ + FILE * stream = tmpfile (); + + if (! stream) + { + int errsave = errno; + perror ("(tmpfile)"); + error = String (strerror (errsave)); + return nullptr; + } + + return new LocalFile ("(tmpfile)", stream); +} + LocalFile::~LocalFile () { // do not close stdin @@ -305,3 +321,85 @@ ERR: perror (m_path); return -1; } + +VFSFileTest LocalTransport::test_file (const char * uri, VFSFileTest test, String & error) +{ + StringBuf path = uri_to_filename (uri); + if (! path) + { + error = String (_("Invalid file name")); + return VFSFileTest (test & VFS_NO_ACCESS); + } + + int passed = 0; + bool need_stat = true; + GStatBuf st; + +#ifdef S_ISLNK + if (test & VFS_IS_SYMLINK) + { + if (g_lstat (path, & st) < 0) + { + error = String (strerror (errno)); + passed |= VFS_NO_ACCESS; + goto out; + } + + if (S_ISLNK (st.st_mode)) + passed |= VFS_IS_SYMLINK; + else + need_stat = false; + } +#endif + + if (test & (VFS_IS_REGULAR | VFS_IS_DIR | VFS_IS_EXECUTABLE | VFS_EXISTS | VFS_NO_ACCESS)) + { + if (need_stat && g_stat (path, & st) < 0) + { + error = String (strerror (errno)); + passed |= VFS_NO_ACCESS; + goto out; + } + + if (S_ISREG (st.st_mode)) + passed |= VFS_IS_REGULAR; + if (S_ISDIR (st.st_mode)) + passed |= VFS_IS_DIR; + if (st.st_mode & S_IXUSR) + passed |= VFS_IS_EXECUTABLE; + + passed |= VFS_EXISTS; + } + +out: + return VFSFileTest (test & passed); +} + +Index<String> LocalTransport::read_folder (const char * uri, String & error) +{ + Index<String> entries; + + StringBuf path = uri_to_filename (uri); + if (! path) + { + error = String (_("Invalid file name")); + return entries; + } + + GError * gerr = nullptr; + GDir * folder = g_dir_open (path, 0, & gerr); + if (! folder) + { + error = String (gerr->message); + g_error_free (gerr); + return entries; + } + + const char * name; + while ((name = g_dir_read_name (folder))) + entries.append (String (filename_to_uri (filename_build ({path, name})))); + + g_dir_close (folder); + + return entries; +} diff --git a/src/libaudcore/vfs_local.h b/src/libaudcore/vfs_local.h index eed258b..071c0d9 100644 --- a/src/libaudcore/vfs_local.h +++ b/src/libaudcore/vfs_local.h @@ -20,9 +20,26 @@ #ifndef LIBAUDCORE_VFS_LOCAL_H #define LIBAUDCORE_VFS_LOCAL_H -#include "vfs.h" +#include "plugin.h" -VFSImpl * vfs_local_fopen (const char * uri, const char * mode, String & error); -VFSImpl * vfs_stdin_fopen (const char * mode, String & error); +class LocalTransport : public TransportPlugin +{ +public: + constexpr LocalTransport () : TransportPlugin (PluginInfo (), nullptr) {} + + VFSImpl * fopen (const char * filename, const char * mode, String & error); + VFSFileTest test_file (const char * filename, VFSFileTest test, String & error); + Index<String> read_folder (const char * filename, String & error); +}; + +class StdinTransport : public TransportPlugin +{ +public: + constexpr StdinTransport () : TransportPlugin (PluginInfo (), nullptr) {} + + VFSImpl * fopen (const char * filename, const char * mode, String & error); +}; + +VFSImpl * vfs_tmpfile (String & error); #endif /* LIBAUDCORE_VFS_LOCAL_H */ diff --git a/src/libaudcore/visualizer.h b/src/libaudcore/visualizer.h index d99eaff..5c825e6 100644 --- a/src/libaudcore/visualizer.h +++ b/src/libaudcore/visualizer.h @@ -20,7 +20,9 @@ #ifndef LIBAUDCORE_VISUALIZER_H #define LIBAUDCORE_VISUALIZER_H -class Visualizer +#include <libaudcore/export.h> + +class LIBAUDCORE_PUBLIC Visualizer { public: enum { diff --git a/src/libaudgui/Makefile b/src/libaudgui/Makefile index 55ba07f..dde93e1 100644 --- a/src/libaudgui/Makefile +++ b/src/libaudgui/Makefile @@ -1,6 +1,6 @@ SHARED_LIB = ${LIB_PREFIX}audgui${LIB_SUFFIX} -LIB_MAJOR = 3 -LIB_MINOR = 1 +LIB_MAJOR = 4 +LIB_MINOR = 0 SRCS = about.cc \ confirm.cc \ diff --git a/src/libaudgui/eq-preset.cc b/src/libaudgui/eq-preset.cc index 2aa37b0..a2f1c9d 100644 --- a/src/libaudgui/eq-preset.cc +++ b/src/libaudgui/eq-preset.cc @@ -103,14 +103,13 @@ static void populate_list () static void save_list () { - auto sort_cb = [] (const EqualizerPreset & a, const EqualizerPreset & b, void *) - { return strcmp (a.name, b.name); }; - Index<EqualizerPreset> presets; for (const PresetItem & item : preset_list) presets.append (item.preset); - presets.sort (sort_cb, nullptr); + presets.sort ([] (const EqualizerPreset & a, const EqualizerPreset & b) + { return strcmp (a.name, b.name); }); + aud_eq_write_presets (presets, "eq.preset"); } diff --git a/src/libaudgui/file-opener.cc b/src/libaudgui/file-opener.cc index a51abb8..efe339c 100644 --- a/src/libaudgui/file-opener.cc +++ b/src/libaudgui/file-opener.cc @@ -104,6 +104,7 @@ static GtkWidget * create_filebrowser (gboolean open) gtk_container_add ((GtkContainer *) window, vbox); GtkWidget * chooser = gtk_file_chooser_widget_new (GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_file_chooser_set_local_only ((GtkFileChooser *) chooser, false); gtk_file_chooser_set_select_multiple ((GtkFileChooser *) chooser, true); String path = aud_get_str ("audgui", "filesel_path"); diff --git a/src/libaudgui/infopopup.cc b/src/libaudgui/infopopup.cc index a117b2b..bc2edfd 100644 --- a/src/libaudgui/infopopup.cc +++ b/src/libaudgui/infopopup.cc @@ -318,7 +318,7 @@ EXPORT void audgui_infopopup_show (int playlist, int entry) String filename = aud_playlist_entry_get_filename (playlist, entry); Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry); - if (filename && tuple) + if (filename && tuple.valid ()) infopopup_show (filename, tuple); } diff --git a/src/libaudgui/infowin.cc b/src/libaudgui/infowin.cc index 353c9c3..5002dfc 100644 --- a/src/libaudgui/infowin.cc +++ b/src/libaudgui/infowin.cc @@ -72,6 +72,7 @@ static struct { static GtkWidget * infowin; static int current_playlist_id, current_entry; static String current_file; +static Tuple current_tuple; static PluginHandle * current_decoder = nullptr; static bool can_write = false; static QueuedFunc ministatus_timer; @@ -200,20 +201,17 @@ static void ministatus_display_message (const char * text) static void infowin_update_tuple () { - Tuple tuple; - tuple.set_filename (current_file); - - set_field_str_from_entry (tuple, Tuple::Title, widgets.title); - set_field_str_from_entry (tuple, Tuple::Artist, widgets.artist); - set_field_str_from_entry (tuple, Tuple::Album, widgets.album); - set_field_str_from_entry (tuple, Tuple::AlbumArtist, widgets.album_artist); - set_field_str_from_entry (tuple, Tuple::Comment, widgets.comment); - set_field_str_from_entry (tuple, Tuple::Genre, gtk_bin_get_child ((GtkBin *) - widgets.genre)); - set_field_int_from_entry (tuple, Tuple::Year, widgets.year); - set_field_int_from_entry (tuple, Tuple::Track, widgets.track); - - if (aud_file_write_tuple (current_file, current_decoder, tuple)) + set_field_str_from_entry (current_tuple, Tuple::Title, widgets.title); + set_field_str_from_entry (current_tuple, Tuple::Artist, widgets.artist); + set_field_str_from_entry (current_tuple, Tuple::Album, widgets.album); + set_field_str_from_entry (current_tuple, Tuple::AlbumArtist, widgets.album_artist); + set_field_str_from_entry (current_tuple, Tuple::Comment, widgets.comment); + set_field_str_from_entry (current_tuple, Tuple::Genre, + gtk_bin_get_child ((GtkBin *) widgets.genre)); + set_field_int_from_entry (current_tuple, Tuple::Year, widgets.year); + set_field_int_from_entry (current_tuple, Tuple::Track, widgets.track); + + if (aud_file_write_tuple (current_file, current_decoder, current_tuple)) { ministatus_display_message (_("Save successful")); gtk_widget_set_sensitive (widgets.apply, false); @@ -279,6 +277,7 @@ static void infowin_destroyed () infowin = nullptr; current_file = String (); + current_tuple = Tuple (); current_decoder = nullptr; } @@ -408,7 +407,7 @@ static void create_infowin () hook_associate ("art ready", (HookFunction) infowin_display_image, nullptr); } -static void infowin_show (int list, int entry, const char * filename, +static void infowin_show (int list, int entry, const String & filename, const Tuple & tuple, PluginHandle * decoder, bool writable) { if (! infowin) @@ -416,7 +415,8 @@ static void infowin_show (int list, int entry, const char * filename, current_playlist_id = aud_playlist_get_unique_id (list); current_entry = entry; - current_file = String (filename); + current_file = filename; + current_tuple = tuple.ref (); current_decoder = decoder; can_write = writable; @@ -469,17 +469,20 @@ EXPORT void audgui_infowin_show (int playlist, int entry) String error; PluginHandle * decoder = aud_playlist_entry_get_decoder (playlist, entry, Playlist::Wait, & error); + Tuple tuple = decoder ? aud_playlist_entry_get_tuple (playlist, entry, + Playlist::Wait, & error) : Tuple (); - if (decoder && ! aud_custom_infowin (filename, decoder)) + if (decoder && tuple.valid () && ! aud_custom_infowin (filename, decoder)) { - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::Wait, & error); - if (tuple) - { - tuple.delete_fallbacks (); - infowin_show (playlist, entry, filename, tuple, decoder, - aud_file_can_write_tuple (filename, decoder)); - } + /* cuesheet entries cannot be updated */ + bool can_write = aud_file_can_write_tuple (filename, decoder) && + ! tuple.is_set (Tuple::StartTime); + + tuple.delete_fallbacks (); + infowin_show (playlist, entry, filename, tuple, decoder, can_write); } + else + audgui_infowin_hide (); if (error) aud_ui_show_error (str_printf (_("Error opening %s:\n%s"), diff --git a/src/libaudgui/init.cc b/src/libaudgui/init.cc index d560bc3..ede7414 100644 --- a/src/libaudgui/init.cc +++ b/src/libaudgui/init.cc @@ -170,5 +170,4 @@ EXPORT void audgui_cleanup () plugin_menu_cleanup (); plugin_prefs_cleanup (); - urilist_cleanup (); } diff --git a/src/libaudgui/internal.h b/src/libaudgui/internal.h index d54204d..b43c97c 100644 --- a/src/libaudgui/internal.h +++ b/src/libaudgui/internal.h @@ -61,7 +61,4 @@ GtkWidget * plugin_view_new (PluginType type); void status_init (); void status_cleanup (); -/* urilist.c */ -void urilist_cleanup (); - #endif /* AUDGUI_INTERNAL_H */ diff --git a/src/libaudgui/jump-to-track-cache.cc b/src/libaudgui/jump-to-track-cache.cc index db07995..7dcf242 100644 --- a/src/libaudgui/jump-to-track-cache.cc +++ b/src/libaudgui/jump-to-track-cache.cc @@ -132,7 +132,7 @@ void JumpToTrackCache::init () item.entry = entry; item.path = String (uri_to_display (aud_playlist_entry_get_filename (playlist, entry))); - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::Guess); + Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::NoWait); item.title = tuple.get_str (Tuple::Title); item.artist = tuple.get_str (Tuple::Artist); item.album = tuple.get_str (Tuple::Album); diff --git a/src/libaudgui/jump-to-track.cc b/src/libaudgui/jump-to-track.cc index b903912..a681163 100644 --- a/src/libaudgui/jump-to-track.cc +++ b/src/libaudgui/jump-to-track.cc @@ -220,7 +220,7 @@ static void list_get_value (void * user, int row, int column, GValue * value) g_value_set_int (value, 1 + entry); break; case 1: - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::Guess); + Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::NoWait); g_value_set_string (value, tuple.get_str (Tuple::FormattedTitle)); break; } diff --git a/src/libaudgui/libaudgui-gtk.h b/src/libaudgui/libaudgui-gtk.h index fc144a5..8bd8c08 100644 --- a/src/libaudgui/libaudgui-gtk.h +++ b/src/libaudgui/libaudgui-gtk.h @@ -63,6 +63,10 @@ void audgui_simple_message (GtkWidget * * widget, GtkMessageType type, GtkWidget * audgui_button_new (const char * text, const char * icon, AudguiCallback callback, void * data); +GtkWidget * audgui_file_entry_new (GtkFileChooserAction action, const char * title); +String audgui_file_entry_get_uri (GtkWidget * entry); +void audgui_file_entry_set_uri (GtkWidget * entry, const char * uri); + GtkWidget * audgui_dialog_new (GtkMessageType type, const char * title, const char * text, GtkWidget * button1, GtkWidget * button2); void audgui_dialog_add_widget (GtkWidget * dialog, GtkWidget * widget); diff --git a/src/libaudgui/list.cc b/src/libaudgui/list.cc index a0b6ce6..906922b 100644 --- a/src/libaudgui/list.cc +++ b/src/libaudgui/list.cc @@ -343,6 +343,10 @@ static void drag_begin (GtkWidget * widget, GdkDragContext * context, g_signal_stop_emission_by_name (widget, "drag-begin"); model->dragging = true; + + /* If button_press_cb preserved a multiple selection, tell button_release_cb + * not to clear it. */ + model->frozen = false; } static void drag_end (GtkWidget * widget, GdkDragContext * context, @@ -426,14 +430,10 @@ static gboolean drag_motion (GtkWidget * widget, GdkDragContext * context, { g_signal_stop_emission_by_name (widget, "drag-motion"); - /* If button_press_cb preserved a multiple selection, tell button_release_cb - * not to clear it. */ - model->frozen = false; - - if (model->dragging && MODEL_HAS_CB (model, shift_rows)) /* dragging within same list */ - gdk_drag_status (context, GDK_ACTION_MOVE, time); - else if (MODEL_HAS_CB (model, data_type)) /* cross-widget dragging */ - gdk_drag_status (context, GDK_ACTION_COPY, time); + if (model->dragging && MODEL_HAS_CB (model, shift_rows)) + gdk_drag_status (context, GDK_ACTION_MOVE, time); /* dragging within same list */ + else if (MODEL_HAS_CB (model, data_type) && MODEL_HAS_CB (model, receive_data)) + gdk_drag_status (context, GDK_ACTION_COPY, time); /* cross-widget dragging */ else return false; @@ -489,15 +489,17 @@ static gboolean drag_drop (GtkWidget * widget, GdkDragContext * context, int x, gboolean success = true; int row = audgui_list_row_at_point_rounded (widget, x, y); - if (model->dragging && MODEL_HAS_CB (model, shift_rows)) /* dragging within same list */ + if (model->dragging && MODEL_HAS_CB (model, shift_rows)) { + /* dragging within same list */ if (model->clicked_row >= 0 && model->clicked_row < model->rows) model->cbs->shift_rows (model->user, model->clicked_row, row); else success = false; } - else if (MODEL_HAS_CB (model, data_type)) /* cross-widget dragging */ + else if (MODEL_HAS_CB (model, data_type) && MODEL_HAS_CB (model, receive_data)) { + /* cross-widget dragging */ model->receive_row = row; gtk_drag_get_data (widget, context, gdk_atom_intern (model->cbs->data_type, false), time); @@ -608,18 +610,22 @@ EXPORT GtkWidget * audgui_list_new_real (const AudguiListCallbacks * cbs, int cb gboolean supports_drag = false; - if (MODEL_HAS_CB (model, data_type) && MODEL_HAS_CB (model, get_data) && - MODEL_HAS_CB (model, receive_data)) + if (MODEL_HAS_CB (model, data_type) && + (MODEL_HAS_CB (model, get_data) || MODEL_HAS_CB (model, receive_data))) { const GtkTargetEntry target = {(char *) cbs->data_type, 0, 0}; - gtk_drag_source_set (list, GDK_BUTTON1_MASK, & target, 1, - GDK_ACTION_COPY); - gtk_drag_dest_set (list, (GtkDestDefaults) 0, & target, 1, GDK_ACTION_COPY); + if (MODEL_HAS_CB (model, get_data)) + { + gtk_drag_source_set (list, GDK_BUTTON1_MASK, & target, 1, GDK_ACTION_COPY); + g_signal_connect (list, "drag-data-get", (GCallback) drag_data_get, model); + } - g_signal_connect (list, "drag-data-get", (GCallback) drag_data_get, model); - g_signal_connect (list, "drag-data-received", (GCallback) - drag_data_received, model); + if (MODEL_HAS_CB (model, receive_data)) + { + gtk_drag_dest_set (list, (GtkDestDefaults) 0, & target, 1, GDK_ACTION_COPY); + g_signal_connect (list, "drag-data-received", (GCallback) drag_data_received, model); + } supports_drag = true; } diff --git a/src/libaudgui/menu.cc b/src/libaudgui/menu.cc index 3069d8b..ef5ac34 100644 --- a/src/libaudgui/menu.cc +++ b/src/libaudgui/menu.cc @@ -65,7 +65,7 @@ static void unhook_cb (GtkCheckMenuItem * check, const AudguiMenuItem * item) EXPORT GtkWidget * audgui_menu_item_new_with_domain (const AudguiMenuItem * item, GtkAccelGroup * accel, const char * domain) { - const char * name = domain ? dgettext (domain, item->name) : item->name; + const char * name = (domain && item->name) ? dgettext (domain, item->name) : item->name; GtkWidget * widget = nullptr; if (name && item->func && ! item->cname) /* normal widget */ diff --git a/src/libaudgui/playlists.cc b/src/libaudgui/playlists.cc index d106292..4922fba 100644 --- a/src/libaudgui/playlists.cc +++ b/src/libaudgui/playlists.cc @@ -68,7 +68,7 @@ static void finish_job (void * data) Playlist::GetMode mode = Playlist::Wait; if (aud_get_bool (nullptr, "metadata_on_play")) - mode = Playlist::Nothing; + mode = Playlist::NoWait; if (list >= 0) { @@ -184,6 +184,7 @@ static void create_selector (ImportExportJob * job, const char * filename, const } job->selector = gtk_file_chooser_dialog_new (title, nullptr, action, nullptr, nullptr); + gtk_file_chooser_set_local_only ((GtkFileChooser *) job->selector, false); if (filename) gtk_file_chooser_set_uri ((GtkFileChooser *) job->selector, filename); diff --git a/src/libaudgui/prefs-widget.cc b/src/libaudgui/prefs-widget.cc index df6b68b..c240ece 100644 --- a/src/libaudgui/prefs-widget.cc +++ b/src/libaudgui/prefs-widget.cc @@ -34,7 +34,7 @@ static void widget_changed (GtkWidget * widget, const PreferencesWidget * w) { case PreferencesWidget::CheckButton: { - gboolean set = gtk_toggle_button_get_active ((GtkToggleButton *) widget); + bool set = gtk_toggle_button_get_active ((GtkToggleButton *) widget); w->cfg.set_bool (set); auto child = (GtkWidget *) g_object_get_data ((GObject *) widget, "child"); @@ -45,10 +45,17 @@ static void widget_changed (GtkWidget * widget, const PreferencesWidget * w) } case PreferencesWidget::RadioButton: - if (gtk_toggle_button_get_active ((GtkToggleButton *) widget)) + { + bool set = gtk_toggle_button_get_active ((GtkToggleButton *) widget); + if (set) w->cfg.set_int (w->data.radio_btn.value); + auto child = (GtkWidget *) g_object_get_data ((GObject *) widget, "child"); + if (child) + gtk_widget_set_sensitive (child, set); + break; + } case PreferencesWidget::SpinButton: if (w->cfg.type == WidgetConfig::Int) @@ -66,6 +73,13 @@ static void widget_changed (GtkWidget * widget, const PreferencesWidget * w) w->cfg.set_string (gtk_entry_get_text ((GtkEntry *) widget)); break; + case PreferencesWidget::FileEntry: + { + String uri = audgui_file_entry_get_uri (widget); + w->cfg.set_string (uri ? uri : ""); + break; + } + case PreferencesWidget::ComboBox: { auto items = (const ComboItem *) g_object_get_data ((GObject *) widget, "comboitems"); @@ -120,6 +134,10 @@ static void widget_update (void *, void * widget) gtk_entry_set_text ((GtkEntry *) widget, w->cfg.get_string ()); break; + case PreferencesWidget::FileEntry: + audgui_file_entry_set_uri ((GtkWidget *) widget, w->cfg.get_string ()); + break; + case PreferencesWidget::ComboBox: combobox_update ((GtkWidget *) widget, w); break; @@ -158,6 +176,7 @@ static void widget_init (GtkWidget * widget, const PreferencesWidget * w) break; case PreferencesWidget::Entry: + case PreferencesWidget::FileEntry: case PreferencesWidget::ComboBox: g_signal_connect (widget, "changed", (GCallback) widget_changed, (void *) w); break; @@ -247,6 +266,31 @@ static void create_entry (const PreferencesWidget * widget, GtkWidget * * label, widget_init (* entry, widget); } +/* WIDGET_FILE_ENTRY */ + +static void create_file_entry (const PreferencesWidget * widget, + GtkWidget * * label, GtkWidget * * entry, const char * domain) +{ + switch (widget->data.file_entry.mode) + { + case FileSelectMode::File: + * entry = audgui_file_entry_new (GTK_FILE_CHOOSER_ACTION_OPEN, _("Choose File")); + break; + + case FileSelectMode::Folder: + * entry = audgui_file_entry_new (GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("Choose Folder")); + break; + } + + if (widget->label) + { + * label = gtk_label_new (dgettext (domain, widget->label)); + gtk_misc_set_alignment ((GtkMisc *) * label, 1, 0.5); + } + + widget_init (* entry, widget); +} + /* WIDGET_COMBO_BOX */ static void combobox_update (GtkWidget * combobox, const PreferencesWidget * widget) @@ -339,6 +383,11 @@ static void fill_table (GtkWidget * table, middle_policy = (GtkAttachOptions) (GTK_EXPAND | GTK_FILL); break; + case PreferencesWidget::FileEntry: + create_file_entry (& w, & widget_left, & widget_middle, domain); + middle_policy = (GtkAttachOptions) (GTK_EXPAND | GTK_FILL); + break; + case PreferencesWidget::ComboBox: create_cbox (& w, & widget_left, & widget_middle, domain); break; @@ -371,7 +420,7 @@ void audgui_create_widgets_with_domain (GtkWidget * box, { GtkWidget * widget = nullptr, * child_box = nullptr; bool disable_child = false; - GSList * radio_btn_group = nullptr; + GSList * radio_btn_group[2] = {}; int indent = 0; int spacing = 0; @@ -406,8 +455,11 @@ void audgui_create_widgets_with_domain (GtkWidget * box, widget = nullptr; disable_child = false; - if (radio_btn_group && w.type != PreferencesWidget::RadioButton) - radio_btn_group = nullptr; + if (w.type != PreferencesWidget::RadioButton) + radio_btn_group[w.child] = nullptr; + + if (! w.child) + radio_btn_group[true] = nullptr; switch (w.type) { @@ -449,9 +501,10 @@ void audgui_create_widgets_with_domain (GtkWidget * box, } case PreferencesWidget::RadioButton: - widget = gtk_radio_button_new_with_mnemonic (radio_btn_group, - dgettext (domain, w.label)); - radio_btn_group = gtk_radio_button_get_group ((GtkRadioButton *) widget); + widget = gtk_radio_button_new_with_mnemonic + (radio_btn_group[w.child], dgettext (domain, w.label)); + radio_btn_group[w.child] = gtk_radio_button_get_group ((GtkRadioButton *) widget); + disable_child = (w.cfg.get_int () != w.data.radio_btn.value); widget_init (widget, & w); break; @@ -503,11 +556,16 @@ void audgui_create_widgets_with_domain (GtkWidget * box, break; case PreferencesWidget::Entry: + case PreferencesWidget::FileEntry: { widget = gtk_hbox_new (false, 6); GtkWidget * entry = nullptr; - create_entry (& w, & label, & entry, domain); + + if (w.type == PreferencesWidget::FileEntry) + create_file_entry (& w, & label, & entry, domain); + else + create_entry (& w, & label, & entry, domain); if (label) gtk_box_pack_start ((GtkBox *) widget, label, false, false, 0); diff --git a/src/libaudgui/prefs-window.cc b/src/libaudgui/prefs-window.cc index 948119c..5ba9a09 100644 --- a/src/libaudgui/prefs-window.cc +++ b/src/libaudgui/prefs-window.cc @@ -125,12 +125,20 @@ static const ComboItem chardet_detector_presets[] = { }; static const ComboItem bitdepth_elements[] = { + ComboItem (N_("Automatic"), -1), ComboItem ("16", 16), ComboItem ("24", 24), ComboItem ("32", 32), ComboItem (N_("Floating point"), 0) }; +static const ComboItem record_elements[] = { + ComboItem (N_("As decoded"), (int) OutputStream::AsDecoded), + ComboItem (N_("After applying ReplayGain"), (int) OutputStream::AfterReplayGain), + ComboItem (N_("After applying effects"), (int) OutputStream::AfterEffects), + ComboItem (N_("After applying equalization"), (int) OutputStream::AfterEqualizer) +}; + static Index<ComboItem> iface_combo_elements; static int iface_combo_selected; static GtkWidget * iface_prefs_box; @@ -160,7 +168,7 @@ static void output_bit_depth_changed (); static const PreferencesWidget output_combo_widgets[] = { WidgetCombo (N_("Output plugin:"), - WidgetInt (output_combo_selected, output_combo_changed), + WidgetInt (output_combo_selected, output_combo_changed, "audgui update output combo"), {0, output_combo_fill}), WidgetCustomGTK (output_create_config_button), WidgetCustomGTK (output_create_about_button) @@ -199,15 +207,19 @@ static const PreferencesWidget audio_page_widgets[] = { WidgetSpin (N_("Buffer size:"), WidgetInt (0, "output_buffer_size"), {100, 10000, 1000, N_("ms")}), - WidgetCustomGTK (record_create_checkbox), - WidgetBox ({{record_buttons}, true}, - WIDGET_CHILD), WidgetCheck (N_("Soft clipping"), WidgetBool (0, "soft_clipping")), WidgetCheck (N_("Use software volume control (not recommended)"), WidgetBool (0, "software_volume_control")), - WidgetLabel (N_("<b>Replay Gain</b>")), - WidgetCheck (N_("Enable Replay Gain"), + WidgetLabel (N_("<b>Recording Settings</b>")), + WidgetCustomGTK (record_create_checkbox), + WidgetBox ({{record_buttons}, true}, + WIDGET_CHILD), + WidgetCombo (N_("Record stream:"), + WidgetInt (0, "record_stream"), + {{record_elements}}), + WidgetLabel (N_("<b>ReplayGain</b>")), + WidgetCheck (N_("Enable ReplayGain"), WidgetBool (0, "enable_replay_gain")), WidgetCheck (N_("Album mode"), WidgetBool (0, "replay_gain_album"), @@ -215,8 +227,6 @@ static const PreferencesWidget audio_page_widgets[] = { WidgetCheck (N_("Prevent clipping (recommended)"), WidgetBool (0, "enable_clipping_prevention"), WIDGET_CHILD), - WidgetLabel (N_("<b>Adjust Levels</b>"), - WIDGET_CHILD), WidgetTable ({{gain_table}}, WIDGET_CHILD) }; @@ -279,8 +289,10 @@ static const PreferencesWidget playlist_page_widgets[] = { WidgetLabel (N_("<b>Song Display</b>")), WidgetCheck (N_("Show song numbers"), WidgetBool (0, "show_numbers_in_pl", send_title_change)), - WidgetCheck (N_("Show leading zeroes (02:00 instead of 2:00)"), + WidgetCheck (N_("Show leading zeroes (02:00 vs. 2:00)"), WidgetBool (0, "leading_zero", send_title_change)), + WidgetCheck (N_("Show hours separately (1:30:00 vs. 90:00)"), + WidgetBool (0, "show_hours", send_title_change)), WidgetCustomGTK (create_titlestring_table), WidgetLabel (N_("<b>Compatibility</b>")), WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"), @@ -617,13 +629,20 @@ static void create_appearance_category () static void output_combo_changed () { - PluginHandle * plugin = aud_plugin_list (PluginType::Output)[output_combo_selected]; + auto & list = aud_plugin_list (PluginType::Output); + PluginHandle * plugin = list[output_combo_selected]; if (aud_plugin_enable (plugin, true)) { gtk_widget_set_sensitive (output_config_button, aud_plugin_has_configure (plugin)); gtk_widget_set_sensitive (output_about_button, aud_plugin_has_about (plugin)); } + else + { + /* set combo box back to current output */ + output_combo_selected = list.find (aud_plugin_get_current (PluginType::Output)); + hook_call ("audgui update output combo", nullptr); + } } static ArrayRef<ComboItem> output_combo_fill () diff --git a/src/libaudgui/preset-browser.cc b/src/libaudgui/preset-browser.cc index fa7ccb5..9144086 100644 --- a/src/libaudgui/preset-browser.cc +++ b/src/libaudgui/preset-browser.cc @@ -51,6 +51,8 @@ static void show_preset_browser (const char * title, gboolean save, GTK_RESPONSE_CANCEL, save ? _("Save") : _("Load"), GTK_RESPONSE_ACCEPT, nullptr); + gtk_file_chooser_set_local_only ((GtkFileChooser *) browser, false); + if (default_filename) gtk_file_chooser_set_current_name ((GtkFileChooser *) browser, default_filename); diff --git a/src/libaudgui/queue-manager.cc b/src/libaudgui/queue-manager.cc index 195a6ba..2b5e6d0 100644 --- a/src/libaudgui/queue-manager.cc +++ b/src/libaudgui/queue-manager.cc @@ -45,7 +45,7 @@ static void get_value (void * user, int row, int column, GValue * value) g_value_set_int (value, 1 + entry); break; case COLUMN_TITLE: - Tuple tuple = aud_playlist_entry_get_tuple (list, entry, Playlist::Guess); + Tuple tuple = aud_playlist_entry_get_tuple (list, entry, Playlist::NoWait); g_value_set_string (value, tuple.get_str (Tuple::FormattedTitle)); break; } diff --git a/src/libaudgui/status.cc b/src/libaudgui/status.cc index fa04fd3..6ba9b3c 100644 --- a/src/libaudgui/status.cc +++ b/src/libaudgui/status.cc @@ -27,7 +27,7 @@ static GtkWidget * progress_window; static GtkWidget * progress_label, * progress_label_2; -static GtkWidget * error_window; +static GtkWidget * error_window, * info_window; static void create_progress_window () { @@ -85,12 +85,18 @@ static void show_error (void * data, void * user) audgui_simple_message (& error_window, GTK_MESSAGE_ERROR, _("Error"), (const char *) data); } +static void show_info (void * data, void * user) +{ + audgui_simple_message (& info_window, GTK_MESSAGE_INFO, _("Information"), (const char *) data); +} + void status_init () { hook_associate ("ui show progress", show_progress, nullptr); hook_associate ("ui show progress 2", show_progress_2, nullptr); hook_associate ("ui hide progress", hide_progress, nullptr); hook_associate ("ui show error", show_error, nullptr); + hook_associate ("ui show info", show_info, nullptr); } void status_cleanup () @@ -99,9 +105,12 @@ void status_cleanup () hook_dissociate ("ui show progress 2", show_progress_2); hook_dissociate ("ui hide progress", hide_progress); hook_dissociate ("ui show error", show_error); + hook_dissociate ("ui show info", show_info); if (progress_window) gtk_widget_destroy (progress_window); if (error_window) gtk_widget_destroy (error_window); + if (info_window) + gtk_widget_destroy (info_window); } diff --git a/src/libaudgui/urilist.cc b/src/libaudgui/urilist.cc index 1dc04e4..a0809f4 100644 --- a/src/libaudgui/urilist.cc +++ b/src/libaudgui/urilist.cc @@ -21,33 +21,13 @@ #include <libaudcore/audstrings.h> #include <libaudcore/drct.h> -#include <libaudcore/mainloop.h> -#include <libaudcore/multihash.h> #include <libaudcore/playlist.h> -#include <libaudcore/tuple.h> -#include <libaudcore/vfs.h> #include "libaudgui.h" -static SimpleHash<String, Tuple> tuple_cache; -static QueuedFunc cleanup_timer; - -void urilist_cleanup () -{ - tuple_cache.clear (); - cleanup_timer.stop (); -} - static String check_uri (const char * name) { - if (! strstr (name, "://")) - { - StringBuf uri = filename_to_uri (name); - if (uri) - return String (uri); - } - - return String (name); + return strstr (name, "://") ? String (name) : String (filename_to_uri (name)); } static Index<PlaylistAddItem> urilist_to_index (const char * list) @@ -67,12 +47,7 @@ static Index<PlaylistAddItem> urilist_to_index (const char * list) next = end = strchr (list, 0); if (end > list) - { - String filename = check_uri (str_copy (list, end - list)); - const Tuple * tuple = tuple_cache.lookup (filename); - - index.append (filename, tuple ? tuple->ref () : Tuple ()); - } + index.append (check_uri (str_copy (list, end - list))); list = next; } @@ -92,6 +67,8 @@ EXPORT void audgui_urilist_insert (int playlist, int at, const char * list) EXPORT Index<char> audgui_urilist_create_from_selected (int playlist) { + aud_playlist_cache_selected (playlist); + Index<char> buf; int entries = aud_playlist_entry_count (playlist); @@ -103,15 +80,9 @@ EXPORT Index<char> audgui_urilist_create_from_selected (int playlist) buf.append ('\n'); String filename = aud_playlist_entry_get_filename (playlist, count); - Tuple tuple = aud_playlist_entry_get_tuple (playlist, count, Playlist::Nothing); - buf.insert (filename, -1, strlen (filename)); - if (tuple) - tuple_cache.add (filename, std::move (tuple)); } } - cleanup_timer.queue (30000, [] (void *) { urilist_cleanup (); }, nullptr); - return buf; } diff --git a/src/libaudgui/util.cc b/src/libaudgui/util.cc index 54e1c74..98307e5 100644 --- a/src/libaudgui/util.cc +++ b/src/libaudgui/util.cc @@ -125,6 +125,86 @@ EXPORT GtkWidget * audgui_button_new (const char * text, const char * icon, return button; } +struct FileEntryData { + GtkFileChooserAction action; + String title; +}; + +static void entry_response_cb (GtkWidget * dialog, int response, GtkWidget * entry) +{ + if (response == GTK_RESPONSE_ACCEPT) + { + char * uri = gtk_file_chooser_get_uri ((GtkFileChooser *) dialog); + if (uri) + { + audgui_file_entry_set_uri (entry, uri); + g_free (uri); + } + } + + gtk_widget_destroy (dialog); +} + +static void entry_browse_cb (GtkWidget * entry, GtkEntryIconPosition pos, + GdkEvent * event, const FileEntryData * data) +{ + GtkWidget * dialog = gtk_file_chooser_dialog_new (data->title, nullptr, + data->action, _("Open"), GTK_RESPONSE_ACCEPT, _("Cancel"), + GTK_RESPONSE_REJECT, nullptr); + + gtk_file_chooser_set_local_only ((GtkFileChooser *) dialog, false); + + String uri = audgui_file_entry_get_uri (entry); + if (uri) + gtk_file_chooser_set_uri ((GtkFileChooser *) dialog, uri); + + g_signal_connect (dialog, "response", (GCallback) entry_response_cb, entry); + g_signal_connect_object (entry, "destroy", (GCallback) gtk_widget_destroy, + dialog, G_CONNECT_SWAPPED); + + gtk_widget_show (dialog); +} + +EXPORT GtkWidget * audgui_file_entry_new (GtkFileChooserAction action, const char * title) +{ + GtkWidget * entry = gtk_entry_new (); + + auto data = new FileEntryData {action, String (title)}; + auto destroy_cb = [] (void * data) { delete (FileEntryData *) data; }; + g_object_set_data_full ((GObject *) entry, "file-entry-data", data, destroy_cb); + + gtk_entry_set_icon_from_icon_name ((GtkEntry *) entry, + GTK_ENTRY_ICON_SECONDARY, "document-open"); + g_signal_connect (entry, "icon-press", (GCallback) entry_browse_cb, data); + + return entry; +} + +EXPORT String audgui_file_entry_get_uri (GtkWidget * entry) +{ + const char * text = gtk_entry_get_text ((GtkEntry *) entry); + + if (! text[0]) + return String (); + else if (strstr (text, "://")) + return String (text); + else + return String (filename_to_uri (filename_normalize (filename_expand (str_copy (text))))); +} + +EXPORT void audgui_file_entry_set_uri (GtkWidget * entry, const char * uri) +{ + if (! uri || ! uri[0]) + { + gtk_entry_set_text ((GtkEntry *) entry, ""); + return; + } + + StringBuf path = uri_to_filename (uri, false); + gtk_entry_set_text ((GtkEntry *) entry, path ? filename_contract (std::move (path)) : uri); + gtk_editable_set_position ((GtkEditable *) entry, -1); +} + EXPORT GtkWidget * audgui_dialog_new (GtkMessageType type, const char * title, const char * text, GtkWidget * button1, GtkWidget * button2) { diff --git a/src/libaudqt/Makefile b/src/libaudqt/Makefile index e8bbd2f..b3b6f9a 100644 --- a/src/libaudqt/Makefile +++ b/src/libaudqt/Makefile @@ -1,6 +1,6 @@ SHARED_LIB = ${LIB_PREFIX}audqt${LIB_SUFFIX} -LIB_MAJOR = 0 -LIB_MINOR = 1 +LIB_MAJOR = 1 +LIB_MINOR = 0 SRCS = about.cc \ art.cc \ @@ -22,7 +22,11 @@ SRCS = about.cc \ util.cc \ volumebutton.cc -INCLUDES = libaudqt.h iface.h volumebutton.h info-widget.h menu.h +INCLUDES = export.h \ + iface.h \ + info-widget.h \ + libaudqt.h \ + menu.h include ../../buildsys.mk include ../../extra.mk @@ -33,7 +37,8 @@ LD = ${CXX} CPPFLAGS := -I.. -I../.. \ ${CPPFLAGS} \ - ${QT_CFLAGS} + ${QT_CFLAGS} \ + -DLIBAUDQT_BUILD CFLAGS += ${LIB_CFLAGS} diff --git a/src/libaudqt/export.h b/src/libaudqt/export.h new file mode 100644 index 0000000..457e848 --- /dev/null +++ b/src/libaudqt/export.h @@ -0,0 +1,33 @@ +/* + * export.h + * Copyright 2016 John Lindgren + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions, and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions, and the following disclaimer in the documentation + * provided with the distribution. + * + * This software is provided "as is" and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising from + * the use of this software. + */ + +#ifndef LIBAUDQT_EXPORT_H +#define LIBAUDQT_EXPORT_H + +#ifdef _WIN32 + #ifdef LIBAUDQT_BUILD + #define LIBAUDQT_PUBLIC __declspec(dllexport) + #else + #define LIBAUDQT_PUBLIC __declspec(dllimport) + #endif +#else + #define LIBAUDQT_PUBLIC __attribute__ ((visibility ("default"))) +#endif + +#endif // LIBAUDQT_EXPORT_H diff --git a/src/libaudqt/iface.h b/src/libaudqt/iface.h index e799dd1..fe36766 100644 --- a/src/libaudqt/iface.h +++ b/src/libaudqt/iface.h @@ -21,30 +21,33 @@ #define LIBAUDQT_IFACE_H #include <libaudcore/plugin.h> +#include <libaudqt/export.h> #include <libaudqt/libaudqt.h> #include <libaudqt/menu.h> namespace audqt { -class QtIfacePlugin : public IfacePlugin +class LIBAUDQT_PUBLIC QtIfacePlugin : public IfacePlugin { public: constexpr QtIfacePlugin (PluginInfo info) : IfacePlugin (info) {} void show_about_window () { aboutwindow_show (); } void hide_about_window () { aboutwindow_hide (); } - void show_filebrowser (bool open) { fileopener_show (open ? FileMode::Open : FileMode::Add); } + + void show_filebrowser (bool open) + { fileopener_show (open ? FileMode::Open : FileMode::Add); } + void hide_filebrowser () {} void show_jump_to_song () {} void hide_jump_to_song () {} void show_prefs_window () { prefswin_show (); } void hide_prefs_window () { prefswin_hide (); } - void plugin_menu_add (AudMenuID id, void func (), const char * name, const char * icon) { - menu_add (id, func, name, icon); - } - void plugin_menu_remove (AudMenuID id, void func ()) { - menu_remove (id, func); - } + + void plugin_menu_add (AudMenuID id, void func (), const char * name, const char * icon) + { menu_add (id, func, name, icon); } + void plugin_menu_remove (AudMenuID id, void func ()) + { menu_remove (id, func); } }; } // namespace audqt diff --git a/src/libaudqt/info-widget.cc b/src/libaudqt/info-widget.cc index 85b0c0c..aa86e14 100644 --- a/src/libaudqt/info-widget.cc +++ b/src/libaudqt/info-widget.cc @@ -25,6 +25,7 @@ #include <libaudcore/i18n.h> #include <libaudcore/probe.h> +#include <libaudcore/tuple.h> namespace audqt { @@ -51,15 +52,48 @@ static const TupleFieldMap tuple_field_map[] = { {nullptr, Tuple::Invalid, false}, {N_("Technical"), Tuple::Invalid, false}, {N_("Length"), Tuple::Length, false}, - {N_("MIME Type"), Tuple::MIMEType, false}, {N_("Codec"), Tuple::Codec, false}, {N_("Quality"), Tuple::Quality, false}, {N_("Bitrate"), Tuple::Bitrate, false}, }; -EXPORT InfoWidget::InfoWidget (QWidget * parent) : QTreeView (parent) +class InfoModel : public QAbstractTableModel { - setModel (& m_model); +public: + InfoModel (QObject * parent = nullptr) : + QAbstractTableModel (parent) {} + + int rowCount (const QModelIndex & parent = QModelIndex ()) const + { return aud::n_elems (tuple_field_map); } + int columnCount (const QModelIndex & parent = QModelIndex ()) const + { return 2; } + + QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; + bool setData (const QModelIndex & index, const QVariant & value, int role = Qt::EditRole); + Qt::ItemFlags flags (const QModelIndex & index) const; + + void setTupleData (const Tuple & tuple, String filename, PluginHandle * plugin) + { + m_tuple = tuple.ref (); + m_filename = filename; + m_plugin = plugin; + m_dirty = false; + } + + bool updateFile () const; + +private: + Tuple m_tuple; + String m_filename; + PluginHandle * m_plugin = nullptr; + bool m_dirty = false; +}; + +EXPORT InfoWidget::InfoWidget (QWidget * parent) : + QTreeView (parent), + m_model (new InfoModel (this)) +{ + setModel (m_model); header ()->hide (); setIndentation (0); resizeColumnToContents (0); @@ -72,29 +106,14 @@ EXPORT InfoWidget::~InfoWidget () EXPORT void InfoWidget::fillInfo (int playlist, int entry, const char * filename, const Tuple & tuple, PluginHandle * decoder, bool updating_enabled) { - m_model.setTupleData (tuple, String (filename), decoder); + m_model->setTupleData (tuple, String (filename), decoder); reset (); setEditTriggers (updating_enabled ? QAbstractItemView::SelectedClicked : QAbstractItemView::NoEditTriggers); } EXPORT bool InfoWidget::updateFile () { - return m_model.updateFile (); -} - -InfoModel::InfoModel (QObject * parent) : QAbstractTableModel (parent) -{ -} - -int InfoModel::rowCount (const QModelIndex & parent) const -{ - auto r = ArrayRef<TupleFieldMap> (tuple_field_map); - return r.len; -} - -int InfoModel::columnCount (const QModelIndex & parent) const -{ - return 2; + return m_model->updateFile (); } bool InfoModel::updateFile () const @@ -102,10 +121,7 @@ bool InfoModel::updateFile () const if (! m_dirty) return true; - Tuple t = m_tuple.ref (); - t.set_filename (m_filename); - - return aud_file_write_tuple (m_filename, m_plugin, t); + return aud_file_write_tuple (m_filename, m_plugin, m_tuple); } bool InfoModel::setData (const QModelIndex & index, const QVariant & value, int role) @@ -122,7 +138,7 @@ bool InfoModel::setData (const QModelIndex & index, const QVariant & value, int auto t = Tuple::field_get_type (field_id); if (t == Tuple::String) { - m_tuple.set_str (field_id, value.toString ().toLocal8Bit ()); + m_tuple.set_str (field_id, value.toString ().toUtf8 ()); emit dataChanged (index, index, {role}); return true; } @@ -144,25 +160,19 @@ QVariant InfoModel::data (const QModelIndex & index, int role) const { if (index.column () == 0) return translate_str (tuple_field_map [index.row ()].name); - else if (index.column () == 1 && m_tuple) + else if (index.column () == 1) { if (field_id == Tuple::Invalid) return QVariant (); - auto t = Tuple::field_get_type (field_id); - - if (t == Tuple::String) + switch (m_tuple.get_value_type (field_id)) { - const char * res = m_tuple.get_str (field_id); - if (res) - return QString (res); - } - else if (t == Tuple::Int) - { - int res = m_tuple.get_int (field_id); - if (res == -1) - return QVariant (); - return res; + case Tuple::String: + return QString (m_tuple.get_str (field_id)); + case Tuple::Int: + return m_tuple.get_int (field_id); + default: + return QVariant (); } } } @@ -197,12 +207,4 @@ Qt::ItemFlags InfoModel::flags (const QModelIndex & index) const return Qt::ItemNeverHasChildren; } -void InfoModel::setTupleData (const Tuple & tuple, String filename, PluginHandle * plugin) -{ - m_tuple = tuple.ref (); - m_filename = filename; - m_plugin = plugin; - m_dirty = false; -} - } // namespace audqt diff --git a/src/libaudqt/info-widget.h b/src/libaudqt/info-widget.h index edc43d1..86e3925 100644 --- a/src/libaudqt/info-widget.h +++ b/src/libaudqt/info-widget.h @@ -18,37 +18,17 @@ * the use of this software. */ -#include <QAbstractTableModel> #include <QTreeView> - -#include <libaudcore/tuple.h> +#include <libaudqt/export.h> class PluginHandle; +class Tuple; namespace audqt { -class InfoModel : public QAbstractTableModel -{ -public: - InfoModel (QObject * parent = nullptr); - - int rowCount (const QModelIndex & parent = QModelIndex ()) const; - int columnCount (const QModelIndex & parent = QModelIndex ()) const; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; - bool setData (const QModelIndex & index, const QVariant & value, int role = Qt::EditRole); - Qt::ItemFlags flags (const QModelIndex & index) const; - - void setTupleData (const Tuple & tuple, String filename, PluginHandle * plugin); - bool updateFile () const; - -private: - Tuple m_tuple; - String m_filename; - PluginHandle * m_plugin = nullptr; - bool m_dirty = false; -}; +class InfoModel; -class InfoWidget : public QTreeView +class LIBAUDQT_PUBLIC InfoWidget : public QTreeView { public: InfoWidget (QWidget * parent = nullptr); @@ -59,7 +39,7 @@ public: bool updateFile (); private: - InfoModel m_model; + InfoModel * m_model; }; } // namespace audqt diff --git a/src/libaudqt/infowin.cc b/src/libaudqt/infowin.cc index 321e51d..a72e3c3 100644 --- a/src/libaudqt/infowin.cc +++ b/src/libaudqt/infowin.cc @@ -98,7 +98,8 @@ void InfoWindow::displayImage (const char * filename) static InfoWindow * s_infowin = nullptr; -EXPORT void infowin_show (int playlist, int entry) +static void show_infowin (int playlist, int entry, const char * filename, + const Tuple & tuple, PluginHandle * decoder, bool can_write) { if (! s_infowin) { @@ -110,29 +111,38 @@ EXPORT void infowin_show (int playlist, int entry) }); } + s_infowin->fillInfo (playlist, entry, filename, tuple, decoder, can_write); + s_infowin->resize (700, 300); + window_bring_to_front (s_infowin); +} + +EXPORT void infowin_show (int playlist, int entry) +{ String filename = aud_playlist_entry_get_filename (playlist, entry); if (! filename) return; - PluginHandle * decoder = aud_playlist_entry_get_decoder (playlist, entry); - if (! decoder) - return; - - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry); + String error; + PluginHandle * decoder = aud_playlist_entry_get_decoder (playlist, entry, + Playlist::Wait, & error); + Tuple tuple = decoder ? aud_playlist_entry_get_tuple (playlist, entry, + Playlist::Wait, & error) : Tuple (); - if (tuple) + if (decoder && tuple.valid () && ! aud_custom_infowin (filename, decoder)) { + /* cuesheet entries cannot be updated */ + bool can_write = aud_file_can_write_tuple (filename, decoder) && + ! tuple.is_set (Tuple::StartTime); + tuple.delete_fallbacks (); - s_infowin->fillInfo (playlist, entry, filename, tuple, decoder, - aud_file_can_write_tuple (filename, decoder)); + show_infowin (playlist, entry, filename, tuple, decoder, can_write); } else - aud_ui_show_error (str_printf (_("No info available for %s.\n"), - (const char *) filename)); + infowin_hide (); - s_infowin->resize (700, 300); - - window_bring_to_front (s_infowin); + if (error) + aud_ui_show_error (str_printf (_("Error opening %s:\n%s"), + (const char *) filename, (const char *) error)); } EXPORT void infowin_show_current () diff --git a/src/libaudqt/libaudqt.h b/src/libaudqt/libaudqt.h index 54c68ce..ea21708 100644 --- a/src/libaudqt/libaudqt.h +++ b/src/libaudqt/libaudqt.h @@ -27,6 +27,7 @@ class QBoxLayout; class QLayout; class QPixmap; +class QToolButton; class QWidget; enum class PluginType; @@ -111,6 +112,9 @@ void infowin_hide (); void queue_manager_show (); void queue_manager_hide (); +/* volumebutton.cc */ +QToolButton * volume_button_new (QWidget * parent = nullptr); + } // namespace audqt #endif diff --git a/src/libaudqt/prefs-builder.cc b/src/libaudqt/prefs-builder.cc index 33cd0e9..dece721 100644 --- a/src/libaudqt/prefs-builder.cc +++ b/src/libaudqt/prefs-builder.cc @@ -39,8 +39,8 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c QBoxLayout * parent_layout = nullptr; QBoxLayout * parent_orig_layout = nullptr; - BooleanWidget * parent_widget = nullptr; - QButtonGroup * radio_btn_group = nullptr; + ParentWidget * parent_widget = nullptr; + QButtonGroup * radio_btn_group[2] = {nullptr, nullptr}; for (const PreferencesWidget & w : widgets) { @@ -89,8 +89,10 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c parent_widget = nullptr; } - if (radio_btn_group && w.type != PreferencesWidget::RadioButton) - radio_btn_group = nullptr; + if (w.type != PreferencesWidget::RadioButton) + radio_btn_group[w.child] = nullptr; + if (! w.child) + radio_btn_group[true] = nullptr; switch (w.type) { @@ -149,15 +151,24 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c break; case PreferencesWidget::Entry: + /* TODO: implement file chooser */ + case PreferencesWidget::FileEntry: layout->addWidget (new StringWidget (& w, domain)); break; case PreferencesWidget::RadioButton: - if (! radio_btn_group) - radio_btn_group = new QButtonGroup; + { + if (! radio_btn_group[w.child]) + radio_btn_group[w.child] = new QButtonGroup; + + auto radio_btn = new RadioButtonWidget (& w, domain, radio_btn_group[w.child]); + layout->addWidget (radio_btn); + + if (! w.child) + parent_widget = radio_btn; - layout->addWidget (new RadioButtonWidget (& w, domain, radio_btn_group)); break; + } case PreferencesWidget::FontButton: /* XXX: unimplemented */ diff --git a/src/libaudqt/prefs-widget.cc b/src/libaudqt/prefs-widget.cc index f4f2dfc..096809b 100644 --- a/src/libaudqt/prefs-widget.cc +++ b/src/libaudqt/prefs-widget.cc @@ -60,7 +60,7 @@ ButtonWidget::ButtonWidget (const PreferencesWidget * parent, const char * domai /* boolean widget (checkbox) */ BooleanWidget::BooleanWidget (const PreferencesWidget * parent, const char * domain) : QCheckBox (translate_str (parent->label, domain)), - HookableWidget (parent, domain) + ParentWidget (parent, domain) { update (); @@ -85,7 +85,7 @@ void BooleanWidget::update () RadioButtonWidget::RadioButtonWidget (const PreferencesWidget * parent, const char * domain, QButtonGroup * btn_group) : QRadioButton (translate_str (parent->label, domain)), - HookableWidget (parent, domain) + ParentWidget (parent, domain) { if (btn_group) btn_group->addButton (this, parent->data.radio_btn.value); @@ -97,13 +97,18 @@ RadioButtonWidget::RadioButtonWidget (const PreferencesWidget * parent, return; if (checked) m_parent->cfg.set_int (m_parent->data.radio_btn.value); + if (m_child_layout) + enable_layout (m_child_layout, checked); }); } void RadioButtonWidget::update () { - if (m_parent->cfg.get_int () == m_parent->data.radio_btn.value) + bool checked = (m_parent->cfg.get_int () == m_parent->data.radio_btn.value); + if (checked) setChecked (true); + if (m_child_layout) + enable_layout (m_child_layout, checked); } /* integer (spinbox) */ @@ -196,7 +201,7 @@ StringWidget::StringWidget (const PreferencesWidget * parent, const char * domai if (parent->label) layout->addWidget (new QLabel (translate_str (parent->label, domain))); - if (parent->data.entry.password) + if (parent->type == PreferencesWidget::Entry && parent->data.entry.password) m_lineedit->setEchoMode (QLineEdit::Password); layout->addWidget (m_lineedit, 1); diff --git a/src/libaudqt/prefs-widget.h b/src/libaudqt/prefs-widget.h index f4deb4f..773945a 100644 --- a/src/libaudqt/prefs-widget.h +++ b/src/libaudqt/prefs-widget.h @@ -68,6 +68,19 @@ private: SmartPtr<HookReceiver<HookableWidget>> hook; }; +/* shared class which allows disabling child widgets */ +class ParentWidget : public HookableWidget { +public: + void set_child_layout (QLayout * layout) + { m_child_layout = layout; } + +protected: + ParentWidget (const PreferencesWidget * parent, const char * domain) : + HookableWidget (parent, domain) {} + + QLayout * m_child_layout = nullptr; +}; + /* button widget */ class ButtonWidget : public QPushButton { public: @@ -75,16 +88,11 @@ public: }; /* boolean widget (checkbox) */ -class BooleanWidget : public QCheckBox, public HookableWidget { +class BooleanWidget : public QCheckBox, public ParentWidget { public: BooleanWidget (const PreferencesWidget * parent, const char * domain); - - void set_child_layout (QLayout * layout) - { m_child_layout = layout; } - private: void update (); - QLayout * m_child_layout = nullptr; }; /* integer widget (spinner) */ @@ -97,7 +105,7 @@ private: }; /* integer widget (radio button) */ -class RadioButtonWidget : public QRadioButton, HookableWidget { +class RadioButtonWidget : public QRadioButton, public ParentWidget { public: RadioButtonWidget (const PreferencesWidget * parent, const char * domain, QButtonGroup * btn_group); diff --git a/src/libaudqt/prefs-window.cc b/src/libaudqt/prefs-window.cc index e79b465..f9627e1 100644 --- a/src/libaudqt/prefs-window.cc +++ b/src/libaudqt/prefs-window.cc @@ -129,6 +129,7 @@ static const ComboItem chardet_detector_presets[] = { }; static const ComboItem bitdepth_elements[] = { + ComboItem (N_("Automatic"), -1), ComboItem ("16", 16), ComboItem ("24", 24), ComboItem ("32", 32), @@ -164,7 +165,7 @@ static void output_bit_depth_changed (); static const PreferencesWidget output_combo_widgets[] = { WidgetCombo (N_("Output plugin:"), - WidgetInt (output_combo_selected, output_combo_changed), + WidgetInt (output_combo_selected, output_combo_changed, "audqt update output combo"), {0, output_combo_fill}), WidgetCustomQt (output_create_config_button), WidgetCustomQt (output_create_about_button) @@ -194,8 +195,8 @@ static const PreferencesWidget audio_page_widgets[] = { WidgetBool (0, "soft_clipping")), WidgetCheck (N_("Use software volume control (not recommended)"), WidgetBool (0, "software_volume_control")), - WidgetLabel (N_("<b>Replay Gain</b>")), - WidgetCheck (N_("Enable Replay Gain"), + WidgetLabel (N_("<b>ReplayGain</b>")), + WidgetCheck (N_("Enable ReplayGain"), WidgetBool (0, "enable_replay_gain")), WidgetCheck (N_("Album mode"), WidgetBool (0, "replay_gain_album"), @@ -270,8 +271,10 @@ static const PreferencesWidget playlist_page_widgets[] = { WidgetLabel (N_("<b>Song Display</b>")), WidgetCheck (N_("Show song numbers"), WidgetBool (0, "show_numbers_in_pl", send_title_change)), - WidgetCheck (N_("Show leading zeroes (02:00 instead of 2:00)"), + WidgetCheck (N_("Show leading zeroes (02:00 vs. 2:00)"), WidgetBool (0, "leading_zero", send_title_change)), + WidgetCheck (N_("Show hours separately (1:30:00 vs. 90:00)"), + WidgetBool (0, "show_hours", send_title_change)), WidgetCustomQt (create_titlestring_table), WidgetLabel (N_("<b>Compatibility</b>")), WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"), @@ -367,7 +370,7 @@ static void * create_titlestring_table () } QObject::connect (le, &QLineEdit::textChanged, [=] (const QString & text) { - aud_set_str (nullptr, "generic_title_format", text.toLocal8Bit ().data ()); + aud_set_str (nullptr, "generic_title_format", text.toUtf8 ().data ()); }); QObject::connect (cbox, @@ -469,13 +472,20 @@ static void * iface_create_prefs_box () static void output_combo_changed () { - PluginHandle * plugin = aud_plugin_list (PluginType::Output)[output_combo_selected]; + auto & list = aud_plugin_list (PluginType::Output); + PluginHandle * plugin = list[output_combo_selected]; if (aud_plugin_enable (plugin, true)) { output_config_button->setEnabled (aud_plugin_has_configure (plugin)); output_about_button->setEnabled (aud_plugin_has_about (plugin)); } + else + { + /* set combo box back to current output */ + output_combo_selected = list.find (aud_plugin_get_current (PluginType::Output)); + hook_call ("audqt update output combo", nullptr); + } } static void * output_create_config_button () diff --git a/src/libaudqt/queue-manager.cc b/src/libaudqt/queue-manager.cc index 1f46f70..59dbc08 100644 --- a/src/libaudqt/queue-manager.cc +++ b/src/libaudqt/queue-manager.cc @@ -67,7 +67,7 @@ QVariant QueueManagerModel::data (const QModelIndex & index, int role) const return entry + 1; else { - Tuple tuple = aud_playlist_entry_get_tuple (list, entry, Playlist::Guess); + Tuple tuple = aud_playlist_entry_get_tuple (list, entry, Playlist::NoWait); return QString ((const char *) tuple.get_str (Tuple::FormattedTitle)); } } diff --git a/src/libaudqt/util.cc b/src/libaudqt/util.cc index 67224c4..9ca03b0 100644 --- a/src/libaudqt/util.cc +++ b/src/libaudqt/util.cc @@ -47,6 +47,10 @@ EXPORT void init () atexit ([] () { delete qapp; }); qapp->setAttribute (Qt::AA_UseHighDpiPixmaps); +#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0) + qapp->setAttribute (Qt::AA_ForceRasterWidgets); +#endif + qapp->setApplicationName (_("Audacious")); qapp->setWindowIcon (QIcon::fromTheme (app_name)); } @@ -131,6 +135,10 @@ EXPORT void simple_message (const char * title, const char * text, QMessageBox:: /* translate GTK+ accelerators and also handle dgettext() */ EXPORT QString translate_str (const char * str, const char * domain) { + /* handle null and empty strings */ + if (! str || ! str[0]) + return QString (str); + /* translate the GTK+ accelerator (_) into a Qt accelerator (&) */ return QString (dgettext (domain, str)).replace ('_', '&'); } diff --git a/src/libaudqt/volumebutton.cc b/src/libaudqt/volumebutton.cc index ec8f889..ca25310 100644 --- a/src/libaudqt/volumebutton.cc +++ b/src/libaudqt/volumebutton.cc @@ -17,7 +17,6 @@ * the use of this software. */ -#include "volumebutton.h" #include "libaudqt.h" #include <QFrame> @@ -32,7 +31,25 @@ namespace audqt { -EXPORT VolumeButton::VolumeButton (QWidget * parent) : +class VolumeButton : public QToolButton +{ +public: + VolumeButton (QWidget * parent = nullptr); + +private: + void updateIcon (int val); + void updateVolume (); + void showSlider (); + void setVolume (int val); + QToolButton * newSliderButton (int delta); + + void wheelEvent (QWheelEvent * e); + + QSlider * m_slider; + QFrame * m_container; +}; + +VolumeButton::VolumeButton (QWidget * parent) : QToolButton (parent) { setFocusPolicy (Qt::NoFocus); @@ -142,4 +159,9 @@ void VolumeButton::wheelEvent (QWheelEvent * e) m_slider->setValue (++ val); } +EXPORT QToolButton * volume_button_new (QWidget * parent) +{ + return new VolumeButton (parent); +} + } // namespace audqt diff --git a/src/libaudtag/Makefile b/src/libaudtag/Makefile index 6d6d106..961b768 100644 --- a/src/libaudtag/Makefile +++ b/src/libaudtag/Makefile @@ -1,6 +1,6 @@ SHARED_LIB = ${LIB_PREFIX}audtag${LIB_SUFFIX} -LIB_MAJOR = 2 -LIB_MINOR = 1 +LIB_MAJOR = 3 +LIB_MINOR = 0 SRCS = audtag.cc \ util.cc \ diff --git a/src/libaudtag/ape/ape.cc b/src/libaudtag/ape/ape.cc index 165fed4..5188aa8 100644 --- a/src/libaudtag/ape/ape.cc +++ b/src/libaudtag/ape/ape.cc @@ -240,12 +240,8 @@ static Index<ValuePair> ape_read_items (VFSFile & handle) return list; } -bool APETagModule::read_tag (VFSFile & handle, Tuple * ptuple, Index<char> * image) +bool APETagModule::read_tag (VFSFile & handle, Tuple & tuple, Index<char> * image) { - if (! ptuple) - return true; // nothing to do - - Tuple & tuple = * ptuple; Index<ValuePair> list = ape_read_items (handle); for (const ValuePair & pair : list) diff --git a/src/libaudtag/audtag.cc b/src/libaudtag/audtag.cc index bb50788..1eef8ca 100644 --- a/src/libaudtag/audtag.cc +++ b/src/libaudtag/audtag.cc @@ -26,7 +26,7 @@ namespace audtag { -EXPORT bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image) +EXPORT bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image) { TagModule * module = find_tag_module (file, TagType::None); @@ -39,19 +39,7 @@ EXPORT bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image) return module->read_tag (file, tuple, image); } -EXPORT bool tuple_read (Tuple & tuple, VFSFile & file) -{ - return read_tag (file, & tuple, nullptr); -} - -EXPORT Index<char> image_read (VFSFile & file) -{ - Index<char> image; - read_tag (file, nullptr, & image); - return image; -} - -EXPORT bool tuple_write (const Tuple & tuple, VFSFile & file, TagType new_type) +EXPORT bool write_tuple (VFSFile & file, const Tuple & tuple, TagType new_type) { TagModule * module = find_tag_module (file, new_type); diff --git a/src/libaudtag/audtag.h b/src/libaudtag/audtag.h index e2467c3..70ec3b7 100644 --- a/src/libaudtag/audtag.h +++ b/src/libaudtag/audtag.h @@ -32,15 +32,11 @@ enum class TagType ID3v2 }; -bool tuple_read (Tuple & tuple, VFSFile & file) __attribute__((deprecated)); -Index<char> image_read (VFSFile & file) __attribute__((deprecated)); +bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); /* new_type specifies the type of tag (see the TagType enum) that should be * written if the file does not have any existing tag. */ -bool tuple_write (const Tuple & tuple, VFSFile & file, TagType new_type); - -/* since Audacious 3.7: reads tuple and image in one pass */ -bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); +bool write_tuple (VFSFile & file, const Tuple & tuple, TagType new_type); } diff --git a/src/libaudtag/builtin.h b/src/libaudtag/builtin.h index 86a1f0c..1426def 100644 --- a/src/libaudtag/builtin.h +++ b/src/libaudtag/builtin.h @@ -17,42 +17,44 @@ * the use of this software. */ -#include "libaudtag/audtag.h" -#include "libaudtag/tag_module.h" -#include "libaudtag/util.h" +#include "tag_module.h" #ifndef __LIBAUDTAG_BUILTIN_H__ #define __LIBAUDTAG_BUILTIN_H__ namespace audtag { -struct ID3v1TagModule : TagModule { - ID3v1TagModule() : TagModule("ID3v1", TagType::None) { }; +struct ID3v1TagModule : TagModule +{ + constexpr ID3v1TagModule () : TagModule ("ID3v1", TagType::None) {} bool can_handle_file (VFSFile & file); - bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); + bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); }; -struct ID3v22TagModule : TagModule { - ID3v22TagModule() : TagModule("ID3v2.2", TagType::None) { }; +struct ID3v22TagModule : TagModule +{ + constexpr ID3v22TagModule () : TagModule ("ID3v2.2", TagType::None) {} bool can_handle_file (VFSFile & file); - bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); + bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); }; -struct ID3v24TagModule : TagModule { - ID3v24TagModule() : TagModule("ID3v2.3/v2.4", TagType::ID3v2) { }; +struct ID3v24TagModule : TagModule +{ + constexpr ID3v24TagModule () : TagModule ("ID3v2.3/v2.4", TagType::ID3v2) {} bool can_handle_file (VFSFile & file); - bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); + bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); bool write_tag (VFSFile & file, const Tuple & tuple); }; -struct APETagModule : TagModule { - APETagModule() : TagModule("APE", TagType::APE) { }; +struct APETagModule : TagModule +{ + constexpr APETagModule () : TagModule ("APE", TagType::APE) {} bool can_handle_file (VFSFile & file); - bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); + bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); bool write_tag (VFSFile & file, const Tuple & tuple); }; diff --git a/src/libaudtag/id3/id3-common.cc b/src/libaudtag/id3/id3-common.cc index 78e3d06..5d6f80a 100644 --- a/src/libaudtag/id3/id3-common.cc +++ b/src/libaudtag/id3/id3-common.cc @@ -34,12 +34,12 @@ #define ID3_ENCODING_UTF16_BE 2 #define ID3_ENCODING_UTF8 3 -static void * memchr16 (const void * mem, int16_t chr, int len) +static const void * find_nul_utf16 (const void * mem, int len) { while (len >= 2) { - if (* (int16_t *) mem == chr) - return (void *) mem; + if (! ((const char *) mem)[0] && ! ((const char *) mem)[1]) + return mem; mem = (char *) mem + 2; len -= 2; @@ -52,7 +52,7 @@ static void id3_strnlen (const char * data, int size, int encoding, int * bytes_without_nul, int * bytes_with_nul) { bool is16 = (encoding == ID3_ENCODING_UTF16 || encoding == ID3_ENCODING_UTF16_BE); - char * nul = is16 ? (char *) memchr16 (data, 0, size) : (char *) memchr (data, 0, size); + auto nul = (const char *) (is16 ? find_nul_utf16 (data, size) : memchr (data, 0, size)); if (nul) { @@ -181,8 +181,8 @@ static bool decode_rva_block (const char * * _data, int * _size, return false; * channel = (unsigned char) data[0]; - * adjustment = (char) data[1]; /* first byte is signed */ - * adjustment = (* adjustment << 8) | (unsigned char) data[2]; + /* first byte is signed, but C/C++ allows shifting only unsigned values */ + * adjustment = (int16_t) ((unsigned char) data[1] << 8) | (unsigned char) data[2]; * adjustment_unit = 512; peak_bits = (unsigned char) data[3]; diff --git a/src/libaudtag/id3/id3v1.cc b/src/libaudtag/id3/id3v1.cc index 96d7b5a..65ec9e5 100644 --- a/src/libaudtag/id3/id3v1.cc +++ b/src/libaudtag/id3/id3v1.cc @@ -24,6 +24,7 @@ #include <libaudcore/audstrings.h> #include <libaudtag/builtin.h> +#include <libaudtag/util.h> #pragma pack(push) #pragma pack(1) @@ -95,12 +96,8 @@ static bool combine_string (Tuple & tuple, Tuple::Field field, return true; } -bool ID3v1TagModule::read_tag (VFSFile & file, Tuple * ptuple, Index<char> * image) +bool ID3v1TagModule::read_tag (VFSFile & file, Tuple & tuple, Index<char> * image) { - if (! ptuple) - return true; // nothing to do - - Tuple & tuple = * ptuple; ID3v1Tag tag; ID3v1Ext ext; diff --git a/src/libaudtag/id3/id3v22.cc b/src/libaudtag/id3/id3v22.cc index 56235f9..b435faa 100644 --- a/src/libaudtag/id3/id3v22.cc +++ b/src/libaudtag/id3/id3v22.cc @@ -26,6 +26,7 @@ #include <libaudcore/audstrings.h> #include <libaudcore/runtime.h> #include <libaudtag/builtin.h> +#include <libaudtag/util.h> #include "id3-common.h" @@ -209,7 +210,7 @@ bool ID3v22TagModule::can_handle_file (VFSFile & handle) & data_size); } -bool ID3v22TagModule::read_tag (VFSFile & handle, Tuple * ptuple, Index<char> * image) +bool ID3v22TagModule::read_tag (VFSFile & handle, Tuple & tuple, Index<char> * image) { int version, header_size, data_size; bool syncsafe; @@ -223,9 +224,6 @@ bool ID3v22TagModule::read_tag (VFSFile & handle, Tuple * ptuple, Index<char> * AUDDBG ("Reading tags from %i bytes of ID3 data in %s\n", data_size, handle.filename ()); - Tuple trash; // dump data here if caller does not want the tuple - Tuple & tuple = ptuple ? * ptuple : trash; - for (pos = 0; pos < data_size; ) { int frame_size; diff --git a/src/libaudtag/id3/id3v24.cc b/src/libaudtag/id3/id3v24.cc index f48cff2..c38f227 100644 --- a/src/libaudtag/id3/id3v24.cc +++ b/src/libaudtag/id3/id3v24.cc @@ -30,6 +30,7 @@ #include <libaudcore/multihash.h> #include <libaudcore/runtime.h> #include <libaudtag/builtin.h> +#include <libaudtag/util.h> #include "id3-common.h" @@ -381,7 +382,7 @@ static void read_all_frames (const Index<char> & data, int version, FrameDict & } } -static bool write_frame (int fd, const GenericFrame & frame, int version, int * frame_size) +static bool write_frame (VFSFile & file, const GenericFrame & frame, int version, int * frame_size) { AUDDBG ("Writing frame %s, size %d\n", (const char *) frame.key, frame.len ()); @@ -396,10 +397,10 @@ static bool write_frame (int fd, const GenericFrame & frame, int version, int * header.size = TO_BE32 (size); header.flags = 0; - if (write (fd, & header, sizeof (ID3v2FrameHeader)) != sizeof (ID3v2FrameHeader)) + if (file.fwrite (& header, 1, sizeof (ID3v2FrameHeader)) != sizeof (ID3v2FrameHeader)) return false; - if (write (fd, & frame[0], frame.len ()) != frame.len ()) + if (file.fwrite (& frame[0], 1, frame.len ()) != frame.len ()) return false; * frame_size = sizeof (ID3v2FrameHeader) + frame.len (); @@ -407,7 +408,7 @@ static bool write_frame (int fd, const GenericFrame & frame, int version, int * } struct WriteState { - int fd; + VFSFile & file; int version; int written_size; }; @@ -419,21 +420,21 @@ static void write_frame_list (const String & key, FrameList & list, void * user) for (const GenericFrame & frame : list) { int size; - if (write_frame (state->fd, frame, state->version, & size)) + if (write_frame (state->file, frame, state->version, & size)) state->written_size += size; } } -static int write_all_frames (int fd, FrameDict & dict, int version) +static int write_all_frames (VFSFile & file, FrameDict & dict, int version) { - WriteState state = {fd, version, 0}; + WriteState state = {file, version, 0}; dict.iterate (write_frame_list, & state); AUDDBG ("Total frame bytes written = %d.\n", state.written_size); return state.written_size; } -static bool write_header (int fd, int version, int size) +static bool write_header (VFSFile & file, int version, int size) { ID3v2Header header; @@ -443,7 +444,7 @@ static bool write_header (int fd, int version, int size) header.flags = 0; header.size = TO_BE32 (syncsafe32 (size)); - return write (fd, & header, sizeof (ID3v2Header)) == sizeof (ID3v2Header); + return file.fwrite (& header, 1, sizeof (ID3v2Header)) == sizeof (ID3v2Header); } static int get_frame_id (const char * key) @@ -550,7 +551,7 @@ bool ID3v24TagModule::can_handle_file (VFSFile & handle) & data_size, & footer_size); } -bool ID3v24TagModule::read_tag (VFSFile & handle, Tuple * ptuple, Index<char> * image) +bool ID3v24TagModule::read_tag (VFSFile & handle, Tuple & tuple, Index<char> * image) { int version, header_size, data_size, footer_size; bool syncsafe; @@ -561,10 +562,6 @@ bool ID3v24TagModule::read_tag (VFSFile & handle, Tuple * ptuple, Index<char> * return false; Index<char> data = read_tag_data (handle, data_size, syncsafe); - - Tuple trash; // dump data here if caller does not want the tuple - Tuple & tuple = ptuple ? * ptuple : trash; - FrameList rva_frames; for (const char * pos = data.begin (); pos < data.end (); ) @@ -671,26 +668,26 @@ bool ID3v24TagModule::write_tag (VFSFile & f, const Tuple & tuple) int64_t mp3_offset = offset ? 0 : header_size + data_size + footer_size; int64_t mp3_size = offset ? offset : -1; - TempFile temp; - if (! temp.create ()) + auto temp = VFSFile::tmpfile (); + if (! temp) return false; /* write empty header (will be overwritten later) */ - if (! write_header (temp.fd (), version, 0)) + if (! write_header (temp, version, 0)) return false; /* write tag data */ - data_size = write_all_frames (temp.fd (), dict, version); + data_size = write_all_frames (temp, dict, version); /* copy non-tag data */ - if (! temp.copy_from (f, mp3_offset, mp3_size)) + if (f.fseek (mp3_offset, VFS_SEEK_SET) < 0 || ! temp.copy_from (f, mp3_size)) return false; /* go back to beginning and write real header */ - if (lseek (temp.fd (), 0, SEEK_SET) < 0 || ! write_header (temp.fd (), version, data_size)) + if (temp.fseek (0, VFS_SEEK_SET) < 0 || ! write_header (temp, version, data_size)) return false; - if (! temp.replace (f)) + if (! f.replace_with (temp)) return false; return true; diff --git a/src/libaudtag/tag_module.cc b/src/libaudtag/tag_module.cc index ae24a49..15110d2 100644 --- a/src/libaudtag/tag_module.cc +++ b/src/libaudtag/tag_module.cc @@ -77,7 +77,7 @@ bool TagModule::can_handle_file (VFSFile & file) return false; } -bool TagModule::read_tag (VFSFile & file, Tuple * tuple, Index<char> * image) +bool TagModule::read_tag (VFSFile & file, Tuple & tuple, Index<char> * image) { AUDDBG ("%s: read_tag() not implemented.\n", m_name); return false; diff --git a/src/libaudtag/tag_module.h b/src/libaudtag/tag_module.h index b9de501..a08effb 100644 --- a/src/libaudtag/tag_module.h +++ b/src/libaudtag/tag_module.h @@ -20,21 +20,23 @@ #ifndef TAG_MODULE_H #define TAG_MODULE_H -#include "libaudcore/tuple.h" -#include "libaudcore/vfs.h" +#include "audtag.h" namespace audtag { -struct TagModule { - const char *m_name; +struct TagModule +{ + const char * m_name; TagType m_type; /* set to None if the module cannot create new tags */ virtual bool can_handle_file (VFSFile & file); - virtual bool read_tag (VFSFile & file, Tuple * tuple, Index<char> * image); + virtual bool read_tag (VFSFile & file, Tuple & tuple, Index<char> * image); virtual bool write_tag (VFSFile & file, const Tuple & tuple); protected: - TagModule(const char *name, TagType type) : m_name(name), m_type(type) { }; + constexpr TagModule (const char * name, TagType type) : + m_name (name), + m_type (type) {} }; TagModule * find_tag_module (VFSFile & handle, TagType new_type); diff --git a/src/libaudtag/util.cc b/src/libaudtag/util.cc index cc06f74..7b66728 100644 --- a/src/libaudtag/util.cc +++ b/src/libaudtag/util.cc @@ -17,13 +17,6 @@ * the use of this software. */ -#include <assert.h> -#include <unistd.h> - -#include <glib/gstdio.h> - -#include <libaudcore/audstrings.h> - #include "util.h" const char *convert_numericgenre_to_text(int numericgenre) @@ -173,104 +166,10 @@ const char *convert_numericgenre_to_text(int numericgenre) uint32_t unsyncsafe32 (uint32_t x) { - return (x & 0x7f) | ((x & 0x7f00) >> 1) | ((x & 0x7f0000) >> 2) | ((x & - 0x7f000000) >> 3); + return (x & 0x7f) | ((x & 0x7f00) >> 1) | ((x & 0x7f0000) >> 2) | ((x & 0x7f000000) >> 3); } uint32_t syncsafe32 (uint32_t x) { - return (x & 0x7f) | ((x & 0x3f80) << 1) | ((x & 0x1fc000) << 2) | ((x & - 0xfe00000) << 3); -} - -bool TempFile::create () -{ - StringBuf tempname = filename_build ({g_get_tmp_dir (), "audacious-temp-XXXXXX"}); - - assert (m_fd < 0); - m_fd = g_mkstemp (tempname); - if (m_fd < 0) - return false; - - m_name = String (tempname); - - return true; -} - -bool TempFile::copy_from (VFSFile & file, int64_t offset, int64_t size) -{ - if (file.fseek (offset, VFS_SEEK_SET) < 0) - return false; - - char buf[16384]; - - while (size < 0 || size > 0) - { - int64_t readsize; - - if (size > 0) - { - readsize = aud::min (size, (int64_t) sizeof buf); - if (file.fread (buf, 1, readsize) != readsize) - return false; - - size -= readsize; - } - else - { - /* negative size means copy to EOF */ - readsize = file.fread (buf, 1, sizeof buf); - if (! readsize) - break; - } - - int64_t written = 0; - while (written < readsize) - { - int64_t writesize = write (m_fd, buf + written, readsize - written); - if (writesize <= 0) - return false; - - written += writesize; - } - } - - return true; -} - -bool TempFile::replace (VFSFile & file) -{ - if (lseek (m_fd, 0, SEEK_SET) < 0) - return false; - - if (file.fseek (0, VFS_SEEK_SET) < 0) - return false; - - if (file.ftruncate (0) < 0) - return false; - - char buf[16384]; - - while (1) - { - int64_t readsize = read (m_fd, buf, sizeof buf); - if (readsize < 0) - return false; - - if (readsize == 0) - break; - - if (file.fwrite (buf, 1, readsize) != readsize) - return false; - } - - return true; -} - -TempFile::~TempFile () -{ - if (m_fd >= 0) - close (m_fd); - if (m_name) - g_unlink (m_name); + return (x & 0x7f) | ((x & 0x3f80) << 1) | ((x & 0x1fc000) << 2) | ((x & 0xfe00000) << 3); } diff --git a/src/libaudtag/util.h b/src/libaudtag/util.h index c102c02..9d9f03e 100644 --- a/src/libaudtag/util.h +++ b/src/libaudtag/util.h @@ -22,8 +22,6 @@ #include <stdint.h> -#include <libaudcore/vfs.h> - enum { GENRE_BLUES = 0, GENRE_CLASSIC_ROCK, @@ -157,21 +155,4 @@ const char *convert_numericgenre_to_text(int numericgenre); uint32_t unsyncsafe32 (uint32_t x); uint32_t syncsafe32 (uint32_t x); -class TempFile -{ -public: - bool create (); - bool copy_from (VFSFile & file, int64_t offset, int64_t size); - bool replace (VFSFile & file); - - int fd () - { return m_fd; } - - ~TempFile (); - -private: - String m_name; - int m_fd = -1; -}; - #endif /* TAGUTIL_H */ |