<p>Disobedience now supports playlist editing.</p>
+ <p>Disobedience has a <a href="disobedience/manual/index.html">new
+ manual</a>.</p>
+
</div>
<h3>Web Interface</h3>
<td>Mac DisOrder uses wrong sound device</td>
</tr>
+ <tr>
+ <td><a href="http://code.google.com/p/disorder/issues/detail?id=32">#32</a></d>
+ <td>Excessively verbose log chatter on shutdown</td>
+ </tr>
+
<tr>
<td><a href="http://code.google.com/p/disorder/issues/detail?id=33">#33</a></d>
<td>(Some) plugins need -lm.</td>
<td>build-time dependency on <tt>oggdec</tt> removed</td>
</tr>
-
+ <tr>
+ <td><a href="http://code.google.com/p/disorder/issues/detail?id=49">#49</a></d>
+ <td>Disobedience's 'When' column gets out of date</td>
+ </tr>
+
</table>
-
</div>
</div>
<p class=chapter><a href="misc.html">Appendix</a></p>
<ul>
- <li>Reporting bugs</li>
- <li>Copyright notice</li>
+ <li>Network play.</li>
+ <li>Reporting bugs.</li>
+ <li>Copyright notice.</li>
</ul>
</body>
<body>
<h1>Appendix</h1>
+ <h2><a name=netplay>Network Play</a></h2>
+
+ <p>Network play uses a background copy
+ of <tt>disorder-playrtp</tt>. If you quit Disobedience the
+ player will continue playing and can be disabled from a later
+ run of Disobedience.</p>
+
+ <p>The player will log to <tt>~/.disorder/HOSTNAME-rtp.log</tt> so
+ look there if it does not seem to be working.</p>
+
+ <p>You can stop it without running Disobedience by the command
+ <tt>killall disorder-playrtp</tt>.</p>
+
<h2><a name=bugs>Reporting Bugs</a></h2>
<p>Please report bugs using
DisOrder's <a href="http://code.google.com/p/disorder/issues/list">bug
tracker</a>.</p>
+ <p>Known problems include:</p>
+
+ <ul>
+
+ <li>There is no particular provision for multiple users of the
+ same computer sharing a single <tt>disorder-playrtp</tt> process.
+ This shouldn't be too much of a problem in practice but something
+ could perhaps be done given demand.</li>
+
+ <li>Try to do remote user management when the server is
+ configured to refuse this produces rather horrible error
+ behavior.</li>
+
+ <li>Resizing columns doesn't work very well. This is a GTK+
+ bug.</li>
+
+ </ul>
+
<h2><a name=copyright>Copyright Notice</a></h2>
<p>Copyright © 2003-2009 <a
/*
* This file is part of DisOrder.
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
},
{
(char *)"/Edit/Select all tracks", /* path */
- 0, /* accelerator */
+ (char *)"<CTRL>A", /* accelerator */
menu_tab_action, /* callback */
offsetof(struct tabtype, selectall_activate), /* callback_action */
0, /* item_type */
},
{
(char *)"/Edit/Deselect all tracks", /* path */
- 0, /* accelerator */
+ (char *)"<CTRL><SHIFT>A", /* accelerator */
menu_tab_action, /* callback */
offsetof(struct tabtype, selectnone_activate), /* callback_action */
0, /* item_type */
const char *err,
const char *value);
static void playlist_editor_share_set(void *v, const char *err);
+static void playlist_picker_update_section(const char *title, const char *key,
+ int start, int end);
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+ const char *title, const char *key,
+ GtkTreeIter iter[1],
+ gboolean create);
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+ char **exists,
+ int nexists);
+static gboolean playlist_picker_button(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer user_data);
/** @brief Playlist editing window */
static GtkWidget *playlist_window;
static void playlist_menu_playing(void attribute((unused)) *v,
const char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
}
/** @brief Play received playlist contents
const char *err,
int nvec, char **vec) {
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
return;
}
for(int n = 0; n < nvec; ++n)
static void playlist_new_locked(void *v, const char *err) {
char *fullname = v;
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
return;
}
disorder_eclient_playlist_get(client, playlist_new_retrieved,
/* A rare case but not in principle impossible */
err = "A playlist with that name already exists.";
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname);
return;
}
/** @brief Called when the new playlist has been created */
static void playlist_new_created(void attribute((unused)) *v, const char *err) {
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
return;
}
disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL);
/** @brief Called when the newly created playlist has unlocked */
static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
/* Pop down the creation window */
gtk_widget_destroy(playlist_new_window);
}
/** @brief Delete button */
static GtkWidget *playlist_picker_delete_button;
-/** @brief Tree model for list of playlists */
-static GtkListStore *playlist_picker_list;
+/** @brief Tree model for list of playlists
+ *
+ * This has two columns:
+ * - column 0 will be the display name
+ * - column 1 will be the sort key/playlist name (and will not be displayed)
+ */
+static GtkTreeStore *playlist_picker_list;
/** @brief Selection for list of playlists */
static GtkTreeSelection *playlist_picker_selection;
static void playlist_picker_fill(const char attribute((unused)) *event,
void attribute((unused)) *eventdata,
void attribute((unused)) *callbackdata) {
- GtkTreeIter iter[1];
-
if(!playlist_window)
return;
if(!playlist_picker_list)
- playlist_picker_list = gtk_list_store_new(1, G_TYPE_STRING);
- const char *was_selected = playlist_picker_selected;
- gtk_list_store_clear(playlist_picker_list); /* clears playlists_selected */
- for(int n = 0; n < nplaylists; ++n) {
- gtk_list_store_insert_with_values(playlist_picker_list, iter,
- n /*position*/,
- 0, playlists[n], /* column 0 */
- -1); /* no more cols */
- /* Reselect the selected playlist */
- if(was_selected && !strcmp(was_selected, playlists[n]))
- gtk_tree_selection_select_iter(playlist_picker_selection, iter);
+ playlist_picker_list = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ /* We will accumulate a list of all the sections that exist */
+ char **sections = xcalloc(nplaylists, sizeof (char *));
+ int nsections = 0;
+ /* Make sure shared playlists are there */
+ int start = 0, end;
+ for(end = start; end < nplaylists && !strchr(playlists[end], '.'); ++end)
+ ;
+ if(start != end) {
+ playlist_picker_update_section("Shared playlists", "",
+ start, end);
+ sections[nsections++] = (char *)"";
+ }
+ /* Make sure owned playlists are there */
+ while((start = end) < nplaylists) {
+ const int nl = strchr(playlists[start], '.') - playlists[start];
+ char *name = xstrndup(playlists[start], nl);
+ for(end = start;
+ end < nplaylists
+ && playlists[end][nl] == '.'
+ && !strncmp(playlists[start], playlists[end], nl);
+ ++end)
+ ;
+ playlist_picker_update_section(name, name, start, end);
+ sections[nsections++] = name;
+ }
+ /* Delete obsolete sections */
+ playlist_picker_delete_obsolete(NULL, sections, nsections);
+}
+
+/** @brief Update a section in the picker tree model
+ * @param section Section name
+ * @param start First entry in @ref playlists
+ * @param end Past last entry in @ref playlists
+ */
+static void playlist_picker_update_section(const char *title, const char *key,
+ int start, int end) {
+ /* Find the section, creating it if necessary */
+ GtkTreeIter section_iter[1];
+ playlist_picker_find(NULL, title, key, section_iter, TRUE);
+ /* Add missing rows */
+ for(int n = start; n < end; ++n) {
+ GtkTreeIter child[1];
+ char *name;
+ if((name = strchr(playlists[n], '.')))
+ ++name;
+ else
+ name = playlists[n];
+ playlist_picker_find(section_iter,
+ name, playlists[n],
+ child,
+ TRUE);
+ }
+ /* Delete anything that shouldn't exist. */
+ playlist_picker_delete_obsolete(section_iter, playlists + start, end - start);
+}
+
+/** @brief Find and maybe create a row in the picker tree model
+ * @param parent Parent iterator (or NULL for top level)
+ * @param title Display name of section
+ * @param key Key to search for
+ * @param iter Iterator to point at key
+ * @param create If TRUE, key will be created if it doesn't exist
+ * @param compare Row comparison function
+ * @return TRUE if key exists else FALSE
+ *
+ * If the @p key exists then @p iter will point to it and TRUE will be
+ * returned.
+ *
+ * If the @p key does not exist and @p create is TRUE then it will be created.
+ * @p iter wil point to it and TRUE will be returned.
+ *
+ * If the @p key does not exist and @p create is FALSE then FALSE will be
+ * returned.
+ */
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+ const char *title,
+ const char *key,
+ GtkTreeIter iter[1],
+ gboolean create) {
+ gchar *candidate;
+ GtkTreeIter next[1];
+ gboolean it;
+ int row = 0;
+
+ it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+ next,
+ parent);
+ while(it) {
+ /* Find the value at row 'next' */
+ gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+ next,
+ 1, &candidate,
+ -1);
+ /* See how it compares with @p key */
+ int c = strcmp(key, candidate);
+ g_free(candidate);
+ if(!c) {
+ *iter = *next;
+ return TRUE; /* we found our key */
+ }
+ if(c < 0) {
+ /* @p key belongs before row 'next' */
+ if(create) {
+ gtk_tree_store_insert_with_values(playlist_picker_list,
+ iter,
+ parent,
+ row, /* insert here */
+ 0, title, 1, key, -1);
+ return TRUE;
+ } else
+ return FALSE;
+ ++row;
+ }
+ it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list), next);
+ }
+ /* We have reached the end and not found a row that should be later than @p
+ * key. */
+ if(create) {
+ gtk_tree_store_insert_with_values(playlist_picker_list,
+ iter,
+ parent,
+ INT_MAX, /* insert at end */
+ 0, title, 1, key, -1);
+ return TRUE;
+ } else
+ return FALSE;
+}
+
+/** @brief Delete obsolete rows
+ * @param parent Parent or NULL
+ * @param exists List of rows that should exist (by key)
+ * @param nexists Length of @p exists
+ */
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+ char **exists,
+ int nexists) {
+ /* Delete anything that shouldn't exist. */
+ GtkTreeIter iter[1];
+ gboolean it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+ iter,
+ parent);
+ while(it) {
+ /* Find the value at row 'next' */
+ gchar *candidate;
+ gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+ iter,
+ 1, &candidate,
+ -1);
+ gboolean found = FALSE;
+ for(int n = 0; n < nexists; ++n)
+ if((found = !strcmp(candidate, exists[n])))
+ break;
+ if(!found)
+ it = gtk_tree_store_remove(playlist_picker_list, iter);
+ else
+ it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list),
+ iter);
+ g_free(candidate);
}
- /* TODO deselecting then reselecting the current playlist resets the playlist
- * editor, which trashes the user's selection. */
}
/** @brief Called when the selection might have changed */
char *gselected, *selected;
/* Identify the current selection */
- if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)) {
+ if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)
+ && gtk_tree_store_iter_depth(playlist_picker_list, &iter) > 0) {
gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), &iter,
- 0, &gselected, -1);
+ 1, &gselected, -1);
selected = xstrdup(gselected);
g_free(gselected);
} else
selected = 0;
/* Set button sensitivity according to the new state */
- if(selected)
- gtk_widget_set_sensitive(playlist_picker_delete_button, 1);
- else
- gtk_widget_set_sensitive(playlist_picker_delete_button, 0);
- /* TODO delete should not be sensitive for public playlists owned by other
- * users */
+ int deletable = FALSE;
+ if(selected) {
+ if(strchr(selected, '.')) {
+ if(!strncmp(selected, config->username, strlen(config->username)))
+ deletable = TRUE;
+ } else
+ deletable = TRUE;
+ }
+ gtk_widget_set_sensitive(playlist_picker_delete_button, deletable);
/* Eliminate no-change cases */
if(!selected && !playlist_picker_selected)
return;
static void playlists_picker_delete_completed(void attribute((unused)) *v,
const char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
}
/** @brief Called when the 'Delete' button is pressed */
g_signal_connect(tree, "key-press-event",
G_CALLBACK(playlist_picker_keypress), 0);
+ g_signal_connect(tree, "button-press-event",
+ G_CALLBACK(playlist_picker_button), 0);
return vbox;
}
}
}
+static void playlist_picker_select_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* nothing */
+}
+
+static int playlist_picker_select_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_play_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* Re-use the menu-based activation callback */
+ disorder_eclient_playlist_get(client, playlist_menu_received_content,
+ playlist_picker_selected, NULL);
+}
+
+static int playlist_picker_play_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_remove_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* Re-use the 'Remove' button' */
+ playlist_picker_delete(NULL, NULL);
+}
+
+static int playlist_picker_remove_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ if(gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0) {
+ if(strchr(playlist_picker_selected, '.')) {
+ if(!strncmp(playlist_picker_selected, config->username,
+ strlen(config->username)))
+ return TRUE;
+ } else
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/** @brief Pop-up menu for picker */
+static struct menuitem playlist_picker_menuitems[] = {
+ {
+ "Select playlist",
+ playlist_picker_select_activate,
+ playlist_picker_select_sensitive,
+ 0,
+ 0
+ },
+ {
+ "Play playlist",
+ playlist_picker_play_activate,
+ playlist_picker_play_sensitive,
+ 0,
+ 0
+ },
+ {
+ "Remove playlist",
+ playlist_picker_remove_activate,
+ playlist_picker_remove_sensitive,
+ 0,
+ 0
+ },
+};
+
+static gboolean playlist_picker_button(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->type == GDK_BUTTON_PRESS && event->button == 3) {
+ static GtkWidget *playlist_picker_menu;
+
+ /* Right click press pops up a menu */
+ ensure_selected(GTK_TREE_VIEW(widget), event);
+ /* Find the selected row */
+ GtkTreeIter iter[1];
+ if(!gtk_tree_selection_get_selected(playlist_picker_selection, 0, iter))
+ return TRUE;
+ popup(&playlist_picker_menu, event,
+ playlist_picker_menuitems,
+ sizeof playlist_picker_menuitems / sizeof *playlist_picker_menuitems,
+ iter);
+ return TRUE;
+ }
+ return FALSE;
+}
+
static void playlist_picker_destroy(void) {
playlist_picker_delete_button = NULL;
g_object_unref(playlist_picker_list);
static void playlist_editor_share_set(void attribute((unused)) *v,
const attribute((unused)) char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
}
/** @brief Set the editor button state and sensitivity */
const char *value) {
const char *playlist = v;
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
value = NULL;
}
/* Set the currently active button */
int nvec, char **vec) {
const char *playlist = v;
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
return;
}
if(!playlist_picker_selected
static void playlist_modify_locked(void *v, const char *err) {
struct playlist_modify_data *mod = v;
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
return;
}
disorder_eclient_playlist_get(client, playlist_modify_retrieved,
char **vec) {
struct playlist_modify_data *mod = v;
if(err) {
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
return;
}
static void playlist_modify_updated(void attribute((unused)) *v,
const char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
}
static void playlist_modify_unlocked(void attribute((unused)) *v,
const char *err) {
if(err)
- popup_protocol_error(0, err);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
}
/* Drop tracks into a playlist ---------------------------------------------- */
G_CALLBACK(gtk_widget_destroyed), menup);
for(int n = 0; n < nitems; ++n) {
items[n].w = gtk_menu_item_new_with_label(items[n].name);
+ /* TODO accelerators would be useful here. There might be some
+ * interaction with the main menu accelerators, _except_ for playlist
+ * case! */
gtk_menu_attach(GTK_MENU(menu), items[n].w, 0, 1, n, n + 1);
}
set_tool_colors(menu);
ql->ncolumns + QUEUEPOINTER_COLUMN, nqd->new,
-1);
it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+ /* We'll need the new start time */
+ nqd->new->when = q->when;
++kept;
} else {
/* Delete this row (and move iter to the next one) */
/* If there's a track playing, update its row */
if(playing_track)
ql_update_row(playing_track, 0);
+ /* If the first (nonplaying) track starts in the past, update the queue to
+ * get new expected start times; but rate limit this checking. (If we only
+ * do it once a minute then the rest of the queue can get out of date too
+ * easily.) */
+ struct queue_entry *q = ql_queue.q;
+ if(q) {
+ if(q == playing_track)
+ q = q->next;
+ if(q) {
+ time_t now;
+ time(&now);
+ if(q->expected / 15 < now / 15)
+ queue_changed(0,0,0);
+ }
+ }
return TRUE;
}
.\"
-.\" Copyright (C) 2004-2008 Richard Kettlewell
+.\" Copyright (C) 2004-2009 Richard Kettlewell
.\"
.\" This program is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.SH DESCRIPTION
.B disobedience
is a graphical client for DisOrder.
-.SH "WINDOWS AND ICONS"
-.SS "Server Menu"
-This has the following options:
-.TP
-.B Login
-Brings up the \fBLogin Details Window\fR; see below.
-.TP
-.B "Manage Users"
-Brings up the \fBUser Management Window\fR; see below.
-.TP
-.B Quit
-Terminates the program.
-.SS "Edit Menu"
-This has the following options:
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Edit Playlists"
-Edit playlists.
-See
-.B "Playlists Window"
-below.
-.SS "Control Menu"
-This has the following options:
-.TP
-.B Scratch
-Interrupts the currently playing track.
-.TP
-.B Playing
-Pause and resume the current track.
-.TP
-.B "Random play"
-Enable and disable random play.
-Does not take effect until the currently playing track finishes.
-.TP
-.B "Network player"
-Enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-.TP
-.B "Activate Playlist"
-This submenu has the selection of playlists which you are able to play.
-Picking one will add its contents to the queue.
-.SS "Help Menu"
-This has only one option, "About DisOrder", which pops up a box giving the
-name, author and version number of the software.
-.SS "Controls"
-.TP
-.B "Pause button"
-The pause button can be used to pause and resume tracks.
-This button shows either a pause symbol (two vertical bars) or a resume symbol
-(a right-pointing arrow).
-.TP
-.B "Scratch button"
-The scratch button, a red cross, can be used to interrupt the currently playing
-track.
-.TP
-.B "Random play button"
-The random play button can be used to enable and disable random play.
-It does not take effect until the currently playing track finishes.
-When the button is green, random play is enabled.
-When it is grey, random play is disabled.
-.TP
-.B "Play button"
-The play button controls whether tracks will be played at all.
-As above it does not take effect until the currently playing track finishes.
-When the button is green, play is enabled.
-When it is grey, play is disabled.
-.TP
-.B "Network play button"
-The network play buttons enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-When the button is green, network play is enabled.
-When it is grey, network play is disabled.
-.TP
-.B "Volume slider"
-The volume slider indicates the current volume level and can be used to adjust
-it.
-0 is silent and 10 is maximum volume.
-.TP
-.B "Balance slider"
-The balance slider indicates the current balance and can be used to adjust it.
-\-1 means only the left speaker, 0 means both speakers at equal volume and +1
-means the only the right speaker.
-.SS "Queue Tab"
-This displays the currently playing track and the queue.
-The currently playing track is at the top, and can be distinguished by
-the constantly updating timer.
-Queued tracks appear below it.
-.PP
-The left button can be use to select and deselect tracks.
-On its own it just selects the pointed track and deselects everything else.
-With CTRL it flips the state of the pointed track without affecting anything
-else.
-With SHIFT it selects every track from the last click to the current position
-and deselects everything else.
-With both CTRL and SHIFT it selects everything from the last click to the
-current position without deselecting anything.
-.PP
-Tracks can be moved within the queue by dragging them to a new position with
-the left button.
-.PP
-The right button pops up a menu.
-This has the following options:
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.TP
-.B Scratch
-Interrupt the currently playing track.
-(Note that this appears even if you right click over a queued track rather
-than the currently playing track.)
-.TP
-.B "Remove track from queue"
-Remove the selected tracks from the queue.
-.TP
-.B "Adopt track"
-Sets the submitter of a randomly picked track to you.
-.SS "Recent Tab"
-This displays recently played tracks, the most recent at the top.
-.PP
-The left button functions as above, except that drag-and-drop rearrangement
-is not possible.
-The right button pops up a menu with the following options:
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Play track"
-Play the select track(s);
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.SS "Choose Tab"
-This displays all the tracks known to the server in a tree structure.
-.PP
-Directories are represented with an arrow to their left.
-This can be clicked to reveal or hide the contents of the directory.
-The top level "directories" break up tracks by their first letter.
-.PP
-Playable files are represented by their name.
-If they are playing or in the queue then a notes icon appears next to them.
.PP
-Left clicking on a file will select it.
-As with the queue tab you can use SHIFT and CTRL to select multiple files.
-.PP
-Files may be played by dragging them to the queue tab and thence to a
-destination position in the queue.
-.PP
-The text box at the bottom is a search form.
-If you enter search terms here then tracks containing all those words will be
-highlighted.
-You can also limit the results to tracks with particular tags, by including
-\fBtag:\fITAG\fR for each tag.
-.PP
-To start a new search just edit the contents of the search box.
-The cancel button to its right clears the current search.
-The up and down arrows will scroll the window to make the previous or next
-search result visible.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B Play
-Play selected tracks.
-.TP
-.B Properties
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.PP
-A middle click on a track will add it to the queue.
-.PP
-Right clicking over a directory will pop up a menu with the following options:
-.TP
-.B "Play all tracks"
-Play all the tracks in the directory, in the order they appear on screen.
-.TP
-.B "Track properties"
-Edit properties of all tracks in the directory.
-.TP
-.B "Select children"
-Select all the tracks in the directory (and deselect everything else).
-.TP
-.B "Deselect all tracks"
-Deselect everything.
-.SS "Added Tab"
-This displays a list of tracks recently added to the server's database.
-The most recently added track is at the top.
-.PP
-Left clicking a track will select it.
-CTRL and SHIFT work as above to select muliple files.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B "Track properties"
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Play track"
-Play selected tracks.
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.SS "Login Details Window"
-The login details window allows you to edit the connection details and
-authorization information used by Disobedience.
-.PP
-At the top is a 'remote' switch.
-If this is enabled then you can use the \fBHostname\fR and \fBService\fR
-fields to connect to a remote server.
-If it is disabled then then Disobedience will connect to a local server
-instead.
-.PP
-Below this are four text entry fields:
-.TP
-.B Hostname
-The host to connect to.
-.TP
-.B Service
-The service name or port number to connect to.
-.TP
-.B "User name"
-The user name to log in as.
-.TP
-.B Password
-The password to use when logging in.
-Note that this is NOT your login password but is your password to the
-DisOrder server.
-.PP
-It has two buttons:
-.TP
-.B Login
-This button attempts to (re-)connect to the server with the currently displayed
-settings.
-The settings are saved in
-.IR $HOME/.disorder/passwd .
-on success.
-.TP
-.B Close
-This button closes the window, discarding any unsaved changes.
-.SS "Properties Window"
-This window contains details of one or more tracks and allows them to be
-edited.
-.PP
-The Artist, Album and Title fields determine how the tracks appear in
-the queue and recently played tabs.
-.PP
-The Tags field determine which tags apply to the track.
-Tags are separated by commas and can contain any printing characters except
-comma.
-.PP
-The Weight field determines the track weight. Tracks with higher weights are
-proportionately more likely to be picked at random. The default weight is
-90000, and the maximum weight is 2147483647.
-.PP
-The Random checkbox determines whether the track will be picked at random.
-Random play is enabled for every track by default, but it can be turned off
-here.
-.PP
-The double-headed arrow to the right of each preference will propagate its
-value to all the other tracks in the window.
-For instance, this can be used to efficiently correct the artist or album
-fields, or bulk-disable random play for many tracks.
-.PP
-Press "OK" to confirm all changes and close the window, "Apply" to confirm
-changes but keep the window open and "Cancel" to close the window and discard
-all changes.
-.SS "User Management Window"
-This window is primarily of interest to adminstrators, and will not be
-available to users without admin rights. The left hand side is a list of all
-users; the right hand side contains the editable details of the currently
-selected user.
-.PP
-When you select any user you can edit their email address or change their
-password. It is also possible to edit the individual user rights. Click on
-the "Apply" button to commit any changes you make.
-.PP
-The "Add" button creates a new user. You must enter at least a username.
-Default rights are setting according to local configuration, \fInot\fR server
-configuration (but this may be changed in the future). As above, click on
-"Apply" to actually create the new user.
-.PP
-The "Delete" button deletes the selected user. This operation cannot be
-undone.
-.SS "Playlists Window"
-A playlist is a collection of tracks that can be prepared in advance and then
-played as a unit.
-Playlists come in three kinds:
-.TP
-.B shared
-A playlist that anyone can edit.
-These are not owned by any user.
-.TP
-.B public
-A playlist that only one user (its owner) can edit,
-but that anyone can see the contents of.
-.TP
-.B private
-A playlist that is entirely private to one user.
-.PP
-Public and private playlists start with the owner's username.
-.PP
-The left side of the playlist window is a list of all playlists that you can
-see: all shared and public playlists plus your private playlists.
-Selecting one will bring it up in the right hand side of the window,
-allowing it to be reviewed or edited.
-You can drag tracks from the choose tab in the main window into it and
-rearrange tracks already in the playlist by selecting and dragging them.
-.PP
-The add button creates a new playlist.
-You must selected whether it will be a shared, public or private playlist.
-.PP
-At the bottom of the window are radio buttons showing which kind of playlist it
-is.
-If you own the playlist you can use these to switch it between public and
-private, but if it is shared or owned by someone else these buttons will be
-greyed out.
-.SH "KEYBOARD SHORTCUTS"
-.TP
-.B CTRL+A
-Select all tracks (queue/recent)
-.TP
-.B CTRL+L
-Brings up the \fBLogin Details Window\fR.
-.TP
-.B CTRL+Q
-Quit.
-.SH "NETWORK PLAY"
-Network play uses a background
-.BR disorder\-playrtp (1)
-process.
-If you quit Disobedience the player will continue playing and can be
-disabled from a later run of Disobedience.
-.PP
-The player will log to
-.I ~/.disorder/HOSTNAME\-rtp.log
-so look there if it does not seem to be working.
-.PP
-You can stop it without running Disobedience by the command
-.BR "killall disorder\-playrtp" .
+Please refer to Disobedience's HTML manual for further information. This can
+be found at dochtmldir/index.html.
.SH OPTIONS
.TP
.B \-\-config \fIPATH\fR, \fB\-c \fIPATH
.\" .TP
.\" .B \-\-sync
.\" Make all X requests synchronously.
-.SH CONFIGURATION
-If you are using
-.B disobedience
-on the same host as the server then no additional configuration should be
-required.
-.PP
-If it is running on a different host then the easiest way to set it up is to
-use the login details window in Disobedience.
-Enter the connection details, use Login to connect to the server, and then
-use Save to store them for future sessions.
-.PP
-The other clients read their configuration from the same location so after
-setting up with Disobedience, tools such as
-.BR disorder (1)
-should work as well.
-.SH BUGS
-There is no particular provision for multiple users of the same computer
-sharing a single \fBdisorder\-playrtp\fR process.
-This shouldn't be too much of a problem in practice but something could
-perhaps be done given demand.
-.PP
-Try to do remote user management when the server is configured to refuse this
-produces rather horrible error behavior.
-.PP
-Only one track can be dragged at a time.
-.PP
-Resizing columns doesn't work very well.
-This is a GTK+ bug.
-.SH FILES
-.TP
-.I ~/.disorder/HOSTNAME\-rtp
-Socket for communication with RTP player.
-.TP
-.I ~/.disorder/HOSTNAME\-rtp.log
-Log file for RTP player.
.SH "SEE ALSO"
.BR disorder\-playrtp (1),
.BR disorder_config (5)
if(rs->state) {
/* A sample-rate conversion must be performed */
SRC_DATA data;
+ memset(&data, 0, sizeof data);
/* Compute how many frames are expected to come out. */
size_t maxframesout = nframesin * rs->output_rate / rs->input_rate + 1;
output = xcalloc(maxframesout * rs->output_channels, sizeof(float));
data.output_frames = maxframesout;
data.end_of_input = eof;
data.src_ratio = (double)rs->output_rate / rs->input_rate;
+ D(("nframesin=%zu maxframesout=%zu eof=%d ratio=%d.%06d",
+ nframesin, maxframesout, eof,
+ (int)data.src_ratio,
+ ((int)(data.src_ratio * 1000000) % 1000000)));
int error_ = src_process(rs->state, &data);
if(error_)
disorder_fatal(0, "calling src_process: %s", src_strerror(error_));
nframesin = data.input_frames_used;
nsamplesout = data.output_frames_gen * rs->output_channels;
+ D(("new nframesin=%zu nsamplesout=%zu", nframesin, nsamplesout));
}
#endif
if(!output) {
-e 's!pkgconfdir!${sysconfdir}/disorder!g;' \
-e 's!pkgstatedir!${localstatedir}/disorder!g;' \
-e 's!pkgdatadir!${pkgdatadir}!g;' \
+ -e 's!dochtmldir!${dochtmldir}!g;' \
-e 's!SENDMAIL!${SENDMAIL}!g;' \
-e 's!_version_!${VERSION}!g;' \
< $< > $@.new
}
if(!n)
break;
+ D(("NEW HEADER: %"PRIu32" bytes %"PRIu32"Hz %"PRIu8" channels %"PRIu8" bits %"PRIu8" endian",
+ header.nbytes, header.rate, header.channels, header.bits, header.endian));
/* Sanity check the header */
if(header.rate < 100 || header.rate > 1000000)
disorder_fatal(0, "implausible rate %"PRId32"Hz (%#"PRIx32")",
else {
/* If we have a resampler active already check it is suitable and destroy
* it if not */
- if(!formats_equal(&header, &latest_format) && rs_in_use) {
+ if(rs_in_use) {
+ D(("call resample_close"));
resample_close(rs);
rs_in_use = 0;
}
config->sample_format.endian);*/
if(!rs_in_use) {
/* Create a suitable resampler. */
+ D(("call resample_init"));
resample_init(rs,
header.bits,
header.channels,
left -= r;
used += r;
//syslog(LOG_INFO, "read %zd bytes", r);
+ D(("read %zd bytes", r));
}
/*syslog(LOG_INFO, " in: %02x %02x %02x %02x",
(uint8_t)buffer[0],
(uint8_t)buffer[1],
(uint8_t)buffer[2],
(uint8_t)buffer[3]);*/
+ D(("calling resample_convert used=%zu !left=%d", used, !left));
const size_t consumed = resample_convert(rs,
(uint8_t *)buffer, used,
!left,
converted, 0);
//syslog(LOG_INFO, "used=%zu consumed=%zu", used, consumed);
+ D(("consumed=%zu", consumed));
+ assert(consumed != 0);
memmove(buffer, buffer + consumed, used - consumed);
used -= consumed;
}