diff options
author | Mateusz Łukasik <mati75@linuxmint.pl> | 2017-08-21 20:23:34 +0200 |
---|---|---|
committer | Mateusz Łukasik <mati75@linuxmint.pl> | 2017-08-21 20:23:34 +0200 |
commit | fe91e6f4733198be72be8dc036fb715b3ffa59b9 (patch) | |
tree | 36b115b683b859191fe87a75d852b75b8e1b7d84 /src | |
parent | e71d3c04c4dafb4c9c0cb4f8e46180e4f679ed0b (diff) |
New upstream version 3.9
Diffstat (limited to 'src')
91 files changed, 4649 insertions, 3908 deletions
diff --git a/src/audacious/dbus-server.cc b/src/audacious/dbus-server.cc index f208a4c..ce0f1e8 100644 --- a/src/audacious/dbus-server.cc +++ b/src/audacious/dbus-server.cc @@ -38,10 +38,18 @@ typedef GDBusMethodInvocation Invoc; #define FINISH2(name, ...) \ obj_audacious_complete_##name (obj, invoc, __VA_ARGS__) -static int current_playlist () +static bool prefer_playing = true; + +static Playlist current_playlist () { - int list = aud_playlist_get_playing (); - return (list >= 0) ? list : aud_playlist_get_active (); + Playlist list; + + if (prefer_playing) + list = Playlist::playing_playlist (); + if (list == Playlist ()) + list = Playlist::active_playlist (); + + return list; } #define CURRENT current_playlist () @@ -57,28 +65,28 @@ static Index<PlaylistAddItem> strv_to_index (const char * const * strv) static gboolean do_add (Obj * obj, Invoc * invoc, const char * file) { - aud_playlist_entry_insert (CURRENT, -1, file, Tuple (), false); + CURRENT.insert_entry (-1, file, Tuple (), false); FINISH (add); return true; } static gboolean do_add_list (Obj * obj, Invoc * invoc, const char * const * filenames) { - aud_playlist_entry_insert_batch (CURRENT, -1, strv_to_index (filenames), false); + CURRENT.insert_items (-1, strv_to_index (filenames), false); FINISH (add_list); return true; } static gboolean do_add_url (Obj * obj, Invoc * invoc, const char * url) { - aud_playlist_entry_insert (CURRENT, -1, url, Tuple (), false); + CURRENT.insert_entry (-1, url, Tuple (), false); FINISH (add_url); return true; } static gboolean do_advance (Obj * obj, Invoc * invoc) { - aud_drct_pl_next (); + CURRENT.next_song (aud_get_bool (nullptr, "repeat")); FINISH (advance); return true; } @@ -97,22 +105,21 @@ static gboolean do_balance (Obj * obj, Invoc * invoc) static gboolean do_clear (Obj * obj, Invoc * invoc) { - int playlist = CURRENT; - aud_playlist_entry_delete (playlist, 0, aud_playlist_entry_count (playlist)); + CURRENT.remove_all_entries (); FINISH (clear); return true; } static gboolean do_delete (Obj * obj, Invoc * invoc, unsigned pos) { - aud_playlist_entry_delete (CURRENT, pos, 1); + CURRENT.remove_entry (pos); FINISH (delete); return true; } static gboolean do_delete_active_playlist (Obj * obj, Invoc * invoc) { - aud_playlist_delete (CURRENT); + CURRENT.remove_playlist (); FINISH (delete_active_playlist); return true; } @@ -135,13 +142,13 @@ static gboolean do_equalizer_activate (Obj * obj, Invoc * invoc, gboolean active static gboolean do_get_active_playlist (Obj * obj, Invoc * invoc) { - FINISH2 (get_active_playlist, CURRENT); + FINISH2 (get_active_playlist, CURRENT.index ()); return true; } static gboolean do_get_active_playlist_name (Obj * obj, Invoc * invoc) { - String title = aud_playlist_get_title (CURRENT); + String title = CURRENT.get_title (); FINISH2 (get_active_playlist_name, title ? title : ""); return true; } @@ -180,7 +187,7 @@ static gboolean do_get_info (Obj * obj, Invoc * invoc) static gboolean do_get_playqueue_length (Obj * obj, Invoc * invoc) { - FINISH2 (get_playqueue_length, aud_playlist_queue_count (CURRENT)); + FINISH2 (get_playqueue_length, CURRENT.n_queued ()); return true; } @@ -207,14 +214,14 @@ static gboolean do_info (Obj * obj, Invoc * invoc) static gboolean do_jump (Obj * obj, Invoc * invoc, unsigned pos) { - aud_playlist_set_position (CURRENT, pos); + CURRENT.set_position (pos); FINISH (jump); return true; } static gboolean do_length (Obj * obj, Invoc * invoc) { - FINISH2 (length, aud_playlist_entry_count (CURRENT)); + FINISH2 (length, CURRENT.n_entries ()); return true; } @@ -226,9 +233,7 @@ static gboolean do_main_win_visible (Obj * obj, Invoc * invoc) static gboolean do_new_playlist (Obj * obj, Invoc * invoc) { - int playlist = CURRENT + 1; - aud_playlist_insert (playlist); - aud_playlist_set_active (playlist); + Playlist::insert_playlist (CURRENT.index () + 1).activate (); aud_drct_stop (); FINISH (new_playlist); return true; @@ -236,7 +241,7 @@ static gboolean do_new_playlist (Obj * obj, Invoc * invoc) static gboolean do_number_of_playlists (Obj * obj, Invoc * invoc) { - FINISH2 (number_of_playlists, aud_playlist_count ()); + FINISH2 (number_of_playlists, Playlist::n_playlists ()); return true; } @@ -276,7 +281,7 @@ static gboolean do_play (Obj * obj, Invoc * invoc) static gboolean do_play_active_playlist (Obj * obj, Invoc * invoc) { - aud_playlist_play (CURRENT); + CURRENT.start_playback (); FINISH (play_active_playlist); return true; } @@ -296,7 +301,7 @@ static gboolean do_playing (Obj * obj, Invoc * invoc) static gboolean do_playlist_add (Obj * obj, Invoc * invoc, const char * list) { - aud_playlist_entry_insert (CURRENT, -1, list, Tuple (), false); + CURRENT.insert_entry (-1, list, Tuple (), false); FINISH (playlist_add); return true; } @@ -310,40 +315,39 @@ static gboolean do_playlist_enqueue_to_temp (Obj * obj, Invoc * invoc, const cha static gboolean do_playlist_ins_url_string (Obj * obj, Invoc * invoc, const char * url, int pos) { - aud_playlist_entry_insert (CURRENT, pos, url, Tuple (), false); + CURRENT.insert_entry (pos, url, Tuple (), false); FINISH (playlist_ins_url_string); return true; } static gboolean do_playqueue_add (Obj * obj, Invoc * invoc, int pos) { - aud_playlist_queue_insert (CURRENT, -1, pos); + CURRENT.queue_insert (-1, pos); FINISH (playqueue_add); return true; } static gboolean do_playqueue_clear (Obj * obj, Invoc * invoc) { - int playlist = CURRENT; - aud_playlist_queue_delete (playlist, 0, aud_playlist_queue_count (playlist)); + CURRENT.queue_remove_all (); FINISH (playqueue_clear); return true; } static gboolean do_playqueue_is_queued (Obj * obj, Invoc * invoc, int pos) { - bool queued = (aud_playlist_queue_find_entry (CURRENT, pos) >= 0); + bool queued = (CURRENT.queue_find_entry (pos) >= 0); FINISH2 (playqueue_is_queued, queued); return true; } static gboolean do_playqueue_remove (Obj * obj, Invoc * invoc, int pos) { - int playlist = CURRENT; - int qpos = aud_playlist_queue_find_entry (playlist, pos); + auto playlist = CURRENT; + int qpos = playlist.queue_find_entry (pos); if (qpos >= 0) - aud_playlist_queue_delete (playlist, qpos, 1); + playlist.queue_remove (qpos); FINISH (playqueue_remove); return true; @@ -378,19 +382,19 @@ static gboolean do_plugin_is_enabled (Obj * obj, Invoc * invoc, const char * nam static gboolean do_position (Obj * obj, Invoc * invoc) { - FINISH2 (position, aud_playlist_get_position (CURRENT)); + FINISH2 (position, CURRENT.get_position ()); return true; } static gboolean do_queue_get_list_pos (Obj * obj, Invoc * invoc, unsigned qpos) { - FINISH2 (queue_get_list_pos, aud_playlist_queue_get_entry (CURRENT, qpos)); + FINISH2 (queue_get_list_pos, CURRENT.queue_get_entry (qpos)); return true; } static gboolean do_queue_get_queue_pos (Obj * obj, Invoc * invoc, unsigned pos) { - FINISH2 (queue_get_queue_pos, aud_playlist_queue_find_entry (CURRENT, pos)); + FINISH2 (queue_get_queue_pos, CURRENT.queue_find_entry (pos)); return true; } @@ -403,14 +407,20 @@ static gboolean do_quit (Obj * obj, Invoc * invoc) static gboolean do_record (Obj * obj, Invoc * invoc) { - aud_drct_enable_record (! aud_drct_get_record_enabled ()); + if (aud_drct_get_record_enabled ()) + aud_set_bool (nullptr, "record", ! aud_get_bool (nullptr, "record")); + FINISH (record); return true; } static gboolean do_recording (Obj * obj, Invoc * invoc) { - FINISH2 (recording, aud_drct_get_record_enabled ()); + bool recording = false; + if (aud_drct_get_record_enabled ()) + recording = aud_get_bool (nullptr, "record"); + + FINISH2 (recording, recording); return true; } @@ -422,7 +432,7 @@ static gboolean do_repeat (Obj * obj, Invoc * invoc) static gboolean do_reverse (Obj * obj, Invoc * invoc) { - aud_drct_pl_prev (); + CURRENT.prev_song (); FINISH (reverse); return true; } @@ -434,13 +444,28 @@ static gboolean do_seek (Obj * obj, Invoc * invoc, unsigned pos) return true; } -static gboolean do_set_active_playlist (Obj * obj, Invoc * invoc, int playlist) +static gboolean do_select_displayed_playlist (Obj * obj, Invoc * invoc) +{ + prefer_playing = false; + FINISH (select_displayed_playlist); + return true; +} + +static gboolean do_select_playing_playlist (Obj * obj, Invoc * invoc) { - aud_playlist_set_active (playlist); + prefer_playing = true; + FINISH (select_playing_playlist); + return true; +} + +static gboolean do_set_active_playlist (Obj * obj, Invoc * invoc, int index) +{ + auto playlist = Playlist::by_index (index); + + playlist.activate (); - // check that the requested playlist exists before switching playback - if (aud_playlist_get_active () == playlist && aud_drct_get_playing ()) - aud_playlist_play (playlist, aud_drct_get_paused ()); + if (prefer_playing && aud_drct_get_playing ()) + playlist.start_playback (aud_drct_get_paused ()); FINISH (set_active_playlist); return true; @@ -448,7 +473,7 @@ static gboolean do_set_active_playlist (Obj * obj, Invoc * invoc, int playlist) static gboolean do_set_active_playlist_name (Obj * obj, Invoc * invoc, const char * title) { - aud_playlist_set_title (CURRENT, title); + CURRENT.set_title (title); FINISH (set_active_playlist_name); return true; } @@ -564,21 +589,21 @@ static gboolean do_shuffle (Obj * obj, Invoc * invoc) static gboolean do_song_filename (Obj * obj, Invoc * invoc, unsigned pos) { - String filename = aud_playlist_entry_get_filename (CURRENT, pos); + String filename = CURRENT.entry_filename (pos); FINISH2 (song_filename, filename ? filename : ""); return true; } static gboolean do_song_frames (Obj * obj, Invoc * invoc, unsigned pos) { - Tuple tuple = aud_playlist_entry_get_tuple (CURRENT, pos); + Tuple tuple = CURRENT.entry_tuple (pos); FINISH2 (song_frames, aud::max (0, tuple.get_int (Tuple::Length))); return true; } static gboolean do_song_length (Obj * obj, Invoc * invoc, unsigned pos) { - Tuple tuple = aud_playlist_entry_get_tuple (CURRENT, pos); + Tuple tuple = CURRENT.entry_tuple (pos); int length = aud::max (0, tuple.get_int (Tuple::Length)); FINISH2 (song_length, length / 1000); return true; @@ -586,7 +611,7 @@ static gboolean do_song_length (Obj * obj, Invoc * invoc, unsigned pos) static gboolean do_song_title (Obj * obj, Invoc * invoc, unsigned pos) { - Tuple tuple = aud_playlist_entry_get_tuple (CURRENT, pos); + Tuple tuple = CURRENT.entry_tuple (pos); String title = tuple.get_str (Tuple::FormattedTitle); FINISH2 (song_title, title ? title : ""); return true; @@ -599,7 +624,7 @@ static gboolean do_song_tuple (Obj * obj, Invoc * invoc, unsigned pos, const cha GVariant * var; if (field >= 0) - tuple = aud_playlist_entry_get_tuple (CURRENT, pos); + tuple = CURRENT.entry_tuple (pos); switch (tuple.get_value_type (field)) { @@ -664,28 +689,28 @@ static gboolean do_time (Obj * obj, Invoc * invoc) static gboolean do_toggle_auto_advance (Obj * obj, Invoc * invoc) { - aud_set_bool (nullptr, "no_playlist_advance", ! aud_get_bool (nullptr, "no_playlist_advance")); + aud_toggle_bool (nullptr, "no_playlist_advance"); FINISH (toggle_auto_advance); return true; } static gboolean do_toggle_repeat (Obj * obj, Invoc * invoc) { - aud_set_bool (nullptr, "repeat", ! aud_get_bool (nullptr, "repeat")); + aud_toggle_bool (nullptr, "repeat"); FINISH (toggle_repeat); return true; } static gboolean do_toggle_shuffle (Obj * obj, Invoc * invoc) { - aud_set_bool (nullptr, "shuffle", ! aud_get_bool (nullptr, "shuffle")); + aud_toggle_bool (nullptr, "shuffle"); FINISH (toggle_shuffle); return true; } static gboolean do_toggle_stop_after (Obj * obj, Invoc * invoc) { - aud_set_bool (nullptr, "stop_after_current_song", ! aud_get_bool (nullptr, "stop_after_current_song")); + aud_toggle_bool (nullptr, "stop_after_current_song"); FINISH (toggle_stop_after); return true; } @@ -761,6 +786,8 @@ handlers[] = {"handle-repeat", (GCallback) do_repeat}, {"handle-reverse", (GCallback) do_reverse}, {"handle-seek", (GCallback) do_seek}, + {"handle-select-displayed-playlist", (GCallback) do_select_displayed_playlist}, + {"handle-select-playing-playlist", (GCallback) do_select_playing_playlist}, {"handle-set-active-playlist", (GCallback) do_set_active_playlist}, {"handle-set-active-playlist-name", (GCallback) do_set_active_playlist_name}, {"handle-set-eq", (GCallback) do_set_eq}, diff --git a/src/audacious/main.cc b/src/audacious/main.cc index f3c4a5f..d8fbbf0 100644 --- a/src/audacious/main.cc +++ b/src/audacious/main.cc @@ -21,8 +21,11 @@ #include <stdlib.h> #include <string.h> -#include <glib.h> /* for g_get_current_dir, g_path_is_absolute */ +#ifdef _WIN32 +#include <windows.h> +#endif +#define AUD_GLIB_INTEGRATION #include <libaudcore/audstrings.h> #include <libaudcore/drct.h> #include <libaudcore/hook.h> @@ -81,8 +84,7 @@ static const struct { static bool parse_options (int argc, char * * argv) { - char * cur = g_get_current_dir (); - bool success = true; + CharPtr cur (g_get_current_dir ()); #ifdef _WIN32 Index<String> args = get_argv_utf8 (); @@ -134,8 +136,7 @@ static bool parse_options (int argc, char * * argv) if (! found) { fprintf (stderr, _("Unknown option: %s\n"), arg); - success = false; - goto OUT; + return false; } } else /* short form */ @@ -157,8 +158,7 @@ static bool parse_options (int argc, char * * argv) if (! found) { fprintf (stderr, _("Unknown option: -%c\n"), arg[c]); - success = false; - goto OUT; + return false; } } } @@ -174,16 +174,14 @@ static bool parse_options (int argc, char * * argv) if (options.qt) aud_set_mainloop_type (MainloopType::Qt); -OUT: - g_free (cur); - return success; + return true; } static void print_help () { static const char pad[21] = " "; - fprintf (stderr, _("Usage: audacious [OPTION] ... [FILE] ...\n\n")); + fprintf (stderr, "%s", _("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) @@ -330,7 +328,7 @@ static void main_cleanup () static bool check_should_quit () { return options.quit_after_play && ! aud_drct_get_playing () && - ! aud_playlist_add_in_progress (-1); + ! Playlist::add_in_progress_any (); } static void maybe_quit () @@ -343,6 +341,9 @@ int main (int argc, char * * argv) { atexit (main_cleanup); +#ifdef _WIN32 + SetErrorMode (SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX); +#endif #ifdef HAVE_SIGWAIT signals_init_one (); #endif diff --git a/src/audtool/audtool.h b/src/audtool/audtool.h index c0bb88d..51bde99 100644 --- a/src/audtool/audtool.h +++ b/src/audtool/audtool.h @@ -59,6 +59,8 @@ void get_current_song_info (int argc, char * * argv); void get_volume (int, char * *); void set_volume (int, char * *); +void select_displayed (int, char * *); +void select_playing (int, char * *); void playlist_position (int, char * *); void playlist_advance (int, char * *); void playlist_auto_advance_status (int, char * *); diff --git a/src/audtool/handlers_playlist.c b/src/audtool/handlers_playlist.c index 21e8f64..e2362c9 100644 --- a/src/audtool/handlers_playlist.c +++ b/src/audtool/handlers_playlist.c @@ -24,6 +24,16 @@ #include "audtool.h" #include "wrappers.h" +void select_displayed (int argc, char * * argv) +{ + obj_audacious_call_select_displayed_playlist_sync (dbus_proxy, NULL, NULL); +} + +void select_playing (int argc, char * * argv) +{ + obj_audacious_call_select_playing_playlist_sync (dbus_proxy, NULL, NULL); +} + void playlist_reverse (int argc, char * * argv) { obj_audacious_call_reverse_sync (dbus_proxy, NULL, NULL); diff --git a/src/audtool/main.c b/src/audtool/main.c index c49d62e..62894e5 100644 --- a/src/audtool/main.c +++ b/src/audtool/main.c @@ -59,6 +59,8 @@ const struct commandhandler handlers[] = {"playback-recording", playback_recording, "exit code = 0 if recording", 0}, {"<sep>", NULL, "Playlist commands", 0}, + {"select-displayed", select_displayed, "apply commands to displayed playlist", 0}, + {"select-playing", select_playing, "apply commands to playing playlist", 0}, {"playlist-advance", playlist_advance, "skip to next song", 0}, {"playlist-reverse", playlist_reverse, "skip to previous song", 0}, {"playlist-addurl", playlist_add_url_string, "add URI at end of playlist", 1}, diff --git a/src/dbus/aud-dbus.xml b/src/dbus/aud-dbus.xml index d028441..0172d98 100644 --- a/src/dbus/aud-dbus.xml +++ b/src/dbus/aud-dbus.xml @@ -21,8 +21,11 @@ --> <node name="/"> - <!-- Audacious General Information --> <interface name="org.atheme.audacious"> + + <!-- General Commands --> + <!-- ++++++++++++++++ --> + <!-- Audacious version --> <method name="Version"> <arg type="s" direction="out" name="version"/> @@ -41,7 +44,13 @@ <!-- Quit Audacious --> <method name="Quit" /> - <!-- Open files (Eject) --> + <!-- Show "Add Files" dialog --> + <method name="ShowFilebrowser"> + <arg type="b" direction="in" name="show"/> + </method> + + <!-- Show "Open Files" dialog --> + <!-- (same as "eject" button in a Winamp skin) --> <method name="Eject" /> <!-- Main window visibility --> @@ -54,6 +63,21 @@ <arg type="b" direction="in" name="show"/> </method> + <!-- Show preferences window --> + <method name="ShowPrefsBox"> + <arg type="b" direction="in" name="show"/> + </method> + + <!-- Show about window --> + <method name="ShowAboutBox"> + <arg type="b" direction="in" name="show"/> + </method> + + <!-- Show jump to file window --> + <method name="ShowJtfBox"> + <arg type="b" direction="in" name="show"/> + </method> + <!-- Send startup notification --> <method name="StartupNotify"> <arg type="s" direction="in" name="id"/> @@ -66,6 +90,8 @@ </method> <!-- Playback Information/Manipulation --> + <!-- +++++++++++++++++++++++++++++++++ --> + <!-- Begin or resume playback --> <method name="Play" /> @@ -112,12 +138,17 @@ </method> <!-- What is the bitrate, frequency, and number of channels of the --> - <!-- current audio format? --> + <!-- current audio format? ("Info" and "GetInfo" are synonymous) --> <method name="Info"> <arg type="i" direction="out" name="rate"/> <arg type="i" direction="out" name="freq"/> <arg type="i" direction="out" name="nch"/> </method> + <method name="GetInfo"> + <arg type="i" direction="out" name="rate"/> + <arg type="i" direction="out" name="freq"/> + <arg type="i" direction="out" name="nch"/> + </method> <!-- What is the current output position? --> <method name="Time"> @@ -131,6 +162,9 @@ <arg type="u" direction="in" name="pos"/> </method> + <!-- Volume and Equalizer --> + <!-- ++++++++++++++++++++ --> + <!-- What is the playback volume? --> <method name="Volume"> <!-- Volume of the left channel --> @@ -153,7 +187,47 @@ <arg type="i" direction="out" name="balance"/> </method> + <!-- equalizer --> + <method name="GetEq"> + <arg type="d" direction="out" name="preamp"/> + <arg type="ad" direction="out" name="bands"/> + </method> + + <method name="GetEqPreamp"> + <arg type="d" direction="out" name="preamp"/> + </method> + + <method name="GetEqBand"> + <arg type="i" direction="in" name="band"/> + <arg type="d" direction="out" name="value"/> + </method> + + <method name="SetEq"> + <arg type="d" direction="in" name="preamp"/> + <arg type="ad" direction="in" name="bands"/> + </method> + + <method name="SetEqPreamp"> + <arg type="d" direction="in" name="preamp"/> + </method> + + <method name="SetEqBand"> + <arg type="i" direction="in" name="band"/> + <arg type="d" direction="in" name="value"/> + </method> + + <!-- Activate/Deactivate Equalizer --> + <method name="EqualizerActivate"> + <arg type="b" direction="in" name="active"/> + </method> + <!-- Playlist Information/Manipulation --> + <!-- +++++++++++++++++++++++++++++++++ --> + + <!-- Select playlist to control --> + <method name="SelectDisplayedPlaylist"/> + <method name="SelectPlayingPlaylist"/> + <!-- Playlist position --> <method name="Position"> <!-- Return position of current song in current playlist --> @@ -226,17 +300,24 @@ <arg type="u" direction="in" name="pos"/> </method> - <!-- Add some file to the current playlist --> + <!-- Add (append) a file to the current playlist --> + <!-- ("Add", "AddUrl", and "PlaylistAdd" are synonymous) --> <method name="Add"> - <!-- File to add --> + <!-- URI of file to add --> <arg type="s" direction="in" name="file"/> </method> - - <!-- Add some URL to the current playlist --> <method name="AddUrl"> - <!-- URL to add --> <arg type="s" direction="in" name="url"/> </method> + <method name="PlaylistAdd"> + <arg type="s" direction="in" name="list"/> + </method> + + <!-- Insert a file at the given position in the playlist --> + <method name="PlaylistInsUrlString"> + <arg type="s" direction="in" name="url"/> + <arg type="i" direction="in" name="pos"/> + </method> <!-- Add a list of files --> <method name="AddList"> @@ -250,6 +331,11 @@ <arg type="as" direction="in" name="filenames"/> </method> + <!-- Open a file in a temporary playlist --> + <method name="PlaylistEnqueueToTemp"> + <arg type="s" direction="in" name="url"/> + </method> + <!-- Open a list of files in a temporary playlist --> <method name="OpenListToTemp"> <!-- Array of filenames to open --> @@ -297,25 +383,8 @@ <!-- Toggle stop-after-song --> <method name="ToggleStopAfter" /> - <!-- Show preferences window --> - <method name="ShowPrefsBox"> - <arg type="b" direction="in" name="show"/> - </method> - - <!-- Show about window --> - <method name="ShowAboutBox"> - <arg type="b" direction="in" name="show"/> - </method> - - <!-- Show jump to file window --> - <method name="ShowJtfBox"> - <arg type="b" direction="in" name="show"/> - </method> - - <!-- Show filebrowser --> - <method name="ShowFilebrowser"> - <arg type="b" direction="in" name="show"/> - </method> + <!-- Playlist Queue --> + <!-- ++++++++++++++ --> <!-- Playqueue get playlist pos --> <method name="QueueGetListPos"> @@ -329,26 +398,10 @@ <arg type="u" direction="out" name="qpos"/> </method> - <!-- Get Info --> - <method name="GetInfo"> - <arg type="i" direction="out" name="rate"/> - <arg type="i" direction="out" name="freq"/> - <arg type="i" direction="out" name="nch"/> - </method> - <method name="GetPlayqueueLength"> <arg type="i" direction="out" name="length"/> </method> - <method name="PlaylistInsUrlString"> - <arg type="s" direction="in" name="url"/> - <arg type="i" direction="in" name="pos"/> - </method> - - <method name="PlaylistAdd"> - <arg type="s" direction="in" name="list"/> - </method> - <method name="PlayqueueAdd"> <arg type="i" direction="in" name="pos"/> </method> @@ -364,43 +417,8 @@ <arg type="b" direction="out" name="is_queued"/> </method> - <method name="PlaylistEnqueueToTemp"> - <arg type="s" direction="in" name="url"/> - </method> - - <!-- equalizer --> - <method name="GetEq"> - <arg type="d" direction="out" name="preamp"/> - <arg type="ad" direction="out" name="bands"/> - </method> - - <method name="GetEqPreamp"> - <arg type="d" direction="out" name="preamp"/> - </method> - - <method name="GetEqBand"> - <arg type="i" direction="in" name="band"/> - <arg type="d" direction="out" name="value"/> - </method> - - <method name="SetEq"> - <arg type="d" direction="in" name="preamp"/> - <arg type="ad" direction="in" name="bands"/> - </method> - - <method name="SetEqPreamp"> - <arg type="d" direction="in" name="preamp"/> - </method> - - <method name="SetEqBand"> - <arg type="i" direction="in" name="band"/> - <arg type="d" direction="in" name="value"/> - </method> - - <!-- Activate/Deactivate Equalizer --> - <method name="EqualizerActivate"> - <arg type="b" direction="in" name="active"/> - </method> + <!-- Add/Remove/Switch Playlists --> + <!-- +++++++++++++++++++++++++++ --> <method name="NumberOfPlaylists"> <arg type="i" direction="out" name="count" /> diff --git a/src/libaudcore/Makefile b/src/libaudcore/Makefile index dde7284..a90d5dc 100644 --- a/src/libaudcore/Makefile +++ b/src/libaudcore/Makefile @@ -1,6 +1,6 @@ SHARED_LIB = ${LIB_PREFIX}audcore${LIB_SUFFIX} -LIB_MAJOR = 4 -LIB_MINOR = 1 +LIB_MAJOR = 5 +LIB_MINOR = 0 SRCS = adder.cc \ art.cc \ @@ -30,6 +30,7 @@ SRCS = adder.cc \ playback.cc \ playlist.cc \ playlist-cache.cc \ + playlist-data.cc \ playlist-files.cc \ playlist-utils.cc \ plugin-init.cc \ diff --git a/src/libaudcore/adder.cc b/src/libaudcore/adder.cc index 2866036..f9d9bca 100644 --- a/src/libaudcore/adder.cc +++ b/src/libaudcore/adder.cc @@ -36,27 +36,30 @@ #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 +// callback for Index::sort due to taking a third argument; +// strcmp also triggers -Wnoexcept-type with GCC 7 static int filename_compare (const char * a, const char * b) - { return strcmp_nocase (a, b); } +#ifdef _WIN32 + { return strcmp_nocase (a, b); } #else -#define filename_compare strcmp + { return strcmp (a, b); } #endif struct AddTask : public ListNode { - int playlist_id, at; + Playlist playlist; + int at; bool play; Index<PlaylistAddItem> items; - PlaylistFilterFunc filter; + Playlist::FilterFunc filter; void * user; }; struct AddResult : public ListNode { - int playlist_id, at; + Playlist playlist; + int at; bool play; String title; Index<PlaylistAddItem> items; @@ -67,7 +70,7 @@ static void * add_worker (void * unused); static List<AddTask> add_tasks; static List<AddResult> add_results; -static int current_playlist_id = -1; +static Playlist current_playlist; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static bool add_thread_started = false; @@ -75,8 +78,10 @@ static bool add_thread_exited = false; static pthread_t add_thread; static QueuedFunc queued_add; static QueuedFunc status_timer; + static char status_path[512]; static int status_count; +static bool status_shown = false; static void status_cb (void * unused) { @@ -97,6 +102,8 @@ static void status_cb (void * unused) hook_call ("ui show progress 2", scratch); } + status_shown = true; + pthread_mutex_unlock (& mutex); } @@ -117,13 +124,18 @@ static void status_done_locked () { status_timer.stop (); - if (aud_get_headless_mode ()) - printf ("\n"); - else - hook_call ("ui hide progress", nullptr); + if (status_shown) + { + if (aud_get_headless_mode ()) + printf ("\n"); + else + hook_call ("ui hide progress", nullptr); + + status_shown = false; + } } -static void add_file (PlaylistAddItem && item, PlaylistFilterFunc filter, +static void add_file (PlaylistAddItem && item, Playlist::FilterFunc filter, void * user, AddResult * result, bool validate) { AUDINFO ("Adding file: %s\n", (const char *) item.filename); @@ -168,8 +180,15 @@ static void add_file (PlaylistAddItem && item, PlaylistFilterFunc filter, result->items.append (std::move (item)); } -static void add_playlist (const char * filename, PlaylistFilterFunc filter, - void * user, AddResult * result, bool is_single) +/* To prevent infinite recursion, we currently allow adding a folder from within + * a playlist, but not a playlist from within a folder, nor a second playlist + * from within a playlist (this last rule is enforced by setting + * <allow_playlist> to false from within add_playlist()). */ +static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter, + void * user, AddResult * result, bool save_title, bool allow_playlist); + +static void add_playlist (const char * filename, Playlist::FilterFunc filter, + void * user, AddResult * result, bool save_title) { AUDINFO ("Adding playlist: %s\n", filename); status_update (filename, result->items.len ()); @@ -180,19 +199,14 @@ static void add_playlist (const char * filename, PlaylistFilterFunc filter, if (! playlist_load (filename, title, items)) return; - if (is_single) + if (save_title) result->title = title; for (auto & item : items) - { - if (! filter || filter (item.filename, user)) - add_file (std::move (item), filter, user, result, false); - else - result->filtered = true; - } + add_generic (std::move (item), filter, user, result, false, false); } -static void add_cuesheets (Index<String> & files, PlaylistFilterFunc filter, +static void add_cuesheets (Index<String> & files, Playlist::FilterFunc filter, void * user, AddResult * result) { Index<String> cuesheets; @@ -250,8 +264,8 @@ static void add_cuesheets (Index<String> & files, PlaylistFilterFunc filter, } } -static void add_folder (const char * filename, PlaylistFilterFunc filter, - void * user, AddResult * result, bool is_single) +static void add_folder (const char * filename, Playlist::FilterFunc filter, + void * user, AddResult * result, bool save_title) { AUDINFO ("Adding folder: %s\n", filename); status_update (filename, result->items.len ()); @@ -265,7 +279,7 @@ static void add_folder (const char * filename, PlaylistFilterFunc filter, if (! files.len ()) return; - if (is_single) + if (save_title) { const char * slash = strrchr (filename, '/'); if (slash) @@ -302,8 +316,8 @@ static void add_folder (const char * filename, PlaylistFilterFunc filter, } } -static void add_generic (PlaylistAddItem && item, PlaylistFilterFunc filter, - void * user, AddResult * result, bool is_single) +static void add_generic (PlaylistAddItem && item, Playlist::FilterFunc filter, + void * user, AddResult * result, bool save_title, bool allow_playlist) { if (filter && ! filter (item.filename, user)) { @@ -326,11 +340,11 @@ static void add_generic (PlaylistAddItem && item, PlaylistFilterFunc filter, (const char *) item.filename, (const char *) error)); else if (mode & VFS_IS_DIR) { - add_folder (item.filename, filter, user, result, is_single); + add_folder (item.filename, filter, user, result, save_title); result->saw_folder = true; } - else if (aud_filename_is_playlist (item.filename)) - add_playlist (item.filename, filter, user, result, is_single); + else if (allow_playlist && Playlist::filename_is_playlist (item.filename)) + add_playlist (item.filename, filter, user, result, save_title); else add_file (std::move (item), filter, user, result, false); } @@ -374,7 +388,8 @@ static void add_finish (void * unused) { add_results.remove (result); - int playlist, count; + PlaylistEx playlist; + int count; if (! result->items.len ()) { @@ -383,42 +398,40 @@ static void add_finish (void * unused) goto FREE; } - playlist = aud_playlist_by_unique_id (result->playlist_id); - if (playlist < 0) /* playlist deleted */ + playlist = result->playlist; + if (! playlist.exists ()) /* 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)); + playlist.remove_all_entries (); else - aud_playlist_queue_delete (playlist, 0, aud_playlist_queue_count (playlist)); + playlist.queue_remove_all (); } - count = aud_playlist_entry_count (playlist); + count = playlist.n_entries (); if (result->at < 0 || result->at > count) result->at = count; if (result->title && ! count) { - String old_title = aud_playlist_get_title (playlist); - - if (! strcmp (old_title, N_("New Playlist"))) - aud_playlist_set_title (playlist, result->title); + if (! strcmp (playlist.get_title (), _("New Playlist"))) + playlist.set_title (result->title); } /* temporarily disable scanning this playlist; the intent is to avoid * scanning until the currently playing entry is known, at which time it * can be scanned more efficiently (album art read in the same pass). */ playlist_enable_scan (false); - playlist_entry_insert_batch_raw (playlist, result->at, std::move (result->items)); + playlist.insert_flat_items (result->at, std::move (result->items)); if (result->play) { if (! aud_get_bool (0, "shuffle")) - aud_playlist_set_position (playlist, result->at); + playlist.set_position (result->at); - aud_playlist_play (playlist); + playlist.start_playback (); } playlist_enable_scan (true); @@ -447,26 +460,26 @@ static void * add_worker (void * unused) { add_tasks.remove (task); - current_playlist_id = task->playlist_id; + current_playlist = task->playlist; pthread_mutex_unlock (& mutex); playlist_cache_load (task->items); AddResult * result = new AddResult (); - result->playlist_id = task->playlist_id; + result->playlist = task->playlist; result->at = task->at; result->play = task->play; - bool is_single = (task->items.len () == 1); + bool save_title = (task->items.len () == 1); for (auto & item : task->items) - add_generic (std::move (item), task->filter, task->user, result, is_single); + add_generic (std::move (item), task->filter, task->user, result, save_title, true); delete task; pthread_mutex_lock (& mutex); - current_playlist_id = -1; + current_playlist = Playlist (); if (! add_results.head ()) queued_add.queue (add_finish, nullptr); @@ -495,32 +508,30 @@ void adder_cleanup () pthread_mutex_unlock (& mutex); } -EXPORT void aud_playlist_entry_insert (int playlist, int at, - const char * filename, Tuple && tuple, bool play) +EXPORT void Playlist::insert_entry (int at, + const char * filename, Tuple && tuple, bool play) const { Index<PlaylistAddItem> items; items.append (String (filename), std::move (tuple)); - aud_playlist_entry_insert_batch (playlist, at, std::move (items), play); + insert_items (at, std::move (items), play); } -EXPORT void aud_playlist_entry_insert_batch (int playlist, int at, - Index<PlaylistAddItem> && items, bool play) +EXPORT void Playlist::insert_items (int at, + Index<PlaylistAddItem> && items, bool play) const { - aud_playlist_entry_insert_filtered (playlist, at, std::move (items), nullptr, nullptr, play); + insert_filtered (at, std::move (items), nullptr, nullptr, play); } -EXPORT void aud_playlist_entry_insert_filtered (int playlist, int at, - Index<PlaylistAddItem> && items, PlaylistFilterFunc filter, void * user, - bool play) +EXPORT void Playlist::insert_filtered (int at, + Index<PlaylistAddItem> && items, Playlist::FilterFunc filter, void * user, + bool play) const { - int playlist_id = aud_playlist_get_unique_id (playlist); - pthread_mutex_lock (& mutex); AddTask * task = new AddTask (); - task->playlist_id = playlist_id; + task->playlist = * this; task->at = at; task->play = play; task->items = std::move (items); @@ -533,32 +544,22 @@ EXPORT void aud_playlist_entry_insert_filtered (int playlist, int at, pthread_mutex_unlock (& mutex); } -EXPORT bool aud_playlist_add_in_progress (int playlist) +EXPORT bool Playlist::add_in_progress () const { pthread_mutex_lock (& mutex); - if (playlist >= 0) + for (AddTask * task = add_tasks.head (); task; task = add_tasks.next (task)) { - int playlist_id = aud_playlist_get_unique_id (playlist); - - for (AddTask * task = add_tasks.head (); task; task = add_tasks.next (task)) - { - if (task->playlist_id == playlist_id) - goto YES; - } - - if (current_playlist_id == playlist_id) + if (task->playlist == * this) goto YES; - - for (AddResult * result = add_results.head (); result; result = add_results.next (result)) - { - if (result->playlist_id == playlist_id) - goto YES; - } } - else + + if (current_playlist == * this) + goto YES; + + for (AddResult * result = add_results.head (); result; result = add_results.next (result)) { - if (add_tasks.head () || current_playlist_id >= 0 || add_results.head ()) + if (result->playlist == * this) goto YES; } @@ -569,3 +570,15 @@ YES: pthread_mutex_unlock (& mutex); return true; } + +EXPORT bool Playlist::add_in_progress_any () +{ + pthread_mutex_lock (& mutex); + + bool in_progress = (add_tasks.head () || + current_playlist != Playlist () || + add_results.head ()); + + pthread_mutex_unlock (& mutex); + return in_progress; +} diff --git a/src/libaudcore/art.cc b/src/libaudcore/art.cc index 6fdbda7..e406eec 100644 --- a/src/libaudcore/art.cc +++ b/src/libaudcore/art.cc @@ -40,7 +40,8 @@ #define FLAG_DONE 1 #define FLAG_SENT 2 -struct ArtItem { +struct AudArtItem { + String filename; int refcount; int flag; @@ -54,25 +55,23 @@ struct ArtItem { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; -static SimpleHash<String, ArtItem> art_items; -static String current_ref; +static SimpleHash<String, AudArtItem> art_items; +static AudArtItem * current_item; static QueuedFunc queued_requests; -static void get_queued_cb (const String & key, ArtItem & item, void * list) +static Index<AudArtItem *> get_queued () { - if (item.flag == FLAG_DONE) - { - ((Index<String> *) list)->append (key); - item.flag = FLAG_SENT; - } -} - -static Index<String> get_queued () -{ - Index<String> queued; + Index<AudArtItem *> queued; pthread_mutex_lock (& mutex); - art_items.iterate (get_queued_cb, & queued); + art_items.iterate ([&] (const String &, AudArtItem & item) + { + if (item.flag == FLAG_DONE) + { + queued.append (& item); + item.flag = FLAG_SENT; + } + }); queued_requests.stop (); @@ -82,16 +81,15 @@ static Index<String> get_queued () static void send_requests (void *) { - Index<String> queued = get_queued (); - - for (const String & file : queued) + auto queued = get_queued (); + for (AudArtItem * item : queued) { - hook_call ("art ready", (void *) (const char *) file); - aud_art_unref (file); /* release temporary reference */ + hook_call ("art ready", (void *) (const char *) item->filename); + aud_art_unref (item); /* release temporary reference */ } } -static void finish_item (ArtItem * item, Index<char> && data, String && art_file) +static void finish_item_locked (AudArtItem * item, Index<char> && data, String && art_file) { /* already finished? */ if (item->flag) @@ -108,24 +106,24 @@ static void request_callback (ScanRequest * request) { pthread_mutex_lock (& mutex); - ArtItem * item = art_items.lookup (request->filename); + AudArtItem * item = art_items.lookup (request->filename); if (item) - finish_item (item, std::move (request->image_data), std::move (request->image_file)); + finish_item_locked (item, std::move (request->image_data), std::move (request->image_file)); pthread_mutex_unlock (& mutex); } -static ArtItem * art_item_get (const String & file, bool * queued) +static AudArtItem * art_item_get_locked (const String & filename, bool * queued) { if (queued) * queued = false; // blacklist stdin - if (! strncmp (file, "stdin://", 8)) + if (! strncmp (filename, "stdin://", 8)) return nullptr; - ArtItem * item = art_items.lookup (file); + AudArtItem * item = art_items.lookup (filename); if (item && item->flag) { @@ -135,10 +133,11 @@ static ArtItem * art_item_get (const String & file, bool * queued) if (! item) { - item = art_items.add (file, ArtItem ()); + item = art_items.add (filename, AudArtItem ()); + item->filename = filename; item->refcount = 1; /* temporary reference */ - scanner_request (new ScanRequest (file, SCAN_IMAGE, request_callback)); + scanner_request (new ScanRequest (filename, SCAN_IMAGE, request_callback)); } if (queued) @@ -147,7 +146,7 @@ static ArtItem * art_item_get (const String & file, bool * queued) return nullptr; } -static void art_item_unref (const String & file, ArtItem * item) +static void art_item_unref_locked (AudArtItem * item) { if (! -- item->refcount) { @@ -159,19 +158,16 @@ static void art_item_unref (const String & file, ArtItem * item) g_unlink (local); } - art_items.remove (file); + art_items.remove (item->filename); } } static void clear_current_locked () { - if (current_ref) + if (current_item) { - ArtItem * item = art_items.lookup (current_ref); - assert (item); - - art_item_unref (current_ref, item); - current_ref = String (); + art_item_unref_locked (current_item); + current_item = nullptr; } } @@ -181,18 +177,19 @@ void art_cache_current (const String & filename, Index<char> && data, String && clear_current_locked (); - ArtItem * item = art_items.lookup (filename); + AudArtItem * item = art_items.lookup (filename); if (! item) { - item = art_items.add (filename, ArtItem ()); + item = art_items.add (filename, AudArtItem ()); + item->filename = filename; item->refcount = 1; /* temporary reference */ } - finish_item (item, std::move (data), std::move (art_file)); + finish_item_locked (item, std::move (data), std::move (art_file)); item->refcount ++; - current_ref = filename; + current_item = item; pthread_mutex_unlock (& mutex); } @@ -206,87 +203,78 @@ void art_clear_current () void art_cleanup () { - Index<String> queued = get_queued (); - for (const String & file : queued) - aud_art_unref (file); /* release temporary reference */ + auto queued = get_queued (); + for (AudArtItem * item : queued) + aud_art_unref (item); /* release temporary reference */ /* playback should already be stopped */ - assert (! current_ref); + assert (! current_item); if (art_items.n_items ()) AUDWARN ("Album art reference count not zero at exit!\n"); } -EXPORT const Index<char> * aud_art_request_data (const char * file, bool * queued) +EXPORT AudArtPtr aud_art_request (const char * file, int format, bool * queued) { - const Index<char> * data = nullptr; pthread_mutex_lock (& mutex); String key (file); - ArtItem * item = art_item_get (key, queued); + AudArtItem * item = art_item_get_locked (key, queued); + bool good = true; if (! item) goto UNLOCK; - /* load data from external image file */ - if (! item->data.len () && item->art_file) + if (format & AUD_ART_DATA) { - VFSFile file (item->art_file, "r"); - if (file) - item->data = file.read_all (); - } - - if (item->data.len ()) - data = & item->data; - else - art_item_unref (key, item); - -UNLOCK: - pthread_mutex_unlock (& mutex); - return data; -} - -EXPORT const char * aud_art_request_file (const char * file, bool * queued) -{ - const char * art_file = nullptr; - pthread_mutex_lock (& mutex); - - String key (file); - ArtItem * item = art_item_get (key, queued); + /* load data from external image file */ + if (! item->data.len () && item->art_file) + { + VFSFile file (item->art_file, "r"); + if (file) + item->data = file.read_all (); + } - if (! item) - goto UNLOCK; + if (! item->data.len ()) + good = false; + } - /* save data to temporary file */ - if (item->data.len () && ! item->art_file) + if (format & AUD_ART_FILE) { - String local = write_temp_file (item->data.begin (), item->data.len ()); - if (local) + /* save data to temporary file */ + if (item->data.len () && ! item->art_file) { - item->art_file = String (filename_to_uri (local)); - item->is_temp = true; + String local = write_temp_file (item->data.begin (), item->data.len ()); + if (local) + { + item->art_file = String (filename_to_uri (local)); + item->is_temp = true; + } } + + if (! item->art_file) + good = false; } - if (item->art_file) - art_file = item->art_file; - else - art_item_unref (key, item); + if (! good) + { + art_item_unref_locked (item); + item = nullptr; + } UNLOCK: pthread_mutex_unlock (& mutex); - return art_file; + return AudArtPtr (item); } -EXPORT void aud_art_unref (const char * file) +EXPORT const Index<char> * aud_art_data (const AudArtItem * item) + { return & item->data; } +EXPORT const char * aud_art_file (const AudArtItem * item) + { return item->art_file; } + +EXPORT void aud_art_unref (AudArtItem * item) { pthread_mutex_lock (& mutex); - - String key (file); - ArtItem * item = art_items.lookup (key); - assert (item); - - art_item_unref (key, item); - + art_item_unref_locked (item); pthread_mutex_unlock (& mutex); } diff --git a/src/libaudcore/audio.cc b/src/libaudcore/audio.cc index 6b20f5d..b553864 100644 --- a/src/libaudcore/audio.cc +++ b/src/libaudcore/audio.cc @@ -27,17 +27,39 @@ #define SW_VOLUME_RANGE 40 /* decibels */ -#define INTERLACE_LOOP(TYPE) \ -for (int c = 0; c < channels; c ++) \ -{ \ - const TYPE * get = (const TYPE *) in[c]; \ - const TYPE * end = get + frames; \ - TYPE * set = (TYPE *) out + c; \ - while (get < end) \ - { \ - * set = * get ++; \ - set += channels; \ - } \ +struct packed24_t { uint8_t b[3]; }; +static_assert (sizeof (packed24_t) == 3, "invalid packed 24-bit type"); + +template<class Word> +void interlace_loop (const void * const * in, int channels, void * out, int frames) +{ + for (int c = 0; c < channels; c ++) + { + auto get = (const Word *) in[c]; + auto end = get + frames; + auto set = (Word *) out + c; + while (get < end) + { + * set = * get ++; + set += channels; + } + } +} + +template<class Word> +void deinterlace_loop (const void * in, int channels, void * const * out, int frames) +{ + for (int c = 0; c < channels; c ++) + { + auto get = (const Word *) in + c; + auto set = (Word *) out[c]; + auto end = set + frames; + while (set < end) + { + * set ++ = * get; + get += channels; + } + } } EXPORT void audio_interlace (const void * const * in, int format, int channels, @@ -46,45 +68,30 @@ EXPORT void audio_interlace (const void * const * in, int format, int channels, switch (format) { case FMT_FLOAT: - INTERLACE_LOOP (float); + interlace_loop<float> (in, channels, out, frames); break; - case FMT_S8: - case FMT_U8: - INTERLACE_LOOP (int8_t); + case FMT_S8: case FMT_U8: + interlace_loop<int8_t> (in, channels, out, frames); break; - case FMT_S16_LE: - case FMT_S16_BE: - case FMT_U16_LE: - case FMT_U16_BE: - INTERLACE_LOOP (int16_t); + case FMT_S16_LE: case FMT_S16_BE: + case FMT_U16_LE: case FMT_U16_BE: + interlace_loop<int16_t> (in, channels, out, frames); break; - case FMT_S24_LE: - case FMT_S24_BE: - case FMT_U24_LE: - case FMT_U24_BE: - case FMT_S32_LE: - case FMT_S32_BE: - case FMT_U32_LE: - case FMT_U32_BE: - INTERLACE_LOOP (int32_t); + case FMT_S24_LE: case FMT_S24_BE: + case FMT_U24_LE: case FMT_U24_BE: + case FMT_S32_LE: case FMT_S32_BE: + case FMT_U32_LE: case FMT_U32_BE: + interlace_loop<int32_t> (in, channels, out, frames); break; - } -} -#define DEINTERLACE_LOOP(TYPE) \ -for (int c = 0; c < channels; c ++) \ -{ \ - const TYPE * get = (const TYPE *) in + c; \ - TYPE * set = (TYPE *) out[c]; \ - TYPE * end = set + frames; \ - while (set < end) \ - { \ - * set ++ = * get; \ - get += channels; \ - } \ + case FMT_S24_3LE: case FMT_S24_3BE: + case FMT_U24_3LE: case FMT_U24_3BE: + interlace_loop<packed24_t> (in, channels, out, frames); + break; + } } EXPORT void audio_deinterlace (const void * in, int format, int channels, @@ -93,131 +100,187 @@ EXPORT void audio_deinterlace (const void * in, int format, int channels, switch (format) { case FMT_FLOAT: - DEINTERLACE_LOOP (float); + deinterlace_loop<float> (in, channels, out, frames); + break; + + case FMT_S8: case FMT_U8: + deinterlace_loop<int8_t> (in, channels, out, frames); break; - case FMT_S8: - case FMT_U8: - DEINTERLACE_LOOP (int8_t); + case FMT_S16_LE: case FMT_S16_BE: + case FMT_U16_LE: case FMT_U16_BE: + deinterlace_loop<int16_t> (in, channels, out, frames); break; - case FMT_S16_LE: - case FMT_S16_BE: - case FMT_U16_LE: - case FMT_U16_BE: - DEINTERLACE_LOOP (int16_t); + case FMT_S24_LE: case FMT_S24_BE: + case FMT_U24_LE: case FMT_U24_BE: + case FMT_S32_LE: case FMT_S32_BE: + case FMT_U32_LE: case FMT_U32_BE: + deinterlace_loop<int32_t> (in, channels, out, frames); break; - case FMT_S24_LE: - case FMT_S24_BE: - case FMT_U24_LE: - case FMT_U24_BE: - case FMT_S32_LE: - case FMT_S32_BE: - case FMT_U32_LE: - case FMT_U32_BE: - DEINTERLACE_LOOP (int32_t); + case FMT_S24_3LE: case FMT_S24_3BE: + case FMT_U24_3LE: case FMT_U24_3BE: + deinterlace_loop<packed24_t> (in, channels, out, frames); break; } } -#define FROM_INT_LOOP(NAME, TYPE, SWAP, OFFSET, RANGE) \ -static void NAME (const TYPE * in, float * out, int samples) \ -{ \ - const TYPE * end = in + samples; \ - while (in < end) { \ - TYPE value = SWAP (* in ++) + (RANGE - OFFSET); \ - if (RANGE == 0x800000) value &= 0xffffff; /* ignore high byte */ \ - * out ++ = (TYPE) (value - RANGE) * (1.0f / RANGE); \ - } \ +static constexpr bool is_le (int format) +{ + return format == FMT_S16_LE || format == FMT_U16_LE || + format == FMT_S24_LE || format == FMT_U24_LE || + format == FMT_S32_LE || format == FMT_U32_LE || + format == FMT_S24_3LE || format == FMT_U24_3LE; +} + +static constexpr bool is_signed (int format) +{ + return (format == FMT_S8 || + format == FMT_S16_LE || format == FMT_S16_BE || + format == FMT_S24_LE || format == FMT_S24_BE || + format == FMT_S32_LE || format == FMT_S32_BE || + format == FMT_S24_3LE || format == FMT_S24_3BE); } -#define TO_INT_LOOP(NAME, TYPE, SWAP, OFFSET, RANGE, RANGE_P) \ -static void NAME (const float * in, TYPE * out, int samples) \ -{ \ - const float * end = in + samples; \ - while (in < end) \ - { \ - float f = (* in ++) * RANGE; \ - TYPE value = OFFSET + TYPE (lrintf (aud::clamp (f, -(float) RANGE, (float) RANGE_P))); \ - if (RANGE == 0x800000) value &= 0xffffff; /* zero high byte */ \ - * out ++ = SWAP (value); \ - } \ +static constexpr unsigned neg_range (int format) +{ + return (format >= FMT_S32_LE && format < FMT_S24_3LE) ? 0x80000000 : + (format >= FMT_S24_LE) ? 0x800000 : + (format >= FMT_S16_LE) ? 0x8000 : 0x80; } -FROM_INT_LOOP (from_s8, int8_t, , 0, 0x80) -FROM_INT_LOOP (from_u8, int8_t, , 0x80, 0x80) - -TO_INT_LOOP (to_s8, int8_t, , 0, 0x80, 0x7f) -TO_INT_LOOP (to_u8, int8_t, , 0x80, 0x80, 0x7f) - -FROM_INT_LOOP (from_s16le, int16_t, FROM_LE16, 0, 0x8000) -FROM_INT_LOOP (from_u16le, int16_t, FROM_LE16, 0x8000, 0x8000) -FROM_INT_LOOP (from_s24le, int32_t, FROM_LE32, 0, 0x800000) -FROM_INT_LOOP (from_u24le, int32_t, FROM_LE32, 0x800000, 0x800000) -FROM_INT_LOOP (from_s32le, int32_t, FROM_LE32, 0, 0x80000000) -FROM_INT_LOOP (from_u32le, int32_t, FROM_LE32, 0x80000000, 0x80000000) - -TO_INT_LOOP (to_s16le, int16_t, TO_LE16, 0, 0x8000, 0x7fff) -TO_INT_LOOP (to_u16le, int16_t, TO_LE16, 0x8000, 0x8000, 0x7fff) -TO_INT_LOOP (to_s24le, int32_t, TO_LE32, 0, 0x800000, 0x7fffff) -TO_INT_LOOP (to_u24le, int32_t, TO_LE32, 0x800000, 0x800000, 0x7fffff) -TO_INT_LOOP (to_s32le, int32_t, TO_LE32, 0, 0x80000000, 0x7fffff80) -TO_INT_LOOP (to_u32le, int32_t, TO_LE32, 0x80000000, 0x80000000, 0x7fffff80) // 0x7fffff80 = largest representable floating-point value before 2^31 +static constexpr unsigned pos_range (int format) +{ + return (format >= FMT_S32_LE && format < FMT_S24_3LE) ? 0x7fffff80 : + (format >= FMT_S24_LE) ? 0x7fffff : + (format >= FMT_S16_LE) ? 0x7fff : 0x7f; +} -FROM_INT_LOOP (from_s16be, int16_t, FROM_BE16, 0, 0x8000) -FROM_INT_LOOP (from_u16be, int16_t, FROM_BE16, 0x8000, 0x8000) -FROM_INT_LOOP (from_s24be, int32_t, FROM_BE32, 0, 0x800000) -FROM_INT_LOOP (from_u24be, int32_t, FROM_BE32, 0x800000, 0x800000) -FROM_INT_LOOP (from_s32be, int32_t, FROM_BE32, 0, 0x80000000) -FROM_INT_LOOP (from_u32be, int32_t, FROM_BE32, 0x80000000, 0x80000000) +template<class T> T do_swap (T value) { return value; } +template<> int16_t do_swap (int16_t value) { return bswap16 (value); } +template<> int32_t do_swap (int32_t value) { return bswap32 (value); } -TO_INT_LOOP (to_s16be, int16_t, TO_BE16, 0, 0x8000, 0x7fff) -TO_INT_LOOP (to_u16be, int16_t, TO_BE16, 0x8000, 0x8000, 0x7fff) -TO_INT_LOOP (to_s24be, int32_t, TO_BE32, 0, 0x800000, 0x7fffff) -TO_INT_LOOP (to_u24be, int32_t, TO_BE32, 0x800000, 0x800000, 0x7fffff) -TO_INT_LOOP (to_s32be, int32_t, TO_BE32, 0, 0x80000000, 0x7fffff80) -TO_INT_LOOP (to_u32be, int32_t, TO_BE32, 0x80000000, 0x80000000, 0x7fffff80) +template<int format, class Word, class Int> +struct Convert +{ +#ifdef WORDS_BIGENDIAN + static constexpr bool native_le = false; +#else + static constexpr bool native_le = true; +#endif -typedef void (* FromFunc) (const void * in, float * out, int samples); -typedef void (* ToFunc) (const float * in, void * out, int samples); + static Int to_int (Word value) + { + if (is_le (format) ^ native_le) + value = do_swap (value); + if (is_signed (format)) + value += neg_range (format); + if (format >= FMT_S24_LE && format <= FMT_U24_BE) + value &= 0xffffff; /* ignore high byte */ + + return value - neg_range (format); + } -static const struct + static Word to_word (Int value) + { + if (! is_signed (format)) + value += neg_range (format); + if (format >= FMT_S24_LE && format <= FMT_U24_BE) + value &= 0xffffff; /* zero high byte */ + if (is_le (format) ^ native_le) + value = do_swap (value); + + return value; + } +}; + +template<int format> +struct Convert<format, packed24_t, int32_t> +{ + static int32_t to_int (packed24_t value) + { + uint8_t hi, mid, lo; + + if (is_le (format)) + hi = value.b[2], mid = value.b[1], lo = value.b[0]; + else + hi = value.b[0], mid = value.b[1], lo = value.b[2]; + + if (! is_signed (format)) + hi -= 0x80; + + return (int8_t (hi) << 16) | (mid << 8) | lo; + } + + static packed24_t to_word (int32_t value) + { + auto hi = uint8_t (value >> 16), + mid = uint8_t (value >> 8), + lo = uint8_t (value); + + if (! is_signed (format)) + hi += 0x80; + + if (is_le (format)) + return {{lo, mid, hi}}; + else + return {{hi, mid, lo}}; + } +}; + +template<int format, class Word, class Int = Word> +void from_int_loop (const void * in_, float * out, int samples) { - int format; - FromFunc from; - ToFunc to; + auto in = (const Word *) in_; + auto end = in + samples; + while (in < end) + { + Int value = Convert<format, Word, Int>::to_int (* in ++); + * out ++ = value * (1.0f / neg_range (format)); + } } -convert_table [] = + +template<int format, class Word, class Int = Word> +void to_int_loop (const float * in, void * out_, int samples) { - {FMT_S8, (FromFunc) from_s8, (ToFunc) to_s8}, - {FMT_U8, (FromFunc) from_u8, (ToFunc) to_u8}, - - {FMT_S16_LE, (FromFunc) from_s16le, (ToFunc) to_s16le}, - {FMT_U16_LE, (FromFunc) from_u16le, (ToFunc) to_u16le}, - {FMT_S24_LE, (FromFunc) from_s24le, (ToFunc) to_s24le}, - {FMT_U24_LE, (FromFunc) from_u24le, (ToFunc) to_u24le}, - {FMT_S32_LE, (FromFunc) from_s32le, (ToFunc) to_s32le}, - {FMT_U32_LE, (FromFunc) from_u32le, (ToFunc) to_u32le}, - - {FMT_S16_BE, (FromFunc) from_s16be, (ToFunc) to_s16be}, - {FMT_U16_BE, (FromFunc) from_u16be, (ToFunc) to_u16be}, - {FMT_S24_BE, (FromFunc) from_s24be, (ToFunc) to_s24be}, - {FMT_U24_BE, (FromFunc) from_u24be, (ToFunc) to_u24be}, - {FMT_S32_BE, (FromFunc) from_s32be, (ToFunc) to_s32be}, - {FMT_U32_BE, (FromFunc) from_u32be, (ToFunc) to_u32be}, -}; + auto end = in + samples; + auto out = (Word *) out_; + while (in < end) + { + float f = (* in ++) * neg_range (format); + f = aud::clamp (f, -(float) neg_range (format), (float) pos_range (format)); + * out ++ = Convert<format, Word, Int>::to_word (lrintf (f)); + } +} EXPORT void audio_from_int (const void * in, int format, float * out, int samples) { - for (auto & conv : convert_table) + switch (format) { - if (conv.format == format) - { - conv.from (in, out, samples); - break; - } + case FMT_S8: from_int_loop<FMT_S8, int8_t> (in, out, samples); break; + case FMT_U8: from_int_loop<FMT_U8, int8_t> (in, out, samples); break; + + case FMT_S16_LE: from_int_loop<FMT_S16_LE, int16_t> (in, out, samples); break; + case FMT_S16_BE: from_int_loop<FMT_S16_BE, int16_t> (in, out, samples); break; + case FMT_U16_LE: from_int_loop<FMT_U16_LE, int16_t> (in, out, samples); break; + case FMT_U16_BE: from_int_loop<FMT_U16_BE, int16_t> (in, out, samples); break; + + case FMT_S24_LE: from_int_loop<FMT_S24_LE, int32_t> (in, out, samples); break; + case FMT_S24_BE: from_int_loop<FMT_S24_BE, int32_t> (in, out, samples); break; + case FMT_U24_LE: from_int_loop<FMT_U24_LE, int32_t> (in, out, samples); break; + case FMT_U24_BE: from_int_loop<FMT_U24_BE, int32_t> (in, out, samples); break; + + case FMT_S32_LE: from_int_loop<FMT_S32_LE, int32_t> (in, out, samples); break; + case FMT_S32_BE: from_int_loop<FMT_S32_BE, int32_t> (in, out, samples); break; + case FMT_U32_LE: from_int_loop<FMT_U32_LE, int32_t> (in, out, samples); break; + case FMT_U32_BE: from_int_loop<FMT_U32_BE, int32_t> (in, out, samples); break; + + case FMT_S24_3LE: from_int_loop<FMT_S24_3LE, packed24_t, int32_t> (in, out, samples); break; + case FMT_S24_3BE: from_int_loop<FMT_S24_3BE, packed24_t, int32_t> (in, out, samples); break; + case FMT_U24_3LE: from_int_loop<FMT_U24_3LE, packed24_t, int32_t> (in, out, samples); break; + case FMT_U24_3BE: from_int_loop<FMT_U24_3BE, packed24_t, int32_t> (in, out, samples); break; } } @@ -226,13 +289,30 @@ EXPORT void audio_to_int (const float * in, void * out, int format, int samples) int save = fegetround (); fesetround (FE_TONEAREST); - for (auto & conv : convert_table) + switch (format) { - if (conv.format == format) - { - conv.to (in, out, samples); - break; - } + case FMT_S8: to_int_loop<FMT_S8, int8_t> (in, out, samples); break; + case FMT_U8: to_int_loop<FMT_U8, int8_t> (in, out, samples); break; + + case FMT_S16_LE: to_int_loop<FMT_S16_LE, int16_t> (in, out, samples); break; + case FMT_S16_BE: to_int_loop<FMT_S16_BE, int16_t> (in, out, samples); break; + case FMT_U16_LE: to_int_loop<FMT_U16_LE, int16_t> (in, out, samples); break; + case FMT_U16_BE: to_int_loop<FMT_U16_BE, int16_t> (in, out, samples); break; + + case FMT_S24_LE: to_int_loop<FMT_S24_LE, int32_t> (in, out, samples); break; + case FMT_S24_BE: to_int_loop<FMT_S24_BE, int32_t> (in, out, samples); break; + case FMT_U24_LE: to_int_loop<FMT_U24_LE, int32_t> (in, out, samples); break; + case FMT_U24_BE: to_int_loop<FMT_U24_BE, int32_t> (in, out, samples); break; + + case FMT_S32_LE: to_int_loop<FMT_S32_LE, int32_t> (in, out, samples); break; + case FMT_S32_BE: to_int_loop<FMT_S32_BE, int32_t> (in, out, samples); break; + case FMT_U32_LE: to_int_loop<FMT_U32_LE, int32_t> (in, out, samples); break; + case FMT_U32_BE: to_int_loop<FMT_U32_BE, int32_t> (in, out, samples); break; + + case FMT_S24_3LE: to_int_loop<FMT_S24_3LE, packed24_t, int32_t> (in, out, samples); break; + case FMT_S24_3BE: to_int_loop<FMT_S24_3BE, packed24_t, int32_t> (in, out, samples); break; + case FMT_U24_3LE: to_int_loop<FMT_U24_3LE, packed24_t, int32_t> (in, out, samples); break; + case FMT_U24_3BE: to_int_loop<FMT_U24_3BE, packed24_t, int32_t> (in, out, samples); break; } fesetround (save); diff --git a/src/libaudcore/audio.h.in b/src/libaudcore/audio.h.in index e49302f..c5b41be 100644 --- a/src/libaudcore/audio.h.in +++ b/src/libaudcore/audio.h.in @@ -22,13 +22,13 @@ #define AUD_MAX_CHANNELS 10 -/* 24-bit integer samples are padded to 32-bit; high byte is always 0 */ enum { FMT_FLOAT, FMT_S8, FMT_U8, FMT_S16_LE, FMT_S16_BE, FMT_U16_LE, FMT_U16_BE, - FMT_S24_LE, FMT_S24_BE, FMT_U24_LE, FMT_U24_BE, - FMT_S32_LE, FMT_S32_BE, FMT_U32_LE, FMT_U32_BE}; + FMT_S24_LE, FMT_S24_BE, FMT_U24_LE, FMT_U24_BE, /* padded to 4 bytes */ + FMT_S32_LE, FMT_S32_BE, FMT_U32_LE, FMT_U32_BE, + FMT_S24_3LE, FMT_S24_3BE, FMT_U24_3LE, FMT_U24_3BE }; /* packed in 3 bytes */ struct ReplayGainInfo { float track_gain; /* dB */ @@ -77,6 +77,8 @@ constexpr uint64_t bswap64 (uint64_t x) #define FMT_U24_NE FMT_U24_BE #define FMT_S32_NE FMT_S32_BE #define FMT_U32_NE FMT_U32_BE +#define FMT_S24_3NE FMT_S24_3BE +#define FMT_U24_3NE FMT_U24_3BE #ifdef WANT_AUD_BSWAP #define FROM_BE16(x) (x) @@ -101,6 +103,8 @@ constexpr uint64_t bswap64 (uint64_t x) #define FMT_U24_NE FMT_U24_LE #define FMT_S32_NE FMT_S32_LE #define FMT_U32_NE FMT_U32_LE +#define FMT_S24_3NE FMT_S24_3LE +#define FMT_U24_3NE FMT_U24_3LE #ifdef WANT_AUD_BSWAP #define FROM_BE16(x) (bswap16 (x)) @@ -119,7 +123,10 @@ constexpr uint64_t bswap64 (uint64_t x) #endif -#define FMT_SIZEOF(f) ((f) == FMT_FLOAT ? sizeof (float) : (f) <= FMT_U8 ? 1 : (f) <= FMT_U16_BE ? 2 : 4) +#define FMT_SIZEOF(f) (((f) >= FMT_S24_3LE) ? 3 : \ + ((f) >= FMT_S24_LE) ? 4 : \ + ((f) >= FMT_S16_LE) ? 2 : \ + ((f) >= FMT_S8) ? 1 : sizeof (float)) void audio_interlace (const void * const * in, int format, int channels, void * out, int frames); void audio_deinterlace (const void * in, int format, int channels, void * const * out, int frames); diff --git a/src/libaudcore/audstrings.cc b/src/libaudcore/audstrings.cc index 6a89121..6d971b9 100644 --- a/src/libaudcore/audstrings.cc +++ b/src/libaudcore/audstrings.cc @@ -55,7 +55,13 @@ static const char uri_legal_table[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" "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x1\x1\x1" // '-' '.' '/' +#ifdef _WIN32 + /* We assume ':' is used with a "reserved purpose" (i.e. drive letter). + * This assumption might need to be reconsidered for non-file URIs. */ + "\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x0\x0\x0\x0\x0" // 0-9 ':' +#else "\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x0\x0\x0\x0\x0\x0" // 0-9 +#endif "\x0\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1" // A-O "\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x0\x0\x0\x0\x1" // P-Z '_' "\x0\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1\x1" // a-o @@ -314,6 +320,37 @@ EXPORT StringBuf str_tolower_utf8 (const char * str) return buf; } +EXPORT StringBuf str_toupper (const char * str) +{ + StringBuf buf (strlen (str)); + char * set = buf; + + while (* str) + * set ++ = g_ascii_toupper (* str ++); + + return buf; +} + +EXPORT StringBuf str_toupper_utf8 (const char * str) +{ + StringBuf buf (6 * strlen (str)); + char * set = buf; + gunichar c; + + while ((c = g_utf8_get_char (str))) + { + if (c < 128) + * set ++ = g_ascii_toupper (c); + else + set += g_unichar_to_utf8 (g_unichar_toupper (c), set); + + str = g_utf8_next_char (str); + } + + buf.resize (set - buf); + return buf; +} + EXPORT void str_replace_char (char * string, char old_c, char new_c) { while ((string = strchr (string, old_c))) @@ -1057,15 +1094,20 @@ EXPORT StringBuf double_array_to_str (const double * array, int count) EXPORT StringBuf str_format_time (int64_t milliseconds) { + bool neg = milliseconds < 0; + + if (neg) + milliseconds *= -1; + int hours = milliseconds / 3600000; int minutes = milliseconds / 60000; int seconds = (milliseconds / 1000) % 60; if (hours && aud_get_bool (nullptr, "show_hours")) - return str_printf ("%d:%02d:%02d", hours, minutes % 60, seconds); + return str_printf ("%s%d:%02d:%02d", neg ? "- " : "", hours, minutes % 60, seconds); else { bool zero = aud_get_bool (nullptr, "leading_zero"); - return str_printf (zero ? "%02d:%02d" : "%d:%02d", minutes, seconds); + return str_printf (zero ? "%s%02d:%02d" : "%s%d:%02d", neg ? "- " : "", minutes, seconds); } } diff --git a/src/libaudcore/audstrings.h b/src/libaudcore/audstrings.h index 679fe5f..8c956f0 100644 --- a/src/libaudcore/audstrings.h +++ b/src/libaudcore/audstrings.h @@ -34,7 +34,11 @@ int strlen_bounded (const char * s, int len = -1); StringBuf str_copy (const char * s, int len = -1); StringBuf str_concat (const std::initializer_list<const char *> & strings); +#ifdef _WIN32 +StringBuf str_printf (const char * format, ...) __attribute__ ((__format__ (gnu_printf, 1, 2))); +#else StringBuf str_printf (const char * format, ...) __attribute__ ((__format__ (__printf__, 1, 2))); +#endif StringBuf str_vprintf (const char * format, va_list args); bool str_has_prefix_nocase (const char * str, const char * prefix); @@ -52,6 +56,8 @@ static inline char * strstr_nocase_utf8 (char * haystack, const char * needle) StringBuf str_tolower (const char * str); StringBuf str_tolower_utf8 (const char * str); +StringBuf str_toupper (const char * str); +StringBuf str_toupper_utf8 (const char * str); void str_replace_char (char * string, char old_c, char new_c); diff --git a/src/libaudcore/charset.cc b/src/libaudcore/charset.cc index 8843979..5a4dba6 100644 --- a/src/libaudcore/charset.cc +++ b/src/libaudcore/charset.cc @@ -190,7 +190,7 @@ EXPORT StringBuf str_to_utf8 (StringBuf && str) return std::move (str); } -static void chardet_update () +static void chardet_update (void * = nullptr, void * = nullptr) { String region = aud_get_str (nullptr, "chardet_detector"); String fallbacks = aud_get_str (nullptr, "chardet_fallback"); @@ -202,14 +202,14 @@ void chardet_init () { chardet_update (); - hook_associate ("set chardet_detector", (HookFunction) chardet_update, nullptr); - hook_associate ("set chardet_fallback", (HookFunction) chardet_update, nullptr); + hook_associate ("set chardet_detector", chardet_update, nullptr); + hook_associate ("set chardet_fallback", chardet_update, nullptr); } void chardet_cleanup () { - hook_dissociate ("set chardet_detector", (HookFunction) chardet_update); - hook_dissociate ("set chardet_fallback", (HookFunction) chardet_update); + hook_dissociate ("set chardet_detector", chardet_update); + hook_dissociate ("set chardet_fallback", chardet_update); set_charsets (nullptr, nullptr); } diff --git a/src/libaudcore/config.cc b/src/libaudcore/config.cc index 2718a6b..84bd0f4 100644 --- a/src/libaudcore/config.cc +++ b/src/libaudcore/config.cc @@ -72,7 +72,7 @@ static const char * const core_defaults[] = { "output_bit_depth", "-1", "output_buffer_size", "500", "record_stream", aud::numeric_string<(int) OutputStream::AfterReplayGain>::str, - "replay_gain_album", "FALSE", + "replay_gain_mode", aud::numeric_string<(int) ReplayGainMode::Track>::str, "replay_gain_preamp", "0", "soft_clipping", "FALSE", "software_volume_control", "FALSE", @@ -118,66 +118,53 @@ struct ConfigItem { String value; }; -struct ConfigNode { - MultiHash::Node node; - ConfigItem item; -}; +struct ConfigNode; -struct ConfigOp { +// combined Data and Operation class +struct ConfigOp +{ OpType type; const char * section; const char * key; String value; unsigned hash; bool result; -}; -struct SaveState { - Index<ConfigItem> list; + ConfigNode * add (const ConfigOp *); + bool found (ConfigNode * node); }; -static int item_compare (const ConfigItem & a, const ConfigItem & b) -{ - if (a.section == b.section) - return strcmp (a.key, b.key); - else - return strcmp (a.section, b.section); -} - -static bool config_node_match (const MultiHash::Node * node0, const void * data) +struct ConfigNode : public MultiHash::Node, public ConfigItem { - const ConfigNode * node = (const ConfigNode *) node0; - const ConfigOp * op = (const ConfigOp *) data; - - return ! strcmp (node->item.section, op->section) && ! strcmp (node->item.key, op->key); -} + bool match (const ConfigOp * op) const + { return ! strcmp (section, op->section) && ! strcmp (key, op->key); } +}; -static MultiHash defaults (config_node_match); -static MultiHash config (config_node_match); +typedef MultiHash_T<ConfigNode, ConfigOp> ConfigTable; -static volatile bool modified; +static ConfigTable s_defaults, s_config; +static volatile bool s_modified; -static MultiHash::Node * add_cb (const void * data, void * state) +ConfigNode * ConfigOp::add (const ConfigOp *) { - ConfigOp * op = (ConfigOp *) state; - - switch (op->type) + switch (type) { case OP_IS_DEFAULT: - op->result = ! op->value[0]; /* empty string is default */ + result = ! value[0]; /* empty string is default */ return nullptr; case OP_SET: - op->result = true; - modified = true; + result = true; + s_modified = true; + // fall-through case OP_SET_NO_FLAG: { ConfigNode * node = new ConfigNode; - node->item.section = String (op->section); - node->item.key = String (op->key); - node->item.value = op->value; - return (MultiHash::Node *) node; + node->section = String (section); + node->key = String (key); + node->value = value; + return node; } default: @@ -185,33 +172,32 @@ static MultiHash::Node * add_cb (const void * data, void * state) } } -static bool action_cb (MultiHash::Node * node0, void * state) +bool ConfigOp::found (ConfigNode * node) { - ConfigNode * node = (ConfigNode *) node0; - ConfigOp * op = (ConfigOp *) state; - - switch (op->type) + switch (type) { case OP_IS_DEFAULT: - op->result = ! strcmp (node->item.value, op->value); + result = ! strcmp (node->value, value); return false; case OP_GET: - op->value = node->item.value; + value = node->value; return false; case OP_SET: - op->result = !! strcmp (node->item.value, op->value); - if (op->result) - modified = true; + result = !! strcmp (node->value, value); + if (result) + s_modified = true; + // fall-through case OP_SET_NO_FLAG: - node->item.value = op->value; + node->value = value; return false; case OP_CLEAR: - op->result = true; - modified = true; + result = true; + s_modified = true; + // fall-through case OP_CLEAR_NO_FLAG: delete node; @@ -222,14 +208,14 @@ static bool action_cb (MultiHash::Node * node0, void * state) } } -static bool config_op_run (ConfigOp * op, MultiHash * table) +static bool config_op_run (ConfigOp & op, ConfigTable & table) { - if (! op->hash) - op->hash = str_calc_hash (op->section) + str_calc_hash (op->key); + if (! op.hash) + op.hash = str_calc_hash (op.section) + str_calc_hash (op.key); - op->result = false; - table->lookup (op, op->hash, add_cb, action_cb, op); - return op->result; + op.result = false; + table.lookup (& op, op.hash, op); + return op.result; } class ConfigParser : public IniParser @@ -246,7 +232,7 @@ private: return; ConfigOp op = {OP_SET_NO_FLAG, section, key, String (value)}; - config_op_run (& op, & config); + config_op_run (op, s_config); } }; @@ -263,29 +249,35 @@ void config_load () } aud_config_set_defaults (nullptr, core_defaults); -} -static bool add_to_save_list (MultiHash::Node * node0, void * state0) -{ - ConfigNode * node = (ConfigNode *) node0; - SaveState * state = (SaveState *) state0; - - state->list.append (node->item); - - modified = false; - - return false; + /* migrate from previous versions */ + if (aud_get_bool (0, "replay_gain_album")) + { + aud_set_str (0, "replay_gain_album", ""); + aud_set_int (0, "replay_gain_mode", (int) ReplayGainMode::Album); + } } void config_save () { - if (! modified) + if (! s_modified) return; - SaveState state = SaveState (); + Index<ConfigItem> list; - config.iterate (add_to_save_list, & state); - state.list.sort (item_compare); + s_config.iterate ([&] (ConfigNode * node) { + list.append (* node); + + s_modified = false; // must be inside MultiHash lock + return false; + }); + + list.sort ([] (const ConfigItem & a, const ConfigItem & b) { + if (a.section == b.section) + return strcmp (a.key, b.key); + else + return strcmp (a.section, b.section); + }); StringBuf path = filename_to_uri (aud_get_path (AudPath::UserDir)); path.insert (-1, "/config"); @@ -296,7 +288,7 @@ void config_save () if (! file) goto FAILED; - for (const ConfigItem & item : state.list) + for (const ConfigItem & item : list) { if (item.section != current_heading) { @@ -332,15 +324,14 @@ EXPORT void aud_config_set_defaults (const char * section, const char * const * break; ConfigOp op = {OP_SET_NO_FLAG, section, name, String (value)}; - config_op_run (& op, & defaults); + config_op_run (op, s_defaults); } } void config_cleanup () { - ConfigOp op = {OP_CLEAR_NO_FLAG}; - config.iterate (action_cb, & op); - defaults.iterate (action_cb, & op); + s_config.clear (); + s_defaults.clear (); } EXPORT void aud_set_str (const char * section, const char * name, const char * value) @@ -348,10 +339,10 @@ EXPORT void aud_set_str (const char * section, const char * name, const char * v assert (name && value); ConfigOp op = {OP_IS_DEFAULT, section ? section : DEFAULT_SECTION, name, String (value)}; - bool is_default = config_op_run (& op, & defaults); + bool is_default = config_op_run (op, s_defaults); op.type = is_default ? OP_CLEAR : OP_SET; - bool changed = config_op_run (& op, & config); + bool changed = config_op_run (op, s_config); if (changed && ! section) event_queue (str_concat ({"set ", name}), nullptr); @@ -362,10 +353,10 @@ EXPORT String aud_get_str (const char * section, const char * name) assert (name); ConfigOp op = {OP_GET, section ? section : DEFAULT_SECTION, name}; - config_op_run (& op, & config); + config_op_run (op, s_config); if (! op.value) - config_op_run (& op, & defaults); + config_op_run (op, s_defaults); return op.value ? op.value : String (""); } @@ -380,6 +371,11 @@ EXPORT bool aud_get_bool (const char * section, const char * name) return ! strcmp (aud_get_str (section, name), "TRUE"); } +EXPORT void aud_toggle_bool (const char * section, const char * name) +{ + aud_set_bool (section, name, ! aud_get_bool (section, name)); +} + EXPORT void aud_set_int (const char * section, const char * name, int value) { aud_set_str (section, name, int_to_str (value)); diff --git a/src/libaudcore/drct.cc b/src/libaudcore/drct.cc index d8a0589..2ad76eb 100644 --- a/src/libaudcore/drct.cc +++ b/src/libaudcore/drct.cc @@ -21,6 +21,7 @@ #include "hook.h" #include "i18n.h" +#include "interface.h" #include "internal.h" #include "playlist-internal.h" #include "plugins-internal.h" @@ -44,9 +45,9 @@ EXPORT void aud_drct_play () } else { - int playlist = aud_playlist_get_active (); - aud_playlist_set_position (playlist, aud_playlist_get_position (playlist)); - aud_playlist_play (playlist); + auto playlist = Playlist::active_playlist (); + playlist.set_position (playlist.get_position ()); + playlist.start_playback (); } } @@ -58,22 +59,15 @@ EXPORT void aud_drct_play_pause () aud_drct_play (); } -EXPORT void aud_drct_stop () -{ - aud_playlist_play (-1); -} - EXPORT int aud_drct_get_position () { - int playlist = aud_playlist_get_playing (); - return aud_playlist_get_position (playlist); + return Playlist::playing_playlist ().get_position (); } EXPORT String aud_drct_get_filename () { - int playlist = aud_playlist_get_playing (); - int position = aud_playlist_get_position (playlist); - return aud_playlist_entry_get_filename (playlist, position); + auto playlist = Playlist::playing_playlist (); + return playlist.entry_filename (playlist.get_position ()); } /* --- RECORDING CONTROL --- */ @@ -85,10 +79,25 @@ static PluginHandle * record_plugin; static bool record_plugin_watcher (PluginHandle *, void *) { + if (! aud_drct_get_record_enabled ()) + aud_set_bool (nullptr, "record", false); + hook_call ("enable record", nullptr); return true; } +static void validate_record_setting (void *, void *) +{ + if (aud_get_bool (nullptr, "record") && ! aud_drct_get_record_enabled ()) + { + /* User attempted to start recording without a recording plugin enabled. + * This is probably not the best response, but better than nothing. */ + aud_set_bool (nullptr, "record", false); + aud_ui_show_error (_("Stream recording must be configured in Audio " + "Settings before it can be used.")); + } +} + void record_init () { auto plugin = aud_plugin_lookup_basename ("filewriter"); @@ -97,10 +106,17 @@ void record_init () record_plugin = plugin; aud_plugin_add_watch (plugin, record_plugin_watcher, nullptr); } + + if (! aud_drct_get_record_enabled ()) + aud_set_bool (nullptr, "record", false); + + hook_associate ("set record", validate_record_setting, nullptr); } void record_cleanup () { + hook_dissociate ("set record", validate_record_setting); + if (record_plugin) { aud_plugin_remove_watch (record_plugin, record_plugin_watcher, nullptr); @@ -178,28 +194,28 @@ EXPORT void aud_drct_set_volume_balance (int balance) EXPORT void aud_drct_pl_next () { - int playlist = aud_playlist_get_playing (); - if (playlist < 0) - playlist = aud_playlist_get_active (); + PlaylistEx playlist = Playlist::playing_playlist (); + if (playlist == Playlist ()) + playlist = Playlist::active_playlist (); - playlist_next_song (playlist, aud_get_bool (nullptr, "repeat")); + playlist.next_song (aud_get_bool (nullptr, "repeat")); } EXPORT void aud_drct_pl_prev () { - int playlist = aud_playlist_get_playing (); - if (playlist < 0) - playlist = aud_playlist_get_active (); + PlaylistEx playlist = Playlist::playing_playlist (); + if (playlist == Playlist ()) + playlist = Playlist::active_playlist (); - playlist_prev_song (playlist); + playlist.prev_song (); } static void add_list (Index<PlaylistAddItem> && items, int at, bool to_temp, bool play) { if (to_temp) - aud_playlist_set_active (aud_playlist_get_temporary ()); + Playlist::temporary_playlist ().activate (); - aud_playlist_entry_insert_batch (aud_playlist_get_active (), at, std::move (items), play); + Playlist::active_playlist ().insert_items (at, std::move (items), play); } EXPORT void aud_drct_pl_add (const char * filename, int at) diff --git a/src/libaudcore/drct.h b/src/libaudcore/drct.h index a8ce2dc..a486660 100644 --- a/src/libaudcore/drct.h +++ b/src/libaudcore/drct.h @@ -69,6 +69,10 @@ void aud_drct_get_ab_repeat (int & a, int & b); /* --- RECORDING CONTROL --- */ +/* Note that the behavior of these functions has changed in Audacious 3.9; + * "enabled" now means only that a plugin has been selected for recording. + * The "record" config option is now used to start/stop recording. */ + /* Returns the output plugin that will be used for recording, or null if none is * available. Connect to the "enable record" hook to monitor changes. */ PluginHandle * aud_drct_get_record_plugin (); @@ -77,8 +81,8 @@ PluginHandle * aud_drct_get_record_plugin (); * "enable record" hook to monitor changes. */ bool aud_drct_get_record_enabled (); -/* Enables or disables output recording. If playback is active, recording - * begins immediately. Returns true on success, otherwise false. */ +/* Enables or disables output recording (but does not actually start recording). + * Returns true on success, otherwise false. */ bool aud_drct_enable_record (bool enable); /* --- VOLUME CONTROL --- */ diff --git a/src/libaudcore/equalizer-preset.cc b/src/libaudcore/equalizer-preset.cc index 699d0fb..c57d5f0 100644 --- a/src/libaudcore/equalizer-preset.cc +++ b/src/libaudcore/equalizer-preset.cc @@ -18,13 +18,12 @@ * the use of this software. */ +#define AUD_GLIB_INTEGRATION #include "equalizer.h" #include <math.h> #include <string.h> -#include <glib.h> /* for GKeyFile */ - #include "audstrings.h" #include "runtime.h" #include "vfs.h" @@ -49,7 +48,7 @@ EXPORT Index<EqualizerPreset> aud_eq_read_presets (const char * basename) for (int p = 0;; p ++) { - char * name = g_key_file_get_string (rcfile, "Presets", str_printf ("Preset%d", p), nullptr); + CharPtr name (g_key_file_get_string (rcfile, "Presets", str_printf ("Preset%d", p), nullptr)); if (! name) break; @@ -58,8 +57,6 @@ EXPORT Index<EqualizerPreset> aud_eq_read_presets (const char * basename) for (int i = 0; i < AUD_EQ_NBANDS; i++) preset.bands[i] = g_key_file_get_double (rcfile, name, str_printf ("Band%d", i), nullptr); - - g_free (name); } g_key_file_free (rcfile); @@ -83,13 +80,12 @@ EXPORT bool aud_eq_write_presets (const Index<EqualizerPreset> & list, const cha } size_t len; - char * data = g_key_file_to_data (rcfile, & len, nullptr); + CharPtr data (g_key_file_to_data (rcfile, & len, nullptr)); StringBuf filename = filename_build ({aud_get_path (AudPath::UserDir), basename}); bool success = g_file_set_contents (filename, data, len, nullptr); g_key_file_free (rcfile); - g_free (data); return success; } @@ -166,12 +162,11 @@ EXPORT bool aud_save_preset_file (const EqualizerPreset & preset, VFSFile & file str_printf ("Band%d", i), preset.bands[i]); size_t len; - char * data = g_key_file_to_data (rcfile, & len, nullptr); + CharPtr data (g_key_file_to_data (rcfile, & len, nullptr)); bool success = (file.fwrite (data, 1, len) == (int64_t) len); g_key_file_free (rcfile); - g_free (data); return success; } diff --git a/src/libaudcore/hook.cc b/src/libaudcore/hook.cc index 3b8b80e..e9dd695 100644 --- a/src/libaudcore/hook.cc +++ b/src/libaudcore/hook.cc @@ -127,15 +127,15 @@ DONE: pthread_mutex_unlock (& mutex); } -void leak_cb (const String & name, HookList & list, void *) -{ - AUDWARN ("Hook not disconnected: %s (%d)\n", (const char *) name, list.items.len ()); -} - void hook_cleanup () { pthread_mutex_lock (& mutex); - hooks.iterate (leak_cb, nullptr); + + hooks.iterate ([] (const String & name, HookList & list) { + AUDWARN ("Hook not disconnected: %s (%d)\n", (const char *) name, list.items.len ()); + }); + hooks.clear (); + pthread_mutex_unlock (& mutex); } diff --git a/src/libaudcore/hook.h b/src/libaudcore/hook.h index 6f8ed4c..147591a 100644 --- a/src/libaudcore/hook.h +++ b/src/libaudcore/hook.h @@ -106,7 +106,7 @@ void event_queue (const char * name, void * data, EventDestroyFunc destroy = nul /* Cancels pending hook calls matching <name> and <data>. If <data> is nullptr, * all hook calls matching <name> are canceled. */ -void event_queue_cancel (const char * name, void * data); +void event_queue_cancel (const char * name, void * data = nullptr); /* Convenience wrapper for C++ classes. Allows non-static member functions to * be used as hook callbacks. The HookReceiver should be made a member of the diff --git a/src/libaudcore/index.h b/src/libaudcore/index.h index 68957c3..92be2d9 100644 --- a/src/libaudcore/index.h +++ b/src/libaudcore/index.h @@ -173,19 +173,25 @@ public: // func(val) returns true to remove val, false to keep it template<class F> - void remove_if (F func, bool clear_if_empty = false) + bool remove_if (F func, bool clear_if_empty = false) { T * iter = begin (); + bool changed = false; while (iter != end ()) { if (func (* iter)) + { remove (iter - begin (), 1); + changed = true; + } else iter ++; } if (clear_if_empty && ! len ()) clear (); + + return changed; } // compare(a, b) returns <0 if a<b, 0 if a=b, >0 if a>b diff --git a/src/libaudcore/interface.cc b/src/libaudcore/interface.cc index 2d2db3d..f83a53e 100644 --- a/src/libaudcore/interface.cc +++ b/src/libaudcore/interface.cc @@ -21,6 +21,7 @@ #include "internal.h" #include <assert.h> +#include <glib.h> #include "drct.h" #include "hook.h" @@ -126,8 +127,7 @@ EXPORT void aud_ui_show_error (const char * message) if (aud_get_headless_mode ()) AUDERR ("%s\n", message); else - event_queue ("ui show error", String::raw_get (message), - (EventDestroyFunc) String::raw_unref); + event_queue ("ui show error", g_strdup (message), g_free); } PluginHandle * iface_plugin_get_current () diff --git a/src/libaudcore/internal.h b/src/libaudcore/internal.h index 534ba69..bf2216c 100644 --- a/src/libaudcore/internal.h +++ b/src/libaudcore/internal.h @@ -128,6 +128,18 @@ StringBuf strip_subtune (const char * filename); unsigned int32_hash (unsigned val); unsigned ptr_hash (const void * ptr); +struct IntHashKey +{ + int val; + + constexpr IntHashKey (int val) : + val (val) {} + operator int () const + { return val; } + unsigned hash () const + { return int32_hash (val); } +}; + /* vis-runner.cc */ void vis_runner_start_stop (bool playing, bool paused); void vis_runner_pass_audio (int time, const Index<float> & data, int channels, int rate); diff --git a/src/libaudcore/list.h b/src/libaudcore/list.h index 623603d..a3551f1 100644 --- a/src/libaudcore/list.h +++ b/src/libaudcore/list.h @@ -74,6 +74,18 @@ public: void clear () { ListBase::clear (destroy); } + template<class MatchFunc> + C * find (MatchFunc match) + { + for (C * node = head (); node; node = next (node)) + { + if (match (* node)) + return node; + } + + return nullptr; + } + private: static void destroy (ListNode * node) { delete (C *) node; } diff --git a/src/libaudcore/mainloop.cc b/src/libaudcore/mainloop.cc index a728660..397106f 100644 --- a/src/libaudcore/mainloop.cc +++ b/src/libaudcore/mainloop.cc @@ -46,6 +46,10 @@ struct QueuedFuncParams { bool repeat; }; +// Helper class that is created when the QueuedFunc is activated. The base +// class contains the GLib implementation. The Qt implementation, which is more +// complex, is contained in the QueuedFuncEvent and QueuedFuncTimer subclasses. + struct QueuedFuncHelper { QueuedFuncParams params; @@ -66,47 +70,69 @@ struct QueuedFuncHelper virtual void stop (); private: + struct RunCheck; + int glib_source = 0; }; -struct QueuedFuncNode : public MultiHash::Node { +// The following hash table implements a thread-safe "registry" of active +// QueuedFuncs. This allows a callback executing in the main thread to safely +// look up the information it needs, without accessing the QueuedFunc directly +// and risking use-after-free. + +struct QueuedFuncNode : public MultiHash::Node +{ QueuedFunc * queued; QueuedFuncHelper * helper; bool can_stop; + + bool match (const QueuedFunc * q) const + { return q == queued; } }; -static bool match_cb (const MultiHash::Node * node_, const void * queued_) -{ - auto node = (const QueuedFuncNode *) node_; - return node->queued == (QueuedFunc *) queued_; -} +static MultiHash_T<QueuedFuncNode, QueuedFunc> func_table; -static MultiHash func_table (match_cb); +// Helper logic common between GLib and Qt -void QueuedFuncHelper::run () +// "run" logic executed within the hash table lock +struct QueuedFuncHelper::RunCheck { - struct State { - QueuedFuncHelper * helper; - bool valid; - }; - - auto found_cb = [] (MultiHash::Node * node_, void * s_) - { - auto node = (const QueuedFuncNode *) node_; - auto s = (State *) s_; - - s->valid = (node->helper == s->helper); + QueuedFuncHelper * helper; + bool valid; - if (! s->valid || ! s->helper->params.repeat) - s->helper->stop (); + QueuedFuncNode * add (const QueuedFunc *) + { return nullptr; } - return s->valid && ! s->helper->params.repeat; - }; + bool found (const QueuedFuncNode * node) + { + // Check that the same helper is installed as when the function was + // first queued. If a new helper is installed, then we are processing + // a "stale" event and should not run the callback. + valid = (node->helper == helper); + + // Uninstall the QueuedFunc and helper if this is a one-time (non- + // periodic) callback. Do NOT uninstall the QueuedFunc if has already + // been reinstalled with a new helper (see previous comment). + bool remove = valid && ! helper->params.repeat; + + // Clean up the helper if this is a one-time (non-periodic) callback OR + // a new helper has already been installed. No fields or methods of the + // helper may be accessed after this point. + if (! valid || ! helper->params.repeat) + helper->stop (); + + return remove; + } +}; - State s = {this, false}; - func_table.lookup (queued, ptr_hash (queued), nullptr, found_cb, & s); +void QueuedFuncHelper::run () +{ + // Check whether it's okay to run. The actual check is performed within + // the MultiHash lock to eliminate race conditions. + RunCheck r = {this, false}; + func_table.lookup (queued, ptr_hash (queued), r); - if (s.valid) + if (r.valid) params.func (params.data); } @@ -115,22 +141,20 @@ void QueuedFuncHelper::start_for (QueuedFunc * queued_) queued = queued_; queued->_running = params.repeat; - start (); + start (); // branch to GLib or Qt implementation } +// GLib implementation -- simple wrapper around g_timeout_add_full() + void QueuedFuncHelper::start () { auto callback = [] (void * me) -> gboolean { - ((QueuedFuncHelper *) me)->run (); + (static_cast<QueuedFuncHelper *> (me))->run (); return G_SOURCE_CONTINUE; }; - auto destroy = [] (void * me) { - delete (QueuedFuncHelper *) me; - }; - glib_source = g_timeout_add_full (G_PRIORITY_HIGH, params.interval_ms, - callback, this, destroy); + callback, this, aud::delete_obj<QueuedFuncHelper>); } void QueuedFuncHelper::stop () @@ -141,6 +165,16 @@ void QueuedFuncHelper::stop () #ifdef USE_QT +// Qt implementation -- rather more complicated + +// One-time callbacks are implemented through custom events posted to a "router" +// object. In this case, the QueuedFuncHelper class is the QEvent itself. The +// router object is created in, and associated with, the main thread, thus +// events are executed in the main thread regardless of their origin. Events +// can be queued even before the QApplication has been created. Note that +// QEvents cannot be cancelled once posted; therefore it's necessary to check +// on our side that the QueuedFunc is still active when the event is received. + class QueuedFuncEvent : public QueuedFuncHelper, public QEvent { public: @@ -168,6 +202,15 @@ void QueuedFuncEvent::start () QCoreApplication::postEvent (& router, this, Qt::HighEventPriority); } +// Periodic callbacks are implemented through QObject's timer capability. In +// this case, the QueuedFuncHelper is a QObject that is re-associated with the +// main thread immediately after creation. The timer is started in a roundabout +// fashion due to the fact that QObject::startTimer() does not work until the +// QApplication has been created. To work around this, a single QEvent is first +// posted. When this initial event is executed (in the main thread), we know +// that the QApplication is up. We then start the timer and run our own +// callback from the QTimerEvents we receive. + class QueuedFuncTimer : public QueuedFuncHelper, public QObject { public: @@ -177,9 +220,6 @@ public: void start () { moveToThread (router.thread ()); // main thread - - // The timer cannot be started until QCoreApplication is instantiated. - // Send ourselves an event and wait till it comes back, then start the timer. QCoreApplication::postEvent (this, new QEvent (QEvent::User), Qt::HighEventPriority); } @@ -187,14 +227,15 @@ public: { deleteLater (); } protected: - void customEvent (QEvent * event) + void customEvent (QEvent *) { startTimer (params.interval_ms); } - void timerEvent (QTimerEvent * event) + void timerEvent (QTimerEvent *) { run (); } }; #endif // USE_QT +// creates the appropriate helper subclass static QueuedFuncHelper * create_helper (const QueuedFuncParams & params) { #ifdef USE_QT @@ -210,38 +251,48 @@ static QueuedFuncHelper * create_helper (const QueuedFuncParams & params) return new QueuedFuncHelper (params); } -void QueuedFunc::start (const QueuedFuncParams & params) +// "start" logic executed within the hash table lock +struct QueuedFunc::Starter { - auto add_cb = [] (const void * me_, void * helper_) { - auto me = (QueuedFunc *) me_; - auto helper = (QueuedFuncHelper *) helper_; + QueuedFunc * queued; + QueuedFuncHelper * helper; + // QueuedFunc not yet installed + // install, then create a helper and start it + QueuedFuncNode * add (const QueuedFunc *) + { auto node = new QueuedFuncNode; - node->queued = me; + node->queued = queued; node->helper = helper; node->can_stop = helper->can_stop (); - helper->start_for (me); - - return (MultiHash::Node *) node; - }; + helper->start_for (queued); - auto replace_cb = [] (MultiHash::Node * node_, void * helper_) { - auto node = (QueuedFuncNode *) node_; - auto helper = (QueuedFuncHelper *) helper_; + return node; + } + // QueuedFunc already installed + // first clean up the existing helper + // then create a new helper and start it + bool found (QueuedFuncNode * node) + { if (node->can_stop) node->helper->stop (); node->helper = helper; node->can_stop = helper->can_stop (); - helper->start_for (node->queued); + helper->start_for (queued); return false; // do not remove - }; + } +}; - func_table.lookup (this, ptr_hash (this), add_cb, replace_cb, create_helper (params)); +// common entry point used by all queue() and start() variants +void QueuedFunc::start (const QueuedFuncParams & params) +{ + Starter s = {this, create_helper (params)}; + func_table.lookup (this, ptr_hash (this), s); } EXPORT void QueuedFunc::queue (Func func, void * data) @@ -261,11 +312,16 @@ EXPORT void QueuedFunc::start (int interval_ms, Func func, void * data) start ({func, data, interval_ms, true}); } -EXPORT void QueuedFunc::stop () +// "stop" logic executed within the hash table lock +struct QueuedFunc::Stopper { - auto remove_cb = [] (MultiHash::Node * node_, void *) { - auto node = (QueuedFuncNode *) node_; + // not installed, do nothing + QueuedFuncNode * add (const QueuedFunc *) + { return nullptr; } + // installed, clean up the helper and uninstall + bool found (QueuedFuncNode * node) + { if (node->can_stop) node->helper->stop (); @@ -273,11 +329,17 @@ EXPORT void QueuedFunc::stop () delete node; return true; // remove - }; + } +}; - func_table.lookup (this, ptr_hash (this), nullptr, remove_cb, nullptr); +EXPORT void QueuedFunc::stop () +{ + Stopper s; + func_table.lookup (this, ptr_hash (this), s); } +// main loop implementation follows + EXPORT void mainloop_run () { pthread_mutex_lock (& mainloop_mutex); diff --git a/src/libaudcore/mainloop.h b/src/libaudcore/mainloop.h index dabf9e3..ee78717 100644 --- a/src/libaudcore/mainloop.h +++ b/src/libaudcore/mainloop.h @@ -60,6 +60,9 @@ public: { stop (); } private: + struct Starter; + struct Stopper; + bool _running = false; void start (const QueuedFuncParams & params); diff --git a/src/libaudcore/multihash.h b/src/libaudcore/multihash.h index 4ddd37f..663b756 100644 --- a/src/libaudcore/multihash.h +++ b/src/libaudcore/multihash.h @@ -1,6 +1,6 @@ /* * multihash.h - * Copyright 2013-2014 John Lindgren + * Copyright 2013-2017 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,11 +29,13 @@ class HashBase { public: - /* Skeleton structure containing internal members of a hash node. Actual - * node structures should be defined with Node as the first member. */ + /* Skeleton structure containing internal members of a hash node (except for + * "refs", which is not used internally and included here only to fill an + * alignment gap). Actual node structures should subclass Node. */ struct Node { Node * next; unsigned hash; + unsigned refs; }; /* Represents the location of a node within the table. */ @@ -143,6 +145,69 @@ private: HashBase channels[Channels]; }; +/* Type-safe version using templates. */ + +template<class Node_T, class Data_T> +class MultiHash_T : private MultiHash +{ +public: + // Required interfaces: + // + // class Node_T : public Node + // { + // bool match (const Data_T * data) const; + // }; + // + // class Operation + // { + // Node_T * add (const Data_T * data); + // bool found (Node_T * node); + // }; + + MultiHash_T () : MultiHash (match_cb) {} + + void clear () + { MultiHash::iterate (remove_cb, nullptr); } + + template<class Op> + int lookup (const Data_T * data, unsigned hash, Op & op) + { return MultiHash::lookup (data, hash, WrapOp<Op>::add, WrapOp<Op>::found, & op); } + + template<class F> + void iterate (F func) + { MultiHash::iterate (WrapIterate<F>::run, & func); } + +private: + static bool match_cb (const Node * node, const void * data) + { return (static_cast<const Node_T *> (node))->match + (static_cast<const Data_T *> (data)); } + + static bool remove_cb (Node * node, void *) + { + delete static_cast<Node_T *> (node); + return true; + } + + template<class Op> + struct WrapOp { + static Node * add (const void * data, void * op) + { return (static_cast<Op *> (op))->add + (static_cast<const Data_T *> (data)); } + static bool found (Node * node, void * op) + { return (static_cast<Op *> (op))->found + (static_cast<Node_T *> (node)); } + }; + + template<class F> + struct WrapIterate { + static bool run (Node * node, void * func) + { return (* static_cast<F *> (func)) + (static_cast<Node_T *> (node)); } + }; +}; + +/* Simpler single-thread hash table. */ + template<class Key, class Value> class SimpleHash : private HashBase { @@ -156,14 +221,14 @@ public: Value * lookup (const Key & key) { - Node * node = (Node *) HashBase::lookup (match_cb, & key, key.hash ()); + auto node = static_cast<Node *> (HashBase::lookup (match_cb, & key, key.hash ())); return node ? & node->value : nullptr; } Value * add (const Key & key, Value && value) { unsigned hash = key.hash (); - Node * node = (Node *) HashBase::lookup (match_cb, & key, hash); + auto node = static_cast<Node *> (HashBase::lookup (match_cb, & key, hash)); if (node) node->value = std::move (value); @@ -179,7 +244,7 @@ public: void remove (const Key & key) { NodeLoc loc; - Node * node = (Node *) HashBase::lookup (match_cb, & key, key.hash (), & loc); + auto node = static_cast<Node *> (HashBase::lookup (match_cb, & key, key.hash (), & loc)); if (node) { @@ -188,18 +253,16 @@ public: } } - void iterate (IterFunc func, void * state) - { - IterData data = {func, state}; - HashBase::iterate (iterate_cb, & data); - } - void clear () { HashBase::iterate (remove_cb, nullptr); HashBase::clear (); } + template<class F> + void iterate (F func) + { HashBase::iterate (WrapIterate<F>::run, & func); } + private: struct Node : public HashBase::Node { @@ -217,21 +280,26 @@ private: }; static bool match_cb (const HashBase::Node * node, const void * data) - { return ((const Node *) node)->key == * (const Key *) data; } + { return (static_cast<const Node *> (node))->key == + * static_cast<const Key *> (data); } static bool remove_cb (HashBase::Node * node, void *) { - delete (Node *) node; + delete static_cast<Node *> (node); return true; } - static bool iterate_cb (HashBase::Node * node0, void * data0) + // C-style callback wrapping generic iteration functor + template<class F> + struct WrapIterate { - Node * node = (Node *) node0; - IterData * data = (IterData *) data0; - data->func (node->key, node->value, data->state); - return false; - } + static bool run (HashBase::Node * node_, void * func) + { + auto node = static_cast<Node *> (node_); + (* static_cast<F *> (func)) (node->key, node->value); + return false; + } + }; }; #endif /* LIBAUDCORE_MULTIHASH_H */ diff --git a/src/libaudcore/objects.h b/src/libaudcore/objects.h index 4b98cc6..facc0ba 100644 --- a/src/libaudcore/objects.h +++ b/src/libaudcore/objects.h @@ -20,6 +20,10 @@ #ifndef LIBAUDCORE_OBJECTS_H #define LIBAUDCORE_OBJECTS_H +#ifdef AUD_GLIB_INTEGRATION +#include <glib.h> +#endif + #include <libaudcore/templates.h> // Stores array pointer together with deduced array length. @@ -51,11 +55,7 @@ struct ArrayRef // Smart pointer. Deletes object pointed to when the pointer goes out of scope. -template<class T> -void SmartPtrDelete (T * ptr) - { (void) sizeof (T); delete ptr; } - -template<class T, void (* deleter) (T *) = SmartPtrDelete> +template<class T, void (* deleter) (T *) = aud::delete_typed> class SmartPtr { public: @@ -73,6 +73,13 @@ public: ptr = ptr2; } + T * release () + { + T * ptr2 = ptr; + ptr = nullptr; + return ptr2; + } + void clear () { capture (nullptr); } @@ -119,6 +126,21 @@ SmartPtr<T> SmartNew (Args && ... args) std::forward<Args> (args) ...)); } +// Convenience wrapper for a GLib-style string (char *). + +#ifdef AUD_GLIB_INTEGRATION +class CharPtr : public SmartPtr<char, aud::typed_func<char, g_free>> +{ +public: + CharPtr () : SmartPtr () {} + explicit CharPtr (char * ptr) : SmartPtr (ptr) {} + + // non-const operator omitted to prevent "CharPtr s; g_free(s);" + operator const char * () const + { return get (); } +}; +#endif + // Wrapper class for a string stored in the string pool. class String @@ -174,30 +196,13 @@ public: unsigned hash () const { return raw_hash (raw); } - // raw interface - // avoid using where possible - - static String from_raw (char * str) - { - String s; - s.raw = str; - return s; - } - - char * to_raw () - { - char * str = raw; - raw = nullptr; - return str; - } - +private: static char * raw_get (const char * str); static char * raw_ref (const char * str); static void raw_unref (char * str); static unsigned raw_hash (const char * str); static bool raw_equal (const char * str1, const char * str2); -private: char * raw; }; diff --git a/src/libaudcore/output.cc b/src/libaudcore/output.cc index abb3c7f..e1a1015 100644 --- a/src/libaudcore/output.cc +++ b/src/libaudcore/output.cc @@ -105,7 +105,7 @@ static inline int get_format (bool & automatic) switch (aud_get_int (0, "output_bit_depth")) { case 16: return FMT_S16_NE; - case 24: return FMT_S24_NE; + case 24: return FMT_S24_3NE; case 32: return FMT_S32_NE; // return FMT_FLOAT for "auto" as well @@ -188,6 +188,8 @@ static void setup_output (bool new_input) format = FMT_S32_NE; else if (automatic && format == FMT_S32_NE) format = FMT_S16_NE; + else if (format == FMT_S24_3NE) + format = FMT_S24_NE; /* some output plugins support only padded 24-bit */ else { aud_ui_show_error (error ? (const char *) error : _("Error opening output stream")); @@ -243,7 +245,7 @@ static void setup_secondary (bool new_input) String error; if (! sop->open_audio (FMT_FLOAT, rate, channels, error)) { - aud_ui_show_error (error ? (const char *) error : _("Error opening output stream")); + aud_ui_show_error (error ? (const char *) error : _("Error recording output stream")); return; } @@ -274,7 +276,10 @@ static void apply_replay_gain (Index<float> & data) { float peak; - if (aud_get_bool (0, "replay_gain_album")) + auto mode = (ReplayGainMode) aud_get_int (0, "replay_gain_mode"); + if ((mode == ReplayGainMode::Album) || + (mode == ReplayGainMode::Automatic && + (! aud_get_bool (0, "shuffle") || aud_get_bool (0, "album_shuffle")))) { factor *= powf (10, gain_info.album_gain / 20); peak = gain_info.album_peak; @@ -434,7 +439,9 @@ bool output_open_audio (const String & filename, const Tuple & tuple, setup_effects (); setup_output (true); - setup_secondary (true); + + if (aud_get_bool (0, "record")) + setup_secondary (true); UNLOCK_ALL; return true; @@ -668,7 +675,9 @@ static void output_reset (OutputReset type, OutputPlugin * op) setup_effects (); setup_output (false); - setup_secondary (false); + + if (aud_get_bool (0, "record")) + setup_secondary (false); } s_resetting = false; @@ -745,29 +754,33 @@ bool output_plugin_set_secondary (PluginHandle * plugin) if (sop && ! sop->init ()) sop = nullptr; - if (s_input) + if (s_input && aud_get_bool (0, "record")) setup_secondary (false); UNLOCK_MINOR; return (! plugin || sop); } -static void record_stream_changed (void *, void *) +static void record_settings_changed (void *, void *) { LOCK_MINOR; - if (s_input) + if (s_input && aud_get_bool (0, "record")) setup_secondary (false); + else + cleanup_secondary (); UNLOCK_MINOR; } void output_init () { - hook_associate ("set record_stream", record_stream_changed, nullptr); + hook_associate ("set record", record_settings_changed, nullptr); + hook_associate ("set record_stream", record_settings_changed, nullptr); } void output_cleanup () { - hook_dissociate ("set record_stream", record_stream_changed, nullptr); + hook_dissociate ("set record", record_settings_changed); + hook_dissociate ("set record_stream", record_settings_changed); } diff --git a/src/libaudcore/playback.cc b/src/libaudcore/playback.cc index b7e94a0..aea3b1b 100644 --- a/src/libaudcore/playback.cc +++ b/src/libaudcore/playback.cc @@ -184,13 +184,13 @@ static void playback_cleanup_locked () end_queue.stop (); song_finished = false; - event_queue_cancel ("playback ready", nullptr); - event_queue_cancel ("playback pause", nullptr); - event_queue_cancel ("playback unpause", nullptr); - event_queue_cancel ("playback seek", nullptr); - event_queue_cancel ("info change", nullptr); - event_queue_cancel ("title change", nullptr); - event_queue_cancel ("tuple change", nullptr); + event_queue_cancel ("playback ready"); + event_queue_cancel ("playback pause"); + event_queue_cancel ("playback unpause"); + event_queue_cancel ("playback seek"); + event_queue_cancel ("info change"); + event_queue_cancel ("title change"); + event_queue_cancel ("tuple change"); aud_set_bool (nullptr, "stop_after_current_song", false); } @@ -233,19 +233,19 @@ static void end_cb (void *) song_finished = true; hook_call ("playback end", nullptr); - int playlist = aud_playlist_get_playing (); + PlaylistEx playlist = Playlist::playing_playlist (); auto do_stop = [playlist] () { - aud_playlist_play (-1); - aud_playlist_set_position (playlist, aud_playlist_get_position (playlist)); + aud_drct_stop (); + playlist.set_position (playlist.get_position ()); }; auto do_next = [playlist] () { - if (! playlist_next_song (playlist, aud_get_bool (nullptr, "repeat"))) + if (! playlist.next_song (aud_get_bool (nullptr, "repeat"))) { - aud_playlist_set_position (playlist, -1); + playlist.set_position (-1); hook_call ("playlist end reached", nullptr); } }; @@ -364,10 +364,12 @@ static void finish_playback_locked () if (pb_info.error) { failed_entries ++; - aud_ui_show_error (str_printf (_("Error playing %s:\n%s"), - (const char *) pb_info.filename, pb_info.error_s ? - (const char *) pb_info.error_s : _("Unknown playback error " - "(check the console for detailed error information)"))); + + if (pb_info.error_s) + aud_ui_show_error (str_printf (_("Error playing %s:\n%s"), + (const char *) pb_info.filename, (const char *) pb_info.error_s)); + else + AUDERR ("Playback finished with error.\n"); } else failed_entries = 0; diff --git a/src/libaudcore/playlist-cache.cc b/src/libaudcore/playlist-cache.cc index 9711235..fe371ac 100644 --- a/src/libaudcore/playlist-cache.cc +++ b/src/libaudcore/playlist-cache.cc @@ -27,26 +27,26 @@ 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) +EXPORT void Playlist::cache_selected () const { pthread_mutex_lock (& mutex); - int entries = aud_playlist_entry_count (playlist); + int entries = n_entries (); for (int i = 0; i < entries; i ++) { - if (! aud_playlist_entry_get_selected (playlist, i)) + if (! entry_selected (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); + String filename = entry_filename (i); + Tuple tuple = entry_tuple (i, NoWait); + PluginHandle * decoder = entry_decoder (i, NoWait); if (tuple.valid () || decoder) cache.add (filename, {filename, std::move (tuple), decoder}); } - clear_timer.queue (30000, (QueuedFunc::Func) playlist_cache_clear, nullptr); + clear_timer.queue (30000, playlist_cache_clear, nullptr); pthread_mutex_unlock (& mutex); } @@ -77,7 +77,7 @@ out: pthread_mutex_unlock (& mutex); } -void playlist_cache_clear () +void playlist_cache_clear (void *) { pthread_mutex_lock (& mutex); diff --git a/src/libaudcore/playlist-data.cc b/src/libaudcore/playlist-data.cc new file mode 100644 index 0000000..274a3e5 --- /dev/null +++ b/src/libaudcore/playlist-data.cc @@ -0,0 +1,1123 @@ +/* + * playlist-data.cc + * Copyright 2017 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-data.h" + +#include <stdlib.h> +#include <string.h> + +#include "runtime.h" +#include "scanner.h" +#include "tuple-compiler.h" + +static TupleCompiler s_tuple_formatter; +static bool s_use_tuple_fallbacks = false; + +struct PlaylistEntry +{ + PlaylistEntry (PlaylistAddItem && item); + ~PlaylistEntry (); + + void format (); + void set_tuple (Tuple && new_tuple); + + String filename; + PluginHandle * decoder; + Tuple tuple; + String error; + int number; + int length; + int shuffle_num; + bool selected, queued; +}; + +void PlaylistEntry::format () +{ + tuple.delete_fallbacks (); + + if (s_use_tuple_fallbacks) + tuple.generate_fallbacks (); + else + tuple.generate_title (); + + s_tuple_formatter.format (tuple); +} + +void PlaylistEntry::set_tuple (Tuple && new_tuple) +{ + /* 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; + + error = String (); + + if (! new_tuple.valid ()) + new_tuple.set_filename (filename); + + length = aud::max (0, new_tuple.get_int (Tuple::Length)); + tuple = std::move (new_tuple); + + format (); +} + +PlaylistEntry::PlaylistEntry (PlaylistAddItem && item) : + filename (item.filename), + decoder (item.decoder), + number (-1), + length (0), + shuffle_num (0), + selected (false), + queued (false) +{ + set_tuple (std::move (item.tuple)); +} + +PlaylistEntry::~PlaylistEntry () +{ + pl_signal_entry_deleted (this); +} + +void PlaylistData::update_formatter () // static +{ + s_tuple_formatter.compile (aud_get_str (nullptr, "generic_title_format")); + s_use_tuple_fallbacks = aud_get_bool (nullptr, "metadata_fallbacks"); +} + +void PlaylistData::cleanup_formatter () // static + { s_tuple_formatter.reset (); } + +void PlaylistData::delete_entry (PlaylistEntry * entry) // static + { delete entry; } + +PlaylistData::PlaylistData (Playlist::ID * id, const char * title) : + modified (true), + scan_status (NotScanning), + title (title), + resume_time (0), + m_id (id), + m_position (nullptr), + m_focus (nullptr), + m_selected_count (0), + m_last_shuffle_num (0), + m_total_length (0), + m_selected_length (0), + m_last_update (), + m_next_update (), + m_position_changed (false) {} + +PlaylistData::~PlaylistData () +{ + pl_signal_playlist_deleted (m_id); +} + +void PlaylistData::number_entries (int at, int length) +{ + for (int i = at; i < at + length; i ++) + m_entries[i]->number = i; +} + +PlaylistEntry * PlaylistData::entry_at (int i) +{ + return (i >= 0 && i < m_entries.len ()) ? m_entries[i].get () : nullptr; +} + +const PlaylistEntry * PlaylistData::entry_at (int i) const +{ + return (i >= 0 && i < m_entries.len ()) ? m_entries[i].get () : nullptr; +} + +String PlaylistData::entry_filename (int i) const +{ + auto entry = entry_at (i); + return entry ? entry->filename : String (); +} + +PluginHandle * PlaylistData::entry_decoder (int i, String * error) const +{ + auto entry = entry_at (i); + if (error) * error = entry ? entry->error : String (); + return entry ? entry->decoder : nullptr; +} + +Tuple PlaylistData::entry_tuple (int i, String * error) const +{ + auto entry = entry_at (i); + if (error) * error = entry ? entry->error : String (); + return entry ? entry->tuple.ref () : Tuple (); +} + +void PlaylistData::set_entry_tuple (PlaylistEntry * entry, Tuple && tuple) +{ + m_total_length -= entry->length; + if (entry->selected) + m_selected_length -= entry->length; + + entry->set_tuple (std::move (tuple)); + + m_total_length += entry->length; + if (entry->selected) + m_selected_length += entry->length; +} + +void PlaylistData::queue_update (Playlist::UpdateLevel level, int at, int count, int flags) +{ + if (m_next_update.level) + { + m_next_update.level = aud::max (m_next_update.level, level); + m_next_update.before = aud::min (m_next_update.before, at); + m_next_update.after = aud::min (m_next_update.after, m_entries.len () - at - count); + } + else + { + m_next_update.level = level; + m_next_update.before = at; + m_next_update.after = m_entries.len () - at - count; + } + + if ((flags & QueueChanged)) + m_next_update.queue_changed = true; + + pl_signal_update_queued (m_id, level, flags); +} + +void PlaylistData::queue_position_change () +{ + m_position_changed = true; + pl_signal_position_changed (m_id); +} + +void PlaylistData::cancel_updates () +{ + m_last_update = Playlist::Update (); + m_next_update = Playlist::Update (); + m_position_changed = false; +} + +void PlaylistData::swap_updates (bool & position_changed) +{ + m_last_update = m_next_update; + m_next_update = Playlist::Update (); + position_changed = m_position_changed; + m_position_changed = false; +} + +void PlaylistData::insert_items (int at, Index<PlaylistAddItem> && items) +{ + int n_entries = m_entries.len (); + int n_items = items.len (); + + if (at < 0 || at > n_entries) + at = n_entries; + + m_entries.insert (at, n_items); + + int i = at; + for (auto & item : items) + { + auto entry = new PlaylistEntry (std::move (item)); + m_entries[i ++].capture (entry); + m_total_length += entry->length; + } + + items.clear (); + + number_entries (at, n_entries + n_items - at); + queue_update (Playlist::Structure, at, n_items); +} + +void PlaylistData::remove_entries (int at, int number) +{ + int n_entries = m_entries.len (); + bool position_changed = false; + int update_flags = 0; + + if (at < 0 || at > n_entries) + at = n_entries; + if (number < 0 || number > n_entries - at) + number = n_entries - at; + + if (m_position && m_position->number >= at && m_position->number < at + number) + { + set_position (nullptr, false); + position_changed = true; + } + + if (m_focus && m_focus->number >= at && m_focus->number < at + number) + { + if (at + number < n_entries) + m_focus = m_entries[at + number].get (); + else if (at > 0) + m_focus = m_entries[at - 1].get (); + else + m_focus = nullptr; + } + + for (int i = 0; i < number; i ++) + { + PlaylistEntry * entry = m_entries [at + i].get (); + + if (entry->queued) + { + m_queued.remove (m_queued.find (entry), 1); + update_flags |= QueueChanged; + } + + if (entry->selected) + { + m_selected_count --; + m_selected_length -= entry->length; + } + + m_total_length -= entry->length; + } + + m_entries.remove (at, number); + + number_entries (at, n_entries - at - number); + queue_update (Playlist::Structure, at, 0, update_flags); + + if (position_changed) + { + if (aud_get_bool (nullptr, "advance_on_delete")) + next_song_with_hint (aud_get_bool (nullptr, "repeat"), at); + + queue_position_change (); + } +} + +int PlaylistData::position () const +{ + return m_position ? m_position->number : -1; +} + +int PlaylistData::focus () const +{ + return m_focus ? m_focus->number : -1; +} + +bool PlaylistData::entry_selected (int entry_num) const +{ + auto entry = entry_at (entry_num); + return entry ? entry->selected : false; +} + +int PlaylistData::n_selected (int at, int number) const +{ + int n_entries = m_entries.len (); + + if (at < 0 || at > n_entries) + at = n_entries; + if (number < 0 || number > n_entries - at) + number = n_entries - at; + + int n_selected = 0; + + if (at == 0 && number == n_entries) + n_selected = m_selected_count; + else + { + for (int i = 0; i < number; i ++) + { + if (m_entries[at + i]->selected) + n_selected ++; + } + } + + return n_selected; +} + +void PlaylistData::set_focus (int entry_num) +{ + auto new_focus = entry_at (entry_num); + if (new_focus == m_focus) + return; + + int first = m_entries.len (); + int last = -1; + + if (m_focus) + { + first = aud::min (first, m_focus->number); + last = aud::max (last, m_focus->number); + } + + m_focus = new_focus; + + if (m_focus) + { + first = aud::min (first, m_focus->number); + last = aud::max (last, m_focus->number); + } + + if (first <= last) + queue_update (Playlist::Selection, first, last + 1 - first); +} + +void PlaylistData::select_entry (int entry_num, bool selected) +{ + auto entry = entry_at (entry_num); + if (! entry || entry->selected == selected) + return; + + entry->selected = selected; + + if (selected) + { + m_selected_count ++; + m_selected_length += entry->length; + } + else + { + m_selected_count --; + m_selected_length -= entry->length; + } + + queue_update (Playlist::Selection, entry_num, 1); +} + +void PlaylistData::select_all (bool selected) +{ + int n_entries = m_entries.len (); + int first = n_entries, last = 0; + + for (auto & entry : m_entries) + { + if (entry->selected != selected) + { + entry->selected = selected; + first = aud::min (first, entry->number); + last = entry->number; + } + } + + if (selected) + { + m_selected_count = n_entries; + m_selected_length = m_total_length; + } + else + { + m_selected_count = 0; + m_selected_length = 0; + } + + if (first < n_entries) + queue_update (Playlist::Selection, first, last + 1 - first); +} + +int PlaylistData::shift_entries (int entry_num, int distance) +{ + PlaylistEntry * entry = entry_at (entry_num); + if (! entry || ! entry->selected || ! distance) + return 0; + + int n_entries = m_entries.len (); + int shift = 0, center, top, bottom; + + if (distance < 0) + { + for (center = entry_num; center > 0 && shift > distance; ) + { + if (! m_entries[-- center]->selected) + shift --; + } + } + else + { + for (center = entry_num + 1; center < n_entries && shift < distance; ) + { + if (! m_entries[center ++]->selected) + shift ++; + } + } + + top = bottom = center; + + for (int i = 0; i < top; i ++) + { + if (m_entries[i]->selected) + top = i; + } + + for (int i = n_entries; i > bottom; i --) + { + if (m_entries[i - 1]->selected) + bottom = i; + } + + Index<EntryPtr> temp; + + for (int i = top; i < center; i ++) + { + if (! m_entries[i]->selected) + temp.append (std::move (m_entries[i])); + } + + for (int i = top; i < bottom; i ++) + { + if (m_entries[i] && m_entries[i]->selected) + temp.append (std::move (m_entries[i])); + } + + for (int i = center; i < bottom; i ++) + { + if (m_entries[i] && ! m_entries[i]->selected) + temp.append (std::move (m_entries[i])); + } + + m_entries.move_from (temp, 0, top, bottom - top, false, true); + + number_entries (top, bottom - top); + queue_update (Playlist::Structure, top, bottom - top); + + return shift; +} + +void PlaylistData::remove_selected () +{ + if (! m_selected_count) + return; + + int n_entries = m_entries.len (); + bool position_changed = false; + int update_flags = 0; + + if (m_position && m_position->selected) + { + set_position (nullptr, false); + position_changed = true; + } + + m_focus = find_unselected_focus (); + + int before = 0; // number of entries before first selected + int after = 0; // number of entries after last selected + + while (before < n_entries && ! m_entries[before]->selected) + before ++; + + int to = before; + + for (int from = before; from < n_entries; from ++) + { + PlaylistEntry * entry = m_entries[from].get (); + + if (entry->selected) + { + if (entry->queued) + { + m_queued.remove (m_queued.find (entry), 1); + update_flags |= QueueChanged; + } + + m_total_length -= entry->length; + after = 0; + } + else + { + m_entries[to ++] = std::move (m_entries[from]); + after ++; + } + } + + n_entries = to; + m_entries.remove (n_entries, -1); + + m_selected_count = 0; + m_selected_length = 0; + + number_entries (before, n_entries - before); + queue_update (Playlist::Structure, before, n_entries - after - before, update_flags); + + if (position_changed) + { + if (aud_get_bool (nullptr, "advance_on_delete")) + next_song_with_hint (aud_get_bool (nullptr, "repeat"), n_entries - after); + + queue_position_change (); + } +} + +void PlaylistData::sort_entries (Index<EntryPtr> & entries, const CompareData & data) // static +{ + entries.sort ([data] (const EntryPtr & a, const EntryPtr & b) { + if (data.filename_compare) + return data.filename_compare (a->filename, b->filename); + else + return data.tuple_compare (a->tuple, b->tuple); + }); +} + +void PlaylistData::sort (const CompareData & data) +{ + sort_entries (m_entries, data); + + number_entries (0, m_entries.len ()); + queue_update (Playlist::Structure, 0, m_entries.len ()); +} + +void PlaylistData::sort_selected (const CompareData & data) +{ + int n_entries = m_entries.len (); + + Index<EntryPtr> selected; + + for (auto & entry : m_entries) + { + if (entry->selected) + selected.append (std::move (entry)); + } + + sort_entries (selected, data); + + int i = 0; + for (auto & entry : m_entries) + { + if (! entry) + entry = std::move (selected[i ++]); + } + + number_entries (0, n_entries); + queue_update (Playlist::Structure, 0, n_entries); +} + +void PlaylistData::reverse_order () +{ + int n_entries = m_entries.len (); + + for (int i = 0; i < n_entries / 2; i ++) + std::swap (m_entries[i], m_entries[n_entries - 1 - i]); + + number_entries (0, n_entries); + queue_update (Playlist::Structure, 0, n_entries); +} + +void PlaylistData::reverse_selected () +{ + int n_entries = m_entries.len (); + + int top = 0; + int bottom = n_entries - 1; + + while (1) + { + while (top < bottom && ! m_entries[top]->selected) + top ++; + while (top < bottom && ! m_entries[bottom]->selected) + bottom --; + + if (top >= bottom) + break; + + std::swap (m_entries[top ++], m_entries[bottom --]); + } + + number_entries (0, n_entries); + queue_update (Playlist::Structure, 0, n_entries); +} + +void PlaylistData::randomize_order () +{ + int n_entries = m_entries.len (); + + for (int i = 0; i < n_entries; i ++) + std::swap (m_entries[i], m_entries[rand () % n_entries]); + + number_entries (0, n_entries); + queue_update (Playlist::Structure, 0, n_entries); +} + +void PlaylistData::randomize_selected () +{ + int n_entries = m_entries.len (); + + Index<PlaylistEntry *> selected; + + for (auto & entry : m_entries) + { + if (entry->selected) + selected.append (entry.get ()); + } + + int n_selected = selected.len (); + + for (int i = 0; i < n_selected; i ++) + { + int a = selected[i]->number; + int b = selected[rand () % n_selected]->number; + std::swap (m_entries[a], m_entries[b]); + } + + number_entries (0, n_entries); + queue_update (Playlist::Structure, 0, n_entries); +} + +int PlaylistData::queue_get_entry (int at) const +{ + return (at >= 0 && at < m_queued.len ()) ? m_queued[at]->number : -1; +} + +int PlaylistData::queue_find_entry (int entry_num) const +{ + auto entry = entry_at (entry_num); + return entry->queued ? m_queued.find ((PlaylistEntry *) entry) : -1; +} + +void PlaylistData::queue_insert (int at, int entry_num) +{ + auto entry = entry_at (entry_num); + if (entry->queued) + return; + + if (at < 0 || at > m_queued.len ()) + m_queued.append (entry); + else + { + m_queued.insert (at, 1); + m_queued[at] = entry; + } + + entry->queued = true; + + queue_update (Playlist::Selection, entry_num, 1, QueueChanged); +} + +void PlaylistData::queue_insert_selected (int at) +{ + if (at < 0 || at > m_queued.len ()) + at = m_queued.len (); + + Index<PlaylistEntry *> add; + int first = m_entries.len (); + int last = 0; + + for (auto & entry : m_entries) + { + if (! entry->selected || entry->queued) + continue; + + add.append (entry.get ()); + entry->queued = true; + first = aud::min (first, entry->number); + last = entry->number; + } + + m_queued.move_from (add, 0, at, -1, true, true); + + if (first < m_entries.len ()) + queue_update (Playlist::Selection, first, last + 1 - first, QueueChanged); +} + +void PlaylistData::queue_remove (int at, int number) +{ + int queue_len = m_queued.len (); + + if (at < 0 || at > queue_len) + at = queue_len; + if (number < 0 || number > queue_len - at) + number = queue_len - at; + + int n_entries = m_entries.len (); + int first = n_entries, last = 0; + + for (int i = at; i < at + number; i ++) + { + PlaylistEntry * entry = m_queued[i]; + entry->queued = false; + first = aud::min (first, entry->number); + last = entry->number; + } + + m_queued.remove (at, number); + + if (first < n_entries) + queue_update (Playlist::Selection, first, last + 1 - first, QueueChanged); +} + +void PlaylistData::queue_remove_selected () +{ + int n_entries = m_entries.len (); + int first = n_entries, last = 0; + + for (int i = 0; i < m_queued.len ();) + { + PlaylistEntry * entry = m_queued[i]; + + if (entry->selected) + { + m_queued.remove (i, 1); + entry->queued = false; + first = aud::min (first, entry->number); + last = entry->number; + } + else + i ++; + } + + if (first < n_entries) + queue_update (Playlist::Selection, first, last + 1 - first, QueueChanged); +} + +void PlaylistData::set_position (PlaylistEntry * entry, bool update_shuffle) +{ + m_position = entry; + resume_time = 0; + + /* move entry to top of shuffle list */ + if (entry && update_shuffle) + entry->shuffle_num = ++ m_last_shuffle_num; +} + +void PlaylistData::set_position (int entry_num) +{ + set_position (entry_at (entry_num), true); + queue_position_change (); +} + +bool PlaylistData::shuffle_prev () +{ + PlaylistEntry * found = nullptr; + + for (auto & entry : m_entries) + { + if (entry->shuffle_num && + (! m_position || entry->shuffle_num < m_position->shuffle_num) && + (! found || entry->shuffle_num > found->shuffle_num)) + { + found = entry.get (); + } + } + + if (! found) + return false; + + set_position (found, false); + return true; +} + +bool PlaylistData::shuffle_next () +{ + bool by_album = aud_get_bool (nullptr, "album_shuffle"); + + // helper #1: determine whether two entries are in the same album + auto same_album = [] (const Tuple & a, const Tuple & b) + { + String album = a.get_str (Tuple::Album); + return (album && album == b.get_str (Tuple::Album)); + }; + + // helper #2: determine whether an entry is among the shuffle choices + auto is_choice = [&] (PlaylistEntry * prev, PlaylistEntry * entry) + { + return (! entry->shuffle_num) && (! by_album || ! prev || + prev->shuffle_num || ! same_album (prev->tuple, entry->tuple)); + }; + + if (m_position) + { + // step #1: check to see if the shuffle order is already established + PlaylistEntry * next = nullptr; + + for (auto & entry : m_entries) + { + if (entry->shuffle_num > m_position->shuffle_num && + (! next || entry->shuffle_num < next->shuffle_num)) + next = entry.get (); + } + + if (next) + { + set_position (next, false); + return true; + } + + // step #2: check to see if we should advance to the next entry + if (by_album && m_position->number + 1 < m_entries.len ()) + { + next = m_entries[m_position->number + 1].get (); + + if (! next->shuffle_num && same_album (m_position->tuple, next->tuple)) + { + set_position (next, true); + return true; + } + } + } + + // step #3: count the number of possible shuffle choices + int choices = 0; + PlaylistEntry * prev = nullptr; + + for (auto & entry : m_entries) + { + if (is_choice (prev, entry.get ())) + choices ++; + + prev = entry.get (); + } + + if (! choices) + return false; + + // step #4: pick one of those choices by random and find it again + choices = rand () % choices; + prev = nullptr; + + for (auto & entry : m_entries) + { + if (is_choice (prev, entry.get ())) + { + if (! choices) + { + set_position (entry.get (), true); + return true; + } + + choices --; + } + + prev = entry.get (); + } + + return false; // never reached +} + +void PlaylistData::shuffle_reset () +{ + m_last_shuffle_num = 0; + + for (auto & entry : m_entries) + entry->shuffle_num = 0; +} + +bool PlaylistData::prev_song () +{ + if (aud_get_bool (nullptr, "shuffle")) + { + if (! shuffle_prev ()) + return false; + } + else + { + int pos = position (); + if (pos < 1) + return false; + + set_position (m_entries[pos - 1].get (), true); + } + + queue_position_change (); + return true; +} + +bool PlaylistData::next_song_with_hint (bool repeat, int hint) +{ + int n_entries = m_entries.len (); + if (! n_entries) + return false; + + PlaylistEntry * entry; + if ((entry = queue_pop ())) + { + set_position (entry, true); + return true; + } + + if (aud_get_bool (nullptr, "shuffle")) + { + if (shuffle_next ()) + return true; + + if (! repeat) + return false; + + shuffle_reset (); + + return shuffle_next (); + } + + if (hint < 0 || hint >= n_entries) + { + if (! repeat) + return false; + + hint = 0; + } + + set_position (m_entries[hint].get (), true); + return true; +} + +bool PlaylistData::next_song (bool repeat) +{ + if (! next_song_with_hint (repeat, position () + 1)) // -1 becomes 0 + return false; + + queue_position_change (); + return true; +} + +int PlaylistData::next_unscanned_entry (int entry_num) const +{ + if (entry_num < 0) + return -1; + + for (; entry_num < m_entries.len (); entry_num ++) + { + auto & entry = *m_entries[entry_num]; + + if (entry.tuple.state () == Tuple::Initial && + strncmp (entry.filename, "stdin://", 8)) // blacklist stdin + { + return entry_num; + } + } + + return -1; +} + +ScanRequest * PlaylistData::create_scan_request (PlaylistEntry * entry, + ScanRequest::Callback callback, int extra_flags) +{ + int flags = extra_flags; + if (! entry->tuple.valid ()) + flags |= SCAN_TUPLE; + + /* scanner uses Tuple::AudioFile from existing tuple, if valid */ + return new ScanRequest (entry->filename, flags, callback, entry->decoder, + (flags & SCAN_TUPLE) ? Tuple () : entry->tuple.ref ()); +} + +void PlaylistData::update_entry_from_scan (PlaylistEntry * entry, ScanRequest * request, int update_flags) +{ + if (! entry->decoder) + entry->decoder = request->decoder; + + if (! entry->tuple.valid () && request->tuple.valid ()) + { + set_entry_tuple (entry, std::move (request->tuple)); + queue_update (Playlist::Metadata, entry->number, 1, update_flags); + } + + if (! entry->decoder || ! entry->tuple.valid ()) + entry->error = request->error; + + if (entry->tuple.state () == Tuple::Initial) + { + entry->tuple.set_state (Tuple::Failed); + queue_update (Playlist::Metadata, entry->number, 1, update_flags); + } +} + +void PlaylistData::update_playback_entry (Tuple && tuple) +{ + /* don't update cuesheet entries with stream metadata */ + if (m_position && ! m_position->tuple.is_set (Tuple::StartTime)) + { + set_entry_tuple (m_position, std::move (tuple)); + queue_update (Playlist::Metadata, m_position->number, 1); + } +} + +bool PlaylistData::entry_needs_rescan (PlaylistEntry * entry, bool need_decoder, bool need_tuple) +{ + if (! strncmp (entry->filename, "stdin://", 8)) // blacklist stdin + return false; + + // check whether requested data (decoder and/or tuple) has been read + return (need_decoder && ! entry->decoder) || (need_tuple && ! entry->tuple.valid ()); +} + +void PlaylistData::reformat_titles () +{ + for (auto & entry : m_entries) + entry->format (); + + queue_update (Playlist::Metadata, 0, m_entries.len ()); +} + +void PlaylistData::reset_tuples (bool selected_only) +{ + for (auto & entry : m_entries) + { + if (! selected_only || entry->selected) + set_entry_tuple (entry.get (), Tuple ()); + } + + queue_update (Playlist::Metadata, 0, m_entries.len ()); + pl_signal_rescan_needed (m_id); +} + +void PlaylistData::reset_tuple_of_file (const char * filename) +{ + bool found = false; + + for (auto & entry : m_entries) + { + if (! strcmp (entry->filename, filename)) + { + set_entry_tuple (entry.get (), Tuple ()); + queue_update (Playlist::Metadata, entry->number, 1); + found = true; + } + } + + if (found) + pl_signal_rescan_needed (m_id); +} + +PlaylistEntry * PlaylistData::find_unselected_focus () +{ + if (! m_focus || ! m_focus->selected) + return m_focus; + + int n_entries = m_entries.len (); + + for (int search = m_focus->number + 1; search < n_entries; search ++) + { + if (! m_entries[search]->selected) + return m_entries[search].get (); + } + + for (int search = m_focus->number; search --;) + { + if (! m_entries[search]->selected) + return m_entries[search].get (); + } + + return nullptr; +} + +PlaylistEntry * PlaylistData::queue_pop () +{ + if (! m_queued.len ()) + return nullptr; + + auto entry = m_queued[0]; + m_queued.remove (0, 1); + entry->queued = false; + + queue_update (Playlist::Selection, entry->number, 1, QueueChanged); + + return entry; +} diff --git a/src/libaudcore/playlist-data.h b/src/libaudcore/playlist-data.h new file mode 100644 index 0000000..c94a94c --- /dev/null +++ b/src/libaudcore/playlist-data.h @@ -0,0 +1,172 @@ +/* + * playlist-data.h + * Copyright 2017 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 PLAYLIST_DATA_H +#define PLAYLIST_DATA_H + +#include "playlist.h" +#include "scanner.h" + +class TupleCompiler; +struct PlaylistEntry; + +class PlaylistData +{ +public: + /* update flags */ + enum { + QueueChanged = (1 << 0), + DelayedUpdate = (1 << 1) + }; + + /* scan status */ + enum ScanStatus { + NotScanning, + ScanActive, + ScanEnding + }; + + struct CompareData { + Playlist::StringCompareFunc filename_compare; + Playlist::TupleCompareFunc tuple_compare; + }; + + PlaylistData (Playlist::ID * m_id, const char * title); + ~PlaylistData (); + + PlaylistEntry * entry_at (int i); + const PlaylistEntry * entry_at (int i) const; + + String entry_filename (int i) const; + PluginHandle * entry_decoder (int i, String * error = nullptr) const; + Tuple entry_tuple (int i, String * error = nullptr) const; + + void cancel_updates (); + void swap_updates (bool & position_changed); + + void insert_items (int at, Index<PlaylistAddItem> && items); + void remove_entries (int at, int number); + + int position () const; + int focus () const; + + bool entry_selected (int entry_num) const; + int n_selected (int at, int number) const; + + void set_focus (int entry_num); + + void select_entry (int entry_num, bool selected); + void select_all (bool selected); + int shift_entries (int entry_num, int distance); + void remove_selected (); + + void sort (const CompareData & data); + void sort_selected (const CompareData & data); + + void reverse_order (); + void randomize_order (); + void reverse_selected (); + void randomize_selected (); + + int queue_get_entry (int at) const; + int queue_find_entry (int entry_num) const; + + void queue_insert (int at, int entry_num); + void queue_insert_selected (int pos); + void queue_remove (int at, int number); + void queue_remove_selected (); + + void set_position (int entry_num); + + bool prev_song (); + bool next_song (bool repeat); + + int next_unscanned_entry (int entry_num) const; + bool entry_needs_rescan (PlaylistEntry * entry, bool need_decoder, bool need_tuple); + ScanRequest * create_scan_request (PlaylistEntry * entry, + ScanRequest::Callback callback, int extra_flags); + void update_entry_from_scan (PlaylistEntry * entry, ScanRequest * request, int update_flags); + void update_playback_entry (Tuple && tuple); + + void reformat_titles (); + void reset_tuples (bool selected_only); + void reset_tuple_of_file (const char * filename); + + Playlist::ID * id () const { return m_id; } + + int n_entries () const { return m_entries.len (); } + int n_queued () const { return m_queued.len (); } + + int64_t total_length () const { return m_total_length; } + int64_t selected_length () const { return m_selected_length; } + + const Playlist::Update & last_update () const { return m_last_update; } + bool update_pending () const { return m_next_update.level != Playlist::NoUpdate; } + + static void update_formatter (); + static void cleanup_formatter (); + +private: + static void delete_entry (PlaylistEntry * entry); + typedef SmartPtr<PlaylistEntry, delete_entry> EntryPtr; + + void number_entries (int at, int length); + void set_entry_tuple (PlaylistEntry * entry, Tuple && tuple); + void queue_update (Playlist::UpdateLevel level, int at, int count, int flags = 0); + void queue_position_change (); + + static void sort_entries (Index<EntryPtr> & entries, const CompareData & data); + + void set_position (PlaylistEntry * entry, bool update_shuffle); + + bool shuffle_prev (); + bool shuffle_next (); + void shuffle_reset (); + + bool next_song_with_hint (bool repeat, int hint); + + PlaylistEntry * find_unselected_focus (); + PlaylistEntry * queue_pop (); + +public: + bool modified; + ScanStatus scan_status; + String filename, title; + int resume_time; + +private: + Playlist::ID * m_id; + Index<EntryPtr> m_entries; + PlaylistEntry * m_position, * m_focus; + int m_selected_count; + int m_last_shuffle_num; + Index<PlaylistEntry *> m_queued; + int64_t m_total_length, m_selected_length; + Playlist::Update m_last_update, m_next_update; + bool m_position_changed; +}; + +/* callbacks or "signals" (in the QObject sense) */ +void pl_signal_entry_deleted (PlaylistEntry * entry); +void pl_signal_position_changed (Playlist::ID * id); +void pl_signal_update_queued (Playlist::ID * id, Playlist::UpdateLevel level, int flags); +void pl_signal_rescan_needed (Playlist::ID * id); +void pl_signal_playlist_deleted (Playlist::ID * id); + +#endif // PLAYLIST_DATA_H diff --git a/src/libaudcore/playlist-files.cc b/src/libaudcore/playlist-files.cc index 872e9ff..29cbff0 100644 --- a/src/libaudcore/playlist-files.cc +++ b/src/libaudcore/playlist-files.cc @@ -26,7 +26,7 @@ #include "plugins-internal.h" #include "runtime.h" -EXPORT bool aud_filename_is_playlist (const char * filename) +EXPORT bool Playlist::filename_is_playlist (const char * filename) { StringBuf ext = uri_get_extension (filename); @@ -93,7 +93,7 @@ bool playlist_load (const char * filename, String & title, Index<PlaylistAddItem // 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) +bool PlaylistEx::insert_flat_playlist (const char * filename) const { String title; Index<PlaylistAddItem> items; @@ -101,26 +101,26 @@ bool playlist_insert_playlist_raw (int list, int at, const char * filename) if (! playlist_load (filename, title, items)) return false; - if (title && ! aud_playlist_entry_count (list)) - aud_playlist_set_title (list, title); + if (title) + set_title (title); - playlist_entry_insert_batch_raw (list, at, std::move (items)); + insert_flat_items (0, std::move (items)); return true; } -EXPORT bool aud_playlist_save (int list, const char * filename, Playlist::GetMode mode) +EXPORT bool Playlist::save_to_file (const char * filename, GetMode mode) const { - String title = aud_playlist_get_title (list); + String title = get_title (); Index<PlaylistAddItem> items; - items.insert (0, aud_playlist_entry_count (list)); + items.insert (0, n_entries ()); int i = 0; for (PlaylistAddItem & item : items) { - item.filename = aud_playlist_entry_get_filename (list, i); - item.tuple = aud_playlist_entry_get_tuple (list, i, mode); + item.filename = entry_filename (i); + item.tuple = entry_tuple (i, mode); item.tuple.delete_fallbacks (); i ++; } @@ -153,7 +153,7 @@ EXPORT bool aud_playlist_save (int list, const char * filename, Playlist::GetMod return false; } -EXPORT Index<Playlist::SaveFormat> aud_playlist_save_formats () +EXPORT Index<Playlist::SaveFormat> Playlist::save_formats () { Index<Playlist::SaveFormat> formats; diff --git a/src/libaudcore/playlist-internal.h b/src/libaudcore/playlist-internal.h index 781cd4c..bbccd6b 100644 --- a/src/libaudcore/playlist-internal.h +++ b/src/libaudcore/playlist-internal.h @@ -33,33 +33,44 @@ struct DecodeInfo String error; }; +/* extended handle for accessing internal playlist functions */ +class PlaylistEx : public Playlist +{ +public: + PlaylistEx (Playlist::ID * id = nullptr) : + Playlist (id) {} + PlaylistEx (Playlist playlist) : + Playlist (playlist) {} + + int stamp () const; + + static Playlist insert_with_stamp (int at, int stamp); + + bool get_modified () const; + void set_modified (bool modified) const; + + bool insert_flat_playlist (const char * filename) const; + void insert_flat_items (int at, Index<PlaylistAddItem> && items) const; +}; + /* playlist.cc */ void playlist_init (); void playlist_enable_scan (bool enable); +void playlist_clear_updates (); void playlist_end (); -void playlist_insert_with_id (int at, int id); -void playlist_set_modified (int playlist, bool modified); -bool playlist_get_modified (int playlist); - void playlist_load_state (); void playlist_save_state (); -void playlist_entry_insert_batch_raw (int playlist, int at, Index<PlaylistAddItem> && items); - -bool playlist_prev_song (int playlist); -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 (); +void playlist_cache_clear (void * = nullptr); /* 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); /* playlist-utils.cc */ void load_playlists (); diff --git a/src/libaudcore/playlist-utils.cc b/src/libaudcore/playlist-utils.cc index d1f8ffd..c017261 100644 --- a/src/libaudcore/playlist-utils.cc +++ b/src/libaudcore/playlist-utils.cc @@ -67,51 +67,27 @@ static int tuple_compare_int (const Tuple & a, const Tuple & b, Tuple::Field fie } static int tuple_compare_title (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::Title); -} - + { return tuple_compare_string (a, b, Tuple::Title); } static int tuple_compare_album (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::Album); -} - + { return tuple_compare_string (a, b, Tuple::Album); } static int tuple_compare_artist (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::Artist); -} - + { return tuple_compare_string (a, b, Tuple::Artist); } static int tuple_compare_album_artist (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::AlbumArtist); -} - + { return tuple_compare_string (a, b, Tuple::AlbumArtist); } static int tuple_compare_date (const Tuple & a, const Tuple & b) -{ - return tuple_compare_int (a, b, Tuple::Year); -} - + { return tuple_compare_int (a, b, Tuple::Year); } static int tuple_compare_genre (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::Genre); -} - + { return tuple_compare_string (a, b, Tuple::Genre); } static int tuple_compare_track (const Tuple & a, const Tuple & b) -{ - return tuple_compare_int (a, b, Tuple::Track); -} - + { return tuple_compare_int (a, b, Tuple::Track); } static int tuple_compare_formatted_title (const Tuple & a, const Tuple & b) -{ - return tuple_compare_string (a, b, Tuple::FormattedTitle); -} - + { return tuple_compare_string (a, b, Tuple::FormattedTitle); } static int tuple_compare_length (const Tuple & a, const Tuple & b) -{ - return tuple_compare_int (a, b, Tuple::Length); -} + { return tuple_compare_int (a, b, Tuple::Length); } +static int tuple_compare_comment (const Tuple & a, const Tuple & b) + { return tuple_compare_string (a, b, Tuple::Comment); } -static const PlaylistStringCompareFunc filename_comparisons[] = { +static const Playlist::StringCompareFunc filename_comparisons[] = { str_compare_encoded, // path filename_compare_basename, // filename nullptr, // title @@ -122,10 +98,11 @@ static const PlaylistStringCompareFunc filename_comparisons[] = { nullptr, // genre nullptr, // track nullptr, // formatted title - nullptr // length + nullptr, // length + nullptr // comment }; -static const PlaylistTupleCompareFunc tuple_comparisons[] = { +static const Playlist::TupleCompareFunc tuple_comparisons[] = { nullptr, // path nullptr, // filename tuple_compare_title, @@ -136,100 +113,101 @@ static const PlaylistTupleCompareFunc tuple_comparisons[] = { tuple_compare_genre, tuple_compare_track, tuple_compare_formatted_title, - tuple_compare_length + tuple_compare_length, + tuple_compare_comment }; static_assert (aud::n_elems (filename_comparisons) == Playlist::n_sort_types && aud::n_elems (tuple_comparisons) == Playlist::n_sort_types, "Update playlist comparison functions"); -EXPORT void aud_playlist_sort_by_scheme (int playlist, Playlist::SortType scheme) +EXPORT void Playlist::sort_entries (SortType scheme) const { if (filename_comparisons[scheme]) - aud_playlist_sort_by_filename (playlist, filename_comparisons[scheme]); + sort_by_filename (filename_comparisons[scheme]); else if (tuple_comparisons[scheme]) - aud_playlist_sort_by_tuple (playlist, tuple_comparisons[scheme]); + sort_by_tuple (tuple_comparisons[scheme]); } -EXPORT void aud_playlist_sort_selected_by_scheme (int playlist, Playlist::SortType scheme) +EXPORT void Playlist::sort_selected (SortType scheme) const { if (filename_comparisons[scheme]) - aud_playlist_sort_selected_by_filename (playlist, filename_comparisons[scheme]); + sort_selected_by_filename (filename_comparisons[scheme]); else if (tuple_comparisons[scheme]) - aud_playlist_sort_selected_by_tuple (playlist, tuple_comparisons[scheme]); + sort_selected_by_tuple (tuple_comparisons[scheme]); } /* FIXME: this considers empty fields as duplicates */ -EXPORT void aud_playlist_remove_duplicates_by_scheme (int playlist, Playlist::SortType scheme) +EXPORT void Playlist::remove_duplicates (SortType scheme) const { - int entries = aud_playlist_entry_count (playlist); + int entries = n_entries (); if (entries < 1) return; - aud_playlist_select_all (playlist, false); + select_all (false); if (filename_comparisons[scheme]) { - PlaylistStringCompareFunc compare = filename_comparisons[scheme]; + StringCompareFunc compare = filename_comparisons[scheme]; - aud_playlist_sort_by_filename (playlist, compare); - String last = aud_playlist_entry_get_filename (playlist, 0); + sort_by_filename (compare); + String last = entry_filename (0); - for (int count = 1; count < entries; count ++) + for (int i = 1; i < entries; i ++) { - String current = aud_playlist_entry_get_filename (playlist, count); + String current = entry_filename (i); if (compare (last, current) == 0) - aud_playlist_entry_set_selected (playlist, count, true); + select_entry (i, true); last = current; } } else if (tuple_comparisons[scheme]) { - PlaylistTupleCompareFunc compare = tuple_comparisons[scheme]; + TupleCompareFunc compare = tuple_comparisons[scheme]; - aud_playlist_sort_by_tuple (playlist, compare); - Tuple last = aud_playlist_entry_get_tuple (playlist, 0); + sort_by_tuple (compare); + Tuple last = entry_tuple (0); - for (int count = 1; count < entries; count ++) + for (int i = 1; i < entries; i ++) { - Tuple current = aud_playlist_entry_get_tuple (playlist, count); + Tuple current = entry_tuple (i); if (last.valid () && current.valid () && compare (last, current) == 0) - aud_playlist_entry_set_selected (playlist, count, true); + select_entry (i, true); last = std::move (current); } } - aud_playlist_delete_selected (playlist); + remove_selected (); } -EXPORT void aud_playlist_remove_failed (int playlist) +EXPORT void Playlist::remove_unavailable () const { - int entries = aud_playlist_entry_count (playlist); + int entries = n_entries (); - aud_playlist_select_all (playlist, false); + select_all (false); - for (int count = 0; count < entries; count ++) + for (int i = 0; i < entries; i ++) { - String filename = aud_playlist_entry_get_filename (playlist, count); + String filename = entry_filename (i); /* 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); + select_entry (i, true); } - aud_playlist_delete_selected (playlist); + remove_selected (); } -EXPORT void aud_playlist_select_by_patterns (int playlist, const Tuple & patterns) +EXPORT void Playlist::select_by_patterns (const Tuple & patterns) const { - int entries = aud_playlist_entry_count (playlist); + int entries = n_entries (); - aud_playlist_select_all (playlist, true); + select_all (true); for (Tuple::Field field : {Tuple::Title, Tuple::Album, Tuple::Artist, Tuple::Basename}) { @@ -240,16 +218,16 @@ EXPORT void aud_playlist_select_by_patterns (int playlist, const Tuple & pattern G_REGEX_CASELESS, (GRegexMatchFlags) 0, nullptr))) continue; - for (int entry = 0; entry < entries; entry ++) + for (int i = 0; i < entries; i ++) { - if (! aud_playlist_entry_get_selected (playlist, entry)) + if (! entry_selected (i)) continue; - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry); + Tuple tuple = entry_tuple (i); String string = tuple.get_str (field); if (! string || ! g_regex_match (regex, string, (GRegexMatchFlags) 0, nullptr)) - aud_playlist_entry_set_selected (playlist, entry, false); + select_entry (i, false); } g_regex_unref (regex); @@ -279,9 +257,9 @@ static void load_playlists_real () if (! g_file_test (path, G_FILE_TEST_EXISTS)) break; - aud_playlist_insert (count); - playlist_insert_playlist_raw (count, 0, filename_to_uri (path)); - playlist_set_modified (count, true); + PlaylistEx playlist = Playlist::insert_playlist (count); + playlist.insert_flat_playlist (filename_to_uri (path)); + playlist.set_modified (true); } /* unique ID-based naming scheme */ @@ -308,24 +286,19 @@ static void load_playlists_real () if (! g_file_test (path, G_FILE_TEST_EXISTS)) path.steal (filename_build ({folder, name2})); - playlist_insert_with_id (count + i, atoi (number)); - playlist_insert_playlist_raw (count + i, 0, filename_to_uri (path)); - playlist_set_modified (count + i, false); - - if (g_str_has_suffix (path, ".xspf")) - playlist_set_modified (count + i, true); + PlaylistEx playlist = PlaylistEx::insert_with_stamp (count + i, atoi (number)); + playlist.insert_flat_playlist (filename_to_uri (path)); + playlist.set_modified (g_str_has_suffix (path, ".xspf")); } DONE: - if (! aud_playlist_count ()) - aud_playlist_insert (0); - - aud_playlist_set_active (0); + if (! Playlist::n_playlists ()) + Playlist::insert_playlist (0); } static void save_playlists_real () { - int lists = aud_playlist_count (); + int lists = Playlist::n_playlists (); const char * folder = aud_get_path (AudPath::PlaylistDir); /* save playlists */ @@ -335,15 +308,15 @@ static void save_playlists_real () for (int i = 0; i < lists; i ++) { - int id = aud_playlist_get_unique_id (i); - StringBuf number = int_to_str (id); + PlaylistEx playlist = Playlist::by_index (i); + StringBuf number = int_to_str (playlist.stamp ()); StringBuf name = str_concat ({number, ".audpl"}); - if (playlist_get_modified (i)) + if (playlist.get_modified ()) { StringBuf path = filename_build ({folder, name}); - aud_playlist_save (i, filename_to_uri (path), Playlist::NoWait); - playlist_set_modified (i, false); + playlist.save_to_file (filename_to_uri (path), Playlist::NoWait); + playlist.set_modified (false); } order.append (String (number)); diff --git a/src/libaudcore/playlist.cc b/src/libaudcore/playlist.cc index f2bd034..fff37e1 100644 --- a/src/libaudcore/playlist.cc +++ b/src/libaudcore/playlist.cc @@ -1,6 +1,6 @@ /* * playlist.cc - * Copyright 2009-2014 John Lindgren + * Copyright 2009-2017 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -17,11 +17,7 @@ * the use of this software. */ -// uncomment to print a backtrace when scanning blocks the main thread -// #define WARN_BLOCKED - #include "playlist-internal.h" -#include "runtime.h" #include <assert.h> #include <pthread.h> @@ -35,25 +31,13 @@ #include "drct.h" #include "hook.h" #include "i18n.h" -#include "interface.h" #include "internal.h" #include "list.h" #include "mainloop.h" #include "multihash.h" -#include "objects.h" #include "parse.h" -#include "plugins.h" -#include "scanner.h" -#include "tuple.h" -#include "tuple-compiler.h" - -#ifdef WARN_BLOCKED -#include <execinfo.h> -#include <stdio.h> -#include <stdlib.h> -#endif - -using namespace Playlist; +#include "playlist-data.h" +#include "runtime.h" enum { ResumeStop, @@ -61,15 +45,18 @@ enum { ResumePause }; -enum PlaybackChange { - NoChange, - NextSong, - PlaybackStopped +/* update hooks */ +enum { + SetActive = (1 << 0), + SetPlaying = (1 << 1), + PlaybackBegin = (1 << 2), + PlaybackStop = (1 << 3) }; -enum { - QueueChanged = (1 << 0), - DelayedUpdate = (1 << 1) +enum class UpdateState { + None, + Delayed, + Queued }; #define STATE_FILE "playlist-state" @@ -82,67 +69,21 @@ enum { return __VA_ARGS__; \ } while (0) -#define ENTER_GET_PLAYLIST(...) ENTER; \ - PlaylistData * playlist = lookup_playlist (playlist_num); \ +#define ENTER_GET_PLAYLIST(...) \ + ENTER; \ + PlaylistData * playlist = m_id ? m_id->data : nullptr; \ if (! playlist) \ - RETURN (__VA_ARGS__); + RETURN (__VA_ARGS__) -#define ENTER_GET_ENTRY(...) ENTER_GET_PLAYLIST (__VA_ARGS__); \ - Entry * entry = lookup_entry (playlist, entry_num); \ - if (! entry) \ - RETURN (__VA_ARGS__); +#define SIMPLE_WRAPPER(type, failcode, func, ...) \ + ENTER_GET_PLAYLIST (failcode); \ + type retval = playlist->func (__VA_ARGS__); \ + RETURN (retval) -struct UniqueID -{ - constexpr UniqueID (int val) : - val (val) {} - - operator int () const - { return val; } - - unsigned hash () const - { return int32_hash (val); } - -private: - int val; -}; - -struct Entry { - Entry (PlaylistAddItem && item); - ~Entry (); - - void format (); - void set_tuple (Tuple && new_tuple); - - String filename; - PluginHandle * decoder; - Tuple tuple; - String error; - int number; - int length; - int shuffle_num; - bool selected, queued; -}; - -struct PlaylistData { - PlaylistData (int id); - ~PlaylistData (); - - void set_entry_tuple (Entry * entry, Tuple && tuple); - - int number, unique_id; - String filename, title; - bool modified; - Index<SmartPtr<Entry>> entries; - Entry * position, * focus; - int selected_count; - int last_shuffle_num; - Index<Entry *> queued; - int64_t total_length, selected_length; - bool scanning, scan_ending; - Update next_update, last_update; - int resume_time; -}; +#define SIMPLE_VOID_WRAPPER(func, ...) \ + ENTER_GET_PLAYLIST (); \ + playlist->func (__VA_ARGS__); \ + LEAVE static const char * const default_title = N_("New Playlist"); static const char * const temp_title = N_("Now Playing"); @@ -150,31 +91,44 @@ static const char * const temp_title = N_("Now Playing"); static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; -#ifdef WARN_BLOCKED -static pthread_t main_thread; -#endif +/* + * Each playlist is associated with its own ID struct, which contains a unique + * integer "stamp" (this is the source of the internal filenames 1000.audpl, + * 1001.audpl, etc.) The ID struct also serves as a "weak" pointer to the + * actual data, and persists even after the playlist itself is destroyed. + * The IDs are stored in a hash table, allowing lookup by stamp. + * + * In brief: Playlist (public handle) + * points to -> + * Playlist::ID (unique ID / weak pointer) + * points to -> + * PlaylistData (actual playlist data) + */ +struct Playlist::ID +{ + int stamp; // integer stamp, determines filename + int index; // display order + PlaylistData * data; // pointer to actual playlist data +}; -/* The unique ID table contains pointers to PlaylistData for ID's in use and nullptr - * for "dead" (previously used and therefore unavailable) ID's. */ -static SimpleHash<UniqueID, PlaylistData *> unique_id_table; -static int next_unique_id = 1000; +static SimpleHash<IntHashKey, Playlist::ID> id_table; +static int next_stamp = 1000; static Index<SmartPtr<PlaylistData>> playlists; -static PlaylistData * active_playlist = nullptr; -static PlaylistData * playing_playlist = nullptr; +static Playlist::ID * active_id = nullptr; +static Playlist::ID * playing_id = nullptr; static int resume_playlist = -1; static bool resume_paused = false; -static bool metadata_fallbacks = false; -static TupleCompiler title_formatter; - static QueuedFunc queued_update; -static UpdateLevel update_level; -static bool update_delayed; +static Playlist::UpdateLevel update_level; +static int update_hooks; +static UpdateState update_state; struct ScanItem : public ListNode { - ScanItem (PlaylistData * playlist, Entry * entry, ScanRequest * request, bool for_playback) : + ScanItem (PlaylistData * playlist, PlaylistEntry * entry, + ScanRequest * request, bool for_playback) : playlist (playlist), entry (entry), request (request), @@ -182,7 +136,7 @@ struct ScanItem : public ListNode handled_by_playback (false) {} PlaylistData * playlist; - Entry * entry; + PlaylistEntry * entry; ScanRequest * request; bool for_playback; bool handled_by_playback; @@ -193,301 +147,156 @@ static int scan_playlist, scan_row; static List<ScanItem> scan_list; static void scan_finish (ScanRequest * request); -static void scan_cancel (Entry * entry); -static void scan_queue_playlist (PlaylistData * playlist); +static void scan_cancel (PlaylistEntry * entry); static void scan_restart (); -static bool next_song_locked (PlaylistData * playlist, bool repeat, int hint); - -static void playlist_reformat_titles (); -static void playlist_trigger_scan (); - -void Entry::format () +/* creates a new playlist with the requested stamp (if not already in use) */ +static Playlist::ID * create_playlist (int stamp) { - tuple.delete_fallbacks (); + Playlist::ID * id; - if (metadata_fallbacks) - tuple.generate_fallbacks (); + if (stamp >= 0 && ! id_table.lookup (stamp)) + id = id_table.add (stamp, {stamp, -1, nullptr}); else - tuple.generate_title (); - - title_formatter.format (tuple); -} - -void Entry::set_tuple (Tuple && new_tuple) -{ - /* 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; - - error = String (); - - if (! new_tuple.valid ()) - new_tuple.set_filename (filename); - - length = aud::max (0, new_tuple.get_int (Tuple::Length)); - tuple = std::move (new_tuple); - - format (); -} - -void PlaylistData::set_entry_tuple (Entry * entry, Tuple && tuple) -{ - total_length -= entry->length; - if (entry->selected) - selected_length -= entry->length; - - entry->set_tuple (std::move (tuple)); - - total_length += entry->length; - if (entry->selected) - selected_length += entry->length; -} - -Entry::Entry (PlaylistAddItem && item) : - filename (item.filename), - decoder (item.decoder), - number (-1), - length (0), - shuffle_num (0), - selected (false), - queued (false) -{ - set_tuple (std::move (item.tuple)); -} - -Entry::~Entry () -{ - scan_cancel (this); -} - -static int new_unique_id (int preferred) -{ - if (preferred >= 0 && ! unique_id_table.lookup (preferred)) - return preferred; - - while (unique_id_table.lookup (next_unique_id)) - next_unique_id ++; + { + while (id_table.lookup (next_stamp)) + next_stamp ++; - return next_unique_id ++; -} + id = id_table.add (next_stamp, {next_stamp, -1, nullptr}); + } -PlaylistData::PlaylistData (int id) : - number (-1), - unique_id (new_unique_id (id)), - title (_(default_title)), - modified (true), - position (nullptr), - focus (nullptr), - selected_count (0), - last_shuffle_num (0), - total_length (0), - selected_length (0), - scanning (false), - scan_ending (false), - next_update (), - last_update (), - resume_time (0) -{ - unique_id_table.add (unique_id, (PlaylistData *) this); -} + id->data = new PlaylistData (id, _(default_title)); -PlaylistData::~PlaylistData () -{ - unique_id_table.add (unique_id, nullptr); + return id; } static void number_playlists (int at, int length) { for (int i = at; i < at + length; i ++) - playlists[i]->number = i; -} - -static PlaylistData * lookup_playlist (int i) -{ - return (i >= 0 && i < playlists.len ()) ? playlists[i].get () : nullptr; -} - -static void number_entries (PlaylistData * p, int at, int length) -{ - for (int i = at; i < at + length; i ++) - p->entries[i]->number = i; -} - -static Entry * lookup_entry (PlaylistData * p, int i) -{ - return (i >= 0 && i < p->entries.len ()) ? p->entries[i].get () : nullptr; + playlists[i]->id ()->index = i; } static void update (void *) { ENTER; + int hooks = update_hooks; + auto level = update_level; + + Index<PlaylistEx> position_change_list; + for (auto & p : playlists) { - p->last_update = p->next_update; - p->next_update = Update (); + bool position_changed = false; + p->swap_updates (position_changed); + + if (position_changed) + position_change_list.append (p->id ()); } - UpdateLevel level = update_level; - update_level = NoUpdate; - update_delayed = false; + update_hooks = 0; + update_level = Playlist::NoUpdate; + update_state = UpdateState::None; LEAVE; - hook_call ("playlist update", aud::to_ptr (level)); + if (level != Playlist::NoUpdate) + hook_call ("playlist update", aud::to_ptr (level)); + + for (PlaylistEx playlist : position_change_list) + hook_call ("playlist position", aud::to_ptr (playlist)); + + if ((hooks & SetActive)) + hook_call ("playlist activate", nullptr); + if ((hooks & SetPlaying)) + hook_call ("playlist set playing", nullptr); + if ((hooks & PlaybackBegin)) + hook_call ("playback begin", nullptr); + if ((hooks & PlaybackStop)) + hook_call ("playback stop", nullptr); } -static void queue_update (UpdateLevel level, PlaylistData * p, int at, int count, int flags = 0) +static void queue_update_hooks (int hooks) { - if (p) - { - if (level == Structure) - scan_queue_playlist (p); + if ((hooks & PlaybackBegin)) + update_hooks &= ~PlaybackStop; + if ((hooks & PlaybackStop)) + update_hooks &= ~PlaybackBegin; - if (level >= Metadata) - { - if (p == playing_playlist && p->position) - playback_set_info (p->position->number, p->position->tuple.ref ()); + update_hooks |= hooks; - p->modified = true; - } - - if (p->next_update.level) - { - p->next_update.level = aud::max (p->next_update.level, level); - p->next_update.before = aud::min (p->next_update.before, at); - p->next_update.after = aud::min (p->next_update.after, p->entries.len () - at - count); - } - else - { - p->next_update.level = level; - p->next_update.before = at; - p->next_update.after = p->entries.len () - at - count; - } - - if ((flags & QueueChanged)) - p->next_update.queue_changed = true; + if (update_state < UpdateState::Queued) + { + queued_update.queue (update, nullptr); + update_state = UpdateState::Queued; } +} - if (level == Structure) +static void queue_global_update (Playlist::UpdateLevel level, int flags = 0) +{ + if (level == Playlist::Structure) scan_restart (); - // only allow delayed update if a scan is still in progress - if ((flags & DelayedUpdate) && scan_enabled && p && (p->scanning || p->scan_ending)) + if ((flags & PlaylistData::DelayedUpdate)) { - if (! update_level) + if (update_state < UpdateState::Delayed) { queued_update.queue (250, update, nullptr); - update_delayed = true; + update_state = UpdateState::Delayed; } } else { - if (! update_level || update_delayed) + if (update_state < UpdateState::Queued) { queued_update.queue (update, nullptr); - update_delayed = false; + update_state = UpdateState::Queued; } } update_level = aud::max (update_level, level); } -EXPORT bool aud_playlist_update_pending (int playlist_num) -{ - if (playlist_num >= 0) - { - ENTER_GET_PLAYLIST (false); - bool pending = playlist->next_update.level ? true : false; - RETURN (pending); - } - else - { - ENTER; - bool pending = update_level ? true : false; - RETURN (pending); - } -} - -EXPORT Update aud_playlist_update_detail (int playlist_num) +EXPORT bool Playlist::update_pending_any () { - ENTER_GET_PLAYLIST (Update ()); - Update update = playlist->last_update; - RETURN (update); + ENTER; + bool pending = (update_level != Playlist::NoUpdate); + RETURN (pending); } -EXPORT bool aud_playlist_scan_in_progress (int playlist_num) +EXPORT bool Playlist::scan_in_progress () const { - if (playlist_num >= 0) - { - ENTER_GET_PLAYLIST (false); - bool scanning = playlist->scanning || playlist->scan_ending; - RETURN (scanning); - } - else - { - ENTER; - - bool scanning = false; - for (auto & p : playlists) - { - if (p->scanning || p->scan_ending) - scanning = true; - } - - RETURN (scanning); - } + ENTER_GET_PLAYLIST (false); + bool scanning = (playlist->scan_status != PlaylistData::NotScanning); + RETURN (scanning); } -static ScanItem * scan_list_find_playlist (PlaylistData * playlist) +EXPORT bool Playlist::scan_in_progress_any () { - for (ScanItem * item = scan_list.head (); item; item = scan_list.next (item)) - { - if (item->playlist == playlist) - return item; - } - - return nullptr; -} + ENTER; -static ScanItem * scan_list_find_entry (Entry * entry) -{ - for (ScanItem * item = scan_list.head (); item; item = scan_list.next (item)) + bool scanning = false; + for (auto & p : playlists) { - if (item->entry == entry) - return item; + if (p->scan_status != PlaylistData::NotScanning) + scanning = true; } - return nullptr; + RETURN (scanning); } -static ScanItem * scan_list_find_request (ScanRequest * request) +static ScanItem * scan_list_find_entry (PlaylistEntry * entry) { - for (ScanItem * item = scan_list.head (); item; item = scan_list.next (item)) - { - if (item->request == request) - return item; - } + auto match = [entry] (const ScanItem & item) + { return item.entry == entry; }; - return nullptr; + return scan_list.find (match); } -static void scan_queue_entry (PlaylistData * playlist, Entry * entry, bool for_playback = false) +static void scan_queue_entry (PlaylistData * playlist, PlaylistEntry * entry, bool for_playback = false) { - int flags = 0; - if (! entry->tuple.valid ()) - flags |= SCAN_TUPLE; - if (for_playback) - flags |= (SCAN_IMAGE | SCAN_FILE); - - /* 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 ()); + int extra_flags = for_playback ? (SCAN_IMAGE | SCAN_FILE) : 0; + auto request = playlist->create_scan_request (entry, scan_finish, extra_flags); scan_list.append (new ScanItem (playlist, entry, request, for_playback)); @@ -498,33 +307,37 @@ static void scan_queue_entry (PlaylistData * playlist, Entry * entry, bool for_p static void scan_reset_playback () { - for (ScanItem * item = scan_list.head (); item; item = scan_list.next (item)) - { - if (item->for_playback) - { - item->for_playback = false; + auto match = [] (const ScanItem & item) + { return item.for_playback; }; - /* if playback was canceled before the entry was scanned, requeue it */ - if (! item->handled_by_playback) - scanner_request (item->request); - } - } + ScanItem * item = scan_list.find (match); + if (! item) + return; + + item->for_playback = false; + + /* if playback was canceled before the entry was scanned, requeue it */ + if (! item->handled_by_playback) + scanner_request (item->request); } static void scan_check_complete (PlaylistData * playlist) { - if (! playlist->scan_ending || scan_list_find_playlist (playlist)) + auto match = [playlist] (const ScanItem & item) + { return item.playlist == playlist; }; + + if (playlist->scan_status != PlaylistData::ScanEnding || scan_list.find (match)) return; - playlist->scan_ending = false; + playlist->scan_status = PlaylistData::NotScanning; - if (update_delayed) + if (update_state == UpdateState::Delayed) { queued_update.queue (update, nullptr); - update_delayed = false; + update_state = UpdateState::Queued; } - event_queue_cancel ("playlist scan complete", nullptr); + event_queue_cancel ("playlist scan complete"); event_queue ("playlist scan complete", nullptr); } @@ -537,24 +350,25 @@ static bool scan_queue_next_entry () { PlaylistData * playlist = playlists[scan_playlist].get (); - if (playlist->scanning) + if (playlist->scan_status == PlaylistData::ScanActive) { - while (scan_row < playlist->entries.len ()) + while (1) { - Entry * entry = playlist->entries[scan_row ++].get (); + scan_row = playlist->next_unscanned_entry (scan_row); + if (scan_row < 0) + break; - // blacklist stdin - if (entry->tuple.state () == Tuple::Initial && - ! scan_list_find_entry (entry) && - strncmp (entry->filename, "stdin://", 8)) + auto entry = playlist->entry_at (scan_row); + if (! scan_list_find_entry (entry)) { scan_queue_entry (playlist, entry); return true; } + + scan_row ++; } - playlist->scanning = false; - playlist->scan_ending = true; + playlist->scan_status = PlaylistData::ScanEnding; scan_check_complete (playlist); } @@ -586,32 +400,24 @@ static void scan_finish (ScanRequest * request) { ENTER; - ScanItem * item = scan_list_find_request (request); + auto match = [request] (const ScanItem & item) + { return item.request == request; }; + + ScanItem * item = scan_list.find (match); if (! item) RETURN (); PlaylistData * playlist = item->playlist; - Entry * entry = item->entry; + PlaylistEntry * entry = item->entry; scan_list.remove (item); - if (! entry->decoder) - entry->decoder = request->decoder; - - if (! entry->tuple.valid () && request->tuple.valid ()) - { - playlist->set_entry_tuple (entry, std::move (request->tuple)); - queue_update (Metadata, playlist, entry->number, 1, DelayedUpdate); - } + // only use delayed update if a scan is still in progress + int update_flags = 0; + if (scan_enabled && playlist->scan_status != PlaylistData::NotScanning) + update_flags = PlaylistData::DelayedUpdate; - 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); - } + playlist->update_entry_from_scan (entry, request, update_flags); delete item; @@ -623,7 +429,7 @@ static void scan_finish (ScanRequest * request) LEAVE; } -static void scan_cancel (Entry * entry) +static void scan_cancel (PlaylistEntry * entry) { ScanItem * item = scan_list_find_entry (entry); if (! item) @@ -633,12 +439,6 @@ static void scan_cancel (Entry * entry) delete (item); } -static void scan_queue_playlist (PlaylistData * playlist) -{ - playlist->scanning = true; - playlist->scan_ending = false; -} - static void scan_restart () { scan_playlist = 0; @@ -646,52 +446,25 @@ static void scan_restart () scan_schedule (); } -#ifdef WARN_BLOCKED -static void warn_main_thread_blocked () -{ - printf ("\nMain thread blocked, backtrace:\n"); - - void * syms[100]; - int n_syms = backtrace (syms, aud::n_elems (syms)); - char * * names = backtrace_symbols (syms, n_syms); - - for (int i = 0; i < n_syms; i ++) - printf ("%d. %s\n", i, names[i]); - - free (names); -} -#endif - /* mutex may be unlocked during the call */ -static Entry * get_entry (int playlist_num, int entry_num, - bool need_decoder, bool need_tuple) +static void wait_for_entry (PlaylistData * playlist, int entry_num, bool need_decoder, bool need_tuple) { -#ifdef WARN_BLOCKED - if ((need_decoder || need_tuple) && pthread_self () == main_thread) - warn_main_thread_blocked (); -#endif - bool scan_started = false; while (1) { - PlaylistData * playlist = lookup_playlist (playlist_num); - Entry * entry = playlist ? lookup_entry (playlist, entry_num) : nullptr; + PlaylistEntry * entry = playlist->entry_at (entry_num); - // check whether entry was deleted; also blacklist stdin - if (! entry || ! strncmp (entry->filename, "stdin://", 8)) - return entry; - - // check whether requested data (decoder and/or tuple) has been read - if ((! need_decoder || entry->decoder) && (! need_tuple || entry->tuple.valid ())) - return entry; + // check whether entry is deleted or has already been scanned + if (! entry || ! playlist->entry_needs_rescan (entry, need_decoder, need_tuple)) + return; // start scan if not already running ... if (! scan_list_find_entry (entry)) { // ... but only once if (scan_started) - return entry; + return; scan_queue_entry (playlist, entry); } @@ -702,20 +475,23 @@ static Entry * get_entry (int playlist_num, int entry_num, } } -static void start_playback (int seek_time, bool pause) +static void start_playback_locked (int seek_time, bool pause) { art_clear_current (); scan_reset_playback (); playback_play (seek_time, pause); + auto playlist = playing_id->data; + auto entry = playlist->entry_at (playlist->position ()); + // 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); + scan_cancel (entry); + scan_queue_entry (playlist, entry, true); } -static void stop_playback () +static void stop_playback_locked () { art_clear_current (); scan_reset_playback (); @@ -723,1478 +499,576 @@ static void stop_playback () playback_stop (); } -void playlist_init () -{ - srand (time (nullptr)); - -#ifdef WARN_BLOCKED - main_thread = pthread_self (); -#endif - - ENTER; - - update_level = NoUpdate; - update_delayed = false; - scan_enabled = false; - scan_playlist = scan_row = 0; - - LEAVE; - - /* initialize title formatter */ - playlist_reformat_titles (); - - 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); -} - -void playlist_enable_scan (bool enable) -{ - ENTER; - - scan_enabled_nominal = enable; - scan_enabled = scan_enabled_nominal && ! aud_get_bool (nullptr, "metadata_on_play"); - scan_restart (); - - LEAVE; -} - -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 */ - assert (! playing_playlist); - - queued_update.stop (); - - active_playlist = nullptr; - resume_playlist = -1; - resume_paused = false; - - playlists.clear (); - unique_id_table.clear (); - - title_formatter.reset (); - - LEAVE; -} - -EXPORT int aud_playlist_count () -{ - ENTER; - int count = playlists.len (); - RETURN (count); -} - -void playlist_insert_with_id (int at, int id) -{ - ENTER; - - if (at < 0 || at > playlists.len ()) - at = playlists.len (); - - auto playlist = new PlaylistData (id); - playlists.insert (at, 1); - playlists[at].capture (playlist); - - number_playlists (at, playlists.len () - at); - - queue_update (Structure, playlist, 0, 0); - LEAVE; -} - -EXPORT void aud_playlist_insert (int at) +void pl_signal_entry_deleted (PlaylistEntry * entry) { - playlist_insert_with_id (at, -1); + scan_cancel (entry); } -EXPORT void aud_playlist_reorder (int from, int to, int count) +void pl_signal_position_changed (Playlist::ID * id) { - ENTER; - - if (from < 0 || from + count > playlists.len () || to < 0 || to + - count > playlists.len () || count < 0) - RETURN (); - - Index<SmartPtr<PlaylistData>> displaced; - - if (to < from) - displaced.move_from (playlists, to, -1, from - to, true, false); - else - displaced.move_from (playlists, from + count, -1, to - from, true, false); - - playlists.shift (from, to, count); - - if (to < from) + if (update_state < UpdateState::Queued) { - playlists.move_from (displaced, 0, to + count, from - to, false, true); - number_playlists (to, from + count - to); + queued_update.queue (update, nullptr); + update_state = UpdateState::Queued; } - else + + if (id == playing_id) { - playlists.move_from (displaced, 0, from, to - from, false, true); - number_playlists (from, to + count - from); + if (id->data->position () >= 0) + { + start_playback_locked (0, aud_drct_get_paused ()); + queue_update_hooks (PlaybackBegin); + } + else + { + playing_id = nullptr; + stop_playback_locked (); + queue_update_hooks (SetPlaying | PlaybackStop); + } } - - queue_update (Structure, nullptr, 0, 0); - LEAVE; } -EXPORT void aud_playlist_delete (int playlist_num) +void pl_signal_update_queued (Playlist::ID * id, Playlist::UpdateLevel level, int flags) { - ENTER_GET_PLAYLIST (); + auto playlist = id->data; - bool was_active = false; - bool was_playing = false; + if (level == Playlist::Structure) + playlist->scan_status = PlaylistData::ScanActive; - playlists.remove (playlist_num, 1); - - if (! playlists.len ()) - playlists.append (SmartNew<PlaylistData> (-1)); - - number_playlists (playlist_num, playlists.len () - playlist_num); - - if (playlist == active_playlist) + if (level >= Playlist::Metadata) { - int active_num = aud::min (playlist_num, playlists.len () - 1); - active_playlist = playlists[active_num].get (); - was_active = true; - } + int pos = playlist->position (); + if (id == playing_id && pos >= 0) + playback_set_info (pos, playlist->entry_tuple (pos)); - if (playlist == playing_playlist) - { - playing_playlist = nullptr; - stop_playback (); - was_playing = true; + playlist->modified = true; } - queue_update (Structure, nullptr, 0, 0); - LEAVE; - - if (was_active) - hook_call ("playlist activate", nullptr); - - if (was_playing) - { - hook_call ("playlist set playing", nullptr); - hook_call ("playback stop", nullptr); - } + queue_global_update (level, flags); } -EXPORT int aud_playlist_get_unique_id (int playlist_num) +void pl_signal_rescan_needed (Playlist::ID * id) { - ENTER_GET_PLAYLIST (-1); - int unique_id = playlist->unique_id; - RETURN (unique_id); -} - -EXPORT int aud_playlist_by_unique_id (int id) -{ - ENTER; - - PlaylistData * * ptr = unique_id_table.lookup (id); - int num = (ptr && * ptr) ? (* ptr)->number : -1; - - RETURN (num); + id->data->scan_status = PlaylistData::ScanActive; + scan_restart (); } -EXPORT void aud_playlist_set_filename (int playlist_num, const char * filename) +void pl_signal_playlist_deleted (Playlist::ID * id) { - ENTER_GET_PLAYLIST (); - - playlist->filename = String (filename); - playlist->modified = true; - - queue_update (Metadata, nullptr, 0, 0); - LEAVE; + /* break weak pointer link */ + id->data = nullptr; + id->index = -1; } -EXPORT String aud_playlist_get_filename (int playlist_num) +static void pl_hook_reformat_titles (void *, void *) { - ENTER_GET_PLAYLIST (String ()); - String filename = playlist->filename; - RETURN (filename); -} + ENTER; -EXPORT void aud_playlist_set_title (int playlist_num, const char * title) -{ - ENTER_GET_PLAYLIST (); + PlaylistData::update_formatter (); - playlist->title = String (title); - playlist->modified = true; + for (auto & playlist : playlists) + playlist->reformat_titles (); - queue_update (Metadata, nullptr, 0, 0); LEAVE; } -EXPORT String aud_playlist_get_title (int playlist_num) +static void pl_hook_trigger_scan (void *, void *) { - ENTER_GET_PLAYLIST (String ()); - String title = playlist->title; - RETURN (title); -} - -void playlist_set_modified (int playlist_num, bool modified) -{ - ENTER_GET_PLAYLIST (); - playlist->modified = modified; + ENTER; + scan_enabled = scan_enabled_nominal && ! aud_get_bool (nullptr, "metadata_on_play"); + scan_restart (); LEAVE; } -bool playlist_get_modified (int playlist_num) +void playlist_init () { - ENTER_GET_PLAYLIST (false); - bool modified = playlist->modified; - RETURN (modified); -} + srand (time (nullptr)); -EXPORT void aud_playlist_set_active (int playlist_num) -{ - ENTER_GET_PLAYLIST (); + ENTER; - bool changed = false; + PlaylistData::update_formatter (); - if (playlist != active_playlist) - { - changed = true; - active_playlist = playlist; - } + update_level = Playlist::NoUpdate; + update_hooks = 0; + update_state = UpdateState::None; + scan_enabled = scan_enabled_nominal = false; + scan_playlist = scan_row = 0; LEAVE; - if (changed) - hook_call ("playlist activate", nullptr); + hook_associate ("set generic_title_format", pl_hook_reformat_titles, nullptr); + hook_associate ("set leading_zero", pl_hook_reformat_titles, nullptr); + hook_associate ("set metadata_fallbacks", pl_hook_reformat_titles, nullptr); + hook_associate ("set show_hours", pl_hook_reformat_titles, nullptr); + hook_associate ("set show_numbers_in_pl", pl_hook_reformat_titles, nullptr); + hook_associate ("set metadata_on_play", pl_hook_trigger_scan, nullptr); } -EXPORT int aud_playlist_get_active () -{ - ENTER; - int list = active_playlist ? active_playlist->number : -1; - RETURN (list); -} - -EXPORT int aud_playlist_new () -{ - int playlist = aud_playlist_get_active () + 1; - aud_playlist_insert (playlist); - aud_playlist_set_active (playlist); - return playlist; -} - -EXPORT void aud_playlist_play (int playlist_num, bool paused) +void playlist_enable_scan (bool enable) { ENTER; - PlaylistData * playlist = lookup_playlist (playlist_num); - bool position_changed = false; - - if (playlist == playing_playlist) - { - /* already playing, just need to pause/unpause */ - if (aud_drct_get_paused () != paused) - aud_drct_pause (); - - RETURN (); - } - - if (playing_playlist) - playing_playlist->resume_time = aud_drct_get_time (); - - /* is there anything to play? */ - if (playlist && ! playlist->position) - { - if (next_song_locked (playlist, true, 0)) - position_changed = true; - else - playlist = nullptr; - } - - playing_playlist = playlist; - - if (playlist) - start_playback (playlist->resume_time, paused); - else - stop_playback (); + scan_enabled_nominal = enable; + scan_enabled = scan_enabled_nominal && ! aud_get_bool (nullptr, "metadata_on_play"); + scan_restart (); LEAVE; - - if (position_changed) - hook_call ("playlist position", aud::to_ptr (playlist_num)); - - hook_call ("playlist set playing", nullptr); - - if (playlist) - hook_call ("playback begin", nullptr); - else - hook_call ("playback stop", nullptr); } -EXPORT int aud_playlist_get_playing () +void playlist_clear_updates () { ENTER; - int list = playing_playlist ? playing_playlist->number: -1; - RETURN (list); -} - -EXPORT int aud_playlist_get_blank () -{ - int list = aud_playlist_get_active (); - String title = aud_playlist_get_title (list); - - if (strcmp (title, _(default_title)) || aud_playlist_entry_count (list) > 0) - aud_playlist_insert (++ list); - return list; -} - -EXPORT int aud_playlist_get_temporary () -{ - int count = aud_playlist_count (); - - for (int list = 0; list < count; list ++) - { - String title = aud_playlist_get_title (list); - if (! strcmp (title, _(temp_title))) - return list; - } - - int list = aud_playlist_get_blank (); - aud_playlist_set_title (list, _(temp_title)); - return list; -} - -static void set_position (PlaylistData * playlist, Entry * entry, bool update_shuffle) -{ - playlist->position = entry; - playlist->resume_time = 0; - - /* move entry to top of shuffle list */ - if (entry && update_shuffle) - entry->shuffle_num = ++ playlist->last_shuffle_num; -} - -// updates playback state (while locked) if playlist position was changed -static PlaybackChange change_playback (PlaylistData * playlist) -{ - if (playlist != playing_playlist) - return NoChange; - - if (playlist->position) - { - start_playback (0, aud_drct_get_paused ()); - return NextSong; - } - else - { - playing_playlist = nullptr; - stop_playback (); - return PlaybackStopped; - } -} - -// call hooks (while unlocked) if playback state was changed -static void call_playback_change_hooks (PlaybackChange change) -{ - if (change == NextSong) - hook_call ("playback begin", nullptr); - - if (change == PlaybackStopped) - { - hook_call ("playlist set playing", nullptr); - hook_call ("playback stop", nullptr); - } -} - -EXPORT int aud_playlist_entry_count (int playlist_num) -{ - ENTER_GET_PLAYLIST (0); - int count = playlist->entries.len (); - RETURN (count); -} - -void playlist_entry_insert_batch_raw (int playlist_num, int at, Index<PlaylistAddItem> && items) -{ - ENTER_GET_PLAYLIST (); - - int entries = playlist->entries.len (); - - if (at < 0 || at > entries) - at = entries; - - int number = items.len (); - - playlist->entries.insert (at, number); - - int i = at; - for (auto & item : items) - { - Entry * entry = new Entry (std::move (item)); - playlist->entries[i ++].capture (entry); - playlist->total_length += entry->length; - } - - items.clear (); + /* clear updates queued during init sequence */ + for (auto & playlist : playlists) + playlist->cancel_updates (); - number_entries (playlist, at, entries + number - at); + queued_update.stop (); + update_level = Playlist::NoUpdate; + update_hooks = 0; + update_state = UpdateState::None; - queue_update (Structure, playlist, at, number); LEAVE; } -EXPORT void aud_playlist_entry_delete (int playlist_num, int at, int number) +void playlist_end () { - ENTER_GET_PLAYLIST (); - - int entries = playlist->entries.len (); - bool position_changed = false; - int update_flags = 0; - PlaybackChange change = NoChange; - - if (at < 0 || at > entries) - at = entries; - if (number < 0 || number > entries - at) - number = entries - at; + hook_dissociate ("set generic_title_format", pl_hook_reformat_titles); + hook_dissociate ("set leading_zero", pl_hook_reformat_titles); + hook_dissociate ("set metadata_fallbacks", pl_hook_reformat_titles); + hook_dissociate ("set show_hours", pl_hook_reformat_titles); + hook_dissociate ("set show_numbers_in_pl", pl_hook_reformat_titles); + hook_dissociate ("set metadata_on_play", pl_hook_trigger_scan); - if (playlist->position && playlist->position->number >= at && - playlist->position->number < at + number) - { - set_position (playlist, nullptr, false); - position_changed = true; - } - - if (playlist->focus && playlist->focus->number >= at && - playlist->focus->number < at + number) - { - if (at + number < entries) - playlist->focus = playlist->entries[at + number].get (); - else if (at > 0) - playlist->focus = playlist->entries[at - 1].get (); - else - playlist->focus = nullptr; - } - - for (int count = 0; count < number; count ++) - { - Entry * entry = playlist->entries [at + count].get (); + playlist_cache_clear (); - if (entry->queued) - { - playlist->queued.remove (playlist->queued.find (entry), 1); - update_flags |= QueueChanged; - } + ENTER; - if (entry->selected) - { - playlist->selected_count --; - playlist->selected_length -= entry->length; - } + /* playback should already be stopped */ + assert (! playing_id); + assert (! scan_list.head ()); - playlist->total_length -= entry->length; - } + queued_update.stop (); - playlist->entries.remove (at, number); - number_entries (playlist, at, entries - at - number); + active_id = nullptr; + resume_playlist = -1; + resume_paused = false; - if (position_changed) - { - if (aud_get_bool (nullptr, "advance_on_delete")) - next_song_locked (playlist, aud_get_bool (nullptr, "repeat"), at); + playlists.clear (); + id_table.clear (); - change = change_playback (playlist); - } + PlaylistData::cleanup_formatter (); - queue_update (Structure, playlist, at, 0, update_flags); LEAVE; - - if (position_changed) - hook_call ("playlist position", aud::to_ptr (playlist_num)); - - call_playback_change_hooks (change); } -EXPORT String aud_playlist_entry_get_filename (int playlist_num, int entry_num) +EXPORT int Playlist::n_entries () const + { SIMPLE_WRAPPER (int, 0, n_entries); } +EXPORT void Playlist::remove_entries (int at, int number) const + { SIMPLE_VOID_WRAPPER (remove_entries, at, number); } +EXPORT String Playlist::entry_filename (int entry_num) const + { SIMPLE_WRAPPER (String, String (), entry_filename, entry_num); } + +EXPORT int Playlist::get_position () const + { SIMPLE_WRAPPER (int, -1, position); } +EXPORT void Playlist::set_position (int entry_num) const + { SIMPLE_VOID_WRAPPER (set_position, entry_num); } +EXPORT bool Playlist::prev_song () const + { SIMPLE_WRAPPER (bool, false, prev_song); } +EXPORT bool Playlist::next_song (bool repeat) const + { SIMPLE_WRAPPER (bool, false, next_song, repeat); } +EXPORT int Playlist::get_focus () const + { SIMPLE_WRAPPER (int, -1, focus); } +EXPORT void Playlist::set_focus (int entry_num) const + { SIMPLE_VOID_WRAPPER (set_focus, entry_num); } +EXPORT bool Playlist::entry_selected (int entry_num) const + { SIMPLE_WRAPPER (bool, false, entry_selected, entry_num); } +EXPORT void Playlist::select_entry (int entry_num, bool selected) const + { SIMPLE_VOID_WRAPPER (select_entry, entry_num, selected); } +EXPORT int Playlist::n_selected (int at, int number) const + { SIMPLE_WRAPPER (int, 0, n_selected, at, number); } +EXPORT void Playlist::select_all (bool selected) const + { SIMPLE_VOID_WRAPPER (select_all, selected); } +EXPORT int Playlist::shift_entries (int entry_num, int distance) const + { SIMPLE_WRAPPER (int, 0, shift_entries, entry_num, distance); } +EXPORT void Playlist::remove_selected () const + { SIMPLE_VOID_WRAPPER (remove_selected); } + +EXPORT void Playlist::sort_by_filename (StringCompareFunc compare) const + { SIMPLE_VOID_WRAPPER (sort, {compare, nullptr}); } +EXPORT void Playlist::sort_by_tuple (TupleCompareFunc compare) const + { SIMPLE_VOID_WRAPPER (sort, {nullptr, compare}); } +EXPORT void Playlist::sort_selected_by_filename (StringCompareFunc compare) const + { SIMPLE_VOID_WRAPPER (sort_selected, {compare, nullptr}); } +EXPORT void Playlist::sort_selected_by_tuple (TupleCompareFunc compare) const + { SIMPLE_VOID_WRAPPER (sort_selected, {nullptr, compare}); } +EXPORT void Playlist::reverse_order () const + { SIMPLE_VOID_WRAPPER (reverse_order); } +EXPORT void Playlist::reverse_selected () const + { SIMPLE_VOID_WRAPPER (reverse_selected); } +EXPORT void Playlist::randomize_order () const + { SIMPLE_VOID_WRAPPER (randomize_order); } +EXPORT void Playlist::randomize_selected () const + { SIMPLE_VOID_WRAPPER (randomize_selected); } + +EXPORT void Playlist::rescan_all () const + { SIMPLE_VOID_WRAPPER (reset_tuples, false); } +EXPORT void Playlist::rescan_selected () const + { SIMPLE_VOID_WRAPPER (reset_tuples, true); } + +EXPORT int64_t Playlist::total_length_ms () const + { SIMPLE_WRAPPER (int64_t, 0, total_length); } +EXPORT int64_t Playlist::selected_length_ms () const + { SIMPLE_WRAPPER (int64_t, 0, selected_length); } + +EXPORT int Playlist::n_queued () const + { SIMPLE_WRAPPER (int, 0, n_queued); } +EXPORT void Playlist::queue_insert (int at, int entry_num) const + { SIMPLE_VOID_WRAPPER (queue_insert, at, entry_num); } +EXPORT void Playlist::queue_insert_selected (int at) const + { SIMPLE_VOID_WRAPPER (queue_insert_selected, at); } +EXPORT int Playlist::queue_get_entry (int at) const + { SIMPLE_WRAPPER (int, -1, queue_get_entry, at); } +EXPORT int Playlist::queue_find_entry (int entry_num) const + { SIMPLE_WRAPPER (int, -1, queue_find_entry, entry_num); } +EXPORT void Playlist::queue_remove (int at, int number) const + { SIMPLE_VOID_WRAPPER (queue_remove, at, number); } +EXPORT void Playlist::queue_remove_selected () const + { SIMPLE_VOID_WRAPPER (queue_remove_selected); } + +EXPORT bool Playlist::update_pending () const + { SIMPLE_WRAPPER (bool, false, update_pending); } +EXPORT Playlist::Update Playlist::update_detail () const + { SIMPLE_WRAPPER (Update, Update (), last_update); } + +void PlaylistEx::insert_flat_items (int at, Index<PlaylistAddItem> && items) const + { SIMPLE_VOID_WRAPPER (insert_items, at, std::move (items)); } + +EXPORT int Playlist::index () const { - ENTER_GET_ENTRY (String ()); - String filename = entry->filename; - RETURN (filename); + ENTER_GET_PLAYLIST (-1); + int at = m_id->index; + RETURN (at); } -EXPORT PluginHandle * aud_playlist_entry_get_decoder (int playlist_num, - int entry_num, GetMode mode, String * error) +EXPORT int PlaylistEx::stamp () const { - ENTER; - - Entry * entry = get_entry (playlist_num, entry_num, (mode == Wait), false); - PluginHandle * decoder = entry ? entry->decoder : nullptr; - - if (error) - * error = entry ? entry->error : String (); - - RETURN (decoder); + ENTER_GET_PLAYLIST (-1); + int stamp = m_id->stamp; + RETURN (stamp); } -EXPORT Tuple aud_playlist_entry_get_tuple (int playlist_num, int entry_num, - GetMode mode, String * error) +EXPORT int Playlist::n_playlists () { ENTER; - - 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 (); - - RETURN (tuple); -} - -EXPORT void aud_playlist_set_position (int playlist_num, int entry_num) -{ - ENTER_GET_PLAYLIST (); - - Entry * entry = lookup_entry (playlist, entry_num); - set_position (playlist, entry, true); - - PlaybackChange change = change_playback (playlist); - - LEAVE; - - hook_call ("playlist position", aud::to_ptr (playlist_num)); - call_playback_change_hooks (change); + int count = playlists.len (); + RETURN (count); } -EXPORT int aud_playlist_get_position (int playlist_num) +EXPORT Playlist Playlist::by_index (int at) { - ENTER_GET_PLAYLIST (-1); - int position = playlist->position ? playlist->position->number : -1; - RETURN (position); + ENTER; + Playlist::ID * id = (at >= 0 && at < playlists.len ()) ? playlists[at]->id () : nullptr; + RETURN (Playlist (id)); } -EXPORT void aud_playlist_set_focus (int playlist_num, int entry_num) +static Playlist::ID * insert_playlist_locked (int at, int stamp = -1) { - ENTER_GET_PLAYLIST (); - - Entry * new_focus = lookup_entry (playlist, entry_num); - if (new_focus == playlist->focus) - RETURN (); - - int first = INT_MAX; - int last = -1; + if (at < 0 || at > playlists.len ()) + at = playlists.len (); - if (playlist->focus) - { - first = aud::min (first, playlist->focus->number); - last = aud::max (last, playlist->focus->number); - } + auto id = create_playlist (stamp); - playlist->focus = new_focus; + playlists.insert (at, 1); + playlists[at].capture (id->data); - if (playlist->focus) - { - first = aud::min (first, playlist->focus->number); - last = aud::max (last, playlist->focus->number); - } + number_playlists (at, playlists.len () - at); - if (first <= last) - queue_update (Selection, playlist, first, last + 1 - first); + /* this will only happen at startup */ + if (! active_id) + active_id = id; - LEAVE; -} + queue_global_update (Playlist::Structure); -EXPORT int aud_playlist_get_focus (int playlist_num) -{ - ENTER_GET_PLAYLIST (-1); - int focus = playlist->focus ? playlist->focus->number : -1; - RETURN (focus); + return id; } -EXPORT void aud_playlist_entry_set_selected (int playlist_num, int entry_num, - bool selected) +static Playlist::ID * get_blank_locked () { - ENTER_GET_ENTRY (); - - if (entry->selected == selected) - RETURN (); - - entry->selected = selected; + if (! strcmp (active_id->data->title, _(default_title)) && ! active_id->data->n_entries ()) + return active_id; - if (selected) - { - playlist->selected_count++; - playlist->selected_length += entry->length; - } - else - { - playlist->selected_count--; - playlist->selected_length -= entry->length; - } - - queue_update (Selection, playlist, entry_num, 1); - LEAVE; + return insert_playlist_locked (active_id->index + 1); } -EXPORT bool aud_playlist_entry_get_selected (int playlist_num, int entry_num) +Playlist PlaylistEx::insert_with_stamp (int at, int stamp) { - ENTER_GET_ENTRY (false); - bool selected = entry->selected; - RETURN (selected); + ENTER; + auto id = insert_playlist_locked (at, stamp); + RETURN (Playlist (id)); } -EXPORT int aud_playlist_selected_count (int playlist_num) +EXPORT Playlist Playlist::insert_playlist (int at) { - ENTER_GET_PLAYLIST (0); - int selected_count = playlist->selected_count; - RETURN (selected_count); + ENTER; + auto id = insert_playlist_locked (at); + RETURN (Playlist (id)); } -EXPORT void aud_playlist_select_all (int playlist_num, bool selected) +EXPORT void Playlist::reorder_playlists (int from, int to, int count) { - ENTER_GET_PLAYLIST (); + ENTER; - int entries = playlist->entries.len (); - int first = entries, last = 0; + if (from < 0 || from + count > playlists.len () || to < 0 || to + + count > playlists.len () || count < 0) + RETURN (); - for (auto & entry : playlist->entries) - { - if ((selected && ! entry->selected) || (entry->selected && ! selected)) - { - entry->selected = selected; - first = aud::min (first, entry->number); - last = entry->number; - } - } + Index<SmartPtr<PlaylistData>> displaced; - if (selected) - { - playlist->selected_count = entries; - playlist->selected_length = playlist->total_length; - } + if (to < from) + displaced.move_from (playlists, to, -1, from - to, true, false); else - { - playlist->selected_count = 0; - playlist->selected_length = 0; - } - - if (first < entries) - queue_update (Selection, playlist, first, last + 1 - first); - - LEAVE; -} - -EXPORT int aud_playlist_shift (int playlist_num, int entry_num, int distance) -{ - ENTER_GET_ENTRY (0); - - if (! entry->selected || ! distance) - RETURN (0); + displaced.move_from (playlists, from + count, -1, to - from, true, false); - int entries = playlist->entries.len (); - int shift = 0, center, top, bottom; + playlists.shift (from, to, count); - if (distance < 0) + if (to < from) { - for (center = entry_num; center > 0 && shift > distance; ) - { - if (! playlist->entries[-- center]->selected) - shift --; - } + playlists.move_from (displaced, 0, to + count, from - to, false, true); + number_playlists (to, from + count - to); } else { - for (center = entry_num + 1; center < entries && shift < distance; ) - { - if (! playlist->entries[center ++]->selected) - shift ++; - } - } - - top = bottom = center; - - for (int i = 0; i < top; i ++) - { - if (playlist->entries[i]->selected) - top = i; - } - - for (int i = entries; i > bottom; i --) - { - if (playlist->entries[i - 1]->selected) - bottom = i; - } - - Index<SmartPtr<Entry>> temp; - - for (int i = top; i < center; i ++) - { - if (! playlist->entries[i]->selected) - temp.append (std::move (playlist->entries[i])); - } - - for (int i = top; i < bottom; i ++) - { - if (playlist->entries[i] && playlist->entries[i]->selected) - temp.append (std::move (playlist->entries[i])); - } - - for (int i = center; i < bottom; i ++) - { - if (playlist->entries[i] && ! playlist->entries[i]->selected) - temp.append (std::move (playlist->entries[i])); - } - - playlist->entries.move_from (temp, 0, top, bottom - top, false, true); - - number_entries (playlist, top, bottom - top); - queue_update (Structure, playlist, top, bottom - top); - - RETURN (shift); -} - -static Entry * find_unselected_focus (PlaylistData * playlist) -{ - if (! playlist->focus || ! playlist->focus->selected) - return playlist->focus; - - int entries = playlist->entries.len (); - - for (int search = playlist->focus->number + 1; search < entries; search ++) - { - Entry * entry = playlist->entries[search].get (); - if (! entry->selected) - return entry; - } - - for (int search = playlist->focus->number; search --;) - { - Entry * entry = playlist->entries[search].get (); - if (! entry->selected) - return entry; + playlists.move_from (displaced, 0, from, to - from, false, true); + number_playlists (from, to + count - from); } - return nullptr; + queue_global_update (Structure); + LEAVE; } -EXPORT void aud_playlist_delete_selected (int playlist_num) +EXPORT void Playlist::remove_playlist () const { ENTER_GET_PLAYLIST (); - if (! playlist->selected_count) - RETURN (); - - int entries = playlist->entries.len (); - bool position_changed = false; - int update_flags = 0; - PlaybackChange change = NoChange; - - if (playlist->position && playlist->position->selected) - { - set_position (playlist, nullptr, false); - position_changed = true; - } + int at = m_id->index; + playlists.remove (at, 1); - playlist->focus = find_unselected_focus (playlist); - - int before = 0; // number of entries before first selected - int after = 0; // number of entries after last selected - - while (before < entries && ! playlist->entries[before]->selected) - before ++; + if (! playlists.len ()) + playlists.append (create_playlist (-1)->data); - int to = before; + number_playlists (at, playlists.len () - at); - for (int from = before; from < entries; from ++) + if (m_id == active_id) { - Entry * entry = playlist->entries[from].get (); - - if (entry->selected) - { - if (entry->queued) - { - playlist->queued.remove (playlist->queued.find (entry), 1); - update_flags |= QueueChanged; - } - - playlist->total_length -= entry->length; - after = 0; - } - else - { - playlist->entries[to ++] = std::move (playlist->entries[from]); - after ++; - } + int active_num = aud::min (at, playlists.len () - 1); + active_id = playlists[active_num]->id (); + queue_update_hooks (SetActive); } - entries = to; - playlist->entries.remove (entries, -1); - number_entries (playlist, before, entries - before); - - playlist->selected_count = 0; - playlist->selected_length = 0; - - if (position_changed) + if (m_id == playing_id) { - if (aud_get_bool (nullptr, "advance_on_delete")) - next_song_locked (playlist, aud_get_bool (nullptr, "repeat"), entries - after); - - change = change_playback (playlist); + playing_id = nullptr; + stop_playback_locked (); + queue_update_hooks (SetPlaying | PlaybackStop); } - queue_update (Structure, playlist, before, entries - after - before, update_flags); - LEAVE; - - if (position_changed) - hook_call ("playlist position", aud::to_ptr (playlist_num)); - - call_playback_change_hooks (change); -} - -EXPORT void aud_playlist_reverse (int playlist_num) -{ - ENTER_GET_PLAYLIST (); - - int entries = playlist->entries.len (); - - for (int i = 0; i < entries / 2; i ++) - std::swap (playlist->entries[i], playlist->entries[entries - 1 - i]); - - number_entries (playlist, 0, entries); - queue_update (Structure, playlist, 0, entries); + queue_global_update (Structure); LEAVE; } -EXPORT void aud_playlist_reverse_selected (int playlist_num) +EXPORT void Playlist::set_filename (const char * filename) const { ENTER_GET_PLAYLIST (); - int entries = playlist->entries.len (); - - int top = 0; - int bottom = entries - 1; - - while (1) - { - while (top < bottom && ! playlist->entries[top]->selected) - top ++; - while (top < bottom && ! playlist->entries[bottom]->selected) - bottom --; - - if (top >= bottom) - break; - - std::swap (playlist->entries[top ++], playlist->entries[bottom --]); - } + playlist->filename = String (filename); + playlist->modified = true; - number_entries (playlist, 0, entries); - queue_update (Structure, playlist, 0, entries); + queue_global_update (Metadata); LEAVE; } -EXPORT void aud_playlist_randomize (int playlist_num) +EXPORT String Playlist::get_filename () const { - ENTER_GET_PLAYLIST (); - - int entries = playlist->entries.len (); - - for (int i = 0; i < entries; i ++) - std::swap (playlist->entries[i], playlist->entries[rand () % entries]); - - number_entries (playlist, 0, entries); - queue_update (Structure, playlist, 0, entries); - LEAVE; + ENTER_GET_PLAYLIST (String ()); + String filename = playlist->filename; + RETURN (filename); } -EXPORT void aud_playlist_randomize_selected (int playlist_num) +EXPORT void Playlist::set_title (const char * title) const { ENTER_GET_PLAYLIST (); - int entries = playlist->entries.len (); - - Index<Entry *> selected; - - for (auto & entry : playlist->entries) - { - if (entry->selected) - selected.append (entry.get ()); - } - - int n_selected = selected.len (); - - for (int i = 0; i < n_selected; i ++) - { - int a = selected[i]->number; - int b = selected[rand () % n_selected]->number; - std::swap (playlist->entries[a], playlist->entries[b]); - } + playlist->title = String (title); + playlist->modified = true; - number_entries (playlist, 0, entries); - queue_update (Structure, playlist, 0, entries); + queue_global_update (Metadata); LEAVE; } -enum {COMPARE_TYPE_FILENAME, COMPARE_TYPE_TUPLE, COMPARE_TYPE_TITLE}; - -struct CompareData { - PlaylistStringCompareFunc filename_compare; - PlaylistTupleCompareFunc tuple_compare; -}; - -static void sort_entries (Index<SmartPtr<Entry>> & entries, CompareData * data) -{ - 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) +EXPORT String Playlist::get_title () const { - sort_entries (playlist->entries, data); - number_entries (playlist, 0, playlist->entries.len ()); - - queue_update (Structure, playlist, 0, playlist->entries.len ()); -} - -static void sort_selected (PlaylistData * playlist, CompareData * data) -{ - int entries = playlist->entries.len (); - - Index<SmartPtr<Entry>> selected; - - for (auto & entry : playlist->entries) - { - if (entry->selected) - selected.append (std::move (entry)); - } - - sort_entries (selected, data); - - int i = 0; - for (auto & entry : playlist->entries) - { - if (! entry) - entry = std::move (selected[i ++]); - } - - number_entries (playlist, 0, entries); - queue_update (Structure, playlist, 0, entries); -} - -static bool entries_are_scanned (PlaylistData * playlist, bool selected) -{ - for (auto & entry : playlist->entries) - { - if (selected && ! entry->selected) - continue; - - 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).")); - return false; - } - } - - return true; -} - -EXPORT void aud_playlist_sort_by_filename (int playlist_num, PlaylistStringCompareFunc compare) -{ - ENTER_GET_PLAYLIST (); - - CompareData data = {compare}; - sort (playlist, & data); - - LEAVE; + ENTER_GET_PLAYLIST (String ()); + String title = playlist->title; + RETURN (title); } -EXPORT void aud_playlist_sort_by_tuple (int playlist_num, PlaylistTupleCompareFunc compare) +void PlaylistEx::set_modified (bool modified) const { ENTER_GET_PLAYLIST (); - - CompareData data = {nullptr, compare}; - if (entries_are_scanned (playlist, false)) - sort (playlist, & data); - + playlist->modified = modified; LEAVE; } -EXPORT void aud_playlist_sort_selected_by_filename (int playlist_num, - PlaylistStringCompareFunc compare) +bool PlaylistEx::get_modified () const { - ENTER_GET_PLAYLIST (); - - CompareData data = {compare}; - sort_selected (playlist, & data); - - LEAVE; + ENTER_GET_PLAYLIST (false); + bool modified = playlist->modified; + RETURN (modified); } -EXPORT void aud_playlist_sort_selected_by_tuple (int playlist_num, - PlaylistTupleCompareFunc compare) +EXPORT void Playlist::activate () const { ENTER_GET_PLAYLIST (); - CompareData data = {nullptr, compare}; - if (entries_are_scanned (playlist, true)) - sort_selected (playlist, & data); - - LEAVE; -} - -static void playlist_reformat_titles () -{ - ENTER; - - metadata_fallbacks = aud_get_bool (nullptr, "metadata_fallbacks"); - title_formatter.compile (aud_get_str (nullptr, "generic_title_format")); - - for (auto & playlist : playlists) + if (m_id != active_id) { - for (auto & entry : playlist->entries) - entry->format (); - - queue_update (Metadata, playlist.get (), 0, playlist->entries.len ()); + active_id = m_id; + queue_update_hooks (SetActive); } LEAVE; } -static void playlist_trigger_scan () +EXPORT Playlist Playlist::active_playlist () { ENTER; - - scan_enabled = scan_enabled_nominal && ! aud_get_bool (nullptr, "metadata_on_play"); - scan_restart (); - - LEAVE; + auto id = active_id; + RETURN (Playlist (id)); } -static void playlist_rescan_real (int playlist_num, bool selected) +EXPORT Playlist Playlist::new_playlist () { - ENTER_GET_PLAYLIST (); - - for (auto & entry : playlist->entries) - { - if (! selected || entry->selected) - playlist->set_entry_tuple (entry.get (), Tuple ()); - } - - queue_update (Metadata, playlist, 0, playlist->entries.len ()); - scan_queue_playlist (playlist); - scan_restart (); + ENTER; - LEAVE; -} + int at = active_id->index + 1; + auto id = insert_playlist_locked (at); -EXPORT void aud_playlist_rescan (int playlist_num) -{ - playlist_rescan_real (playlist_num, false); -} + active_id = id; + queue_update_hooks (SetActive); -EXPORT void aud_playlist_rescan_selected (int playlist_num) -{ - playlist_rescan_real (playlist_num, true); + RETURN (Playlist (id)); } -EXPORT void aud_playlist_rescan_file (const char * filename) +static void set_playing_locked (Playlist::ID * id, bool paused) { - ENTER; - - bool restart = false; - - for (auto & playlist : playlists) + if (id == playing_id) { - bool queue = false; - - for (auto & entry : playlist->entries) - { - if (! strcmp (entry->filename, filename)) - { - playlist->set_entry_tuple (entry.get (), Tuple ()); - queue_update (Metadata, playlist.get (), entry->number, 1); - queue = true; - } - } + /* already playing, just need to pause/unpause */ + if (aud_drct_get_paused () != paused) + aud_drct_pause (); - if (queue) - { - scan_queue_playlist (playlist.get ()); - restart = true; - } + return; } - if (restart) - scan_restart (); + if (playing_id) + playing_id->data->resume_time = aud_drct_get_time (); - LEAVE; -} - -EXPORT int64_t aud_playlist_get_total_length (int playlist_num) -{ - ENTER_GET_PLAYLIST (0); - int64_t length = playlist->total_length; - RETURN (length); -} - -EXPORT int64_t aud_playlist_get_selected_length (int playlist_num) -{ - ENTER_GET_PLAYLIST (0); - int64_t length = playlist->selected_length; - RETURN (length); -} - -EXPORT int aud_playlist_queue_count (int playlist_num) -{ - ENTER_GET_PLAYLIST (0); - int count = playlist->queued.len (); - RETURN (count); -} + /* is there anything to play? */ + if (id && id->data->position () < 0 && ! id->data->next_song (true)) + id = nullptr; -EXPORT void aud_playlist_queue_insert (int playlist_num, int at, int entry_num) -{ - ENTER_GET_ENTRY (); + playing_id = id; - if (entry->queued || at > playlist->queued.len ()) - RETURN (); - - if (at < 0) - playlist->queued.append (entry); - else + if (id) { - playlist->queued.insert (at, 1); - playlist->queued[at] = entry; + start_playback_locked (id->data->resume_time, paused); + queue_update_hooks (SetPlaying | PlaybackBegin); } - - entry->queued = true; - - queue_update (Selection, playlist, entry_num, 1, QueueChanged); - LEAVE; -} - -EXPORT void aud_playlist_queue_insert_selected (int playlist_num, int at) -{ - ENTER_GET_PLAYLIST (); - - if (at > playlist->queued.len ()) - RETURN (); - - Index<Entry *> add; - int first = playlist->entries.len (); - int last = 0; - - for (auto & entry : playlist->entries) + else { - if (! entry->selected || entry->queued) - continue; - - add.append (entry.get ()); - entry->queued = true; - first = aud::min (first, entry->number); - last = entry->number; + stop_playback_locked (); + queue_update_hooks (SetPlaying | PlaybackStop); } - - playlist->queued.move_from (add, 0, at, -1, true, true); - - if (first < playlist->entries.len ()) - queue_update (Selection, playlist, first, last + 1 - first, QueueChanged); - - LEAVE; -} - -EXPORT int aud_playlist_queue_get_entry (int playlist_num, int at) -{ - ENTER_GET_PLAYLIST (-1); - - int entry_num = -1; - if (at >= 0 && at < playlist->queued.len ()) - entry_num = playlist->queued[at]->number; - - RETURN (entry_num); -} - -EXPORT int aud_playlist_queue_find_entry (int playlist_num, int entry_num) -{ - ENTER_GET_ENTRY (-1); - int pos = entry->queued ? playlist->queued.find (entry) : -1; - RETURN (pos); } -EXPORT void aud_playlist_queue_delete (int playlist_num, int at, int number) +EXPORT void Playlist::start_playback (bool paused) const { ENTER_GET_PLAYLIST (); - - if (at < 0 || number < 0 || at + number > playlist->queued.len ()) - RETURN (); - - int entries = playlist->entries.len (); - int first = entries, last = 0; - - for (int i = at; i < at + number; i ++) - { - Entry * entry = playlist->queued[i]; - entry->queued = false; - first = aud::min (first, entry->number); - last = entry->number; - } - - playlist->queued.remove (at, number); - - if (first < entries) - queue_update (Selection, playlist, first, last + 1 - first, QueueChanged); - + set_playing_locked (m_id, paused); LEAVE; } -EXPORT void aud_playlist_queue_delete_selected (int playlist_num) +EXPORT void aud_drct_stop () { - ENTER_GET_PLAYLIST (); - - int entries = playlist->entries.len (); - int first = entries, last = 0; - - for (int i = 0; i < playlist->queued.len ();) - { - Entry * entry = playlist->queued[i]; - - if (entry->selected) - { - playlist->queued.remove (i, 1); - entry->queued = false; - first = aud::min (first, entry->number); - last = entry->number; - } - else - i ++; - } - - if (first < entries) - queue_update (Selection, playlist, first, last + 1 - first, QueueChanged); - + ENTER; + set_playing_locked (nullptr, false); LEAVE; } -static bool shuffle_prev (PlaylistData * playlist) +EXPORT Playlist Playlist::playing_playlist () { - Entry * found = nullptr; - - for (auto & entry : playlist->entries) - { - if (entry->shuffle_num && (! playlist->position || - entry->shuffle_num < playlist->position->shuffle_num) && (! found - || entry->shuffle_num > found->shuffle_num)) - found = entry.get (); - } - - if (! found) - return false; - - set_position (playlist, found, false); - return true; + ENTER; + auto id = playing_id; + RETURN (Playlist (id)); } -bool playlist_prev_song (int playlist_num) +EXPORT Playlist Playlist::blank_playlist () { - ENTER_GET_PLAYLIST (false); - - if (aud_get_bool (nullptr, "shuffle")) - { - if (! shuffle_prev (playlist)) - RETURN (false); - } - else - { - if (! playlist->position || playlist->position->number == 0) - RETURN (false); - - set_position (playlist, playlist->entries[playlist->position->number - 1].get (), true); - } - - PlaybackChange change = change_playback (playlist); - - LEAVE; - - hook_call ("playlist position", aud::to_ptr (playlist_num)); - call_playback_change_hooks (change); - return true; + ENTER; + auto id = get_blank_locked (); + RETURN (Playlist (id)); } -static bool shuffle_next (PlaylistData * playlist) +EXPORT Playlist Playlist::temporary_playlist () { - bool by_album = aud_get_bool (nullptr, "album_shuffle"); - - // helper #1: determine whether two entries are in the same album - auto same_album = [] (const Tuple & a, const Tuple & b) - { - String album = a.get_str (Tuple::Album); - return (album && album == b.get_str (Tuple::Album)); - }; + ENTER; - // helper #2: determine whether an entry is among the shuffle choices - auto is_choice = [&] (Entry * prev, Entry * entry) - { - return (! entry->shuffle_num) && (! by_album || ! prev || - prev->shuffle_num || ! same_album (prev->tuple, entry->tuple)); - }; + const char * title = _(temp_title); + ID * id = nullptr; - if (playlist->position) + for (auto & playlist : playlists) { - // step #1: check to see if the shuffle order is already established - Entry * next = nullptr; - - for (auto & entry : playlist->entries) - { - if (entry->shuffle_num > playlist->position->shuffle_num && - (! next || entry->shuffle_num < next->shuffle_num)) - next = entry.get (); - } - - if (next) - { - set_position (playlist, next, false); - return true; - } - - // step #2: check to see if we should advance to the next entry - if (by_album && playlist->position->number + 1 < playlist->entries.len ()) + if (! strcmp (playlist->title, title)) { - next = playlist->entries[playlist->position->number + 1].get (); - - if (! next->shuffle_num && same_album (playlist->position->tuple, next->tuple)) - { - set_position (playlist, next, true); - return true; - } + id = playlist->id (); + break; } } - // step #3: count the number of possible shuffle choices - int choices = 0; - Entry * prev = nullptr; - - for (auto & entry : playlist->entries) - { - if (is_choice (prev, entry.get ())) - choices ++; - - prev = entry.get (); - } - - if (! choices) - return false; - - // step #4: pick one of those choices by random and find it again - choices = rand () % choices; - prev = nullptr; - - for (auto & entry : playlist->entries) + if (! id) { - if (is_choice (prev, entry.get ())) - { - if (! choices) - { - set_position (playlist, entry.get (), true); - return true; - } - - choices --; - } - - prev = entry.get (); + id = get_blank_locked (); + id->data->title = String (title); } - return false; // never reached + RETURN (Playlist (id)); } -static void shuffle_reset (PlaylistData * playlist) +EXPORT PluginHandle * Playlist::entry_decoder (int entry_num, GetMode mode, String * error) const { - playlist->last_shuffle_num = 0; - - for (auto & entry : playlist->entries) - entry->shuffle_num = 0; + ENTER_GET_PLAYLIST (nullptr); + wait_for_entry (playlist, entry_num, (mode == Wait), false); + PluginHandle * decoder = playlist->entry_decoder (entry_num, error); + RETURN (decoder); } -static bool next_song_locked (PlaylistData * playlist, bool repeat, int hint) +EXPORT Tuple Playlist::entry_tuple (int entry_num, GetMode mode, String * error) const { - int entries = playlist->entries.len (); - if (! entries) - return false; - - if (playlist->queued.len ()) - { - set_position (playlist, playlist->queued[0], true); - playlist->queued.remove (0, 1); - playlist->position->queued = false; - - queue_update (Selection, playlist, playlist->position->number, 1, QueueChanged); - } - else if (aud_get_bool (nullptr, "shuffle")) - { - if (! shuffle_next (playlist)) - { - if (! repeat) - return false; - - shuffle_reset (playlist); - - if (! shuffle_next (playlist)) - return false; - } - } - else - { - if (hint >= entries) - { - if (! repeat) - return false; - - hint = 0; - } - - set_position (playlist, playlist->entries[hint].get (), true); - } - - return true; + ENTER_GET_PLAYLIST (Tuple ()); + wait_for_entry (playlist, entry_num, false, (mode == Wait)); + Tuple tuple = playlist->entry_tuple (entry_num, error); + RETURN (tuple); } -bool playlist_next_song (int playlist_num, bool repeat) +EXPORT void Playlist::rescan_file (const char * filename) { - ENTER_GET_PLAYLIST (false); - - int hint = playlist->position ? playlist->position->number + 1 : 0; - - if (! next_song_locked (playlist, repeat, hint)) - RETURN (false); + ENTER; - PlaybackChange change = change_playback (playlist); + for (auto & playlist : playlists) + playlist->reset_tuple_of_file (filename); LEAVE; - - hook_call ("playlist position", aud::to_ptr (playlist_num)); - call_playback_change_hooks (change); - return true; -} - -static Entry * get_playback_entry (int serial) -{ - if (! playback_check_serial (serial)) - return nullptr; - - assert (playing_playlist && playing_playlist->position); - return playing_playlist->position; } // called from playback thread @@ -2202,10 +1076,12 @@ DecodeInfo playback_entry_read (int serial) { ENTER; DecodeInfo dec; - Entry * entry; - if ((entry = get_playback_entry (serial))) + if (playback_check_serial (serial)) { + auto playlist = playing_id->data; + auto entry = playlist->entry_at (playlist->position ()); + ScanItem * item = scan_list_find_entry (entry); assert (item && item->for_playback); @@ -2216,13 +1092,17 @@ DecodeInfo playback_entry_read (int serial) request->run (); ENTER; - if ((entry = get_playback_entry (serial))) + if (playback_check_serial (serial)) { - playback_set_info (entry->number, entry->tuple.ref ()); - art_cache_current (entry->filename, std::move (request->image_data), - std::move (request->image_file)); + assert (playlist == playing_id->data); - dec.filename = entry->filename; + int pos = playlist->position (); + playback_set_info (pos, playlist->entry_tuple (pos)); + + art_cache_current (request->filename, + std::move (request->image_data), std::move (request->image_file)); + + dec.filename = request->filename; dec.ip = request->ip; dec.file = std::move (request->file); dec.error = std::move (request->error); @@ -2238,14 +1118,9 @@ DecodeInfo playback_entry_read (int serial) void playback_entry_set_tuple (int serial, Tuple && tuple) { ENTER; - Entry * entry = get_playback_entry (serial); - /* 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); - } + if (playback_check_serial (serial)) + playing_id->data->update_playback_entry (std::move (tuple)); LEAVE; } @@ -2265,20 +1140,20 @@ void playlist_save_state () if (! handle) RETURN (); - fprintf (handle, "active %d\n", active_playlist ? active_playlist->number : -1); - fprintf (handle, "playing %d\n", playing_playlist ? playing_playlist->number : -1); + fprintf (handle, "active %d\n", active_id ? active_id->index : -1); + fprintf (handle, "playing %d\n", playing_id ? playing_id->index : -1); for (auto & playlist : playlists) { - fprintf (handle, "playlist %d\n", playlist->number); + fprintf (handle, "playlist %d\n", playlist->id ()->index); if (playlist->filename) fprintf (handle, "filename %s\n", (const char *) playlist->filename); - fprintf (handle, "position %d\n", playlist->position ? playlist->position->number : -1); + fprintf (handle, "position %d\n", playlist->position ()); /* resume state is stored per-playlist for historical reasons */ - bool is_playing = (playlist.get () == playing_playlist); + bool is_playing = (playlist->id () == playing_id); fprintf (handle, "resume-state %d\n", (is_playing && paused) ? ResumePause : ResumePlay); fprintf (handle, "resume-time %d\n", is_playing ? time : playlist->resume_time); } @@ -2303,19 +1178,19 @@ void playlist_load_state () if (parser.get_int ("active", playlist_num)) { - if (! (active_playlist = lookup_playlist (playlist_num))) - active_playlist = playlists[0].get (); + if (playlist_num >= 0 && playlist_num < playlists.len ()) + active_id = playlists[playlist_num]->id (); + parser.next (); } if (parser.get_int ("playing", resume_playlist)) parser.next (); - while (parser.get_int ("playlist", playlist_num) && playlist_num >= 0 && - playlist_num < playlists.len ()) + while (parser.get_int ("playlist", playlist_num) && + playlist_num >= 0 && playlist_num < playlists.len ()) { PlaylistData * playlist = playlists[playlist_num].get (); - int entries = playlist->entries.len (); parser.next (); @@ -2325,10 +1200,10 @@ void playlist_load_state () int position = -1; if (parser.get_int ("position", position)) + { + playlist->set_position (position); parser.next (); - - if (position >= 0 && position < entries) - set_position (playlist, playlist->entries [position].get (), true); + } /* resume state is stored per-playlist for historical reasons */ int resume_state = ResumePlay; @@ -2350,30 +1225,19 @@ void playlist_load_state () fclose (handle); /* set initial focus and selection */ - /* clear updates queued during init sequence */ - for (auto & playlist : playlists) { - Entry * focus = playlist->position; - if (! focus && playlist->entries.len ()) - focus = playlist->entries[0].get (); + int focus = playlist->position (); + if (focus < 0 && playlist->n_entries ()) + focus = 0; - if (focus) + if (focus >= 0) { - focus->selected = true; - playlist->focus = focus; - playlist->selected_count = 1; - playlist->selected_length = focus->length; + playlist->set_focus (focus); + playlist->select_entry (focus, true); } - - playlist->next_update = Update (); - playlist->last_update = Update (); } - queued_update.stop (); - update_level = NoUpdate; - update_delayed = false; - LEAVE; } @@ -2382,5 +1246,5 @@ EXPORT void aud_resume () if (aud_get_bool (nullptr, "always_resume_paused")) resume_paused = true; - aud_playlist_play (resume_playlist, resume_paused); + Playlist::by_index (resume_playlist).start_playback (resume_paused); } diff --git a/src/libaudcore/playlist.h b/src/libaudcore/playlist.h index ef63a28..18292dc 100644 --- a/src/libaudcore/playlist.h +++ b/src/libaudcore/playlist.h @@ -1,6 +1,6 @@ /* * playlist.h - * Copyright 2010-2013 John Lindgren + * Copyright 2010-2017 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -25,372 +25,367 @@ #include <libaudcore/index.h> #include <libaudcore/tuple.h> -namespace Playlist { - -/* The values which can be passed to the "playlist update" hook. Selection - * means that entries have been selected or unselected, or that entries have - * been added to or removed from the queue. Metadata means that new metadata - * has been read for some entries, or that the title or filename of a playlist - * has changed, and implies Selection. Structure covers any other change, and - * implies both Selection and Metadata. */ -enum UpdateLevel { - NoUpdate = 0, - Selection, - Metadata, - Structure -}; +/* + * Persistent handle attached to a playlist. + * Follows the same playlist even if playlists are reordered. + * Does not prevent the playlist from being deleted (check exists()). + */ +class Playlist +{ +public: + /* --- TYPES --- */ + + /* Opaque type which uniquely identifies a playlist */ + struct ID; + + /* The values which can be passed to the "playlist update" hook. Selection + * means that entries have been selected or unselected, or that entries have + * been added to or removed from the queue. Metadata means that new metadata + * has been read for some entries, or that the title or filename of a playlist + * has changed, and implies Selection. Structure covers any other change, and + * implies both Selection and Metadata. */ + enum UpdateLevel { + NoUpdate = 0, + Selection, + Metadata, + Structure + }; + + struct Update { + UpdateLevel level; // type of update + int before; // number of unaffected entries at playlist start + int after; // number of unaffected entries at playlist end + bool queue_changed; // true if entries have been added to/removed from queue + }; + + /* Preset sorting "schemes" */ + enum SortType { + Path, // entry's entire URI + Filename, // base name (no folder path) + Title, + Album, + Artist, + AlbumArtist, + Date, // release date (not modification time) + Genre, + Track, + FormattedTitle, + Length, + Comment, + n_sort_types + }; + + /* Possible behaviors for entry_{decoder, tuple}. */ + enum GetMode { + 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 save_formats() */ + struct SaveFormat { + String name; // human-readable format name + Index<String> exts; // supported filename extensions + }; + + typedef bool (* FilterFunc) (const char * filename, void * user); + typedef int (* StringCompareFunc) (const char * a, const char * b); + typedef int (* TupleCompareFunc) (const Tuple & a, const Tuple & b); + + /* --- CONSTRUCTOR ETC. --- */ + + /* Default constructor; indicates "no playlist" */ + constexpr Playlist () : m_id (nullptr) {} + + bool operator== (const Playlist & b) const { return m_id == b.m_id; } + bool operator!= (const Playlist & b) const { return m_id != b.m_id; } + + /* The number of the playlist in display order, starting from 0. + * Returns -1 if the playlist no longer exists. */ + int index () const; + + /* True if the playlist exists (i.e. has not been deleted). */ + bool exists () const { return index () >= 0; } + + /* --- CORE (STATIC) API --- */ + + /* Returns the number of playlists currently open (>= 1). */ + static int n_playlists (); + + /* Looks up a playlist by display order. */ + static Playlist by_index (int at); + + /* Adds a new playlist before the one numbered <at> (-1 = insert at end). */ + static Playlist insert_playlist (int at); + + /* Moves a contiguous block of <count> playlists starting with the one + * numbered <from> such that that playlist ends up at the position <to>. */ + static void reorder_playlists (int from, int to, int count); + + /* Returns the active (i.e. displayed) playlist. */ + static Playlist active_playlist (); + + /* Convenience function which adds a new playlist after the active one and + * then sets the new one as active. Returns the new playlist. */ + static Playlist new_playlist (); + + /* Returns the currently playing playlist. If no playlist is playing, + * returns Playlist(). */ + static Playlist playing_playlist (); + + /* Returns the number of a "blank" playlist. The active playlist is + * returned if it has the default title and has no entries; otherwise, a new + * playlist is added and returned. */ + static Playlist blank_playlist (); + + /* Returns the number of the "temporary" playlist (which is no different + * from any other playlist except in name). If the playlist does not exist, + * a "blank" playlist is renamed to become the temporary playlist. */ + static Playlist temporary_playlist (); + + /* Discards the metadata stored for all the entries that refer to a + * particular song file, in whatever playlist they appear, and starts + * reading it afresh from that file in the background. */ + static void rescan_file (const char * filename); + + /* --- CORE (NON-STATIC) API --- */ + + /* Gets/sets the filename associated with this playlist. + * (Audacious currently makes no use of the filename.) */ + String get_filename () const; + void set_filename (const char * filename) const; + + /* Gets/sets the title of the playlist. */ + String get_title () const; + void set_title (const char * title) const; + + /* Closes the playlist. + * The playlist is not saved, and no confirmation is presented to the user. + * When the last playlist is closed, a new one is added in its place. + * When the active playlist is closed, another is made active. + * When the playing playlist is closed, playback stops. */ + void remove_playlist () const; + + /* Makes this the active (i.e. displayed) playlist. */ + void activate () const; + + /* Starts playback of this playlist, unless it is empty. + * Resumes from the position last played if possible. + * If <paused> is true, starts playback in a paused state. */ + void start_playback (bool paused = false) const; + + /* Returns the number of entries (numbered from 0). */ + int n_entries () const; + + /* Adds a single song file, playlist file, or folder before the entry <at>. + * If <at> is negative or equal to the number of entries, the item is added + * after the last entry. <tuple> may be null, in which case Audacious will + * attempt to read metadata from the song file. If <play> is true, + * Audacious will begin playback of the items once they have been added. + * + * This function is asynchronous (the items are added in the background). */ + void insert_entry (int at, const char * filename, Tuple && tuple, bool play) const; + + /* Adds multiple song files, playlist files, or folders to a playlist. */ + void insert_items (int at, Index<PlaylistAddItem> && items, bool play) const; + + /* Similar to insert_items() but allows the caller to prevent some items + * from being added by returning false from the <filter> callback. Useful + * for searching a folder and adding only new files to the playlist. <user> + * is an opaque pointer passed to the callback. */ + void insert_filtered (int at, Index<PlaylistAddItem> && items, + FilterFunc filter, void * user, bool play) const; + + /* Removes entries from the playlist. The playback position may be moved, + * or playback may be stopped (according to user preference). */ + void remove_entries (int at, int number) const; + void remove_entry (int at) const { remove_entries (at, 1); } + void remove_all_entries () const { remove_entries (0, -1); } + + /* Returns an entry's filename. */ + String entry_filename (int entry) const; + + /* Returns an entry's decoder plugin. On error, or if the entry has not yet + * been scanned, returns nullptr according to <mode>. An optional error + * message may be returned. */ + PluginHandle * entry_decoder (int entry, GetMode mode = Wait, String * error = nullptr) const; + + /* Returns an entry's metadata. The state of the returned tuple may + * indicate that the entry has not yet been scanned, or an error occurred, + * according to <mode>. An optional error message may be returned. */ + Tuple entry_tuple (int entry, GetMode mode = Wait, String * error = nullptr) const; + + /* Gets/sets the playing or last-played entry (-1 = no entry). + * Affects playback only if this playlist is currently playing. + * set_position(get_position()) restarts playback from 0:00. + * set_position(-1) stops playback. */ + int get_position () const; + void set_position (int position) const; + + /* Advances the playlist position to the next entry in playback order, + * taking current shuffle settings into account. At the end of the + * playlist, wraps around to the beginning if <repeat> is true. Returns + * true on success, false if playlist position was not changed. */ + bool next_song (bool repeat) const; + + /* Returns the playlist position to the previous entry in playback order. + * Does not support wrapping past the beginning of the playlist. Returns + * true on success, false if playlist position was not changed. */ + bool prev_song () const; + + /* Gets/sets the entry which has keyboard focus (-1 = no entry). */ + int get_focus () const; + void set_focus (int entry) const; + + /* Gets/sets whether an entry is selected. */ + bool entry_selected (int entry) const; + void select_entry (int entry, bool selected) const; + + /* Returns the number of selected entries. + * An optional range of entries to examine may be specified. */ + int n_selected (int at = 0, int number = -1) const; + + /* Selects all (or none) of the entries in a playlist. */ + void select_all (bool selected) const; + + /* Moves a selected entry within a playlist by an offset of <distance> + * entries. Other selected entries are gathered around it. Returns the + * offset by which the entry was actually moved (which may be less than the + * requested offset. */ + int shift_entries (int position, int distance) const; + + /* Removes all selected entries. */ + void remove_selected () const; + + /* Sorts the entries in a playlist based on filename. The callback function + * should return negative if the first filename comes before the second, + * positive if it comes after, or zero if the two are indistinguishable. */ + void sort_by_filename (StringCompareFunc compare) const; + + /* Sorts the entries in a playlist based on tuple. May fail if metadata + * scanning is still in progress (or has been disabled). */ + void sort_by_tuple (TupleCompareFunc compare) const; + + /* Sorts the entries in a playlist based on formatted title string. May fail if + * metadata scanning is still in progress (or has been disabled). */ + void sort_by_title (StringCompareFunc compare) const; + + /* Sorts only the selected entries in a playlist based on filename. */ + void sort_selected_by_filename (StringCompareFunc compare) const; + + /* Sorts only the selected entries in a playlist based on tuple. May fail if + * metadata scanning is still in progress (or has been disabled). */ + void sort_selected_by_tuple (TupleCompareFunc compare) const; + + /* Sorts only the selected entries in a playlist based on formatted title + * string. May fail if metadata scanning is still in progress (or has been + * disabled). */ + void sort_selected_by_title (StringCompareFunc compare) const; + + /* Reverses the order of the entries in a playlist. */ + void reverse_order () const; + + /* Reorders the entries in a playlist randomly. */ + void randomize_order () const; + + /* Reverses the order of the selected entries in a playlist. */ + void reverse_selected () const; + + /* Reorders the selected entries in a playlist randomly. */ + void randomize_selected () const; -struct Update { - UpdateLevel level; // type of update - int before; // number of unaffected entries at playlist start - int after; // number of unaffected entries at playlist end - bool queue_changed; // true if entries have been added to/removed from queue -}; + /* Discards the metadata stored for entries in a playlist and starts reading + * it fresh from the song files in the background. */ + void rescan_all () const; + void rescan_selected () const; -/* The values which can be passed to playlist_sort_by_scheme(), - * playlist_sort_selected_by_scheme(), and - * playlist_remove_duplicates_by_scheme(). PlaylistSort::Path means the entire - * URI of a song file; PlaylistSort::Filename means the portion after the last - * "/" (forward slash). PlaylistSort::Date means the song's release date (not - * the file's modification time). */ -enum SortType { - Path, - Filename, - Title, - Album, - Artist, - AlbumArtist, - Date, - Genre, - Track, - FormattedTitle, - Length, - n_sort_types -}; + /* Calculates the length in milliseconds of entries in a playlist. Only + * takes into account entries for which metadata has already been read. */ + int64_t total_length_ms () const; + int64_t selected_length_ms () const; -/* Possible behaviors for playlist_entry_get_{decoder, tuple}. */ -enum GetMode { - 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 -}; + /* Returns the number of entries in a playlist queue. */ + int n_queued () const; -/* Format descriptor returned by playlist_save_formats() */ -struct SaveFormat { - String name; // human-readable format name - Index<String> exts; // supported filename extensions -}; + /* Adds an entry to the queue at <pos> (-1 = at end of queue). + * The same entry cannot be added to the queue more than once. */ + void queue_insert (int pos, int entry) const; + void queue_insert_selected (int pos) const; -} // namespace Playlist - -typedef bool (* PlaylistFilterFunc) (const char * filename, void * user); -typedef int (* PlaylistStringCompareFunc) (const char * a, const char * b); -typedef int (* PlaylistTupleCompareFunc) (const Tuple & a, const Tuple & b); - -/* --- PLAYLIST CORE API --- */ - -/* Returns the number of playlists currently open. There will always be at - * least one playlist open. The playlists are numbered starting from zero. */ -int aud_playlist_count (); - -/* Adds a new playlist before the one numbered <at>. If <at> is -1 or equal to - * the number of playlists, adds a new playlist after the last one. */ -void aud_playlist_insert (int at); - -/* Moves a contiguous block of <count> playlists starting with the one numbered - * <from> such that that playlist ends up at the position <to>. */ -void aud_playlist_reorder (int from, int to, int count); - -/* Closes a playlist. CAUTION: The playlist is not saved, and no confirmation - * is presented to the user. If <playlist> is the only playlist, a new playlist - * is added. If <playlist> is the active playlist, another playlist is marked - * active. If <playlist> is the currently playing playlist, playback is - * stopped. */ -void aud_playlist_delete (int playlist); - -/* Returns a unique non-negative integer which can be used to identify a given - * playlist even if its numbering changes (as when playlists are reordered). - * On error, returns -1. */ -int aud_playlist_get_unique_id (int playlist); - -/* Returns the number of the playlist identified by a given integer ID as - * returned by playlist_get_unique_id(). If the playlist no longer exists, - * returns -1. */ -int aud_playlist_by_unique_id (int id); - -/* Sets the filename associated with a playlist. (Audacious currently makes no - * use of the filename.) */ -void aud_playlist_set_filename (int playlist, const char * filename); - -/* Returns the filename associated with a playlist. */ -String aud_playlist_get_filename (int playlist); - -/* Sets the title associated with a playlist. */ -void aud_playlist_set_title (int playlist, const char * title); - -/* Returns the title associated with a playlist. */ -String aud_playlist_get_title (int playlist); - -/* Sets the active playlist. This is the playlist that user interfaces will - * show to the user. */ -void aud_playlist_set_active (int playlist); - -/* Returns the number of the active playlist. */ -int aud_playlist_get_active (); - -/* Convenience function which adds a new playlist after the active one and then - * sets the new one as active. Returns the number of the new playlist. */ -int aud_playlist_new (); - -/* Starts playback of a playlist, resuming from the position last played if - * possible. If <playlist> is -1 or if the requested playlist is empty, stops - * playback. If <paused> is true, starts playback in a paused state. */ -void aud_playlist_play (int playlist, bool paused = false); - -/* Returns the number of the currently playing playlist. If no playlist is - * playing, returns -1. */ -int aud_playlist_get_playing (); - -/* Returns the number of a "blank" playlist. The active playlist is returned if - * it has the default title and has no entries; otherwise, a new playlist is - * added and returned. */ -int aud_playlist_get_blank (); - -/* Returns the number of the "temporary" playlist (which is no different from - * any other playlist except in name). If the playlist does not exist, a - * "blank" playlist is obtained from playlist_get_blank() and is renamed to - * become the temporary playlist. */ -int aud_playlist_get_temporary (); - -/* Returns the number of entries in a playlist. The entries are numbered - * starting from zero. */ -int aud_playlist_entry_count (int playlist); - -/* Adds a song file, playlist file, or folder to a playlist before the entry - * numbered <at>. If <at> is negative or equal to the number of entries, the - * item is added after the last entry. <tuple> may be nullptr, in which case - * Audacious will attempt to read metadata from the song file. If <play> is - * true, Audacious will begin playback of the items once they have been - * added. - * - * Because adding items to the playlist can be a slow process, this function may - * return before the process is complete. Hence, the caller must not assume - * that there will be new entries in the playlist immediately. */ -void aud_playlist_entry_insert (int playlist, int at, const char * filename, - Tuple && tuple, bool play); - -/* Similar to playlist_entry_insert, adds multiple song files, playlist files, - * or folders to a playlist. */ -void aud_playlist_entry_insert_batch (int playlist, int at, - Index<PlaylistAddItem> && items, bool play); - -/* Similar to playlist_entry_insert_batch, but allows the caller to prevent some - * items from being added by returning false from the <filter> callback. Useful - * for searching a folder and adding only new files to the playlist. <user> is - * an additional, untyped pointer passed to the callback. */ -void aud_playlist_entry_insert_filtered (int playlist, int at, - Index<PlaylistAddItem> && items, PlaylistFilterFunc filter, void * user, - bool play); - -/* Removes a contiguous block of <number> entries starting from the one numbered - * <at> from a playlist. If necessary, the playback position is moved elsewhere - * in the playlist and playback is restarted (or stopped). */ -void aud_playlist_entry_delete (int playlist, int at, int number); - -/* Returns the filename of an entry. */ -String aud_playlist_entry_get_filename (int playlist, int entry); - -/* Returns a handle to the decoder plugin associated with an entry. On error, - * 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::Wait, String * error = nullptr); - -/* 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::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 - * currently playing playlist, playback is restarted (or stopped). */ -void aud_playlist_set_position (int playlist, int position); - -/* Returns the playback position, or -1 if it is not set. Note that the - * position may be set even if <playlist> is not currently playing. */ -int aud_playlist_get_position (int playlist); - -/* Sets the entry which has keyboard focus (-1 means no entry). */ -void aud_playlist_set_focus (int playlist, int entry); - -/* Gets the entry which has keyboard focus (-1 means no entry). */ -int aud_playlist_get_focus (int playlist); - -/* Sets whether an entry is selected. */ -void aud_playlist_entry_set_selected (int playlist, int entry, bool selected); - -/* Returns whether an entry is selected. */ -bool aud_playlist_entry_get_selected (int playlist, int entry); - -/* Returns the number of selected entries in a playlist. */ -int aud_playlist_selected_count (int playlist); - -/* Selects all (or none) of the entries in a playlist. */ -void aud_playlist_select_all (int playlist, bool selected); - -/* Moves a selected entry within a playlist by an offset of <distance> entries. - * Other selected entries are gathered around it. Returns the offset by which - * the entry was actually moved, which may be less in absolute value than the - * requested offset. */ -int aud_playlist_shift (int playlist, int position, int distance); - -/* Removes the selected entries from a playlist. If necessary, the playback - * position is moved elsewhere in the playlist and playback is restarted (or - * stopped). */ -void aud_playlist_delete_selected (int playlist); - -/* Sorts the entries in a playlist based on filename. The callback function - * should return negative if the first filename comes before the second, - * positive if it comes after, or zero if the two are indistinguishable. */ -void aud_playlist_sort_by_filename (int playlist, PlaylistStringCompareFunc compare); - -/* Sorts the entries in a playlist based on tuple. May fail if metadata - * scanning is still in progress (or has been disabled). */ -void aud_playlist_sort_by_tuple (int playlist, PlaylistTupleCompareFunc compare); - -/* Sorts the entries in a playlist based on formatted title string. May fail if - * metadata scanning is still in progress (or has been disabled). */ -void aud_playlist_sort_by_title (int playlist, PlaylistStringCompareFunc compare); - -/* Sorts only the selected entries in a playlist based on filename. */ -void aud_playlist_sort_selected_by_filename (int playlist, PlaylistStringCompareFunc compare); - -/* Sorts only the selected entries in a playlist based on tuple. May fail if - * metadata scanning is still in progress (or has been disabled). */ -void aud_playlist_sort_selected_by_tuple (int playlist, PlaylistTupleCompareFunc compare); - -/* Sorts only the selected entries in a playlist based on formatted title - * string. May fail if metadata scanning is still in progress (or has been - * disabled). */ -void aud_playlist_sort_selected_by_title (int playlist, PlaylistStringCompareFunc compare); - -/* Reverses the order of the entries in a playlist. */ -void aud_playlist_reverse (int playlist); - -/* Reorders the entries in a playlist randomly. */ -void aud_playlist_randomize (int playlist); - -/* Reverses the order of the selected entries in a playlist. */ -void aud_playlist_reverse_selected (int playlist); - -/* Reorders the selected entries in a playlist randomly. */ -void aud_playlist_randomize_selected (int playlist); - -/* Discards the metadata stored for all the entries in a playlist and starts - * reading it afresh from the song files in the background. */ -void aud_playlist_rescan (int playlist); + /* Returns the entry at the given queue position. */ + int queue_get_entry (int pos) const; + + /* Returns the queue position of the given entry (-1 if not queued). */ + int queue_find_entry (int entry) const; -/* Like playlist_rescan, but applies only to the selected entries in a playlist. */ -void aud_playlist_rescan_selected (int playlist); - -/* Discards the metadata stored for all the entries that refer to a particular - * song file, in whatever playlist they appear, and starts reading it afresh - * from that file in the background. */ -void aud_playlist_rescan_file (const char * filename); + /* Removes entries from the queue. */ + void queue_remove (int pos, int number = 1) const; + void queue_remove_all () const { queue_remove (0, -1); } -/* Calculates the total length in milliseconds of all the entries in a playlist. - * Only takes into account entries for which metadata has already been read. */ -int64_t aud_playlist_get_total_length (int playlist); + /* Removes the selected entries in a playlist from the queue, if they are in it. */ + void queue_remove_selected () const; -/* Calculates the total length in milliseconds of only the selected entries in a - * playlist. Only takes into account entries for which metadata has already - * been read. */ -int64_t aud_playlist_get_selected_length (int playlist); + /* Returns true if a "playlist update" hook call is pending. + * A running hook call is not considered pending. */ + bool update_pending () const; + static bool update_pending_any (); -/* Returns the number of entries in a playlist queue. The entries are numbered - * starting from zero, lower numbers being played first. */ -int aud_playlist_queue_count (int playlist); + /* May be called within the "playlist update" hook to determine the update + * level and number of entries changed in a playlist. */ + Update update_detail () const; -/* Adds an entry to a playlist's queue before the entry numbered <at> in the - * queue. If <at> is negative or equal to the number of entries in the queue, - * adds the entry after the last one in the queue. The same entry cannot be - * added to the queue more than once. */ -void aud_playlist_queue_insert (int playlist, int at, int entry); + /* Returns true if entries are being added in the background. */ + bool add_in_progress () const; + static bool add_in_progress_any (); -/* Adds the selected entries in a playlist to the queue, if they are not already - * in it. */ -void aud_playlist_queue_insert_selected (int playlist, int at); + /* Returns true if entries are being scanned in the background. */ + bool scan_in_progress () const; + static bool scan_in_progress_any (); -/* Returns the position in the playlist of the entry at a given position in the - * queue. */ -int aud_playlist_queue_get_entry (int playlist, int at); + /* --- UTILITY API --- */ -/* Returns the position in the queue of the entry at a given position in the - * playlist. If it is not in the queue, returns -1. */ -int aud_playlist_queue_find_entry (int playlist, int entry); + /* Sorts entries according to a preset scheme. */ + void sort_entries (SortType scheme) const; + void sort_selected (SortType scheme) const; -/* Removes a contiguous block of <number> entries starting with the one numbered - * <at> from the queue. */ -void aud_playlist_queue_delete (int playlist, int at, int number); - -/* Removes the selected entries in a playlist from the queue, if they are in it. */ -void aud_playlist_queue_delete_selected (int playlist); - -/* Returns true if a "playlist update" hook call is pending for the given - * playlist (or for any playlist, if <playlist> is -1). If called from within - * the hook, the current hook call is not considered pending. */ -bool aud_playlist_update_pending (int playlist = -1); - -/* May be called within the "playlist update" hook to determine the update level - * and number of entries changed in a playlist. */ -Playlist::Update aud_playlist_update_detail (int playlist); - -/* Returns true if entries are being added to a playlist in the background. - * If <playlist> is -1, checks all playlists. */ -bool aud_playlist_add_in_progress (int playlist); + /* Removes duplicate entries according to a preset scheme. + * The current implementation also sorts the playlist. */ + void remove_duplicates (SortType scheme) const; -/* Returns true if entries in a playlist are being scanned for metadata in - * the background. If <playlist> is -1, checks all playlists. */ -bool aud_playlist_scan_in_progress (int playlist); + /* Removes all entries referring to inaccessible files in a playlist. */ + void remove_unavailable () const; -/* --- PLAYLIST UTILITY API --- */ + /* Selects entries by matching regular expressions. + * Example: To select all titles starting with the letter "A", + * create a blank tuple and set its title field to "^A". */ + void select_by_patterns (const Tuple & patterns) const; -/* Sorts the entries in a playlist according to one of the schemes listed in - * playlist.h. */ -void aud_playlist_sort_by_scheme (int playlist, Playlist::SortType scheme); + /* Saves metadata for the selected entries to an internal cache. + * This will speed up adding those entries to another playlist. */ + void cache_selected () const; -/* Sorts only the selected entries in a playlist according to one of those - * schemes. */ -void aud_playlist_sort_selected_by_scheme (int playlist, Playlist::SortType scheme); + /* Saves the entries in a playlist to a playlist file. + * The format of the file is determined from the file extension. + * <mode> specifies whether to wait for metadata scanning to complete. + * Returns true on success. */ + bool save_to_file (const char * filename, GetMode mode) const; -/* Removes duplicate entries in a playlist according to one of those schemes. - * As currently implemented, first sorts the playlist. */ -void aud_playlist_remove_duplicates_by_scheme (int playlist, Playlist::SortType scheme); + /* Checks a filename for an extension matching a known playlist format. */ + static bool filename_is_playlist (const char * filename); -/* Removes all entries referring to unavailable files in a playlist. ("Remove - * failed" is something of a misnomer for the current behavior.) */ -void aud_playlist_remove_failed (int playlist); + /* Generates a list of the currently supported formats for saving playlists. + * The list should not be cached since it may change as plugins are enabled or + * disabled. */ + static Index<SaveFormat> save_formats (); -/* Selects all the entries in a playlist that match regular expressions stored - * in the fields of a tuple. Does not free the memory used by the tuple. - * Example: To select all the songs whose title starts with the letter "A", - * create a blank tuple and set its title field to "^A". */ -void aud_playlist_select_by_patterns (int playlist, const Tuple & patterns); + /* --- IMPLEMENTATION --- */ -/* 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); - -/* Saves the entries in a playlist to a playlist file. The format of the file - * is determined from the file extension. Returns true on success. */ -bool aud_playlist_save (int playlist, const char * filename, Playlist::GetMode mode); - -/* Generates a list of the currently supported formats for saving playlists. - * The list should not be cached since it may change as plugins are enabled or - * disabled. */ -Index<Playlist::SaveFormat> aud_playlist_save_formats (); +private: + ID * m_id; + + explicit constexpr Playlist (ID * id) : + m_id (id) {} + + friend class PlaylistEx; +}; #endif diff --git a/src/libaudcore/plugin-load.cc b/src/libaudcore/plugin-load.cc index c249fea..5edb062 100644 --- a/src/libaudcore/plugin-load.cc +++ b/src/libaudcore/plugin-load.cc @@ -168,6 +168,7 @@ void plugin_system_init () void plugin_system_cleanup () { plugin_registry_save (); + plugin_registry_cleanup (); for (LoadedModule & loaded : loaded_modules) plugin_unload (loaded); diff --git a/src/libaudcore/plugin-registry.cc b/src/libaudcore/plugin-registry.cc index cdeed7e..3ca79f3 100644 --- a/src/libaudcore/plugin-registry.cc +++ b/src/libaudcore/plugin-registry.cc @@ -118,6 +118,7 @@ static constexpr aud::array<InputKey, const char *> input_key_names = { static aud::array<PluginType, Index<PluginHandle *>> plugins; static aud::array<PluginType, Index<PluginHandle *>> compatible; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static bool modified = false; static StringBuf get_basename (const char * path) { @@ -190,6 +191,9 @@ static void plugin_save (PluginHandle * plugin, FILE * handle) void plugin_registry_save () { + if (! modified) + return; + FILE * handle = open_registry_file ("w"); if (! handle) return; @@ -199,18 +203,25 @@ void plugin_registry_save () for (auto & list : plugins) { for (PluginHandle * plugin : list) - { plugin_save (plugin, handle); + } + + fclose (handle); + modified = false; +} + +void plugin_registry_cleanup () +{ + for (auto & list : plugins) + { + for (PluginHandle * plugin : list) delete plugin; - } list.clear (); } for (auto & list : compatible) list.clear (); - - fclose (handle); } static void transport_plugin_parse (PluginHandle * plugin, TextParser & parser) @@ -556,6 +567,7 @@ void plugin_register (const char * path, int timestamp) plugin->timestamp = timestamp; plugin_get_info (plugin, false); + modified = true; } } else @@ -570,6 +582,7 @@ void plugin_register (const char * path, int timestamp) plugins[plugin->type].append (plugin); plugin_get_info (plugin, true); + modified = true; } } @@ -656,6 +669,7 @@ void plugin_set_enabled (PluginHandle * plugin, PluginEnabled enabled) { plugin->enabled = enabled; plugin_call_watches (plugin); + modified = true; } void plugin_set_failed (PluginHandle * plugin) diff --git a/src/libaudcore/plugins-internal.h b/src/libaudcore/plugins-internal.h index ef293d5..d521c5c 100644 --- a/src/libaudcore/plugins-internal.h +++ b/src/libaudcore/plugins-internal.h @@ -50,6 +50,7 @@ Plugin * plugin_load (const char * path); void plugin_registry_load (); void plugin_registry_prune (); void plugin_registry_save (); +void plugin_registry_cleanup (); void plugin_register (const char * path, int timestamp); PluginEnabled plugin_get_enabled (PluginHandle * plugin); diff --git a/src/libaudcore/probe.cc b/src/libaudcore/probe.cc index b2fff60..81bac55 100644 --- a/src/libaudcore/probe.cc +++ b/src/libaudcore/probe.cc @@ -205,7 +205,7 @@ EXPORT bool aud_file_write_tuple (const char * filename, success = false; if (success) - aud_playlist_rescan_file (filename); + Playlist::rescan_file (filename); return success; } diff --git a/src/libaudcore/probe.h b/src/libaudcore/probe.h index 8944289..c206f6c 100644 --- a/src/libaudcore/probe.h +++ b/src/libaudcore/probe.h @@ -27,23 +27,53 @@ class PluginHandle; class Tuple; class VFSFile; -/* Gets album art for <file> (the URI of a song file) as JPEG or PNG data. If - * the album art is not yet loaded, sets *queued to true, returns nullptr, and - * begins to load the album art in the background. On completion, the "art - * ready" hook is called, with <file> as a parameter. If no album art could be - * loaded, sets *queued to false and returns nullptr. - * - * 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); +/* ====== ALBUM ART API ====== */ + +/* request format */ +enum { + AUD_ART_DATA = (1 << 0), /* image data in memory */ + AUD_ART_FILE = (1 << 1) /* filename of image data on disk */ +}; + +/* opaque type storing art data */ +struct AudArtItem; + +/* don't use these directly, use AudArtPtr */ +const Index<char> * aud_art_data (const AudArtItem * item); +const char * aud_art_file (const AudArtItem * item); +void aud_art_unref (AudArtItem * item); + +/* handle for accessing/tracking album art data */ +class AudArtPtr : public SmartPtr<AudArtItem, aud_art_unref> +{ +public: + AudArtPtr () : SmartPtr () {} + explicit AudArtPtr (AudArtItem * ptr) : SmartPtr (ptr) {} + + const Index<char> * data () const + { return get () ? aud_art_data (get ()) : nullptr; } + const char * file () const + { return get () ? aud_art_file (get ()) : nullptr; } +}; -/* Similar to art_request_data() but returns the URI of an image file. - * (A temporary file will be created if necessary.) */ -const char * aud_art_request_file (const char * file, bool * queued = nullptr); +/* + * Gets album art for <file> (the URI of a song file). The data will be + * returned in the requested <format> (AUD_ART_DATA or AUD_ART_FILE). + * + * This is a non-blocking call. If the data is not yet loaded, it sets *queued + * to true, returns a null pointer, and begins to load the data in the back- + * ground. On completion, the "art ready" hook is called, with <file> as a + * parameter. The data can then be requested again from within the hook. + * + * As a special case, album art data for the currently playing song is preloaded + * by the time the "playback ready" hook is called, so in that case there is no + * need to implement a separate "art ready" handler. + * + * On error, a null pointer is returned and *queued is set to false. + */ +AudArtPtr aud_art_request (const char * file, int format, bool * queued = nullptr); -/* Releases album art returned by art_request_data() or art_request_file(). */ -void aud_art_unref (const char * file); +/* ====== GENERAL PROBING API ====== */ /* The following two functions take an additional VFSFile parameter to allow * opening a file, probing for a decoder, and then reading the song metadata diff --git a/src/libaudcore/runtime.cc b/src/libaudcore/runtime.cc index 5c3660f..7eb8e5e 100644 --- a/src/libaudcore/runtime.cc +++ b/src/libaudcore/runtime.cc @@ -303,6 +303,7 @@ static void do_autosave (void *) { hook_call ("config save", nullptr); save_playlists (false); + plugin_registry_save (); config_save (); } @@ -312,6 +313,7 @@ EXPORT void aud_run () * avoid scanning until the currently playing entry is known, at which time * it can be scanned more efficiently (album art read in the same pass). */ playlist_enable_scan (true); + playlist_clear_updates (); start_plugins_two (); static QueuedFunc autosave; @@ -330,7 +332,7 @@ EXPORT void aud_cleanup () { save_playlists (true); - aud_playlist_play (-1); + aud_drct_stop (); playback_stop (true); adder_cleanup (); diff --git a/src/libaudcore/runtime.h b/src/libaudcore/runtime.h index 570f948..30ad950 100644 --- a/src/libaudcore/runtime.h +++ b/src/libaudcore/runtime.h @@ -52,6 +52,12 @@ enum class OutputStream { AfterEqualizer }; +enum class ReplayGainMode { + Track, + Album, + Automatic +}; + namespace audlog { enum Level { @@ -68,8 +74,13 @@ namespace audlog void subscribe (Handler handler, Level level); void unsubscribe (Handler handler); +#ifdef _WIN32 + void log (Level level, const char * file, int line, const char * func, + const char * format, ...) __attribute__ ((__format__ (gnu_printf, 5, 6))); +#else void log (Level level, const char * file, int line, const char * func, const char * format, ...) __attribute__ ((__format__ (__printf__, 5, 6))); +#endif const char * get_level_name (Level level); } @@ -101,6 +112,7 @@ void aud_set_str (const char * section, const char * name, const char * value); String aud_get_str (const char * section, const char * name); void aud_set_bool (const char * section, const char * name, bool value); bool aud_get_bool (const char * section, const char * name); +void aud_toggle_bool (const char * section, const char * name); void aud_set_int (const char * section, const char * name, int value); int aud_get_int (const char * section, const char * name); void aud_set_double (const char * section, const char * name, double value); diff --git a/src/libaudcore/strpool.cc b/src/libaudcore/strpool.cc index 8f3679d..c9308d7 100644 --- a/src/libaudcore/strpool.cc +++ b/src/libaudcore/strpool.cc @@ -1,6 +1,6 @@ /* * strpool.c - * Copyright 2011-2013 John Lindgren + * Copyright 2011-2017 John Lindgren * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -17,8 +17,6 @@ * the use of this software. */ -#include <assert.h> -#include <stddef.h> #include <stdlib.h> #include <string.h> @@ -30,126 +28,87 @@ #ifdef VALGRIND_FRIENDLY -struct StrNode { - unsigned hash; - char magic; - char str[]; -}; - -#define NODE_SIZE_FOR(s) (offsetof (StrNode, str) + strlen (s) + 1) -#define NODE_OF(s) ((StrNode *) ((s) - offsetof (StrNode, str))) +#include <glib.h> EXPORT char * String::raw_get (const char * str) -{ - if (! str) - return nullptr; - - StrNode * node = (StrNode *) malloc (NODE_SIZE_FOR (str)); - if (! node) - throw std::bad_alloc (); - - node->magic = '@'; - node->hash = str_calc_hash (str); - - strcpy (node->str, str); - return node->str; -} - + { return g_strdup (str); } EXPORT char * String::raw_ref (const char * str) -{ - if (! str) - return nullptr; - - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); - assert (str_calc_hash (str) == node->hash); - - return raw_get (str); -} - + { return g_strdup (str); } EXPORT void String::raw_unref (char * str) -{ - if (! str) - return; + { g_free (str); } - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); - assert (str_calc_hash (str) == node->hash); - - node->magic = 0; - free (node); -} +void string_leak_check () {} EXPORT unsigned String::raw_hash (const char * str) -{ - if (! str) - return 0; - - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); - - return str_calc_hash (str); -} - + { return str_calc_hash (str); } EXPORT bool String::raw_equal (const char * str1, const char * str2) -{ - assert (! str1 || NODE_OF (str1)->magic == '@'); - assert (! str2 || NODE_OF (str2)->magic == '@'); + { return ! strcmp_safe (str1, str2); } - return ! strcmp_safe (str1, str2); -} +#else // ! VALGRIND_FRIENDLY -EXPORT void string_leak_check () +struct StrNode : public MultiHash::Node { -} + /* the characters of the string immediately follow the StrNode struct */ + const char * str () const + { return reinterpret_cast<const char *> (this + 1); } + char * str () + { return reinterpret_cast<char *> (this + 1); } -#else /* ! VALGRIND_FRIENDLY */ + static const StrNode * of (const char * s) + { return reinterpret_cast<const StrNode *> (s) - 1; } + static StrNode * of (char * s) + { return reinterpret_cast<StrNode *> (s) - 1; } -struct StrNode { - MultiHash::Node base; - unsigned refs; - char magic; - char str[1]; // variable size -}; - -#define NODE_SIZE_FOR(s) (offsetof (StrNode, str) + strlen (s) + 1) -#define NODE_OF(s) ((StrNode *) ((s) - offsetof (StrNode, str))) + static StrNode * create (const char * s) + { + auto size = sizeof (StrNode) + strlen (s) + 1; + auto node = static_cast<StrNode *> (malloc (size)); + if (! node) + throw std::bad_alloc (); -static bool match_cb (const MultiHash::Node * node_, const void * data_) -{ - const StrNode * node = (const StrNode *) node_; - const char * data = (const char *) data_; + strcpy (node->str (), s); + return node; + } - return data == node->str || ! strcmp (data, node->str); -} + bool match (const char * data) const + { return data == str () || ! strcmp (data, str ()); } +}; -static MultiHash strpool_table (match_cb); +static MultiHash_T<StrNode, char> strpool_table; -static MultiHash::Node * add_cb (const void * data_, void * state) +struct Getter { - const char * data = (const char *) data_; - - StrNode * node = (StrNode *) malloc (NODE_SIZE_FOR (data)); - if (! node) - throw std::bad_alloc (); + StrNode * node; - node->refs = 1; - node->magic = '@'; - strcpy (node->str, data); + StrNode * add (const char * data) + { + node = StrNode::create (data); + node->refs = 1; + return node; + } - * ((char * *) state) = node->str; - return (MultiHash::Node *) node; -} + bool found (StrNode * node_) + { + node = node_; + __sync_fetch_and_add (& node->refs, 1); + return false; + } +}; -static bool ref_cb (MultiHash::Node * node_, void * state) +struct Remover { - StrNode * node = (StrNode *) node_; + StrNode * add (const char *) + { return nullptr; } - __sync_fetch_and_add (& node->refs, 1); + bool found (StrNode * node) + { + if (! __sync_bool_compare_and_swap (& node->refs, 1, 0)) + return false; - * ((char * *) state) = node->str; - return false; -} + free (node); + return true; + } +}; /* If the pool contains a copy of <str>, increments its reference count. * Otherwise, adds a copy of <str> to the pool with a reference count of one. @@ -161,38 +120,24 @@ EXPORT char * String::raw_get (const char * str) if (! str) return nullptr; - char * ret = nullptr; - strpool_table.lookup (str, str_calc_hash (str), add_cb, ref_cb, & ret); - return ret; + Getter op; + strpool_table.lookup (str, str_calc_hash (str), op); + return op.node->str (); } /* Increments the reference count of <str>, where <str> is the address of a * string already in the pool. Faster than calling raw_get() a second time. * Returns <str> for convenience. If <str> is null, simply returns null with no * side effects. */ -EXPORT char * String::raw_ref (const char * str) +EXPORT char * String::raw_ref (const char * str_) { + auto str = const_cast<char *> (str_); if (! str) return nullptr; - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); - + auto node = StrNode::of (str); __sync_fetch_and_add (& node->refs, 1); - - return (char *) str; -} - -static bool remove_cb (MultiHash::Node * node_, void * state) -{ - StrNode * node = (StrNode *) node_; - - if (! __sync_bool_compare_and_swap (& node->refs, 1, 0)) - return false; - - node->magic = 0; - free (node); - return true; + return str; } /* Decrements the reference count of <str>, where <str> is the address of a @@ -204,13 +149,11 @@ EXPORT void String::raw_unref (char * str) if (! str) return; - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); + auto node = StrNode::of (str); while (1) { - int refs = __sync_fetch_and_add (& node->refs, 0); - + unsigned refs = __sync_fetch_and_add (& node->refs, 0); if (refs > 1) { if (__sync_bool_compare_and_swap (& node->refs, refs, refs - 1)) @@ -218,49 +161,36 @@ EXPORT void String::raw_unref (char * str) } else { - int status = strpool_table.lookup (node->str, node->base.hash, nullptr, - remove_cb, nullptr); - - assert (status & MultiHash::Found); + Remover op; + int status = strpool_table.lookup (str, node->hash, op); + if (! (status & MultiHash::Found)) + throw std::bad_alloc (); if (status & MultiHash::Removed) break; } } } -static bool leak_cb (MultiHash::Node * node, void * state) -{ - AUDWARN ("String leaked: %s\n", ((StrNode *) node)->str); - return false; -} - void string_leak_check () { - strpool_table.iterate (leak_cb, nullptr); + strpool_table.iterate ([] (const StrNode * node) { + AUDWARN ("String leaked: %s\n", node->str ()); + return false; + }); } /* Returns the cached hash value of a pooled string (or 0 for null). */ EXPORT unsigned String::raw_hash (const char * str) { - if (! str) - return 0; - - StrNode * node = NODE_OF (str); - assert (node->magic == '@'); - - return node->base.hash; + return str ? StrNode::of (str)->hash : 0; } - /* Checks whether two pooled strings are equal. Since the pool never contains * duplicate strings, this is a simple pointer comparison and thus much faster * than strcmp(). null is considered equal to null but not equal to any string. */ EXPORT bool String::raw_equal (const char * str1, const char * str2) { - assert (! str1 || NODE_OF (str1)->magic == '@'); - assert (! str2 || NODE_OF (str2)->magic == '@'); - return str1 == str2; } -#endif /* ! VALGRIND_FRIENDLY */ +#endif // ! VALGRIND_FRIENDLY diff --git a/src/libaudcore/templates.h b/src/libaudcore/templates.h index e78aea0..d14c3d8 100644 --- a/src/libaudcore/templates.h +++ b/src/libaudcore/templates.h @@ -98,6 +98,25 @@ inline T from_ptr (void * v) return u.t; } +// Function wrappers (or "casts") for interaction with C-style APIs +// ================================================================ + +template<class T> +void delete_obj (void * obj) + { (void) sizeof (T); delete (T *) obj; } + +template<class T> +void delete_typed (T * obj) + { (void) sizeof (T); delete obj; } + +template<class T, void (* func) (void *)> +void typed_func (T * obj) + { func (obj); } + +template<class T, void (T::* func) ()> +static void obj_member (void * obj) + { (((T *) obj)->* func) (); } + // Wrapper class allowing enumerations to be used as array indexes; // the enumeration must begin with zero and have a "count" constant // ================================================================ diff --git a/src/libaudcore/tests/test.cc b/src/libaudcore/tests/test.cc index dac2724..a7a6ba8 100644 --- a/src/libaudcore/tests/test.cc +++ b/src/libaudcore/tests/test.cc @@ -38,9 +38,19 @@ static void test_audio_conversion () {0x800000, 0x800001, 0x800002, -2, -1, 0, 1, 2, 0x7ffffe, 0x7fffff}; float f[10]; + char packed[30]; int32_t out[10]; audio_from_int (in, FMT_S24_NE, f, 10); + + for (int format = FMT_S24_3LE; format <= FMT_U24_3BE; format ++) + { + memset (packed, 0, sizeof packed); + audio_to_int (f, packed, format, 10); + memset (f, 0, sizeof f); + audio_from_int (packed, format, f, 10); + } + audio_to_int (f, out, FMT_S24_NE, 10); assert (f[0] == -1.0f); @@ -50,6 +60,67 @@ static void test_audio_conversion () assert (out[i] == (in[i] & 0xffffff)); } +static void test_case_conversion () +{ + const char in[] = "AÄaäEÊeêIÌiìOÕoõUÚuú"; + const char low_ascii[] = "aÄaäeÊeêiÌiìoÕoõuÚuú"; + const char low_utf8[] = "aäaäeêeêiìiìoõoõuúuú"; + const char hi_ascii[] = "AÄAäEÊEêIÌIìOÕOõUÚUú"; + const char hi_utf8[] = "AÄAÄEÊEÊIÌIÌOÕOÕUÚUÚ"; + + assert (! strcmp (low_ascii, str_tolower (in))); + assert (! strcmp (low_utf8, str_tolower_utf8 (in))); + assert (! strcmp (hi_ascii, str_toupper (in))); + assert (! strcmp (hi_utf8, str_toupper_utf8 (in))); + + assert (! strcmp_safe ("abc", "abc")); + assert (! strcmp_safe ("abc", "abcdef", 3)); + assert (strcmp_safe ("abc", "def") < 0); + assert (strcmp_safe ("def", "abc") > 0); + assert (! strcmp_safe (nullptr, nullptr)); + assert (strcmp_safe (nullptr, "abc") < 0); + assert (strcmp_safe ("abc", nullptr) > 0); + + assert (! strcmp_nocase ("abc", "ABC")); + assert (! strcmp_nocase ("ABC", "abcdef", 3)); + assert (strcmp_nocase ("abc", "DEF") < 0); + assert (strcmp_nocase ("ABC", "def") < 0); + assert (strcmp_nocase ("def", "ABC") > 0); + assert (strcmp_nocase ("DEF", "abc") > 0); + assert (! strcmp_nocase (nullptr, nullptr)); + assert (strcmp_nocase (nullptr, "abc") < 0); + assert (strcmp_nocase ("abc", nullptr) > 0); + + assert (! strcmp_nocase (in, low_ascii)); + assert (strcmp_nocase (in, low_utf8)); + assert (! strcmp_nocase (in, hi_ascii)); + assert (strcmp_nocase (in, hi_utf8)); + + assert (str_has_prefix_nocase (low_ascii, "AÄaä")); + assert (! str_has_prefix_nocase (low_utf8, "AÄaä")); + assert (str_has_prefix_nocase (hi_ascii, "AÄaä")); + assert (! str_has_prefix_nocase (hi_utf8, "AÄaä")); + + assert (str_has_suffix_nocase (low_ascii, "UÚuú")); + assert (! str_has_suffix_nocase (low_utf8, "UÚuú")); + assert (str_has_suffix_nocase (hi_ascii, "UÚuú")); + assert (! str_has_suffix_nocase (hi_utf8, "UÚuú")); + + assert (! str_has_suffix_nocase ("abc", "abcd")); + + assert (! strcmp (strstr_nocase (low_ascii, "OÕoõ"), "oÕoõuÚuú")); + assert (strstr_nocase (low_utf8, "OÕoõ") == nullptr); + assert (! strcmp (strstr_nocase (hi_ascii, "OÕoõ"), "OÕOõUÚUú")); + assert (strstr_nocase (hi_utf8, "OÕoõ") == nullptr); + + assert (! strcmp (strstr_nocase_utf8 (low_ascii, "OÕoõ"), "oÕoõuÚuú")); + assert (! strcmp (strstr_nocase_utf8 (low_utf8, "OÕoõ"), "oõoõuúuú")); + assert (strstr_nocase_utf8 (low_utf8, "OOoo") == nullptr); + assert (! strcmp (strstr_nocase_utf8 (hi_ascii, "OÕoõ"), "OÕOõUÚUú")); + assert (! strcmp (strstr_nocase_utf8 (hi_utf8, "OÕoõ"), "OÕOÕUÚUÚ")); + assert (strstr_nocase_utf8 (hi_utf8, "OOoo") == nullptr); +} + static void test_numeric_conversion () { static const char * in[] = { @@ -386,6 +457,7 @@ static void test_ringbuf () int main () { test_audio_conversion (); + test_case_conversion (); test_numeric_conversion (); test_filename_split (); test_tuple_formats (); diff --git a/src/libaudgui/Makefile b/src/libaudgui/Makefile index 2ef4ed3..b082d02 100644 --- a/src/libaudgui/Makefile +++ b/src/libaudgui/Makefile @@ -1,6 +1,6 @@ SHARED_LIB = ${LIB_PREFIX}audgui${LIB_SUFFIX} -LIB_MAJOR = 4 -LIB_MINOR = 1 +LIB_MAJOR = 5 +LIB_MINOR = 0 SRCS = about.cc \ confirm.cc \ diff --git a/src/libaudgui/confirm.cc b/src/libaudgui/confirm.cc index bfcc176..1f9b235 100644 --- a/src/libaudgui/confirm.cc +++ b/src/libaudgui/confirm.cc @@ -20,78 +20,79 @@ #include <gtk/gtk.h> #include <libaudcore/audstrings.h> +#include <libaudcore/drct.h> +#include <libaudcore/hook.h> #include <libaudcore/i18n.h> +#include <libaudcore/interface.h> #include <libaudcore/playlist.h> #include <libaudcore/runtime.h> #include "libaudgui.h" #include "libaudgui-gtk.h" -static void no_confirm_cb (GtkToggleButton * toggle) +static void show_question_dialog (const char * title, const char * text, + GtkWidget * widget, GtkWidget * action) { - aud_set_bool ("audgui", "no_confirm_playlist_delete", gtk_toggle_button_get_active (toggle)); + auto cancel = audgui_button_new (_("_Cancel"), "process-stop", nullptr, nullptr); + auto dialog = audgui_dialog_new (GTK_MESSAGE_QUESTION, title, text, action, cancel); + + audgui_dialog_add_widget (dialog, widget); + gtk_widget_show_all (dialog); } -static void confirm_delete_cb (void * data) +static void no_confirm_cb (GtkToggleButton * toggle, void * config) { - int list = aud_playlist_by_unique_id (GPOINTER_TO_INT (data)); + aud_set_bool ("audgui", (const char *) config, gtk_toggle_button_get_active (toggle)); +} - if (list >= 0) - aud_playlist_delete (list); +static void show_confirm_dialog (const char * title, const char * text, + GtkWidget * action, const char * config) +{ + auto check = gtk_check_button_new_with_mnemonic (_("_Don’t ask again")); + g_signal_connect (check, "toggled", (GCallback) no_confirm_cb, (void *) config); + + show_question_dialog (title, text, check, action); } -EXPORT void audgui_confirm_playlist_delete (int playlist) +static void confirm_delete_cb (void * data) +{ + aud::from_ptr<Playlist> (data).remove_playlist (); +} + +EXPORT void audgui_confirm_playlist_delete (Playlist playlist) { if (aud_get_bool ("audgui", "no_confirm_playlist_delete")) { - aud_playlist_delete (playlist); + playlist.remove_playlist (); return; } - StringBuf message = str_printf (_("Do you want to permanently remove “%s”?"), - (const char *) aud_playlist_get_title (playlist)); - - int id = aud_playlist_get_unique_id (playlist); - GtkWidget * button1 = audgui_button_new (_("_Remove"), "edit-delete", - confirm_delete_cb, GINT_TO_POINTER (id)); - GtkWidget * button2 = audgui_button_new (_("_Cancel"), "process-stop", nullptr, nullptr); + StringBuf text = str_printf (_("Do you want to permanently remove “%s”?"), + (const char *) playlist.get_title ()); - GtkWidget * dialog = audgui_dialog_new (GTK_MESSAGE_QUESTION, - _("Remove Playlist"), message, button1, button2); + auto button = audgui_button_new (_("_Remove"), "edit-delete", + confirm_delete_cb, aud::to_ptr (playlist)); - GtkWidget * check = gtk_check_button_new_with_mnemonic (_("_Don’t ask again")); - g_signal_connect (check, "toggled", (GCallback) no_confirm_cb, nullptr); - audgui_dialog_add_widget (dialog, check); - - gtk_widget_show_all (dialog); + show_confirm_dialog (_("Remove Playlist"), text, button, "no_confirm_playlist_delete"); } static void rename_cb (void * entry) { - void * data = g_object_get_data ((GObject *) entry, "playlist-id"); - int list = aud_playlist_by_unique_id (GPOINTER_TO_INT (data)); + void * data = g_object_get_data ((GObject *) entry, "playlist"); + auto playlist = aud::from_ptr<Playlist> (data); - if (list >= 0) - aud_playlist_set_title (list, gtk_entry_get_text ((GtkEntry *) entry)); + playlist.set_title (gtk_entry_get_text ((GtkEntry *) entry)); } -EXPORT void audgui_show_playlist_rename (int playlist) +EXPORT void audgui_show_playlist_rename (Playlist playlist) { - GtkWidget * entry = gtk_entry_new (); - gtk_entry_set_text ((GtkEntry *) entry, aud_playlist_get_title (playlist)); + auto entry = gtk_entry_new (); + gtk_entry_set_text ((GtkEntry *) entry, playlist.get_title ()); gtk_entry_set_activates_default ((GtkEntry *) entry, true); - int id = aud_playlist_get_unique_id (playlist); - g_object_set_data ((GObject *) entry, "playlist-id", GINT_TO_POINTER (id)); + g_object_set_data ((GObject *) entry, "playlist", aud::to_ptr (playlist)); - GtkWidget * button1 = audgui_button_new (_("_Rename"), "insert-text", rename_cb, entry); - GtkWidget * button2 = audgui_button_new (_("_Cancel"), "process-stop", nullptr, nullptr); - - GtkWidget * dialog = audgui_dialog_new (GTK_MESSAGE_QUESTION, - _("Rename Playlist"), _("What would you like to call this playlist?"), - button1, button2); - - audgui_dialog_add_widget (dialog, entry); - - gtk_widget_show_all (dialog); + auto text = _("What would you like to call this playlist?"); + auto button = audgui_button_new (_("_Rename"), "insert-text", rename_cb, entry); + show_question_dialog (_("Rename Playlist"), text, entry, button); } diff --git a/src/libaudgui/file-opener.cc b/src/libaudgui/file-opener.cc index efe339c..adaab4f 100644 --- a/src/libaudgui/file-opener.cc +++ b/src/libaudgui/file-opener.cc @@ -19,6 +19,7 @@ #include <gtk/gtk.h> +#define AUD_GLIB_INTEGRATION #include <libaudcore/drct.h> #include <libaudcore/i18n.h> #include <libaudcore/runtime.h> @@ -58,12 +59,9 @@ static void open_cb (void * data) static void destroy_cb (GtkWidget * chooser) { - char * path = gtk_file_chooser_get_current_folder ((GtkFileChooser *) chooser); + CharPtr path (gtk_file_chooser_get_current_folder ((GtkFileChooser *) chooser)); if (path) - { aud_set_str ("audgui", "filesel_path", path); - g_free (path); - } } static void toggled_cb (GtkToggleButton * toggle, void * option) diff --git a/src/libaudgui/infopopup.cc b/src/libaudgui/infopopup.cc index bc2edfd..680a0b1 100644 --- a/src/libaudgui/infopopup.cc +++ b/src/libaudgui/infopopup.cc @@ -21,6 +21,7 @@ #include <gtk/gtk.h> #include <string.h> +#define AUD_GLIB_INTEGRATION #include <libaudcore/audstrings.h> #include <libaudcore/drct.h> #include <libaudcore/hook.h> @@ -57,15 +58,14 @@ static GtkWidget * infopopup_queued; static bool infopopup_display_image (const char * filename) { bool queued; - GdkPixbuf * pb = audgui_pixbuf_request (filename, & queued); + AudguiPixbuf pb = audgui_pixbuf_request (filename, & queued); if (! pb) return ! queued; - audgui_pixbuf_scale_within (& pb, audgui_get_dpi ()); - gtk_image_set_from_pixbuf ((GtkImage *) widgets.image, pb); + audgui_pixbuf_scale_within (pb, audgui_get_dpi ()); + gtk_image_set_from_pixbuf ((GtkImage *) widgets.image, pb.get ()); gtk_widget_show (widgets.image); - g_object_unref (pb); return true; } @@ -144,9 +144,8 @@ static void infopopup_add_category (GtkWidget * grid, int position, gtk_widget_modify_fg (* header, GTK_STATE_NORMAL, & gray); gtk_widget_modify_fg (* label, GTK_STATE_NORMAL, & white); - char * markup = g_markup_printf_escaped ("<span style=\"italic\">%s</span>", text); + CharPtr markup (g_markup_printf_escaped ("<span style=\"italic\">%s</span>", text)); gtk_label_set_markup ((GtkLabel *) * header, markup); - g_free (markup); gtk_table_attach ((GtkTable *) grid, * header, 0, 1, position, position + 1, GTK_FILL, GTK_FILL, 0, 0); @@ -313,10 +312,10 @@ static void infopopup_show (const char * filename, const Tuple & tuple) infopopup_queued = infopopup; } -EXPORT void audgui_infopopup_show (int playlist, int entry) +EXPORT void audgui_infopopup_show (Playlist playlist, int entry) { - String filename = aud_playlist_entry_get_filename (playlist, entry); - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry); + String filename = playlist.entry_filename (entry); + Tuple tuple = playlist.entry_tuple (entry); if (filename && tuple.valid ()) infopopup_show (filename, tuple); @@ -324,11 +323,11 @@ EXPORT void audgui_infopopup_show (int playlist, int entry) EXPORT void audgui_infopopup_show_current () { - int playlist = aud_playlist_get_playing (); - if (playlist < 0) - playlist = aud_playlist_get_active (); + auto playlist = Playlist::playing_playlist (); + if (playlist == Playlist ()) + playlist = Playlist::active_playlist (); - int position = aud_playlist_get_position (playlist); + int position = playlist.get_position (); if (position < 0) return; diff --git a/src/libaudgui/infowin.cc b/src/libaudgui/infowin.cc index 350455d..b36c1a6 100644 --- a/src/libaudgui/infowin.cc +++ b/src/libaudgui/infowin.cc @@ -70,7 +70,8 @@ static struct { } widgets; static GtkWidget * infowin; -static int current_playlist_id, current_entry; +static Playlist current_playlist; +static int current_entry; static String current_file; static Tuple current_tuple; static PluginHandle * current_decoder = nullptr; @@ -222,15 +223,14 @@ static void infowin_update_tuple () static void infowin_next () { - int list = aud_playlist_by_unique_id (current_playlist_id); int entry = current_entry + 1; - if (list >= 0 && entry < aud_playlist_entry_count (list)) + if (entry < current_playlist.n_entries ()) { - aud_playlist_select_all (list, false); - aud_playlist_entry_set_selected (list, entry, true); - aud_playlist_set_focus (list, entry); - audgui_infowin_show (list, entry); + current_playlist.select_all (false); + current_playlist.select_entry (entry, true); + current_playlist.set_focus (entry); + audgui_infowin_show (current_playlist, entry); } else audgui_infowin_hide (); @@ -262,15 +262,12 @@ static void infowin_display_image (const char * filename) if (! current_file || strcmp (filename, current_file)) return; - GdkPixbuf * pb = audgui_pixbuf_request (filename); + AudguiPixbuf pb = audgui_pixbuf_request (filename); if (! pb) pb = audgui_pixbuf_fallback (); if (pb) - { - audgui_scaled_image_set (widgets.image, pb); - g_object_unref (pb); - } + audgui_scaled_image_set (widgets.image, pb.get ()); } static void infowin_destroyed () @@ -413,13 +410,13 @@ static void create_infowin () hook_associate ("art ready", (HookFunction) infowin_display_image, nullptr); } -static void infowin_show (int list, int entry, const String & filename, +static void infowin_show (Playlist list, int entry, const String & filename, const Tuple & tuple, PluginHandle * decoder, bool writable) { if (! infowin) create_infowin (); - current_playlist_id = aud_playlist_get_unique_id (list); + current_playlist = list; current_entry = entry; current_file = filename; current_tuple = tuple.ref (); @@ -467,16 +464,14 @@ static void infowin_show (int list, int entry, const String & filename, audgui_show_unique_window (AUDGUI_INFO_WINDOW, infowin); } -EXPORT void audgui_infowin_show (int playlist, int entry) +EXPORT void audgui_infowin_show (Playlist playlist, int entry) { - String filename = aud_playlist_entry_get_filename (playlist, entry); + String filename = playlist.entry_filename (entry); g_return_if_fail (filename != nullptr); 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 (); + PluginHandle * decoder = playlist.entry_decoder (entry, Playlist::Wait, & error); + Tuple tuple = decoder ? playlist.entry_tuple (entry, Playlist::Wait, & error) : Tuple (); if (decoder && tuple.valid () && ! aud_custom_infowin (filename, decoder)) { @@ -497,15 +492,12 @@ EXPORT void audgui_infowin_show (int playlist, int entry) EXPORT void audgui_infowin_show_current () { - int playlist = aud_playlist_get_playing (); - int position; - - if (playlist == -1) - playlist = aud_playlist_get_active (); - - position = aud_playlist_get_position (playlist); + auto playlist = Playlist::playing_playlist (); + if (playlist == Playlist ()) + playlist = Playlist::active_playlist (); - if (position == -1) + int position = playlist.get_position (); + if (position < 0) return; audgui_infowin_show (playlist, position); diff --git a/src/libaudgui/init.cc b/src/libaudgui/init.cc index b6192fd..aa5b93c 100644 --- a/src/libaudgui/init.cc +++ b/src/libaudgui/init.cc @@ -35,6 +35,7 @@ static const char * const audgui_defaults[] = { "close_dialog_add", "FALSE", "close_dialog_open", "TRUE", "close_jtf_dialog", "TRUE", + "record", "FALSE", "remember_jtf_entry", "TRUE", nullptr }; @@ -133,7 +134,7 @@ static void playlist_set_playing_cb (void *, void *) static void playlist_position_cb (void * list, void *) { - if (aud::from_ptr<int> (list) == aud_playlist_get_playing ()) + if (aud::from_ptr<Playlist> (list) == Playlist::playing_playlist ()) audgui_pixbuf_uncache (); } diff --git a/src/libaudgui/jump-to-track-cache.cc b/src/libaudgui/jump-to-track-cache.cc index 7dcf242..754cce1 100644 --- a/src/libaudgui/jump-to-track-cache.cc +++ b/src/libaudgui/jump-to-track-cache.cc @@ -118,8 +118,8 @@ const KeywordMatches * JumpToTrackCache::search_within */ void JumpToTrackCache::init () { - int playlist = aud_playlist_get_active (); - int entries = aud_playlist_entry_count (playlist); + auto playlist = Playlist::active_playlist (); + int entries = playlist.n_entries (); // the empty string will match all playlist entries KeywordMatches & k = * add (String (""), KeywordMatches ()); @@ -130,9 +130,9 @@ void JumpToTrackCache::init () { KeywordMatch & item = k[entry]; item.entry = entry; - item.path = String (uri_to_display (aud_playlist_entry_get_filename (playlist, entry))); + item.path = String (uri_to_display (playlist.entry_filename (entry))); - Tuple tuple = aud_playlist_entry_get_tuple (playlist, entry, Playlist::NoWait); + Tuple tuple = playlist.entry_tuple (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 a681163..b6db4f4 100644 --- a/src/libaudgui/jump-to-track.cc +++ b/src/libaudgui/jump-to-track.cc @@ -78,9 +78,9 @@ static void do_jump (void *) if (entry < 0) return; - int playlist = aud_playlist_get_active (); - aud_playlist_set_position (playlist, entry); - aud_playlist_play (playlist); + auto playlist = Playlist::active_playlist (); + playlist.set_position (entry); + playlist.start_playback (); if (aud_get_bool ("audgui", "close_jtf_dialog")) audgui_jump_to_track_hide (); @@ -97,7 +97,7 @@ static void update_queue_button (int entry) } else { - if (aud_playlist_queue_find_entry (aud_playlist_get_active (), entry) != -1) + if (Playlist::active_playlist ().queue_find_entry (entry) >= 0) gtk_button_set_label ((GtkButton *) queue_button, _("Un_queue")); else gtk_button_set_label ((GtkButton *) queue_button, _("_Queue")); @@ -108,16 +108,16 @@ static void update_queue_button (int entry) static void do_queue (void *) { - int playlist = aud_playlist_get_active (); + auto playlist = Playlist::active_playlist (); int entry = get_selected_entry (); if (entry < 0) return; - int queued = aud_playlist_queue_find_entry (playlist, entry); + int queued = playlist.queue_find_entry (entry); if (queued >= 0) - aud_playlist_queue_delete (playlist, queued, 1); + playlist.queue_remove (queued); else - aud_playlist_queue_insert (playlist, -1, entry); + playlist.queue_insert (-1, entry); update_queue_button (entry); } @@ -211,7 +211,7 @@ static void list_get_value (void * user, int row, int column, GValue * value) g_return_if_fail (column >= 0 && column < 2); g_return_if_fail (row >= 0 && row < search_matches->len ()); - int playlist = aud_playlist_get_active (); + auto playlist = Playlist::active_playlist (); int entry = (* search_matches)[row].entry; switch (column) @@ -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::NoWait); + Tuple tuple = playlist.entry_tuple (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 a789a6b..f98ba27 100644 --- a/src/libaudgui/libaudgui-gtk.h +++ b/src/libaudgui/libaudgui-gtk.h @@ -32,12 +32,27 @@ struct PreferencesWidget; typedef void (* AudguiCallback) (void * data); +class AudguiPixbuf : public SmartPtr<GdkPixbuf, aud::typed_func<GdkPixbuf, g_object_unref>> +{ +public: + AudguiPixbuf () : SmartPtr () {} + explicit AudguiPixbuf (GdkPixbuf * ptr) : SmartPtr (ptr) {} + + int width () + { return gdk_pixbuf_get_width (get ()); } + int height () + { return gdk_pixbuf_get_height (get ()); } + + AudguiPixbuf ref () + { return AudguiPixbuf (get () ? (GdkPixbuf *) g_object_ref (get ()) : nullptr); } +}; + /* pixbufs.c */ -GdkPixbuf * audgui_pixbuf_from_data (const void * data, int64_t size); -GdkPixbuf * audgui_pixbuf_fallback (); -void audgui_pixbuf_scale_within (GdkPixbuf * * pixbuf, int size); -GdkPixbuf * audgui_pixbuf_request (const char * filename, bool * queued = nullptr); -GdkPixbuf * audgui_pixbuf_request_current (bool * queued = nullptr); +AudguiPixbuf audgui_pixbuf_from_data (const void * data, int64_t size); +AudguiPixbuf audgui_pixbuf_fallback (); +void audgui_pixbuf_scale_within (AudguiPixbuf & pixbuf, int size); +AudguiPixbuf audgui_pixbuf_request (const char * filename, bool * queued = nullptr); +AudguiPixbuf audgui_pixbuf_request_current (bool * queued = nullptr); /* plugin-menu.c */ GtkWidget * audgui_get_plugin_menu (AudMenuID id); diff --git a/src/libaudgui/libaudgui.h b/src/libaudgui/libaudgui.h index 3cff301..020c611 100644 --- a/src/libaudgui/libaudgui.h +++ b/src/libaudgui/libaudgui.h @@ -25,6 +25,7 @@ enum class AudMenuID; enum class PluginType; +class Playlist; class PluginHandle; struct EqualizerPreset; @@ -33,8 +34,8 @@ void audgui_show_about_window (); void audgui_hide_about_window (); /* confirm.c */ -void audgui_confirm_playlist_delete (int playlist); -void audgui_show_playlist_rename (int playlist); +void audgui_confirm_playlist_delete (Playlist playlist); +void audgui_show_playlist_rename (Playlist playlist); /* eq-preset.c */ void audgui_show_eq_preset_window (); @@ -45,7 +46,7 @@ void audgui_show_equalizer_window (); void audgui_hide_equalizer_window (); /* infopopup.c */ -void audgui_infopopup_show (int playlist, int entry); +void audgui_infopopup_show (Playlist playlist, int entry); void audgui_infopopup_show_current (); void audgui_infopopup_hide (); @@ -54,7 +55,7 @@ void audgui_run_filebrowser (bool open); void audgui_hide_filebrowser (); /* infowin.c */ -void audgui_infowin_show (int playlist, int entry); +void audgui_infowin_show (Playlist playlist, int entry); void audgui_infowin_show_current (); void audgui_infowin_hide (); @@ -91,8 +92,8 @@ void audgui_queue_manager_show (); /* urilist.c */ void audgui_urilist_open (const char * list); -void audgui_urilist_insert (int playlist, int position, const char * list); -Index<char> audgui_urilist_create_from_selected (int playlist); +void audgui_urilist_insert (Playlist playlist, int position, const char * list); +Index<char> audgui_urilist_create_from_selected (Playlist playlist); /* url-opener.c */ void audgui_show_add_url_window (bool open); diff --git a/src/libaudgui/list.cc b/src/libaudgui/list.cc index 906922b..7f68061 100644 --- a/src/libaudgui/list.cc +++ b/src/libaudgui/list.cc @@ -656,7 +656,7 @@ EXPORT void * audgui_list_get_user (GtkWidget * list) } EXPORT void audgui_list_add_column (GtkWidget * list, const char * title, - int column, GType type, int width) + int column, GType type, int width, bool use_markup) { ListModel * model = (ListModel *) gtk_tree_view_get_model ((GtkTreeView *) list); @@ -667,9 +667,13 @@ EXPORT void audgui_list_add_column (GtkWidget * list, const char * title, (type)); GtkCellRenderer * renderer = gtk_cell_renderer_text_new (); - GtkTreeViewColumn * tree_column = gtk_tree_view_column_new_with_attributes - (title, renderer, "text", RESERVED_COLUMNS + column, "weight", - HIGHLIGHT_COLUMN, nullptr); + + GtkTreeViewColumn * tree_column = use_markup ? + gtk_tree_view_column_new_with_attributes + (title, renderer, "markup", RESERVED_COLUMNS + column, nullptr) : + gtk_tree_view_column_new_with_attributes + (title, renderer, "text", RESERVED_COLUMNS + column, "weight", HIGHLIGHT_COLUMN, nullptr); + gtk_tree_view_column_set_sizing (tree_column, GTK_TREE_VIEW_COLUMN_FIXED); int pad1, pad2, pad3; diff --git a/src/libaudgui/list.h b/src/libaudgui/list.h index df9d52d..7226cd6 100644 --- a/src/libaudgui/list.h +++ b/src/libaudgui/list.h @@ -62,7 +62,7 @@ GtkWidget * audgui_list_new_real (const AudguiListCallbacks * cbs, int cbs_size, void * audgui_list_get_user (GtkWidget * list); void audgui_list_add_column (GtkWidget * list, const char * title, - int column, GType type, int width); + int column, GType type, int width, bool use_markup = false); int audgui_list_row_count (GtkWidget * list); void audgui_list_insert_rows (GtkWidget * list, int at, int rows); diff --git a/src/libaudgui/pixbufs.cc b/src/libaudgui/pixbufs.cc index 259469b..bb12c93 100644 --- a/src/libaudgui/pixbufs.cc +++ b/src/libaudgui/pixbufs.cc @@ -27,35 +27,28 @@ #include "internal.h" #include "libaudgui-gtk.h" -static GdkPixbuf * current_pixbuf; +static AudguiPixbuf current_pixbuf; -EXPORT GdkPixbuf * audgui_pixbuf_fallback () +EXPORT AudguiPixbuf audgui_pixbuf_fallback () { - static GdkPixbuf * fallback = nullptr; + static AudguiPixbuf fallback; if (! fallback) - fallback = gdk_pixbuf_new_from_file (filename_build - ({aud_get_path (AudPath::DataDir), "images", "album.png"}), nullptr); + fallback.capture (gdk_pixbuf_new_from_file (filename_build + ({aud_get_path (AudPath::DataDir), "images", "album.png"}), nullptr)); - if (fallback) - g_object_ref ((GObject *) fallback); - - return fallback; + return fallback.ref (); } void audgui_pixbuf_uncache () { - if (current_pixbuf) - { - g_object_unref ((GObject *) current_pixbuf); - current_pixbuf = nullptr; - } + current_pixbuf.clear (); } -EXPORT void audgui_pixbuf_scale_within (GdkPixbuf * * pixbuf, int size) +EXPORT void audgui_pixbuf_scale_within (AudguiPixbuf & pixbuf, int size) { - int width = gdk_pixbuf_get_width (* pixbuf); - int height = gdk_pixbuf_get_height (* pixbuf); + int width = pixbuf.width (); + int height = pixbuf.height (); if (width <= size && height <= size) return; @@ -76,24 +69,18 @@ EXPORT void audgui_pixbuf_scale_within (GdkPixbuf * * pixbuf, int size) if (height < 1) height = 1; - GdkPixbuf * pixbuf2 = gdk_pixbuf_scale_simple (* pixbuf, width, height, GDK_INTERP_BILINEAR); - g_object_unref (* pixbuf); - * pixbuf = pixbuf2; + pixbuf.capture (gdk_pixbuf_scale_simple (pixbuf.get (), width, height, GDK_INTERP_BILINEAR)); } -EXPORT GdkPixbuf * audgui_pixbuf_request (const char * filename, bool * queued) +EXPORT AudguiPixbuf audgui_pixbuf_request (const char * filename, bool * queued) { - const Index<char> * data = aud_art_request_data (filename, queued); - if (! data) - return nullptr; + AudArtPtr art = aud_art_request (filename, AUD_ART_DATA, queued); - GdkPixbuf * p = audgui_pixbuf_from_data (data->begin (), data->len ()); - - aud_art_unref (filename); - return p; + auto data = art.data (); + return data ? audgui_pixbuf_from_data (data->begin (), data->len ()) : AudguiPixbuf (); } -EXPORT GdkPixbuf * audgui_pixbuf_request_current (bool * queued) +EXPORT AudguiPixbuf audgui_pixbuf_request_current (bool * queued) { if (queued) * queued = false; @@ -105,13 +92,10 @@ EXPORT GdkPixbuf * audgui_pixbuf_request_current (bool * queued) current_pixbuf = audgui_pixbuf_request (filename, queued); } - if (current_pixbuf) - g_object_ref ((GObject *) current_pixbuf); - - return current_pixbuf; + return current_pixbuf.ref (); } -EXPORT GdkPixbuf * audgui_pixbuf_from_data (const void * data, int64_t size) +EXPORT AudguiPixbuf audgui_pixbuf_from_data (const void * data, int64_t size) { GdkPixbuf * pixbuf = nullptr; GdkPixbufLoader * loader = gdk_pixbuf_loader_new (); @@ -130,5 +114,5 @@ EXPORT GdkPixbuf * audgui_pixbuf_from_data (const void * data, int64_t size) } g_object_unref (loader); - return pixbuf; + return AudguiPixbuf (pixbuf); } diff --git a/src/libaudgui/playlists.cc b/src/libaudgui/playlists.cc index 4922fba..6f00008 100644 --- a/src/libaudgui/playlists.cc +++ b/src/libaudgui/playlists.cc @@ -20,6 +20,7 @@ #include <string.h> #include <gtk/gtk.h> +#define AUD_GLIB_INTEGRATION #include <libaudcore/audstrings.h> #include <libaudcore/i18n.h> #include <libaudcore/interface.h> @@ -34,13 +35,13 @@ struct ImportExportJob { bool save; - int list_id; - String filename; + Playlist list; + CharPtr filename; GtkWidget * selector = nullptr; GtkWidget * confirm = nullptr; - ImportExportJob (bool save, int list_id) : - save (save), list_id (list_id) {} + ImportExportJob (bool save, Playlist list) : + save (save), list (list) {} }; /* "destroy" callback; do not call directly */ @@ -48,13 +49,10 @@ static void cleanup_job (void * data) { ImportExportJob * job = (ImportExportJob *) data; - char * folder = gtk_file_chooser_get_current_folder_uri ((GtkFileChooser *) job->selector); - + CharPtr folder (gtk_file_chooser_get_current_folder_uri ((GtkFileChooser *) job->selector)); if (folder) aud_set_str ("audgui", "playlist_path", folder); - g_free (folder); - if (job->confirm) gtk_widget_destroy (job->confirm); @@ -64,22 +62,21 @@ static void cleanup_job (void * data) static void finish_job (void * data) { ImportExportJob * job = (ImportExportJob *) data; - int list = aud_playlist_by_unique_id (job->list_id); Playlist::GetMode mode = Playlist::Wait; if (aud_get_bool (nullptr, "metadata_on_play")) mode = Playlist::NoWait; - if (list >= 0) + if (job->list.exists ()) { - aud_playlist_set_filename (list, job->filename); + job->list.set_filename (job->filename); if (job->save) - aud_playlist_save (list, job->filename, mode); + job->list.save_to_file (job->filename, mode); else { - aud_playlist_entry_delete (list, 0, aud_playlist_entry_count (list)); - aud_playlist_entry_insert (list, 0, job->filename, Tuple (), false); + job->list.remove_all_entries (); + job->list.insert_entry (0, job->filename, Tuple (), false); } } @@ -107,13 +104,10 @@ static void check_overwrite (void * data) { ImportExportJob * job = (ImportExportJob *) data; - char * filename = gtk_file_chooser_get_uri ((GtkFileChooser *) job->selector); - if (! filename) + job->filename = CharPtr (gtk_file_chooser_get_uri ((GtkFileChooser *) job->selector)); + if (! job->filename) return; - job->filename = String (filename); - g_free (filename); - if (job->save && ! strchr (job->filename, '.')) { const char * default_ext = nullptr; @@ -129,7 +123,7 @@ static void check_overwrite (void * data) return; } - job->filename = String (str_concat ({job->filename, ".", default_ext})); + job->filename.capture (g_strconcat (job->filename, ".", default_ext, nullptr)); } if (job->save && VFSFile::test_file (job->filename, VFS_EXISTS)) @@ -147,7 +141,7 @@ static void set_format_filters (GtkWidget * selector) gtk_file_filter_add_pattern (filter, "*"); gtk_file_chooser_add_filter ((GtkFileChooser *) selector, filter); - for (auto & format : aud_playlist_save_formats ()) + for (auto & format : Playlist::save_formats ()) { filter = gtk_file_filter_new (); gtk_file_filter_set_name (filter, format.name); @@ -211,12 +205,12 @@ static void create_selector (ImportExportJob * job, const char * filename, const static GtkWidget * start_job (bool save) { - int list = aud_playlist_get_active (); + auto list = Playlist::active_playlist (); - String filename = aud_playlist_get_filename (list); + String filename = list.get_filename (); String folder = aud_get_str ("audgui", "playlist_path"); - ImportExportJob * job = new ImportExportJob (save, aud_playlist_get_unique_id (list)); + ImportExportJob * job = new ImportExportJob (save, list); create_selector (job, filename, folder[0] ? (const char *) folder : nullptr); return job->selector; diff --git a/src/libaudgui/prefs-window.cc b/src/libaudgui/prefs-window.cc index 987bbc6..e126745 100644 --- a/src/libaudgui/prefs-window.cc +++ b/src/libaudgui/prefs-window.cc @@ -1,5 +1,5 @@ /* - * prefs-window.c + * prefs-window.cc * Copyright 2006-2014 William Pitcock, Tomasz Moń, Michael Färber, and * John Lindgren * @@ -95,18 +95,19 @@ static const PluginCategory plugin_categories[] = { }; static const TitleFieldTag title_field_tags[] = { - { N_("Artist") , "${artist}" }, - { N_("Album") , "${album}" }, - { N_("Title") , "${title}" }, + { N_("Artist") , "${artist}" }, + { N_("Album") , "${album}" }, + { N_("Album artist"), "${album-artist}" }, + { N_("Title") , "${title}" }, { N_("Track number"), "${track-number}" }, - { N_("Genre") , "${genre}" }, + { N_("Genre") , "${genre}" }, { N_("File name") , "${file-name}" }, { N_("File path") , "${file-path}" }, - { N_("Date") , "${date}" }, - { N_("Year") , "${year}" }, - { N_("Comment") , "${comment}" }, - { N_("Codec") , "${codec}" }, - { N_("Quality") , "${quality}" } + { N_("Date") , "${date}" }, + { N_("Year") , "${year}" }, + { N_("Comment") , "${comment}" }, + { N_("Codec") , "${codec}" }, + { N_("Quality") , "${quality}" } }; static const ComboItem chardet_detector_presets[] = { @@ -139,6 +140,12 @@ static const ComboItem record_elements[] = { ComboItem (N_("After applying equalization"), (int) OutputStream::AfterEqualizer) }; +static const ComboItem replaygainmode_elements[] = { + ComboItem (N_("Track"), (int) ReplayGainMode::Track), + ComboItem (N_("Album"), (int) ReplayGainMode::Album), + ComboItem (N_("Based on shuffle"), (int) ReplayGainMode::Automatic) +}; + static Index<ComboItem> iface_combo_elements; static int iface_combo_selected; static GtkWidget * iface_prefs_box; @@ -148,10 +155,10 @@ static void iface_combo_changed (); static void * iface_create_prefs_box (); static const PreferencesWidget appearance_page_widgets[] = { - WidgetLabel (N_("<b>Interface Settings</b>")), - WidgetCombo (N_("Interface plugin:"), + WidgetCombo (N_("Interface:"), WidgetInt (iface_combo_selected, iface_combo_changed), {0, iface_combo_fill}), + WidgetSeparator ({true}), WidgetCustomGTK (iface_create_prefs_box) }; @@ -219,8 +226,9 @@ static const PreferencesWidget audio_page_widgets[] = { WidgetLabel (N_("<b>ReplayGain</b>")), WidgetCheck (N_("Enable ReplayGain"), WidgetBool (0, "enable_replay_gain")), - WidgetCheck (N_("Album mode"), - WidgetBool (0, "replay_gain_album"), + WidgetCombo (N_("Mode:"), + WidgetInt (0, "replay_gain_mode"), + {{replaygainmode_elements}}, WIDGET_CHILD), WidgetCheck (N_("Prevent clipping (recommended)"), WidgetBool (0, "enable_clipping_prevention"), @@ -295,7 +303,7 @@ static const PreferencesWidget playlist_page_widgets[] = { WidgetLabel (N_("<b>Compatibility</b>")), WidgetCheck (N_("Interpret \\ (backward slash) as a folder delimiter"), WidgetBool (0, "convert_backslash")), - WidgetTable ({{chardet_elements}}), + WidgetTable ({{chardet_elements}}) }; static const PreferencesWidget song_info_page_widgets[] = { @@ -475,13 +483,10 @@ static void fill_category_list (GtkTreeView * treeview, GtkNotebook * notebook) gettext (category.name), -1); StringBuf path = filename_build ({data_dir, "images", category.icon_path}); - GdkPixbuf * img = gdk_pixbuf_new_from_file (path, nullptr); + AudguiPixbuf img (gdk_pixbuf_new_from_file (path, nullptr)); if (img) - { - gtk_list_store_set (store, & iter, CATEGORY_VIEW_COL_ICON, img, -1); - g_object_unref (img); - } + gtk_list_store_set (store, & iter, CATEGORY_VIEW_COL_ICON, img.get (), -1); } g_object_unref (store); @@ -733,7 +738,7 @@ static void record_update (void * = nullptr, void * = nullptr) gtk_widget_set_sensitive (record_checkbox, true); gtk_button_set_label ((GtkButton *) record_checkbox, - str_printf (_("Record audio stream using %s"), aud_plugin_get_name (p))); + str_printf (_("Enable audio stream recording with %s"), aud_plugin_get_name (p))); gtk_toggle_button_set_active ((GtkToggleButton *) record_checkbox, enabled); gtk_widget_set_sensitive (record_config_button, enabled && aud_plugin_has_configure (p)); gtk_widget_set_sensitive (record_about_button, enabled && aud_plugin_has_about (p)); diff --git a/src/libaudgui/preset-browser.cc b/src/libaudgui/preset-browser.cc index 9144086..5b5c78d 100644 --- a/src/libaudgui/preset-browser.cc +++ b/src/libaudgui/preset-browser.cc @@ -17,6 +17,7 @@ * the use of this software. */ +#define AUD_GLIB_INTEGRATION #include "internal.h" #include "libaudgui.h" #include "preset-browser.h" @@ -35,9 +36,8 @@ static void browser_response (GtkWidget * dialog, int response, void * data) { if (response == GTK_RESPONSE_ACCEPT) { - char * filename = gtk_file_chooser_get_uri ((GtkFileChooser *) dialog); + CharPtr filename (gtk_file_chooser_get_uri ((GtkFileChooser *) dialog)); ((FilebrowserCallback) data) (filename); - g_free (filename); } gtk_widget_destroy (dialog); diff --git a/src/libaudgui/queue-manager.cc b/src/libaudgui/queue-manager.cc index 2b5e6d0..67a6ef6 100644 --- a/src/libaudgui/queue-manager.cc +++ b/src/libaudgui/queue-manager.cc @@ -36,8 +36,8 @@ enum { static void get_value (void * user, int row, int column, GValue * value) { - int list = aud_playlist_get_active (); - int entry = aud_playlist_queue_get_entry (list, row); + auto list = Playlist::active_playlist (); + int entry = list.queue_get_entry (row); switch (column) { @@ -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::NoWait); + Tuple tuple = list.entry_tuple (entry, Playlist::NoWait); g_value_set_string (value, tuple.get_str (Tuple::FormattedTitle)); break; } @@ -53,36 +53,36 @@ static void get_value (void * user, int row, int column, GValue * value) static bool get_selected (void * user, int row) { - int list = aud_playlist_get_active (); - return aud_playlist_entry_get_selected (list, aud_playlist_queue_get_entry (list, row)); + auto list = Playlist::active_playlist (); + return list.entry_selected (list.queue_get_entry (row)); } static void set_selected (void * user, int row, bool selected) { - int list = aud_playlist_get_active (); - aud_playlist_entry_set_selected (list, aud_playlist_queue_get_entry (list, row), selected); + auto list = Playlist::active_playlist (); + list.select_entry (list.queue_get_entry (row), selected); } static void select_all (void * user, bool selected) { - int list = aud_playlist_get_active (); - int count = aud_playlist_queue_count (list); + auto list = Playlist::active_playlist (); + int count = list.n_queued (); for (int i = 0; i < count; i ++) - aud_playlist_entry_set_selected (list, aud_playlist_queue_get_entry (list, i), selected); + list.select_entry (list.queue_get_entry (i), selected); } static void shift_rows (void * user, int row, int before) { Index<int> shift; - int list = aud_playlist_get_active (); - int count = aud_playlist_queue_count (list); + auto list = Playlist::active_playlist (); + int count = list.n_queued (); for (int i = 0; i < count; i ++) { - int entry = aud_playlist_queue_get_entry (list, i); + int entry = list.queue_get_entry (i); - if (aud_playlist_entry_get_selected (list, entry)) + if (list.entry_selected (entry)) { shift.append (entry); @@ -91,10 +91,10 @@ static void shift_rows (void * user, int row, int before) } } - aud_playlist_queue_delete_selected (list); + list.queue_remove_selected (); for (int i = 0; i < shift.len (); i ++) - aud_playlist_queue_insert (list, before + i, shift[i]); + list.queue_insert (before + i, shift[i]); } static const AudguiListCallbacks callbacks = { @@ -109,17 +109,17 @@ static const AudguiListCallbacks callbacks = { static void remove_selected (void *) { - int list = aud_playlist_get_active (); - int count = aud_playlist_queue_count (list); + auto list = Playlist::active_playlist (); + int count = list.n_queued (); for (int i = 0; i < count; ) { - int entry = aud_playlist_queue_get_entry (list, i); + int entry = list.queue_get_entry (i); - if (aud_playlist_entry_get_selected (list, entry)) + if (list.entry_selected (entry)) { - aud_playlist_queue_delete (list, i, 1); - aud_playlist_entry_set_selected (list, entry, false); + list.queue_remove (i); + list.select_entry (entry, false); count --; } else @@ -132,7 +132,7 @@ static void update_hook (void * data, void * user) GtkWidget * qm_list = (GtkWidget *) user; int oldrows = audgui_list_row_count (qm_list); - int newrows = aud_playlist_queue_count (aud_playlist_get_active ()); + int newrows = Playlist::active_playlist ().n_queued (); int focus = audgui_list_get_focus (qm_list); audgui_list_update_rows (qm_list, 0, aud::min (oldrows, newrows)); @@ -183,7 +183,7 @@ static GtkWidget * create_queue_manager () GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_box_pack_start ((GtkBox *) vbox, scrolled, true, true, 0); - int count = aud_playlist_queue_count (aud_playlist_get_active ()); + int count = Playlist::active_playlist ().n_queued (); GtkWidget * qm_list = audgui_list_new (& callbacks, nullptr, count); gtk_tree_view_set_headers_visible ((GtkTreeView *) qm_list, false); audgui_list_add_column (qm_list, nullptr, 0, G_TYPE_INT, 7); diff --git a/src/libaudgui/scaled-image.cc b/src/libaudgui/scaled-image.cc index 41c804a..cc079c9 100644 --- a/src/libaudgui/scaled-image.cc +++ b/src/libaudgui/scaled-image.cc @@ -33,12 +33,12 @@ static GdkPixbuf * get_scaled (GtkWidget * widget, int maxwidth, int maxheight) { if (width * maxheight > height * maxwidth) { - height = height * maxwidth / width; + height = aud::rescale (height, width, maxwidth); width = maxwidth; } else { - width = width * maxheight / height; + width = aud::rescale (width, height, maxheight); height = maxheight; } } @@ -49,12 +49,10 @@ static GdkPixbuf * get_scaled (GtkWidget * widget, int maxwidth, int maxheight) { if (gdk_pixbuf_get_width (scaled) == width && gdk_pixbuf_get_height (scaled) == height) return scaled; - - g_object_unref (scaled); } scaled = gdk_pixbuf_scale_simple (unscaled, width, height, GDK_INTERP_BILINEAR); - g_object_set_data ((GObject *) widget, "pixbuf-scaled", scaled); + g_object_set_data_full ((GObject *) widget, "pixbuf-scaled", scaled, g_object_unref); return scaled; } @@ -82,33 +80,20 @@ static int expose_cb (GtkWidget * widget, GdkEventExpose * event) EXPORT void audgui_scaled_image_set (GtkWidget * widget, GdkPixbuf * pixbuf) { - GdkPixbuf * old; - - if ((old = (GdkPixbuf *) g_object_get_data ((GObject *) widget, "pixbuf-unscaled"))) - g_object_unref (old); - if ((old = (GdkPixbuf *) g_object_get_data ((GObject *) widget, "pixbuf-scaled"))) - g_object_unref (old); - if (pixbuf) g_object_ref (pixbuf); - g_object_set_data ((GObject *) widget, "pixbuf-unscaled", pixbuf); - g_object_set_data ((GObject *) widget, "pixbuf-scaled", nullptr); + g_object_set_data_full ((GObject *) widget, "pixbuf-unscaled", pixbuf, g_object_unref); + g_object_set_data_full ((GObject *) widget, "pixbuf-scaled", nullptr, g_object_unref); gtk_widget_queue_draw (widget); } -static void destroy_cb (GtkWidget * widget) -{ - audgui_scaled_image_set (widget, nullptr); -} - EXPORT GtkWidget * audgui_scaled_image_new (GdkPixbuf * pixbuf) { GtkWidget * widget = gtk_drawing_area_new (); g_signal_connect (widget, "expose-event", (GCallback) expose_cb, nullptr); - g_signal_connect (widget, "destroy", (GCallback) destroy_cb, nullptr); audgui_scaled_image_set (widget, pixbuf); diff --git a/src/libaudgui/urilist.cc b/src/libaudgui/urilist.cc index a0809f4..6874f4f 100644 --- a/src/libaudgui/urilist.cc +++ b/src/libaudgui/urilist.cc @@ -60,26 +60,26 @@ EXPORT void audgui_urilist_open (const char * list) aud_drct_pl_open_list (urilist_to_index (list)); } -EXPORT void audgui_urilist_insert (int playlist, int at, const char * list) +EXPORT void audgui_urilist_insert (Playlist playlist, int at, const char * list) { - aud_playlist_entry_insert_batch (playlist, at, urilist_to_index (list), false); + playlist.insert_items (at, urilist_to_index (list), false); } -EXPORT Index<char> audgui_urilist_create_from_selected (int playlist) +EXPORT Index<char> audgui_urilist_create_from_selected (Playlist playlist) { - aud_playlist_cache_selected (playlist); + playlist.cache_selected (); Index<char> buf; - int entries = aud_playlist_entry_count (playlist); + int entries = playlist.n_entries (); - for (int count = 0; count < entries; count ++) + for (int i = 0; i < entries; i ++) { - if (aud_playlist_entry_get_selected (playlist, count)) + if (playlist.entry_selected (i)) { if (buf.len ()) buf.append ('\n'); - String filename = aud_playlist_entry_get_filename (playlist, count); + String filename = playlist.entry_filename (i); buf.insert (filename, -1, strlen (filename)); } } diff --git a/src/libaudgui/util.cc b/src/libaudgui/util.cc index 79f8727..2994c89 100644 --- a/src/libaudgui/util.cc +++ b/src/libaudgui/util.cc @@ -27,6 +27,7 @@ #include <gdk/gdkkeysyms.h> #include <gtk/gtk.h> +#define AUD_GLIB_INTEGRATION #include <libaudcore/audstrings.h> #include <libaudcore/hook.h> #include <libaudcore/i18n.h> @@ -156,12 +157,9 @@ static void entry_response_cb (GtkWidget * dialog, int response, GtkWidget * ent { if (response == GTK_RESPONSE_ACCEPT) { - char * uri = gtk_file_chooser_get_uri ((GtkFileChooser *) dialog); + CharPtr uri (gtk_file_chooser_get_uri ((GtkFileChooser *) dialog)); if (uri) - { audgui_file_entry_set_uri (entry, uri); - g_free (uri); - } } gtk_widget_destroy (dialog); @@ -227,6 +225,12 @@ EXPORT void audgui_file_entry_set_uri (GtkWidget * entry, const char * uri) gtk_editable_set_position ((GtkEditable *) entry, -1); } +static void set_label_wrap (GtkWidget * label, void *) +{ + if (GTK_IS_LABEL (label)) + gtk_label_set_line_wrap_mode ((GtkLabel *) label, PANGO_WRAP_WORD_CHAR); +} + EXPORT GtkWidget * audgui_dialog_new (GtkMessageType type, const char * title, const char * text, GtkWidget * button1, GtkWidget * button2) { @@ -234,6 +238,9 @@ EXPORT GtkWidget * audgui_dialog_new (GtkMessageType type, const char * title, GTK_BUTTONS_NONE, "%s", text); gtk_window_set_title ((GtkWindow *) dialog, title); + GtkWidget * box = gtk_message_dialog_get_message_area ((GtkMessageDialog *) dialog); + gtk_container_foreach ((GtkContainer *) box, set_label_wrap, nullptr); + if (button2) { gtk_dialog_add_action_widget ((GtkDialog *) dialog, button2, GTK_RESPONSE_NONE); @@ -255,39 +262,6 @@ EXPORT void audgui_dialog_add_widget (GtkWidget * dialog, GtkWidget * widget) gtk_box_pack_start ((GtkBox *) box, widget, false, false, 0); } -static StringBuf ellipsize (const char * text) -{ - constexpr int maxword = 100; - - StringBuf buf = str_copy (text); - int start = 0; - - while (1) - { - while (buf[start] && g_ascii_isspace (buf[start])) - start ++; - - if (! buf[start]) - break; - - int stop = start + 1; - while (buf[stop] && ! g_ascii_isspace (buf[stop])) - stop ++; - - if (stop - start > maxword) - { - buf.remove (start + maxword / 2, stop - start - maxword); - buf.insert (start + maxword / 2, "…"); - - stop = start + maxword + strlen ("…"); - } - - start = stop; - } - - return buf; -} - EXPORT void audgui_simple_message (GtkWidget * * widget, GtkMessageType type, const char * title, const char * text) { @@ -308,10 +282,9 @@ EXPORT void audgui_simple_message (GtkWidget * * widget, GtkMessageType type, if (messages > 10) text = _("\n(Further messages have been hidden.)"); - StringBuf shortened = ellipsize (text); - if (! strstr (old, shortened)) + if (! strstr (old, text)) { - StringBuf both = str_concat ({old, "\n", shortened}); + StringBuf both = str_concat ({old, "\n", text}); g_object_set ((GObject *) * widget, "text", (const char *) both, nullptr); g_object_set_data ((GObject *) * widget, "messages", GINT_TO_POINTER (messages + 1)); } @@ -322,7 +295,7 @@ EXPORT void audgui_simple_message (GtkWidget * * widget, GtkMessageType type, else { GtkWidget * button = audgui_button_new (_("_Close"), "window-close", nullptr, nullptr); - * widget = audgui_dialog_new (type, title, ellipsize (text), button, nullptr); + * widget = audgui_dialog_new (type, title, text, button, nullptr); g_object_set_data ((GObject *) * widget, "messages", GINT_TO_POINTER (1)); g_signal_connect (* widget, "destroy", (GCallback) gtk_widget_destroyed, widget); diff --git a/src/libaudqt/Makefile b/src/libaudqt/Makefile index b3b6f9a..7417d4e 100644 --- a/src/libaudqt/Makefile +++ b/src/libaudqt/Makefile @@ -1,5 +1,5 @@ SHARED_LIB = ${LIB_PREFIX}audqt${LIB_SUFFIX} -LIB_MAJOR = 1 +LIB_MAJOR = 2 LIB_MINOR = 0 SRCS = about.cc \ diff --git a/src/libaudqt/about.cc b/src/libaudqt/about.cc index e6a64f3..2fdccbd 100644 --- a/src/libaudqt/about.cc +++ b/src/libaudqt/about.cc @@ -38,6 +38,8 @@ static QTabWidget * buildCreditsNotebook (QWidget * parent) const char * filenames[2] = {"AUTHORS", "COPYING"}; auto tabs = new QTabWidget (parent); + tabs->setDocumentMode (true); + tabs->setMinimumSize (6 * audqt::sizes.OneInch, 2 * audqt::sizes.OneInch); for (int i = 0; i < 2; i ++) { @@ -49,6 +51,7 @@ static QTabWidget * buildCreditsNotebook (QWidget * parent) auto edit = new QPlainTextEdit (in.readAll ().trimmed (), parent); edit->setReadOnly (true); + edit->setFrameStyle (QFrame::NoFrame); tabs->addTab (edit, _(titles[i])); f.close (); @@ -65,6 +68,7 @@ static QDialog * buildAboutWindow () const char * website = "http://audacious-media-player.org"; auto window = new QDialog; + window->setWindowTitle (_("About Audacious")); auto logo = new QLabel (window); logo->setPixmap (QPixmap (logo_path)); @@ -76,18 +80,14 @@ static QDialog * buildAboutWindow () auto anchor = QString ("<a href='%1'>%1</a>").arg (website); auto link_label = new QLabel (anchor, window); link_label->setAlignment (Qt::AlignHCenter); - link_label->setContentsMargins (0, 5, 0, 0); link_label->setOpenExternalLinks (true); - auto layout = new QVBoxLayout (window); + auto layout = audqt::make_vbox (window); layout->addWidget (logo); layout->addWidget (text); layout->addWidget (link_label); layout->addWidget (buildCreditsNotebook (window)); - window->setWindowTitle (_("About Audacious")); - window->setFixedSize (590, 450); - return window; } diff --git a/src/libaudqt/art.cc b/src/libaudqt/art.cc index 55d910d..c691992 100644 --- a/src/libaudqt/art.cc +++ b/src/libaudqt/art.cc @@ -28,31 +28,37 @@ namespace audqt { -EXPORT QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi) +static QImage load_fallback () { - const Index<char> * data = aud_art_request_data (filename); - QImage img; - - if (data) - { - img = QImage::fromData ((const uchar *) data->begin (), data->len ()); + static QImage fallback; + static bool loaded = false; - aud_art_unref (filename); - } - else - { - QString fallback = QString (filename_build + if (! loaded) + fallback.load ((const char *) filename_build ({aud_get_path (AudPath::DataDir), "images", "album.png"})); - img = QImage (fallback); - } + return fallback; // shallow copy +} + +EXPORT QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi) +{ + AudArtPtr art = aud_art_request (filename, AUD_ART_DATA); + + auto data = art.data (); + auto img = data ? QImage::fromData ((const uchar *) data->begin (), data->len ()) : QImage (); - if (w == 0 && h == 0) + if (img.isNull ()) { - w = img.size ().width (); - h = img.size ().height (); + img = load_fallback (); + if (img.isNull ()) + return QPixmap (); } + // return original image if requested size is zero, + // or original size is smaller than requested size + if ((w == 0 && h == 0) || ((unsigned) img.width () <= w && (unsigned) img.height () <= h)) + return QPixmap::fromImage (img); + if (! want_hidpi) return QPixmap::fromImage (img.scaled (w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation)); diff --git a/src/libaudqt/equalizer.cc b/src/libaudqt/equalizer.cc index 7d2bbb1..01907ce 100644 --- a/src/libaudqt/equalizer.cc +++ b/src/libaudqt/equalizer.cc @@ -66,18 +66,20 @@ public: class EqualizerSlider : public QWidget { public: + QSlider slider; + EqualizerSlider (const char * label, QWidget * parent) : QWidget (parent), slider (Qt::Vertical) { + slider.setMinimumHeight (audqt::sizes.OneInch); slider.setRange (-AUD_EQ_MAX_GAIN, AUD_EQ_MAX_GAIN); slider.setTickInterval (AUD_EQ_MAX_GAIN >> 1); slider.setTickPosition (QSlider::TicksBothSides); - auto layout = new QVBoxLayout (this); + auto layout = audqt::make_vbox (this); auto value_label = new QLabel ("0"); - layout->setContentsMargins (0, 0, 0, 0); layout->addWidget (new VLabel (label, this), 1, Qt::AlignCenter); layout->addWidget (& slider, 0, Qt::AlignCenter); layout->addWidget (value_label, 0, Qt::AlignCenter); @@ -86,8 +88,6 @@ public: value_label->setText (QString::number (value)); }); } - - QSlider slider; }; class EqualizerWindow : public QDialog @@ -118,7 +118,7 @@ EqualizerWindow::EqualizerWindow () : N_("4 kHz"), N_("8 kHz"), N_("16 kHz")}; auto slider_container = new QWidget (this); - auto slider_layout = new QHBoxLayout (slider_container); + auto slider_layout = audqt::make_hbox (slider_container, audqt::sizes.TwoPt); m_preamp_slider = new EqualizerSlider (_("Preamp"), this); slider_layout->addWidget (m_preamp_slider); @@ -134,11 +134,14 @@ EqualizerWindow::EqualizerWindow () : slider_layout->addWidget (m_sliders[i]); } - auto layout = new QVBoxLayout (this); + auto layout = audqt::make_vbox (this); + layout->setSizeConstraint (QLayout::SetFixedSize); layout->addWidget (& m_onoff_checkbox); layout->addWidget (slider_container); setWindowTitle (_("Equalizer")); + setContentsMargins (audqt::margins.EightPt); + m_onoff_checkbox.setFocus (); updateActive (); @@ -191,7 +194,6 @@ EXPORT void equalizer_show () { s_equalizer = new EqualizerWindow; s_equalizer->setAttribute (Qt::WA_DeleteOnClose); - s_equalizer->layout ()->setSizeConstraint (QLayout::SetFixedSize); QObject::connect (s_equalizer, & QObject::destroyed, [] () { s_equalizer = nullptr; diff --git a/src/libaudqt/info-widget.cc b/src/libaudqt/info-widget.cc index aa86e14..ef9b218 100644 --- a/src/libaudqt/info-widget.cc +++ b/src/libaudqt/info-widget.cc @@ -103,7 +103,7 @@ EXPORT InfoWidget::~InfoWidget () { } -EXPORT void InfoWidget::fillInfo (int playlist, int entry, const char * filename, const Tuple & tuple, +EXPORT void InfoWidget::fillInfo (const char * filename, const Tuple & tuple, PluginHandle * decoder, bool updating_enabled) { m_model->setTupleData (tuple, String (filename), decoder); diff --git a/src/libaudqt/info-widget.h b/src/libaudqt/info-widget.h index 86e3925..80e0483 100644 --- a/src/libaudqt/info-widget.h +++ b/src/libaudqt/info-widget.h @@ -18,6 +18,9 @@ * the use of this software. */ +#ifndef LIBAUDQT_INFO_WIDGET_H +#define LIBAUDQT_INFO_WIDGET_H + #include <QTreeView> #include <libaudqt/export.h> @@ -34,7 +37,7 @@ public: InfoWidget (QWidget * parent = nullptr); ~InfoWidget (); - void fillInfo (int playlist, int entry, const char * filename, const Tuple & tuple, + void fillInfo (const char * filename, const Tuple & tuple, PluginHandle * decoder, bool updating_enabled); bool updateFile (); @@ -43,3 +46,5 @@ private: }; } // namespace audqt + +#endif // LIBAUDQT_INFO_WIDGET_H diff --git a/src/libaudqt/infowin.cc b/src/libaudqt/infowin.cc index a72e3c3..e51159a 100644 --- a/src/libaudqt/infowin.cc +++ b/src/libaudqt/infowin.cc @@ -44,7 +44,7 @@ class InfoWindow : public QDialog public: InfoWindow (QWidget * parent = nullptr); - void fillInfo (int playlist, int entry, const char * filename, const Tuple & tuple, + void fillInfo (const char * filename, const Tuple & tuple, PluginHandle * decoder, bool updating_enabled); private: @@ -61,12 +61,13 @@ private: InfoWindow::InfoWindow (QWidget * parent) : QDialog (parent) { setWindowTitle (_("Song Info")); + setContentsMargins (margins.TwoPt); - auto hbox = new QHBoxLayout; + auto hbox = make_hbox (nullptr); hbox->addWidget (& m_image); hbox->addWidget (& m_infowidget); - auto vbox = new QVBoxLayout (this); + auto vbox = make_vbox (this); vbox->addLayout (hbox); auto bbox = new QDialogButtonBox (QDialogButtonBox::Save | QDialogButtonBox::Close, this); @@ -82,23 +83,23 @@ InfoWindow::InfoWindow (QWidget * parent) : QDialog (parent) QObject::connect (bbox, & QDialogButtonBox::rejected, this, & QObject::deleteLater); } -void InfoWindow::fillInfo (int playlist, int entry, const char * filename, const Tuple & tuple, +void InfoWindow::fillInfo (const char * filename, const Tuple & tuple, PluginHandle * decoder, bool updating_enabled) { m_filename = String (filename); displayImage (filename); - m_infowidget.fillInfo (playlist, entry, filename, tuple, decoder, updating_enabled); + m_infowidget.fillInfo (filename, tuple, decoder, updating_enabled); } void InfoWindow::displayImage (const char * filename) { if (! strcmp_safe (filename, m_filename)) - m_image.setPixmap (art_request (filename)); + m_image.setPixmap (art_request (filename, 2 * sizes.OneInch, 2 * sizes.OneInch)); } static InfoWindow * s_infowin = nullptr; -static void show_infowin (int playlist, int entry, const char * filename, +static void show_infowin (const char * filename, const Tuple & tuple, PluginHandle * decoder, bool can_write) { if (! s_infowin) @@ -111,22 +112,20 @@ static void show_infowin (int playlist, int entry, const char * filename, }); } - s_infowin->fillInfo (playlist, entry, filename, tuple, decoder, can_write); - s_infowin->resize (700, 300); + s_infowin->fillInfo (filename, tuple, decoder, can_write); + s_infowin->resize (6 * sizes.OneInch, 3 * sizes.OneInch); window_bring_to_front (s_infowin); } -EXPORT void infowin_show (int playlist, int entry) +EXPORT void infowin_show (Playlist playlist, int entry) { - String filename = aud_playlist_entry_get_filename (playlist, entry); + String filename = playlist.entry_filename (entry); if (! filename) return; 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 (); + PluginHandle * decoder = playlist.entry_decoder (entry, Playlist::Wait, & error); + Tuple tuple = decoder ? playlist.entry_tuple (entry, Playlist::Wait, & error) : Tuple (); if (decoder && tuple.valid () && ! aud_custom_infowin (filename, decoder)) { @@ -135,7 +134,7 @@ EXPORT void infowin_show (int playlist, int entry) ! tuple.is_set (Tuple::StartTime); tuple.delete_fallbacks (); - show_infowin (playlist, entry, filename, tuple, decoder, can_write); + show_infowin (filename, tuple, decoder, can_write); } else infowin_hide (); @@ -147,15 +146,12 @@ EXPORT void infowin_show (int playlist, int entry) EXPORT void infowin_show_current () { - int playlist = aud_playlist_get_playing (); - int position; + auto playlist = Playlist::playing_playlist (); + if (playlist == Playlist ()) + playlist = Playlist::active_playlist (); - if (playlist == -1) - playlist = aud_playlist_get_active (); - - position = aud_playlist_get_position (playlist); - - if (position == -1) + int position = playlist.get_position (); + if (position < 0) return; infowin_show (playlist, position); diff --git a/src/libaudqt/log-inspector.h b/src/libaudqt/libaudqt-internal.h index 5b88845..ea00ed9 100644 --- a/src/libaudqt/log-inspector.h +++ b/src/libaudqt/libaudqt-internal.h @@ -1,6 +1,6 @@ /* - * log-inspector.h - * Copyright 2014 William Pitcock + * libaudqt-internal.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,29 +17,15 @@ * the use of this software. */ -#include <QAbstractListModel> - -#ifndef LIBAUDQT_LOG_INSPECTOR_H -#define LIBAUDQT_LOG_INSPECTOR_H +#ifndef LIBAUDQT_INTERNAL_H +#define LIBAUDQT_INTERNAL_H namespace audqt { -struct LogEntry; - -class LogEntryModel : public QAbstractListModel -{ -public: - LogEntryModel (QObject * parent = nullptr); - ~LogEntryModel (); - - int rowCount (const QModelIndex & parent = QModelIndex ()) const; - int columnCount (const QModelIndex & parent = QModelIndex ()) const; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; - QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - - void addEntry (LogEntry * entry); -}; +/* log-inspector.cc */ +void log_init (); +void log_cleanup (); } // namespace audqt -#endif +#endif // LIBAUDQT_INTERNAL_H diff --git a/src/libaudqt/libaudqt.h b/src/libaudqt/libaudqt.h index ea21708..3f242c2 100644 --- a/src/libaudqt/libaudqt.h +++ b/src/libaudqt/libaudqt.h @@ -20,17 +20,22 @@ #ifndef LIBAUDQT_H #define LIBAUDQT_H +#include <QMargins> #include <QMessageBox> #include <QString> #include <libaudcore/objects.h> -class QBoxLayout; class QLayout; +class QBoxLayout; +class QHBoxLayout; +class QVBoxLayout; + class QPixmap; class QToolButton; class QWidget; enum class PluginType; +class Playlist; class PluginHandle; struct PreferencesWidget; @@ -44,6 +49,19 @@ enum class FileMode { count }; +struct PixelSizes { + int OneInch; + int TwoPt; + int FourPt; + int EightPt; +}; + +struct PixelMargins { + QMargins TwoPt; + QMargins FourPt; + QMargins EightPt; +}; + struct MenuItem; /* about.cc */ @@ -51,8 +69,8 @@ void aboutwindow_show (); void aboutwindow_hide (); /* playlist-management.cc */ -void playlist_show_rename (int playlist); -void playlist_confirm_delete (int playlist); +void playlist_show_rename (Playlist playlist); +void playlist_confirm_delete (Playlist playlist); /* equalizer.cc */ void equalizer_show (); @@ -65,11 +83,23 @@ void fileopener_show (FileMode mode); void urlopener_show (bool open); /* util.cc */ + +extern const PixelSizes & sizes; +extern const PixelMargins & margins; + +static inline int to_native_dpi (int x) + { return aud::rescale (x, 96, sizes.OneInch); } +static inline int to_portable_dpi (int x) + { return aud::rescale (x, sizes.OneInch, 96); } + void init (); void run (); void quit (); void cleanup (); +QHBoxLayout * make_hbox (QWidget * parent, int spacing = sizes.FourPt); +QVBoxLayout * make_vbox (QWidget * parent, int spacing = sizes.FourPt); + void enable_layout (QLayout * layout, bool enabled); void clear_layout (QLayout * layout); void window_bring_to_front (QWidget * win); @@ -100,11 +130,11 @@ void log_inspector_show (); void log_inspector_hide (); /* art.cc */ -QPixmap art_request (const char * filename, unsigned int w = 256, unsigned int h = 256, bool want_hidpi = true); -QPixmap art_request_current (unsigned int w = 256, unsigned int h = 256, bool want_hidpi = true); +QPixmap art_request (const char * filename, unsigned int w, unsigned int h, bool want_hidpi = true); +QPixmap art_request_current (unsigned int w, unsigned int h, bool want_hidpi = true); /* infowin.cc */ -void infowin_show (int playlist, int entry); +void infowin_show (Playlist playlist, int entry); void infowin_show_current (); void infowin_hide (); diff --git a/src/libaudqt/log-inspector.cc b/src/libaudqt/log-inspector.cc index 9394ce0..bcaefd5 100644 --- a/src/libaudqt/log-inspector.cc +++ b/src/libaudqt/log-inspector.cc @@ -17,29 +17,31 @@ * the use of this software. */ -#include "log-inspector.h" #include "libaudqt.h" +#include "libaudqt-internal.h" #include <QComboBox> #include <QDialog> +#include <QDialogButtonBox> #include <QHBoxLayout> #include <QLabel> #include <QVBoxLayout> +#include <QPushButton> #include <QTreeView> #include <QWidget> +#include <libaudcore/audstrings.h> +#include <libaudcore/hook.h> #include <libaudcore/i18n.h> -#include <libaudcore/index.h> +#include <libaudcore/ringbuf.h> #include <libaudcore/runtime.h> -namespace audqt { +#define LOGENTRY_MAX 1024 -const int LOGENTRY_MAX = 1000; +namespace audqt { enum LogEntryColumn { Level, - File, - Line, Function, Message, Count @@ -47,56 +49,79 @@ enum LogEntryColumn { struct LogEntry { audlog::Level level; - const char * filename; - unsigned int line; - const char * function; - char * message = nullptr; - - ~LogEntry () - { free (message); } + String function; + String message; }; -static Index<SmartPtr<LogEntry>> entries; +class LogEntryModel : public QAbstractListModel +{ +public: + LogEntryModel (QObject * parent = nullptr) : + QAbstractListModel (parent) {} -static void log_handler (audlog::Level level, const char * file, int line, - const char * func, const char * message); + void cleanup (); +protected: + int rowCount (const QModelIndex & parent = QModelIndex ()) const + { return m_entries.len (); } + int columnCount (const QModelIndex & parent = QModelIndex ()) const + { return LogEntryColumn::Count; } -/* log entry model */ -LogEntryModel::LogEntryModel (QObject * parent) : QAbstractListModel (parent) -{ -} + QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; + QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; -LogEntryModel::~LogEntryModel () -{ -} +private: + RingBuf<LogEntry> m_entries; -int LogEntryModel::rowCount (const QModelIndex & parent) const + void addEntry (const LogEntry * entry); + HookReceiver<LogEntryModel, const LogEntry *> + log_hook {"audqt log entry", this, & LogEntryModel::addEntry}; +}; + +void LogEntryModel::cleanup () { - return entries.len (); + if (m_entries.len () > 0) + { + beginRemoveRows (QModelIndex (), 0, m_entries.len () - 1); + m_entries.destroy (); + endRemoveRows (); + } } -int LogEntryModel::columnCount (const QModelIndex & parent) const +/* log entry model */ +void LogEntryModel::addEntry (const LogEntry * entry) { - return LogEntryColumn::Count; + if (! m_entries.space ()) + { + if (m_entries.len () < LOGENTRY_MAX) + m_entries.alloc (aud::max (16, 2 * m_entries.len ())); + else + { + beginRemoveRows (QModelIndex (), 0, 0); + m_entries.pop (); + endRemoveRows (); + } + } + + beginInsertRows (QModelIndex (), m_entries.len (), m_entries.len ()); + m_entries.push (* entry); + endInsertRows (); } QVariant LogEntryModel::data (const QModelIndex & index, int role) const { int row = index.row (); - if (row < 0 || row >= entries.len ()) + if (row < 0 || row >= m_entries.len ()) return QVariant (); - auto & e = entries[row]; + auto & e = m_entries[row]; if (role == Qt::DisplayRole) { switch (index.column ()) { - case LogEntryColumn::Level: return QString (audlog::get_level_name (e->level)); - case LogEntryColumn::File: return QString (e->filename); - case LogEntryColumn::Line: return e->line; - case LogEntryColumn::Function: return QString (e->function); - case LogEntryColumn::Message: return QString (e->message); + case LogEntryColumn::Level: return QString (audlog::get_level_name (e.level)); + case LogEntryColumn::Function: return QString (e.function); + case LogEntryColumn::Message: return QString (e.message); } } @@ -110,8 +135,6 @@ QVariant LogEntryModel::headerData (int section, Qt::Orientation orientation, in switch (section) { case LogEntryColumn::Level: return QString (_("Level")); - case LogEntryColumn::File: return QString (_("Filename")); - case LogEntryColumn::Line: return QString (_("Line")); case LogEntryColumn::Function: return QString (_("Function")); case LogEntryColumn::Message: return QString (_("Message")); } @@ -120,18 +143,38 @@ QVariant LogEntryModel::headerData (int section, Qt::Orientation orientation, in return QVariant (); } -void LogEntryModel::addEntry (LogEntry * entry) +/* static model */ +static SmartPtr<LogEntryModel> s_model; +static audlog::Level s_level = audlog::Warning; + +static void log_handler (audlog::Level level, const char * file, int line, + const char * func, const char * message) { - if (entries.len () >= LOGENTRY_MAX) + auto messages = str_list_to_index (message, "\n"); + + for (auto & message : messages) { - beginRemoveRows (QModelIndex (), 0, 0); - entries.remove (0, 1); - endRemoveRows (); + auto entry = new LogEntry; + + entry->level = level; + entry->function = String (str_printf ("%s (%s:%d)", func, file, line)); + entry->message = std::move (message); + + event_queue ("audqt log entry", entry, aud::delete_obj<LogEntry>); } +} - beginInsertRows (QModelIndex (), entries.len (), entries.len ()); - entries.append (SmartPtr<LogEntry> (entry)); - endInsertRows (); +void log_init () +{ + s_model.capture (new LogEntryModel); + audlog::subscribe (log_handler, s_level); +} + +void log_cleanup () +{ + audlog::unsubscribe (log_handler); + event_queue_cancel ("audqt log entry"); + s_model.clear (); } /* log entry inspector */ @@ -139,23 +182,9 @@ class LogEntryInspector : public QDialog { public: LogEntryInspector (QWidget * parent = nullptr); - ~LogEntryInspector (); - - audlog::Level m_level; - - void addEntry (LogEntry * entry) - { m_model->addEntry (entry); } private: - QVBoxLayout m_layout; - LogEntryModel * m_model; - QTreeView * m_view; - - QWidget m_bottom_container; - QHBoxLayout m_bottom_layout; - QComboBox m_level_combobox; - QLabel m_level_label; void setLogLevel (audlog::Level level); }; @@ -164,69 +193,63 @@ LogEntryInspector::LogEntryInspector (QWidget * parent) : QDialog (parent) { setWindowTitle (_("Log Inspector")); - setLayout (& m_layout); - - m_model = new LogEntryModel (this); - m_view = new QTreeView (this); - m_view->setModel (m_model); + setContentsMargins (margins.TwoPt); - m_layout.addWidget (m_view); + auto view = new QTreeView (this); + view->setModel (s_model.get ()); - m_bottom_layout.setContentsMargins (0, 0, 0, 0); - - m_level_label.setText (_("Log Level:")); - m_bottom_layout.addWidget (& m_level_label); + view->setAllColumnsShowFocus (true); + view->setIndentation (0); + view->setUniformRowHeights (true); + view->scrollToBottom (); m_level_combobox.addItem (_("Debug"), audlog::Debug); m_level_combobox.addItem (_("Info"), audlog::Info); m_level_combobox.addItem (_("Warning"), audlog::Warning); m_level_combobox.addItem (_("Error"), audlog::Error); + m_level_combobox.setCurrentIndex (s_level); + QObject::connect (& m_level_combobox, static_cast <void (QComboBox::*) (int)> (&QComboBox::currentIndexChanged), [this] (int idx) { setLogLevel ((audlog::Level) idx); }); - m_bottom_layout.addWidget (& m_level_combobox); + auto btnbox = new QDialogButtonBox (this); - m_bottom_container.setLayout (& m_bottom_layout); - m_layout.addWidget (& m_bottom_container); + auto btn1 = btnbox->addButton (translate_str (N_("Cl_ear")), QDialogButtonBox::ActionRole); + btn1->setIcon (QIcon::fromTheme ("edit-clear-all")); + btn1->setAutoDefault (false); + QObject::connect (btn1, & QPushButton::clicked, [] () { + s_model.get ()->cleanup (); + }); - resize (800, 350); + auto btn2 = btnbox->addButton (QDialogButtonBox::Close); + btn2->setText (translate_str (N_("_Close"))); + btn2->setAutoDefault (false); + QObject::connect (btn2, & QPushButton::clicked, this, & QDialog::close); - setLogLevel (audlog::Info); -} + auto hbox = make_hbox (nullptr); + hbox->addWidget (new QLabel (_("Log Level:"), this)); + hbox->addWidget (& m_level_combobox); + hbox->addWidget (btnbox); -LogEntryInspector::~LogEntryInspector () -{ - audlog::unsubscribe (log_handler); + auto vbox = make_vbox (this); + vbox->addWidget (view); + vbox->addLayout (hbox); + + resize (6 * sizes.OneInch, 3 * sizes.OneInch); } static LogEntryInspector * s_inspector = nullptr; -static void log_handler (audlog::Level level, const char * file, int line, - const char * func, const char * message) -{ - LogEntry * l = new LogEntry; - - l->level = level; - l->filename = file; - l->line = line; - l->function = func; - - l->message = strdup (message); - l->message[strlen (l->message) - 1] = 0; - - s_inspector->addEntry (l); -} - void LogEntryInspector::setLogLevel (audlog::Level level) { - m_level = level; + s_level = level; audlog::unsubscribe (log_handler); audlog::subscribe (log_handler, level); - m_level_combobox.setCurrentIndex (m_level); + m_level_combobox.setCurrentIndex (level); } EXPORT void log_inspector_show () diff --git a/src/libaudqt/playlist-management.cc b/src/libaudqt/playlist-management.cc index 906acff..60b3120 100644 --- a/src/libaudqt/playlist-management.cc +++ b/src/libaudqt/playlist-management.cc @@ -34,11 +34,11 @@ namespace audqt { -static QDialog * buildRenameDialog (int playlist) +static QDialog * buildRenameDialog (Playlist playlist) { auto dialog = new QDialog; auto prompt = new QLabel (_("What would you like to call this playlist?"), dialog); - auto entry = new QLineEdit ((const char *) aud_playlist_get_title (playlist), dialog); + auto entry = new QLineEdit ((const char *) playlist.get_title (), dialog); auto rename = new QPushButton (translate_str (N_("_Rename")), dialog); auto cancel = new QPushButton (translate_str (N_("_Cancel")), dialog); @@ -46,28 +46,28 @@ static QDialog * buildRenameDialog (int playlist) buttonbox->addButton (rename, QDialogButtonBox::AcceptRole); buttonbox->addButton (cancel, QDialogButtonBox::RejectRole); - int id = aud_playlist_get_unique_id (playlist); - QObject::connect (buttonbox, & QDialogButtonBox::accepted, [dialog, entry, id] () { - int list = aud_playlist_by_unique_id (id); - if (list >= 0) - aud_playlist_set_title (list, entry->text ().toUtf8 ()); + QObject::connect (buttonbox, & QDialogButtonBox::accepted, [dialog, entry, playlist] () { + playlist.set_title (entry->text ().toUtf8 ()); dialog->close (); }); QObject::connect (buttonbox, & QDialogButtonBox::rejected, dialog, & QDialog::close); - auto layout = new QVBoxLayout (dialog); + auto layout = make_vbox (dialog); layout->addWidget (prompt); layout->addWidget (entry); + layout->addStretch (1); layout->addWidget (buttonbox); dialog->setWindowTitle (_("Rename Playlist")); + dialog->setContentsMargins (margins.EightPt); + entry->selectAll (); return dialog; } -static QDialog * buildDeleteDialog (int playlist) +static QDialog * buildDeleteDialog (Playlist playlist) { auto dialog = new QMessageBox; auto skip_prompt = new QCheckBox (translate_str (N_("_Don’t ask again")), dialog); @@ -77,7 +77,7 @@ static QDialog * buildDeleteDialog (int playlist) dialog->setIcon (QMessageBox::Question); dialog->setWindowTitle (_("Remove Playlist")); dialog->setText ((const char *) str_printf (_("Do you want to permanently remove “%s”?"), - (const char *) aud_playlist_get_title (playlist))); + (const char *) playlist.get_title ())); dialog->setCheckBox (skip_prompt); dialog->addButton (remove, QMessageBox::AcceptRole); dialog->addButton (cancel, QMessageBox::RejectRole); @@ -90,28 +90,25 @@ static QDialog * buildDeleteDialog (int playlist) }); QObject::connect (remove, & QPushButton::clicked, [dialog, playlist] () { - int id = aud_playlist_get_unique_id (playlist); - int list = aud_playlist_by_unique_id (id); - if (list >= 0) - aud_playlist_delete (list); + playlist.remove_playlist (); dialog->close (); }); return dialog; } -EXPORT void playlist_show_rename (int playlist) +EXPORT void playlist_show_rename (Playlist playlist) { auto dialog = buildRenameDialog (playlist); dialog->setAttribute (Qt::WA_DeleteOnClose); dialog->show (); } -EXPORT void playlist_confirm_delete (int playlist) +EXPORT void playlist_confirm_delete (Playlist playlist) { if (aud_get_bool ("audgui", "no_confirm_playlist_delete")) { - aud_playlist_delete (playlist); + playlist.remove_playlist (); return; } diff --git a/src/libaudqt/plugin-menu.cc b/src/libaudqt/plugin-menu.cc index e0f16ea..935d83a 100644 --- a/src/libaudqt/plugin-menu.cc +++ b/src/libaudqt/plugin-menu.cc @@ -42,19 +42,15 @@ static void show_prefs () } MenuItem default_menu_items[] = { - MenuCommand ({N_("Plugins ...")}, show_prefs), - MenuSep () + MenuCommand ({N_("Plugins ..."), "preferences-system"}, show_prefs), }; -EXPORT QMenu * menu_get_by_id (AudMenuID id) +void menu_rebuild (AudMenuID id) { if (menus[id]) - return menus[id]; - - menus[id] = new QMenu (_("Services")); - - for (auto & item : default_menu_items) - menus[id]->addAction (menu_action (item, PACKAGE, menus[id])); + menus[id]->clear (); + else + menus[id] = new QMenu (_("Services")); for (auto & item : items[id]) { @@ -62,18 +58,26 @@ EXPORT QMenu * menu_get_by_id (AudMenuID id) menus[id]->addAction (item.action.get ()); } + if (! menus[id]->isEmpty ()) + menus[id]->addAction (menu_action (MenuSep (), PACKAGE, menus[id])); + + for (auto & item : default_menu_items) + menus[id]->addAction (menu_action (item, PACKAGE, menus[id])); +} + +EXPORT QMenu * menu_get_by_id (AudMenuID id) +{ + if (! menus[id]) + menu_rebuild (id); + return menus[id]; } EXPORT void menu_add (AudMenuID id, MenuFunc func, const char * name, const char * icon) { - auto & item = items[id].append (MenuCommand ({name, icon}, func)); + items[id].append (MenuCommand ({name, icon}, func)); - if (menus[id]) - { - item.action.capture (menu_action (item.item, nullptr)); - menus[id]->addAction (item.action.get ()); - } + menu_rebuild(id); } EXPORT void menu_remove (AudMenuID id, MenuFunc func) @@ -81,7 +85,8 @@ EXPORT void menu_remove (AudMenuID id, MenuFunc func) auto is_match = [func] (ItemData & item) { return item.item.func == func; }; - items[id].remove_if (is_match, true); + if (items[id].remove_if (is_match, true)) + menu_rebuild (id); } } // namespace audqt diff --git a/src/libaudqt/prefs-builder.cc b/src/libaudqt/prefs-builder.cc index dece721..2bcdd24 100644 --- a/src/libaudqt/prefs-builder.cc +++ b/src/libaudqt/prefs-builder.cc @@ -54,14 +54,13 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c /* create new layout for child widgets */ if (dynamic_cast<QHBoxLayout *> (parent_layout)) - layout = new QHBoxLayout; + layout = make_hbox (nullptr, sizes.TwoPt); else { - layout = new QVBoxLayout; - layout->setContentsMargins (12, 0, 0, 0); + layout = make_vbox (nullptr, sizes.TwoPt); + layout->setContentsMargins (sizes.EightPt, 0, 0, 0); } - layout->setSpacing (parent_layout->spacing ()); parent_layout->addLayout (layout); orig_layout = layout; @@ -117,16 +116,15 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c if (strstr (w.label, "<b>")) { - /* double spacing above a header */ + /* extra spacing above a header */ if (orig_layout->itemAt (0)) - orig_layout->addSpacing (orig_layout->spacing ()); + orig_layout->addSpacing (sizes.EightPt); orig_layout->addWidget (label); /* create indented layout below header */ - layout = new QVBoxLayout; - layout->setContentsMargins (12, 0, 0, 0); - layout->setSpacing (orig_layout->spacing ()); + layout = make_vbox (nullptr, sizes.TwoPt); + layout->setContentsMargins (sizes.EightPt, 0, 0, 0); orig_layout->addLayout (layout); } else @@ -203,7 +201,11 @@ void prefs_populate (QBoxLayout * layout, ArrayRef<PreferencesWidget> widgets, c QFrame * f = new QFrame; f->setFrameShape (w.data.separator.horizontal ? QFrame::HLine : QFrame::VLine); f->setFrameShadow (QFrame::Sunken); + + layout->addSpacing (sizes.FourPt); layout->addWidget (f); + layout->addSpacing (sizes.FourPt); + break; } diff --git a/src/libaudqt/prefs-plugin.cc b/src/libaudqt/prefs-plugin.cc index 8b38950..03fa53d 100644 --- a/src/libaudqt/prefs-plugin.cc +++ b/src/libaudqt/prefs-plugin.cc @@ -100,6 +100,7 @@ EXPORT void plugin_prefs (PluginHandle * ph) cw->root = new QDialog; cw->root->setAttribute (Qt::WA_DeleteOnClose); + cw->root->setContentsMargins (margins.FourPt); if (p->init) p->init (); @@ -117,10 +118,7 @@ EXPORT void plugin_prefs (PluginHandle * ph) cw->root->setWindowTitle ((const char *) str_printf(_("%s Settings"), name)); - QVBoxLayout * vbox = new QVBoxLayout (cw->root); - vbox->setContentsMargins (4, 4, 4, 4); - vbox->setSpacing (4); - + auto vbox = make_vbox (cw->root, sizes.TwoPt); prefs_populate (vbox, p->widgets, header->info.domain); vbox->addStretch (1); diff --git a/src/libaudqt/prefs-pluginlist-model.cc b/src/libaudqt/prefs-pluginlist-model.cc index 0778849..969cad7 100644 --- a/src/libaudqt/prefs-pluginlist-model.cc +++ b/src/libaudqt/prefs-pluginlist-model.cc @@ -1,6 +1,6 @@ /* * prefs-pluginlist-model.cc - * Copyright 2014 William Pitcock + * Copyright 2014-2017 John Lindgren and William Pitcock * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -20,43 +20,127 @@ #include "prefs-pluginlist-model.h" #include <QIcon> + +#include <libaudcore/i18n.h> #include <libaudcore/plugins.h> +#include <libaudcore/runtime.h> namespace audqt { -PluginListModel::PluginListModel (QObject * parent, PluginType category_id) : QAbstractListModel (parent), - m_list (aud_plugin_list (category_id)) +struct PluginCategory { + PluginType type; + const char * name; +}; + +static const PluginCategory categories[] = { + { PluginType::General, N_("General") }, + { PluginType::Effect, N_("Effect") }, + { PluginType::Vis, N_("Visualization") }, + { PluginType::Input, N_("Input") }, + { PluginType::Playlist, N_("Playlist") }, + { PluginType::Transport, N_("Transport") } +}; + +static constexpr int n_categories = aud::n_elems (categories); + +// The model hierarchy is as follows: +// +// Root (invalid index) +// + General category (index with row 0, null internal pointer) +// + General plugin (index with row 0, internal pointer to PluginHandle) +// + General plugin (index with row 1, internal pointer to PluginHandle) +// + General plugin ... +// + Effect category (index with row 1, null internal pointer) +// + Effect plugin ... +// + More categories ... + +QModelIndex PluginListModel::index (int row, int column, const QModelIndex & parent) const { + // is parent the root node? + if (! parent.isValid ()) + return createIndex (row, column, nullptr); + + // is parent a plugin node? + if (parent.internalPointer () != nullptr) + return QModelIndex (); + + // parent must be a category node + int cat = parent.row (); + if (cat < 0 || cat >= n_categories) + return QModelIndex (); + auto & list = aud_plugin_list (categories[cat].type); + if (row < 0 || row >= list.len ()) + return QModelIndex (); + + return createIndex (row, column, list[row]); } -PluginListModel::~PluginListModel () +// for a plugin node, return the category node +// for all other nodes, return an invalid index +QModelIndex PluginListModel::parent (const QModelIndex & child) const { + auto p = pluginForIndex (child); + return p ? indexForType (aud_plugin_get_type (p)) : QModelIndex (); +} +// retrieve the PluginHandle from a plugin node +PluginHandle * PluginListModel::pluginForIndex (const QModelIndex & index) const +{ + return (PluginHandle *) index.internalPointer (); } -int PluginListModel::rowCount (const QModelIndex & parent) const +// look up the category node for a given plugin type +QModelIndex PluginListModel::indexForType (PluginType type) const { - return m_list.len (); + for (int cat = 0; cat < n_categories; cat ++) + { + if (categories[cat].type == type) + return createIndex (cat, 0, nullptr); + } + + return QModelIndex (); } -int PluginListModel::columnCount (const QModelIndex & parent) const +int PluginListModel::rowCount (const QModelIndex & parent) const { - return NumColumns; + // for the root node, return the # of categories + if (! parent.isValid ()) + return n_categories; + + // for a plugin node, return 0 (no children) + if (parent.internalPointer () != nullptr) + return 0; + + // for a category node, return the # of plugins + int cat = parent.row (); + if (cat < 0 || cat >= n_categories) + return 0; + + return aud_plugin_list (categories[cat].type).len (); } -QVariant PluginListModel::headerData (int section, Qt::Orientation orientation, int role) const +int PluginListModel::columnCount (const QModelIndex & parent) const { - return QVariant (); + return NumColumns; } QVariant PluginListModel::data (const QModelIndex & index, int role) const { - int row = index.row (); - if (row < 0 || row >= m_list.len ()) - return QVariant (); + auto p = pluginForIndex (index); + + if (! p) // category node? + { + if (role != Qt::DisplayRole || index.column () != 0) + return QVariant (); + + int cat = index.row (); + if (cat < 0 || cat >= n_categories) + return QVariant (); + + return QString (_(categories[cat].name)); + } - PluginHandle * p = m_list[row]; bool enabled = aud_plugin_get_enabled (p); switch (index.column ()) @@ -87,15 +171,14 @@ QVariant PluginListModel::data (const QModelIndex & index, int role) const bool PluginListModel::setData (const QModelIndex &index, const QVariant &value, int role) { - int row = index.row (); - if (row < 0 || row >= m_list.len ()) + if (role != Qt::CheckStateRole) return false; - if (role == Qt::CheckStateRole) - { - aud_plugin_enable (m_list[row], value.toUInt () != Qt::Unchecked); - } + auto p = pluginForIndex (index); + if (! p) + return false; + aud_plugin_enable (p, value.toUInt () != Qt::Unchecked); emit dataChanged (index, index.sibling (index.row (), NumColumns - 1)); return true; } @@ -105,33 +188,4 @@ Qt::ItemFlags PluginListModel::flags (const QModelIndex & index) const return (Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); } -bool PluginListModel::insertRows (int row, int count, const QModelIndex & parent) -{ - int last = row + count - 1; - beginInsertRows (parent, row, last); - endInsertRows (); - return true; -} - -bool PluginListModel::removeRows (int row, int count, const QModelIndex & parent) -{ - int last = row + count - 1; - beginRemoveRows (parent, row, last); - endRemoveRows (); - return true; -} - -void PluginListModel::updateRows (int row, int count) -{ - int bottom = row + count - 1; - auto topLeft = createIndex (row, 0); - auto bottomRight = createIndex (bottom, columnCount () - 1); - emit dataChanged (topLeft, bottomRight); -} - -void PluginListModel::updateRow (int row) -{ - updateRows (row, 1); -} - } // namespace audqt diff --git a/src/libaudqt/prefs-pluginlist-model.h b/src/libaudqt/prefs-pluginlist-model.h index 1b74b6f..aaf1874 100644 --- a/src/libaudqt/prefs-pluginlist-model.h +++ b/src/libaudqt/prefs-pluginlist-model.h @@ -1,6 +1,6 @@ /* * prefs-pluginlist-model.h - * Copyright 2014 William Pitcock + * Copyright 2014-2017 John Lindgren and William Pitcock * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -20,15 +20,14 @@ #ifndef PREFS_PLUGINLIST_MODEL_H #define PREFS_PLUGINLIST_MODEL_H -#include <QAbstractListModel> -#include <libaudcore/index.h> +#include <QAbstractItemModel> enum class PluginType; class PluginHandle; namespace audqt { -class PluginListModel : public QAbstractListModel +class PluginListModel : public QAbstractItemModel { public: enum { @@ -38,23 +37,20 @@ public: NumColumns }; - PluginListModel (QObject * parent, PluginType category_id); - ~PluginListModel (); + PluginListModel (QObject * parent) : QAbstractItemModel (parent) {} - int rowCount (const QModelIndex & parent = QModelIndex ()) const; - int columnCount (const QModelIndex & parent = QModelIndex ()) const; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; - QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - bool setData (const QModelIndex & index, const QVariant & value, int role = Qt::DisplayRole); - Qt::ItemFlags flags (const QModelIndex & parent = QModelIndex ()) const; + QModelIndex index (int row, int column, const QModelIndex & parent) const; + QModelIndex parent (const QModelIndex & child) const; + + PluginHandle * pluginForIndex (const QModelIndex & index) const; + QModelIndex indexForType (PluginType type) const; - bool insertRows (int row, int count, const QModelIndex & parent = QModelIndex ()); - bool removeRows (int row, int count, const QModelIndex & parent = QModelIndex ()); - void updateRows (int row, int count); - void updateRow (int row); + int rowCount (const QModelIndex & parent) const; + int columnCount (const QModelIndex & parent) const; -private: - const Index<PluginHandle *> & m_list; + QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; + bool setData (const QModelIndex & index, const QVariant & value, int role); + Qt::ItemFlags flags (const QModelIndex & parent) const; }; } // namespace audqt diff --git a/src/libaudqt/prefs-widget.cc b/src/libaudqt/prefs-widget.cc index 096809b..c67280f 100644 --- a/src/libaudqt/prefs-widget.cc +++ b/src/libaudqt/prefs-widget.cc @@ -116,9 +116,7 @@ IntegerWidget::IntegerWidget (const PreferencesWidget * parent, const char * dom HookableWidget (parent, domain), m_spinner (new QSpinBox) { - auto layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); + auto layout = make_hbox (this); if (parent->label) layout->addWidget (new QLabel (translate_str (parent->label, domain))); @@ -155,9 +153,7 @@ DoubleWidget::DoubleWidget (const PreferencesWidget * parent, const char * domai HookableWidget (parent, domain), m_spinner (new QDoubleSpinBox) { - auto layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); + auto layout = make_hbox (this); if (parent->label) layout->addWidget (new QLabel (translate_str (parent->label, domain))); @@ -194,9 +190,7 @@ StringWidget::StringWidget (const PreferencesWidget * parent, const char * domai HookableWidget (parent, domain), m_lineedit (new QLineEdit) { - auto layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); + auto layout = make_hbox (this); if (parent->label) layout->addWidget (new QLabel (translate_str (parent->label, domain))); @@ -224,9 +218,7 @@ ComboBoxWidget::ComboBoxWidget (const PreferencesWidget * parent, const char * d HookableWidget (parent, domain), m_combobox (new QComboBox) { - auto layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); + auto layout = make_hbox (this); if (parent->label) layout->addWidget (new QLabel (translate_str (parent->label, domain))); @@ -324,12 +316,9 @@ BoxWidget::BoxWidget (const PreferencesWidget * parent, const char * domain, boo { QBoxLayout * layout; if (parent->data.box.horizontal) - layout = new QHBoxLayout (this); + layout = make_hbox (this, sizes.TwoPt); else - layout = new QVBoxLayout (this); - - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); + layout = make_vbox (this, sizes.TwoPt); prefs_populate (layout, parent->data.box.widgets, domain); @@ -341,10 +330,7 @@ BoxWidget::BoxWidget (const PreferencesWidget * parent, const char * domain, boo TableWidget::TableWidget (const PreferencesWidget * parent, const char * domain) { // TODO: proper table layout - auto layout = new QVBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); - layout->setSpacing (4); - + auto layout = make_vbox (this, sizes.TwoPt); prefs_populate (layout, parent->data.table.widgets, domain); } @@ -353,10 +339,9 @@ NotebookWidget::NotebookWidget (const PreferencesWidget * parent, const char * d for (const NotebookTab & tab : parent->data.notebook.tabs) { auto widget = new QWidget (this); - auto layout = new QVBoxLayout (widget); - layout->setContentsMargins (4, 4, 4, 4); - layout->setSpacing (4); + widget->setContentsMargins (margins.FourPt); + auto layout = make_vbox (widget, sizes.TwoPt); prefs_populate (layout, tab.widgets, domain); layout->addStretch (1); diff --git a/src/libaudqt/prefs-window.cc b/src/libaudqt/prefs-window.cc index c11e133..19f5dc1 100644 --- a/src/libaudqt/prefs-window.cc +++ b/src/libaudqt/prefs-window.cc @@ -19,6 +19,7 @@ */ #include <QAction> +#include <QCheckBox> #include <QComboBox> #include <QDialog> #include <QDialogButtonBox> @@ -55,13 +56,63 @@ namespace audqt { -struct Category { - const char * icon_path; - const char * name; +class PrefsWindow : public QDialog +{ +public: + static PrefsWindow * get_instance () { + if (! instance) + (void) new PrefsWindow; + return instance; + } + + static void destroy_instance () { + if (instance) + delete instance; + } + + static ArrayRef<ComboItem> get_output_combo () { + return {instance->output_combo_elements.begin (), + instance->output_combo_elements.len ()}; + } + + static int output_combo_selected; + static void output_combo_changed () { instance->output_change (); } + + static void * get_output_config_button () { return instance->output_config_button; } + static void * get_output_about_button () { return instance->output_about_button; } + + static void * get_record_checkbox () { return instance->record_checkbox; } + static void * get_record_config_button () { return instance->record_config_button; } + static void * get_record_about_button () { return instance->record_about_button; } + +private: + static PrefsWindow * instance; + + PrefsWindow (); + ~PrefsWindow () { instance = nullptr; } + + Index<ComboItem> output_combo_elements; + QPushButton * output_config_button, * output_about_button; + + QCheckBox * record_checkbox; + QPushButton * record_config_button, * record_about_button; + + void output_setup (); + void output_change (); + + void record_setup (); + void record_update (); + + const HookReceiver<PrefsWindow> + record_hook {"enable record", this, & PrefsWindow::record_update}; }; -struct PluginCategory { - PluginType type; +/* static data */ +PrefsWindow * PrefsWindow::instance = nullptr; +int PrefsWindow::output_combo_selected; + +struct Category { + const char * icon_path; const char * name; }; @@ -89,28 +140,20 @@ static const Category categories[] = { { "plugins.png", N_("Plugins") } }; -static const PluginCategory plugin_categories[] = { - { PluginType::General, N_("General") }, - { PluginType::Effect, N_("Effect") }, - { PluginType::Vis, N_("Visualization") }, - { PluginType::Input, N_("Input") }, - { PluginType::Playlist, N_("Playlist") }, - { PluginType::Transport, N_("Transport") } -}; - static const TitleFieldTag title_field_tags[] = { - { N_("Artist") , "${artist}" }, - { N_("Album") , "${album}" }, - { N_("Title") , "${title}" }, + { N_("Artist") , "${artist}" }, + { N_("Album") , "${album}" }, + { N_("Album Artist"), "${album-artist}" }, + { N_("Title") , "${title}" }, { N_("Track number"), "${track-number}" }, - { N_("Genre") , "${genre}" }, - { N_("File name") , "${file-name}" }, - { N_("File path") , "${file-path}" }, - { N_("Date") , "${date}" }, - { N_("Year") , "${year}" }, - { N_("Comment") , "${comment}" }, - { N_("Codec") , "${codec}" }, - { N_("Quality") , "${quality}" } + { N_("Genre") , "${genre}" }, + { N_("File name") , "${file-name}" }, + { N_("File path") , "${file-path}" }, + { N_("Date") , "${date}" }, + { N_("Year") , "${year}" }, + { N_("Comment") , "${comment}" }, + { N_("Codec") , "${codec}" }, + { N_("Quality") , "${quality}" } }; static const ComboItem chardet_detector_presets[] = { @@ -136,6 +179,19 @@ static const ComboItem bitdepth_elements[] = { 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 const ComboItem replaygainmode_elements[] = { + ComboItem (N_("Track"), (int) ReplayGainMode::Track), + ComboItem (N_("Album"), (int) ReplayGainMode::Album), + ComboItem (N_("Based on shuffle"), (int) ReplayGainMode::Automatic) +}; + static Index<ComboItem> iface_combo_elements; static int iface_combo_selected; static QWidget * iface_prefs_box; @@ -145,30 +201,28 @@ static void iface_combo_changed (); static void * iface_create_prefs_box (); static const PreferencesWidget appearance_page_widgets[] = { - WidgetLabel (N_("<b>Interface Settings</b>")), - WidgetCombo (N_("Interface plugin:"), + WidgetCombo (N_("Interface:"), WidgetInt (iface_combo_selected, iface_combo_changed), {0, iface_combo_fill}), + WidgetSeparator ({true}), WidgetCustomQt (iface_create_prefs_box) }; -static Index<ComboItem> output_combo_elements; -static int output_combo_selected; -static QPushButton * output_config_button; -static QPushButton * output_about_button; - -static ArrayRef<ComboItem> output_combo_fill (); -static void output_combo_changed (); -static void * output_create_config_button (); -static void * output_create_about_button (); static void output_bit_depth_changed (); static const PreferencesWidget output_combo_widgets[] = { WidgetCombo (N_("Output plugin:"), - 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) + WidgetInt (PrefsWindow::output_combo_selected, + PrefsWindow::output_combo_changed, + "audqt update output combo"), + {0, PrefsWindow::get_output_combo}), + WidgetCustomQt (PrefsWindow::get_output_config_button), + WidgetCustomQt (PrefsWindow::get_output_about_button) +}; + +static const PreferencesWidget record_buttons[] = { + WidgetCustomQt (PrefsWindow::get_record_config_button), + WidgetCustomQt (PrefsWindow::get_record_about_button) }; static const PreferencesWidget gain_table[] = { @@ -193,11 +247,19 @@ 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>Recording Settings</b>")), + WidgetCustomQt (PrefsWindow::get_record_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"), + WidgetCombo (N_("Mode:"), + WidgetInt (0, "replay_gain_mode"), + {{replaygainmode_elements}}, WIDGET_CHILD), WidgetCheck (N_("Prevent clipping (recommended)"), WidgetBool (0, "enable_clipping_prevention"), @@ -336,6 +398,8 @@ static void * create_titlestring_table () { QWidget * w = new QWidget; QGridLayout * l = new QGridLayout (w); + l->setContentsMargins (0, 0, 0, 0); + l->setSpacing (sizes.TwoPt); QLabel * lbl = new QLabel (_("Title format:"), w); l->addWidget (lbl, 0, 0); @@ -362,13 +426,12 @@ static void * create_titlestring_table () cbox->setCurrentIndex (i); } - QObject::connect (le, &QLineEdit::textChanged, [=] (const QString & text) { + QObject::connect (le, & QLineEdit::textChanged, [] (const QString & text) { aud_set_str (nullptr, "generic_title_format", text.toUtf8 ().data ()); }); - QObject::connect (cbox, - static_cast <void (QComboBox::*) (int)> (&QComboBox::currentIndexChanged), - [=] (int idx) { + void (QComboBox::* signal) (int) = & QComboBox::currentIndexChanged; + QObject::connect (cbox, signal, [le] (int idx) { if (idx < TITLESTRING_NPRESETS) le->setText (titlestring_presets [idx]); }); @@ -377,7 +440,6 @@ static void * create_titlestring_table () QPushButton * btn_mnu = new QPushButton (w); btn_mnu->setFixedWidth (btn_mnu->sizeHint ().height ()); btn_mnu->setIcon (QIcon::fromTheme ("list-add")); - btn_mnu->setIconSize (QSize (16, 16)); l->addWidget (btn_mnu, 1, 2); QMenu * mnu_fields = new QMenu (w); @@ -419,10 +481,7 @@ static void iface_fill_prefs_box () Plugin * header = (Plugin *) aud_plugin_get_header (aud_plugin_get_current (PluginType::Iface)); if (header && header->info.prefs) { - QVBoxLayout * vbox = new QVBoxLayout (iface_prefs_box); - - vbox->setContentsMargins (0, 0, 0, 0); - vbox->setSpacing (4); + auto vbox = make_vbox (iface_prefs_box, sizes.TwoPt); prefs_populate (vbox, header->info.prefs->widgets, header->info.domain); } } @@ -449,8 +508,8 @@ static ArrayRef<ComboItem> iface_combo_fill () if (! iface_combo_elements.len ()) { iface_combo_elements = fill_plugin_combo (PluginType::Iface); - iface_combo_selected = aud_plugin_list (PluginType::Iface) - .find (aud_plugin_get_current (PluginType::Iface)); + iface_combo_selected = aud_plugin_list (PluginType::Iface). + find (aud_plugin_get_current (PluginType::Iface)); } return {iface_combo_elements.begin (), iface_combo_elements.len ()}; @@ -463,64 +522,6 @@ static void * iface_create_prefs_box () return iface_prefs_box; } -static void output_combo_changed () -{ - 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 () -{ - bool enabled = aud_plugin_has_configure (aud_plugin_get_current (PluginType::Output)); - - output_config_button = new QPushButton (translate_str (N_("_Settings"))); - output_config_button->setEnabled (enabled); - - QObject::connect (output_config_button, & QAbstractButton::clicked, [=] (bool) { - plugin_prefs (aud_plugin_get_current (PluginType::Output)); - }); - - return output_config_button; -} - -static void * output_create_about_button () -{ - bool enabled = aud_plugin_has_about (aud_plugin_get_current (PluginType::Output)); - - output_about_button = new QPushButton (translate_str (N_("_About"))); - output_about_button->setEnabled (enabled); - - QObject::connect (output_about_button, &QAbstractButton::clicked, [=] (bool) { - plugin_about (aud_plugin_get_current (PluginType::Output)); - }); - - return output_about_button; -} - -static ArrayRef<ComboItem> output_combo_fill () -{ - if (! output_combo_elements.len ()) - { - output_combo_elements = fill_plugin_combo (PluginType::Output); - output_combo_selected = aud_plugin_list (PluginType::Output) - .find (aud_plugin_get_current (PluginType::Output)); - } - - return {output_combo_elements.begin (), output_combo_elements.len ()}; -} - static void output_bit_depth_changed () { aud_output_reset (OutputReset::ReopenStream); @@ -529,91 +530,81 @@ static void output_bit_depth_changed () static void create_category (QStackedWidget * notebook, ArrayRef<PreferencesWidget> widgets) { QWidget * w = new QWidget; - QVBoxLayout * vbox = new QVBoxLayout (w); - - vbox->setContentsMargins (0, 0, 0, 0); - vbox->setSpacing (4); + auto vbox = make_vbox (w, sizes.TwoPt); prefs_populate (vbox, widgets, nullptr); vbox->addStretch (1); notebook->addWidget (w); } -static void create_plugin_category_page (PluginType category_id, const char * category_name, QTabWidget * parent) +static QTreeView * s_plugin_view; +static PluginListModel * s_plugin_model; + +static void create_plugin_category (QStackedWidget * parent) { - QTreeView * view = new QTreeView; - QHeaderView * header = view->header (); + s_plugin_view = new QTreeView (parent); + s_plugin_model = new PluginListModel (s_plugin_view); + + s_plugin_view->setModel (s_plugin_model); + s_plugin_view->setSelectionMode (QTreeView::NoSelection); - view->setIndentation (0); - view->setModel (new PluginListModel (view, category_id)); - view->setSelectionMode (view->NoSelection); + auto header = s_plugin_view->header (); header->hide (); header->setSectionResizeMode (header->ResizeToContents); header->setStretchLastSection (false); - parent->addTab (view, category_name); + parent->addWidget (s_plugin_view); - QObject::connect (view, & QAbstractItemView::clicked, - [category_id] (const QModelIndex & index) + QObject::connect (s_plugin_view, & QAbstractItemView::clicked, [] (const QModelIndex & index) { - int row = index.row (); - auto & list = aud_plugin_list (category_id); - - if (row < 0 || row >= list.len () || ! aud_plugin_get_enabled (list[row])) + auto p = s_plugin_model->pluginForIndex (index); + if (! p) return; switch (index.column ()) { case PluginListModel::AboutColumn: - plugin_about (list[row]); + plugin_about (p); break; case PluginListModel::SettingsColumn: - plugin_prefs (list[row]); + plugin_prefs (p); break; } }); } -static QTabWidget * plugin_tabs = nullptr; - -static void create_plugin_category (QStackedWidget * parent) -{ - plugin_tabs = new QTabWidget; - - for (const PluginCategory & w : plugin_categories) - { - create_plugin_category_page (w.type, _(w.name), plugin_tabs); - } - - parent->addWidget (plugin_tabs); -} - -static QDialog * s_prefswin = nullptr; static QStackedWidget * s_category_notebook = nullptr; -static void create_prefs_window () +PrefsWindow::PrefsWindow () : + output_combo_elements (fill_plugin_combo (PluginType::Output)), + output_config_button (new QPushButton (translate_str (N_("_Settings")))), + output_about_button (new QPushButton (translate_str (N_("_About")))), + record_checkbox (new QCheckBox), + record_config_button (new QPushButton (translate_str (N_("_Settings")))), + record_about_button (new QPushButton (translate_str (N_("_About")))) { - s_prefswin = new QDialog; - s_prefswin->setWindowTitle (_("Audacious Settings")); - s_prefswin->setAttribute (Qt::WA_DeleteOnClose); - - QObject::connect (s_prefswin, & QObject::destroyed, [] () { - s_prefswin = nullptr; - }); + /* initialize static data */ + instance = this; + output_combo_selected = aud_plugin_list (PluginType::Output) + .find (aud_plugin_get_current (PluginType::Output)); - QVBoxLayout * vbox_parent = new QVBoxLayout (s_prefswin); - vbox_parent->setSpacing (0); - vbox_parent->setContentsMargins (0, 0, 0, 0); + setAttribute (Qt::WA_DeleteOnClose); + setWindowTitle (_("Audacious Settings")); + setContentsMargins (0, 0, 0, 0); QToolBar * toolbar = new QToolBar; toolbar->setToolButtonStyle (Qt::ToolButtonTextUnderIcon); - vbox_parent->addWidget (toolbar); QWidget * child = new QWidget; - QVBoxLayout * child_vbox = new QVBoxLayout (child); + child->setContentsMargins (margins.FourPt); + + auto vbox_parent = make_vbox (this); + vbox_parent->addWidget (toolbar); vbox_parent->addWidget (child); + auto child_vbox = make_vbox (child); + s_category_notebook = new QStackedWidget; child_vbox->addWidget (s_category_notebook); @@ -628,9 +619,9 @@ static void create_prefs_window () bbox->button (QDialogButtonBox::Close)->setText (translate_str (N_("_Close"))); child_vbox->addWidget (bbox); - QObject::connect (bbox, & QDialogButtonBox::rejected, s_prefswin, & QObject::deleteLater); + QObject::connect (bbox, & QDialogButtonBox::rejected, this, & QObject::deleteLater); - QSignalMapper * mapper = new QSignalMapper (s_prefswin); + QSignalMapper * mapper = new QSignalMapper (this); const char * data_dir = aud_get_path (AudPath::DataDir); QObject::connect (mapper, static_cast <void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), @@ -642,24 +633,101 @@ static void create_prefs_window () QAction * a = new QAction (ico, translate_str (categories[i].name), toolbar); toolbar->addAction (a); - mapper->setMapping (a, i); - QObject::connect (a, & QAction::triggered, mapper, static_cast <void (QSignalMapper::*)()>(& QSignalMapper::map)); + void (QSignalMapper::* slot) () = & QSignalMapper::map; + QObject::connect (a, & QAction::triggered, mapper, slot); } + + output_setup (); + record_setup (); + record_update (); } -EXPORT void prefswin_show () +void PrefsWindow::output_setup () { - if (! s_prefswin) - create_prefs_window (); + auto p = aud_plugin_get_current (PluginType::Output); - window_bring_to_front (s_prefswin); + output_config_button->setEnabled (aud_plugin_has_configure (p)); + output_about_button->setEnabled (aud_plugin_has_about (p)); + + QObject::connect (output_config_button, & QPushButton::clicked, [] (bool) { + plugin_prefs (aud_plugin_get_current (PluginType::Output)); + }); + + QObject::connect (output_about_button, & QPushButton::clicked, [] (bool) { + plugin_about (aud_plugin_get_current (PluginType::Output)); + }); +} + +void PrefsWindow::output_change () +{ + auto & list = aud_plugin_list (PluginType::Output); + auto p = list[output_combo_selected]; + + if (aud_plugin_enable (p, true)) + { + output_config_button->setEnabled (aud_plugin_has_configure (p)); + output_about_button->setEnabled (aud_plugin_has_about (p)); + } + 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); + } +} + +void PrefsWindow::record_setup () +{ + QObject::connect (record_checkbox, & QCheckBox::clicked, [] (bool checked) { + aud_drct_enable_record (checked); + }); + + QObject::connect (record_config_button, & QPushButton::clicked, [] (bool) { + if (aud_drct_get_record_enabled ()) + plugin_prefs (aud_drct_get_record_plugin ()); + }); + + QObject::connect (record_about_button, & QPushButton::clicked, [] (bool) { + if (aud_drct_get_record_enabled ()) + plugin_about (aud_drct_get_record_plugin ()); + }); +} + +void PrefsWindow::record_update () +{ + auto p = aud_drct_get_record_plugin (); + + if (p) + { + bool enabled = aud_drct_get_record_enabled (); + auto text = str_printf (_("Enable audio stream recording with %s"), aud_plugin_get_name (p)); + + record_checkbox->setEnabled (true); + record_checkbox->setText ((const char *) text); + record_checkbox->setChecked (enabled); + record_config_button->setEnabled (enabled && aud_plugin_has_configure (p)); + record_about_button->setEnabled (enabled && aud_plugin_has_about (p)); + } + else + { + record_checkbox->setEnabled (false); + record_checkbox->setText (_("No audio recording plugin available")); + record_checkbox->setChecked (false); + record_config_button->setEnabled (false); + record_about_button->setEnabled (false); + } +} + +EXPORT void prefswin_show () +{ + window_bring_to_front (PrefsWindow::get_instance ()); } EXPORT void prefswin_hide () { - delete s_prefswin; + PrefsWindow::destroy_instance (); } EXPORT void prefswin_show_page (int id, bool show) @@ -667,35 +735,34 @@ EXPORT void prefswin_show_page (int id, bool show) if (id < 0 || id > CATEGORY_COUNT) return; - if (! s_prefswin) - create_prefs_window (); - + auto win = PrefsWindow::get_instance (); s_category_notebook->setCurrentIndex (id); if (show) - window_bring_to_front (s_prefswin); + window_bring_to_front (win); } EXPORT void prefswin_show_plugin_page (PluginType type) { - if (! s_prefswin) - create_prefs_window (); - if (type == PluginType::Iface) - return prefswin_show_page (CATEGORY_APPEARANCE); + prefswin_show_page (CATEGORY_APPEARANCE); else if (type == PluginType::Output) - return prefswin_show_page (CATEGORY_AUDIO); + prefswin_show_page (CATEGORY_AUDIO); else { prefswin_show_page (CATEGORY_PLUGINS, false); - for (const PluginCategory & category : plugin_categories) + s_plugin_view->collapseAll (); + + auto index = s_plugin_model->indexForType (type); + if (index.isValid ()) { - if (category.type == type) - plugin_tabs->setCurrentIndex (& category - plugin_categories); + s_plugin_view->expand (index); + s_plugin_view->scrollTo (index, QTreeView::PositionAtTop); + s_plugin_view->setCurrentIndex (index); } - window_bring_to_front (s_prefswin); + window_bring_to_front (PrefsWindow::get_instance ()); } } diff --git a/src/libaudqt/queue-manager.cc b/src/libaudqt/queue-manager.cc index 59dbc08..11301b3 100644 --- a/src/libaudqt/queue-manager.cc +++ b/src/libaudqt/queue-manager.cc @@ -60,14 +60,14 @@ QVariant QueueManagerModel::data (const QModelIndex & index, int role) const { if (role == Qt::DisplayRole) { - int list = aud_playlist_get_active (); - int entry = aud_playlist_queue_get_entry (list, index.row ()); + auto list = Playlist::active_playlist (); + int entry = list.queue_get_entry (index.row ()); if (index.column () == 0) return entry + 1; else { - Tuple tuple = aud_playlist_entry_get_tuple (list, entry, Playlist::NoWait); + Tuple tuple = list.entry_tuple (entry, Playlist::NoWait); return QString ((const char *) tuple.get_str (Tuple::FormattedTitle)); } } @@ -79,8 +79,8 @@ QVariant QueueManagerModel::data (const QModelIndex & index, int role) const void QueueManagerModel::update (QItemSelectionModel * sel) { - int list = aud_playlist_get_active (); - int rows = aud_playlist_queue_count (list); + auto list = Playlist::active_playlist (); + int rows = list.n_queued (); int keep = aud::min (rows, m_rows); m_in_update = true; @@ -107,7 +107,7 @@ void QueueManagerModel::update (QItemSelectionModel * sel) for (int i = 0; i < rows; i ++) { - if (aud_playlist_entry_get_selected (list, aud_playlist_queue_get_entry (list, i))) + if (list.entry_selected (list.queue_get_entry (i))) sel->select (createIndex (i, 0), sel->Select | sel->Rows); else sel->select (createIndex (i, 0), sel->Deselect | sel->Rows); @@ -122,15 +122,13 @@ void QueueManagerModel::selectionChanged (const QItemSelection & selected, if (m_in_update) return; - int list = aud_playlist_get_active (); + auto list = Playlist::active_playlist (); for (auto & index : selected.indexes ()) - aud_playlist_entry_set_selected (list, - aud_playlist_queue_get_entry (list, index.row ()), true); + list.select_entry (list.queue_get_entry (index.row ()), true); for (auto & index : deselected.indexes ()) - aud_playlist_entry_set_selected (list, - aud_playlist_queue_get_entry (list, index.row ()), false); + list.select_entry (list.queue_get_entry (index.row ()), false); } class QueueManagerDialog : public QDialog @@ -139,7 +137,6 @@ public: QueueManagerDialog (QWidget * parent = nullptr); private: - QVBoxLayout m_layout; QTreeView m_treeview; QDialogButtonBox m_buttonbox; QPushButton m_btn_unqueue; @@ -157,6 +154,9 @@ private: QueueManagerDialog::QueueManagerDialog (QWidget * parent) : QDialog (parent) { + setWindowTitle (_("Queue Manager")); + setContentsMargins (margins.TwoPt); + m_btn_unqueue.setText (translate_str (N_("_Unqueue"))); m_btn_close.setText (translate_str (N_("_Close"))); @@ -166,39 +166,37 @@ QueueManagerDialog::QueueManagerDialog (QWidget * parent) : m_buttonbox.addButton (& m_btn_close, QDialogButtonBox::AcceptRole); m_buttonbox.addButton (& m_btn_unqueue, QDialogButtonBox::AcceptRole); - m_layout.addWidget (& m_treeview); - m_layout.addWidget (& m_buttonbox); + auto layout = make_vbox (this); + layout->addWidget (& m_treeview); + layout->addWidget (& m_buttonbox); m_treeview.setIndentation (0); m_treeview.setModel (& m_model); m_treeview.setSelectionMode (QAbstractItemView::ExtendedSelection); m_treeview.setHeaderHidden (true); - setLayout (& m_layout); - setWindowTitle (_("Queue Manager")); - update (); connect (m_treeview.selectionModel (), & QItemSelectionModel::selectionChanged, & m_model, & QueueManagerModel::selectionChanged); - resize (500, 250); + resize (4 * sizes.OneInch, 3 * sizes.OneInch); } void QueueManagerDialog::removeSelected () { - int list = aud_playlist_get_active (); - int count = aud_playlist_queue_count (list); + auto list = Playlist::active_playlist (); + int count = list.n_queued (); for (int i = 0; i < count; ) { - int entry = aud_playlist_queue_get_entry (list, i); + int entry = list.queue_get_entry (i); - if (aud_playlist_entry_get_selected (list, entry)) + if (list.entry_selected (entry)) { - aud_playlist_queue_delete (list, i, 1); - aud_playlist_entry_set_selected (list, entry, false); + list.queue_remove (i); + list.select_entry (entry, false); count --; } else diff --git a/src/libaudqt/url-opener.cc b/src/libaudqt/url-opener.cc index 37b6e14..49645f0 100644 --- a/src/libaudqt/url-opener.cc +++ b/src/libaudqt/url-opener.cc @@ -51,12 +51,13 @@ static QDialog * buildUrlDialog (bool open) auto dialog = new QDialog; dialog->setWindowTitle (title); - dialog->setMinimumWidth (325); + dialog->setContentsMargins (margins.EightPt); auto label = new QLabel (_("Enter URL:"), dialog); auto combobox = new QComboBox (dialog); combobox->setEditable (true); + combobox->setMinimumContentsLength (50); auto button1 = new QPushButton (translate_str (verb), dialog); button1->setIcon (QIcon::fromTheme (icon)); @@ -67,12 +68,11 @@ static QDialog * buildUrlDialog (bool open) auto buttonbox = new QDialogButtonBox (dialog); buttonbox->addButton (button1, QDialogButtonBox::AcceptRole); buttonbox->addButton (button2, QDialogButtonBox::RejectRole); - buttonbox->setContentsMargins (0, 10, 0, 0); - auto layout = new QVBoxLayout (dialog); - layout->setSizeConstraint (QLayout::SetFixedSize); + auto layout = make_vbox (dialog); layout->addWidget (label); layout->addWidget (combobox); + layout->addStretch (1); layout->addWidget (buttonbox); for (int i = 0;; i ++) diff --git a/src/libaudqt/util.cc b/src/libaudqt/util.cc index 9ca03b0..14ed33c 100644 --- a/src/libaudqt/util.cc +++ b/src/libaudqt/util.cc @@ -20,6 +20,7 @@ #include <stdlib.h> #include <QApplication> +#include <QDesktopWidget> #include <QPushButton> #include <QVBoxLayout> @@ -27,6 +28,7 @@ #include <libaudcore/i18n.h> #include <libaudcore/runtime.h> +#include "libaudqt-internal.h" #include "libaudqt.h" namespace audqt { @@ -34,6 +36,12 @@ namespace audqt { static int init_count; static QApplication * qapp; +static PixelSizes sizes_local; +static PixelMargins margins_local; + +EXPORT const PixelSizes & sizes = sizes_local; +EXPORT const PixelMargins & margins = margins_local; + EXPORT void init () { if (init_count ++ || qapp) @@ -53,6 +61,18 @@ EXPORT void init () qapp->setApplicationName (_("Audacious")); qapp->setWindowIcon (QIcon::fromTheme (app_name)); + + auto desktop = qapp->desktop (); + sizes_local.OneInch = aud::max (96, (desktop->logicalDpiX () + desktop->logicalDpiY ()) / 2); + sizes_local.TwoPt = aud::rescale (2, 72, sizes_local.OneInch); + sizes_local.FourPt = aud::rescale (4, 72, sizes_local.OneInch); + sizes_local.EightPt = aud::rescale (8, 72, sizes_local.OneInch); + + margins_local.TwoPt = QMargins (sizes.TwoPt, sizes.TwoPt, sizes.TwoPt, sizes.TwoPt); + margins_local.FourPt = QMargins (sizes.FourPt, sizes.FourPt, sizes.FourPt, sizes.FourPt); + margins_local.EightPt = QMargins (sizes.EightPt, sizes.EightPt, sizes.EightPt, sizes.EightPt); + + log_init (); } EXPORT void run () @@ -76,6 +96,24 @@ EXPORT void cleanup () log_inspector_hide (); prefswin_hide (); queue_manager_hide (); + + log_cleanup (); +} + +EXPORT QHBoxLayout * make_hbox (QWidget * parent, int spacing) +{ + auto layout = new QHBoxLayout (parent); + layout->setContentsMargins (0, 0, 0, 0); + layout->setSpacing (spacing); + return layout; +} + +EXPORT QVBoxLayout * make_vbox (QWidget * parent, int spacing) +{ + auto layout = new QVBoxLayout (parent); + layout->setContentsMargins (0, 0, 0, 0); + layout->setSpacing (spacing); + return layout; } EXPORT void enable_layout (QLayout * layout, bool enabled) diff --git a/src/libaudqt/volumebutton.cc b/src/libaudqt/volumebutton.cc index ca25310..2668263 100644 --- a/src/libaudqt/volumebutton.cc +++ b/src/libaudqt/volumebutton.cc @@ -57,15 +57,15 @@ VolumeButton::VolumeButton (QWidget * parent) : m_container = new QFrame (this, Qt::Popup); m_container->setFrameShape (QFrame::StyledPanel); - auto layout = new QVBoxLayout (m_container); - layout->setSpacing (0); - layout->setMargin (2); - m_slider = new QSlider (Qt::Vertical, this); + m_slider->setMinimumHeight (audqt::sizes.OneInch); m_slider->setRange (0, 100); m_slider->setSingleStep (2); m_slider->setPageStep (20); + auto layout = make_vbox (m_container, sizes.TwoPt); + layout->setContentsMargins (margins.TwoPt); + layout->addWidget (newSliderButton (5)); layout->addWidget (m_slider); layout->addWidget (newSliderButton (-5)); @@ -87,11 +87,11 @@ void VolumeButton::updateIcon (int val) { if (val == 0) setIcon (QIcon::fromTheme ("audio-volume-muted")); - else if (val > 0 && val < 35) + else if (val < 34) setIcon (QIcon::fromTheme ("audio-volume-low")); - else if (val >= 35 && val < 70) + else if (val < 67) setIcon (QIcon::fromTheme ("audio-volume-medium")); - else if (val >= 70) + else setIcon (QIcon::fromTheme ("audio-volume-high")); setToolTip (QString ("%1 %").arg (val)); diff --git a/src/libaudtag/id3/id3v24.cc b/src/libaudtag/id3/id3v24.cc index c38f227..f8ab6e9 100644 --- a/src/libaudtag/id3/id3v24.cc +++ b/src/libaudtag/id3/id3v24.cc @@ -407,31 +407,22 @@ static bool write_frame (VFSFile & file, const GenericFrame & frame, int version return true; } -struct WriteState { - VFSFile & file; - int version; - int written_size; -}; - -static void write_frame_list (const String & key, FrameList & list, void * user) +static int write_all_frames (VFSFile & file, FrameDict & dict, int version) { - WriteState * state = (WriteState *) user; + int written_size = 0; - for (const GenericFrame & frame : list) + dict.iterate ([&] (const String & key, FrameList & list) { - int size; - if (write_frame (state->file, frame, state->version, & size)) - state->written_size += size; - } -} - -static int write_all_frames (VFSFile & file, FrameDict & dict, int version) -{ - WriteState state = {file, version, 0}; - dict.iterate (write_frame_list, & state); + for (const GenericFrame & frame : list) + { + int size; + if (write_frame (file, frame, version, & size)) + written_size += size; + } + }); - AUDDBG ("Total frame bytes written = %d.\n", state.written_size); - return state.written_size; + AUDDBG ("Total frame bytes written = %d.\n", written_size); + return written_size; } static bool write_header (VFSFile & file, int version, int size) |