From 798846af1d72e70a98566e6acfc4ea64b3523d80 Mon Sep 17 00:00:00 2001 From: alle Date: Sat, 11 Jul 2009 16:46:19 +0000 Subject: [PATCH] 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 --- apps/dsp.c | 18 +- apps/dsp.h | 8 +- apps/gui/pitchscreen.c | 681 +++++++++++++++++++++++++++++++-------- apps/gui/pitchscreen.h | 5 + apps/lang/english.lang | 42 +++ apps/plugin.h | 6 +- apps/settings.h | 5 + apps/settings_list.c | 9 +- apps/tdspeed.c | 11 +- apps/tdspeed.h | 19 +- docs/CREDITS | 1 + firmware/export/sound.h | 4 +- firmware/sound.c | 19 +- manual/rockbox_interface/wps.tex | 57 ++-- 14 files changed, 697 insertions(+), 188 deletions(-) diff --git a/apps/dsp.c b/apps/dsp.c index 30b4ed357..ec5941762 100644 --- a/apps/dsp.c +++ b/apps/dsp.c @@ -162,7 +162,7 @@ struct dsp_config 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 @@ static int treble; /* A/V */ #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 @@ static inline int32_t clip_sample_16(int32_t sample) 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 @@ static void tdspeed_setup(struct dsp_config *dspc) 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_timestretch_enable(bool enabled) } } -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 @@ intptr_t dsp_configure(struct dsp_config *dsp, int setting, intptr_t value) 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 3d24b2424..7d1e2b3dd 100644 --- a/apps/dsp.h +++ b/apps/dsp.h @@ -83,10 +83,10 @@ void dsp_set_eq_coefs(int band); 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 16fac0c3b..a699d4a7b 100644 --- a/apps/gui/pitchscreen.c +++ b/apps/gui/pitchscreen.c @@ -22,6 +22,7 @@ #include #include #include +#include #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 @@ enum 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_icons(struct screen *display, 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 @@ static void pitchscreen_draw(struct screen *display, int max_lines, { /* 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 @@ static void pitchscreen_draw(struct screen *display, int max_lines, /* 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 @@ static void pitchscreen_draw(struct screen *display, int max_lines, if ((show_lang_pitch = (max_lines >= 3))) { #if CONFIG_CODEC == SWCODEC - if (!pitch_mode_timestretch) + if(global_settings.pitch_mode_timestretch) { -#endif - /* LANG_PITCH */ - snprintf(buf, sizeof(buf), "%s", str(LANG_PITCH)); -#if CONFIG_CODEC == SWCODEC + /* Pitch:XXX.X% */ + 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 { - /* Pitch:XXX.X% */ - snprintf(buf, sizeof(buf), "%s:%d.%d%%", str(LANG_PITCH), - pitch / 10, pitch % 10); + /* Rate */ + snprintf(buf, sizeof(buf), "%s:", str(LANG_PLAYBACK_RATE)); } -#endif 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 +#endif { - /* "Speed:XXX%" */ - snprintf(buf, sizeof(buf), "%s:%d%%", str(LANG_SPEED), - speed / 1000); + 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)); + } } -#endif + 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 @@ static void pitchscreen_draw(struct screen *display, int max_lines, 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 @@ static int pitch_increase(int pitch, int pitch_delta, bool allow_cutoff) /* 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]) + { + semitone++; + } + + + /* now find the fractional part */ + while(pitch > (cent_interp[fractional_index + 1] * + semitone_table[semitone] / PITCH_SPEED_100)) { - tmp = tmp * PITCH_SEMITONE_FACTOR; - round_fct = PITCH_K_FCT; + /* 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)) + { + new_semitone = SEMITONE_START * PITCH_SPEED_PRECISION; + at_limit = true; + } + else if(new_semitone > (SEMITONE_END * PITCH_SPEED_PRECISION)) { - tmp = (tmp * PITCH_KN_FCT) / PITCH_SEMITONE_FACTOR; - round_fct = PITCH_N_FCT; + new_semitone = SEMITONE_END * PITCH_SPEED_PRECISION; + at_limit = true; } - /* Scaling down with rounding */ - tmp = (tmp + round_fct / 2) / round_fct; - return pitch_increase(pitch, tmp - pitch, false); + + 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 @@ static int pitch_increase_semitone(int pitch, bool up) 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 @@ int gui_syncpitchscreen_run(void) 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 @@ int gui_syncpitchscreen_run(void) { 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 @@ int gui_syncpitchscreen_run(void) 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 @@ int gui_syncpitchscreen_run(void) 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 @@ int gui_syncpitchscreen_run(void) } if (pitch_delta) { - if (pitch_mode_semitone) - pitch = pitch_increase_semitone(pitch, pitch_delta > 0); + if (global_settings.pitch_mode_semitone) + { + semitone = pitch_increase_semitone(pitch, semitone, pitch_delta +#if CONFIG_CODEC == SWCODEC + , speed +#endif + ); + pitch = get_pitch_from_semitone(semitone); + } else - pitch = pitch_increase(pitch, pitch_delta, true); + { + pitch = pitch_increase(pitch, pitch_delta, true #if CONFIG_CODEC == SWCODEC - if (pitch_mode_timestretch) + , speed +#endif + ); + semitone = get_semitone_from_pitch(pitch); + } +#if CONFIG_CODEC == SWCODEC + if (global_settings.pitch_mode_timestretch) { - /* 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); - } + /* do this to make sure we properly obey the stretch limits */ + new_speed = speed; } else - speed = stretch * pitch; -#endif + { + 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 41eb1fd41..e421c6559 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 7d1e242c6..68a562f00 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -12604,3 +12604,45 @@ remote: "Remote Statusbar" + + id: LANG_SEMITONE + desc: + user: core + + *: "Semitone" + + + *: "Semitone" + + + *: "Semitone" + + + + id: LANG_STRETCH_LIMIT + desc: "limit" in pitch screen + user: core + + *: "Limit" + + + *: "Limit" + + + *: "Limit" + + + + id: LANG_PLAYBACK_RATE + desc: "rate" in pitch screen + user: core + + *: "Rate" + + + *: "Rate" + + + *: "Rate" + + diff --git a/apps/plugin.h b/apps/plugin.h index d1a57129a..bb74d7333 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -128,12 +128,12 @@ void* plugin_get_buffer(size_t *buffer_size); #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 @@ struct plugin_api { #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 93a998ea1..8cf9bcffd 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -739,6 +739,11 @@ struct user_settings 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 9cfd9aafc..78d1fc870 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 @@ const struct settings_list settings[] = { 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 07f8beb13..cd01099a7 100644 --- a/apps/tdspeed.c +++ b/apps/tdspeed.c @@ -25,7 +25,6 @@ #include #include #include "buffer.h" -#include "debug.h" #include "system.h" #include "tdspeed.h" #include "settings.h" @@ -72,7 +71,7 @@ void tdspeed_init() } -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 @@ bool tdspeed_config(int samplerate, bool stereo, int factor) 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 @@ bool tdspeed_config(int samplerate, bool stereo, int factor) 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 1a3df126f..2fd949844 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 cc626f74e..32e7ea4df 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -479,6 +479,7 @@ Andre Lupa 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 70c4a2244..674b2f6ae 100644 --- a/firmware/export/sound.h +++ b/firmware/export/sound.h @@ -60,8 +60,8 @@ void sound_set(int setting, int value); 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 f4a2f87ca..6a2f03df0 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 @@ sound_set_type* sound_get_fn(int setting) #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 @@ int sound_val2phys(int setting, int value) 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 @@ void sound_set_pitch(int pitch) } } -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 2d637d683..de47f97eb 100644 --- a/manual/rockbox_interface/wps.tex +++ b/manual/rockbox_interface/wps.tex @@ -273,34 +273,52 @@ Delete the currently playing file. \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. - \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. + 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}{ \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 @@ Delete the currently playing file. } \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 @@ Delete the currently playing file. % \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} -- 2.11.4.GIT