shithub: sox

Download patch

ref: 86791c4beb0235121173d9ced1b166d03eeda31b
parent: fbbc16d6de5a374253ea07b751c2e0d1c09befd5
parent: a586ad952b11c3717d419ce9817391d21e10e045
author: Ulrich Klauer <ulrich@chirlu.de>
date: Thu Jan 31 21:23:42 EST 2013

Merge branch 'positions'

Extended syntax for specifying audio positions to several effects
(bend, delay, fade, pad, spectrogram, splice; trim had it before, but
now uses the shared lsx_parseposition function).

--- a/ChangeLog
+++ b/ChangeLog
@@ -35,6 +35,8 @@
   o Support multi-channel LADSPA plugins. (Eric Wong)
   o Support infinite repetition with repeat. (Ulrich Klauer)
   o Improved pink noise frequency response in synth. (robs)
+  o Extended syntax for specifying audio positions to several
+    effects. (Ulrich Klauer)
 
 Other new features:
 
--- a/sox.1
+++ b/sox.1
@@ -1460,6 +1460,23 @@
 A power gain in dB.
 Zero gives no gain; less than zero gives an attenuation.
 .TP
+\fIposition\fR
+A position within the audio stream; the syntax is
+[\fB=\fR\^|\^\fB+\fR\^|\^\fB\-\fR]\fItimespec\fR, where \fItimespec\fR is a
+time specification (see below).  The optional first character indicates
+whether the \fItimespec\fR is to be interpreted relative to the start
+(\fB=\fR) or end (\fB\-\fR) of audio, or to the previous \fIposition\fR if
+the effect accepts multiple position arguments (\fB+\fR).  The audio length
+must be known for end-relative locations to work; some effects do accept
+\fB\-0\fR for end-of-audio, though, even if the length is unknown.  Which of
+\fB=\fR, \fB+\fR, \fB\-\fR is the default depends on the effect and is shown
+in its syntax as, e.g., \fIposition(+)\fR.
+.SP
+Examples: \fB=2:00\fR (two minutes into the audio stream), \fB\-100s\fR (one
+hundred samples before the end of audio), \fB+0:12+10s\fR (twelve seconds
+and ten samples after the previous position), \fB\-0.5+1s\fR (one sample less
+than half a second before the end of audio).
+.TP
 \fIwidth\fR[\fBh\fR\^|\^\fBk\fR\^|\^\fBo\fR\^|\^\fBq\fR]
 Used to specify the band-width of a filter.  A number of different
 methods to specify the width are available (though not all for every effect).
@@ -1499,8 +1516,8 @@
 .PP
 Time specifications can also be chained with \fB+\fR or \fB\-\fR into a new
 time specification where the right part is added to or subtracted from the
-left, respectively: `3:00\-200s' means two hundred samples before the three
-minute mark.
+left, respectively: `3:00\-200s' means two hundred samples less than three
+minutes.
 .SP
 To see if SoX has support for an optional effect, enter
 .B sox \-h
@@ -1611,16 +1628,13 @@
 .SP
 See also \fBequalizer\fR for a peaking equalisation effect.
 .TP
-\fBbend\fR [\fB\-f \fIframe-rate\fR(25)] [\fB\-o \fIover-sample\fR(16)] { \fIdelay\fB,\fIcents\fB,\fIduration\fR }
+\fBbend\fR [\fB\-f \fIframe-rate\fR(25)] [\fB\-o \fIover-sample\fR(16)] { \fIstart-position(+)\fB,\fIcents\fB,\fIend-position(+)\fR }
 Changes pitch by specified amounts at specified times.
-Each given triple: \fIdelay\fB,\fIcents\fB,\fIduration\fR specifies one bend.
-.I delay
-is the amount of time after the start of the audio stream, or the end of the previous bend, at which to start bending the pitch;
-.I cents
-is the number of cents (100 cents = 1 semitone) by which to bend the pitch, and
-.I duration
-the length of time over which the pitch will be bent.  For \fIdelay\fR and
-\fIduration\fR, any time specification may be used.
+Each given triple: \fIstart-position\fB,\fIcents\fB,\fIend-position\fR
+specifies one bend.
+\fIcents\fR is the number of cents (100 cents = 1 semitone) by which to
+bend the pitch. The other values specify the points in time at which to start
+and end bending the pitch, respectively.
 .SP
 The pitch-bending algorithm utilises the Discrete Fourier Transform (DFT)
 at a particular frame rate and over-sampling rate.
@@ -1638,6 +1652,8 @@
    play \-n synth 2.5 sin 667 gain 1 \\
 	bend .35,180,.25  .15,740,.53  0,\-520,.3
 .EE
+Here, the first bend runs from 0.35 to 0.6, and the second one from 0.75
+to 1.28 seconds.
 Note that the clipping that is produced in this example is deliberate;
 to remove it, use
 .B gain\ \-5
@@ -1921,14 +1937,15 @@
 .SP
 See also the \fBbass\fR and \fBtreble\fR shelving equalisation effects.
 .TP
-\fBdelay\fR {\fItime\fR}
-Delay one or more audio channels by the given \fItime\fR (a time
-specification).
+\fBdelay\fR {\fIposition(=)\fR}
+Delay one or more audio channels such that they start at the given
+\fIposition\fR.
 For example,
-.B delay 1\*d5 0 3000s
-delays the first channel by 1\*d5 seconds, the third channel by 3000
-samples, and leaves the second channel (and any other channels that may be
-present) un-delayed.
+.B delay 1\*d5 +1 3000s
+delays the first channel by 1\*d5 seconds, the second channel by 2\*d5
+seconds (one second more than the previous channel), the third channel
+by 3000 samples, and leaves any other channels that may be
+present un-delayed.
 The following (one long) command plays a chime sound:
 .EX
 .ne 3
@@ -2103,7 +2120,7 @@
 .SP
 See also \fBbass\fR and \fBtreble\fR for shelving equalisation effects.
 .TP
-\fBfade\fR [\fItype\fR] \fIfade-in-length\fR [\fIstop-time\fR [\fIfade-out-length\fR]]
+\fBfade\fR [\fItype\fR] \fIfade-in-length\fR [\fIstop-position(=)\fR [\fIfade-out-length\fR]]
 Apply a fade effect to the beginning, end, or both of the audio.
 .SP
 An optional \fItype\fR can be specified to select the shape of the fade
@@ -2117,21 +2134,25 @@
 no fade-in is wanted.
 .SP
 For fade-outs, the audio will be truncated at
-.I stop-time
+.I stop-position
 and the signal level will be ramped from full volume down to 0 over an
-interval of \fIfade-out-length\fR before the \fIstop-time\fR.  If
+interval of \fIfade-out-length\fR before the \fIstop-position\fR.  If
 .I fade-out-length
 is not specified, it defaults to the same value as
 \fIfade-in-length\fR.
 No fade-out is performed if
-.I stop-time
+.I stop-position
 is not specified.
 If the audio length can be determined from the input file header and any
-previous effects, then \fB0\fR may be specified for
-.I stop-time
+previous effects, then \fB\-0\fR (or, for historical reasons, \fB0\fR) may
+be specified for
+.I stop-position
 to indicate the usual case of a fade-out that ends at the end of the input
-audio stream.  Any time specification may be used for these parameters.
+audio stream.
 .SP
+Any time specification may be used for \fIfade-in-length\fR and
+\fIfade-out-length\fR.
+.SP
 See also the
 .B splice
 effect.
@@ -2507,7 +2528,7 @@
 The \fIcolour\fR parameter controls the amount of even harmonic content
 in the over-driven output.
 .TP
-\fBpad\fR { \fIlength\fR[\fB@\fIposition\fR] }
+\fBpad\fR { \fIlength\fR[\fB@\fIposition(=)\fR] }
 Pad the audio with silence, at the beginning, the end, or any
 specified points through the audio.
 .I length
@@ -3298,7 +3319,7 @@
 See also
 .B \-X
 for an alternative way of setting the X-axis resolution.
-.IP \fB\-S\ \fItime\fR
+.IP \fB\-S\ \fIposition(=)\fR
 Start the spectrogram at the given point in the audio stream.  For
 example
 .EX
@@ -3306,7 +3327,6 @@
 .EE
 creates a spectrogram showing all but the first minute of the audio
 (the output file, however, receives the entire audio stream).
-Any time specification may be used.
 .RE
 .TP
 \ 
@@ -3335,7 +3355,7 @@
 .B tempo
 effects.
 .TP
-\fBsplice \fR [\fB\-h\fR\^|\^\fB\-t\fR\^|\^\fB\-q\fR] { \fIposition\fR[\fB,\fIexcess\fR[\fB,\fIleeway\fR]] }
+\fBsplice \fR [\fB\-h\fR\^|\^\fB\-t\fR\^|\^\fB\-q\fR] { \fIposition(=)\fR[\fB,\fIexcess\fR[\fB,\fIleeway\fR]] }
 Splice together audio sections.  This effect provides two things over
 simple audio concatenation: a (usually short) cross-fade is applied at
 the join, and a wave similarity comparison is made to help determine the
@@ -3371,12 +3391,12 @@
 .IR excess
 (before the ideal joining point), plus an additional
 .I leeway
-(default 0\*d005 seconds).  SoX should then be invoked with the two
+(default 0\*d005 seconds).  Any time specification may be used for these
+parameters.  SoX should then be invoked with the two
 audio sections as input files and the
 .B splice
 effect given with the position at which to perform the splice\*mthis is
 length of the first audio section (including the excess).
-Any time specification may be used for these parameters.
 .SP
 The following diagram uses the tape analogy to illustrate the splice
 operation.  The effect simulates the diagonal cuts and joins the two pieces:
@@ -3435,17 +3455,15 @@
 #!/bin/sh
 # Audio Copy and Paste Over
 # acpo infile copy-start copy-stop paste-over-start outfile
-# All times measured in samples.
-rate=\`soxi \-r "$1"\`
-e=\`expr $rate '*' 5 / 1000\`  # Using default excess
+# No chained time specifications allowed for the parameters
+# (i.e. such that contain +/\-).
+e=0.005                      # Using default excess
 l=$e                         # and leeway.
-sox "$1" piece.wav trim \`expr $2 \- $e \- $l\`s \\
-   \`expr $3 \- $2 + $e + $l + $e\`s
-sox "$1" part1.wav trim 0 \`expr $4 + $e\`s
-sox "$1" part2.wav trim \`expr $4 + $3 \- $2 \- $e \- $l\`s
-sox part1.wav piece.wav part2.wav "$5" splice \\
-   \`expr $4 + $e\`s \\
-   \`expr $4 + $e + $3 \- $2 + $e + $l + $e\`s
+sox "$1" piece.wav trim $2\-$e\-$l =$3+$e
+sox "$1" part1.wav trim 0 $4+$e
+sox "$1" part2.wav trim $4+$3\-$2\-$e\-$l
+sox part1.wav piece.wav part2.wav "$5" \\
+   splice $4+$e +$3\-$2+$e+$l+$e
 .EE
 In the above Bourne shell script,
 two splices are used to `copy and paste' audio.
@@ -3961,20 +3979,12 @@
 .I depth
 (default 40).
 .TP
-\fBtrim\fR {[\fB=\fR\^|\^\fB\-\fR]\fIposition\fR}
+\fBtrim\fR {\fIposition(+)\fR}
 Cuts portions out of the audio.  Any number of \fIposition\fRs may be
 given; audio is not sent to the output until the first \fIposition\fR
 is reached.  The effect then alternates between copying and discarding
-audio at each \fIposition\fR.
-.SP
-If a \fIposition\fR is preceded by an equals or minus sign, it is
-interpreted relative to the beginning or the end of the audio,
-respectively.  (The audio length must be known for end-relative
-locations to work.)  Otherwise, it is considered an offset from the
-last \fIposition\fR, or from the start of audio for the first
-parameter.  Using a value of 0 for the first \fIposition\fR
+audio at each \fIposition\fR.  Using a value of 0 for the first \fIposition\fR
 parameter allows copying from the beginning of the audio.
-Any time specification may be used for \fIposition\fR.
 .SP
 For example,
 .EX
@@ -3984,7 +3994,11 @@
 .EX
    play infile trim 12:34 =15:00 -2:00
 .EE
-will play from 12 minutes 34 seconds into the audio up to 15 minutes into
+and
+.EX
+   play infile trim 12:34 2:26 -2:00
+.EE
+will both play from 12 minutes 34 seconds into the audio up to 15 minutes into
 the audio (i.e. 2 minutes and 26 seconds long), then resume playing two
 minutes before the end of audio.
 .TP
--- a/src/bend.c
+++ b/src/bend.c
@@ -70,23 +70,43 @@
 {
   priv_t *p = (priv_t *) effp->priv;
   size_t i;
-  uint64_t time = 0, delay;
   char const *next;
+  uint64_t last_seen = 0;
+  const uint64_t in_length = argv ? 0 :
+    (effp->in_signal.length != SOX_UNKNOWN_LEN ?
+     effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN);
 
   for (i = 0; i < p->nbends; ++i) {
     if (argv)   /* 1st parse only */
       p->bends[i].str = lsx_strdup(argv[i]);
-    next = lsx_parsesamples(rate, p->bends[i].str, &delay, 't');
+
+    next = lsx_parseposition(rate, p->bends[i].str,
+             argv ? NULL : &p->bends[i].start, last_seen, in_length, '+');
+    last_seen = p->bends[i].start;
     if (next == NULL || *next != ',')
       break;
-    p->bends[i].start = time += delay;
+
     p->bends[i].cents = strtod(next + 1, (char **)&next);
     if (p->bends[i].cents == 0 || *next != ',')
       break;
-    next = lsx_parsesamples(rate, next + 1, &p->bends[i].duration, 't');
+
+    next = lsx_parseposition(rate, next + 1,
+             argv ? NULL : &p->bends[i].duration, last_seen, in_length, '+');
+    last_seen = p->bends[i].duration;
     if (next == NULL || *next != '\0')
       break;
-    time += p->bends[i].duration;
+
+    /* sanity checks */
+    if (!argv && p->bends[i].duration < p->bends[i].start) {
+      lsx_fail("Bend %" PRIuPTR " has negative width", i+1);
+      break;
+    }
+    if (!argv && i && p->bends[i].start < p->bends[i-1].start) {
+      lsx_fail("Bend %" PRIuPTR " overlaps with previous one", i+1);
+      break;
+    }
+
+    p->bends[i].duration -= p->bends[i].start;
   }
   if (i < p->nbends)
     return lsx_usage(effp);
@@ -298,7 +318,7 @@
 sox_effect_handler_t const *lsx_bend_effect_fn(void)
 {
   static sox_effect_handler_t handler = {
-    "bend", "[-f frame-rate(25)] [-o over-sample(16)] {delay,cents,duration}",
+    "bend", "[-f frame-rate(25)] [-o over-sample(16)] {start,cents,end}",
     0, create, start, flow, 0, stop, lsx_kill, sizeof(priv_t)
   };
   return &handler;
--- a/src/delay.c
+++ b/src/delay.c
@@ -43,7 +43,6 @@
 static int create(sox_effect_t * effp, int argc, char * * argv)
 {
   priv_t * p = (priv_t *)effp->priv;
-  uint64_t dummy;
   unsigned i;
 
   --argc, ++argv;
@@ -51,7 +50,7 @@
   p->args = lsx_calloc(p->argc, sizeof(*p->args));
   p->max_delay = lsx_malloc(sizeof(*p->max_delay));
   for (i = 0; i < p->argc; ++i) {
-    char const * next = lsx_parsesamples(1e5, p->args[i].str = lsx_strdup(argv[i]), &dummy, 't');
+    char const * next = lsx_parseposition(0., p->args[i].str = lsx_strdup(argv[i]), NULL, (uint64_t)0, (uint64_t)0, '=');
     if (!next || *next) {
       lsx_kill(effp);
       return lsx_usage(effp);
@@ -70,7 +69,9 @@
 static int start(sox_effect_t * effp)
 {
   priv_t * p = (priv_t *)effp->priv;
-  uint64_t max_delay = 0, delay;
+  uint64_t max_delay = 0, last_seen = 0, delay;
+  uint64_t in_length = effp->in_signal.length != SOX_UNKNOWN_LEN ?
+    effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN;
 
   if (effp->flow == 0) {
     unsigned i;
@@ -79,8 +80,11 @@
       return SOX_EOF;
     }
     for (i = 0; i < p->argc; ++i) {
-      lsx_parsesamples(effp->in_signal.rate, p->args[i].str, &delay, 't');
-      p->args[i].delay = delay;
+      if (!lsx_parseposition(effp->in_signal.rate, p->args[i].str, &delay, last_seen, in_length, '=') || delay == SOX_UNKNOWN_LEN) {
+        lsx_fail("Position relative to end of audio specified, but audio length is unknown");
+        return SOX_EOF;
+      }
+      p->args[i].delay = last_seen = delay;
       if (delay > max_delay) {
         max_delay = delay;
       }
@@ -152,7 +156,7 @@
 sox_effect_handler_t const * lsx_delay_effect_fn(void)
 {
   static sox_effect_handler_t handler = {
-    "delay", "{length}", SOX_EFF_LENGTH | SOX_EFF_MODIFY,
+    "delay", "{position}", SOX_EFF_LENGTH | SOX_EFF_MODIFY,
     create, start, flow, drain, stop, lsx_kill, sizeof(priv_t)
   };
   return &handler;
--- a/src/effects_i.c
+++ b/src/effects_i.c
@@ -1,7 +1,7 @@
 /* Implements a libSoX internal interface for implementing effects.
  * All public functions & data are prefixed with lsx_ .
  *
- * Copyright (c) 2005-8 Chris Bagwell and SoX contributors
+ * Copyright (c) 2005-2012 Chris Bagwell and SoX contributors
  *
  * This library is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as published by
@@ -150,12 +150,18 @@
  * for a bare number like "123".  It can either be 't' or 's'.
  * Returns NULL on error, pointer to next char to parse otherwise.
  */
+static char const * parsesamples(sox_rate_t rate, const char *str0, uint64_t *samples, int def, int combine);
+
 char const * lsx_parsesamples(sox_rate_t rate, const char *str0, uint64_t *samples, int def)
 {
-  char * str = (char *)str0;
-  char combine = '+';
   *samples = 0;
+  return parsesamples(rate, str0, samples, def, '+');
+}
 
+static char const * parsesamples(sox_rate_t rate, const char *str0, uint64_t *samples, int def, int combine)
+{
+  char * str = (char *)str0;
+
   do {
     uint64_t samples_part;
     sox_bool found_samples = sox_false, found_time = sox_false;
@@ -311,6 +317,67 @@
   return 0;
 }
 #endif
+
+/*
+ * lsx_parseposition
+ *
+ * Parse a string for an audio position.  Similar to lsx_parsesamples
+ * above, but an initial '=', '+' or '-' indicates that the specified time
+ * is relative to the start of audio, last used position or end of audio,
+ * respectively.  Parameter def states which of these is the default.
+ * Parameters latest and end are the positions to which '+' and '-' relate;
+ * end may be SOX_UNKNOWN_LEN, in which case "-0" is the only valid
+ * end-relative input and will result in a position of SOX_UNKNOWN_LEN.
+ * Other parameters and return value are the same as for lsx_parsesamples.
+ *
+ * A test parse that only checks for valid syntax can be done by
+ * specifying samples = NULL.  If this passes, a later reparse of the same
+ * input will only fail if it is relative to the end ("-"), not "-0", and
+ * the end position is unknown.
+ */
+char const * lsx_parseposition(sox_rate_t rate, const char *str0, uint64_t *samples, uint64_t latest, uint64_t end, int def)
+{
+  char *str = (char *)str0;
+  char anchor, combine;
+
+  if (!strchr("+-=", def))
+    return NULL; /* error: invalid default anchor */
+  anchor = def;
+  if (*str && strchr("+-=", *str))
+    anchor = *str++;
+
+  combine = '+';
+  if (strchr("+-", anchor)) {
+    combine = anchor;
+    if (*str && strchr("+-", *str))
+      combine = *str++;
+  }
+
+  if (!samples) {
+    /* dummy parse, syntax checking only */
+    uint64_t dummy = 0;
+    return parsesamples(0., str, &dummy, 't', '+');
+  }
+
+  switch (anchor) {
+    case '=': *samples = 0; break;
+    case '+': *samples = latest; break;
+    case '-': *samples = end; break;
+  }
+
+  if (anchor == '-' && end == SOX_UNKNOWN_LEN) {
+    /* "-0" only valid input here */
+    char const *l;
+    for (l = str; *l && strchr("0123456789:.ets+-", *l); ++l);
+    if (l == str+1 && *str == '0') {
+      /* *samples already set to SOX_UNKNOWN_LEN */
+      return l;
+    }
+    return NULL; /* error: end-relative position, but end unknown */
+  }
+
+  return parsesamples(rate, str, samples, 't', combine);
+}
 
 /* a note is given as an int,
  * 0   => 440 Hz = A
--- a/src/fade.c
+++ b/src/fade.c
@@ -90,7 +90,7 @@
             fade->out_stop_str = lsx_strdup(argv[t_argno]);
 
             /* Do a dummy parse to see if it will fail */
-            n = lsx_parsesamples(0., fade->out_stop_str, &samples, 't');
+            n = lsx_parseposition(0., fade->out_stop_str, NULL, (uint64_t)0, (uint64_t)0, '=');
             if (!n || *n)
               return lsx_usage(effp);
             fade->out_stop = samples;
@@ -119,6 +119,8 @@
     priv_t * fade = (priv_t *) effp->priv;
     sox_bool truncate = sox_false;
     uint64_t samples;
+    uint64_t in_length = effp->in_signal.length != SOX_UNKNOWN_LEN ?
+      effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN;
 
     /* converting time values to samples */
     fade->in_start = 0;
@@ -132,9 +134,12 @@
     if (fade->out_stop_str)
     {
         fade->do_out = 1;
-        if (lsx_parsesamples(effp->in_signal.rate, fade->out_stop_str,
-                            &samples, 't') == NULL)
-          return lsx_usage(effp);
+        if (!lsx_parseposition(effp->in_signal.rate, fade->out_stop_str,
+                            &samples, (uint64_t)0, in_length, '=') ||
+            samples == SOX_UNKNOWN_LEN) {
+          lsx_fail("audio length is unknown");
+          return SOX_EOF;
+        }
         fade->out_stop = samples;
 
         if (!(truncate = !!fade->out_stop)) {
@@ -376,7 +381,7 @@
 
 static sox_effect_handler_t sox_fade_effect = {
   "fade",
-  "[ type ] fade-in-length [ stop-time [ fade-out-length ] ]\n"
+  "[ type ] fade-in-length [ stop-position [ fade-out-length ] ]\n"
   "       Time is in hh:mm:ss.frac format.\n"
   "       Fade type one of q, h, t, l or p.",
   SOX_EFF_MCHAN | SOX_EFF_LENGTH,
--- a/src/pad.c
+++ b/src/pad.c
@@ -35,6 +35,10 @@
   priv_t * p = (priv_t *)effp->priv;
   char const * next;
   unsigned i;
+  uint64_t last_seen = 0;
+  const uint64_t in_length = argv ? 0 :
+    (effp->in_signal.length != SOX_UNKNOWN_LEN ?
+     effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN);
 
   for (i = 0; i < p->npads; ++i) {
     if (argv) /* 1st parse only */
@@ -45,8 +49,12 @@
       p->pads[i].start = i? UINT64_MAX : 0;
     else {
       if (*next != '@') break;
-      next = lsx_parsesamples(rate, next+1, &p->pads[i].start, 't');
+      next = lsx_parseposition(rate, next+1, argv ? NULL : &p->pads[i].start,
+               last_seen, in_length, '=');
       if (next == NULL || *next != '\0') break;
+      last_seen = p->pads[i].start;
+      if (p->pads[i].start == SOX_UNKNOWN_LEN)
+        p->pads[i].start = UINT64_MAX; /* currently the same value, but ... */
     }
     if (!argv) {
       /* Do this check only during the second pass when the actual
--- a/src/sox_i.h
+++ b/src/sox_i.h
@@ -2,7 +2,7 @@
  *
  *   This file is meant for libSoX internal use only
  *
- * Copyright 2001-2008 Chris Bagwell and SoX Contributors
+ * Copyright 2001-2012 Chris Bagwell and SoX Contributors
  *
  * This source code is freely redistributable and may be used for
  * any purpose.  This copyright notice must be maintained.
@@ -74,6 +74,7 @@
     double max,         /* Maximum value on the y-axis. (e.g. +1) */
     double phase);      /* Phase at 1st point; 0..2pi. (e.g. pi/2 for cosine) */
 char const * lsx_parsesamples(sox_rate_t rate, const char *str, uint64_t *samples, int def);
+char const * lsx_parseposition(sox_rate_t rate, const char *str, uint64_t *samples, uint64_t latest, uint64_t end, int def);
 int lsx_parse_note(char const * text, char * * end_ptr);
 double lsx_parse_frequency_k(char const * text, char * * end_ptr, int key);
 #define lsx_parse_frequency(a, b) lsx_parse_frequency_k(a, b, INT_MAX)
--- a/src/spectrogram.c
+++ b/src/spectrogram.c
@@ -127,7 +127,7 @@
     case 't': p->title            = optstate.arg; break;
     case 'c': p->comment          = optstate.arg; break;
     case 'o': p->out_name         = optstate.arg; break;
-    case 'S': next = lsx_parsesamples(1e5, optstate.arg, &dummy, 't');
+    case 'S': next = lsx_parseposition(0., optstate.arg, NULL, (uint64_t)0, (uint64_t)0, '=');
       if (next && !*next) {p->start_time_str = lsx_strdup(optstate.arg); break;}
       return lsx_usage(effp);
     case 'd': next = lsx_parsesamples(1e5, optstate.arg, &dummy, 't');
@@ -216,7 +216,12 @@
     duration = d / effp->in_signal.rate;
   }
   if (p->start_time_str) {
-    lsx_parsesamples(effp->in_signal.rate, p->start_time_str, &d, 't');
+    uint64_t in_length = effp->in_signal.length != SOX_UNKNOWN_LEN ?
+      effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN;
+    if (!lsx_parseposition(effp->in_signal.rate, p->start_time_str, &d, (uint64_t)0, in_length, '=') || d == SOX_UNKNOWN_LEN) {
+      lsx_fail("-S option: audio length is unknown");
+      return SOX_EOF;
+    }
     start_time = d / effp->in_signal.rate;
     p->skip = d;
   }
@@ -675,7 +680,7 @@
     "\t-c text\tComment text",
     "\t-o text\tOutput file name; default `spectrogram.png'",
     "\t-d time\tAudio duration to fit to X-axis; e.g. 1:00, 48",
-    "\t-S time\tStart the spectrogram at the given time through the input",
+    "\t-S position\tStart the spectrogram at the given input position",
   };
   static char * usage;
   handler.usage = lsx_usage_lines(&usage, lines, array_length(lines));
--- a/src/splice.c
+++ b/src/splice.c
@@ -119,6 +119,10 @@
   priv_t * p = (priv_t *)effp->priv;
   char const * next;
   size_t i, buffer_size;
+  uint64_t last_seen = 0;
+  const uint64_t in_length = argv ? 0 :
+    (effp->in_signal.length != SOX_UNKNOWN_LEN ?
+     effp->in_signal.length / effp->in_signal.channels : SOX_UNKNOWN_LEN);
 
   p->max_buffer_size = 0;
   for (i = 0; i < p->nsplices; ++i) {
@@ -128,8 +132,10 @@
     p->splices[i].overlap = rate * 0.01 + .5;
     p->splices[i].search = p->fade_type == Cosine_4? 0 : p->splices[i].overlap;
 
-    next = lsx_parsesamples(rate, p->splices[i].str, &p->splices[i].start, 't');
+    next = lsx_parseposition(rate, p->splices[i].str,
+             argv ? NULL : &p->splices[i].start, last_seen, in_length, '=');
     if (next == NULL) break;
+    last_seen = p->splices[i].start;
 
     if (*next == ',') {
       next = lsx_parsesamples(rate, next + 1, &p->splices[i].overlap, 't');
@@ -145,11 +151,13 @@
     p->splices[i].overlap = max(p->splices[i].overlap + 4, 16);
     p->splices[i].overlap &= ~7; /* Make divisible by 8 for loop optimisation */
 
-    if (i > 0 && p->splices[i].start <= p->splices[i-1].start) break;
-    if (p->splices[i].start < p->splices[i].overlap) break;
-    p->splices[i].start -= p->splices[i].overlap;
-    buffer_size = 2 * p->splices[i].overlap + p->splices[i].search;
-    p->max_buffer_size = max(p->max_buffer_size, buffer_size);
+    if (!argv) {
+      if (i > 0 && p->splices[i].start <= p->splices[i-1].start) break;
+      if (p->splices[i].start < p->splices[i].overlap) break;
+      p->splices[i].start -= p->splices[i].overlap;
+      buffer_size = 2 * p->splices[i].overlap + p->splices[i].search;
+      p->max_buffer_size = max(p->max_buffer_size, buffer_size);
+    }
   }
   if (i < p->nsplices)
     return lsx_usage(effp);
--- a/src/trim.c
+++ b/src/trim.c
@@ -28,15 +28,11 @@
   struct {
     uint64_t sample; /* NB: wide samples */
     char *argstr;
-    enum {
-      a_start, a_latest, a_end
-    } anchor;
   } *pos;
   /* state */
   unsigned int current_pos;
   uint64_t samples_read; /* NB: wide samples */
   sox_bool copying;
-  sox_bool uses_end;
 } priv_t;
 
 static int parse(sox_effect_t *effp, int argc, char **argv)
@@ -46,24 +42,15 @@
   --argc, ++argv;
   p->num_pos = argc;
   lsx_Calloc(p->pos, p->num_pos);
-  p->uses_end = sox_false;
   for (i = 0; i < p->num_pos; i++) {
-    uint64_t dummy;
     const char *arg = argv[i];
-    if (arg[0] == '=') {
-      p->pos[i].anchor = a_start;
-      arg++;
-    } else if (arg[0] == '-') {
-      p->pos[i].anchor = a_end;
-      p->uses_end = sox_true;
-      arg++;
-    } else
-      p->pos[i].anchor = a_latest;
     p->pos[i].argstr = lsx_strdup(arg);
     /* dummy parse to check for syntax errors */
-    arg = lsx_parsesamples(0., arg, &dummy, 't');
-    if (!arg || *arg)
+    arg = lsx_parseposition(0., arg, NULL, (uint64_t)0, (uint64_t)0, '+');
+    if (!arg || *arg) {
+      lsx_fail("Error parsing position %u", i+1);
       return lsx_usage(effp);
+    }
   }
   return SOX_SUCCESS;
 }
@@ -80,28 +67,13 @@
   p->copying = sox_false;
 
   /* calculate absolute positions */
-  if (in_length == SOX_UNKNOWN_LEN && p->uses_end) {
-    lsx_fail("Can't use positions relative to end: audio length is unknown.");
-    return SOX_EOF;
-  }
   for (i = 0; i < p->num_pos; i++) {
-    uint64_t s, res = 0;
-    if (!lsx_parsesamples(effp->in_signal.rate, p->pos[i].argstr, &s, 't'))
-      return lsx_usage(effp);
-    switch (p->pos[i].anchor) {
-      case a_start: res = s; break;
-      case a_latest: res = last_seen + s; break;
-      case a_end:
-        if (s <= in_length)
-          res = in_length - s;
-        else {
-          lsx_fail("Position %u is before start of audio.", i+1);
-          return SOX_EOF;
-        }
-        break;
+    if (!lsx_parseposition(effp->in_signal.rate, p->pos[i].argstr, &p->pos[i].sample, last_seen, in_length, '+')) {
+      lsx_fail("Position %u is relative to end of audio, but audio length is unknown", i+1);
+      return SOX_EOF;
     }
-    last_seen = p->pos[i].sample = res;
-    lsx_debug_more("position %u at %" PRIu64, i+1, res);
+    last_seen = p->pos[i].sample;
+    lsx_debug_more("position %u at %" PRIu64, i+1, last_seen);
   }
 
   /* sanity checks */
@@ -119,6 +91,13 @@
       lsx_warn("%s position is after expected end of audio.",
           p->pos[0].sample > in_length ? "Start" : "End");
 
+  /* avoid unnecessary work */
+  if (in_length == SOX_UNKNOWN_LEN)
+    while (p->num_pos && p->pos[p->num_pos-1].sample == SOX_UNKNOWN_LEN) {
+      lsx_debug_more("removing `-0' position");
+      p->num_pos--;
+      free(p->pos[p->num_pos].argstr);
+    }
   if (p->num_pos == 1 && !p->pos[0].sample)
     return SOX_EFF_NULL;
 
@@ -207,7 +186,7 @@
 sox_effect_handler_t const *lsx_trim_effect_fn(void)
 {
   static sox_effect_handler_t handler = {
-    "trim", "{[=|-]position}",
+    "trim", "{position}",
     SOX_EFF_MCHAN | SOX_EFF_LENGTH | SOX_EFF_MODIFY,
     parse, start, flow, drain, NULL, lsx_kill,
     sizeof(priv_t)