<p>This file documents recent user-visible changes to <a
href="http://www.greenend.org.uk/rjk/disorder/">DisOrder</a>.</p>
-<h2>Changes up to version 4.4</h2>
+<h2>Changes up to version 5.0</h2>
<div class=section>
<p>Gapless play should be more reliable, and playback latency over RTP
should be a bit lower. Note though that all the sound output code has
been reorganized and in some cases completely rewritten, so it's possible
- that bugs may have been (re-)introduced.</p>
+ that bugs may have been (re-)introduced. Decoding of scratches is also
+ initiated ahead of time, giving more reliable playback.</p>
<p>The <tt>command</tt> backend now (optionally) sends silence instead
of suspending writes when a pause occurs or no track is playing.</p>
<th>ID</th>
<th>Description</th>
</tr>
-
+
+ <tr>
+ <td><a href="http://code.google.com/p/disorder/issues/detail?id=22">#22</a></td>
+ <td>Background decoders interact badly with server reload</td>
+ </tr>
+
<tr>
<td><a href="http://code.google.com/p/disorder/issues/detail?id=27">#27</a></td>
<td>Mac DisOrder uses wrong sound device</td>
<td>disobedience doesn't configure its back end</td>
</tr>
+ <tr>
+ <td><a href="http://code.google.com/p/disorder/issues/detail?id=46">#46</a></d>
+ <td>Sort search results in web interface</td>
+ </tr>
+
<tr>
<td><a href="http://code.google.com/p/disorder/issues/detail?id=48">#48</a></d>
<td>build-time dependency on <tt>oggdec</tt> removed</td>
<td>Disobedience's 'When' column gets out of date</td>
</tr>
+ <tr>
+ <td>(none)</td>
+ <td>“found track in no collection” messages for scratches
+ are now suppressed</td>
+ </tr>
+
</table>
</div>
</div>
/* Get the list */
if(fn(dcgi_client, dir, re, &tracks, &ntracks))
return 0;
- /* Sort it. NB trackname_transform() does not go to the server. */
- tsd = tracksort_init(ntracks, tracks, type);
- /* Expand the subsiduary templates. We chuck in @sort and @display because
- * it is particularly easy to do so. */
- for(n = 0; n < ntracks; ++n)
- if((rc = mx_expand(mx_rewritel(m,
- "index", make_index(n),
- "parity", n % 2 ? "odd" : "even",
- "track", tsd[n].track,
- "first", n == 0 ? "true" : "false",
- "last", n + 1 == ntracks ? "false" : "true",
- "sort", tsd[n].sort,
- "display", tsd[n].display,
- (char *)0),
- output, u)))
- return rc;
+ if(type) {
+ /* Sort it. NB trackname_transform() does not go to the server. */
+ tsd = tracksort_init(ntracks, tracks, type);
+ /* Expand the subsiduary templates. We chuck in @sort and @display because
+ * it is particularly easy to do so. */
+ for(n = 0; n < ntracks; ++n)
+ if((rc = mx_expand(mx_rewritel(m,
+ "index", make_index(n),
+ "parity", n % 2 ? "odd" : "even",
+ "track", tsd[n].track,
+ "first", n == 0 ? "true" : "false",
+ "last", n + 1 == ntracks ? "false" : "true",
+ "sort", tsd[n].sort,
+ "display", tsd[n].display,
+ (char *)0),
+ output, u)))
+ return rc;
+ } else {
+ for(n = 0; n < ntracks; ++n)
+ if((rc = mx_expand(mx_rewritel(m,
+ "index", make_index(n),
+ "parity", n % 2 ? "odd" : "even",
+ "track", tracks[n],
+ "first", n == 0 ? "true" : "false",
+ "last", n + 1 == ntracks ? "false" : "true",
+ (char *)0),
+ output, u)))
+ return rc;
+ }
return 0;
-
}
/*$ @tracks{DIR}{RE}{TEMPLATE}
* - @parity: "even" or "odd" alternately
* - @first: "true" on the first directory and "false" otherwise
* - @last: "true" on the last directory and "false" otherwise
- * - @sort: the sort key for this track
- * - @display: the UNQUOTED display string for this track
*/
static int exp_search(int nargs,
const struct mx_node **args,
struct sink *output,
void *u) {
- return exp__files_dirs(nargs, args, output, u, "track", exp__search_shim);
+ return exp__files_dirs(nargs, args, output, u, NULL, exp__search_shim);
}
/*$ @label{NAME}
const struct collection *c = find_track_collection(track);
if(c)
return c->root;
+ /* Suppress this message for scratches */
+ for(int n = 0; n < config->scratch.n; ++n)
+ if(!strcmp(track, config->scratch.s[n]))
+ return 0;
disorder_error(0, "found track in no collection '%s'", track);
return 0;
}
/* Comparison function for path names that groups all entries in a directory
* together */
-/* Convenient wrapper for compare_path_raw */
+/** @brief Compare two paths
+ * @param ap First path
+ * @param bp Second path
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ * A wrapper around compare_path_raw().
+ */
static inline int compare_path(const char *ap, const char *bp) {
return compare_path_raw((const unsigned char *)ap, strlen(ap),
(const unsigned char *)bp, strlen(bp));
#include "log.h"
#include "unicode.h"
+/** @brief Compare two tracks
+ * @param sa First sort key
+ * @param sb Second sort key
+ * @param da First display string
+ * @param db Second display string
+ * @param ta First raw track
+ * @param tb Second raw track
+ * @return -ve, 0 or +ve for a <, = or > b
+ *
+ * Tries the following comparisons until a difference is found:
+ * - case-independent comparison of sort keys
+ * - case-dependent comparison of sort keys
+ * - case-independent comparison of display strings
+ * - case-dependent comparison of display strings
+ * - case-dependent comparison of paths (see compare_path())
+ */
int compare_tracks(const char *sa, const char *sb,
const char *da, const char *db,
const char *ta, const char *tb) {
return compare_path(ta, tb);
}
+/** @brief Compare two paths
+ * @param ap First path
+ * @param an Length of @p ap
+ * @param bp Second path
+ * @param bn Length @p bp
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ *
+ * See also compare_path().
+ */
int compare_path_raw(const unsigned char *ap, size_t an,
const unsigned char *bp, size_t bn) {
/* Don't change this function! The database sort order depends on it */
ea->track, eb->track);
}
+/** @brief Sort tracks
+ * @param ntracks Number of tracks to sort
+ * @param tracks List of tracks
+ * @param type Comparison type
+ * @return Sorted track data
+ *
+ * Tracks are compared using compare_tracks(), with the sort key and display
+ * string set according to @p type, which should be "track" if the tracks are
+ * really tracks and "dir" if they are directories.
+ */
struct tracksort_data *tracksort_init(int ntracks,
char **tracks,
const char *type) {
#define WHERE_END 1 /* Add to end of queue */
#define WHERE_BEFORE_RANDOM 2 /* End, or before random track */
#define WHERE_AFTER 3 /* After the target */
+#define WHERE_NOWHERE 4 /* Don't add to queue at all */
/* add an entry to the queue. Return a pointer to the new entry. */
void queue_remove(struct queue_entry *q, const char *who);
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;
}
static int prepare_child(struct queue_entry *q,
const struct pbgc_params *params,
void attribute((unused)) *bgdata);
+static void ensure_next_scratch(ev_source *ev);
/** @brief File descriptor of our end of the socket to the speaker */
static int speaker_fd = -1;
* some time before the speaker reports it as finished) or when a non-raw
* (i.e. non-speaker) player terminates. In the latter case it's imaginable
* that the OS has buffered the last few samples.
- *
+ *
+ * NB. The finished track might NOT be in the queue (yet) - it might be a
+ * pre-chosen scratch.
*/
static int player_finished(ev_source *ev,
pid_t pid,
* potentially be a just-added random track. */
if(qhead.next != &qhead)
prepare(ev, qhead.next);
+ /* Make sure there is a prepared scratch */
+ ensure_next_scratch(ev);
break;
}
}
/* Scratching --------------------------------------------------------------- */
+/** @brief Track to play next time something is scratched */
+static struct queue_entry *next_scratch;
+
+/** @brief Ensure there isa prepared scratch */
+static void ensure_next_scratch(ev_source *ev) {
+ if(next_scratch) /* There's one already */
+ return;
+ if(!config->scratch.n) /* There are no scratches */
+ return;
+ int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+ next_scratch = queue_add(config->scratch.s[r], NULL,
+ WHERE_NOWHERE, NULL, origin_scratch);
+ if(ev)
+ prepare(ev, next_scratch);
+}
+
/** @brief Scratch a track
* @param who User responsible (or NULL)
* @param id Track ID (or NULL for current)
*/
void scratch(const char *who, const char *id) {
- struct queue_entry *q;
struct speaker_message sm;
D(("scratch playing=%p state=%d id=%s playing->id=%s",
speaker_send(speaker_fd, &sm);
D(("sending SM_CANCEL for %s", playing->id));
}
- /* put a scratch track onto the front of the queue (but don't
- * bother if playing is disabled) */
- if(playing_is_enabled() && config->scratch.n) {
- int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
- q = queue_add(config->scratch.s[r], who, WHERE_START, NULL,
- origin_scratch);
+ /* Try to make sure there is a scratch */
+ ensure_next_scratch(NULL);
+ /* Insert it at the head of the queue */
+ if(next_scratch){
+ next_scratch->submitter = who;
+ queue_insert_entry(&qhead, next_scratch);
+ eventlog_raw("queue", queue_marshall(next_scratch), (const char *)0);
+ next_scratch = NULL;
}
notify_scratch(playing->track, playing->submitter, who,
xtime(0) - playing->played);
}
queue_insert_entry(afterme, q);
break;
+ case WHERE_NOWHERE:
+ return q;
}
/* submitter will be a null pointer for a scratch */
if(submitter)