Improvements to the pitch screen UI (FS#10359 by David Johnston)

git-svn-id: svn://svn.rockbox.org/rockbox/trunk@21781 a1c6a512-1295-4272-9138-f99709370657
diff --git a/apps/dsp.c b/apps/dsp.c
index 30b4ed3..ec59417 100644
--- a/apps/dsp.c
+++ b/apps/dsp.c
@@ -162,7 +162,7 @@
     int  sample_depth;
     int  sample_bytes;
     int  stereo_mode;
-    int  tdspeed_percent; /* Speed % */
+    int32_t  tdspeed_percent; /* Speed% * PITCH_SPEED_PRECISION */
     bool tdspeed_active;  /* Timestretch is in use */
     int  frac_bits;
 #ifdef HAVE_SW_TONE_CONTROLS
@@ -205,7 +205,7 @@
 #endif
 
 /* Settings applicable to audio codec only */
-static int  pitch_ratio = 1000;
+static int32_t  pitch_ratio = PITCH_SPEED_100;
 static int  channels_mode;
        long dsp_sw_gain;
        long dsp_sw_cross;
@@ -254,14 +254,14 @@
     return sample;
 }
 
-int sound_get_pitch(void)
+int32_t sound_get_pitch(void)
 {
     return pitch_ratio;
 }
 
-void sound_set_pitch(int permille)
+void sound_set_pitch(int32_t percent)
 {
-    pitch_ratio = permille;
+    pitch_ratio = percent;
     dsp_configure(&AUDIO_DSP, DSP_SWITCH_FREQUENCY,
                   AUDIO_DSP.codec_frequency);
 }
@@ -277,7 +277,7 @@
     if(!dsp_timestretch_available())
         return; /* Timestretch not enabled or buffer not allocated */
     if (dspc->tdspeed_percent == 0)
-        dspc->tdspeed_percent = 100;
+        dspc->tdspeed_percent = PITCH_SPEED_100;
     if (!tdspeed_config(
         dspc->codec_frequency == 0 ? NATIVE_FREQUENCY : dspc->codec_frequency,
         dspc->stereo_mode != STEREO_MONO,
@@ -312,13 +312,13 @@
     }
 }
 
-void dsp_set_timestretch(int percent)
+void dsp_set_timestretch(int32_t percent)
 {
     AUDIO_DSP.tdspeed_percent = percent;
     tdspeed_setup(&AUDIO_DSP);
 }
 
-int dsp_get_timestretch()
+int32_t dsp_get_timestretch()
 {
     return AUDIO_DSP.tdspeed_percent;
 }
@@ -1347,7 +1347,7 @@
            not need this feature.
          */
         if (dsp == &AUDIO_DSP)
-            dsp->frequency = pitch_ratio * dsp->codec_frequency / 1000;
+            dsp->frequency = pitch_ratio * dsp->codec_frequency / PITCH_SPEED_100;
         else
             dsp->frequency = dsp->codec_frequency;
 
diff --git a/apps/dsp.h b/apps/dsp.h
index 3d24b24..7d1e2b3 100644
--- a/apps/dsp.h
+++ b/apps/dsp.h
@@ -83,10 +83,10 @@
 void dsp_dither_enable(bool enable);
 void dsp_timestretch_enable(bool enable);
 bool dsp_timestretch_available(void);
-void sound_set_pitch(int r);
-int sound_get_pitch(void);
-void dsp_set_timestretch(int percent);
-int dsp_get_timestretch(void);
+void sound_set_pitch(int32_t r);
+int32_t sound_get_pitch(void);
+void dsp_set_timestretch(int32_t percent);
+int32_t dsp_get_timestretch(void);
 int dsp_callback(int msg, intptr_t param);
 
 #endif
diff --git a/apps/gui/pitchscreen.c b/apps/gui/pitchscreen.c
index 16fac0c..a699d4a 100644
--- a/apps/gui/pitchscreen.c
+++ b/apps/gui/pitchscreen.c
@@ -22,6 +22,7 @@
 #include <stdbool.h>
 #include <string.h>
 #include <stdio.h>
+#include <math.h>
 #include "config.h"
 #include "sprintf.h"
 #include "action.h"
@@ -36,24 +37,27 @@
 #include "system.h"
 #include "misc.h"
 #include "pitchscreen.h"
+#include "settings.h"
 #if CONFIG_CODEC == SWCODEC
 #include "tdspeed.h"
 #endif
 
+#define ABS(x) ((x) > 0 ? (x) : -(x))
 
 #define ICON_BORDER 12 /* icons are currently 7x8, so add ~2 pixels  */
                        /*   on both sides when drawing */
 
-#define PITCH_MAX         2000
-#define PITCH_MIN         500
-#define PITCH_SMALL_DELTA 1
-#define PITCH_BIG_DELTA   10
-#define PITCH_NUDGE_DELTA 20
+#define PITCH_MAX         (200 * PITCH_SPEED_PRECISION)
+#define PITCH_MIN         (50 * PITCH_SPEED_PRECISION)
+#define PITCH_SMALL_DELTA (PITCH_SPEED_PRECISION / 10)      /* .1% */
+#define PITCH_BIG_DELTA   (PITCH_SPEED_PRECISION)           /*  1% */
+#define PITCH_NUDGE_DELTA (2 * PITCH_SPEED_PRECISION)       /*  2% */
 
-static bool pitch_mode_semitone = false;
-#if CONFIG_CODEC == SWCODEC
-static bool pitch_mode_timestretch = false;
-#endif
+#define SPEED_SMALL_DELTA (PITCH_SPEED_PRECISION / 10)      /* .1% */
+#define SPEED_BIG_DELTA   (PITCH_SPEED_PRECISION)           /*  1% */
+
+#define SEMITONE_SMALL_DELTA (PITCH_SPEED_PRECISION / 10)  /* 10 cents   */
+#define SEMITONE_BIG_DELTA   PITCH_SPEED_PRECISION         /* 1 semitone */
 
 enum
 {
@@ -63,25 +67,111 @@
     PITCH_ITEM_COUNT,
 };
 
+
+/* This is a table of semitone percentage values of the appropriate 
+   precision (based on PITCH_SPEED_PRECISION).  Note that these are
+   all constant expressions, which will be evaluated at compile time,
+   so no need to worry about how complex the expressions look.  
+   That's just to get the precision right.
+
+   I calculated these values, starting from 50, as 
+
+   x(n) = 50 * 2^(n/12)
+
+   All that math in each entry simply converts the float constant
+   to an integer equal to PITCH_SPEED_PRECISION times the float value,
+   with as little precision loss as possible.
+*/
+#define SEMITONE_VALUE(x) \
+    ( (int)(((x) + 0.5 / PITCH_SPEED_PRECISION) * PITCH_SPEED_PRECISION) )
+    
+static const int semitone_table[] =
+{
+    SEMITONE_VALUE(50),
+    SEMITONE_VALUE(52.97315472),
+    SEMITONE_VALUE(56.12310242),
+    SEMITONE_VALUE(59.46035575),
+    SEMITONE_VALUE(62.99605249),
+    SEMITONE_VALUE(66.74199271),
+    SEMITONE_VALUE(70.71067812),
+    SEMITONE_VALUE(74.91535384),
+    SEMITONE_VALUE(79.3700526 ),
+    SEMITONE_VALUE(84.08964153),
+    SEMITONE_VALUE(89.08987181),
+    SEMITONE_VALUE(94.38743127),
+    SEMITONE_VALUE(100        ),
+    SEMITONE_VALUE(105.9463094),
+    SEMITONE_VALUE(112.2462048),
+    SEMITONE_VALUE(118.9207115),
+    SEMITONE_VALUE(125.992105 ),
+    SEMITONE_VALUE(133.4839854),
+    SEMITONE_VALUE(141.4213562),
+    SEMITONE_VALUE(149.8307077),
+    SEMITONE_VALUE(158.7401052),
+    SEMITONE_VALUE(168.1792831),
+    SEMITONE_VALUE(178.1797436),
+    SEMITONE_VALUE(188.7748625),
+    SEMITONE_VALUE(200        )
+};
+
+#define NUM_SEMITONES   ((int)(sizeof(semitone_table) / sizeof(int)))
+#define SEMITONE_START -12
+#define SEMITONE_END    12
+
+/* A table of values for approximating the cent curve with 
+   linear interpolation.  Multipy the next lowest semitone
+   by this much to find the corresponding cent percentage. 
+   
+   These values were calculated as 
+   x(n) = 100 * 2^(n * 20/1200) 
+*/
+
+#define CENT_INTERP(x) \
+    ( (int)(((x) + 0.5 / PITCH_SPEED_PRECISION) * PITCH_SPEED_PRECISION) )
+
+
+static const int cent_interp[] =
+{
+    PITCH_SPEED_100,
+    CENT_INTERP(101.1619440),
+    CENT_INTERP(102.3373892),
+    CENT_INTERP(103.5264924),
+    CENT_INTERP(104.7294123),
+    /* this one's the next semitone but we have it here for convenience */
+    CENT_INTERP(105.9463094),
+};
+
+/* Number of cents between entries in the cent_interp table */
+#define CENT_INTERP_INTERVAL 20
+#define CENT_INTERP_NUM      ((int)(sizeof(cent_interp)/sizeof(int)))
+
+/* This stores whether the pitch and speed are at their own limits */
+/* or that of the timestretching algorithm                         */
+static bool at_limit = false;
+
 static void pitchscreen_fix_viewports(struct viewport *parent,
         struct viewport pitch_viewports[PITCH_ITEM_COUNT])
 {
-    int i, height;
-    height = font_get(parent->font)->height;
+    int i, font_height;
+    font_height = font_get(parent->font)->height;
     for (i = 0; i < PITCH_ITEM_COUNT; i++)
     {
         pitch_viewports[i] = *parent;
-        pitch_viewports[i].height = height;
+        pitch_viewports[i].height = font_height;
     }
     pitch_viewports[PITCH_TOP].y += ICON_BORDER;
 
     pitch_viewports[PITCH_MID].x += ICON_BORDER;
     pitch_viewports[PITCH_MID].width = parent->width - ICON_BORDER*2;
-    pitch_viewports[PITCH_MID].height = height * 2;
+    pitch_viewports[PITCH_MID].height = parent->height - ICON_BORDER*2 
+                                        - font_height * 2;
+    if(pitch_viewports[PITCH_MID].height < font_height * 2)
+        pitch_viewports[PITCH_MID].height = font_height * 2;
     pitch_viewports[PITCH_MID].y += parent->height / 2 -
             pitch_viewports[PITCH_MID].height / 2;
 
-    pitch_viewports[PITCH_BOTTOM].y += parent->height - height - ICON_BORDER;
+    pitch_viewports[PITCH_BOTTOM].y += parent->height - font_height 
+                                       - ICON_BORDER;
 }
 
 /* must be called before pitchscreen_draw, or within
@@ -107,9 +197,9 @@
 
 static void pitchscreen_draw(struct screen *display, int max_lines,
                              struct viewport pitch_viewports[PITCH_ITEM_COUNT],
-                             int pitch
+                             int32_t pitch, int32_t semitone
 #if CONFIG_CODEC == SWCODEC
-                             ,int speed
+                             ,int32_t speed
 #endif
                              )
 {
@@ -123,7 +213,7 @@
     {
         /* UP: Pitch Up */
         display->set_viewport(&pitch_viewports[PITCH_TOP]);
-        if (pitch_mode_semitone)
+        if (global_settings.pitch_mode_semitone)
             ptr = str(LANG_PITCH_UP_SEMITONE);
         else
             ptr = str(LANG_PITCH_UP);
@@ -136,7 +226,7 @@
 
         /* DOWN: Pitch Down */
         display->set_viewport(&pitch_viewports[PITCH_BOTTOM]);
-        if (pitch_mode_semitone)
+        if (global_settings.pitch_mode_semitone)
             ptr = str(LANG_PITCH_DOWN_SEMITONE);
         else
             ptr = str(LANG_PITCH_DOWN);
@@ -157,55 +247,95 @@
     if ((show_lang_pitch = (max_lines >= 3)))
     {
 #if CONFIG_CODEC == SWCODEC
-        if (!pitch_mode_timestretch)
-        {
-#endif
-            /* LANG_PITCH */
-            snprintf(buf, sizeof(buf), "%s", str(LANG_PITCH));
-#if CONFIG_CODEC == SWCODEC
-        }
-        else
+        if(global_settings.pitch_mode_timestretch)
         {
             /* Pitch:XXX.X% */
-            snprintf(buf, sizeof(buf), "%s:%d.%d%%", str(LANG_PITCH),
-                     pitch / 10, pitch % 10);
+            if(global_settings.pitch_mode_semitone)
+            {
+                snprintf(buf, sizeof(buf), "%s: %s%ld.%02ld", str(LANG_PITCH),
+                         semitone >= 0 ? "+" : "-",
+                         ABS(semitone / PITCH_SPEED_PRECISION), 
+                         ABS((semitone % PITCH_SPEED_PRECISION) / 
+                                         (PITCH_SPEED_PRECISION / 100))
+                        );
+            }
+            else
+            {
+                snprintf(buf, sizeof(buf), "%s: %ld.%ld%%", str(LANG_PITCH),
+                         pitch / PITCH_SPEED_PRECISION, 
+                         (pitch % PITCH_SPEED_PRECISION) / 
+                         (PITCH_SPEED_PRECISION / 10));
+            }
         }
+        else
 #endif
+        {
+            /* Rate */
+            snprintf(buf, sizeof(buf), "%s:", str(LANG_PLAYBACK_RATE));
+        }
         display->getstringsize(buf, &w, &h);
-        display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2),
-                        0, buf);
+        display->putsxy((pitch_viewports[PITCH_MID].width  / 2) - (w / 2),
+                        (pitch_viewports[PITCH_MID].height / 2) - h, buf);
         if (w > width_used)
             width_used = w;
     }
 
     /* Middle section lower line */
+    /* "Speed:XXX%" */
 #if CONFIG_CODEC == SWCODEC
-    if (!pitch_mode_timestretch)
+    if(global_settings.pitch_mode_timestretch)
     {
-#endif
-        /* "XXX.X%" */
-        snprintf(buf, sizeof(buf), "%d.%d%%",
-             pitch / 10, pitch % 10);
-#if CONFIG_CODEC == SWCODEC
+        snprintf(buf, sizeof(buf), "%s: %ld.%ld%%", str(LANG_SPEED), 
+                 speed / PITCH_SPEED_PRECISION, 
+                 (speed % PITCH_SPEED_PRECISION) / (PITCH_SPEED_PRECISION / 10));
     }
     else
-    {
-        /* "Speed:XXX%" */
-        snprintf(buf, sizeof(buf), "%s:%d%%", str(LANG_SPEED), 
-             speed / 1000);
-    }
 #endif
+    {
+        if(global_settings.pitch_mode_semitone)
+        {
+            snprintf(buf, sizeof(buf), "%s%ld.%02ld",
+                     semitone >= 0 ? "+" : "-",
+                     ABS(semitone / PITCH_SPEED_PRECISION), 
+                     ABS((semitone % PITCH_SPEED_PRECISION) / 
+                                     (PITCH_SPEED_PRECISION / 100))
+                    );
+        }
+        else
+        {
+            snprintf(buf, sizeof(buf), "%ld.%ld%%",
+                     pitch / PITCH_SPEED_PRECISION, 
+                     (pitch  % PITCH_SPEED_PRECISION) / (PITCH_SPEED_PRECISION / 10));
+        }
+    }
+
     display->getstringsize(buf, &w, &h);
     display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2),
-        (show_lang_pitch ? h : h/2), buf);
+        show_lang_pitch ? (pitch_viewports[PITCH_MID].height / 2) : 
+                          (pitch_viewports[PITCH_MID].height / 2) - (h / 2), 
+        buf);
     if (w > width_used)
         width_used = w;
 
+    /* "limit" and "timestretch" labels */
+    if (max_lines >= 7)
+    {
+        if(at_limit)
+        {
+            snprintf(buf, sizeof(buf), "%s", str(LANG_STRETCH_LIMIT));
+            display->getstringsize(buf, &w, &h);
+            display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2),
+                            (pitch_viewports[PITCH_MID].height / 2) + h, buf);
+            if (w > width_used)
+                width_used = w;
+        }
+    }
+
     /* Middle section left/right labels */
     const char *leftlabel = "-2%";
     const char *rightlabel = "+2%";
 #if CONFIG_CODEC == SWCODEC
-    if (pitch_mode_timestretch)
+    if (global_settings.pitch_mode_timestretch)
     {
         leftlabel = "<<";
         rightlabel = ">>";
@@ -220,37 +350,67 @@
 
     if (width_used <= pitch_viewports[PITCH_MID].width)
     {
-        display->putsxy(0, h / 2, leftlabel);
-        display->putsxy(pitch_viewports[PITCH_MID].width - w, h /2, rightlabel);
+        display->putsxy(0, (pitch_viewports[PITCH_MID].height / 2) - (h / 2),
+                        leftlabel);
+        display->putsxy((pitch_viewports[PITCH_MID].width - w), 
+                        (pitch_viewports[PITCH_MID].height / 2) - (h / 2), 
+                        rightlabel);
     }
     display->update_viewport();
     display->set_viewport(NULL);
 }
 
-static int pitch_increase(int pitch, int pitch_delta, bool allow_cutoff)
+static int32_t pitch_increase(int32_t pitch, int32_t pitch_delta, bool allow_cutoff
+#if CONFIG_CODEC == SWCODEC
+                          /* need this to maintain correct pitch/speed caps */
+                          , int32_t speed
+#endif
+                          )
 {
-    int new_pitch;
+    int32_t new_pitch;
+#if CONFIG_CODEC == SWCODEC
+    int32_t new_stretch;
+#endif
+    at_limit = false;
 
     if (pitch_delta < 0)
     {
-        if (pitch + pitch_delta >= PITCH_MIN)
-            new_pitch = pitch + pitch_delta;
-        else
+        /* for large jumps, snap up to whole numbers */
+        if(allow_cutoff && pitch_delta <= -PITCH_SPEED_PRECISION && 
+           (pitch + pitch_delta) % PITCH_SPEED_PRECISION != 0)
+        {
+            pitch_delta += PITCH_SPEED_PRECISION - ((pitch + pitch_delta) % PITCH_SPEED_PRECISION);
+        }
+
+        new_pitch = pitch + pitch_delta;
+
+        if (new_pitch < PITCH_MIN)
         {
             if (!allow_cutoff)
+            {
                 return pitch;
+            }
             new_pitch = PITCH_MIN;
+            at_limit = true;
         }
     }
     else if (pitch_delta > 0)
     {
-        if (pitch + pitch_delta <= PITCH_MAX)
-            new_pitch = pitch + pitch_delta;
-        else
+        /* for large jumps, snap down to whole numbers */
+        if(allow_cutoff && pitch_delta >= PITCH_SPEED_PRECISION && 
+           (pitch + pitch_delta) % PITCH_SPEED_PRECISION != 0)
+        {
+            pitch_delta -= (pitch + pitch_delta) % PITCH_SPEED_PRECISION;
+        }
+
+        new_pitch = pitch + pitch_delta;
+
+        if (new_pitch > PITCH_MAX)
         {
             if (!allow_cutoff)
                 return pitch;
             new_pitch = PITCH_MAX;
+            at_limit = true;
         }
     }
     else
@@ -258,47 +418,164 @@
         /* pitch_delta == 0 -> no real change */
         return pitch;
     }
+#if CONFIG_CODEC == SWCODEC
+    if (dsp_timestretch_available())
+    {
+        /* increase the multiple to increase precision of this calculation */
+        new_stretch = GET_STRETCH(new_pitch, speed);
+        if(new_stretch < STRETCH_MIN)
+        {
+            /* we have to ignore allow_cutoff, because we can't have the */
+            /* stretch go higher than STRETCH_MAX                        */
+            new_pitch = GET_PITCH(speed, STRETCH_MIN);
+        }
+        else if(new_stretch > STRETCH_MAX)
+        {
+            /* we have to ignore allow_cutoff, because we can't have the */
+            /* stretch go higher than STRETCH_MAX                        */
+            new_pitch = GET_PITCH(speed, STRETCH_MAX);
+        }
+
+        if(new_stretch >= STRETCH_MAX || 
+           new_stretch <= STRETCH_MIN)
+        {
+            at_limit = true;
+        }
+    }
+#endif
+
     sound_set_pitch(new_pitch);
 
     return new_pitch;
 }
 
-/* Factor for changing the pitch one half tone up.
-   The exact value is 2^(1/12) = 1.05946309436
-   But we use only integer arithmetics, so take
-   rounded factor multiplied by 10^5=100,000. This is
-   enough to get the same promille values as if we
-   had used floating point (checked with a spread
-   sheet).
- */
-#define PITCH_SEMITONE_FACTOR 105946L
-
-/* Some helpful constants. K is the scaling factor for SEMITONE.
-   N is for more accurate rounding
-   KN is K * N
- */
-#define PITCH_K_FCT           100000UL
-#define PITCH_N_FCT           10
-#define PITCH_KN_FCT          1000000UL
-
-static int pitch_increase_semitone(int pitch, bool up)
+static int32_t get_semitone_from_pitch(int32_t pitch)
 {
-    uint32_t tmp;
-    uint32_t round_fct; /* How much to scale down at the end */
-    tmp = pitch;
-    if (up)
+    int semitone = 0;
+    int32_t fractional_index = 0;
+
+    while(semitone < NUM_SEMITONES - 1 &&
+          pitch >= semitone_table[semitone + 1])
     {
-        tmp = tmp * PITCH_SEMITONE_FACTOR;
-        round_fct = PITCH_K_FCT;
+        semitone++;
+    }
+
+
+    /* now find the fractional part */
+    while(pitch > (cent_interp[fractional_index + 1] * 
+                   semitone_table[semitone] / PITCH_SPEED_100))
+    {
+        /* Check to make sure fractional_index isn't too big */
+        /* This should never happen. */
+        if(fractional_index >= CENT_INTERP_NUM - 1)
+        {
+            break;
+        }
+        fractional_index++;
+    }
+
+    int32_t semitone_pitch_a = cent_interp[fractional_index] * 
+                               semitone_table[semitone] /
+                               PITCH_SPEED_100;
+    int32_t semitone_pitch_b = cent_interp[fractional_index + 1] * 
+                               semitone_table[semitone] /
+                               PITCH_SPEED_100;
+    /* this will be the integer offset from the cent_interp entry */
+    int32_t semitone_frac_ofs = (pitch - semitone_pitch_a) * CENT_INTERP_INTERVAL /
+                            (semitone_pitch_b - semitone_pitch_a);
+    semitone = (semitone + SEMITONE_START) * PITCH_SPEED_PRECISION + 
+                     fractional_index * CENT_INTERP_INTERVAL + 
+                     semitone_frac_ofs;
+
+    return semitone;
+}
+
+static int32_t get_pitch_from_semitone(int32_t semitone)
+{
+    int32_t adjusted_semitone = semitone - SEMITONE_START * PITCH_SPEED_PRECISION;
+
+    /* Find the index into the semitone table */
+    int32_t semitone_index = (adjusted_semitone / PITCH_SPEED_PRECISION);
+
+    /* set pitch to the semitone's integer part value */
+    int32_t pitch = semitone_table[semitone_index];
+    /* get the range of the cent modification for future calculation */
+    int32_t pitch_mod_a = 
+        cent_interp[(adjusted_semitone % PITCH_SPEED_PRECISION) / 
+                    CENT_INTERP_INTERVAL];
+    int32_t pitch_mod_b = 
+        cent_interp[(adjusted_semitone % PITCH_SPEED_PRECISION) / 
+                    CENT_INTERP_INTERVAL + 1];
+    /* figure out the cent mod amount based on the semitone fractional value */
+    int32_t pitch_mod = pitch_mod_a + (pitch_mod_b - pitch_mod_a) * 
+                   (adjusted_semitone % CENT_INTERP_INTERVAL) / CENT_INTERP_INTERVAL;
+
+    /* modify pitch based on the mod amount we just calculated */
+    return (pitch * pitch_mod  + PITCH_SPEED_100 / 2) / PITCH_SPEED_100;
+}
+
+static int32_t pitch_increase_semitone(int32_t pitch,
+                                       int32_t current_semitone, 
+                                       int32_t semitone_delta
+#if CONFIG_CODEC == SWCODEC
+                                       , int32_t speed
+#endif                            
+                                      )
+{
+    int32_t new_semitone = current_semitone;
+
+    /* snap to the delta interval */
+    if(current_semitone % semitone_delta != 0)
+    {
+        if(current_semitone > 0 && semitone_delta > 0)
+            new_semitone += semitone_delta;
+        else if(current_semitone < 0 && semitone_delta < 0)
+            new_semitone += semitone_delta;
+
+        new_semitone -= new_semitone % semitone_delta;
     }
     else
+        new_semitone += semitone_delta;
+
+    /* clamp the pitch so it doesn't go beyond the pitch limits */
+    if(new_semitone < (SEMITONE_START * PITCH_SPEED_PRECISION))
     {
-        tmp = (tmp * PITCH_KN_FCT) / PITCH_SEMITONE_FACTOR;
-        round_fct = PITCH_N_FCT;
+        new_semitone = SEMITONE_START * PITCH_SPEED_PRECISION;
+        at_limit = true;
     }
-    /* Scaling down with rounding */
-    tmp = (tmp + round_fct / 2) / round_fct;
-    return pitch_increase(pitch, tmp - pitch, false);
+    else if(new_semitone > (SEMITONE_END * PITCH_SPEED_PRECISION))
+    {
+        new_semitone = SEMITONE_END * PITCH_SPEED_PRECISION;
+        at_limit = true;
+    }
+
+    int32_t new_pitch = get_pitch_from_semitone(new_semitone);
+
+#if CONFIG_CODEC == SWCODEC
+    int32_t new_stretch = GET_STRETCH(new_pitch, speed);
+
+    /* clamp the pitch so it doesn't go beyond the stretch limits */
+    if( new_stretch > STRETCH_MAX)
+    {
+        new_pitch = GET_PITCH(speed, STRETCH_MAX);
+        new_semitone = get_semitone_from_pitch(new_pitch);
+        at_limit = true;
+    }
+    else if (new_stretch < STRETCH_MIN)
+    {
+        new_pitch = GET_PITCH(speed, STRETCH_MIN);
+        new_semitone = get_semitone_from_pitch(new_pitch);
+        at_limit = true;
+    }
+#endif
+
+    pitch_increase(pitch, new_pitch - pitch, false
+#if CONFIG_CODEC == SWCODEC
+                   , speed
+#endif          
+                   );
+
+    return new_semitone;
 }
 
 /*
@@ -310,13 +587,11 @@
 int gui_syncpitchscreen_run(void)
 {
     int button, i;
-    int pitch = sound_get_pitch();
-#if CONFIG_CODEC == SWCODEC
-    int stretch = dsp_get_timestretch();
-    int speed = stretch * pitch; /* speed to maintain */
-#endif
-    int new_pitch;
-    int pitch_delta;
+    int32_t pitch = sound_get_pitch();
+    int32_t semitone;
+
+    int32_t new_pitch;
+    int32_t pitch_delta;
     bool nudged = false;
     bool exit = false;
     /* should maybe be passed per parameter later, not needed for now */
@@ -324,6 +599,31 @@
     struct viewport pitch_viewports[NB_SCREENS][PITCH_ITEM_COUNT];
     int max_lines[NB_SCREENS];
 
+#if CONFIG_CODEC == SWCODEC
+    int32_t new_speed = 0, new_stretch;
+
+    /* the speed variable holds the apparent speed of the playback */
+    int32_t speed;
+    if (dsp_timestretch_available())
+    {
+        speed = GET_SPEED(pitch, dsp_get_timestretch());
+    }
+    else
+    {
+        speed = pitch;
+    }
+
+    /* Figure out whether to be in timestretch mode */
+    if (global_settings.pitch_mode_timestretch && !dsp_timestretch_available())
+    {
+        global_settings.pitch_mode_timestretch = false;
+        settings_save();
+    }
+#endif
+
+    /* set the semitone index based on the current pitch */
+    semitone = get_semitone_from_pitch(pitch);
+
     /* initialize pitchscreen vps */
     FOR_NB_SCREENS(i)
     {
@@ -343,49 +643,80 @@
     {
         FOR_NB_SCREENS(i)
             pitchscreen_draw(&screens[i], max_lines[i],
-                              pitch_viewports[i], pitch
+                              pitch_viewports[i], pitch, semitone
 #if CONFIG_CODEC == SWCODEC
                               , speed
 #endif
                               );
         pitch_delta = 0;
+#if CONFIG_CODEC == SWCODEC
+        new_speed = 0;
+#endif
         button = get_action(CONTEXT_PITCHSCREEN, HZ);
         switch (button)
         {
             case ACTION_PS_INC_SMALL:
-                pitch_delta = PITCH_SMALL_DELTA;
+                if(global_settings.pitch_mode_semitone)
+                    pitch_delta = SEMITONE_SMALL_DELTA;
+                else 
+                    pitch_delta = PITCH_SMALL_DELTA;
                 break;
 
             case ACTION_PS_INC_BIG:
-                pitch_delta = PITCH_BIG_DELTA;
+                if(global_settings.pitch_mode_semitone)
+                    pitch_delta = SEMITONE_BIG_DELTA;
+                else 
+                    pitch_delta = PITCH_BIG_DELTA;
                 break;
 
             case ACTION_PS_DEC_SMALL:
-                pitch_delta = -PITCH_SMALL_DELTA;
+                if(global_settings.pitch_mode_semitone)
+                    pitch_delta = -SEMITONE_SMALL_DELTA;
+                else 
+                    pitch_delta = -PITCH_SMALL_DELTA;
                 break;
 
             case ACTION_PS_DEC_BIG:
-                pitch_delta = -PITCH_BIG_DELTA;
+                if(global_settings.pitch_mode_semitone)
+                    pitch_delta = -SEMITONE_BIG_DELTA;
+                else 
+                    pitch_delta = -PITCH_BIG_DELTA;
                 break;
 
             case ACTION_PS_NUDGE_RIGHT:
 #if CONFIG_CODEC == SWCODEC
-                if (!pitch_mode_timestretch)
+                if (!global_settings.pitch_mode_timestretch)
                 {
 #endif
-                    new_pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false);
+                    new_pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false
+#if CONFIG_CODEC == SWCODEC
+                                               , speed
+#endif                            
+                        );
                     nudged = (new_pitch != pitch);
                     pitch = new_pitch;
+                    semitone = get_semitone_from_pitch(pitch);
+#if CONFIG_CODEC == SWCODEC
+                    speed = pitch;
+#endif
                     break;
 #if CONFIG_CODEC == SWCODEC
                 }
+                else
+                {
+                    new_speed = speed + SPEED_SMALL_DELTA;
+                    at_limit = false;
+                }
+                break;
 
             case ACTION_PS_FASTER:
-                if (pitch_mode_timestretch && stretch < STRETCH_MAX)
+                if (global_settings.pitch_mode_timestretch)
                 {
-                    stretch++;
-                    dsp_set_timestretch(stretch);
-                    speed = stretch * pitch;
+                    new_speed = speed + SPEED_BIG_DELTA;
+                    /* snap to whole numbers */
+                    if(new_speed % PITCH_SPEED_PRECISION != 0)
+                        new_speed -= new_speed % PITCH_SPEED_PRECISION;
+                    at_limit = false;
                 }
                 break;
 #endif
@@ -393,29 +724,53 @@
             case ACTION_PS_NUDGE_RIGHTOFF:
                 if (nudged)
                 {
-                    pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false);
+                    pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false
+#if CONFIG_CODEC == SWCODEC
+                                           , speed
+#endif                            
+                        );
+#if CONFIG_CODEC == SWCODEC
+                    speed = pitch;
+#endif
+                    semitone = get_semitone_from_pitch(pitch);
                     nudged = false;
                 }
                 break;
 
             case ACTION_PS_NUDGE_LEFT:
 #if CONFIG_CODEC == SWCODEC
-                if (!pitch_mode_timestretch)
+                if (!global_settings.pitch_mode_timestretch)
                 {
 #endif
-                    new_pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false);
+                    new_pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false
+#if CONFIG_CODEC == SWCODEC
+                                               , speed
+#endif                            
+                        );
                     nudged = (new_pitch != pitch);
                     pitch = new_pitch;
+                    semitone = get_semitone_from_pitch(pitch);
+#if CONFIG_CODEC == SWCODEC
+                    speed = pitch;
+#endif
                     break;
 #if CONFIG_CODEC == SWCODEC
                 }
+                else
+                {
+                    new_speed = speed - SPEED_SMALL_DELTA;
+                    at_limit = false;
+                }
+                break;
 
             case ACTION_PS_SLOWER:
-                if (pitch_mode_timestretch && stretch > STRETCH_MIN)
+                if (global_settings.pitch_mode_timestretch)
                 {
-                    stretch--;
-                    dsp_set_timestretch(stretch);
-                    speed = stretch * pitch;
+                    new_speed = speed - SPEED_BIG_DELTA;
+                    /* snap to whole numbers */
+                    if(new_speed % PITCH_SPEED_PRECISION != 0)
+                        new_speed += PITCH_SPEED_PRECISION - speed % PITCH_SPEED_PRECISION;
+                    at_limit = false;
                 }
                 break;
 #endif
@@ -423,27 +778,49 @@
             case ACTION_PS_NUDGE_LEFTOFF:
                 if (nudged)
                 {
-                    pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false);
+                    pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false
+#if CONFIG_CODEC == SWCODEC
+                                           , speed
+#endif                            
+                        );
+#if CONFIG_CODEC == SWCODEC
+                    speed = pitch;
+#endif
+                    semitone = get_semitone_from_pitch(pitch);
                     nudged = false;
                 }
                 break;
 
             case ACTION_PS_RESET:
-                pitch = 1000;
+                pitch = PITCH_SPEED_100;
                 sound_set_pitch(pitch);
 #if CONFIG_CODEC == SWCODEC
-                stretch = 100;
-                dsp_set_timestretch(stretch);
-                speed = stretch * pitch;
+                speed = PITCH_SPEED_100;
+                if (dsp_timestretch_available())
+                {
+                    dsp_set_timestretch(PITCH_SPEED_100);
+                    at_limit = false;
+                }
 #endif
+                semitone = get_semitone_from_pitch(pitch);
                 break;
 
             case ACTION_PS_TOGGLE_MODE:
+                global_settings.pitch_mode_semitone = !global_settings.pitch_mode_semitone;
 #if CONFIG_CODEC == SWCODEC
-                if (dsp_timestretch_available() && pitch_mode_semitone)
-                    pitch_mode_timestretch = !pitch_mode_timestretch;
+
+                if (dsp_timestretch_available() && !global_settings.pitch_mode_semitone)
+                {
+                    global_settings.pitch_mode_timestretch = !global_settings.pitch_mode_timestretch;
+                    if(!global_settings.pitch_mode_timestretch)
+                    {
+                        /* no longer in timestretch mode.  Reset speed */
+                        speed = pitch;
+                        dsp_set_timestretch(PITCH_SPEED_100);
+                    }
+                }
+                settings_save();
 #endif
-                pitch_mode_semitone = !pitch_mode_semitone;
                 break;
 
             case ACTION_PS_EXIT:
@@ -457,27 +834,73 @@
         }
         if (pitch_delta)
         {
-            if (pitch_mode_semitone)
-                pitch = pitch_increase_semitone(pitch, pitch_delta > 0);
-            else
-                pitch = pitch_increase(pitch, pitch_delta, true);
-#if CONFIG_CODEC == SWCODEC
-            if (pitch_mode_timestretch)
+            if (global_settings.pitch_mode_semitone)
             {
-                /* Set stretch to maintain speed  */
-                /* i.e. increase pitch, reduce stretch */
-                int new_stretch = speed / pitch;
-                if (new_stretch >= STRETCH_MIN && new_stretch <= STRETCH_MAX)
-                {
-                    stretch = new_stretch;
-                    dsp_set_timestretch(stretch);
-                }
+                semitone = pitch_increase_semitone(pitch, semitone, pitch_delta
+#if CONFIG_CODEC == SWCODEC
+                                                , speed
+#endif                            
+                );
+                pitch = get_pitch_from_semitone(semitone);
             }
             else
-                speed = stretch * pitch;
-#endif           
+            {
+                pitch = pitch_increase(pitch, pitch_delta, true
+#if CONFIG_CODEC == SWCODEC
+                                       , speed
+#endif                            
+                );
+                semitone = get_semitone_from_pitch(pitch);
+            }
+#if CONFIG_CODEC == SWCODEC
+            if (global_settings.pitch_mode_timestretch)
+            {
+                /* do this to make sure we properly obey the stretch limits */
+                new_speed = speed;
+            }
+            else
+            {
+                speed = pitch;
+            }
+#endif
         }
-    }
+
+#if CONFIG_CODEC == SWCODEC
+        if(new_speed)
+        {
+            new_stretch = GET_STRETCH(pitch, new_speed);
+
+            /* limit the amount of stretch */
+            if(new_stretch > STRETCH_MAX)
+            {
+                new_stretch = STRETCH_MAX;
+                new_speed = GET_SPEED(pitch, new_stretch);
+            }
+            else if(new_stretch < STRETCH_MIN)
+            {
+                new_stretch = STRETCH_MIN;
+                new_speed = GET_SPEED(pitch, new_stretch);
+            }
+
+            new_stretch = GET_STRETCH(pitch, new_speed);
+            if(new_stretch >= STRETCH_MAX || 
+               new_stretch <= STRETCH_MIN)
+            {
+                at_limit = true;
+            }
+
+            /* set the amount of stretch */
+            dsp_set_timestretch(new_stretch);
+
+            /* update the speed variable with the new speed */
+            speed = new_speed;
+
+            /* Reset new_speed so we only call dsp_set_timestretch */
+            /* when needed                                         */
+            new_speed = 0;
+        }
+#endif
+}
 #if CONFIG_CODEC == SWCODEC
     pcmbuf_set_low_latency(false);
 #endif
diff --git a/apps/gui/pitchscreen.h b/apps/gui/pitchscreen.h
index 41eb1fd..e421c65 100644
--- a/apps/gui/pitchscreen.h
+++ b/apps/gui/pitchscreen.h
@@ -22,6 +22,11 @@
 #ifndef _PITCHSCREEN_H_
 #define _PITCHSCREEN_H_
 
+/* precision of the pitch and speed variables */
+/* One zero per decimal (100 means two decimal places */
+#define PITCH_SPEED_PRECISION 100L
+#define PITCH_SPEED_100 (100L * PITCH_SPEED_PRECISION)  /* 100% speed */
+
 int gui_syncpitchscreen_run(void);
 
 #endif /* _PITCHSCREEN_H_ */
diff --git a/apps/lang/english.lang b/apps/lang/english.lang
index 7d1e242..68a562f 100644
--- a/apps/lang/english.lang
+++ b/apps/lang/english.lang
@@ -12604,3 +12604,45 @@
     remote: "Remote Statusbar"
   </voice>
 </phrase>
+<phrase>
+  id: LANG_SEMITONE
+  desc: 
+  user: core
+  <source>
+    *: "Semitone"
+  </source>
+  <dest>
+    *: "Semitone"
+  </dest>
+  <voice>
+    *: "Semitone"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_STRETCH_LIMIT
+  desc: "limit" in pitch screen
+  user: core
+  <source>
+    *: "Limit"
+  </source>
+  <dest>
+    *: "Limit"
+  </dest>
+  <voice>
+    *: "Limit"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_PLAYBACK_RATE
+  desc: "rate" in pitch screen
+  user: core
+  <source>
+    *: "Rate"
+  </source>
+  <dest>
+    *: "Rate"
+  </dest>
+  <voice>
+    *: "Rate"
+  </voice>
+</phrase>
diff --git a/apps/plugin.h b/apps/plugin.h
index d1a5712..bb74d73 100644
--- a/apps/plugin.h
+++ b/apps/plugin.h
@@ -128,12 +128,12 @@
 #define PLUGIN_MAGIC 0x526F634B /* RocK */
 
 /* increase this every time the api struct changes */
-#define PLUGIN_API_VERSION 159
+#define PLUGIN_API_VERSION 160
 
 /* update this to latest version if a change to the api struct breaks
    backwards compatibility (and please take the opportunity to sort in any
    new function which are "waiting" at the end of the function table) */
-#define PLUGIN_MIN_API_VERSION 159
+#define PLUGIN_MIN_API_VERSION 160
 
 /* plugin return codes */
 enum plugin_status {
@@ -618,7 +618,7 @@
 #endif
 #if (CONFIG_CODEC == MAS3587F) || (CONFIG_CODEC == MAS3539F) || \
     (CONFIG_CODEC == SWCODEC)
-    void (*sound_set_pitch)(int pitch);
+    void (*sound_set_pitch)(int32_t pitch);
 #endif
 
     /* MAS communication */
diff --git a/apps/settings.h b/apps/settings.h
index 93a998e..8cf9bcf 100644
--- a/apps/settings.h
+++ b/apps/settings.h
@@ -739,6 +739,11 @@
     struct touchscreen_parameter ts_calibration_data;
 #endif
 
+    /* pitch screen settings */
+    bool pitch_mode_semitone;
+#if CONFIG_CODEC == SWCODEC
+    bool pitch_mode_timestretch;
+#endif
     /* If values are just added to the end, no need to bump plugin API
        version. */
     /* new stuff to be added at the end */
diff --git a/apps/settings_list.c b/apps/settings_list.c
index 9cfd9aa..78d1fc8 100644
--- a/apps/settings_list.c
+++ b/apps/settings_list.c
@@ -33,7 +33,6 @@
 #include "settings_list.h"
 #include "sound.h"
 #include "dsp.h"
-#include "debug.h"
 #include "mpeg.h"
 #include "audio.h"
 #include "power.h"
@@ -1528,6 +1527,14 @@
                     tsc_is_changed, tsc_set_default),
 #endif
     OFFON_SETTING(0, prevent_skip, LANG_PREVENT_SKIPPING, false, "prevent track skip", NULL),
+
+    OFFON_SETTING(0, pitch_mode_semitone, LANG_SEMITONE, false, 
+                  "Semitone pitch change", NULL),
+#if CONFIG_CODEC == SWCODEC
+    OFFON_SETTING(0, pitch_mode_timestretch, LANG_TIMESTRETCH, false, 
+                  "Timestretch mode", NULL),
+#endif
+
 };
 
 const int nb_settings = sizeof(settings)/sizeof(*settings);
diff --git a/apps/tdspeed.c b/apps/tdspeed.c
index 07f8beb..cd01099 100644
--- a/apps/tdspeed.c
+++ b/apps/tdspeed.c
@@ -25,7 +25,6 @@
 #include <stdio.h>
 #include <string.h>
 #include "buffer.h"
-#include "debug.h"
 #include "system.h"
 #include "tdspeed.h"
 #include "settings.h"
@@ -72,7 +71,7 @@
 }
 
 
-bool tdspeed_config(int samplerate, bool stereo, int factor)
+bool tdspeed_config(int samplerate, bool stereo, int32_t factor)
 {
     struct tdspeed_state_s *st = &tdspeed_state;
     int src_frame_sz;
@@ -84,7 +83,7 @@
         return false;
 
     /* Check parameters */
-    if (factor == 100)
+    if (factor == PITCH_SPEED_100)
         return false;
     if (samplerate < MIN_RATE || samplerate > MAX_RATE)
         return false;
@@ -94,14 +93,14 @@
     st->stereo = stereo;
     st->dst_step = samplerate / MINFREQ;
 
-    if (factor > 100)
-        st->dst_step = st->dst_step * 100 / factor;
+    if (factor > PITCH_SPEED_100)
+        st->dst_step = st->dst_step * PITCH_SPEED_100 / factor;
     st->dst_order = 1;
 
     while (st->dst_step >>= 1)
         st->dst_order++;
     st->dst_step = (1 << st->dst_order);
-    st->src_step = st->dst_step * factor / 100;
+    st->src_step = st->dst_step * factor / PITCH_SPEED_100;
     st->shift_max = (st->dst_step > st->src_step) ? st->dst_step : st->src_step;
 
     src_frame_sz = st->shift_max + st->dst_step;
diff --git a/apps/tdspeed.h b/apps/tdspeed.h
index 1a3df12..2fd9498 100644
--- a/apps/tdspeed.h
+++ b/apps/tdspeed.h
@@ -23,15 +23,28 @@
 #ifndef _TDSPEED_H
 #define _TDSPEED_H
 
+#include "dsp.h"
+/* for the precision #defines: */
+#include "pitchscreen.h"
+
 #define TDSPEED_OUTBUFSIZE 4096
 
+/* some #define functions to get the pitch, stretch and speed values based on */
+/* two known values.  Remember that params are alphabetical.                  */
+#define GET_SPEED(pitch, stretch) \
+    ((pitch * stretch + PITCH_SPEED_100 / 2L) / PITCH_SPEED_100)
+#define GET_PITCH(speed, stretch) \
+    ((speed * PITCH_SPEED_100 + stretch / 2L) / stretch)
+#define GET_STRETCH(pitch, speed) \
+    ((speed * PITCH_SPEED_100 + pitch   / 2L) / pitch)
+
 void tdspeed_init(void);
-bool tdspeed_config(int samplerate, bool stereo, int factor);
+bool tdspeed_config(int samplerate, bool stereo, int32_t factor);
 long tdspeed_est_output_size(void);
 long tdspeed_est_input_size(long size);
 int tdspeed_doit(int32_t *src[], int count);
 
-#define STRETCH_MAX 250
-#define STRETCH_MIN 35
+#define STRETCH_MAX (250L * PITCH_SPEED_PRECISION) /* 250% */
+#define STRETCH_MIN (35L  * PITCH_SPEED_PRECISION) /* 35%  */
 
 #endif
diff --git a/docs/CREDITS b/docs/CREDITS
index cc626f7..32e7ea4 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -479,6 +479,7 @@
 Hilton Shumway
 Matthew Bonnett
 Nick Tryon
+David Johnston
 
 The libmad team
 The wavpack team
diff --git a/firmware/export/sound.h b/firmware/export/sound.h
index 70c4a22..674b2f6 100644
--- a/firmware/export/sound.h
+++ b/firmware/export/sound.h
@@ -60,8 +60,8 @@
 int sound_val2phys(int setting, int value);
 
 #if (CONFIG_CODEC == MAS3587F) || (CONFIG_CODEC == MAS3539F)
-void sound_set_pitch(int permille);
-int sound_get_pitch(void);
+void sound_set_pitch(int32_t pitch);
+int32_t sound_get_pitch(void);
 #endif
 
 #endif
diff --git a/firmware/sound.c b/firmware/sound.c
index f4a2f87..6a2f03d 100644
--- a/firmware/sound.c
+++ b/firmware/sound.c
@@ -25,6 +25,8 @@
 #include "sound.h"
 #include "logf.h"
 #include "system.h"
+/* for the pitch and speed precision #defines: */
+#include "pitchscreen.h"
 #ifndef SIMULATOR
 #include "i2c.h"
 #include "mas.h"
@@ -159,6 +161,7 @@
 
 #if CONFIG_CODEC == SWCODEC
 /* Copied from dsp.h, nasty nasty, but we don't want to include dsp.h */
+
 enum {
     DSP_CALLBACK_SET_PRESCALE = 0,
     DSP_CALLBACK_SET_BASS,
@@ -698,18 +701,18 @@
    crystal frequency than we actually have. It will adjust its internal
    parameters and the result is that the audio is played at another pitch.
 
-   The pitch value is in tenths of percent.
+   The pitch value precision is based on PITCH_SPEED_PRECISION (in dsp.h)
 */
-static int last_pitch = 1000;
+static int last_pitch = PITCH_SPEED_100;
 
-void sound_set_pitch(int pitch)
+void sound_set_pitch(int32_t pitch)
 {
     unsigned long val;
 
     if (pitch != last_pitch)
     {
         /* Calculate the new (bogus) frequency */
-        val = 18432 * 1000 / pitch;
+        val = 18432 * PITCH_SPEED_100 / pitch;
 
         mas_writemem(MAS_BANK_D0, MAS_D0_OFREQ_CONTROL, &val, 1);
 
@@ -721,19 +724,19 @@
     }
 }
 
-int sound_get_pitch(void)
+int32_t sound_get_pitch(void)
 {
     return last_pitch;
 }
 #else /* SIMULATOR */
-void sound_set_pitch(int pitch)
+void sound_set_pitch(int32_t pitch)
 {
     (void)pitch;
 }
 
-int sound_get_pitch(void)
+int32_t sound_get_pitch(void)
 {
-    return 1000;
+    return PITCH_SPEED_100;
 }
 #endif /* SIMULATOR */
 #endif /* (CONFIG_CODEC == MAS3587F) || (CONFIG_CODEC == MAS3539F) */
diff --git a/manual/rockbox_interface/wps.tex b/manual/rockbox_interface/wps.tex
index 2d637d6..de47f97 100644
--- a/manual/rockbox_interface/wps.tex
+++ b/manual/rockbox_interface/wps.tex
@@ -273,34 +273,52 @@
 \nopt{player}{
   \subsubsection{\label{sec:pitchscreen}Pitch}
   
-  The \setting{Pitch Screen} allows you to change the pitch and the playback 
-  speed of your \dap. The pitch value can be adjusted between 50\% and 200\%.
-  50\% means half the normal playback speed and the pitch that is an octave lower 
-  than the normal pitch. 200\% means double playback speed and the pitch that 
-  is an octave higher than the normal pitch.  
+  The \setting{Pitch Screen} allows you to change the rate of playback
+  (i.e. the playback speed and at the same time the pitch) of your
+  \dap. The rate value can be adjusted between 50\% and 200\%. 50\%
+  means half the normal playback speed and a pitch that is an octave
+  lower than the normal pitch. 200\% means double playback speed and a
+  pitch that is an octave higher than the normal pitch.
+
+  The rate can be changed in two modes: procentual and semitone.
+  Initially, procentual mode is active.
+  
+  \opt{swcodec}{
+    If you've enabled the \setting{Timestretch} option in
+    \setting{Sound Settings} and have since rebooted, you can also use
+    timestretch mode. This allows you to change the playback speed
+    without affecting the pitch, and vice versa.
+    
+    In timestretch mode there are separate displays for pitch and
+    speed, and each can be altered independently.  Due to the
+    limitations of the algorithm, speed is limited to be between 35\%
+    and 250\% of the current pitch value.  Pitch must maintain the
+    same ratio as well as remain between 50\% and 200\%.
+  }
+  
+  The value of the \opt{swcodec}{rate, pitch and speed}\nopt{swcodec}{rate}
+  is not persisted, i.e. after the \dap\ is turned on it will
+  always be set to 100\%.
 
   \opt{masf}{
-    Changing the pitch can be done in two modes: procentual and semitone.
-    Initially (after the \dap{} is switched on), procentual mode is active.
-
     \begin{table}
       \begin{btnmap}{}{}
         \ActionPsToggleMode
         & Toggle pitch changing mode \\
         %
         \ActionPsIncSmall{} / \ActionPsDecSmall
-        & Increase / Decrease pitch by 0.1\% (in procentual mode) or a semitone
-          (in semitone mode)\\
+        & Increase / Decrease pitch by 0.1\% (in procentual mode) or by 0.1
+          semitone (in semitone mode)\\
         %
         \ActionPsIncBig{} / \ActionPsDecBig
         & Increase / Decrease pitch by 1\% (in procentual mode) or a semitone
           (in semitone mode)\\
         %
         \ActionPsNudgeLeft{} / \ActionPsNudgeRight
-        & Temporarily change pitch by 2.0\% (beatmatch) \\
+        & Temporarily change pitch by 2\% (beatmatch) \\
         %
         \ActionPsReset
-        & Reset pitch to 100\% \\
+        & Reset rate to 100\% \\
         %
         \ActionPsExit
         & Leave the Pitch Screen \\
@@ -312,23 +330,16 @@
   }
 
   \opt{swcodec}{
-    Changing the pitch can be done in three modes: procentual, semitone and 
-    timestretch. Initially (after the \dap{} is switched on), procentual mode is active.
-
-    Timestretch mode allows you to change the playback speed of your recording without 
-    affecting the pitch, and vice versa.  To access this you must enable the \setting{Timestretch} 
-    option in \setting{Sound Settings} and reboot.
-  
     \begin{table}
       \begin{btnmap}{}{}
         \ActionPsToggleMode
         \opt{HAVEREMOTEKEYMAP}{& \ActionRCPsToggleMode}
-        & Toggle pitch changing mode \\
+        & Toggle pitch changing mode (cycles through all available modes)\\
         %
         \ActionPsIncSmall{} / \ActionPsDecSmall
         \opt{HAVEREMOTEKEYMAP}{& \ActionRCPsIncSmall{} / \ActionRCPsDecSmall}
-        & Increase / Decrease pitch by 0.1\% (in procentual mode) or a semitone
-          (in semitone mode)\\
+        & Increase / Decrease pitch by 0.1\% (in procentual mode) or 0.1
+          semitone (in semitone mode)\\
         %
         \ActionPsIncBig{} / \ActionPsDecBig
         \opt{HAVEREMOTEKEYMAP}{& \ActionRCPsIncBig{} / \ActionRCPsDecBig}
@@ -337,7 +348,7 @@
         %
         \ActionPsNudgeLeft{} / \ActionPsNudgeRight
         \opt{HAVEREMOTEKEYMAP}{& \ActionRCPsNudgeLeft{} / \ActionPsNudgeRight}
-        & Temporarily change pitch by 2.0\% (beatmatch), or modify speed (in timestretch mode) \\
+        & Temporarily change pitch by 2\% (beatmatch), or modify speed (in timestretch mode) \\
         %
         \ActionPsReset
         \opt{HAVEREMOTEKEYMAP}{& \ActionRCPsReset}